diff --git a/CMakeLists.txt b/CMakeLists.txt index 45586a0daa8a4f81f7227ed340ffda7ea5e0fb75..11c0dd07f65d1eb6d1c1b82aed2b1fdc69f5ac52 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -122,17 +122,6 @@ if(MSVC) ################################################################################ if("${CMAKE_VS_PLATFORM_NAME}" STREQUAL "x64") target_include_directories(${PROJECT_NAME} PUBLIC - "$<$<CONFIG:ReleaseLib_win32>:" - "${CMAKE_CURRENT_SOURCE_DIR}/contrib/msvc/include/upnp;" - "${CMAKE_CURRENT_SOURCE_DIR}/contrib/build/ffmpeg/Build/win32/x64/include;" - "${CMAKE_CURRENT_SOURCE_DIR}/contrib/build/sndfile/src;" - "${CMAKE_CURRENT_SOURCE_DIR}/contrib/build/openssl/include;" - "${CMAKE_CURRENT_SOURCE_DIR}/contrib/build/asio/asio/include;" - "${CMAKE_CURRENT_SOURCE_DIR}/contrib/build/restinio/dev;" - "${CMAKE_CURRENT_SOURCE_DIR}/contrib/build/fmt/include;" - "${CMAKE_CURRENT_SOURCE_DIR}/contrib/build/http_parser;" - "${CMAKE_CURRENT_SOURCE_DIR}/contrib/build/natpmp" - ">" "${CMAKE_CURRENT_SOURCE_DIR}/.;" "${CMAKE_CURRENT_SOURCE_DIR}/src;" "${CMAKE_CURRENT_SOURCE_DIR}/src/client;" @@ -152,6 +141,15 @@ if(MSVC) "${CMAKE_CURRENT_SOURCE_DIR}/compat/msvc;" "${CMAKE_CURRENT_SOURCE_DIR}/contrib/msvc;" "${CMAKE_CURRENT_SOURCE_DIR}/contrib/msvc/include;" + "${CMAKE_CURRENT_SOURCE_DIR}/contrib/msvc/include/upnp;" + "${CMAKE_CURRENT_SOURCE_DIR}/contrib/build/ffmpeg/Build/win32/x64/include;" + "${CMAKE_CURRENT_SOURCE_DIR}/contrib/build/sndfile/src;" + "${CMAKE_CURRENT_SOURCE_DIR}/contrib/build/openssl/include;" + "${CMAKE_CURRENT_SOURCE_DIR}/contrib/build/asio/asio/include;" + "${CMAKE_CURRENT_SOURCE_DIR}/contrib/build/restinio/dev;" + "${CMAKE_CURRENT_SOURCE_DIR}/contrib/build/fmt/include;" + "${CMAKE_CURRENT_SOURCE_DIR}/contrib/build/http_parser;" + "${CMAKE_CURRENT_SOURCE_DIR}/contrib/build/natpmp" "${CMAKE_CURRENT_SOURCE_DIR}/contrib/build/msgpack-c/include;" "${CMAKE_CURRENT_SOURCE_DIR}/contrib/build/opendht/include;" "${CMAKE_CURRENT_SOURCE_DIR}/contrib/build/libarchive/libarchive;" @@ -164,6 +162,7 @@ if(MSVC) "${CMAKE_CURRENT_SOURCE_DIR}/contrib/build/pjproject/third_party;" "${CMAKE_CURRENT_SOURCE_DIR}/contrib/build/pjproject/pjmedia/include" "${CMAKE_CURRENT_SOURCE_DIR}/contrib/build/speexdsp/include;" + "${CMAKE_CURRENT_SOURCE_DIR}/contrib/build/webrtc-audio-processing" ) endif() @@ -190,6 +189,9 @@ if(MSVC) "NOMINMAX;" "HAVE_CONFIG_H;" "WIN32_LEAN_AND_MEAN;" + "WEBRTC_WIN;" + "WEBRTC_AUDIO_PROCESSING_ONLY_BUILD;" + "WEBRTC_NS_FLOAT;" ) endif() @@ -274,6 +276,7 @@ if(MSVC) ${CMAKE_CURRENT_SOURCE_DIR}/contrib/build/openssl/libcrypto.lib ${CMAKE_CURRENT_SOURCE_DIR}/contrib/build/openssl/libssl.lib ${CMAKE_CURRENT_SOURCE_DIR}/contrib/build/speexdsp/lib/libspeexdsp.lib + ${CMAKE_CURRENT_SOURCE_DIR}/contrib/build/webrtc-audio-processing/build/Release/webrtc-audio-processing.lib /ignore:4006 " ) diff --git a/compat/msvc/config.h b/compat/msvc/config.h index a45cd4de226a7b38ca1b7990b15e90a890e24c5e..ed4b174ae857cf006ffef694aabd40714625660f 100644 --- a/compat/msvc/config.h +++ b/compat/msvc/config.h @@ -13,7 +13,7 @@ systems. This function is required for `alloca.c' support on those systems. #define HAVE_ALLOCA 1 /* Define to 1 if you have <alloca.h> and it should be used (not on Ultrix). -*/ + */ #define HAVE_ALLOCA_H 1 /* Define if you have alsa */ @@ -100,6 +100,9 @@ systems. This function is required for `alloca.c' support on those systems. /* Define if you have libspeexdsp */ #define HAVE_SPEEXDSP 1 +/* Define if you have webrtc-audio-processing */ +#define HAVE_WEBRTC_AP 1 + /* Define to 1 if stdbool.h conforms to C99. */ #define HAVE_STDBOOL_H 1 diff --git a/src/media/audio/audiolayer.cpp b/src/media/audio/audiolayer.cpp index 927db4896d6171e5f73dff7e24483adf90a410d4..2fa6cc4d4288c6122f9f5956f0aed293e3c65bc8 100644 --- a/src/media/audio/audiolayer.cpp +++ b/src/media/audio/audiolayer.cpp @@ -27,7 +27,13 @@ #include "audio/resampler.h" #include "tonecontrol.h" #include "client/ring_signal.h" + +// aec +#if HAVE_WEBRTC_AP +#include "echo-cancel/webrtc_echo_canceller.h" +#else #include "echo-cancel/null_echo_canceller.h" +#endif #include <ctime> #include <algorithm> @@ -118,12 +124,32 @@ AudioLayer::setHasNativeAEC(bool hasEAC) void AudioLayer::checkAEC() { + std::lock_guard<std::mutex> lk(ecMutex_); bool shouldSoftAEC = not hasNativeAEC_ and playbackStarted_ and recordStarted_; - if (not echoCanceller_ and shouldSoftAEC) { - JAMI_WARN("Starting AEC"); - echoCanceller_.reset(new NullEchoCanceller(audioFormat_, audioFormat_.sample_rate / 100)); - } else if (echoCanceller_ and not shouldSoftAEC) { + auto nb_channels = std::min(audioFormat_.nb_channels, audioInputFormat_.nb_channels); + auto sample_rate = std::min(audioFormat_.sample_rate, audioInputFormat_.sample_rate); + if (sample_rate % 16000u != 0) + sample_rate = 16000u * ((sample_rate / 16000u) + 1u); + sample_rate = std::clamp(sample_rate, 16000u, 96000u); + AudioFormat format {sample_rate, nb_channels}; + auto frame_size = sample_rate / 100u; + JAMI_WARN("Input {%d Hz, %d channels}", + audioInputFormat_.sample_rate, + audioInputFormat_.nb_channels); + JAMI_WARN("Output {%d Hz, %d channels}", audioFormat_.sample_rate, audioFormat_.nb_channels); + JAMI_WARN("Starting AEC {%d Hz, %d channels, %d samples/frame}", + sample_rate, + nb_channels, + frame_size); + +#if HAVE_WEBRTC_AP + echoCanceller_.reset(new WebRTCEchoCanceller(format, frame_size)); +#else + echoCanceller_.reset(new NullEchoCanceller(format, frame_size)); +#endif + } else if (echoCanceller_ and not shouldSoftAEC and not playbackStarted_ + and not recordStarted_) { JAMI_WARN("Stopping AEC"); echoCanceller_.reset(); } @@ -209,11 +235,18 @@ AudioLayer::getToPlay(AudioFormat format, size_t writableSamples) } else if (auto buf = bufferPool.getData(RingBufferPool::DEFAULT_ID)) { resampled = resampler_->resample(std::move(buf), format); } else { + if (echoCanceller_) { + auto silence = std::make_shared<AudioFrame>(format, writableSamples); + libav_utils::fillWithSilence(silence->pointer()); + std::lock_guard<std::mutex> lk(ecMutex_); + echoCanceller_->putPlayback(silence); + } break; } if (resampled) { if (echoCanceller_) { + std::lock_guard<std::mutex> lk(ecMutex_); echoCanceller_->putPlayback(resampled); } playbackQueue_->enqueue(std::move(resampled)); @@ -228,9 +261,11 @@ void AudioLayer::putRecorded(std::shared_ptr<AudioFrame>&& frame) { if (echoCanceller_) { + std::lock_guard<std::mutex> lk(ecMutex_); echoCanceller_->putRecorded(std::move(frame)); - while (auto rec = echoCanceller_->getProcessed()) + 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 679b35610c5b284ae38440dfaac64bd411582d49..79ad74d36b24c13277d3fec0c38693f356b56b47 100644 --- a/src/media/audio/audiolayer.h +++ b/src/media/audio/audiolayer.h @@ -289,6 +289,7 @@ protected: */ std::unique_ptr<Resampler> resampler_; + std::mutex ecMutex_ {}; std::unique_ptr<EchoCanceller> echoCanceller_; private: diff --git a/src/media/audio/echo-cancel/CMakeLists.txt b/src/media/audio/echo-cancel/CMakeLists.txt index fec1af7d8f9cf8e5df463c637ab74cb28cb07e42..93e8440fbaa4ed620880cdca7b4098f86360bd35 100644 --- a/src/media/audio/echo-cancel/CMakeLists.txt +++ b/src/media/audio/echo-cancel/CMakeLists.txt @@ -7,6 +7,8 @@ list (APPEND Source_Files__media__audio__echo_cancel "${CMAKE_CURRENT_SOURCE_DIR}/null_echo_canceller.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/speex_echo_canceller.h" "${CMAKE_CURRENT_SOURCE_DIR}/speex_echo_canceller.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/webrtc_echo_canceller.h" + "${CMAKE_CURRENT_SOURCE_DIR}/webrtc_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 index 2222ce86a5758d60d9e3fb6007a6dbd679e65010..a7eab76fe0f13c97380b952edc9a23dc6c717d4b 100644 --- a/src/media/audio/echo-cancel/Makefile.am +++ b/src/media/audio/echo-cancel/Makefile.am @@ -10,6 +10,11 @@ EC_SRC += speex_echo_canceller.cpp EC_HDR += speex_echo_canceller.h endif +if HAVE_WEBRTC_AP +EC_SRC += webrtc_echo_canceller.cpp +EC_HDR += webrtc_echo_canceller.h +endif + libecho_cancel_la_SOURCES = \ $(EC_SRC) diff --git a/src/media/audio/echo-cancel/echo_canceller.h b/src/media/audio/echo-cancel/echo_canceller.h index c159957f4866c0224189d798eaf8764ba1ddb1b7..b925f2fa994361df7ef389cf9a546907a2acb552 100644 --- a/src/media/audio/echo-cancel/echo_canceller.h +++ b/src/media/audio/echo-cancel/echo_canceller.h @@ -22,9 +22,13 @@ #include "noncopyable.h" #include "audio/audio_frame_resizer.h" +#include "audio/resampler.h" #include "audio/audiobuffer.h" #include "libav_deps.h" +#include <atomic> +#include <memory> + namespace jami { class EchoCanceller @@ -36,19 +40,26 @@ public: EchoCanceller(AudioFormat format, unsigned frameSize) : playbackQueue_(format, frameSize) , recordQueue_(format, frameSize) - , sampleRate_(format.sample_rate) + , resampler_(new Resampler) + , format_(format) , frameSize_(frameSize) {} virtual ~EchoCanceller() = default; virtual void putRecorded(std::shared_ptr<AudioFrame>&& buf) { - recordQueue_.enqueue(std::move(buf)); + recordStarted_ = true; + if (!playbackStarted_) + return; + enqueue(recordQueue_, std::move(buf)); }; virtual void putPlayback(const std::shared_ptr<AudioFrame>& buf) { - auto c = buf; - playbackQueue_.enqueue(std::move(c)); + playbackStarted_ = true; + if (!recordStarted_) + return; + auto copy = buf; + enqueue(playbackQueue_, std::move(copy)); }; virtual std::shared_ptr<AudioFrame> getProcessed() = 0; virtual void done() = 0; @@ -56,8 +67,21 @@ public: protected: AudioFrameResizer playbackQueue_; AudioFrameResizer recordQueue_; - unsigned sampleRate_; + std::unique_ptr<Resampler> resampler_; + std::atomic_bool playbackStarted_; + std::atomic_bool recordStarted_; + AudioFormat format_; unsigned frameSize_; + +private: + void enqueue(AudioFrameResizer& frameResizer, std::shared_ptr<AudioFrame>&& buf) + { + if (buf->getFormat() != format_) { + auto resampled = resampler_->resample(std::move(buf), format_); + frameResizer.enqueue(std::move(resampled)); + } else + frameResizer.enqueue(std::move(buf)); + }; }; } // namespace jami diff --git a/src/media/audio/echo-cancel/speex_echo_canceller.cpp b/src/media/audio/echo-cancel/speex_echo_canceller.cpp index aace775c12372fdb7776f59def00cfbf0fe2eb7c..463292720d759eeddc5de2d383efed13903228e8 100644 --- a/src/media/audio/echo-cancel/speex_echo_canceller.cpp +++ b/src/media/audio/echo-cancel/speex_echo_canceller.cpp @@ -50,7 +50,7 @@ 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_); + speex_echo_ctl(pimpl_->state.get(), SPEEX_ECHO_SET_SAMPLING_RATE, &format_.sample_rate); } void diff --git a/src/media/audio/echo-cancel/webrtc_echo_canceller.cpp b/src/media/audio/echo-cancel/webrtc_echo_canceller.cpp new file mode 100644 index 0000000000000000000000000000000000000000..8b20507edce1b3520851113c8339eb31da8a921d --- /dev/null +++ b/src/media/audio/echo-cancel/webrtc_echo_canceller.cpp @@ -0,0 +1,170 @@ +/* + * 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 "webrtc_echo_canceller.h" + +#include <webrtc/modules/audio_processing/include/audio_processing.h> + +namespace jami { + +WebRTCEchoCanceller::WebRTCEchoCanceller(AudioFormat format, unsigned frameSize) + : EchoCanceller(format, frameSize) + , pimpl_(std::make_unique<WebRTCAPMImpl>(format, frameSize)) + , fRecordBuffer_(format.nb_channels, std::vector<float>(frameSize_, 0)) + , fPlaybackBuffer_(format.nb_channels, std::vector<float>(frameSize_, 0)) + , iRecordBuffer_(frameSize_, format) + , iPlaybackBuffer_(frameSize_, format) +{} + +struct WebRTCEchoCanceller::WebRTCAPMImpl +{ + using APMPtr = std::unique_ptr<webrtc::AudioProcessing>; + APMPtr apm; + webrtc::StreamConfig streamConfig; + + WebRTCAPMImpl(AudioFormat format, unsigned frameSize) + : streamConfig(format.sample_rate, format.nb_channels) + { + webrtc::ProcessingConfig pconfig; + webrtc::Config config; + + config.Set<webrtc::ExtendedFilter>(new webrtc::ExtendedFilter(true)); + config.Set<webrtc::DelayAgnostic>(new webrtc::DelayAgnostic(true)); + + apm.reset(webrtc::AudioProcessing::Create(config)); + + pconfig = { + streamConfig, /* input stream */ + streamConfig, /* output stream */ + streamConfig, /* reverse input stream */ + streamConfig, /* reverse output stream */ + }; + + if (apm->Initialize(pconfig) != webrtc::AudioProcessing::kNoError) { + JAMI_ERR("[webrtc-ap] Error initialising audio processing module"); + } + + // aec + apm->echo_cancellation()->set_suppression_level( + webrtc::EchoCancellation::SuppressionLevel::kModerateSuppression); + apm->echo_cancellation()->enable_drift_compensation(true); + apm->echo_cancellation()->Enable(true); + + // hpf + apm->high_pass_filter()->Enable(true); + + // ns + apm->noise_suppression()->set_level(webrtc::NoiseSuppression::kHigh); + apm->noise_suppression()->Enable(true); + + // agc + apm->gain_control()->set_analog_level_limits(0, 255); + apm->gain_control()->set_mode(webrtc::GainControl::kAdaptiveAnalog); + apm->gain_control()->Enable(true); + } +}; + +void +WebRTCEchoCanceller::putRecorded(std::shared_ptr<AudioFrame>&& buf) +{ + EchoCanceller::putRecorded(std::move(buf)); +} + +void +WebRTCEchoCanceller::putPlayback(const std::shared_ptr<AudioFrame>& buf) +{ + EchoCanceller::putPlayback(buf); +} + +std::shared_ptr<AudioFrame> +WebRTCEchoCanceller::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()) { + // If there are not enough samples in either queue, we can't + // process anything. + // JAMI_DBG("underrun p:%d / r:%d", playbackQueue_.samples(), recordQueue_.samples()); + return {}; + } + + int driftSamples = playbackQueue_.samples() - recordQueue_.samples(); + + auto playback = playbackQueue_.dequeue(); + auto record = recordQueue_.dequeue(); + if (!playback || !record) + return {}; + + auto processed = std::make_shared<AudioFrame>(format_, frameSize_); + + webrtc::StreamConfig& sc = pimpl_->streamConfig; + + // analyze deinterleaved float playback data + iPlaybackBuffer_.deinterleave((const AudioSample*) playback->pointer()->data[0], + frameSize_, + format_.nb_channels); + std::vector<float*> playData {format_.nb_channels}; + for (auto c = 0; c < format_.nb_channels; ++c) { + playData[c] = fPlaybackBuffer_[c].data(); + iPlaybackBuffer_.channelToFloat(playData[c], c); + } + if (pimpl_->apm->ProcessReverseStream(playData.data(), sc, sc, playData.data()) + != webrtc::AudioProcessing::kNoError) + JAMI_ERR("[webrtc-ap] ProcessReverseStream failed"); + + // process deinterleaved float recorded data + iRecordBuffer_.deinterleave((const AudioSample*) record->pointer()->data[0], + frameSize_, + format_.nb_channels); + std::vector<float*> recData {format_.nb_channels}; + for (auto c = 0; c < format_.nb_channels; ++c) { + recData[c] = fRecordBuffer_[c].data(); + iRecordBuffer_.channelToFloat(recData[c], c); + } + // TODO: implement this correctly (it MUST be called prior to ProcessStream) + // delay = (t_render - t_analyze) + (t_process - t_capture) + pimpl_->apm->set_stream_delay_ms(0); + pimpl_->apm->gain_control()->set_stream_analog_level(analogLevel_); + pimpl_->apm->echo_cancellation()->set_stream_drift_samples(driftSamples); + if (pimpl_->apm->ProcessStream(recData.data(), sc, sc, recData.data()) + != webrtc::AudioProcessing::kNoError) + JAMI_ERR("[webrtc-ap] ProcessStream failed"); + analogLevel_ = pimpl_->apm->gain_control()->stream_analog_level(); + + // return interleaved s16 data + iRecordBuffer_.convertFloatPlanarToSigned16((uint8_t**) recData.data(), + frameSize_, + format_.nb_channels); + iRecordBuffer_.interleave((AudioSample*) processed->pointer()->data[0]); + return processed; +} + +void +WebRTCEchoCanceller::done() +{} + +} // namespace jami diff --git a/src/media/audio/echo-cancel/webrtc_echo_canceller.h b/src/media/audio/echo-cancel/webrtc_echo_canceller.h new file mode 100644 index 0000000000000000000000000000000000000000..02e7eece35522c0af1489c51fda10506774ed002 --- /dev/null +++ b/src/media/audio/echo-cancel/webrtc_echo_canceller.h @@ -0,0 +1,53 @@ +/* + * 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" + +#include <memory> + +namespace jami { + +class WebRTCEchoCanceller final : public EchoCanceller +{ +public: + WebRTCEchoCanceller(AudioFormat format, unsigned frameSize); + ~WebRTCEchoCanceller() = 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 WebRTCAPMImpl; + std::unique_ptr<WebRTCAPMImpl> pimpl_; + + using fChannelBuffer = std::vector<std::vector<float>>; + fChannelBuffer fRecordBuffer_; + fChannelBuffer fPlaybackBuffer_; + AudioBuffer iRecordBuffer_; + AudioBuffer iPlaybackBuffer_; + int analogLevel_ {0}; +}; +} // namespace jami diff --git a/src/media/audio/portaudio/portaudiolayer.cpp b/src/media/audio/portaudio/portaudiolayer.cpp index 3a6aca9fb8c5611f5254e764635abf88108449bb..f473c11875c9981251c18ce595c53b9a81ba25f3 100644 --- a/src/media/audio/portaudio/portaudiolayer.cpp +++ b/src/media/audio/portaudio/portaudiolayer.cpp @@ -91,6 +91,8 @@ PortAudioLayer::PortAudioLayer(const AudioPreference& pref) : AudioLayer {pref} , pimpl_ {new PortAudioLayerImpl(*this, pref)} { + setHasNativeAEC(false); + auto numDevices = Pa_GetDeviceCount(); if (numDevices < 0) { JAMI_ERR("Pa_CountDevices returned 0x%x", numDevices);