echo-cancel: Add the WebRTC echo canceller
authorArun Raghavan <arun.raghavan@collabora.co.uk>
Mon, 19 Sep 2011 08:11:13 +0000 (13:41 +0530)
committerArun Raghavan <arun.raghavan@collabora.co.uk>
Mon, 17 Oct 2011 11:12:59 +0000 (16:42 +0530)
This adds the WebRTC echo canceller as another module-echo-cancel
backend. We're exposing both the full echo canceller as well as the
mobile echo control version as modargs.

Pending items:

1. The mobile canceller doesn't seem to work at the moment.

2. We still need to add bits to hook in drift compensation (to support
   sink and source from different devices).

The most controversial part of this patch would probably be the
mandatory build-time dependency on a C++ compiler. If the optional
--enable-webrtc-aec is set, then there's also a dependency on libstdc++.

configure.ac
src/Makefile.am
src/modules/echo-cancel/echo-cancel.h
src/modules/echo-cancel/module-echo-cancel.c
src/modules/echo-cancel/webrtc.cc [new file with mode: 0644]

index 0bf40a8..feeae75 100644 (file)
@@ -78,6 +78,9 @@ AC_PROG_MKDIR_P
 AC_PROG_CC
 AC_PROG_CC_C99
 AM_PROG_CC_C_O
+# Only required if you want the WebRTC canceller -- no runtime dep on
+# libstdc++ otherwise
+AC_PROG_CXX
 AC_PROG_GCC_TRADITIONAL
 AC_USE_SYSTEM_EXTENSIONS
 
@@ -1139,6 +1142,20 @@ if test "x$os_is_darwin" = "x1" ; then
     fi
 fi
 
+AC_ARG_ENABLE([webrtc-aec],
+    AS_HELP_STRING([--enable-webrtc-aec], [Enable the optional WebRTC-based echo canceller]))
+
+AS_IF([test "x$enable_webrtc_aec" != "xno"],
+    [PKG_CHECK_MODULES(WEBRTC, [ webrtc-audio-processing ], [HAVE_WEBRTC=1], [HAVE_WEBRTC=0])],
+    [HAVE_WEBRTC=0])
+
+AS_IF([test "x$enable_webrtc_aec" = "xyes" && test "x$HAVE_WEBRTC" = "x0"],
+    [AC_MSG_ERROR([*** webrtc-audio-processing library not found])])
+
+AC_SUBST(WEBRTC_CFLAGS)
+AC_SUBST(WEBRTC_LIBS)
+AM_CONDITIONAL([HAVE_WEBRTC], [test "x$HAVE_WEBRTC" = "x1"])
+
 
 ###################################
 #            Output               #
@@ -1275,6 +1292,7 @@ AS_IF([test "x$HAVE_IPV6" = "x1"], ENABLE_IPV6=yes, ENABLE_IPV6=no)
 AS_IF([test "x$HAVE_OPENSSL" = "x1"], ENABLE_OPENSSL=yes, ENABLE_OPENSSL=no)
 AS_IF([test "x$HAVE_FFTW" = "x1"], ENABLE_FFTW=yes, ENABLE_FFTW=no)
 AS_IF([test "x$HAVE_ORC" = "xyes"], ENABLE_ORC=yes, ENABLE_ORC=no)
+AS_IF([test "x$HAVE_WEBRTC" = "x1"], ENABLE_WEBRTC=yes, ENABLE_WEBRTC=no)
 AS_IF([test "x$HAVE_TDB" = "x1"], ENABLE_TDB=yes, ENABLE_TDB=no)
 AS_IF([test "x$HAVE_GDBM" = "x1"], ENABLE_GDBM=yes, ENABLE_GDBM=no)
 AS_IF([test "x$HAVE_SIMPLEDB" = "x1"], ENABLE_SIMPLEDB=yes, ENABLE_SIMPLEDB=no)
@@ -1321,6 +1339,7 @@ echo "
     Enable OpenSSL (for Airtunes): ${ENABLE_OPENSSL}
     Enable fftw:                   ${ENABLE_FFTW}
     Enable orc:                    ${ENABLE_ORC}
+    Enable WebRTC echo canceller:  ${ENABLE_WEBRTC}
     Database
       tdb:                         ${ENABLE_TDB}
       gdbm:                        ${ENABLE_GDBM}
index d3fe1c3..5f8a9bb 100644 (file)
@@ -48,6 +48,7 @@ AM_CFLAGS = \
        $(PTHREAD_CFLAGS) \
        -DPA_ALSA_PATHS_DIR=\"$(alsapathsdir)\" \
        -DPA_ALSA_PROFILE_SETS_DIR=\"$(alsaprofilesetsdir)\"
