From cd28de4eb1d1bfc4873d8bb51eb249000ab8b3c7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 14 May 2026 03:25:51 +0000 Subject: [PATCH] fix(nests): cap AudioTrack ring at ~250 ms so audio tracks the speaking-now ring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `AudioTrackPlayer` sized the AudioTrack ring at `max(minBuffer * 16, 250 ms)`. The intent (per the kdoc) was ~250 ms of jitter slack with the device's `getMinBufferSize` as a floor — but the `* 16` multiplier silently dominated on common devices. A Pixel reports `minBuffer ≈ 40 ms` for 48 kHz mono; `× 16 = 640 ms`. Add the 200 ms preroll in `NestPlayer` and decoded PCM sat in the ring for ~800 ms – 1 s before the speaker played it. Two-phone testing confirmed the symptom: the speaking-now ring lit up ~1 s before the listener's audio. The ring fires on `onLevel(peakAmplitude(pcm))` in `NestPlayer.play()` immediately after `decoder.decode()` succeeds — before `player.enqueue(pcm)` — so the ring is real-time and the delay was entirely between `enqueue` and the speaker. The web (NostrNests) listener uses an `AudioWorklet` with no equivalent ring buffer, so its audio aligned with the wire. Drop the `* 16` so the formula matches the intent: `max(minBuffer, 250 ms)`. On devices whose floor is below 250 ms (the common case) the 250 ms target wins; on devices whose floor is above 250 ms (rare) we still respect the device-reported minimum. Steady-state ear-to- speaker delay drops from ~1 s to ~450 ms (250 ms ring + 200 ms preroll). --- .../nestsclient/audio/AudioTrackPlayer.kt | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/nestsClient/src/androidMain/kotlin/com/vitorpamplona/nestsclient/audio/AudioTrackPlayer.kt b/nestsClient/src/androidMain/kotlin/com/vitorpamplona/nestsclient/audio/AudioTrackPlayer.kt index e32824ca5..28b162292 100644 --- a/nestsClient/src/androidMain/kotlin/com/vitorpamplona/nestsclient/audio/AudioTrackPlayer.kt +++ b/nestsClient/src/androidMain/kotlin/com/vitorpamplona/nestsclient/audio/AudioTrackPlayer.kt @@ -152,16 +152,21 @@ class AudioTrackPlayer( } // Target ~250 ms of audio: enough headroom so the decode loop can // miss its 20 ms cadence by an order of magnitude before the device - // underruns. Take the larger of `minBuffer * 16` and an explicit - // 250 ms-equivalent so devices that report a small minBuffer still - // get the same wall-clock slack. Stereo doubles the byte count - // per sample (interleaved L,R 16-bit shorts); a higher sample - // rate scales the per-second byte count linearly — both factor - // into the target so the wall-clock 250 ms target is preserved - // regardless of channel layout / sample rate. + // underruns. Use `minBuffer` as the floor — most devices report a + // floor well under 250 ms, so the 250 ms target wins and we cap + // at the intended wall-clock slack. The earlier `minBuffer * 16` + // multiplier silently inflated the ring to ~600 ms – 1 s on + // common handsets (Pixel `getMinBufferSize` ≈ 40 ms × 16 = 640 ms), + // so audio sat in the ring for that long before the speaker + // played it — visible as the speaking-now ring lighting up ~1 s + // before the listener heard the speaker. Stereo doubles the + // byte count per sample (interleaved L,R 16-bit shorts); a + // higher sample rate scales the per-second byte count linearly + // — both factor into the target so the wall-clock 250 ms target + // is preserved regardless of channel layout / sample rate. val targetBytes250Ms = (sampleRate / 4) * AudioFormat.BYTES_PER_SAMPLE * channelCount - val bufferBytes = maxOf(minBuffer * 16, targetBytes250Ms) + val bufferBytes = maxOf(minBuffer, targetBytes250Ms) val newTrack = try {