androidengineers.Book a session

Microphone capture and bounded input

Microphone capture: PCM, partial reads and bounded queues

article35–50 min

Audio has a contract before it has a model

PocketCook sends mono, little-endian, 16-bit PCM sampled at 16 kHz. PCM is a sequence of sample values, not an MP3 or a WAV file. A WAV header added to these bytes would become unwanted sample data unless removed. The output path uses a different rate, 24 kHz. Keep capture and playback configurations separate.

The Live API specifications describe these input/output formats. The lesson's queue sizes and timings come from the PocketCook implementation, not from a universal API requirement.

Calculate the input budget before changing a constant:

16,000 samples/second × 1 channel × 2 bytes/sample = 32,000 bytes/second
100 milliseconds = 3,200 bytes
8 queued chunks × 100 milliseconds = 800 milliseconds

The queue may also have one item in flight. Queue capacity is not an end-to-end latency measurement: device buffers, scheduling, encoding, socket backlog, model processing and playback add their own delay.

Permission and resource ownership

The Compose permission launcher requests RECORD_AUDIO only after an explicit voice action. On denial, the recipe remains usable and a notice offers settings. AudioRecord belongs to AndroidPcmAudio, not the composable. The adapter requests transient audio focus, uses communication mode and optionally enables platform echo cancellation when available. Those choices require physical-device evaluation; enabling an effect does not prove echo has disappeared.

// AndroidPcmAudio.kt — configuration excerpt
AudioFormat.Builder()
    .setSampleRate(16000)
    .setChannelMask(AudioFormat.CHANNEL_IN_MONO)
    .setEncoding(AudioFormat.ENCODING_PCM_16BIT)
    .build()

When reading, the sample uses READ_NON_BLOCKING. A read returns available data and can be shorter than the supplied buffer. See the AudioRecord reference. A buffer with room for 100 ms does not make every returned read 100 ms long.

The bug we actually encountered

The first implementation forwarded every partial read to an eight-item channel. Ten-millisecond fragments could therefore fill a queue intended to hold much more audio. The user saw “Audio capture exceeded its buffer.” Enlarging the item count alone would conceal the mismatch between items and time.

The committed fix introduces PcmChunker. It accumulates partial reads until it can emit exactly 3,200 bytes. It copies the full chunk before reusing its internal buffer, so the producer cannot overwrite bytes the consumer has not sent yet.

// PcmChunker.kt — complete append method, read in its class context
fun append(input: ByteArray, count: Int, emit: (ByteArray) -> Unit) {
    require(count in 0..input.size && count % 2 == 0)
    var offset = 0
    while (offset < count) {
        val length = minOf(pending.size - used, count - offset)
        input.copyInto(pending, used, offset, offset + length)
        used += length
        offset += length
        if (used == pending.size) {
            val chunk = pending.copyOf()
            used = 0
            emit(chunk)
        }
    }
}

count is the valid byte length returned by the read, not necessarily input.size. Even lengths preserve whole PCM16 samples. At the device boundary, the baseline truncates an odd trailing byte defensively before calling this method; do not generalize that to arbitrary compressed formats.

Backpressure needs a policy

Fixed chunks do not make overload impossible. If the consumer stays blocked, the bounded channel still fills. PocketCook then ends the session with a retry message. It does not silently drop arbitrary speech or allow memory to grow without limit. This is a deliberate learning-sample policy; an adaptive production policy would need measurements and its own tests.

The transport also checks a 256,000-byte outgoing socket backlog. Incoming messages and decoded audio have separate bounds. Each protects a different allocation or queue. Replacing all limits with one larger buffer would lose those distinctions.

Test duration, order and ownership

PcmChunkerTest simulates ten minutes of 10 ms fragments and checks every emitted byte, not only the chunk count. Another test fills half a chunk, clears at a mute boundary, then verifies that the next output contains no old samples. Neither test records a human voice or contacts Gemini.

Run these tests, then explain why passing them does not establish microphone permission handling or absence of acoustic echo. In the codelab you will add a configurable chunk-size contract while preserving the 100 ms default. Your acceptance criteria include even byte lengths, bounded capacity and no mutation of already-emitted chunks.

Course study guide · Hands-on codelab · Pinned Android source

YOUR LEARNING JOURNEY

0 of 16 available lessons completed

Progress saved in this browser. No account needed.
Microphone capture: PCM, partial reads and bounded queues | Gemini Live for Android with PocketCook | Android Engineers