+AM_CXXFLAGS = $(AM_CFLAGS)
 SERVER_CFLAGS = -D__INCLUDED_FROM_PULSE_AUDIO
 
 AM_LIBADD = $(PTHREAD_LIBS) $(INTLLIBS)
@@ -523,6 +524,7 @@ echo_cancel_test_SOURCES = $(module_echo_cancel_la_SOURCES)
 nodist_echo_cancel_test_SOURCES = $(nodist_module_echo_cancel_la_SOURCES)
 echo_cancel_test_LDADD = $(module_echo_cancel_la_LIBADD)
 echo_cancel_test_CFLAGS = $(module_echo_cancel_la_CFLAGS) -DECHO_CANCEL_TEST=1
+echo_cancel_test_CXXFLAGS = $(module_echo_cancel_la_CXXFLAGS) -DECHO_CANCEL_TEST=1
 echo_cancel_test_LDFLAGS = $(AM_LDFLAGS) $(BINLDFLAGS)
 
 ###################################
@@ -1753,6 +1755,12 @@ nodist_module_echo_cancel_la_SOURCES = \
 module_echo_cancel_la_LIBADD += $(ORC_LIBS)
 module_echo_cancel_la_CFLAGS += $(ORC_CFLAGS) -I$(top_builddir)/src/modules/echo-cancel
 endif
+if HAVE_WEBRTC
+module_echo_cancel_la_SOURCES += modules/echo-cancel/webrtc.cc
+module_echo_cancel_la_CFLAGS += -DHAVE_WEBRTC=1
+module_echo_cancel_la_CXXFLAGS = $(AM_CXXFLAGS) $(SERVER_CFLAGS) $(WEBRTC_CFLAGS) -DHAVE_WEBRTC=1
+module_echo_cancel_la_LIBADD += $(WEBRTC_LIBS)
+endif
 
 # RTP modules
 module_rtp_send_la_SOURCES = modules/rtp/module-rtp-send.c
index 9f67980..19e1350 100644 (file)
@@ -49,6 +49,15 @@ struct pa_echo_canceller_params {
             uint32_t blocksize;
             AEC *aec;
         } adrian;
+#ifdef HAVE_WEBRTC
+        struct {
+            /* This is a void* so that we don't have to convert this whole file
+             * to C++ linkage. apm is a pointer to an AudioProcessing object */
+            void *apm;
+            uint32_t blocksize;
+            pa_sample_spec sample_spec;
+        } webrtc;
+#endif
         /* each canceller-specific structure goes here */
     } priv;
 };
@@ -86,4 +95,16 @@ pa_bool_t pa_adrian_ec_init(pa_core *c, pa_echo_canceller *ec,
 void pa_adrian_ec_run(pa_echo_canceller *ec, const uint8_t *rec, const uint8_t *play, uint8_t *out);
 void pa_adrian_ec_done(pa_echo_canceller *ec);
 
+#ifdef HAVE_WEBRTC
+/* WebRTC canceller functions */
+PA_C_DECL_BEGIN
+pa_bool_t pa_webrtc_ec_init(pa_core *c, pa_echo_canceller *ec,
+                            pa_sample_spec *source_ss, pa_channel_map *source_map,
+                            pa_sample_spec *sink_ss, pa_channel_map *sink_map,
+                            uint32_t *blocksize, const char *args);
+void pa_webrtc_ec_run(pa_echo_canceller *ec, const uint8_t *rec, const uint8_t *play, uint8_t *out);
+void pa_webrtc_ec_done(pa_echo_canceller *ec);
+PA_C_DECL_END
+#endif
+
 #endif /* fooechocancelhfoo */
index 20541f4..7360b27 100644 (file)
@@ -83,6 +83,9 @@ 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;
 
 #define DEFAULT_ECHO_CANCELLER "speex"
@@ -100,6 +103,14 @@ 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,
+        .run                    = pa_webrtc_ec_run,
+        .done                   = pa_webrtc_ec_done,
+    },
+#endif
 };
 
 #define DEFAULT_RATE 32000
@@ -1340,6 +1351,10 @@ static pa_echo_canceller_method_t get_ec_method_from_string(const char *method)
         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;
 }
