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:
+13
-8
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user