X-Git-Url: https://code.delx.au/pulseaudio/blobdiff_plain/af6229cbe1c0040701095eced39729c72da9c125..e8028304b30c359ee4c0e97fb57a0f6e985227e8:/src/modules/echo-cancel/module-echo-cancel.c diff --git a/src/modules/echo-cancel/module-echo-cancel.c b/src/modules/echo-cancel/module-echo-cancel.c index 04e5b028..64e17bff 100644 --- a/src/modules/echo-cancel/module-echo-cancel.c +++ b/src/modules/echo-cancel/module-echo-cancel.c @@ -31,6 +31,7 @@ #endif #include +#include #include "echo-cancel.h" @@ -66,19 +67,16 @@ PA_MODULE_USAGE( "sink_properties= " "sink_master= " "adjust_time= " + "adjust_threshold= " "format= " "rate= " "channels= " "channel_map= " "aec_method= " "aec_args= " - "agc= " - "denoise= " - "echo_suppress= " - "echo_suppress_attenuation= " - "echo_suppress_attenuation_active= " "save_aec= " "autoloaded= " + "use_volume_sharing= " )); /* NOTE: Make sure the enum and ec_table are maintained in the correct order */ @@ -86,9 +84,16 @@ typedef enum { PA_ECHO_CANCELLER_INVALID = -1, PA_ECHO_CANCELLER_SPEEX = 0, PA_ECHO_CANCELLER_ADRIAN, +#ifdef HAVE_WEBRTC + PA_ECHO_CANCELLER_WEBRTC, +#endif } pa_echo_canceller_method_t; +#if HAVE_WEBRTC +#define DEFAULT_ECHO_CANCELLER "webrtc" +#else #define DEFAULT_ECHO_CANCELLER "speex" +#endif static const pa_echo_canceller ec_table[] = { { @@ -103,20 +108,32 @@ static const pa_echo_canceller ec_table[] = { .run = pa_adrian_ec_run, .done = pa_adrian_ec_done, }, +#ifdef HAVE_WEBRTC + { + /* WebRTC's audio processing engine */ + .init = pa_webrtc_ec_init, + .play = pa_webrtc_ec_play, + .record = pa_webrtc_ec_record, + .set_drift = pa_webrtc_ec_set_drift, + .run = pa_webrtc_ec_run, + .done = pa_webrtc_ec_done, + }, +#endif }; #define DEFAULT_RATE 32000 #define DEFAULT_CHANNELS 1 #define DEFAULT_ADJUST_TIME_USEC (1*PA_USEC_PER_SEC) -#define DEFAULT_AGC_ENABLED TRUE -#define DEFAULT_DENOISE_ENABLED TRUE -#define DEFAULT_ECHO_SUPPRESS_ENABLED TRUE -#define DEFAULT_ECHO_SUPPRESS_ATTENUATION 0 -#define DEFAULT_SAVE_AEC 0 +#define DEFAULT_ADJUST_TOLERANCE (5*PA_USEC_PER_MSEC) +#define DEFAULT_SAVE_AEC FALSE #define DEFAULT_AUTOLOADED FALSE #define MEMBLOCKQ_MAXLENGTH (16*1024*1024) +/* Can only be used in main context */ +#define IS_ACTIVE(u) ((pa_source_get_state((u)->source) == PA_SOURCE_RUNNING) && \ + (pa_sink_get_state((u)->sink) == PA_SINK_RUNNING)) + /* This module creates a new (virtual) source and sink. * * The data sent to the new sink is kept in a memblockq before being @@ -146,6 +163,16 @@ static const pa_echo_canceller ec_table[] = { * don't give enough accuracy to be able to do that right now. */ +struct userdata; + +struct pa_echo_canceller_msg { + pa_msgobject parent; + struct userdata *userdata; +}; + +PA_DEFINE_PRIVATE_CLASS(pa_echo_canceller_msg, pa_msgobject); +#define PA_ECHO_CANCELLER_MSG(o) (pa_echo_canceller_msg_cast(o)) + struct snapshot { pa_usec_t sink_now; pa_usec_t sink_latency; @@ -165,7 +192,8 @@ struct userdata { pa_module *module; pa_bool_t autoloaded; - uint32_t save_aec; + pa_bool_t dead; + pa_bool_t save_aec; pa_echo_canceller *ec; uint32_t blocksize; @@ -173,7 +201,6 @@ struct userdata { pa_bool_t need_realign; /* to wakeup the source I/O thread */ - pa_bool_t in_push; pa_asyncmsgq *asyncmsgq; pa_rtpoll_item *rtpoll_item_read, *rtpoll_item_write; @@ -191,15 +218,26 @@ struct userdata { int64_t recv_counter; size_t sink_skip; + /* Bytes left over from previous iteration */ + size_t sink_rem; + size_t source_rem; + pa_atomic_t request_resync; - int active_mask; pa_time_event *time_event; pa_usec_t adjust_time; + int adjust_threshold; FILE *captured_file; FILE *played_file; FILE *canceled_file; + FILE *drift_file; + + pa_bool_t use_volume_sharing; + + struct { + pa_cvolume current_volume; + } thread_info; }; static void source_output_snapshot_within_thread(struct userdata *u, struct snapshot *snapshot); @@ -212,19 +250,16 @@ static const char* const valid_modargs[] = { "sink_properties", "sink_master", "adjust_time", + "adjust_threshold", "format", "rate", "channels", "channel_map", "aec_method", "aec_args", - "agc", - "denoise", - "echo_suppress", - "echo_suppress_attenuation", - "echo_suppress_attenuation_active", "save_aec", "autoloaded", + "use_volume_sharing", NULL }; @@ -239,6 +274,10 @@ enum { SINK_INPUT_MESSAGE_LATENCY_SNAPSHOT }; +enum { + ECHO_CANCELLER_MESSAGE_SET_VOLUME, +}; + static int64_t calc_diff(struct userdata *u, struct snapshot *snapshot) { int64_t buffer, diff_time, buffer_latency; @@ -286,7 +325,7 @@ static void time_callback(pa_mainloop_api *a, pa_time_event *e, const struct tim pa_assert(u->time_event == e); pa_assert_ctl_context(); - if (u->active_mask != 3) + if (!IS_ACTIVE(u)) return; /* update our snapshots */ @@ -309,7 +348,7 @@ static void time_callback(pa_mainloop_api *a, pa_time_event *e, const struct tim new_rate = base_rate; } else { - if (diff_time > 1000) { + if (diff_time > u->adjust_threshold) { /* diff too big, quickly adjust */ pa_asyncmsgq_post(u->asyncmsgq, PA_MSGOBJECT(u->source_output), SOURCE_OUTPUT_MESSAGE_APPLY_DIFF_TIME, NULL, diff_time, NULL, NULL); @@ -363,6 +402,9 @@ static int source_process_msg_cb(pa_msgobject *o, int code, void *data, int64_t return 0; + case PA_SOURCE_MESSAGE_SET_VOLUME_SYNCED: + u->thread_info.current_volume = u->source->reference_volume; + break; } return pa_source_process_msg(o, code, data, offset, chunk); @@ -411,20 +453,17 @@ static int source_set_state_cb(pa_source *s, pa_source_state_t state) { !PA_SOURCE_OUTPUT_IS_LINKED(pa_source_output_get_state(u->source_output))) return 0; - pa_log_debug("Source state %d %d", state, u->active_mask); - if (state == PA_SOURCE_RUNNING) { /* restart timer when both sink and source are active */ - u->active_mask |= 1; - if (u->active_mask == 3) + if (IS_ACTIVE(u) && u->adjust_time) pa_core_rttime_restart(u->core, u->time_event, pa_rtclock_now() + u->adjust_time); pa_atomic_store(&u->request_resync, 1); pa_source_output_cork(u->source_output, FALSE); } else if (state == PA_SOURCE_SUSPENDED) { - u->active_mask &= ~1; pa_source_output_cork(u->source_output, TRUE); } + return 0; } @@ -439,20 +478,17 @@ static int sink_set_state_cb(pa_sink *s, pa_sink_state_t state) { !PA_SINK_INPUT_IS_LINKED(pa_sink_input_get_state(u->sink_input))) return 0; - pa_log_debug("Sink state %d %d", state, u->active_mask); - if (state == PA_SINK_RUNNING) { /* restart timer when both sink and source are active */ - u->active_mask |= 2; - if (u->active_mask == 3) + if (IS_ACTIVE(u) && u->adjust_time) pa_core_rttime_restart(u->core, u->time_event, pa_rtclock_now() + u->adjust_time); pa_atomic_store(&u->request_resync, 1); pa_sink_input_cork(u->sink_input, FALSE); } else if (state == PA_SINK_SUSPENDED) { - u->active_mask &= ~2; pa_sink_input_cork(u->sink_input, TRUE); } + return 0; } @@ -650,60 +686,120 @@ static void do_resync(struct userdata *u) { apply_diff_time(u, diff_time); } -/* Called from input thread context */ -static void source_output_push_cb(pa_source_output *o, const pa_memchunk *chunk) { - struct userdata *u; +/* 1. Calculate drift at this point, pass to canceller + * 2. Push out playback samples in blocksize chunks + * 3. Push out capture samples in blocksize chunks + * 4. ??? + * 5. Profit + */ +static void do_push_drift_comp(struct userdata *u) { size_t rlen, plen; + pa_memchunk rchunk, pchunk, cchunk; + uint8_t *rdata, *pdata, *cdata; + float drift; + int unused PA_GCC_UNUSED; - pa_source_output_assert_ref(o); - pa_source_output_assert_io_context(o); - pa_assert_se(u = o->userdata); + rlen = pa_memblockq_get_length(u->source_memblockq); + plen = pa_memblockq_get_length(u->sink_memblockq); - if (!PA_SOURCE_OUTPUT_IS_LINKED(pa_source_output_get_state(u->source_output))) { - pa_log("push when no link?"); - return; + /* Estimate snapshot drift as follows: + * pd: amount of data consumed since last time + * rd: amount of data consumed since last time + * + * drift = (pd - rd) / rd; + * + * We calculate pd and rd as the memblockq length less the number of + * samples left from the last iteration (to avoid double counting + * those remainder samples. + */ + drift = ((float)(plen - u->sink_rem) - (rlen - u->source_rem)) / ((float)(rlen - u->source_rem)); + u->sink_rem = plen % u->blocksize; + u->source_rem = rlen % u->blocksize; + + /* Now let the canceller work its drift compensation magic */ + u->ec->set_drift(u->ec, drift); + + if (u->save_aec) { + if (u->drift_file) + fprintf(u->drift_file, "d %a\n", drift); } - /* handle queued messages */ - u->in_push = TRUE; - while (pa_asyncmsgq_process_one(u->asyncmsgq) > 0) - ; - u->in_push = FALSE; + /* Send in the playback samples first */ + while (plen >= u->blocksize) { + pa_memblockq_peek_fixed_size(u->sink_memblockq, u->blocksize, &pchunk); + pdata = pa_memblock_acquire(pchunk.memblock); + pdata += pchunk.index; - if (pa_atomic_cmpxchg (&u->request_resync, 1, 0)) { - do_resync(u); - } + u->ec->play(u->ec, pdata); - pa_memblockq_push_align(u->source_memblockq, chunk); + if (u->save_aec) { + if (u->drift_file) + fprintf(u->drift_file, "p %d\n", u->blocksize); + if (u->played_file) + unused = fwrite(pdata, 1, u->blocksize, u->played_file); + } - rlen = pa_memblockq_get_length(u->source_memblockq); - plen = pa_memblockq_get_length(u->sink_memblockq); + pa_memblock_release(pchunk.memblock); + pa_memblockq_drop(u->sink_memblockq, u->blocksize); + pa_memblock_unref(pchunk.memblock); - while (rlen >= u->blocksize) { - pa_memchunk rchunk, pchunk; + plen -= u->blocksize; + } - /* take fixed block from recorded samples */ + /* And now the capture samples */ + while (rlen >= u->blocksize) { pa_memblockq_peek_fixed_size(u->source_memblockq, u->blocksize, &rchunk); - if (plen > u->blocksize && u->source_skip == 0) { - uint8_t *rdata, *pdata, *cdata; - pa_memchunk cchunk; + rdata = pa_memblock_acquire(rchunk.memblock); + rdata += rchunk.index; - if (u->sink_skip) { - size_t to_skip; + cchunk.index = 0; + cchunk.length = u->blocksize; + cchunk.memblock = pa_memblock_new(u->source->core->mempool, cchunk.length); + cdata = pa_memblock_acquire(cchunk.memblock); - if (u->sink_skip > plen) - to_skip = plen; - else - to_skip = u->sink_skip; + u->ec->record(u->ec, rdata, cdata); - pa_memblockq_drop(u->sink_memblockq, to_skip); - plen -= to_skip; + if (u->save_aec) { + if (u->drift_file) + fprintf(u->drift_file, "c %d\n", u->blocksize); + if (u->captured_file) + unused = fwrite(rdata, 1, u->blocksize, u->captured_file); + if (u->canceled_file) + unused = fwrite(cdata, 1, u->blocksize, u->canceled_file); + } - u->sink_skip -= to_skip; - } + pa_memblock_release(cchunk.memblock); + pa_memblock_release(rchunk.memblock); + + pa_memblock_unref(rchunk.memblock); + + pa_source_post(u->source, &cchunk); + pa_memblock_unref(cchunk.memblock); + + pa_memblockq_drop(u->source_memblockq, u->blocksize); + rlen -= u->blocksize; + } +} - if (plen > u->blocksize && u->sink_skip == 0) { +/* This one's simpler than the drift compensation case -- we just iterate over + * the capture buffer, and pass the canceller blocksize bytes of playback and + * capture data. */ +static void do_push(struct userdata *u) { + size_t rlen, plen; + pa_memchunk rchunk, pchunk, cchunk; + uint8_t *rdata, *pdata, *cdata; + int unused PA_GCC_UNUSED; + + rlen = pa_memblockq_get_length(u->source_memblockq); + plen = pa_memblockq_get_length(u->sink_memblockq); + + while (rlen >= u->blocksize) { + /* take fixed block from recorded samples */ + pa_memblockq_peek_fixed_size(u->source_memblockq, u->blocksize, &rchunk); + + if (plen > u->blocksize) { + if (plen > u->blocksize) { /* take fixed block from played samples */ pa_memblockq_peek_fixed_size(u->sink_memblockq, u->blocksize, &pchunk); @@ -719,21 +815,17 @@ static void source_output_push_cb(pa_source_output *o, const pa_memchunk *chunk) if (u->save_aec) { if (u->captured_file) - fwrite(rdata, 1, u->blocksize, u->captured_file); + unused = fwrite(rdata, 1, u->blocksize, u->captured_file); if (u->played_file) - fwrite(pdata, 1, u->blocksize, u->played_file); + unused = fwrite(pdata, 1, u->blocksize, u->played_file); } /* perform echo cancellation */ u->ec->run(u->ec, rdata, pdata, cdata); - /* preprecessor is run after AEC. This is not a mistake! */ - if (u->ec->pp_state) - speex_preprocess_run(u->ec->pp_state, (spx_int16_t *) cdata); - if (u->save_aec) { if (u->canceled_file) - fwrite(cdata, 1, u->blocksize, u->canceled_file); + unused = fwrite(cdata, 1, u->blocksize, u->canceled_file); } pa_memblock_release(cchunk.memblock); @@ -759,17 +851,91 @@ static void source_output_push_cb(pa_source_output *o, const pa_memchunk *chunk) pa_memblockq_drop(u->source_memblockq, u->blocksize); rlen -= u->blocksize; + } +} - if (u->source_skip) { - if (u->source_skip > u->blocksize) { - u->source_skip -= u->blocksize; - } - else { - u->sink_skip += (u->blocksize - u->source_skip); - u->source_skip = 0; - } +/* Called from input thread context */ +static void source_output_push_cb(pa_source_output *o, const pa_memchunk *chunk) { + struct userdata *u; + size_t rlen, plen, to_skip; + pa_memchunk rchunk; + + pa_source_output_assert_ref(o); + pa_source_output_assert_io_context(o); + pa_assert_se(u = o->userdata); + + if (!PA_SOURCE_OUTPUT_IS_LINKED(pa_source_output_get_state(u->source_output))) { + pa_log("push when no link?"); + return; + } + + if (PA_UNLIKELY(u->source->thread_info.state != PA_SOURCE_RUNNING || + u->sink->thread_info.state != PA_SINK_RUNNING)) { + pa_source_post(u->source, chunk); + return; + } + + /* handle queued messages, do any message sending of our own */ + while (pa_asyncmsgq_process_one(u->asyncmsgq) > 0) + ; + + pa_memblockq_push_align(u->source_memblockq, chunk); + + rlen = pa_memblockq_get_length(u->source_memblockq); + plen = pa_memblockq_get_length(u->sink_memblockq); + + /* Let's not do anything else till we have enough data to process */ + if (rlen < u->blocksize) + return; + + /* See if we need to drop samples in order to sync */ + if (pa_atomic_cmpxchg (&u->request_resync, 1, 0)) { + do_resync(u); + } + + /* Okay, skip cancellation for skipped source samples if needed. */ + if (PA_UNLIKELY(u->source_skip)) { + /* The slightly tricky bit here is that we drop all but modulo + * blocksize bytes and then adjust for that last bit on the sink side. + * We do this because the source data is coming at a fixed rate, which + * means the only way to try to catch up is drop sink samples and let + * the canceller cope up with this. */ + to_skip = rlen >= u->source_skip ? u->source_skip : rlen; + to_skip -= to_skip % u->blocksize; + + if (to_skip) { + pa_memblockq_peek_fixed_size(u->source_memblockq, to_skip, &rchunk); + pa_source_post(u->source, &rchunk); + + pa_memblock_unref(rchunk.memblock); + pa_memblockq_drop(u->source_memblockq, u->blocksize); + + rlen -= to_skip; + u->source_skip -= to_skip; + } + + if (rlen && u->source_skip % u->blocksize) { + u->sink_skip += u->blocksize - (u->source_skip % u->blocksize); + u->source_skip -= (u->source_skip % u->blocksize); } } + + /* And for the sink, these samples have been played back already, so we can + * just drop them and get on with it. */ + if (PA_UNLIKELY(u->sink_skip)) { + to_skip = plen >= u->sink_skip ? u->sink_skip : plen; + + pa_memblockq_drop(u->sink_memblockq, to_skip); + + plen -= to_skip; + u->sink_skip -= to_skip; + } + + /* process and push out samples */ + if (u->ec->params.drift_compensation) + do_push_drift_comp(u); + else + do_push(u); } /* Called from I/O thread context */ @@ -865,7 +1031,7 @@ static int source_output_process_msg_cb(pa_msgobject *obj, int code, void *data, pa_source_output_assert_io_context(u->source_output); - if (PA_SOURCE_IS_OPENED(u->source_output->source->thread_info.state)) + if (u->source_output->source->thread_info.state == PA_SOURCE_RUNNING) pa_memblockq_push_align(u->sink_memblockq, chunk); else pa_memblockq_flush_write(u->sink_memblockq, TRUE); @@ -1064,7 +1230,7 @@ static void source_output_attach_cb(pa_source_output *o) { pa_source_set_fixed_latency_within_thread(u->source, o->source->thread_info.fixed_latency); pa_source_set_max_rewind_within_thread(u->source, pa_source_output_get_max_rewind(o)); - pa_log_debug("Source output %p attach", o); + pa_log_debug("Source output %d attach", o->index); pa_source_attach_within_thread(u->source); @@ -1094,7 +1260,7 @@ static void sink_input_attach_cb(pa_sink_input *i) { pa_sink_set_max_request_within_thread(u->sink, pa_sink_input_get_max_request(i)); pa_sink_set_max_rewind_within_thread(u->sink, pa_sink_input_get_max_rewind(i)); - pa_log_debug("Sink input %p attach", i); + pa_log_debug("Sink input %d attach", i->index); u->rtpoll_item_write = pa_rtpoll_item_new_asyncmsgq_write( i->sink->thread_info.rtpoll, @@ -1116,7 +1282,7 @@ static void source_output_detach_cb(pa_source_output *o) { pa_source_detach_within_thread(u->source); pa_source_set_rtpoll(u->source, NULL); - pa_log_debug("Source output %p detach", o); + pa_log_debug("Source output %d detach", o->index); if (u->rtpoll_item_read) { pa_rtpoll_item_free(u->rtpoll_item_read); @@ -1135,7 +1301,7 @@ static void sink_input_detach_cb(pa_sink_input *i) { pa_sink_set_rtpoll(u->sink, NULL); - pa_log_debug("Sink input %p detach", i); + pa_log_debug("Sink input %d detach", i->index); if (u->rtpoll_item_write) { pa_rtpoll_item_free(u->rtpoll_item_write); @@ -1151,7 +1317,7 @@ static void source_output_state_change_cb(pa_source_output *o, pa_source_output_ pa_source_output_assert_io_context(o); pa_assert_se(u = o->userdata); - pa_log_debug("Source output %p state %d", o, state); + pa_log_debug("Source output %d state %d", o->index, state); } /* Called from IO thread context */ @@ -1161,7 +1327,7 @@ static void sink_input_state_change_cb(pa_sink_input *i, pa_sink_input_state_t s pa_sink_input_assert_ref(i); pa_assert_se(u = i->userdata); - pa_log_debug("Sink input %p state %d", i, state); + pa_log_debug("Sink input %d state %d", i->index, state); /* If we are added for the first time, ask for a rewinding so that * we are heard right-away. */ @@ -1180,6 +1346,8 @@ static void source_output_kill_cb(pa_source_output *o) { pa_assert_ctl_context(); pa_assert_se(u = o->userdata); + u->dead = TRUE; + /* The order here matters! We first kill the source output, followed * by the source. That means the source callbacks must be protected * against an unconnected source output! */ @@ -1192,7 +1360,7 @@ static void source_output_kill_cb(pa_source_output *o) { pa_source_unref(u->source); u->source = NULL; - pa_log_debug("Source output kill %p", o); + pa_log_debug("Source output kill %d", o->index); pa_module_unload_request(u->module, TRUE); } @@ -1204,6 +1372,8 @@ static void sink_input_kill_cb(pa_sink_input *i) { pa_sink_input_assert_ref(i); pa_assert_se(u = i->userdata); + u->dead = TRUE; + /* The order here matters! We first kill the sink input, followed * by the sink. That means the sink callbacks must be protected * against an unconnected sink input! */ @@ -1216,7 +1386,7 @@ static void sink_input_kill_cb(pa_sink_input *i) { pa_sink_unref(u->sink); u->sink = NULL; - pa_log_debug("Sink input kill %p", i); + pa_log_debug("Sink input kill %d", i->index); pa_module_unload_request(u->module, TRUE); } @@ -1229,6 +1399,9 @@ static pa_bool_t source_output_may_move_to_cb(pa_source_output *o, pa_source *de pa_assert_ctl_context(); pa_assert_se(u = o->userdata); + if (u->dead || u->autoloaded) + return FALSE; + return (u->source != dest) && (u->sink != dest->monitor_of); } @@ -1239,6 +1412,9 @@ static pa_bool_t sink_input_may_move_to_cb(pa_sink_input *i, pa_sink *dest) { pa_sink_input_assert_ref(i); pa_assert_se(u = i->userdata); + if (u->dead || u->autoloaded) + return FALSE; + return u->sink != dest; } @@ -1257,13 +1433,14 @@ static void source_output_moving_cb(pa_source_output *o, pa_source *dest) { pa_source_set_asyncmsgq(u->source, NULL); if (u->source_auto_desc && dest) { - const char *z; + const char *y, *z; pa_proplist *pl; pl = pa_proplist_new(); + y = pa_proplist_gets(u->sink_input->sink->proplist, PA_PROP_DEVICE_DESCRIPTION); z = pa_proplist_gets(dest->proplist, PA_PROP_DEVICE_DESCRIPTION); - pa_proplist_setf(pl, PA_PROP_DEVICE_DESCRIPTION, "Echo-Cancel Source %s on %s", - pa_proplist_gets(u->source->proplist, "device.echo-cancel.name"), z ? z : dest->name); + pa_proplist_setf(pl, PA_PROP_DEVICE_DESCRIPTION, "%s (echo cancelled with %s)", z ? z : dest->name, + y ? y : u->sink_input->sink->name); pa_source_update_proplist(u->source, PA_UPDATE_REPLACE, pl); pa_proplist_free(pl); @@ -1284,13 +1461,14 @@ static void sink_input_moving_cb(pa_sink_input *i, pa_sink *dest) { pa_sink_set_asyncmsgq(u->sink, NULL); if (u->sink_auto_desc && dest) { - const char *z; + const char *y, *z; pa_proplist *pl; pl = pa_proplist_new(); + y = pa_proplist_gets(u->source_output->source->proplist, PA_PROP_DEVICE_DESCRIPTION); z = pa_proplist_gets(dest->proplist, PA_PROP_DEVICE_DESCRIPTION); - pa_proplist_setf(pl, PA_PROP_DEVICE_DESCRIPTION, "Echo-Cancel Sink %s on %s", - pa_proplist_gets(u->sink->proplist, "device.echo-cancel.name"), z ? z : dest->name); + pa_proplist_setf(pl, PA_PROP_DEVICE_DESCRIPTION, "%s (echo cancelled with %s)", z ? z : dest->name, + y ? y : u->source_output->source->name); pa_sink_update_proplist(u->sink, PA_UPDATE_REPLACE, pl); pa_proplist_free(pl); @@ -1317,15 +1495,98 @@ static void sink_input_mute_changed_cb(pa_sink_input *i) { pa_sink_mute_changed(u->sink, i->muted); } +/* Called from main context */ +static int canceller_process_msg_cb(pa_msgobject *o, int code, void *userdata, int64_t offset, pa_memchunk *chunk) { + struct pa_echo_canceller_msg *msg; + struct userdata *u; + + pa_assert(o); + + msg = PA_ECHO_CANCELLER_MSG(o); + u = msg->userdata; + + switch (code) { + case ECHO_CANCELLER_MESSAGE_SET_VOLUME: { + pa_cvolume *v = (pa_cvolume *) userdata; + + if (u->use_volume_sharing) + pa_source_set_volume(u->source, v, TRUE, FALSE); + else + pa_source_output_set_volume(u->source_output, v, FALSE, TRUE); + + break; + } + + default: + pa_assert_not_reached(); + break; + } + + return 0; +} + +/* Called by the canceller, so thread context */ +void pa_echo_canceller_get_capture_volume(pa_echo_canceller *ec, pa_cvolume *v) { + *v = ec->msg->userdata->thread_info.current_volume; +} + +/* Called by the canceller, so thread context */ +void pa_echo_canceller_set_capture_volume(pa_echo_canceller *ec, pa_cvolume *v) { + if (!pa_cvolume_equal(&ec->msg->userdata->thread_info.current_volume, v)) { + pa_cvolume *vol = pa_xnewdup(pa_cvolume, v, 1); + + pa_asyncmsgq_post(pa_thread_mq_get()->outq, PA_MSGOBJECT(ec->msg), ECHO_CANCELLER_MESSAGE_SET_VOLUME, vol, 0, NULL, + pa_xfree); + } +} + static pa_echo_canceller_method_t get_ec_method_from_string(const char *method) { if (pa_streq(method, "speex")) return PA_ECHO_CANCELLER_SPEEX; else if (pa_streq(method, "adrian")) return PA_ECHO_CANCELLER_ADRIAN; +#ifdef HAVE_WEBRTC + else if (pa_streq(method, "webrtc")) + return PA_ECHO_CANCELLER_WEBRTC; +#endif else return PA_ECHO_CANCELLER_INVALID; } +/* Common initialisation bits between module-echo-cancel and the standalone test program */ +static int init_common(pa_modargs *ma, struct userdata *u, pa_sample_spec *source_ss, pa_channel_map *source_map) { + pa_echo_canceller_method_t ec_method; + + if (pa_modargs_get_sample_spec_and_channel_map(ma, source_ss, source_map, PA_CHANNEL_MAP_DEFAULT) < 0) { + pa_log("Invalid sample format specification or channel map"); + goto fail; + } + + u->ec = pa_xnew0(pa_echo_canceller, 1); + if (!u->ec) { + pa_log("Failed to alloc echo canceller"); + goto fail; + } + + if ((ec_method = get_ec_method_from_string(pa_modargs_get_value(ma, "aec_method", DEFAULT_ECHO_CANCELLER))) < 0) { + pa_log("Invalid echo canceller implementation"); + goto fail; + } + + u->ec->init = ec_table[ec_method].init; + u->ec->play = ec_table[ec_method].play; + u->ec->record = ec_table[ec_method].record; + u->ec->set_drift = ec_table[ec_method].set_drift; + u->ec->run = ec_table[ec_method].run; + u->ec->done = ec_table[ec_method].done; + + return 0; + +fail: + return -1; +} + + int pa__init(pa_module*m) { struct userdata *u; pa_sample_spec source_ss, sink_ss; @@ -1338,8 +1599,7 @@ int pa__init(pa_module*m) { pa_source_new_data source_data; pa_sink_new_data sink_data; pa_memchunk silence; - pa_echo_canceller_method_t ec_method; - uint32_t adjust_time_sec; + uint32_t temp; pa_assert(m); @@ -1360,14 +1620,15 @@ int pa__init(pa_module*m) { } pa_assert(sink_master); + if (source_master->monitor_of == sink_master) { + pa_log("Can't cancel echo between a sink and its monitor"); + goto fail; + } + source_ss = source_master->sample_spec; source_ss.rate = DEFAULT_RATE; source_ss.channels = DEFAULT_CHANNELS; pa_channel_map_init_auto(&source_map, source_ss.channels, PA_CHANNEL_MAP_DEFAULT); - if (pa_modargs_get_sample_spec_and_channel_map(ma, &source_ss, &source_map, PA_CHANNEL_MAP_DEFAULT) < 0) { - pa_log("Invalid sample format specification or channel map"); - goto fail; - } sink_ss = sink_master->sample_spec; sink_map = sink_master->channel_map; @@ -1380,77 +1641,38 @@ int pa__init(pa_module*m) { u->core = m->core; u->module = m; m->userdata = u; + u->dead = FALSE; - u->ec = pa_xnew0(pa_echo_canceller, 1); - if (!u->ec) { - pa_log("Failed to alloc echo canceller"); + u->use_volume_sharing = TRUE; + if (pa_modargs_get_value_boolean(ma, "use_volume_sharing", &u->use_volume_sharing) < 0) { + pa_log("use_volume_sharing= expects a boolean argument"); goto fail; } - if ((ec_method = get_ec_method_from_string(pa_modargs_get_value(ma, "aec_method", DEFAULT_ECHO_CANCELLER))) < 0) { - pa_log("Invalid echo canceller implementation"); - goto fail; - } - - u->ec->init = ec_table[ec_method].init; - u->ec->run = ec_table[ec_method].run; - u->ec->done = ec_table[ec_method].done; - - adjust_time_sec = DEFAULT_ADJUST_TIME_USEC / PA_USEC_PER_SEC; - if (pa_modargs_get_value_u32(ma, "adjust_time", &adjust_time_sec) < 0) { + temp = DEFAULT_ADJUST_TIME_USEC / PA_USEC_PER_SEC; + if (pa_modargs_get_value_u32(ma, "adjust_time", &temp) < 0) { pa_log("Failed to parse adjust_time value"); goto fail; } - if (adjust_time_sec != DEFAULT_ADJUST_TIME_USEC / PA_USEC_PER_SEC) - u->adjust_time = adjust_time_sec * PA_USEC_PER_SEC; + if (temp != DEFAULT_ADJUST_TIME_USEC / PA_USEC_PER_SEC) + u->adjust_time = temp * PA_USEC_PER_SEC; else u->adjust_time = DEFAULT_ADJUST_TIME_USEC; - u->ec->agc = DEFAULT_AGC_ENABLED; - if (pa_modargs_get_value_boolean(ma, "agc", &u->ec->agc) < 0) { - pa_log("Failed to parse agc value"); + temp = DEFAULT_ADJUST_TOLERANCE / PA_USEC_PER_MSEC; + if (pa_modargs_get_value_u32(ma, "adjust_threshold", &temp) < 0) { + pa_log("Failed to parse adjust_threshold value"); goto fail; } - u->ec->denoise = DEFAULT_DENOISE_ENABLED; - if (pa_modargs_get_value_boolean(ma, "denoise", &u->ec->denoise) < 0) { - pa_log("Failed to parse denoise value"); - goto fail; - } - - u->ec->echo_suppress = DEFAULT_ECHO_SUPPRESS_ENABLED; - if (pa_modargs_get_value_boolean(ma, "echo_suppress", &u->ec->echo_suppress) < 0) { - pa_log("Failed to parse echo_suppress value"); - goto fail; - } - if (u->ec->echo_suppress && ec_method != PA_ECHO_CANCELLER_SPEEX) { - pa_log("Echo suppression is only useful with the speex canceller"); - goto fail; - } - - u->ec->echo_suppress_attenuation = DEFAULT_ECHO_SUPPRESS_ATTENUATION; - if (pa_modargs_get_value_s32(ma, "echo_suppress_attenuation", &u->ec->echo_suppress_attenuation) < 0) { - pa_log("Failed to parse echo_suppress_attenuation value"); - goto fail; - } - if (u->ec->echo_suppress_attenuation > 0) { - pa_log("echo_suppress_attenuation should be a negative dB value"); - goto fail; - } - - u->ec->echo_suppress_attenuation_active = DEFAULT_ECHO_SUPPRESS_ATTENUATION; - if (pa_modargs_get_value_s32(ma, "echo_suppress_attenuation_active", &u->ec->echo_suppress_attenuation_active) < 0) { - pa_log("Failed to parse echo_suppress_attenuation_active value"); - goto fail; - } - if (u->ec->echo_suppress_attenuation_active > 0) { - pa_log("echo_suppress_attenuation_active should be a negative dB value"); - goto fail; - } + if (temp != DEFAULT_ADJUST_TOLERANCE / PA_USEC_PER_MSEC) + u->adjust_threshold = temp * PA_USEC_PER_MSEC; + else + u->adjust_threshold = DEFAULT_ADJUST_TOLERANCE; u->save_aec = DEFAULT_SAVE_AEC; - if (pa_modargs_get_value_u32(ma, "save_aec", &u->save_aec) < 0) { + if (pa_modargs_get_value_boolean(ma, "save_aec", &u->save_aec) < 0) { pa_log("Failed to parse save_aec value"); goto fail; } @@ -1461,8 +1683,12 @@ int pa__init(pa_module*m) { goto fail; } + if (init_common(ma, u, &source_ss, &source_map)) + goto fail; + u->asyncmsgq = pa_asyncmsgq_new(0); u->need_realign = TRUE; + if (u->ec->init) { if (!u->ec->init(u->core, u->ec, &source_ss, &source_map, &sink_ss, &sink_map, &u->blocksize, pa_modargs_get_value(ma, "aec_args", NULL))) { pa_log("Failed to init AEC engine"); @@ -1470,30 +1696,8 @@ int pa__init(pa_module*m) { } } - if (u->ec->agc || u->ec->denoise || u->ec->echo_suppress) { - spx_int32_t tmp; - - if (source_ss.channels != 1) { - pa_log("AGC, denoising and echo suppression only work with channels=1"); - goto fail; - } - - u->ec->pp_state = speex_preprocess_state_init(u->blocksize / pa_frame_size(&source_ss), source_ss.rate); - - tmp = u->ec->agc; - speex_preprocess_ctl(u->ec->pp_state, SPEEX_PREPROCESS_SET_AGC, &tmp); - tmp = u->ec->denoise; - speex_preprocess_ctl(u->ec->pp_state, SPEEX_PREPROCESS_SET_DENOISE, &tmp); - if (u->ec->echo_suppress) { - if (u->ec->echo_suppress_attenuation) - speex_preprocess_ctl(u->ec->pp_state, SPEEX_PREPROCESS_SET_ECHO_SUPPRESS, &u->ec->echo_suppress_attenuation); - if (u->ec->echo_suppress_attenuation_active) { - speex_preprocess_ctl(u->ec->pp_state, SPEEX_PREPROCESS_SET_ECHO_SUPPRESS_ACTIVE, - &u->ec->echo_suppress_attenuation_active); - } - speex_preprocess_ctl(u->ec->pp_state, SPEEX_PREPROCESS_SET_ECHO_STATE, u->ec->params.priv.speex.state); - } - } + if (u->ec->params.drift_compensation) + pa_assert(u->ec->set_drift); /* Create source */ pa_source_new_data_init(&source_data); @@ -1507,7 +1711,6 @@ int pa__init(pa_module*m) { pa_proplist_sets(source_data.proplist, PA_PROP_DEVICE_CLASS, "filter"); if (!u->autoloaded) pa_proplist_sets(source_data.proplist, PA_PROP_DEVICE_INTENDED_ROLES, "phone"); - pa_proplist_sets(source_data.proplist, "device.echo-cancel.name", source_data.name); if (pa_modargs_get_proplist(ma, "source_properties", source_data.proplist, PA_UPDATE_REPLACE) < 0) { pa_log("Invalid properties"); @@ -1516,14 +1719,16 @@ int pa__init(pa_module*m) { } if ((u->source_auto_desc = !pa_proplist_contains(source_data.proplist, PA_PROP_DEVICE_DESCRIPTION))) { - const char *z; + const char *y, *z; + y = pa_proplist_gets(sink_master->proplist, PA_PROP_DEVICE_DESCRIPTION); z = pa_proplist_gets(source_master->proplist, PA_PROP_DEVICE_DESCRIPTION); - pa_proplist_setf(source_data.proplist, PA_PROP_DEVICE_DESCRIPTION, "Echo-Cancel Source %s on %s", source_data.name, z ? z : source_master->name); + pa_proplist_setf(source_data.proplist, PA_PROP_DEVICE_DESCRIPTION, "%s (echo cancelled with %s)", + z ? z : source_master->name, y ? y : sink_master->name); } - u->source = pa_source_new(m->core, &source_data, - (source_master->flags & (PA_SOURCE_LATENCY|PA_SOURCE_DYNAMIC_LATENCY))); + u->source = pa_source_new(m->core, &source_data, (source_master->flags & (PA_SOURCE_LATENCY | PA_SOURCE_DYNAMIC_LATENCY)) + | (u->use_volume_sharing ? PA_SOURCE_SHARE_VOLUME_WITH_MASTER : 0)); pa_source_new_data_done(&source_data); if (!u->source) { @@ -1534,11 +1739,13 @@ int pa__init(pa_module*m) { u->source->parent.process_msg = source_process_msg_cb; u->source->set_state = source_set_state_cb; u->source->update_requested_latency = source_update_requested_latency_cb; - pa_source_enable_decibel_volume(u->source, TRUE); - pa_source_set_get_volume_callback(u->source, source_get_volume_cb); - pa_source_set_set_volume_callback(u->source, source_set_volume_cb); pa_source_set_get_mute_callback(u->source, source_get_mute_cb); pa_source_set_set_mute_callback(u->source, source_set_mute_cb); + if (!u->use_volume_sharing) { + pa_source_set_get_volume_callback(u->source, source_get_volume_cb); + pa_source_set_set_volume_callback(u->source, source_set_volume_cb); + pa_source_enable_decibel_volume(u->source, TRUE); + } u->source->userdata = u; pa_source_set_asyncmsgq(u->source, source_master->asyncmsgq); @@ -1555,7 +1762,6 @@ int pa__init(pa_module*m) { pa_proplist_sets(sink_data.proplist, PA_PROP_DEVICE_CLASS, "filter"); if (!u->autoloaded) pa_proplist_sets(sink_data.proplist, PA_PROP_DEVICE_INTENDED_ROLES, "phone"); - pa_proplist_sets(sink_data.proplist, "device.echo-cancel.name", sink_data.name); if (pa_modargs_get_proplist(ma, "sink_properties", sink_data.proplist, PA_UPDATE_REPLACE) < 0) { pa_log("Invalid properties"); @@ -1564,14 +1770,16 @@ int pa__init(pa_module*m) { } if ((u->sink_auto_desc = !pa_proplist_contains(sink_data.proplist, PA_PROP_DEVICE_DESCRIPTION))) { - const char *z; + const char *y, *z; + y = pa_proplist_gets(source_master->proplist, PA_PROP_DEVICE_DESCRIPTION); z = pa_proplist_gets(sink_master->proplist, PA_PROP_DEVICE_DESCRIPTION); - pa_proplist_setf(sink_data.proplist, PA_PROP_DEVICE_DESCRIPTION, "Echo-Cancel Sink %s on %s", sink_data.name, z ? z : sink_master->name); + pa_proplist_setf(sink_data.proplist, PA_PROP_DEVICE_DESCRIPTION, "%s (echo cancelled with %s)", + z ? z : sink_master->name, y ? y : source_master->name); } - u->sink = pa_sink_new(m->core, &sink_data, - (sink_master->flags & (PA_SINK_LATENCY|PA_SINK_DYNAMIC_LATENCY))); + u->sink = pa_sink_new(m->core, &sink_data, (sink_master->flags & (PA_SINK_LATENCY | PA_SINK_DYNAMIC_LATENCY)) + | (u->use_volume_sharing ? PA_SINK_SHARE_VOLUME_WITH_MASTER : 0)); pa_sink_new_data_done(&sink_data); if (!u->sink) { @@ -1583,9 +1791,11 @@ int pa__init(pa_module*m) { u->sink->set_state = sink_set_state_cb; u->sink->update_requested_latency = sink_update_requested_latency_cb; u->sink->request_rewind = sink_request_rewind_cb; - pa_sink_enable_decibel_volume(u->sink, TRUE); - pa_sink_set_set_volume_callback(u->sink, sink_set_volume_cb); pa_sink_set_set_mute_callback(u->sink, sink_set_mute_cb); + if (!u->use_volume_sharing) { + pa_sink_set_set_volume_callback(u->sink, sink_set_volume_cb); + pa_sink_enable_decibel_volume(u->sink, TRUE); + } u->sink->userdata = u; pa_sink_set_asyncmsgq(u->sink, sink_master->asyncmsgq); @@ -1659,7 +1869,8 @@ int pa__init(pa_module*m) { u->sink_input->state_change = sink_input_state_change_cb; u->sink_input->may_move_to = sink_input_may_move_to_cb; u->sink_input->moving = sink_input_moving_cb; - u->sink_input->volume_changed = sink_input_volume_changed_cb; + if (!u->use_volume_sharing) + u->sink_input->volume_changed = sink_input_volume_changed_cb; u->sink_input->mute_changed = sink_input_mute_changed_cb; u->sink_input->userdata = u; @@ -1667,10 +1878,10 @@ int pa__init(pa_module*m) { pa_sink_input_get_silence(u->sink_input, &silence); - u->source_memblockq = pa_memblockq_new(0, MEMBLOCKQ_MAXLENGTH, 0, - pa_frame_size(&source_ss), 1, 1, 0, &silence); - u->sink_memblockq = pa_memblockq_new(0, MEMBLOCKQ_MAXLENGTH, 0, - pa_frame_size(&sink_ss), 1, 1, 0, &silence); + u->source_memblockq = pa_memblockq_new("module-echo-cancel source_memblockq", 0, MEMBLOCKQ_MAXLENGTH, 0, + &source_ss, 1, 1, 0, &silence); + u->sink_memblockq = pa_memblockq_new("module-echo-cancel sink_memblockq", 0, MEMBLOCKQ_MAXLENGTH, 0, + &sink_ss, 1, 1, 0, &silence); pa_memblock_unref(silence.memblock); @@ -1679,11 +1890,14 @@ int pa__init(pa_module*m) { goto fail; } - /* our source and sink are not suspended when we create them */ - u->active_mask = 3; - - if (u->adjust_time > 0) + if (u->adjust_time > 0 && !u->ec->params.drift_compensation) u->time_event = pa_core_rttime_new(m->core, pa_rtclock_now() + u->adjust_time, time_callback, u); + else if (u->ec->params.drift_compensation) { + pa_log_info("Canceller does drift compensation -- built-in compensation will be disabled"); + u->adjust_time = 0; + /* Perform resync just once to give the canceller a leg up */ + pa_atomic_store(&u->request_resync, 1); + } if (u->save_aec) { pa_log("Creating AEC files in /tmp"); @@ -1696,14 +1910,24 @@ int pa__init(pa_module*m) { u->canceled_file = fopen("/tmp/aec_out.sw", "wb"); if (u->canceled_file == NULL) perror ("fopen failed"); + if (u->ec->params.drift_compensation) { + u->drift_file = fopen("/tmp/aec_drift.txt", "w"); + if (u->drift_file == NULL) + perror ("fopen failed"); + } } + u->ec->msg = pa_msgobject_new(pa_echo_canceller_msg); + u->ec->msg->parent.process_msg = canceller_process_msg_cb; + u->ec->msg->userdata = u; + + u->thread_info.current_volume = u->source->reference_volume; + pa_sink_put(u->sink); pa_source_put(u->source); pa_sink_input_put(u->sink_input); pa_source_output_put(u->source_output); - pa_modargs_free(ma); return 0; @@ -1734,6 +1958,8 @@ void pa__done(pa_module*m) { if (!(u = m->userdata)) return; + u->dead = TRUE; + /* See comments in source_output_kill_cb() above regarding * destruction order! */ @@ -1765,9 +1991,6 @@ void pa__done(pa_module*m) { if (u->sink_memblockq) pa_memblockq_free(u->sink_memblockq); - if (u->ec->pp_state) - speex_preprocess_state_destroy(u->ec->pp_state); - if (u->ec) { if (u->ec->done) u->ec->done(u->ec); @@ -1778,5 +2001,193 @@ void pa__done(pa_module*m) { if (u->asyncmsgq) pa_asyncmsgq_unref(u->asyncmsgq); + if (u->save_aec) { + if (u->played_file) + fclose(u->played_file); + if (u->captured_file) + fclose(u->captured_file); + if (u->canceled_file) + fclose(u->canceled_file); + if (u->drift_file) + fclose(u->drift_file); + } + pa_xfree(u); } + +#ifdef ECHO_CANCEL_TEST +/* + * Stand-alone test program for running in the canceller on pre-recorded files. + */ +int main(int argc, char* argv[]) { + struct userdata u; + pa_sample_spec source_ss, sink_ss; + pa_channel_map source_map, sink_map; + pa_modargs *ma = NULL; + uint8_t *rdata = NULL, *pdata = NULL, *cdata = NULL; + int unused PA_GCC_UNUSED; + int ret = 0, i; + char c; + float drift; + + pa_memzero(&u, sizeof(u)); + + if (argc < 4 || argc > 7) { + goto usage; + } + + u.ec = pa_xnew0(pa_echo_canceller, 1); + if (!u.ec) { + pa_log("Failed to alloc echo canceller"); + goto fail; + } + + u.captured_file = fopen(argv[2], "r"); + if (u.captured_file == NULL) { + perror ("fopen failed"); + goto fail; + } + u.played_file = fopen(argv[1], "r"); + if (u.played_file == NULL) { + perror ("fopen failed"); + goto fail; + } + u.canceled_file = fopen(argv[3], "wb"); + if (u.canceled_file == NULL) { + perror ("fopen failed"); + goto fail; + } + + u.core = pa_xnew0(pa_core, 1); + u.core->cpu_info.cpu_type = PA_CPU_X86; + u.core->cpu_info.flags.x86 |= PA_CPU_X86_SSE; + + if (!(ma = pa_modargs_new(argc > 4 ? argv[4] : NULL, valid_modargs))) { + pa_log("Failed to parse module arguments."); + goto fail; + } + + source_ss.format = PA_SAMPLE_S16LE; + source_ss.rate = DEFAULT_RATE; + source_ss.channels = DEFAULT_CHANNELS; + pa_channel_map_init_auto(&source_map, source_ss.channels, PA_CHANNEL_MAP_DEFAULT); + + init_common(ma, &u, &source_ss, &source_map); + + if (!u.ec->init(u.core, u.ec, &source_ss, &source_map, &sink_ss, &sink_map, &u.blocksize, + (argc > 4) ? argv[5] : NULL )) { + pa_log("Failed to init AEC engine"); + goto fail; + } + + if (u.ec->params.drift_compensation) { + if (argc < 7) { + pa_log("Drift compensation enabled but drift file not specified"); + goto fail; + } + + u.drift_file = fopen(argv[6], "r"); + + if (u.drift_file == NULL) { + perror ("fopen failed"); + goto fail; + } + } + + rdata = pa_xmalloc(u.blocksize); + pdata = pa_xmalloc(u.blocksize); + cdata = pa_xmalloc(u.blocksize); + + if (!u.ec->params.drift_compensation) { + while (fread(rdata, u.blocksize, 1, u.captured_file) > 0) { + if (fread(pdata, u.blocksize, 1, u.played_file) == 0) { + perror("Played file ended before captured file"); + goto fail; + } + + u.ec->run(u.ec, rdata, pdata, cdata); + + unused = fwrite(cdata, u.blocksize, 1, u.canceled_file); + } + } else { + while (fscanf(u.drift_file, "%c", &c) > 0) { + switch (c) { + case 'd': + if (!fscanf(u.drift_file, "%a", &drift)) { + perror("Drift file incomplete"); + goto fail; + } + + u.ec->set_drift(u.ec, drift); + + break; + + case 'c': + if (!fscanf(u.drift_file, "%d", &i)) { + perror("Drift file incomplete"); + goto fail; + } + + if (fread(rdata, i, 1, u.captured_file) <= 0) { + perror("Captured file ended prematurely"); + goto fail; + } + + u.ec->record(u.ec, rdata, cdata); + + unused = fwrite(cdata, i, 1, u.canceled_file); + + break; + + case 'p': + if (!fscanf(u.drift_file, "%d", &i)) { + perror("Drift file incomplete"); + goto fail; + } + + if (fread(pdata, i, 1, u.played_file) <= 0) { + perror("Played file ended prematurely"); + goto fail; + } + + u.ec->play(u.ec, pdata); + + break; + } + } + + if (fread(rdata, i, 1, u.captured_file) > 0) + pa_log("All capture data was not consumed"); + if (fread(pdata, i, 1, u.played_file) > 0) + pa_log("All playback data was not consumed"); + } + + u.ec->done(u.ec); + + fclose(u.captured_file); + fclose(u.played_file); + fclose(u.canceled_file); + if (u.drift_file) + fclose(u.drift_file); + +out: + pa_xfree(rdata); + pa_xfree(pdata); + pa_xfree(cdata); + + pa_xfree(u.ec); + pa_xfree(u.core); + + if (ma) + pa_modargs_free(ma); + + return ret; + +usage: + pa_log("Usage: %s play_file rec_file out_file [module args] [aec_args] [drift_file]", argv[0]); + +fail: + ret = -1; + goto out; +} +#endif /* ECHO_CANCEL_TEST */