|
9 | 9 | #include <speex/speex_resampler.h> |
10 | 10 |
|
11 | 11 | #include <algorithm> |
| 12 | +#include <utility> |
12 | 13 |
|
13 | 14 | #include "AudibilityMonitor.h" |
14 | 15 | #include "AudioConfig.h" |
@@ -150,14 +151,21 @@ SpeechRecognitionIPCActorUserGuard::~SpeechRecognitionIPCActorUserGuard() { |
150 | 151 | } |
151 | 152 | } |
152 | 153 |
|
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; |
154 | 164 | static constexpr uint32_t STREAMING_POLL_MS = 20; |
155 | 165 | static constexpr int32_t SPEECH_RECOGNITION_TARGET_RATE = 16000; |
156 | 166 | 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. |
161 | 169 | static constexpr uint32_t PER_CALLBACK_MONO_BUFFER_INITIAL_NUM_FRAMES = 512; |
162 | 170 |
|
163 | 171 | /* static */ |
@@ -192,8 +200,8 @@ SpeechRecognitionBackend::SpeechRecognitionBackend( |
192 | 200 | : mParent(aParent), |
193 | 201 | mLanguage(NS_ConvertUTF16toUTF8(aLanguage)), |
194 | 202 | 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))), |
197 | 205 | mResamplingThread(aResamplingThread), |
198 | 206 | mResamplingCapability(aResamplingThread), |
199 | 207 | mMonoBuffer(PER_CALLBACK_MONO_BUFFER_INITIAL_NUM_FRAMES), |
@@ -414,36 +422,40 @@ void SpeechRecognitionBackend::DataCallback(MediaTrackGraph* aGraph, |
414 | 422 | return; |
415 | 423 | } |
416 | 424 |
|
417 | | - size_t frameCount = static_cast<size_t>(aChunk.mDuration); |
| 425 | + const size_t frameCount = static_cast<size_t>(aChunk.mDuration); |
418 | 426 | // A null chunk is silence the graph did not bother to materialize, not an |
419 | 427 | // absence of audio, so it is fed as zeros rather than dropped. Dropping it |
420 | 428 | // would splice together the audio on either side of a silent gap, hiding the |
421 | 429 | // silence that ends an utterance from the recognizer. |
422 | 430 | const bool isSilence = aChunk.IsNull(); |
423 | 431 |
|
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. |
433 | 435 | AudioDataValue* monoData = mMonoBuffer.Elements(); |
434 | 436 | Span<AudioDataValue* const> outputChannels(&monoData, 1); |
| 437 | + const size_t capacity = mMonoBuffer.Capacity(); |
435 | 438 |
|
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); |
441 | 442 |
|
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 | + } |
444 | 450 |
|
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 | + } |
447 | 459 | } |
448 | 460 | } |
449 | 461 |
|
|
0 commit comments