From 4a826060ea22239e364ebc677a878dd6ef916c23 Mon Sep 17 00:00:00 2001 From: Jaechul Lee Date: Fri, 12 Mar 2021 14:31:25 +0900 Subject: [PATCH] Add Acoustic Echo Cancellation services Added AEC services [Version] 0.13.7 [Issue Type] New feature Change-Id: I4ac638a0d61dae635efecd5e198bf2d2fec28557 Signed-off-by: Jaechul Lee --- Makefile.am | 5 +- aec/Makefile.am | 10 + aec/algo_speex.c | 157 ++++++ aec/algo_speex.h | 23 + aec/loopback.c | 912 +++++++++++++++++++++++++++++++++++ aec/loopback.h | 49 ++ aec/main.c | 522 ++++++++++++++++++++ aec/static_queue.c | 313 ++++++++++++ aec/static_queue.h | 44 ++ configure.ac | 26 + packaging/audio-aec.conf | 19 + packaging/audio-aec.service | 14 + packaging/libmm-sound.spec | 32 +- packaging/org.tizen.AudioAec.service | 4 + 14 files changed, 2128 insertions(+), 2 deletions(-) create mode 100644 aec/Makefile.am create mode 100644 aec/algo_speex.c create mode 100644 aec/algo_speex.h create mode 100644 aec/loopback.c create mode 100644 aec/loopback.h create mode 100644 aec/main.c create mode 100644 aec/static_queue.c create mode 100644 aec/static_queue.h create mode 100644 packaging/audio-aec.conf create mode 100644 packaging/audio-aec.service create mode 100644 packaging/org.tizen.AudioAec.service diff --git a/Makefile.am b/Makefile.am index 1e21cee..88a8d74 100644 --- a/Makefile.am +++ b/Makefile.am @@ -2,9 +2,12 @@ SUBDIRS = common \ pkgconfig \ . \ testsuite - SUBDIRS += focus_server +if ENABLE_AEC +SUBDIRS += aec +endif + if UNITTESTS_ENABLED SUBDIRS += unittest endif diff --git a/aec/Makefile.am b/aec/Makefile.am new file mode 100644 index 0000000..a0748a7 --- /dev/null +++ b/aec/Makefile.am @@ -0,0 +1,10 @@ +bin_PROGRAMS = audio_aec + +audio_aec_SOURCES = loopback.c static_queue.c algo_speex.c main.c + +audio_aec_CFLAGS = -I$(srcdir) $(ALSA_CFLAGS) $(SPEEX_CFLAGS) $(DBUS_CFLAGS) $(DLOG_CFLAGS) $(GLIB2_CFLAGS) $(GIO_CFLAGS) +audio_aec_CFLAGS += -fPIC -pie +audio_aec_CFLAGS += -DLOG_TAG=\"AUDIO_AEC\" + +audio_aec_LDADD = $(ALSA_LIBS) $(SPEEX_LIBS) $(DBUS_LIBS) $(DLOG_LIBS) $(GLIB2_LIBS) $(GIO_LIBS) -lpthread + diff --git a/aec/algo_speex.c b/aec/algo_speex.c new file mode 100644 index 0000000..60e762d --- /dev/null +++ b/aec/algo_speex.c @@ -0,0 +1,157 @@ +/* +* Copyright (c) 2021 Samsung Electronics Co., Ltd All Rights Reserved +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#include +#include +#include +#include +#include "algo_speex.h" + +//#define __DEBUG__ + +#ifdef __DEBUG__ +#include +#include +#include +#include +#include +static int fdrec, fdref, fdout; +#endif + +static int speex_init(int nframes, int rate, int channels); +static int speex_process(unsigned char *rec, unsigned char *ref, unsigned char *out); +static int speex_deinit(); + +static struct ec_speex { + SpeexEchoState *echo_state; + SpeexPreprocessState *preprocess; + int nframes; + int filter_frames; + int channels; + int rate; +} sp; + +static ec_operation_t op = { + .init = speex_init, + .process = speex_process, + .deinit = speex_deinit, +}; + +ec_operation_t *get_speex_instance() +{ + return &op; +} + +static int speex_init(int nframes, int rate, int channels) +{ + sp.nframes = nframes; + sp.filter_frames = nframes; /* TODO:It takes much time when 10 times of nframe */ + sp.channels = channels; + sp.rate = rate; + + LOGI("nframes(%d), filter_frames(%d) rate(%d), channels(%d)", + sp.nframes, sp.filter_frames, sp.rate, sp.channels); + + sp.echo_state = speex_echo_state_init_mc(sp.nframes, sp.filter_frames, + sp.channels, sp.channels); + if (!sp.echo_state) { + LOGE("speex_echo_state_init_mc failed\n"); + return -1; + } + + sp.preprocess = speex_preprocess_state_init(sp.nframes, sp.rate); + if (!sp.preprocess) { + LOGE("speex_echo_state_init_mc failed\n"); + return -1; + } + + if (speex_echo_ctl(sp.echo_state, SPEEX_ECHO_SET_SAMPLING_RATE, &sp.rate)) { + LOGE("speex_echo_ctl SET_SAMPLING_RATE failed\n"); + return -1; + } + + if (speex_preprocess_ctl(sp.preprocess, SPEEX_PREPROCESS_SET_ECHO_STATE, sp.echo_state)) { + LOGE("speex_echo_ctl SET_ECHO_STATE failed\n"); + return -1; + } + +#ifdef __DEBUG__ + unlink("/tmp/rec.raw"); + unlink("/tmp/ref.raw"); + unlink("/tmp/out.raw"); + + fdrec = open("/tmp/rec.raw", O_RDWR | O_CREAT | O_TRUNC, 777); + fdref = open("/tmp/ref.raw", O_RDWR | O_CREAT | O_TRUNC, 777); + fdout = open("/tmp/out.raw", O_RDWR | O_CREAT | O_TRUNC, 777); +#endif + + return 0; +} + +static int speex_process(unsigned char *rec, unsigned char *ref, unsigned char *out) +{ +#ifdef __DEBUG__ + struct timeval before,after; + write(fdrec, rec, sp.nframes * 4); + write(fdref, ref, sp.nframes * 4); + gettimeofday(&before,NULL); +#endif + + speex_echo_cancellation(sp.echo_state, + (const spx_int16_t *)rec, + (const spx_int16_t *)ref, + (spx_int16_t *)out); + speex_preprocess_run(sp.preprocess, (spx_int16_t *)out); + + rec += sp.nframes * sp.channels; + ref += sp.nframes * sp.channels; + out += sp.nframes * sp.channels; + + speex_echo_cancellation(sp.echo_state, + (const spx_int16_t *)rec, + (const spx_int16_t *)ref, + (spx_int16_t *)out); + speex_preprocess_run(sp.preprocess, (spx_int16_t *)out); + +#ifdef __DEBUG__ + gettimeofday(&after,NULL); + LOGE("It takes time(%ld)", + 1000*(after.tv_sec-before.tv_sec) + (after.tv_usec-before.tv_usec)/1000); + write(fdout, out, sp.nframes * 4); +#endif + + return 0; +} + +static int speex_deinit() +{ + if (sp.echo_state) + speex_echo_state_destroy(sp.echo_state); + if (sp.preprocess) + speex_preprocess_state_destroy(sp.preprocess); + +#ifdef __DEBUG__ + if (fdrec) + close(fdrec); + if (fdref) + close(fdref); + if (fdout) + close(fdout); +#endif + + return 0; +} + diff --git a/aec/algo_speex.h b/aec/algo_speex.h new file mode 100644 index 0000000..20c846f --- /dev/null +++ b/aec/algo_speex.h @@ -0,0 +1,23 @@ +/* +* Copyright (c) 2021 Samsung Electronics Co., Ltd All Rights Reserved +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +#ifndef __ALGO_SPEEX__ +#define __ALGO_SPEEX__ + +#include "loopback.h" + +ec_operation_t *get_speex_instance(); + +#endif // __ALGO_SPEEX__ diff --git a/aec/loopback.c b/aec/loopback.c new file mode 100644 index 0000000..56e87f7 --- /dev/null +++ b/aec/loopback.c @@ -0,0 +1,912 @@ +/* +* Copyright (c) 2021 Samsung Electronics Co., Ltd All Rights Reserved +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#include +#include +#include +#include +#include +#include +#include + +#include "loopback.h" +#include "static_queue.h" + +#define FIXME_FORMAT_SIZE 2 +#define CTL_LOOPBACK_DEVICE "hw:Loopback" +#define PCM_MASTER_ACTIVE "PCM Master Active" +#define PCM_SLAVE_ACTIVE "PCM Slave Active" + +enum { + CAPTURE = 0, + PLAYBACK, + DEV_MAX +}; + +enum { + POLL_WAKEUP = 0, + POLL_CTRL, + POLL_CAPTURE, + POLL_PLAYBACK, + POLL_MAX +}; + +enum { + STATE_STOPPED, + STATE_RUNNING, +}; + +struct loopback_dev { + snd_pcm_t *handle; + char *name; + int mode; + int rate; + int channels; + int format; + unsigned int buffer_size; + unsigned int buffer_time; + unsigned int period_size; + unsigned int buffer_frame; + unsigned int period_time; +}; + +struct loopback { + char *name; + struct loopback_dev *dev[DEV_MAX]; + struct pollfd pfds[POLL_MAX]; + int poll_cnt; + int wakeup[2]; + pthread_t t; + volatile int state; + volatile bool quit; + + char *empty_buf; + int ec_frame; + int rate; + int channels; + + /* loopback ctl */ + snd_ctl_t *ctl; + int ctl_id; + int ctl_subid; + snd_ctl_elem_value_t *elm_active; + + /* related to reference */ + ec_operation_t *op; + + sq *q; + + /* reference queue */ + loopback_t *master; + sq *refq; /* recording consumption */ + sq *master_refq; /* playback feeding */ + + int logcnt; +}; + +static int __open_loopback_controls(loopback_t *loopback, const char *dev, const char *elm); +static int __check_ctl_active_event(loopback_t *loopback, int *status); +static int __handle_revents(loopback_t *loopback, int mode, unsigned short revents); +static void *__loop_thread(void *userdata); + +static void __close_dev(loopback_t *loopback); +static int __open_dev(loopback_t *loopback, int index, struct pollfd *pfd); +static int __xrun_recovery(loopback_dev_t *dev, int err); +static int __print_buffer(loopback_t *loopback); + +loopback_dev_t *loopback_alloc_device(const char *name, int mode, int rate, + int channels, int format, int buffer_frame) +{ + loopback_dev_t *dev = (loopback_dev_t *)calloc(1, sizeof(struct loopback_dev)); + if (!dev) { + LOGE("Failed to alloc memory"); + return NULL; + } + + dev->name = strdup(name); + dev->mode = mode; + dev->rate = rate; + dev->channels = channels; + dev->format = format; + dev->buffer_frame = buffer_frame; + + return dev; +} + +int loopback_free_device(loopback_dev_t *dev) +{ + if (!dev) + return -1; + + if (dev->name) { + free(dev->name); + dev->name = NULL; + } + + return 0; +} + +static int __open_loopback_controls(loopback_t *loopback, const char *dev, const char *elm) +{ + int k, err; + char ctl_name[64]; + const char *elm_name = elm; + + loopback->ctl_id = dev[12] - '0'; + loopback->ctl_subid = dev[14] - '0'; + + for (k = 0; *dev != ',' && *dev != '\0'; dev++) + ctl_name[k++] = *dev; + ctl_name[k] = '\0'; + + err = snd_ctl_open(&loopback->ctl, ctl_name, SND_CTL_NONBLOCK); + if (err < 0) { + LOGE("Failed to open capture control. name(%s)", ctl_name); + goto fail; + } + + err = snd_ctl_subscribe_events(loopback->ctl, 1); + if (err < 0) { + LOGE("Failed to subscribe ctrl event"); + goto fail; + } + + if (snd_ctl_elem_value_malloc(&loopback->elm_active) < 0) { + LOGE("Failed to alloc elem"); + goto fail; + } + + snd_ctl_elem_value_set_interface(loopback->elm_active, SND_CTL_ELEM_IFACE_PCM); + snd_ctl_elem_value_set_device(loopback->elm_active, loopback->ctl_id); + snd_ctl_elem_value_set_subdevice(loopback->elm_active, loopback->ctl_subid); + snd_ctl_elem_value_set_name(loopback->elm_active, elm_name); + + LOGD("loopback ctrl opened. name(%s) id(%d) subid(%d) elm_name(%s)", + ctl_name, loopback->ctl_id, loopback->ctl_subid, elm_name); + + return 0; + +fail: + if (loopback->ctl) + snd_ctl_close(loopback->ctl); + if (loopback->elm_active) + snd_ctl_elem_value_free(loopback->elm_active); + + return -1; +} + +loopback_t *loopback_create(const char *name, + loopback_dev_t *capture, + loopback_dev_t *playback, + int process_frame, int nbuffer) +{ + const char *elm_name; + char *dev_loopback; + loopback_t *loopback; + int bytes, len; + + if (capture->rate != playback->rate || + capture->channels != playback->channels || + capture->format != playback->format) { + /* TODO: support resample or remap */ + LOGE("capture and playback info is not matched." + "rate(%d:%d), channels(%d:%d), format(%d:%d)", + capture->rate, playback->rate, + capture->channels, playback->channels, + capture->format, playback->format); + return NULL; + } + + len = strlen(CTL_LOOPBACK_DEVICE); + if (!strncmp(capture->name, CTL_LOOPBACK_DEVICE, len)) + dev_loopback = capture->name; + else if (!strncmp(playback->name, CTL_LOOPBACK_DEVICE, len)) + dev_loopback = playback->name; + else { + LOGE("not support"); + return NULL; + } + + loopback = (loopback_t *)calloc(1, sizeof(struct loopback)); + if (!loopback) { + LOGE("Failed to alloc memory"); + return NULL; + } + loopback->name = name ? strdup(name) : strdup("noname"); + loopback->dev[CAPTURE] = capture; + loopback->dev[PLAYBACK] = playback; + loopback->state = STATE_STOPPED; + + /* TODO: Support resample */ + loopback->channels = capture->channels; + loopback->rate = capture->rate; + + elm_name = dev_loopback == capture->name ? PCM_SLAVE_ACTIVE : PCM_MASTER_ACTIVE; + if (__open_loopback_controls(loopback, dev_loopback, elm_name)) { + LOGE("Failed to open loopback control. name(%s)", elm_name); + goto exit; + } + + loopback->ec_frame = process_frame; + bytes = loopback->ec_frame * capture->channels * FIXME_FORMAT_SIZE; + + // TEMP: padding for speex + bytes += bytes >> 1; + + loopback->q = create_static_queue(bytes, nbuffer); + if (!loopback->q) { + LOGE("Failed to create sq"); + goto exit; + } + LOGI("created static-queue bytes(%d), n(%d)", bytes, nbuffer); + + loopback->empty_buf = (char *)calloc(1, bytes); + if (!loopback->empty_buf) { + LOGE("Failed to alloc empty_buf\n"); + goto exit; + } + + if (pipe(loopback->wakeup)) { + LOGE("Failed to create pipe"); + goto exit; + } + + return loopback; + +exit: + if (loopback->ctl) + snd_ctl_close(loopback->ctl); + if (loopback->elm_active) + snd_ctl_elem_value_free(loopback->elm_active); + if (loopback->q) + destory_static_queue(loopback->q); + if (loopback->empty_buf) + free(loopback->empty_buf); + if (loopback) + free(loopback); + + return NULL; +} + +int loopback_start(loopback_t *loopback) +{ + if (!loopback->q) { + LOGE("loopback is not ready\n"); + return -1; + } + + loopback->pfds[POLL_WAKEUP].fd = loopback->wakeup[0]; + loopback->pfds[POLL_WAKEUP].events = POLLIN; + + /* poll fds */ + if (snd_ctl_poll_descriptors(loopback->ctl, &(loopback->pfds[POLL_CTRL]), 1) <= 0) { + LOGE("cannot get ctrl descriptor\n"); + return -1; + } + + loopback->poll_cnt = POLL_CTRL + 1; + + if (loopback->op) { + if (loopback->op->init(loopback->ec_frame, loopback->rate, loopback->channels)) { + LOGE("ec init failed\n"); + return -1; + } + } + + if (pthread_create(&loopback->t, NULL, __loop_thread, (void *)loopback) < 0) { + LOGE("failed to create thread\nn"); + return -1; + } + + return 0; +} + +int loopback_stop(loopback_t *loopback) +{ + ssize_t n; + int status; + char dummy = 'c'; + + if (!loopback) + return -1; + + loopback->quit = 1; + n = write(loopback->wakeup[1], &dummy, 1); + if (n <= 0) { + LOGE("Failed to wake up thread"); + return -1; + } + + if (pthread_join(loopback->t, (void **)&status)) { + LOGE("thread join failed\nn"); + return -1; + } + + loopback->quit = 0; + + return 0; +} + +int loopback_destroy(loopback_t *loopback) +{ + if (!loopback) + return -1; + + if (loopback->state == STATE_RUNNING) { + LOGE("Failed to destroy loopback. state running"); + return -1; + } + + if (loopback->q) + destory_static_queue(loopback->q); + + if (loopback->refq) + destory_static_queue(loopback->refq); + + if (loopback->name) { + free(loopback->name); + loopback->name = NULL; + } + + if (loopback->empty_buf) { + free(loopback->empty_buf); + loopback->empty_buf = NULL; + } + + if (loopback->elm_active) { + snd_ctl_elem_value_free(loopback->elm_active); + loopback->elm_active = NULL; + } + + if (loopback->ctl) { + snd_ctl_close(loopback->ctl); + loopback->ctl = NULL; + } + + __close_dev(loopback); + + free(loopback); + + return 0; +} + +int loopback_bind_reference(loopback_t *loopback, loopback_t *ref, ec_operation_t *op) +{ + if (loopback->state == STATE_RUNNING) { + LOGE("Failed to bind. state(%d)", loopback->state); + return -1; + } + + if (loopback->refq) { + LOGE("Failed to bind. already bound."); + return -1; + } + + loopback->op = op; + loopback->refq = create_static_queue(0, 0); + + ref->master = loopback; + ref->master_refq = loopback->refq; + + LOGI("bind loopback master(%s), refernece(%s)", loopback->name, ref->name); + + return 0; +} + +static int __handle_revents(struct loopback *loopback, int mode, unsigned short revents) +{ + int err; + struct loopback_dev *dev = loopback->dev[mode]; + snd_pcm_sframes_t frames = loopback->ec_frame; + snd_pcm_sframes_t avail; + + if (revents & POLLIN) { + unsigned char *recbuf; + sqbuffer *rec; + + avail = snd_pcm_avail_update(dev->handle); + if (avail < 0) { + LOGW("%s: Can't get avail: %s", dev->name, snd_strerror(avail)); + + if (__xrun_recovery(dev, avail)) { + LOGE("%s: AEC will be stopped.", dev->name); + return 0; + } + + } else if (avail < frames) { + LOGW("%s: not enough avail(%ld) < frames(%ld)\n", dev->name, avail, frames); + return 0; + } + + rec = sq_get_node_lock(loopback->q); + if (!rec) { + LOGE("%s: Failed to get valid sq node. maybe overflow", dev->name); + return 0; + } + + recbuf = sq_get_buffer(rec); + if ((err = snd_pcm_readi(dev->handle, recbuf, frames)) < 0) { + LOGE("%s: Failed to read: %s(%d)", dev->name, snd_strerror(err), err); + + sq_put_node_lock(rec); + __xrun_recovery(dev, err); + + return 0; + } + + sq_enqueue_node(loopback->q, rec); + + } else if (revents & POLLOUT) { + unsigned char *playbuf; + sqbuffer *play; + + avail = snd_pcm_avail_update(dev->handle); + if (avail < 0) { + LOGW("%s: Can't get avail. Try to recover: %s", dev->name, snd_strerror(avail)); + + if (__xrun_recovery(dev, avail)) + LOGE("%s: Failed to recover. AEC will be stopped.", dev->name); + + return 0; + + } else if (avail < frames) { + LOGW("%s: not enough avail(%ld) < frames(%ld)\n", dev->name, avail, frames); + return 0; + } + + play = sq_dequeue_node(loopback->q); + if (!play) { + if ((err = snd_pcm_writei(dev->handle, loopback->empty_buf, frames)) < 0) + LOGE("%s: Failed to write. frame(%ld): %s(%d)", + dev->name, frames, snd_strerror(err), err); + return 0; + } + + playbuf = sq_get_buffer(play); + + /* only recording thread */ + if (loopback->refq && !sq_is_empty_lock(loopback->refq)) { + sqbuffer *ref = sq_dequeue_node_lock(loopback->refq); + sqbuffer *out = sq_get_node_lock(loopback->q); + unsigned char *refbuf = sq_get_buffer(ref); + unsigned char *outbuf = sq_get_buffer(out); + + loopback->op->process(playbuf, refbuf, outbuf); + + sq_put_node_lock(play); + sq_put_node_lock(ref); + + play = out; + playbuf = outbuf; + } + + + if ((err = snd_pcm_writei(dev->handle, playbuf, frames)) < 0) { + LOGE("%s: Failed to write: %s", dev->name, snd_strerror(err)); + if (__xrun_recovery(dev, err)) + sq_put_node_lock(play); + return 0; + } + + /* only reference thread */ + if (loopback->master && loopback->master->state == STATE_RUNNING) { + sq_enqueue_node_lock(loopback->master_refq, play); + return 0; + } + + sq_put_node_lock(play); + } else + LOGD("unknown event %x\n", revents); + + return 0; +} + +static int __check_ctl_active_event(loopback_t *loopback, int *status) +{ + int err; + unsigned int event_mask; + snd_ctl_event_t *ev; + snd_ctl_elem_id_t *id1, *id2; + + snd_ctl_event_alloca(&ev); + snd_ctl_elem_id_alloca(&id1); + snd_ctl_elem_id_alloca(&id2); + + while ((err = snd_ctl_read(loopback->ctl, ev)) != 0 && err != -EAGAIN); + + snd_ctl_elem_value_get_id(loopback->elm_active, id1); + snd_ctl_event_elem_get_id(ev, id2); + + event_mask = snd_ctl_event_elem_get_mask(ev); + + if (event_mask == SND_CTL_EVENT_MASK_REMOVE || !(event_mask & SND_CTL_EVENT_MASK_VALUE)) + return 0; + + /* + LOGD("intf(%d:%d) id(%d:%d) subid(%d:%d) elem(%s:%s) index(%d:%d)\n", + snd_ctl_elem_id_get_interface(id1), snd_ctl_elem_id_get_interface(id2), + snd_ctl_elem_id_get_device(id1), snd_ctl_elem_id_get_device(id2), + snd_ctl_elem_id_get_subdevice(id1), snd_ctl_elem_id_get_subdevice(id2), + snd_ctl_elem_id_get_name(id1), snd_ctl_elem_id_get_name(id2), + snd_ctl_elem_id_get_index(id1), snd_ctl_elem_id_get_index(id2)); + */ + + if (snd_ctl_elem_id_get_interface(id1) != snd_ctl_elem_id_get_interface(id2)) + return 0; + + if (snd_ctl_elem_id_get_device(id1) != snd_ctl_elem_id_get_device(id2)) + return 0; + + if (snd_ctl_elem_id_get_subdevice(id1) != snd_ctl_elem_id_get_subdevice(id2)) + return 0; + + if (strcmp(snd_ctl_elem_id_get_name(id1), snd_ctl_elem_id_get_name(id2)) != 0) + return 0; + + if (snd_ctl_elem_id_get_index(id1) != snd_ctl_elem_id_get_index(id2)) + return 0; + + err = snd_ctl_elem_read(loopback->ctl, loopback->elm_active); + if (err < 0) { + LOGE("Failed to read ctl elem\n"); + return 0; + } + + err = snd_ctl_elem_value_get_boolean(loopback->elm_active, 0); + + *status = err; + + LOGE("dev:subid:elm(%d:%d:%s) is changed to (%d)\n", + snd_ctl_elem_id_get_device(id2), snd_ctl_elem_id_get_subdevice(id2), + snd_ctl_elem_id_get_name(id2), *status); + + return 1; +} + +static void *__loop_thread(void *userdata) +{ + int err; + unsigned short revents; + struct loopback *loopback = (struct loopback *)userdata; + struct loopback_dev **dev = loopback->dev; + + LOGI("start thread. name(%s), capture(%s) playback(%s), frame(%d), poll_cnt(%d)\n", + loopback->name, dev[CAPTURE]->name, dev[PLAYBACK]->name, + loopback->ec_frame, loopback->poll_cnt); + + while (1) { + if (poll(loopback->pfds, loopback->poll_cnt, -1) < 0) { + LOGE("poll err %s\n", strerror(errno)); + continue; + } + + if (loopback->quit) { + if (loopback->op) + loopback->op->deinit(); + + __close_dev(loopback); + loopback->state = STATE_STOPPED; + + LOGE("thread(%s) exit\n", loopback->name); + + pthread_exit(0); + } + + revents = 0; + if (!snd_ctl_poll_descriptors_revents(loopback->ctl, &loopback->pfds[POLL_CTRL], 1, &revents)) { + int onoff; + + if (__check_ctl_active_event(loopback, &onoff)) { + if (onoff) { /* Turn on */ + if (__open_dev(loopback, PLAYBACK, loopback->pfds+POLL_PLAYBACK)) { + LOGE("failed to open playback dev\n"); + pthread_exit(0); + } + + if (__open_dev(loopback, CAPTURE, loopback->pfds+POLL_CAPTURE)) { + LOGE("failed to open capture dev\n"); + pthread_exit(0); + } + + if ((err = snd_pcm_start(dev[CAPTURE]->handle)) < 0) + LOGE("capture dev start failed. %s\n", snd_strerror(err)); + + loopback->state = STATE_RUNNING; + + } else { /* Turn off */ + loopback->state = STATE_STOPPED; + + __close_dev(loopback); + + sq_flush(loopback->q); + sq_flush_lock(loopback->refq); + + LOGI("sleep...\n"); + + continue; + } + } + } + + if (loopback->state != STATE_RUNNING) + continue; + + revents = 0; + if (!snd_pcm_poll_descriptors_revents(dev[CAPTURE]->handle, + &loopback->pfds[POLL_CAPTURE], 1, &revents)) { + if (revents != 0) + __handle_revents(loopback, CAPTURE, revents); + } + revents = 0; + if (!snd_pcm_poll_descriptors_revents(dev[PLAYBACK]->handle, + &loopback->pfds[POLL_PLAYBACK], 1, &revents)) { + if (revents != 0) + __handle_revents(loopback, PLAYBACK, revents); + } + + if (loopback->logcnt++ > 100) { + loopback->logcnt = 0; + __print_buffer(loopback); + } + } +} + +static void __close_dev(loopback_t *loopback) +{ + if (loopback->dev[PLAYBACK]->handle) { + snd_pcm_close(loopback->dev[PLAYBACK]->handle); + loopback->dev[PLAYBACK]->handle = NULL; + } + loopback->poll_cnt--; + + if (loopback->dev[CAPTURE]->handle) { + snd_pcm_close(loopback->dev[CAPTURE]->handle); + loopback->dev[CAPTURE]->handle = NULL; + } + loopback->poll_cnt--; +} + +static int __xrun_recovery(struct loopback_dev *dev, int err) +{ + snd_pcm_state_t state; + + state = snd_pcm_state(dev->handle); + + if (state != SND_PCM_STATE_XRUN) { + LOGW("Invalid state(%d)", state); + return -1; + } + + LOGE("%s: Try to recover. state(%d)\n", dev->name, state); + + if (err == -EPIPE) { /* underrun or overrun */ + err = snd_pcm_prepare(dev->handle); + if (err < 0) { + LOGE("XRUN: Failed to prepare pcm\n"); + return -1; + } + } else if (err == -ESTRPIPE) { /* suspended */ + while ((err = snd_pcm_resume(dev->handle)) == -EAGAIN) + usleep(50000); + + if (err) { + LOGE("XRUN: Failed to resume pcm\n"); + return -1; + } + + err = snd_pcm_prepare(dev->handle); + if (err < 0) { + LOGE("XRUN: Failed to prepare pcm\n"); + return -1; + } + } else { + LOGE("XRUN: Unknown error\n"); + return -1; + } + + // TODO: add device close open + LOGE("XRUN: recovered"); + + return 0; +} + +static int __open_dev(loopback_t *loopback, int index, struct pollfd *pfd) +{ + int err; + int flags = SND_PCM_NONBLOCK | SND_PCM_NO_AUTO_RESAMPLE | SND_PCM_NO_AUTO_FORMAT; + unsigned int buffer_time; + + const char *name = loopback->dev[index]->name; + int mode = loopback->dev[index]->mode; + unsigned int rate = loopback->dev[index]->rate; + unsigned int _rate = rate; + int channels = loopback->dev[index]->channels; + //int format = dev->format; + snd_pcm_uframes_t buffer_frame = loopback->dev[index]->buffer_frame; + struct loopback_dev *dev = loopback->dev[index]; + + snd_pcm_stream_t _mode = (mode == CAPTURE) ? SND_PCM_STREAM_CAPTURE : SND_PCM_STREAM_PLAYBACK; + snd_pcm_sw_params_t *sw_params = NULL; + snd_pcm_hw_params_t *hw_params = NULL; + snd_pcm_uframes_t buffer_size, period_size; + + if ((err = snd_pcm_open(&dev->handle, name, _mode, flags)) < 0) { + LOGE("%s: cannot open audio device (%s)\n", name, snd_strerror(err)); + goto fail; + } + + if ((err = snd_pcm_hw_params_malloc(&hw_params)) < 0) { + LOGE("%s: cannot allocate hardware parameter structure(%s)\n", name, snd_strerror(err)); + goto fail; + } + + if ((err = snd_pcm_hw_params_any(dev->handle, hw_params)) < 0) { + LOGE("%s: cannot initialize hardware parameter structure(%s)\n", name, snd_strerror(err)); + goto fail; + } + + if ((err = snd_pcm_hw_params_set_rate_resample(dev->handle, hw_params, 0)) < 0) { + LOGE("%s: cannot set sample SAMPLE_RATE(%s)\n", name, snd_strerror(err)); + goto fail; + } + + if ((err = snd_pcm_hw_params_set_access(dev->handle, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) { + LOGE("%s: cannot set access type(%s)\n", name, snd_strerror(err)); + goto fail; + } + + if ((err = snd_pcm_hw_params_set_format(dev->handle, hw_params, SND_PCM_FORMAT_S16_LE)) < 0) { + LOGE("%s: cannot set sample format(%s)\n", name, snd_strerror(err)); + goto fail; + } + + if ((err = snd_pcm_hw_params_set_channels(dev->handle, hw_params, channels)) < 0) { + unsigned int ch; + + LOGE("%s: cannot set channel. try near. channels(%d). %s\n", + name, channels, snd_strerror(err)); + + if ((err = snd_pcm_hw_params_set_channels_near(dev->handle, hw_params, &ch)) < 0) { + LOGE("%s: cannot set channel. %s\n", name, snd_strerror(err)); + goto fail; + } + dev->channels = ch; + } + + if ((err = snd_pcm_hw_params_set_rate_near(dev->handle, hw_params, &_rate, NULL)) < 0) { + LOGE("%s: cannot set sample SAMPLE_RATE(%s)\n", name, snd_strerror(err)); + goto fail; + } + + if (_rate != rate) { + LOGE("%s: rate doesn't matched rate(%d) _rate(%d)\n", name, rate, _rate); + return -1; + } + + if ((err = snd_pcm_hw_params_set_buffer_size_near(dev->handle, hw_params, &buffer_frame)) < 0) { + LOGE("%s: snd_pcm_hw_params_set_buffer_size_near(%s)\n", name, snd_strerror(err)); + goto fail; + } + + snd_pcm_hw_params_get_buffer_size(hw_params, &buffer_size); + snd_pcm_hw_params_get_buffer_time(hw_params, &buffer_time, NULL); + + unsigned int period_time = buffer_time / 4; + if ((err = snd_pcm_hw_params_set_period_time_near(dev->handle, hw_params, &period_time, NULL))) { + LOGE("%s: hw set period time near %d)\n", name, period_time); + goto fail; + } + + if ((err = snd_pcm_hw_params_get_period_size(hw_params, &period_size, NULL))) { + LOGE("%s: hw set period time near %d)\n", name, period_time); + goto fail; + } + + if ((err = snd_pcm_hw_params(dev->handle, hw_params)) < 0) { + LOGE("%s: cannot set parameters(%s)\n", name, snd_strerror(err)); + goto fail; + } + + snd_pcm_hw_params_free(hw_params); + + /* sw params */ + if ((err = snd_pcm_sw_params_malloc(&sw_params)) < 0) { + LOGE("%s: cannot allocate software parameters structure(%s)\n", name, snd_strerror(err)); + goto fail; + } + + err = snd_pcm_sw_params_current(dev->handle, sw_params); + if (err < 0) { + LOGE("%s: cannot initialize software parameters structure(%s)\n", name, snd_strerror(err)); + goto fail; + } + + err = snd_pcm_sw_params_set_start_threshold(dev->handle, sw_params, period_size); + if (err < 0) { + LOGE("Unable to set start threshold mode for %s", snd_strerror(err)); + goto fail; + } + + if ((err = snd_pcm_sw_params_set_avail_min(dev->handle, sw_params, period_size)) < 0) { + LOGE("%s: cannot set minimum available count(%s)\n", name, snd_strerror(err)); + goto fail; + } + + err = snd_pcm_sw_params(dev->handle, sw_params); + if (err < 0) { + LOGE("Unable to set sw params for %s", snd_strerror(err)); + goto fail; + } + + snd_pcm_sw_params_free(sw_params); + + dev->period_size = period_size; + dev->period_time = period_time; + dev->buffer_size = buffer_size; + dev->buffer_time = buffer_time; + + if (pfd) { + if (snd_pcm_poll_descriptors(dev->handle, pfd, 1) <= 0) { + LOGE("cannot get capture descriptor\n"); + goto fail; + } + } + + loopback->poll_cnt++; + + LOGI("%s dev(%s) opened successfully. rate(%d), ch(%d), request buffer_frame(%d)\n", + mode ? "playback" : "capture", name, dev->rate, dev->channels, dev->buffer_frame); + LOGI("\tbuffer size : %ld frames, %d usec\n", buffer_size, buffer_time); + LOGI("\tperiod size : %ld frames, %d usec\n", period_size, period_time); + LOGI("\tthread_hold : %ld, avail_min %ld\n", period_size, period_size); + LOGI("\tloopback process frame %d", loopback->ec_frame); + + return 0; + +fail: + if (dev->handle) + snd_pcm_close(dev->handle); + if (hw_params) + snd_pcm_hw_params_free(hw_params); + if (sw_params) + snd_pcm_sw_params_free(sw_params); + + return -1; +} + +static int __print_buffer(loopback_t *loopback) +{ + struct loopback_dev **dev = loopback->dev; + snd_pcm_sframes_t c_avail, c_delay; + snd_pcm_sframes_t p_avail, p_delay; + float capture; + float playback; + + snd_pcm_avail_delay(dev[CAPTURE]->handle, &c_avail, &c_delay); + snd_pcm_avail_delay(dev[PLAYBACK]->handle, &p_avail, &p_delay); + + capture = (float)c_avail / (float)(dev[CAPTURE]->buffer_size) * 100; + playback = (float)p_avail / (float)(dev[PLAYBACK]->buffer_size) * 100; + + LOGD("name(%s) capture avail(%.2f%%), playback avail(%.2f%%) buffer delay(%d)", + loopback->name, capture, playback, sq_get_work_node_count(loopback->q)); + + if (loopback->refq) + LOGE("refq delay(%d)", sq_get_work_node_count(loopback->refq)); + + return 0; +} + diff --git a/aec/loopback.h b/aec/loopback.h new file mode 100644 index 0000000..c656861 --- /dev/null +++ b/aec/loopback.h @@ -0,0 +1,49 @@ +/* +* Copyright (c) 2021 Samsung Electronics Co., Ltd All Rights Reserved +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +#ifndef __LOOPBACK__ +#define __LOOPBACK__ + +typedef struct loopback loopback_t; +typedef struct loopback_dev loopback_dev_t; + +typedef struct ec_operation { + int (*init)(int, int, int); + int (*process)(unsigned char *, unsigned char *, unsigned char *); + int (*deinit)(); +} ec_operation_t; + +enum { + CAPTURE_MODE = 0, + PLAYBACK_MODE, +}; + +loopback_dev_t *loopback_alloc_device(const char *name, int mode, int rate, + int channels, int format, int latency); +int loopback_open_device(loopback_dev_t *dev); +loopback_t *loopback_create(const char *name, + loopback_dev_t *capture, + loopback_dev_t *playback, + int process_frames, int nbuffer); +int loopback_free_device(loopback_dev_t *dev); +int loopback_start(loopback_t *loopback); +int loopback_stop(loopback_t *loopback); +int loopback_destroy(loopback_t *loopback); + +int loopback_bind_reference(loopback_t *loopback, loopback_t *ref, ec_operation_t *op); + +int loopback_print_buffer(loopback_t *loopback); + +#endif // __LOOPBACK__ diff --git a/aec/main.c b/aec/main.c new file mode 100644 index 0000000..a9b1510 --- /dev/null +++ b/aec/main.c @@ -0,0 +1,522 @@ +/* +* Copyright (c) 2021 Samsung Electronics Co., Ltd All Rights Reserved +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "loopback.h" +#include "algo_speex.h" + +#define LOOPBACK_BUFFER_COUNT 40 +#define FIXME_DEFAULT_FORMAT_SIZE 2 + +/* AEC daemon interfaceis */ +#define AEC_BUS_NAME "org.tizen.AudioAec" +#define AEC_OBJECT "/org/tizen/AudioAec" +#define AEC_INTERFACE "org.tizen.AudioAec" + +/* pulseaudio aec-manager interfaces */ +#define PA_BUS_NAME "org.pulseaudio.Server" +#define PA_AEC_MANAGER_OBJECT_PATH "/org/pulseaudio/AecManager" +#define PA_AEC_MANAGER_INTERFACE "org.pulseaudio.AecManager" +#define PA_AEC_MANAGER_METHOD_NAME_ON "On" +#define PA_AEC_MANAGER_METHOD_NAME_OFF "Off" +#define PA_AEC_MANAGER_METHOD_NAME_GET_SINK_PARAMS "GetSinkParam" +#define PA_AEC_MANAGER_METHOD_NAME_GET_SOURCE_PARAMS "GetSourceParam" + +#define SOUNDCARD_LOOPBACK "Loopback" +#define LOOPBACK_PLAYBACK_SOURCE "hw:Loopback,1,0" +#define LOOPBACK_CAPTURE_SINK "hw:Loopback,0,1" + +#define DEVICE_NAME_MAX 64 + +enum { + CAPTURE_DEVICE, + PLAYBACK_DEVICE, + DEVICE_MAX +}; + +enum { + THREAD_CAPTURE, + THREAD_PLAYBACK, + THREAD_MAX +}; + +static struct sigaction sigabt, sigsegv, sigterm; + +static DBusConnection *conn; +static loopback_dev_t *reference[DEVICE_MAX]; +static loopback_dev_t *recording[DEVICE_MAX]; +static loopback_t *thread[THREAD_MAX]; +static int opt_ec; + +static struct aec_params { + int rate; + int channels; + int format; + int frag_size; + int nfrags; + char *card; + char *device; + char fullname[DEVICE_NAME_MAX]; +} aec_params[DEVICE_MAX]; + +static int _get_devices_params(int dev, struct aec_params *params); +static int __dbus_method_call(const char *method, GVariant *param, GVariant **reply); +static void __register_signal_handler(); + +loopback_t *__create_loopback_instance(const char *name, const char *c, const char *p, + struct aec_params *params, loopback_dev_t **dev) +{ + int i; + const char *devs[DEVICE_MAX] = { c, p }; + loopback_dev_t *_dev[DEVICE_MAX] = { 0, 0 }; + loopback_t *ret = NULL; + + unsigned int buffer_frame, period_frame; + const int BUFFER_COUNT = 60; /* refq needs a lot of nodes than others */ + + if (!c || !p || !name) { + LOGE("Invalid args capture(%s), playback(%s), thread name(%s)", + c != NULL ? c : "", p != NULL ? p : "", + name != NULL ? name : ""); + return NULL; + } + + buffer_frame = params->nfrags * params->frag_size; + buffer_frame /= FIXME_DEFAULT_FORMAT_SIZE * params->channels; + period_frame = params->frag_size / FIXME_DEFAULT_FORMAT_SIZE / params->channels; + + for (i = 0; i < DEVICE_MAX; i++) { + _dev[i] = loopback_alloc_device(devs[i], i, params->rate, params->channels, + FIXME_DEFAULT_FORMAT_SIZE, buffer_frame); + if (!_dev[i]) { + LOGE("Failed to alloc device index(%d)", i); + goto exit; + } + } + + ret = loopback_create(name, _dev[CAPTURE_DEVICE], _dev[PLAYBACK_DEVICE], + period_frame, BUFFER_COUNT); + if (!ret) { + LOGE("Failed to create loopback. capture(%s) playback(%s)", c, p); + goto exit; + } + + dev[CAPTURE_DEVICE] = _dev[CAPTURE_DEVICE]; + dev[PLAYBACK_DEVICE] = _dev[PLAYBACK_DEVICE]; + + return ret; + +exit: + if (_dev[CAPTURE_DEVICE]) + loopback_free_device(_dev[CAPTURE_DEVICE]); + + if (_dev[PLAYBACK_DEVICE]) + loopback_free_device(_dev[PLAYBACK_DEVICE]); + + return NULL; +} + +static void __send_success_reply(DBusConnection *conn, DBusMessage *msg) +{ + DBusMessage *reply = NULL; + + if (!(reply = dbus_message_new_method_return(msg))) { + LOGE("Failed to get reply."); + return; + } + + dbus_connection_send(conn, reply, NULL); + dbus_message_unref(reply); +} + +static void __send_error_reply(DBusConnection *conn, DBusMessage *msg, + const char *name, const char *s) +{ + DBusMessage *reply = NULL; + + if (!(reply = dbus_message_new_error(msg, name, s))) { + LOGE("Failed to get reply."); + return; + } + + dbus_connection_send(conn, reply, NULL); + dbus_message_unref(reply); +} + +static int __run_thread_workers(struct aec_params *params) +{ + int i; + + thread[THREAD_PLAYBACK] = __create_loopback_instance("playback", + LOOPBACK_PLAYBACK_SOURCE, + params[PLAYBACK_DEVICE].fullname, + &aec_params[PLAYBACK_DEVICE], reference); + if (!thread[THREAD_PLAYBACK]) { + LOGE("Failed to create reference thread"); + goto fail; + } + + thread[THREAD_CAPTURE] = __create_loopback_instance("capture", + aec_params[CAPTURE_DEVICE].fullname, + LOOPBACK_CAPTURE_SINK, + &aec_params[CAPTURE_DEVICE], recording); + if (!thread[THREAD_CAPTURE]) { + LOGE("Failed to create recording thread"); + goto fail; + } + + // FIXME + if (opt_ec) { + if (loopback_bind_reference(thread[THREAD_CAPTURE], + thread[THREAD_PLAYBACK], + get_speex_instance())) { + LOGE("Failed to bind reference"); + goto fail; + } + } + + if (loopback_start(thread[THREAD_CAPTURE])) { + LOGE("Failed to start reference thread"); + goto fail; + } + + if (loopback_start(thread[THREAD_PLAYBACK])) { + LOGE("Failed to start recording thread"); + goto fail; + } + + return 0; + +fail: + LOGE("Failed to Launch/Quit. Process will be terminated"); + + for (i = 0; i < THREAD_MAX; i++) { + loopback_stop(thread[i]); + loopback_destroy(thread[i]); + } + for (i = 0; i < DEVICE_MAX; i++) { + loopback_free_device(reference[i]); + loopback_free_device(recording[i]); + } + + return -1; +} + +static int __stop_thread_workers() { + int i; + int ret = 0; + loopback_dev_t **devs[THREAD_MAX] = { recording, reference }; + + for (i = 0; i < THREAD_MAX; i++) { + if (thread[i]) { + ret = loopback_stop(thread[i]); + ret |= loopback_destroy(thread[i]); + thread[i] = NULL; + } + ret |= loopback_free_device(devs[i][CAPTURE_DEVICE]); + ret |= loopback_free_device(devs[i][PLAYBACK_DEVICE]); + recording[CAPTURE_DEVICE] = NULL; + recording[PLAYBACK_DEVICE] = NULL; + + if (ret) + LOGE("Failed to stop thread workers(%d)", i); + } + + return 0; +} + +static DBusHandlerResult __message_handler(DBusConnection *conn, + DBusMessage *msg, void *userdata) +{ + LOGD("type(%d) path(%s) member(%s) intf(%s), sig(%s)", + dbus_message_get_type(msg), dbus_message_get_path(msg), + dbus_message_get_member(msg), dbus_message_get_interface(msg), + dbus_message_get_signature(msg)); + + if (dbus_message_is_method_call(msg, AEC_INTERFACE, "Launch")) { + if (thread[THREAD_PLAYBACK] || thread[THREAD_CAPTURE]) { + LOGW("Already working"); + goto exit; + } + + if (__run_thread_workers(aec_params)) { + __send_error_reply(conn, msg, DBUS_ERROR_FAILED, "Process will be terminated"); + dbus_connection_unref(conn); + raise(SIGTERM); + } + __dbus_method_call(PA_AEC_MANAGER_METHOD_NAME_ON, NULL, NULL); + + LOGI("Launched successfully"); + + } else if (dbus_message_is_method_call(msg, AEC_INTERFACE, "Quit")) { + if (!thread[THREAD_PLAYBACK] || !thread[THREAD_CAPTURE]) { + LOGW("not working"); + goto exit; + } + + if (__stop_thread_workers()) { + __send_error_reply(conn, msg, DBUS_ERROR_FAILED, "Process will be terminated"); + dbus_connection_unref(conn); + raise(SIGTERM); + } + __dbus_method_call(PA_AEC_MANAGER_METHOD_NAME_OFF, NULL, NULL); + + LOGE("Quit successfully"); + } + +exit: + __send_success_reply(conn, msg); + + return DBUS_HANDLER_RESULT_HANDLED; +} + +DBusConnection *register_service() +{ + int rv; + DBusError err; + static DBusObjectPathVTable vtable = { .message_function = __message_handler }; + + dbus_error_init(&err); + + conn = dbus_bus_get(DBUS_BUS_SYSTEM, &err); + if (!conn) { + LOGE("Failed to get a private DBus connection"); + goto exit; + } + + rv = dbus_bus_request_name(conn, AEC_BUS_NAME, DBUS_NAME_FLAG_REPLACE_EXISTING, &err); + if (rv != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) { + LOGE("Failed to request name on bus err(%s)\n", err.message); + goto exit; + } + + if (!dbus_connection_register_object_path(conn, AEC_OBJECT, &vtable, NULL)) { + LOGE("Failed to register object path(%s)", AEC_OBJECT); + goto exit; + } + + return conn; + +exit: + dbus_error_free(&err); + + return NULL; +} + +void run_service(DBusConnection *conn) +{ + while (dbus_connection_read_write_dispatch(conn, -1)); +} + +int main(int argc, char *argv[]) +{ + if (argc != 1 && argc != 2) + exit(EXIT_FAILURE); + + if (snd_card_get_index(SOUNDCARD_LOOPBACK) < 0) { + LOGE("Failed to get Loopback card"); + exit(EXIT_FAILURE); + } + + __register_signal_handler(); + + conn = register_service(); + + if (getopt(argc, argv, "c") == 'c') + opt_ec = 1; + + if (_get_devices_params(CAPTURE_DEVICE, &aec_params[CAPTURE_DEVICE])) { + LOGE("Failed to get source params"); + exit(EXIT_FAILURE); + } + + if (_get_devices_params(PLAYBACK_DEVICE, &aec_params[PLAYBACK_DEVICE])) { + LOGE("Failed to get sink params"); + exit(EXIT_FAILURE); + } + + run_service(conn); + + dbus_connection_flush(conn); + dbus_connection_close(conn); + + return 0; +} + +static int _get_devices_params(int dev, struct aec_params *params) +{ + GVariant *reply = NULL; + + int rate, channels, format, frag_size, nfrags; + char *card = NULL; + char *device = NULL; + char *msg; + + if (dev == PLAYBACK_DEVICE) + msg = PA_AEC_MANAGER_METHOD_NAME_GET_SINK_PARAMS; + else + msg = PA_AEC_MANAGER_METHOD_NAME_GET_SOURCE_PARAMS; + + if (__dbus_method_call(msg, NULL, &reply)) { + LOGE("Failed to get device params"); + return -1; + } + + if (!reply) { + LOGE("Failed to start aec daemon"); + return -1; + } + + g_variant_get(reply, "(iiiii&s&s)", &rate, &channels, &format, + &frag_size, &nfrags, &card, &device); + if (!card || !device) { + LOGE("Failed to get device and card names"); + g_variant_unref(reply); + return -1; + } + + LOGI("source param rate(%d), channels(%d), format(%d), " + "frag_size(%d), nfrag(%d), card(%s), device(%s)", + rate, channels, format, frag_size, nfrags, card, device); + + params->rate = rate; + params->channels = channels; + params->format = format; + params->frag_size = frag_size; + params->nfrags = nfrags; + params->card = strdup(card); + params->device = strdup(device); + snprintf(params->fullname, DEVICE_NAME_MAX, "hw:%s,%s", params->card, params->device); + + g_variant_unref(reply); + + return 0; +} + +static GDBusConnection *__get_dbus_connection(void) +{ + GDBusConnection *conn = NULL; + GError *err = NULL; + + conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &err); + if (!conn) { + LOGE("g_bus_get_sync() error (%s)", err->message); + g_error_free(err); + } + + return conn; +} + +static int __dbus_method_call(const char *method, GVariant *param, GVariant **reply) +{ + GDBusConnection *conn = NULL; + GVariant *_reply = NULL; + GError *err = NULL; + + if (!method) { + LOGE("invalid parameter"); + goto error; + } + + if (!(conn = __get_dbus_connection())) + goto error; + + _reply = g_dbus_connection_call_sync(conn, PA_BUS_NAME, + PA_AEC_MANAGER_OBJECT_PATH, + PA_AEC_MANAGER_INTERFACE, + method, param, NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err); + + g_object_unref(conn); + + if (!_reply) { + LOGE("g_dbus_connection_call_sync() method(%s), err-msg(%s)", method, err->message); + g_error_free(err); + return -1; + } + + if (reply) + *reply = _reply; + else + g_variant_unref(_reply); + + return 0; + +error: + if (param) + g_variant_unref(param); + + return -1; +} + +static void _signal_handler(int sig) +{ + int i; + + __dbus_method_call(PA_AEC_MANAGER_METHOD_NAME_OFF, NULL, NULL); + + for (i = 0; i < 2; i++) { + loopback_stop(thread[i]); + loopback_destroy(thread[i]); + loopback_free_device(reference[i]); + loopback_free_device(recording[i]); + } + + switch (sig) { + case SIGABRT: + sigaction(SIGABRT, &sigabt, NULL); + break; + case SIGSEGV: + sigaction(SIGSEGV, &sigsegv, NULL); + break; + case SIGTERM: + sigaction(SIGTERM, &sigterm, NULL); + break; + default: + break; + } + + raise(sig); +} + +static void __register_signal_handler() +{ + struct sigaction action; + + action.sa_handler = _signal_handler; + action.sa_flags = 0; + sigemptyset(&action.sa_mask); + + sigaction(SIGABRT, &action, &sigabt); + sigaction(SIGSEGV, &action, &sigsegv); + sigaction(SIGTERM, &action, &sigterm); +} + diff --git a/aec/static_queue.c b/aec/static_queue.c new file mode 100644 index 0000000..4dcd181 --- /dev/null +++ b/aec/static_queue.c @@ -0,0 +1,313 @@ +/* +* Copyright (c) 2021 Samsung Electronics Co., Ltd All Rights Reserved +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#include +#include +#include +#include +#include + +#include "static_queue.h" + +struct sqbuffer { + unsigned char *buffer; + struct static_queue *p; + struct sqbuffer *n; +}; + +struct static_queue { + struct sqbuffer *hold; + struct sqbuffer *idle; + struct sqbuffer *front, *rear; + + int count; + int bytes; + int inqueue; + + pthread_mutex_t lock; +}; + +sq *create_static_queue(int bytes, int count) +{ + struct static_queue *sq; + struct sqbuffer *sqb = NULL; + int i; + + if (bytes != 0 && count == 0) { + LOGE("invalid count(%d)", count); + return NULL; + } + + if (bytes == 0 && count != 0) { + LOGE("invalid bytes(%d)", bytes); + return NULL; + } + + sq = (struct static_queue *)calloc(1, sizeof(struct static_queue)); + if (!sq) { + LOGE("Failed to alloc sq"); + return NULL; + } + + sq->bytes = bytes; + sq->count = count; + pthread_mutex_init(&sq->lock, NULL); + + /* for refq */ + if (bytes == 0 && count == 0) + return sq; + + sqb = (struct sqbuffer *)calloc(count, sizeof(struct sqbuffer)); + if (!sqb) { + LOGE("Failed to alloc sqb %d", count); + goto fail; + } + sq->hold = sqb; + + for (i = 0; i < count; i++) { + sqb[i].buffer = (unsigned char *)malloc(bytes); + if (!sqb[i].buffer) { + LOGE("Failed to alloc sqb buffer"); + goto fail; + } + sqb[i].p = sq; + sqb[i].n = sq->idle; + sq->idle = sqb + i; + } + + return sq; + +fail: + if (sqb) { + for (i = 0; i < count; i++) { + if (sqb[i].buffer) + free(sqb[i].buffer); + } + free(sqb); + } + if (sq) + free(sq); + + return NULL; +} + +void destory_static_queue(sq *q) +{ + int i; + + if (!q) { + LOGE("invalid args"); + return; + } + + for (i = 0; i < q->count; i++) { + if (q->hold[i].buffer) + free(q->hold[i].buffer); + } + + pthread_mutex_destroy(&q->lock); + + if (q->hold) + free(q->hold); + if (q) + free(q); +} + +sqbuffer *sq_get_node(sq *q) +{ + sqbuffer *sqb; + + if (!q) { + LOGE("invalid args"); + return NULL; + } + + if (!q->idle) { + LOGE("Failed to get available node"); + return NULL; + } + + sqb = q->idle; + q->idle = sqb->n; + sqb->n = NULL; + + return sqb; +} + +sqbuffer *sq_get_node_lock(sq *q) +{ + sqbuffer *ret; + + if (!q) + return NULL; + + pthread_mutex_lock(&q->lock); + ret = sq_get_node(q); + pthread_mutex_unlock(&q->lock); + + return ret; +} + +void sq_put_node(sqbuffer *b) +{ + sq *q; + + if (!b) { + LOGE("invalid args"); + return; + } + + q = b->p; + b->n = q->idle; + q->idle = b; +} + +void sq_put_node_lock(sqbuffer *b) +{ + sq *q; + + if (!b) { + LOGE("invalid args"); + return; + } + + q = b->p; + + pthread_mutex_lock(&q->lock); + sq_put_node(b); + pthread_mutex_unlock(&q->lock); +} + +unsigned char *sq_get_buffer(sqbuffer *sqb) +{ + return sqb ? sqb->buffer : NULL; +} + +int sq_enqueue_node(sq *q, sqbuffer *b) +{ + if (!q || !b) { + LOGE("invalid args"); + return -1; + } + + if (!q->front) + q->front = q->rear = b; + else + q->rear = q->rear->n = b; + + q->inqueue++; + + return q->inqueue; +} + +int sq_enqueue_node_lock(sq *q, sqbuffer *b) +{ + int ret; + + pthread_mutex_lock(&q->lock); + ret = sq_enqueue_node(q, b); + pthread_mutex_unlock(&q->lock); + + return ret; +} + +sqbuffer *sq_dequeue_node(sq *q) +{ + sqbuffer *sqb; + + if (!q) { + LOGE("invalid args"); + return NULL; + } + + if (!q->front) + return NULL; + + sqb = q->front; + q->front = sqb->n; + sqb->n = NULL; + q->inqueue--; + + return sqb; +} + +sqbuffer *sq_dequeue_node_lock(sq *q) +{ + sqbuffer *sqb; + + pthread_mutex_lock(&q->lock); + sqb = sq_dequeue_node(q); + pthread_mutex_unlock(&q->lock); + + return sqb; +} + +void sq_flush(sq *q) +{ + sqbuffer *i; + + if (!q) + return; + + while ((i = sq_dequeue_node(q)) != NULL) + sq_put_node(i); +} + +void sq_flush_lock(sq *q) +{ + sqbuffer *i; + + if (!q) + return; + + pthread_mutex_lock(&q->lock); + + while ((i = sq_dequeue_node(q)) != NULL) + sq_put_node_lock(i); + + pthread_mutex_unlock(&q->lock); +} + +int sq_is_empty(sq *q) +{ + if (!q) + return -1; + + return q->front ? 0 : 1; +} + +int sq_is_empty_lock(sq *q) +{ + int ret; + + if (!q) + return -1; + + pthread_mutex_lock(&q->lock); + ret = sq_is_empty(q); + pthread_mutex_unlock(&q->lock); + + return ret; +} + +int sq_get_work_node_count(sq *q) +{ + if (!q) + return -1; + + return q->inqueue; +} + diff --git a/aec/static_queue.h b/aec/static_queue.h new file mode 100644 index 0000000..f5ca8ff --- /dev/null +++ b/aec/static_queue.h @@ -0,0 +1,44 @@ +/* +* Copyright (c) 2021 Samsung Electronics Co., Ltd All Rights Reserved +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +#ifndef __STATIC_QUEUE__ +#define __STATIC_QUEUE__ + +typedef struct sqbuffer sqbuffer; +typedef struct static_queue sq; + +sq *create_static_queue(int bytes, int count); +void destory_static_queue(sq *q); + +sqbuffer *sq_get_node(sq *q); +void sq_put_node(sqbuffer *b); +unsigned char *sq_get_buffer(sqbuffer *sqb); +int sq_enqueue_node(sq *q, sqbuffer *b); +sqbuffer *sq_dequeue_node(sq *q); +void sq_flush(sq *q); + +/* lockable api */ +sqbuffer *sq_get_node_lock(sq *q); +void sq_put_node_lock(sqbuffer *b); +int sq_enqueue_node_lock(sq *q, sqbuffer *b); +sqbuffer *sq_dequeue_node_lock(sq *q); +void sq_flush_lock(sq *q); + +int sq_is_empty(sq *q); +int sq_is_empty_lock(sq *q); +int sq_get_work_node_count(sq *q); + +#endif // __STATIC_QUEUE__ + diff --git a/configure.ac b/configure.ac index 123e3f8..a7c5fc5 100644 --- a/configure.ac +++ b/configure.ac @@ -41,6 +41,22 @@ PKG_CHECK_MODULES(VCONF, vconf) AC_SUBST(VCONF_CFLAGS) AC_SUBST(VCONF_LIBS) +PKG_CHECK_MODULES(ALSA, alsa) +AC_SUBST(ALSA_CFLAGS) +AC_SUBST(ALSA_LIBS) + +PKG_CHECK_MODULES(SPEEX, speexdsp) +AC_SUBST(SPEEX_CFLAGS) +AC_SUBST(SPEEX_LIBS) + +PKG_CHECK_MODULES(DBUS, dbus-1) +AC_SUBST(DBUS_CFLAGS) +AC_SUBST(DBUS_LIBS) + +PKG_CHECK_MODULES(DLOG, dlog) +AC_SUBST(DLOG_CFLAGS) +AC_SUBST(DLOG_LIBS) + AC_ARG_ENABLE(pulse, AC_HELP_STRING([--enable-pulse], [enable pulseaudio client]), [ case "${enableval}" in @@ -101,6 +117,15 @@ AS_IF([test "x$enable_unittests" = "xyes"], [ AC_SUBST(GTESTS_LIBS) ]) +AC_ARG_ENABLE(aec, AC_HELP_STRING([--enable-aec], [using aec]), +[ + case "${enableval}" in + yes) ENABLE_AEC=yes ;; + no) ENABLE_AEC=no ;; + *) AC_MSG_ERROR(bad value ${enableval} for --enable-aec) ;; + esac + ],[USE_AEC=no]) +AM_CONDITIONAL(ENABLE_AEC, test "x$ENABLE_AEC" = "xyes") # Checks for header files. AC_HEADER_STDC AC_CHECK_HEADERS([fcntl.h memory.h stdlib.h string.h sys/time.h unistd.h errno.h sys/types.h sys/stat.h]) @@ -128,5 +153,6 @@ pkgconfig/mm-keysound.pc pkgconfig/mm-bootsound.pc testsuite/Makefile unittest/Makefile +aec/Makefile ]) AC_OUTPUT diff --git a/packaging/audio-aec.conf b/packaging/audio-aec.conf new file mode 100644 index 0000000..059cd6f --- /dev/null +++ b/packaging/audio-aec.conf @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + diff --git a/packaging/audio-aec.service b/packaging/audio-aec.service new file mode 100644 index 0000000..9a240cd --- /dev/null +++ b/packaging/audio-aec.service @@ -0,0 +1,14 @@ +[Unit] +Description=Audio Acoustic Echo Cancellation + +[Service] +Type=simple +ExecStart=/usr/bin/audio_aec -c +Restart=no +RestartSec=0 +MemoryLimit=500M +User=multimedia_fw +Group=multimedia_fw +SmackProcessLabel=System +SecureBits=keep-caps +Capabilities=cap_fowner,cap_lease=i diff --git a/packaging/libmm-sound.spec b/packaging/libmm-sound.spec index e213627..3b8e11c 100644 --- a/packaging/libmm-sound.spec +++ b/packaging/libmm-sound.spec @@ -1,10 +1,13 @@ Name: libmm-sound Summary: MMSound Package contains client lib and sound_server binary -Version: 0.13.6 +Version: 0.13.7 Release: 0 Group: System/Libraries License: Apache-2.0 Source0: %{name}-%{version}.tar.gz +Source1: audio-aec.conf +Source2: audio-aec.service +Source3: org.tizen.AudioAec.service Source4: focus-server.service Source5: focus-server.path Source6: focus-server.conf @@ -26,6 +29,10 @@ BuildRequires: pkgconfig(lwipc) %if 0%{?gtests:1} BuildRequires: pkgconfig(gmock) %endif +BuildRequires: pkgconfig(alsa) +BuildRequires: pkgconfig(speexdsp) +BuildRequires: pkgconfig(dbus-1) +BuildRequires: pkgconfig(dlog) %description MMSound package contains focus-server and client interfaces connected to audio system @@ -77,6 +84,9 @@ export LDFLAGS+=" -lgcov " --enable-prelink \ --enable-lwipc \ %endif +%if "%{tizen_profile_name}" != "tv" + --enable-aec \ +%endif %if 0%{?gtests:1} --enable-unittests \ %endif @@ -91,6 +101,10 @@ cp %{SOURCE6} %{buildroot}/etc/dbus-1/system.d/focus-server.conf %if "%{tizen_profile_name}" == "tv" cp %{SOURCE8} %{SOURCE4} %endif +%if "%{tizen_profile_name}" != "tv" +cp %{SOURCE1} %{buildroot}/etc/dbus-1/system.d/audio-aec.conf +%endif + mkdir -p %{buildroot}/usr/share/dbus-1/system-services/ %make_install @@ -102,6 +116,13 @@ install -d %{buildroot}%{_unitdir}/paths.target.wants install -m0644 %{SOURCE4} %{buildroot}%{_unitdir}/ install -m0644 %{SOURCE5} %{buildroot}%{_unitdir}/ ln -sf ../focus-server.path %{buildroot}%{_unitdir}/paths.target.wants/focus-server.path + +%if "%{tizen_profile_name}" != "tv" +install -m0644 %{SOURCE2} %{buildroot}%{_unitdir}/ +%endif +mkdir -p %{buildroot}/usr/share/dbus-1/system-services/ +cp %{SOURCE3} %{buildroot}/usr/share/dbus-1/system-services/org.tizen.AudioAec.service + %post /sbin/ldconfig @@ -122,8 +143,17 @@ ln -sf ../focus-server.path %{buildroot}%{_unitdir}/paths.target.wants/focus-ser %{_unitdir}/paths.target.wants/focus-server.path %{_unitdir}/focus-server.service %{_unitdir}/focus-server.path +%{_datadir}/dbus-1/system-services/org.tizen.AudioAec.service +%if "%{tizen_profile_name}" != "tv" +%{_bindir}/audio_aec +%{_unitdir}/audio-aec.service +%endif + %license LICENSE.APLv2 /etc/dbus-1/system.d/focus-server.conf +%if "%{tizen_profile_name}" != "tv" +/etc/dbus-1/system.d/audio-aec.conf +%endif %if 0%{?gtests:1} %{_bindir}/gtest-libmm-sound %endif diff --git a/packaging/org.tizen.AudioAec.service b/packaging/org.tizen.AudioAec.service new file mode 100644 index 0000000..14a9189 --- /dev/null +++ b/packaging/org.tizen.AudioAec.service @@ -0,0 +1,4 @@ +[D-BUS Service] +Name=org.tizen.AudioAec +Exec=/bin/false +SystemdService=audio-aec.service -- 2.7.4