diff --git a/CMakeLists.txt b/CMakeLists.txt
index e6f5ab86aaf0c14a18b783986cd5fb777ce00147..45586a0daa8a4f81f7227ed340ffda7ea5e0fb75 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -22,6 +22,7 @@ source_group("Source Files\\jamidht\\eth\\libdevcore" FILES ${Source_Files__jami
 source_group("Source Files\\jamidht\\eth\\libdevcrypto" FILES ${Source_Files__jamidht__eth__libdevcrypto})
 source_group("Source Files\\media" FILES ${Source_Files__media})
 source_group("Source Files\\media\\audio" FILES ${Source_Files__media__audio})
+source_group("Source Files\\media\\audio\\echo-cancel" FILES ${Source_Files__media__audio__echo_cancel})
 source_group("Source Files\\media\\audio\\sound" FILES ${Source_Files__media__audio__sound})
 source_group("Source Files\\media\\video" FILES ${Source_Files__media__video})
 source_group("Source Files\\plugin" FILES ${Source_Files__plugin})
@@ -54,6 +55,7 @@ list (APPEND ALL_FILES
       ${Source_Files__media}
       ${Source_Files__media__audio}
       ${Source_Files__media__audio__sound}
+      ${Source_Files__media__audio__echo_cancel}
       ${Source_Files__media__video}
       ${Source_Files__security}
       ${Source_Files__sip}
diff --git a/compat/msvc/config.h b/compat/msvc/config.h
index 151f1eb846757628c2029aa150e2c21a63a3554a..a45cd4de226a7b38ca1b7990b15e90a890e24c5e 100644
--- a/compat/msvc/config.h
+++ b/compat/msvc/config.h
@@ -98,7 +98,7 @@ systems. This function is required for `alloca.c' support on those systems.
 #define HAVE_SPEEX 0
 
 /* Define if you have libspeexdsp */
-#define HAVE_SPEEXDSP 0
+#define HAVE_SPEEXDSP 1
 
 /* Define to 1 if stdbool.h conforms to C99. */
 #define HAVE_STDBOOL_H 1
diff --git a/configure.ac b/configure.ac
index 3946a3f34122e48750baa7c0023b0f434bcb8e7d..18f29692195f039d88c7dab087571cf4de91498f 100644
--- a/configure.ac
+++ b/configure.ac
@@ -641,6 +641,7 @@ AC_CONFIG_FILES([Makefile \
                  src/media/audio/coreaudio/Makefile \
                  src/media/audio/portaudio/Makefile \
                  src/media/audio/sound/Makefile \
+                 src/media/audio/echo-cancel/Makefile \
                  src/config/Makefile \
                  src/client/Makefile \
                  src/media/video/Makefile \
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 095656d2a5205bc0b13c9c7ff9a884db61a1c49c..1ed0cf3915beefd10d1054480c6c9a9253314145 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -95,6 +95,7 @@ set (Source_Files__jamidht__eth__libdevcrypto ${Source_Files__jamidht__eth__libd
 set (Source_Files__media ${Source_Files__media} PARENT_SCOPE)
 set (Source_Files__media__audio ${Source_Files__media__audio} PARENT_SCOPE)
 set (Source_Files__media__audio__sound ${Source_Files__media__audio__sound} PARENT_SCOPE)
+set (Source_Files__media__audio__echo_cancel ${Source_Files__media__audio__echo_cancel} PARENT_SCOPE)
 set (Source_Files__media__video ${Source_Files__media__video} PARENT_SCOPE)
 if(MSVC)
    if(WINDOWS_STORE)
diff --git a/src/media/CMakeLists.txt b/src/media/CMakeLists.txt
index 320115d938a113d14a12f067167b805d0c99a22c..264aeac2808ca653c2f961bec71c36b18d5f4f00 100644
--- a/src/media/CMakeLists.txt
+++ b/src/media/CMakeLists.txt
@@ -54,6 +54,7 @@ if(MSVC)
    set (Source_Files__media__audio__portaudio ${Source_Files__media__audio__portaudio} PARENT_SCOPE)
 endif()
 set (Source_Files__media__audio__sound ${Source_Files__media__audio__sound} PARENT_SCOPE)
+set (Source_Files__media__audio__echo_cancel ${Source_Files__media__audio__echo_cancel} PARENT_SCOPE)
 
 set (Source_Files__media__video ${Source_Files__media__video} PARENT_SCOPE)
 if(MSVC)
diff --git a/src/media/audio/CMakeLists.txt b/src/media/audio/CMakeLists.txt
index f6838572290d29d4ee34b7b4209757ddca507ce3..a6f71f24a679922c1732b238a32c02b336be9112 100644
--- a/src/media/audio/CMakeLists.txt
+++ b/src/media/audio/CMakeLists.txt
@@ -48,4 +48,7 @@ if(MSVC)
 endif()
 
 add_subdirectory(sound)
-set (Source_Files__media__audio__sound ${Source_Files__media__audio__sound} PARENT_SCOPE)
\ No newline at end of file
+set (Source_Files__media__audio__sound ${Source_Files__media__audio__sound} PARENT_SCOPE)
+
+add_subdirectory(echo-cancel)
+set (Source_Files__media__audio__echo_cancel ${Source_Files__media__audio__echo_cancel} PARENT_SCOPE)
\ No newline at end of file
diff --git a/src/media/audio/Makefile.am b/src/media/audio/Makefile.am
index 34138871c411cce5a457bb0e137561c3bd1061c3..2772e51c4a7476187bad8948910afc2aa622bfcb 100644
--- a/src/media/audio/Makefile.am
+++ b/src/media/audio/Makefile.am
@@ -2,7 +2,7 @@ include $(top_srcdir)/globals.mk
 
 noinst_LTLIBRARIES = libaudio.la
 
-SUBDIRS = sound
+SUBDIRS = sound echo-cancel
 
 if BUILD_OPENSL
 SUBDIRS += opensl
@@ -78,7 +78,8 @@ noinst_HEADERS = \
 		tonecontrol.h
 
 libaudio_la_LIBADD = \
-	./sound/libsound.la
+	./sound/libsound.la \
+	./echo-cancel/libecho-cancel.la
 
 if BUILD_PULSE
 libaudio_la_LIBADD += ./pulseaudio/libpulselayer.la
diff --git a/src/media/audio/audiolayer.cpp b/src/media/audio/audiolayer.cpp
index c5a1ba49072299c52cb8e3b6a5a6901d4fbfec7b..927db4896d6171e5f73dff7e24483adf90a410d4 100644
--- a/src/media/audio/audiolayer.cpp
+++ b/src/media/audio/audiolayer.cpp
@@ -27,87 +27,13 @@
 #include "audio/resampler.h"
 #include "tonecontrol.h"
 #include "client/ring_signal.h"
-
-extern "C" {
-#include <speex/speex_echo.h>
-}
+#include "echo-cancel/null_echo_canceller.h"
 
 #include <ctime>
 #include <algorithm>
 
 namespace jami {
 
-struct AudioLayer::EchoState
-{
-    EchoState(AudioFormat format, unsigned frameSize, unsigned)
-        : state(speex_echo_state_init_mc(frameSize,
-                                         frameSize * 16,
-                                         format.nb_channels,
-                                         format.nb_channels),
-                &speex_echo_state_destroy)
-        , playbackQueue(format, frameSize)
-        , recordQueue(format, frameSize)
-    {
-        int sr = format.sample_rate;
-        speex_echo_ctl(state.get(), SPEEX_ECHO_SET_SAMPLING_RATE, &sr);
-    }
-
-    void putRecorded(std::shared_ptr<AudioFrame>&& in)
-    {
-        // JAMI_DBG("putRecorded %s %d", in->getFormat().toString().c_str(), in->getFrameSize());
-        recordQueue.enqueue(std::move(in));
-    }
-
-    void putPlayback(const std::shared_ptr<AudioFrame>& in)
-    {
-        // JAMI_DBG("putPlayback %s %d", in->getFormat().toString().c_str(), in->getFrameSize());
-        auto c = in;
-        playbackQueue.enqueue(std::move(c));
-    }
-
-    std::shared_ptr<AudioFrame> getRecorded()
-    {
-        if (playbackQueue.samples() < playbackQueue.frameSize()
-            or recordQueue.samples() < recordQueue.frameSize()) {
-            JAMI_DBG("getRecorded underflow %d / %d, %d / %d",
-                     playbackQueue.samples(),
-                     playbackQueue.frameSize(),
-                     recordQueue.samples(),
-                     recordQueue.frameSize());
-            return {};
-        }
-        if (recordQueue.samples() > 2 * recordQueue.frameSize() && playbackQueue.samples() == 0) {
-            JAMI_DBG("getRecorded PLAYBACK underflow");
-            return recordQueue.dequeue();
-        }
-        while (playbackQueue.samples() > 10 * playbackQueue.frameSize()) {
-            JAMI_DBG("getRecorded record underflow");
-            playbackQueue.dequeue();
-        }
-        while (recordQueue.samples() > 4 * recordQueue.frameSize()) {
-            JAMI_DBG("getRecorded playback underflow");
-            recordQueue.dequeue();
-        }
-        auto playback = playbackQueue.dequeue();
-        auto record = recordQueue.dequeue();
-        if (playback and record) {
-            auto ret = std::make_shared<AudioFrame>(record->getFormat(), record->getFrameSize());
-            speex_echo_cancellation(state.get(),
-                                    (const int16_t*) record->pointer()->data[0],
-                                    (const int16_t*) playback->pointer()->data[0],
-                                    (int16_t*) ret->pointer()->data[0]);
-            return ret;
-        }
-        return {};
-    }
-
-private:
-    using SpeexEchoStatePtr = std::unique_ptr<SpeexEchoState, void (*)(SpeexEchoState*)>;
-    SpeexEchoStatePtr state;
-    AudioFrameResizer playbackQueue;
-    AudioFrameResizer recordQueue;
-};
-
 AudioLayer::AudioLayer(const AudioPreference& pref)
     : isCaptureMuted_(pref.getCaptureMuted())
     , isPlaybackMuted_(pref.getPlaybackMuted())
@@ -194,13 +120,12 @@ AudioLayer::checkAEC()
 {
     bool shouldSoftAEC = not hasNativeAEC_ and playbackStarted_ and recordStarted_;
 
-    if (not echoState_ and shouldSoftAEC) {
+    if (not echoCanceller_ and shouldSoftAEC) {
         JAMI_WARN("Starting AEC");
-        echoState_.reset(
-            new EchoState(audioFormat_, nativeFrameSize_, audioFormat_.sample_rate / 4));
-    } else if (echoState_ and not shouldSoftAEC) {
+        echoCanceller_.reset(new NullEchoCanceller(audioFormat_, audioFormat_.sample_rate / 100));
+    } else if (echoCanceller_ and not shouldSoftAEC) {
         JAMI_WARN("Stopping AEC");
-        echoState_.reset();
+        echoCanceller_.reset();
     }
 }
 
@@ -288,8 +213,8 @@ AudioLayer::getToPlay(AudioFormat format, size_t writableSamples)
         }
 
         if (resampled) {
-            if (echoState_) {
-                echoState_->putPlayback(resampled);
+            if (echoCanceller_) {
+                echoCanceller_->putPlayback(resampled);
             }
             playbackQueue_->enqueue(std::move(resampled));
         } else
@@ -302,12 +227,9 @@ AudioLayer::getToPlay(AudioFormat format, size_t writableSamples)
 void
 AudioLayer::putRecorded(std::shared_ptr<AudioFrame>&& frame)
 {
-    // if (isCaptureMuted_)
-    //    libav_utils::fillWithSilence(frame->pointer());
-
-    if (echoState_) {
-        echoState_->putRecorded(std::move(frame));
-        while (auto rec = echoState_->getRecorded())
+    if (echoCanceller_) {
+        echoCanceller_->putRecorded(std::move(frame));
+        while (auto rec = echoCanceller_->getProcessed())
             mainRingBuffer_->put(std::move(rec));
     } else {
         mainRingBuffer_->put(std::move(frame));
diff --git a/src/media/audio/audiolayer.h b/src/media/audio/audiolayer.h
index fa11a7581218c228aff01089da3c6e6c420bd662..ab0e5483da6cdff09fde0616a32658500aec6b83 100644
--- a/src/media/audio/audiolayer.h
+++ b/src/media/audio/audiolayer.h
@@ -26,6 +26,7 @@
 #include "dcblocker.h"
 #include "noncopyable.h"
 #include "audio_frame_resizer.h"
+#include "echo-cancel/echo_canceller.h"
 
 #include <chrono>
 #include <mutex>
@@ -52,7 +53,7 @@ typedef struct SpeexEchoState_ SpeexEchoState;
 #define COREAUDIO_API_STR  "coreaudio"
 #define PORTAUDIO_API_STR  "portaudio"
 
-#define PCM_DEFAULT     "default"     // Default ALSA plugin
+#define PCM_DEFAULT     "default" // Default ALSA plugin
 #define PCM_DSNOOP      "plug:dsnoop" // Alsa plugin for microphone sharing
 #define PCM_DMIX_DSNOOP "dmix/dsnoop" // Audio profile using Alsa dmix/dsnoop
 
@@ -288,8 +289,7 @@ protected:
      */
     std::unique_ptr<Resampler> resampler_;
 
-    struct EchoState;
-    std::unique_ptr<EchoState> echoState_;
+    std::unique_ptr<EchoCanceller> echoCanceller_;
 
 private:
     void checkAEC();
diff --git a/src/media/audio/echo-cancel/CMakeLists.txt b/src/media/audio/echo-cancel/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..fec1af7d8f9cf8e5df463c637ab74cb28cb07e42
--- /dev/null
+++ b/src/media/audio/echo-cancel/CMakeLists.txt
@@ -0,0 +1,12 @@
+################################################################################
+# Source groups - echo-cancel
+################################################################################
+list (APPEND Source_Files__media__audio__echo_cancel
+      "${CMAKE_CURRENT_SOURCE_DIR}/echo_canceller.h"
+      "${CMAKE_CURRENT_SOURCE_DIR}/null_echo_canceller.h"
+      "${CMAKE_CURRENT_SOURCE_DIR}/null_echo_canceller.cpp"
+      "${CMAKE_CURRENT_SOURCE_DIR}/speex_echo_canceller.h"
+      "${CMAKE_CURRENT_SOURCE_DIR}/speex_echo_canceller.cpp"
+)
+
+set (Source_Files__media__audio__echo_cancel ${Source_Files__media__audio__echo_cancel} PARENT_SCOPE)
\ No newline at end of file
diff --git a/src/media/audio/echo-cancel/Makefile.am b/src/media/audio/echo-cancel/Makefile.am
new file mode 100644
index 0000000000000000000000000000000000000000..2222ce86a5758d60d9e3fb6007a6dbd679e65010
--- /dev/null
+++ b/src/media/audio/echo-cancel/Makefile.am
@@ -0,0 +1,18 @@
+include $(top_srcdir)/globals.mk
+
+noinst_LTLIBRARIES = libecho-cancel.la
+
+EC_SRC = null_echo_canceller.cpp
+EC_HDR = null_echo_canceller.h
+
+if BUILD_SPEEXDSP
+EC_SRC += speex_echo_canceller.cpp
+EC_HDR += speex_echo_canceller.h
+endif
+
+libecho_cancel_la_SOURCES = \
+		$(EC_SRC)
+
+noinst_HEADERS = \
+	echo_canceller.h \
+	$(EC_HDR)
diff --git a/src/media/audio/echo-cancel/echo_canceller.h b/src/media/audio/echo-cancel/echo_canceller.h
new file mode 100644
index 0000000000000000000000000000000000000000..c159957f4866c0224189d798eaf8764ba1ddb1b7
--- /dev/null
+++ b/src/media/audio/echo-cancel/echo_canceller.h
@@ -0,0 +1,63 @@
+/*
+ *  Copyright (C) 2021 Savoir-faire Linux Inc.
+ *
+ *  Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program 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 General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+#pragma once
+
+#include "noncopyable.h"
+#include "audio/audio_frame_resizer.h"
+#include "audio/audiobuffer.h"
+#include "libav_deps.h"
+
+namespace jami {
+
+class EchoCanceller
+{
+private:
+    NON_COPYABLE(EchoCanceller);
+
+public:
+    EchoCanceller(AudioFormat format, unsigned frameSize)
+        : playbackQueue_(format, frameSize)
+        , recordQueue_(format, frameSize)
+        , sampleRate_(format.sample_rate)
+        , frameSize_(frameSize)
+    {}
+    virtual ~EchoCanceller() = default;
+
+    virtual void putRecorded(std::shared_ptr<AudioFrame>&& buf)
+    {
+        recordQueue_.enqueue(std::move(buf));
+    };
+    virtual void putPlayback(const std::shared_ptr<AudioFrame>& buf)
+    {
+        auto c = buf;
+        playbackQueue_.enqueue(std::move(c));
+    };
+    virtual std::shared_ptr<AudioFrame> getProcessed() = 0;
+    virtual void done() = 0;
+
+protected:
+    AudioFrameResizer playbackQueue_;
+    AudioFrameResizer recordQueue_;
+    unsigned sampleRate_;
+    unsigned frameSize_;
+};
+
+} // namespace jami
diff --git a/src/media/audio/echo-cancel/null_echo_canceller.cpp b/src/media/audio/echo-cancel/null_echo_canceller.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a7b9f10cbbb6571df5e069a7867eaf0b84defc3b
--- /dev/null
+++ b/src/media/audio/echo-cancel/null_echo_canceller.cpp
@@ -0,0 +1,73 @@
+/*
+ *  Copyright (C) 2021 Savoir-faire Linux Inc.
+ *
+ *  Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program 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 General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+#include "null_echo_canceller.h"
+
+#include <cassert>
+
+namespace jami {
+
+NullEchoCanceller::NullEchoCanceller(AudioFormat format, unsigned frameSize)
+    : EchoCanceller(format, frameSize)
+{}
+
+void
+NullEchoCanceller::putRecorded(std::shared_ptr<AudioFrame>&& buf)
+{
+    EchoCanceller::putRecorded(std::move(buf));
+};
+
+void
+NullEchoCanceller::putPlayback(const std::shared_ptr<AudioFrame>& buf)
+{
+    EchoCanceller::putPlayback(buf);
+};
+
+std::shared_ptr<AudioFrame>
+NullEchoCanceller::getProcessed()
+{
+    while (recordQueue_.samples() > recordQueue_.frameSize() * 10) {
+        JAMI_DBG("record overflow %d / %d", recordQueue_.samples(), frameSize_);
+        recordQueue_.dequeue();
+    }
+    while (playbackQueue_.samples() > playbackQueue_.frameSize() * 10) {
+        JAMI_DBG("playback overflow %d / %d", playbackQueue_.samples(), frameSize_);
+        playbackQueue_.dequeue();
+    }
+    if (recordQueue_.samples() < recordQueue_.frameSize()
+        || playbackQueue_.samples() < playbackQueue_.frameSize()) {
+        JAMI_DBG("underflow rec: %d, play: %d fs: %d",
+                 recordQueue_.samples(),
+                 playbackQueue_.samples(),
+                 frameSize_);
+        return {};
+    }
+
+    JAMI_WARN("Processing %d samples, rec: %d, play: %d ",
+              frameSize_,
+              recordQueue_.samples(),
+              playbackQueue_.samples());
+    playbackQueue_.dequeue();
+    return recordQueue_.dequeue();
+};
+
+void NullEchoCanceller::done() {};
+
+} // namespace jami
diff --git a/src/media/audio/echo-cancel/null_echo_canceller.h b/src/media/audio/echo-cancel/null_echo_canceller.h
new file mode 100644
index 0000000000000000000000000000000000000000..098f56eb2f9de2e02e3ac4936e3f52cb9ba47a9a
--- /dev/null
+++ b/src/media/audio/echo-cancel/null_echo_canceller.h
@@ -0,0 +1,39 @@
+/*
+ *  Copyright (C) 2021 Savoir-faire Linux Inc.
+ *
+ *  Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program 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 General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+#pragma once
+
+#include "echo_canceller.h"
+
+namespace jami {
+
+class NullEchoCanceller final : public EchoCanceller
+{
+public:
+    NullEchoCanceller(AudioFormat format, unsigned frameSize);
+    ~NullEchoCanceller() = default;
+
+    void putRecorded(std::shared_ptr<AudioFrame>&& buf) override;
+    void putPlayback(const std::shared_ptr<AudioFrame>& buf) override;
+    std::shared_ptr<AudioFrame> getProcessed() override;
+    void done() override;
+};
+
+} // namespace jami
diff --git a/src/media/audio/echo-cancel/speex_echo_canceller.cpp b/src/media/audio/echo-cancel/speex_echo_canceller.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..aace775c12372fdb7776f59def00cfbf0fe2eb7c
--- /dev/null
+++ b/src/media/audio/echo-cancel/speex_echo_canceller.cpp
@@ -0,0 +1,109 @@
+/*
+ *  Copyright (C) 2021 Savoir-faire Linux Inc.
+ *
+ *  Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program 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 General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+#include "speex_echo_canceller.h"
+
+#include "audio/audiolayer.h"
+
+extern "C" {
+#include <speex/speex_echo.h>
+#include <speex/speex_preprocess.h>
+}
+
+namespace jami {
+
+struct SpeexEchoCanceller::SpeexEchoStateImpl
+{
+    using SpeexEchoStatePtr = std::unique_ptr<SpeexEchoState, void (*)(SpeexEchoState*)>;
+    SpeexEchoStatePtr state;
+
+    SpeexEchoStateImpl(AudioFormat format, unsigned frameSize)
+        : state(speex_echo_state_init_mc(frameSize,
+                                         frameSize * 16,
+                                         format.nb_channels,
+                                         format.nb_channels),
+                &speex_echo_state_destroy)
+    {
+        int sr = format.sample_rate;
+        speex_echo_ctl(state.get(), SPEEX_ECHO_SET_SAMPLING_RATE, &sr);
+    }
+};
+
+SpeexEchoCanceller::SpeexEchoCanceller(AudioFormat format, unsigned frameSize)
+    : EchoCanceller(format, frameSize)
+    , pimpl_(std::make_unique<SpeexEchoStateImpl>(format, frameSize))
+{
+    speex_echo_ctl(pimpl_->state.get(), SPEEX_ECHO_SET_SAMPLING_RATE, &sampleRate_);
+}
+
+void
+SpeexEchoCanceller::putRecorded(std::shared_ptr<AudioFrame>&& buf)
+{
+    EchoCanceller::putRecorded(std::move(buf));
+}
+
+void
+SpeexEchoCanceller::putPlayback(const std::shared_ptr<AudioFrame>& buf)
+{
+    EchoCanceller::putPlayback(buf);
+}
+
+std::shared_ptr<AudioFrame>
+SpeexEchoCanceller::getProcessed()
+{
+    if (playbackQueue_.samples() < playbackQueue_.frameSize()
+        or recordQueue_.samples() < recordQueue_.frameSize()) {
+        JAMI_DBG("getRecorded underflow %d / %d, %d / %d",
+                 playbackQueue_.samples(),
+                 playbackQueue_.frameSize(),
+                 recordQueue_.samples(),
+                 recordQueue_.frameSize());
+        return {};
+    }
+    if (recordQueue_.samples() > 2 * recordQueue_.frameSize() && playbackQueue_.samples() == 0) {
+        JAMI_DBG("getRecorded PLAYBACK underflow");
+        return recordQueue_.dequeue();
+    }
+    while (playbackQueue_.samples() > 10 * playbackQueue_.frameSize()) {
+        JAMI_DBG("getRecorded record underflow");
+        playbackQueue_.dequeue();
+    }
+    while (recordQueue_.samples() > 4 * recordQueue_.frameSize()) {
+        JAMI_DBG("getRecorded playback underflow");
+        recordQueue_.dequeue();
+    }
+    auto playback = playbackQueue_.dequeue();
+    auto record = recordQueue_.dequeue();
+    if (playback and record) {
+        auto ret = std::make_shared<AudioFrame>(record->getFormat(), record->getFrameSize());
+        speex_echo_cancellation(pimpl_->state.get(),
+                                (const int16_t*) record->pointer()->data[0],
+                                (const int16_t*) playback->pointer()->data[0],
+                                (int16_t*) ret->pointer()->data[0]);
+        return ret;
+    }
+    return {};
+}
+
+void
+SpeexEchoCanceller::done()
+{}
+
+} // namespace jami
diff --git a/src/media/audio/echo-cancel/speex_echo_canceller.h b/src/media/audio/echo-cancel/speex_echo_canceller.h
new file mode 100644
index 0000000000000000000000000000000000000000..cbc2eb9ce8badb7a8a22e3ac154fa433a028b74a
--- /dev/null
+++ b/src/media/audio/echo-cancel/speex_echo_canceller.h
@@ -0,0 +1,51 @@
+/*
+ *  Copyright (C) 2021 Savoir-faire Linux Inc.
+ *
+ *  Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 3 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program 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 General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
+ */
+
+#pragma once
+
+#include "audio/echo-cancel/echo_canceller.h"
+#include "audio/audio_frame_resizer.h"
+
+extern "C" {
+struct SpeexEchoState_;
+typedef struct SpeexEchoState_ SpeexEchoState;
+}
+
+#include <memory>
+
+namespace jami {
+
+class SpeexEchoCanceller final : public EchoCanceller
+{
+public:
+    SpeexEchoCanceller(AudioFormat format, unsigned frameSize);
+    ~SpeexEchoCanceller() = default;
+
+    // Inherited via EchoCanceller
+    void putRecorded(std::shared_ptr<AudioFrame>&& buf) override;
+    void putPlayback(const std::shared_ptr<AudioFrame>& buf) override;
+    std::shared_ptr<AudioFrame> getProcessed() override;
+    void done() override;
+
+private:
+    struct SpeexEchoStateImpl;
+    std::unique_ptr<SpeexEchoStateImpl> pimpl_;
+};
+} // namespace jami