Skip to content

Commit cede31e

Browse files
committed
Bug 1940906 - Stream audio in smaller blocks; implement chunked downmixing. r=media-playback-reviewers,alwu
Send audio to the inference process in ~40ms blocks instead of 0.5s, so end-to-end latency is bounded by the streaming model (~180ms) rather than IPC batching. Implement the chunked downmixing path in DataCallback (slice-wise, RT-safe) instead of crashing, and stop clearing the graph-thread scratch buffer from the main thread in Stop() (a data race the lower latency exposed). Count the amount of frame dropped, increase ringbuffer depth to smooth over load spike events. Differential Revision: https://phabricator.services.mozilla.com/D311103
1 parent aab659c commit cede31e

2 files changed

Lines changed: 41 additions & 26 deletions

File tree

‎dom/media/webspeech/recognition/SpeechRecognitionBackend.cpp‎

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <speex/speex_resampler.h>
1010

1111
#include <algorithm>
12+
#include <utility>
1213

1314
#include "AudibilityMonitor.h"
1415
#include "AudioConfig.h"
@@ -150,14 +151,21 @@ SpeechRecognitionIPCActorUserGuard::~SpeechRecognitionIPCActorUserGuard() {
150151
}
151152
}
152153

153-
static constexpr double IPC_BLOCK_SIZE_S = 0.5;
154+
// Audio is streamed to the inference process in small blocks to keep end-to-end
155+
// latency low. The block is well under the model's internal chunk (~80 ms), so
156+
// the buffering delay is hidden behind the model's own latency rather than
157+
// adding to it.
158+
static constexpr double IPC_BLOCK_SIZE_S = 0.04;
159+
// Depth costs no latency (the whole buffer is drained every poll), but having
160+
// this significantly larger than the block size allows not dropping audio
161+
// during long stalls under load -- some of the threads used to convey the audio
162+
// aren't real-time, and can be starved.
163+
static constexpr double RING_BUFFER_SIZE_S = 2.0;
154164
static constexpr uint32_t STREAMING_POLL_MS = 20;
155165
static constexpr int32_t SPEECH_RECOGNITION_TARGET_RATE = 16000;
156166
static constexpr auto SPEECH_RECOGNITION_ENGINE_ID = "parakeet-cpp"_ns;
157-
// The ring buffer holds graph-rate audio, not yet downsampled to
158-
// SPEECH_RECOGNITION_TARGET_RATE, with this much headroom for the resampling
159-
// thread being late to dequeue.
160-
static constexpr uint32_t RING_BUFFER_IPC_BLOCKS = 4;
167+
// DataCallback() downmixes into mMonoBuffer in slices of at most this many
168+
// frames, so this is the only allocation the graph thread normally needs.
161169
static constexpr uint32_t PER_CALLBACK_MONO_BUFFER_INITIAL_NUM_FRAMES = 512;
162170

163171
/* static */
@@ -192,8 +200,8 @@ SpeechRecognitionBackend::SpeechRecognitionBackend(
192200
: mParent(aParent),
193201
mLanguage(NS_ConvertUTF16toUTF8(aLanguage)),
194202
mPhrases(aPhrases.Clone()),
195-
mRingBuffer(MakeUnique<SPSCQueue<float>>(AssertedCast<int>(
196-
aGraphRate * IPC_BLOCK_SIZE_S * RING_BUFFER_IPC_BLOCKS))),
203+
mRingBuffer(MakeUnique<SPSCQueue<float>>(
204+
AssertedCast<int>(aGraphRate * RING_BUFFER_SIZE_S))),
197205
mResamplingThread(aResamplingThread),
198206
mResamplingCapability(aResamplingThread),
199207
mMonoBuffer(PER_CALLBACK_MONO_BUFFER_INITIAL_NUM_FRAMES),
@@ -414,36 +422,40 @@ void SpeechRecognitionBackend::DataCallback(MediaTrackGraph* aGraph,
414422
return;
415423
}
416424

417-
size_t frameCount = static_cast<size_t>(aChunk.mDuration);
425+
const size_t frameCount = static_cast<size_t>(aChunk.mDuration);
418426
// A null chunk is silence the graph did not bother to materialize, not an
419427
// absence of audio, so it is fed as zeros rather than dropped. Dropping it
420428
// would splice together the audio on either side of a silent gap, hiding the
421429
// silence that ends an utterance from the recognizer.
422430
const bool isSilence = aChunk.IsNull();
423431

424-
if (mMonoBuffer.Capacity() < frameCount) {
425-
LOGE("Warning: chunk size {} exceeds pre-allocated buffer capacity {}",
426-
frameCount, mMonoBuffer.Capacity());
427-
mMonoBuffer.SetCapacity(frameCount);
428-
MOZ_DIAGNOSTIC_CRASH("Implement chunked downmixing");
429-
}
430-
431-
mMonoBuffer.SetLengthAndRetainStorage(frameCount);
432-
432+
// Downmix to mono into the fixed-size scratch buffer and enqueue. A single
433+
// graph chunk can be larger than the scratch buffer, so process it in slices
434+
// that fit, avoiding any allocation on the real-time graph thread.
433435
AudioDataValue* monoData = mMonoBuffer.Elements();
434436
Span<AudioDataValue* const> outputChannels(&monoData, 1);
437+
const size_t capacity = mMonoBuffer.Capacity();
435438

436-
if (isSilence) {
437-
PodZero(mMonoBuffer.Elements(), frameCount);
438-
} else {
439-
aChunk.DownMixTo(outputChannels);
440-
}
439+
for (size_t offset = 0; offset < frameCount; offset += capacity) {
440+
const size_t sliceFrames = std::min(capacity, frameCount - offset);
441+
mMonoBuffer.SetLengthAndRetainStorage(sliceFrames);
441442

442-
int written = mRingBuffer->Enqueue(mMonoBuffer.Elements(),
443-
AssertedCast<int>(frameCount));
443+
if (isSilence) {
444+
PodZero(mMonoBuffer.Elements(), sliceFrames);
445+
} else {
446+
AudioChunk slice = aChunk;
447+
slice.SliceTo(offset, offset + sliceFrames);
448+
slice.DownMixTo(outputChannels);
449+
}
444450

445-
if (written < static_cast<int>(frameCount)) {
446-
LOG("Ring buffer overflow: wrote {} of {} frames", written, frameCount);
451+
int written = mRingBuffer->Enqueue(mMonoBuffer.Elements(),
452+
AssertedCast<int>(sliceFrames));
453+
if (written < static_cast<int>(sliceFrames)) {
454+
mFramesDropped += sliceFrames - written;
455+
LOGE("Capture ring buffer overflow: wrote {} of {} frames, {}s"
456+
" dropped total", written, sliceFrames,
457+
double(mFramesDropped) / mGraphRate);
458+
}
447459
}
448460
}
449461

‎dom/media/webspeech/recognition/SpeechRecognitionBackend.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,9 @@ class SpeechRecognitionBackend {
245245
// DetachFromTrack() has stopped the callbacks.
246246
nsTArray<AudioDataValue> mMonoBuffer;
247247
const uint32_t mGraphRate;
248+
// Graph-thread only, number of frames that couldn't be pushed into
249+
// mRingBuffer and has been dropped.
250+
uint64_t mFramesDropped = 0;
248251
// Whether Shutdown() has already run, so it runs exactly once.
249252
bool mStopped MOZ_GUARDED_BY(sMainThreadCapability) = false;
250253
// Whether a soundstart was fired without its soundend. The main thread owns

0 commit comments

Comments
 (0)