fix(nests): cap AudioTrack ring at ~250 ms so audio tracks the speaking-now ring

`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).
This commit is contained in:
Claude
2026-05-14 03:25:51 +00:00
parent dec56c2ee9
commit cd28de4eb1
@@ -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 {