summaryrefslogtreecommitdiffstats
path: root/pica-client/echocanceller.h
diff options
context:
space:
mode:
Diffstat (limited to 'pica-client/echocanceller.h')
-rw-r--r--pica-client/echocanceller.h174
1 files changed, 91 insertions, 83 deletions
diff --git a/pica-client/echocanceller.h b/pica-client/echocanceller.h
index e59f8d7..faf50dd 100644
--- a/pica-client/echocanceller.h
+++ b/pica-client/echocanceller.h
@@ -22,60 +22,79 @@
#include <QVector>
#include <QSharedPointer>
-struct SpeexEchoState_;
-struct SpeexPreprocessState_;
+struct EchoCancellerPrivate;
struct SwrContext;
-// Acoustic echo cancellation for a call, using speexdsp's frequency domain
-// adaptive filter (MDF).
+// Where the acoustic echo of a call is taken out of the microphone signal.
+// One of these three, never two at once: running a second canceller over an
+// already cancelled signal gives it a near silent reference to chase and it
+// chews holes in the speech.
+enum EchoCancellationMode
+{
+ // EchoCanceller below - WebRTC's AEC3, in process.
+ EchoCancellationOwn,
+
+ // Whatever the operating system or sound server does below us, which is
+ // better where it exists: it sees the real speaker signal and the real
+ // clock. Not offered everywhere - see
+ // AudioDevice::PlatformEchoCancellationAvailable().
+ EchoCancellationPlatform,
+
+ // Nothing cancels. Correct with a headset, and the only honest answer
+ // when both of the above make things worse.
+ EchoCancellationNone
+};
+
+// The "audio.echo_cancellation" setting. Both must be called from a thread
+// that may touch the settings database - see AudioDevice::SetLinuxDriverName()
+// for why the audio threads may not.
+//
+// Nothing stored yet means the default for this platform: the platform's own
+// where there is one, ours otherwise. The setting this replaced
+// ("audio.echo_cancel", a plain on/off) is still honoured when it is all
+// there is, so an existing install that had it switched off stays that way.
+EchoCancellationMode loadEchoCancellationSetting();
+void storeEchoCancellationSetting(EchoCancellationMode mode);
+
+// Acoustic echo cancellation for a call, using WebRTC's AEC3 through the
+// audio processing module.
//
// One instance is shared by the two AudioDevice objects of a call - the
// playback one feeds it the far end signal it is about to hand to the sound
// card via pushFarEnd(), the capture one runs the microphone signal through
-// processNearEnd() before encoding. Both run on their own thread, so every
-// entry point here takes a lock.
+// processNearEnd() before encoding. Both run on their own thread, which is
+// what AEC3 expects: one render thread, one capture thread.
//
// Because AudioVideoCallController tears a call down without joining those
// two threads (see stopAudioPipeline()), ownership is shared: hold this
// through a QSharedPointer so the object outlives whichever thread leaves
// last.
//
-// Alignment between the two signals is left to the FIFO: in the steady state
-// the playback side pushes samples at the same rate the capture side pulls
-// them, and the backlog that builds up in the FIFO settles at exactly the
-// sound card's buffer depth - which is also how far ahead of the speaker the
-// playback side is running. What remains is the acoustic flight time from
-// speaker to microphone plus any capture side buffering, and that is what the
-// filter tail is for.
+// One per call, built when the call starts and dropped when it ends. There is
+// nothing worth carrying over: the previous call's room response is not this
+// call's, and the module is cheap enough to build.
+//
+// Nothing here tries to line the two signals up. AEC3 has its own delay
+// estimator and a render buffer to hold the far end audio until the echo of
+// it arrives, which is the whole reason it copes with an application that
+// hands over playback audio some way ahead of the speaker actually making a
+// sound - and with that lead changing when the sound card's buffer does.
class EchoCanceller
{
public:
- // frameMs is the block the filter works on and must divide the audio
- // handed to processNearEnd().
- //
- // tailMs is the longest echo delay the filter can model at all; anything
- // arriving later is invisible to it and can only be dealt with
- // statistically by the residual suppressor. The fixed part of that delay
- // is small - the capture path plus a metre or two of flight, call it 30ms
- // - so nearly all of the tail is budget for the room's reverberation.
- //
- // It is not free in either direction. Too short and late reflections never
- // get modelled and are heard as residual echo; too long and both the
- // convergence time and the steady state misadjustment grow with the tap
- // count. 300ms at 16kHz is 4800 taps, which is still fewer than the 7200
- // this ran at when calls were 48kHz with a 150ms tail - so the room
- // coverage doubles and the filter is still shorter than it used to be.
- EchoCanceller(int sampleRate, int frameMs = 10, int tailMs = 300);
+ // sampleRate must be one of AEC3's native rates - 8000, 16000, 32000 or
+ // 48000. Anything else leaves isValid() false rather than silently
+ // processing at the wrong rate; the caller then runs without
+ // cancellation.
+ explicit EchoCanceller(int sampleRate);
~EchoCanceller();
- bool isValid() const { return m_echo != nullptr; }
+ bool isValid() const;
int sampleRate() const { return m_sampleRate; }
- int frameSize() const { return m_frameSize; }
- // Forget the adapted filter and drop any buffered far end audio. Called
- // when a call starts, since the previous call's room response is not
- // worth keeping and a stale FIFO would start it off misaligned.
- void reset();
+ // The block AEC3 works on: 10ms, always. Audio handed to
+ // processNearEnd() should be a whole number of these.
+ int frameSize() const { return m_frameSize; }
// Playback side: the audio about to be written to the output device.
//
@@ -83,9 +102,7 @@ public:
// we capture at a rate of our choosing but play back at whatever the peer
// announced, so a call with a build that negotiates a different rate ends
// up with the two directions disagreeing. Passing 0 means "same as ours".
- // Anything else is resampled on the way into the FIFO - without this the
- // playback side used to drop the canceller entirely on a rate mismatch,
- // leaving the capture side cancelling against silence.
+ // Anything else is resampled on the way in.
void pushFarEnd(const qint16 *pcm, int nsamples, int srcRate = 0);
// Capture side: removes the echo of what pushFarEnd() was given, in
@@ -93,74 +110,57 @@ public:
// partial trailing block is passed through untouched.
void processNearEnd(qint16 *pcm, int nsamples);
- // Rough measure of how well it is working, in dB - how much quieter the
- // output is than the microphone input while the far end is active. Zero
- // until enough far end audio has gone through to mean anything.
+ // Echo return loss enhancement in dB, as AEC3 measures it: how much
+ // quieter the echo is after cancellation than before. Zero until the
+ // filter has had far end audio to work on.
//
// Only meaningful while the far end is actually talking, which is what
// farEndLevelDb() is for: with nothing coming out of the speaker there is
// no echo to remove and the figure means nothing.
double erle();
- // Recent signal levels in dBFS, averaged over the same window as erle().
+ // Recent signal levels in dBFS, averaged over a few seconds.
// nearEndLevelDb() is the microphone before cancellation - a floor-level
// reading there means the capture path is dead and nothing else in the
// numbers is worth reading.
double nearEndLevelDb();
double farEndLevelDb();
- // Far end samples waiting to be paired with microphone audio. In the
- // steady state this settles at the depth of the sound card's output
- // buffer and stays there, because both are drained by the same clock.
- // Climbing towards m_farCapacity means capture and playback are running
- // off different clocks - separate cards - and the reference is drifting
- // out of alignment with the echo it is supposed to explain.
- int farEndDepth();
-
private:
Q_DISABLE_COPY(EchoCanceller)
int m_sampleRate;
int m_frameSize;
- int m_filterLength;
- // speexdsp state. The echo canceller proper, plus a preprocessor whose
- // only job here is the residual echo suppression that the adaptive
- // filter alone cannot do (SPEEX_PREPROCESS_SET_ECHO_STATE).
- SpeexEchoState_ *m_echo;
- SpeexPreprocessState_ *m_preprocess;
+ // The audio processing module and the stream configurations handed to it,
+ // kept out of this header so that the WebRTC headers - which want C++17
+ // and drag in abseil - are only seen by echocanceller.cpp.
+ EchoCancellerPrivate *m_d;
- // Guards the speex state, which both directions reach into: the capture
- // thread through speex_echo_cancellation(), and reset() from whichever
- // thread starts the call.
- QMutex m_procMutex;
+ // Guards the render direction: the far end accumulator, the resampler,
+ // and ProcessReverseStream(). Only the playback thread takes it.
+ QMutex m_renderMutex;
- // Far end FIFO, written by the playback thread and drained by the
- // capture thread.
- QMutex m_farMutex;
- QVector<qint16> m_farFifo;
- int m_farHead;
+ // Guards the capture direction: ProcessStream() and the statistics read
+ // alongside it. Taken by the capture thread, and by whoever calls the
+ // erle() accessor.
+ QMutex m_captureMutex;
+
+ // Far end audio that did not fill a whole 10ms block, waiting for the
+ // samples that will complete it.
+ QVector<qint16> m_farPending;
// Rate conversion for a far end that is not running at our rate, built on
- // first use and rebuilt if the rate changes. Only the playback thread
- // touches these, under m_farMutex.
+ // first use and rebuilt if the rate changes.
SwrContext *m_farSwr;
int m_farSwrRate;
QVector<qint16> m_farScratch;
- // Upper bound on the FIFO, in samples. Reached only if the two sound
- // cards' clocks drift apart or playback outruns capture; past this the
- // oldest audio is dropped, since keeping it would mean the reference
- // signal lagging further and further behind the echo it is meant to
- // explain.
- int m_farCapacity;
-
- // Exponentially averaged mean square of the near end (microphone), the
- // cancelled output and the far end reference, for erle() and the level
- // readings. Per sample rather than per call, so they stay comparable
- // whatever block size the capture path hands us.
+ // Exponentially averaged mean square of the near end (microphone) and of
+ // the far end reference, for the level readings. Per sample rather than
+ // per call, so they stay comparable whatever block size the capture path
+ // hands us.
double m_nearEnergy;
- double m_outEnergy;
double m_farEnergy;
QMutex m_statMutex;
@@ -170,13 +170,21 @@ private:
qint64 m_processedSamples;
qint64 m_nextReportSamples;
+ // 10ms blocks handed to each direction. AEC3 lines the two streams up by
+ // counting API calls, not by any clock, so the gap between these is what
+ // its alignment is measured in. In a healthy call it settles at the depth
+ // of the sound card's output buffer and stays there; a gap that keeps
+ // growing or shrinking means capture and playback are running off
+ // different clocks and the alignment is being dragged out from under the
+ // filter. Guarded by m_statMutex.
+ qint64 m_renderBlocks;
+ qint64 m_captureBlocks;
+
// How often the canceller reports how it is doing, in seconds.
static const int kReportIntervalSec = 2;
- // Scratch far end block, reused by processNearEnd().
- QVector<qint16> m_farBlock;
-
- bool popFarEnd(qint16 *dst, int nsamples);
+ // True if rate is one of the rates AEC3's integer interface accepts.
+ static bool isNativeRate(int rate);
};
typedef QSharedPointer<EchoCanceller> EchoCancellerPtr;