diff --git a/src/modules/echo-cancel/webrtc.cc b/src/modules/echo-cancel/webrtc.cc
new file mode 100644 (file)
index 0000000..c53e963
--- /dev/null
@@ -0,0 +1,234 @@
+/***
+    This file is part of PulseAudio.
+
+    Copyright 2011 Collabora Ltd.
+
+    Contributor: Arun Raghavan <arun.raghavan@collabora.co.uk>
+
+    PulseAudio is free software; you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published
+    by the Free Software Foundation; either version 2.1 of the License,
+    or (at your option) any later version.
+
+    PulseAudio is distributed in the hope that it will be useful, but
+    WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+    General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with PulseAudio; if not, write to the Free Software
+    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+    USA.
+***/
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <pulse/cdecl.h>
+
+PA_C_DECL_BEGIN
+#include <pulsecore/core-util.h>
+#include <pulsecore/modargs.h>
+
+#include <pulse/timeval.h>
+#include "echo-cancel.h"
+PA_C_DECL_END
+
+#include <audio_processing.h>
+#include <module_common_types.h>
+
+#define BLOCK_SIZE_US 10000
+
+#define DEFAULT_HIGH_PASS_FILTER TRUE
+#define DEFAULT_NOISE_SUPPRESSION TRUE
+#define DEFAULT_ANALOG_GAIN_CONTROL FALSE
+#define DEFAULT_DIGITAL_GAIN_CONTROL TRUE
+#define DEFAULT_MOBILE FALSE
+#define DEFAULT_ROUTING_MODE "speakerphone"
+#define DEFAULT_COMFORT_NOISE TRUE
+
+static const char* const valid_modargs[] = {
+    "high_pass_filter",
+    "noise_suppression",
+    "analog_gain_control",
+    "digital_gain_control",
+    "mobile",
+    "routing_mode",
+    "comfort_noise",
+    NULL
+};
+
+static int routing_mode_from_string(const char *rmode) {
+    if (pa_streq(rmode, "quiet-earpiece-or-headset"))
+        return webrtc::EchoControlMobile::kQuietEarpieceOrHeadset;
+    else if (pa_streq(rmode, "earpiece"))
+        return webrtc::EchoControlMobile::kEarpiece;
+    else if (pa_streq(rmode, "loud-earpiece"))
+        return webrtc::EchoControlMobile::kLoudEarpiece;
+    else if (pa_streq(rmode, "speakerphone"))
+        return webrtc::EchoControlMobile::kSpeakerphone;
+    else if (pa_streq(rmode, "loud-speakerphone"))
+        return webrtc::EchoControlMobile::kLoudSpeakerphone;
+    else
+        return -1;
+}
+
+pa_bool_t pa_webrtc_ec_init(pa_core *c, pa_echo_canceller *ec,
+                            pa_sample_spec *source_ss, pa_channel_map *source_map,
+                            pa_sample_spec *sink_ss, pa_channel_map *sink_map,
+                            uint32_t *blocksize, const char *args)
+{
+    webrtc::AudioProcessing *apm = NULL;
+    pa_bool_t hpf, ns, agc, dgc, mobile, cn;
+    int rm;
+    pa_modargs *ma;
+
+    if (!(ma = pa_modargs_new(args, valid_modargs))) {
+        pa_log("Failed to parse submodule arguments.");
+        goto fail;
+    }
+
+
+    hpf = DEFAULT_HIGH_PASS_FILTER;
+    if (pa_modargs_get_value_boolean(ma, "high_pass_filter", &hpf) < 0) {
+        pa_log("Failed to parse high_pass_filter value");
+        goto fail;
+    }
+
+    ns = DEFAULT_NOISE_SUPPRESSION;
+    if (pa_modargs_get_value_boolean(ma, "noise_suppression", &ns) < 0) {
+        pa_log("Failed to parse noise_suppression value");
+        goto fail;
+    }
+
+    agc = DEFAULT_ANALOG_GAIN_CONTROL;
+    if (pa_modargs_get_value_boolean(ma, "analog_gain_control", &agc) < 0) {
+        pa_log("Failed to parse analog_gain_control value");
+        goto fail;
+    }
+
+    dgc = DEFAULT_DIGITAL_GAIN_CONTROL;
+    if (pa_modargs_get_value_boolean(ma, "analog_gain_control", &dgc) < 0) {
+        pa_log("Failed to parse digital_gain_control value");
+        goto fail;
+    }
+
+    if (agc && dgc) {
+        pa_log("You must pick only one between analog and digital gain control");
+        goto fail;
+    }
+
+    mobile = DEFAULT_MOBILE;
+    if (pa_modargs_get_value_boolean(ma, "mobile", &mobile) < 0) {
+        pa_log("Failed to parse mobile value");
+        goto fail;
+    }
+
+    if (mobile) {
+        if ((rm = routing_mode_from_string(pa_modargs_get_value(ma, "routing_mode", DEFAULT_ROUTING_MODE))) < 0) {
+            pa_log("Failed to parse routing_mode value");
+            goto fail;
+        }
+
+        cn = DEFAULT_COMFORT_NOISE;
+        if (pa_modargs_get_value_boolean(ma, "comfort_noise", &cn) < 0) {
+            pa_log("Failed to parse cn value");
+            goto fail;
+        }
+    } else {
+        if (pa_modargs_get_value(ma, "comfort_noise", NULL) || pa_modargs_get_value(ma, "routing_mode", NULL)) {
+            pa_log("The routing_mode and comfort_noise options are only valid with mobile=true");
+            goto fail;
+        }
+    }
+
+    apm = webrtc::AudioProcessing::Create(0);
+
+    source_ss->format = PA_SAMPLE_S16NE;
+    *sink_ss = *source_ss;
+    /* FIXME: the implementation actually allows a different number of
+     * source/sink channels. Do we want to support that? */
+    *sink_map = *source_map;
+
+    apm->set_sample_rate_hz(source_ss->rate);
+
+    apm->set_num_channels(source_ss->channels, source_ss->channels);
+    apm->set_num_reverse_channels(sink_ss->channels);
+
+    if (hpf)
+        apm->high_pass_filter()->Enable(true);
+
+    if (!mobile) {
+        apm->echo_cancellation()->enable_drift_compensation(false);
+        apm->echo_cancellation()->Enable(true);
+    } else {
+        apm->echo_control_mobile()->set_routing_mode(static_cast<webrtc::EchoControlMobile::RoutingMode>(rm));
+        apm->echo_control_mobile()->enable_comfort_noise(cn);
+        apm->echo_control_mobile()->Enable(true);
+    }
+
+    if (ns) {
+        apm->noise_suppression()->set_level(webrtc::NoiseSuppression::kHigh);
+        apm->noise_suppression()->Enable(true);
+    }
+
+    if (agc || dgc) {
+        if (mobile && rm <= webrtc::EchoControlMobile::kEarpiece)
+            /* Maybe this should be a knob, but we've got a lot of knobs already */
+            apm->gain_control()->set_mode(webrtc::GainControl::kFixedDigital);
+        else if (dgc)
+            apm->gain_control()->set_mode(webrtc::GainControl::kAdaptiveDigital);
+        else {
+            /* FIXME: Hook up for analog AGC */
+            pa_log("Analog gain control isn't implemented yet -- using ditital gain control.");
+            apm->gain_control()->set_mode(webrtc::GainControl::kAdaptiveDigital);
+        }
+    }
+
+    apm->voice_detection()->Enable(true);
+
+    ec->params.priv.webrtc.apm = apm;
+    ec->params.priv.webrtc.sample_spec = *source_ss;
+    ec->params.priv.webrtc.blocksize = *blocksize = (uint64_t)pa_bytes_per_second(source_ss) * BLOCK_SIZE_US / PA_USEC_PER_SEC;
+
+    pa_modargs_free(ma);
+    return TRUE;
+
+fail:
+    if (ma)
+        pa_modargs_free(ma);
+    if (apm)
+        webrtc::AudioProcessing::Destroy(apm);
+
+    return FALSE;
+}
+
+void pa_webrtc_ec_run(pa_echo_canceller *ec, const uint8_t *rec, const uint8_t *play, uint8_t *out) {
+    webrtc::AudioProcessing *apm = (webrtc::AudioProcessing*)ec->params.priv.webrtc.apm;
+    webrtc::AudioFrame play_frame, out_frame;
+    const pa_sample_spec *ss = &ec->params.priv.webrtc.sample_spec;
+
+    play_frame._audioChannel = ss->channels;
+    play_frame._frequencyInHz = ss->rate;
+    play_frame._payloadDataLengthInSamples = ec->params.priv.webrtc.blocksize / pa_frame_size(ss);
+    memcpy(play_frame._payloadData, play, ec->params.priv.webrtc.blocksize);
+
+    out_frame._audioChannel = ss->channels;
+    out_frame._frequencyInHz = ss->rate;
+    out_frame._payloadDataLengthInSamples = ec->params.priv.webrtc.blocksize / pa_frame_size(ss);
+    memcpy(out_frame._payloadData, rec, ec->params.priv.webrtc.blocksize);
+
+    apm->AnalyzeReverseStream(&play_frame);
+    apm->set_stream_delay_ms(0);
+    apm->ProcessStream(&out_frame);
+
+    memcpy(out, out_frame._payloadData, ec->params.priv.webrtc.blocksize);
+}
+
+void pa_webrtc_ec_done(pa_echo_canceller *ec) {
+    if (ec->params.priv.webrtc.apm) {
+        webrtc::AudioProcessing::Destroy((webrtc::AudioProcessing*)ec->params.priv.webrtc.apm);
+        ec->params.priv.webrtc.apm = NULL;
+    }
+}