voice-sdk
Guides

Realtime synthesis

Stream text into a session and receive audio back.

openTTSSession() opens a duplex connection that accepts text incrementally and streams audio back. Use it when the text is produced over time, for example tokens from a language model.

const session = await voice.openTTSSession({
  format: { container: "raw", encoding: "pcm_s16le", sampleRate: 24000 },
});

const playing = (async () => {
  for await (const event of session.output) {
    if (event.type === "audio") speaker.write(event.data);
    if (event.type === "done") break;
  }
})();

for await (const token of llm) session.push(token);
await session.flush();

await playing;
await session.close();

Session interface

interface RealtimeSession<TIn, TEvent> {
  readonly output: AsyncIterable<TEvent>;
  push(input: TIn): void;
  flush(): Promise<void>;
  cancel(): void;
  close(): Promise<void>;
  readonly closed: Promise<void>;
}

push

push() returns void, so a token loop is not blocked waiting on the network. Errors are reported through output and closed instead of being thrown by push() itself.

flush

flush() asks the provider to synthesize whatever text is buffered. Providers trigger generation on sentence boundaries or on whitespace, so a trailing fragment can stay buffered until you call it.

cancel

cancel() stops audio that has already been queued, which is what a barge-in implementation calls when the user starts speaking. Support is best-effort. On ElevenLabs there is no cancel command, so the provider closes the connection and opens a new one on the next push().

close

close() ends the session and waits for the provider to acknowledge, giving up after five seconds. Always close a session when the call it belongs to ends: an open socket keeps the process alive and continues to be billed.

format

session.format is a ResolvedAudioFormat and is available as soon as the session opens, so you can configure a player before any audio arrives.

Events

type TTSEvent =
  | { type: "audio"; data: Uint8Array; offset?: number }
  | { type: "timing"; alignment: Alignment }
  | { type: "flushed"; id?: string | number }
  | { type: "cleared"; id?: string | number }
  | { type: "done" }
  | { type: "metadata"; requestId?: string; model?: string; raw?: unknown }
  | { type: "warning"; message: string; code?: string };

done marks the end of the current generation, not the end of the session. You can push more text afterwards and receive more audio.

To iterate over audio only, use the audioOnly helper:

import { audioOnly } from "@swungstudent/voice";

for await (const bytes of audioOnly(session.output)) {
  speaker.write(bytes);
}

Format support

Realtime synthesis returns headerless audio: raw PCM, mu-law, or A-law. Requesting a container such as wav throws a ValidationError. Use speak() when you need a container format.

On this page