Skip to main content
synthesizeStream() returns a Flow<TTSOutput>, one emission per audio chunk. It is not a suspend function and takes no callback.
Signature:
Each emission is a full TTSOutput, so sample_rate, audio_format, chunk_index, is_final, and duration_ms are all available per chunk. Read the sample rate from the first chunk rather than assuming one.

Cancellation

Cancelling the collector closes the flow, which sets an internal cancellation flag observed by the native chunk callback and then calls the native stop. The synthesizer stops emitting rather than running to completion in the background. stopSynthesis() stops synthesis explicitly:
The synthesis call runs on Dispatchers.IO inside the SDK, so collecting from the main thread does not block the UI.

Silent completion

The flow closes without emitting when the SDK is not initialized or no speech-synthesis model is loaded. It does not throw. Check the loaded model first if you need to distinguish “no voice” from “nothing to say”:

Player

synthesizeStream() produces buffers; playback is yours. A single consumer coroutine pulling off a channel keeps chunks in order.

Speaking an LLM response as it arrives

A useful pattern for voice UIs is to speak sentence by sentence while the model is still generating, rather than waiting for the whole reply. Split the token stream on sentence boundaries and hand each completed sentence to speak(), which suspends until its own audio finishes, so sequential calls do not overlap.
Cap each chunk to what the engine accepts. Some voices have a hard phoneme limit per call (MeloTTS, for example, rejects sequences over 512 phonemes), so hard-split anything longer and skip blank or symbol-only fragments, which produce an empty phoneme sequence and fail synthesis. Log and skip a failed chunk instead of aborting the turn.

Choosing between the two