From 098a1dfc7a7131a2895522501b45a691533240a5 Mon Sep 17 00:00:00 2001 From: Jonathan Nunez Aguin Date: Wed, 8 Jul 2026 12:07:57 +0200 Subject: [PATCH] Fix headless ADM playout drift below 100 Hz PlayThreadProcess/CaptureThreadProcess paced with a stateless relative sleep (SleepMs(10 - work)) that compensates for work time but not the sleep's own wake-up latency, and re-anchored the reference to the wall clock each tick. Wake-up overhead (~1.5 ms on Linux, more under virtualization) therefore accumulated into the frame period, so playout pulled at ~87 Hz instead of 100 Hz and NetEq dropped ~13% of inbound audio to bound latency. Sleep to an absolute 10 ms grid advanced by a fixed +10 each tick so latency is corrected against the deadline instead of accumulating, with a resync guard to avoid frame bursts after a long stall. Fixes #243 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../include/api/HeadlessAudioDeviceModule.h | 7 ++- .../cpp/src/api/HeadlessAudioDeviceModule.cpp | 53 ++++++++++++++----- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/webrtc-jni/src/main/cpp/include/api/HeadlessAudioDeviceModule.h b/webrtc-jni/src/main/cpp/include/api/HeadlessAudioDeviceModule.h index c3ae3c34..8d2c7e63 100644 --- a/webrtc-jni/src/main/cpp/include/api/HeadlessAudioDeviceModule.h +++ b/webrtc-jni/src/main/cpp/include/api/HeadlessAudioDeviceModule.h @@ -166,8 +166,11 @@ namespace jni size_t playoutFramesIn10MS_; size_t recordingFramesIn10MS_; - int64_t lastCallPlayoutMillis_; - int64_t lastCallRecordMillis_; + // Absolute wall-clock deadline (ms) of the next 10 ms tick. Advanced by a + // fixed +10 each tick so scheduling/wake-up latency is corrected against the + // grid rather than accumulating into the frame period. + int64_t nextPlayoutMillis_; + int64_t nextRecordMillis_; mutable webrtc::Mutex mutex_; std::unique_ptr audio_device_buffer_ RTC_GUARDED_BY(mutex_); diff --git a/webrtc-jni/src/main/cpp/src/api/HeadlessAudioDeviceModule.cpp b/webrtc-jni/src/main/cpp/src/api/HeadlessAudioDeviceModule.cpp index 83fa9d84..4e069ce5 100644 --- a/webrtc-jni/src/main/cpp/src/api/HeadlessAudioDeviceModule.cpp +++ b/webrtc-jni/src/main/cpp/src/api/HeadlessAudioDeviceModule.cpp @@ -25,8 +25,8 @@ namespace jni channels_(channels ? channels : 1), playoutFramesIn10MS_(0), recordingFramesIn10MS_(0), - lastCallPlayoutMillis_(0), - lastCallRecordMillis_(0), + nextPlayoutMillis_(0), + nextRecordMillis_(0), audio_callback_(nullptr) { audio_device_buffer_ = std::make_unique(&env.task_queue_factory()); @@ -553,22 +553,35 @@ namespace jni int64_t currentTime = webrtc::TimeMillis(); mutex_.Lock(); - if (lastCallPlayoutMillis_ == 0 || currentTime - lastCallPlayoutMillis_ >= 10) { + // Seed the grid on the first tick. + if (nextPlayoutMillis_ == 0) { + nextPlayoutMillis_ = currentTime; + } + + if (currentTime >= nextPlayoutMillis_) { mutex_.Unlock(); audio_device_buffer_->RequestPlayoutData(playoutFramesIn10MS_); mutex_.Lock(); audio_device_buffer_->GetPlayoutData(play_buffer_.data()); - lastCallPlayoutMillis_ = currentTime; + // Advance the grid by a fixed 10 ms rather than re-anchoring to currentTime, + // so wake-up latency is corrected on the next tick instead of accumulating + // into the frame period (which otherwise pulls the effective rate below 100 Hz). + nextPlayoutMillis_ += 10; + + // If we fell far behind (e.g. the thread was descheduled), resync to now + // instead of bursting frames to catch up. + if (nextPlayoutMillis_ < currentTime - 100) { + nextPlayoutMillis_ = currentTime; + } } + int64_t sleepMillis = nextPlayoutMillis_ - webrtc::TimeMillis(); mutex_.Unlock(); - int64_t deltaTimeMillis = webrtc::TimeMillis() - currentTime; - - if (deltaTimeMillis < 10) { - webrtc::Thread::SleepMs(10 - deltaTimeMillis); + if (sleepMillis > 0) { + webrtc::Thread::SleepMs(sleepMillis); } return true; @@ -588,7 +601,12 @@ namespace jni int64_t currentTime = webrtc::TimeMillis(); mutex_.Lock(); - if (lastCallRecordMillis_ == 0 || currentTime - lastCallRecordMillis_ >= 10) { + // Seed the grid on the first tick. + if (nextRecordMillis_ == 0) { + nextRecordMillis_ = currentTime; + } + + if (currentTime >= nextRecordMillis_) { size_t nSamplesOut = 0; const size_t nBytesPerSample = sizeof(int16_t); const size_t nChannels = channels_; @@ -617,7 +635,16 @@ namespace jni audio_device_buffer_->SetRecordedBuffer(record_buffer_.data(), recordingFramesIn10MS_); audio_device_buffer_->SetVQEData(/*play_delay_ms*/ 0, /*rec_delay_ms*/ 0); - lastCallRecordMillis_ = currentTime; + // Advance the grid by a fixed 10 ms rather than re-anchoring to currentTime, + // so wake-up latency is corrected on the next tick instead of accumulating + // into the frame period (which otherwise pulls the effective rate below 100 Hz). + nextRecordMillis_ += 10; + + // If we fell far behind (e.g. the thread was descheduled), resync to now + // instead of bursting frames to catch up. + if (nextRecordMillis_ < currentTime - 100) { + nextRecordMillis_ = currentTime; + } mutex_.Unlock(); audio_device_buffer_->DeliverRecordedData(); @@ -625,11 +652,11 @@ namespace jni } } + int64_t sleepMillis = nextRecordMillis_ - webrtc::TimeMillis(); mutex_.Unlock(); - int64_t deltaTimeMillis = webrtc::TimeMillis() - currentTime; - if (deltaTimeMillis < 10) { - webrtc::Thread::SleepMs(10 - deltaTimeMillis); + if (sleepMillis > 0) { + webrtc::Thread::SleepMs(sleepMillis); } return true;