optimize Live Call latency: 50ms chunks, smaller playback buffer

- Capture chunks 100ms → 50ms: halves input latency, model hears
  speech 50ms sooner per chunk
- Playback buffer minBuf*4 → minBuf*2: reduces output buffering
  delay while LOW_LATENCY mode keeps playback glitch-free
This commit is contained in:
Bruno Deanoz 2026-06-24 14:00:52 +02:00
parent 965f1876bf
commit 70b91cc7bb

View file

@ -20,15 +20,14 @@ import kotlin.math.sqrt
/** /**
* Bidirectional audio pipeline for Live Call. * Bidirectional audio pipeline for Live Call.
* *
* Capture: AudioRecord (16kHz PCM16 mono, VOICE_COMMUNICATION for hardware AEC) * Capture: AudioRecord (16kHz PCM16 mono, VOICE_COMMUNICATION + AEC)
* 100ms chunks Base64 [onChunk] callback. * 50ms chunks Base64 [onChunk] callback.
* *
* Playback: AudioTrack (24kHz PCM16 mono, LOW_LATENCY, MODE_STREAM) * Playback: AudioTrack (24kHz PCM16 mono, LOW_LATENCY, MODE_STREAM)
* PCM16 chunks from server. * PCM16 chunks from server.
* *
* Mic-ducking: gain reduced to 10% while the KI speaks (prevents echo feedback * Echo prevention: AcousticEchoCanceler (hardware) + hard ducking
* loop on devices with weak AEC). Not a digital gain we skip sending chunks * (zero mic chunks while model speaks).
* whose RMS is below the ducking threshold.
*/ */
class AudioPipeline(private val context: Context) { class AudioPipeline(private val context: Context) {
@ -41,11 +40,9 @@ class AudioPipeline(private val context: Context) {
private const val CHANNEL_OUT = AudioFormat.CHANNEL_OUT_MONO private const val CHANNEL_OUT = AudioFormat.CHANNEL_OUT_MONO
private const val ENCODING = AudioFormat.ENCODING_PCM_16BIT private const val ENCODING = AudioFormat.ENCODING_PCM_16BIT
private const val CHUNK_MS = 100 private const val CHUNK_MS = 50
private const val CAPTURE_CHUNK_SAMPLES = CAPTURE_SAMPLE_RATE * CHUNK_MS / 1000 // 1600 private const val CAPTURE_CHUNK_SAMPLES = CAPTURE_SAMPLE_RATE * CHUNK_MS / 1000 // 800
private const val CAPTURE_CHUNK_BYTES = CAPTURE_CHUNK_SAMPLES * 2 // 3200 private const val CAPTURE_CHUNK_BYTES = CAPTURE_CHUNK_SAMPLES * 2 // 1600
private const val DUCKING_GAIN = 0.10f
} }
var onChunk: ((base64Pcm: String) -> Unit)? = null var onChunk: ((base64Pcm: String) -> Unit)? = null
@ -164,7 +161,7 @@ class AudioPipeline(private val context: Context) {
.setChannelMask(CHANNEL_OUT) .setChannelMask(CHANNEL_OUT)
.build() .build()
) )
.setBufferSizeInBytes(maxOf(minBuf * 4, 24_000 * 2)) .setBufferSizeInBytes(maxOf(minBuf * 2, 24_000))
.setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY) .setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY)
.setTransferMode(AudioTrack.MODE_STREAM) .setTransferMode(AudioTrack.MODE_STREAM)
.build() .build()