androidengineers.Book a session

Interruptions, mute and transcript state

Interruptions, mute and conversation state

article35–50 min

“Stop talking” has several meanings

In PocketCook, server interruption discards the old response's queued playback. Mute stops local microphone capture while keeping the connection. End closes the session and releases audio resources. These actions are not interchangeable. A microphone icon alone cannot explain which owner is active.

Gemini Live supports server-side voice activity and interruption behavior; the core sample consumes its events rather than implementing a custom voice activity detector. See the capabilities guide. The app's peak input level is a UI signal, not a speech detector or a guarantee that the server heard a complete utterance.

Parse all relevant events

A server message may carry more than one kind of content. LiveProtocol.parse builds a list of typed events rather than using a mutually exclusive chain that would discard accompanying transcripts. If the message is interrupted, its inline audio is deliberately ignored.

// LiveConnection.kt — interruption handling
LiveEvent.Interrupted -> {
    audio?.interrupt()
    mutable.update { it.copy(speaking = false) }
}

The call transfers responsibility to the audio owner. Clearing a Compose boolean alone would change the label while old samples kept playing. Follow interrupt() into the adapter: turn epoch changes, pending output is drained, and the track is paused and flushed. Then follow the writer loop to see why a dequeued old chunk cannot continue writing.

The second race: quick mute and unmute

Suppose a microphone chunk has already been handed to a coroutine that has not resumed. Draining the channel removes queued elements but not that in-flight item. If the user mutes and quickly unmutes, checking only the current muted flag would allow old speech through.

The committed core adds a capture epoch to each microphone chunk. The consumer requires both the current connection generation and the current capture epoch:

// LiveConnection.kt — consumer guard, excerpt
if (chunk.generation == generation &&
    chunk.captureEpoch == captureEpoch &&
    state.value.connection == Connection.Connected &&
    !state.value.muted) {
    send(LiveProtocol.audio(chunk.bytes))
}

Connection generation changes when a session ends. Capture epoch changes at mute transitions. Playback turn epoch changes at interruption. Each identity answers a different question. Using one identity for everything would unnecessarily invalidate unrelated work or leave a race unguarded.

The audio adapter also clears partial capture when its own mute epoch changes. Otherwise the first post-unmute chunk could combine speech from before mute with new speech. Muting sends audioStreamEnd to the server; it does not call the controller's end().

Transcript handling is intentionally modest

The sample appends labeled transcript fragments and retains at most 12,000 characters in UI state. Each parsed fragment is capped too. It does not persist transcripts, promise sentence-level assembly, implement diarization beyond the input/output labels, or retain the whole history across backgrounding. The visible transcript is a troubleshooting aid and accessibility starting point, not a durable recording feature.

If you add a transcript history feature, define retention, clearing, fragment merging and consent as product behavior. A database should not be introduced just to preserve everything by default.

Test the ordering, then test the room

Run mutedQueuedSpeechIsDiscardedAndNetworkFailureAllowsRestart. It enqueues input, toggles mute twice before the scheduler consumes it, and checks no PCM was sent. That deliberately adversarial ordering is the point of the test.

Then perform a live experiment: ask for a short explanation, interrupt with “repeat only the next action,” and verify that the old response does not resume. Repeat while a response is queued, then mute and speak. Check that the UI remains muted and the app does not treat those words as a fresh request. Do not use another person's conversation as test material.

Record device, route, model and observations. The protocol unit test proves event handling. The device playback test proves frame consumption. Only a live run tests the combined acoustic and model interaction. Keep those claims separate in your submission.

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.
Interruptions, mute and conversation state | Gemini Live for Android with PocketCook | Android Engineers