Audio
Mic capture, agent audio playback, AEC, and audio gating modes.
Audio I/O is handled by LiveKit. Your application publishes the local microphone track; the server publishes the agent's TTS audio as a remote track. The SDK wires both sides for you.
Mic capture
When you call the SDK's microphone-enable method, the SDK:
- Creates a
LocalAudioTrackfrom the default microphone device. - Publishes it to the LiveKit room with
source: MICROPHONE. - Sends a
mutecontrol frame ({ "type": "mute", "muted": false }) so the server starts processing inbound audio from your participant.
// Unmute (enable mic)
await session.setMuted(false);
// Mute without disconnecting
await session.setMuted(true);The MicToggle component wraps this:
import { MicToggle } from 'cosmo-ai/react';
<MicToggle />await session.set_microphone_enabled(True) # publish track + send mute(false)
await session.set_microphone_enabled(False) # unpublish track + send mute(true)try await session.setMuted(false) // unmute: mute frame + local capture on
try await session.setMuted(true) // mute: mute frame + local capture offA Swift session publishes the microphone at start by default. Pass micMuted: true to agent.start(...) to join without capturing anything — nothing is sent until the first setMuted(false).
Agent audio playback
The agent's TTS output arrives as a remote audio track in the LiveKit room. The TypeScript SDK handles this with <RealtimeAudio />:
import { RealtimeAudio } from 'cosmo-ai/react';
<RealtimeAudio /><RealtimeAudio /> creates a hidden <audio> element, attaches it to the client with client.attachAudioElement(), and manages browser autoplay state. The StartAudio component provides a user-gesture gate for browsers that block autoplay on page load:
import { StartAudio } from 'cosmo-ai/react';
<StartAudio /> // renders a button the user clicks to unblock audioIn Python and Swift, LiveKit's platform audio pipeline plays the remote track automatically once the room is joined.
Level metering
For level meters / waveform UIs, every SDK exposes per-direction RMS (0…1). The cadence is per-SDK: TypeScript emits volume events once per animation frame (display-refresh driven), Python's audio_levels() samples at ~20 Hz, and Swift's level streams throttle to ~30 Hz. Each stream yields only once the corresponding track is published / subscribed.
React hooks, or the raw volume event:
import { useMicLevel, useOutputLevel } from 'cosmo-ai/react';
const mic = useMicLevel();
const out = useOutputLevel();Or, non-React:
session.on('volume', ({ mic, output }) => { /* … */ });An async iterator yielding both directions at once:
async for levels in session.audio_levels():
print(f"mic={levels.mic:.2f} agent={levels.agent:.2f}")One typed AsyncStream<Float> per direction:
Task {
for await level in session.inputLevels {
// 0…1 RMS of the local mic track
}
}
Task {
for await level in session.outputLevels {
// 0…1 RMS of the agent's remote track
}
}Latest-value buffered, so a slow consumer drops old values rather than backing up LiveKit's render callback.
Background voice cancellation
Background voices showing up in the transcript, or the agent answering someone who isn't the user? That symptom has one switch: audio.noise_cancellation (noiseCancellation in TypeScript and Swift) on the agent config. It is off by default, and it takes a mode:
| Mode | What it removes | Use it when |
|---|---|---|
off | nothing | a headset, or a quiet room |
denoise | non-speech noise; every voice survives | several people share one microphone |
voice_focus | noise and every voice but the primary one | one person is speaking and others in the room should not reach the model |
The distinction matters most when two people are meant to be heard. voice_focus decides for itself which speaker is primary and treats the rest of the room as background, so on a shared microphone it attenuates whoever is further away — including the person you wanted. Reach for denoise there.
const agent = client.agent({
instructions: '…',
audio: { noiseCancellation: 'denoise' },
});from cosmo_ai import AudioConfig, NoiseCancellation
agent = client.agent(
instructions="…",
audio=AudioConfig(noise_cancellation=NoiseCancellation.DENOISE),
)let agent = try client.agent(
instructions: "…",
audio: AudioConfig(noiseCancellation: .denoise)
)Whichever mode you pick, the filtered signal is also what turn-taking reads, so it costs some barge-in responsiveness — filter for noisy environments (cafés, open offices, call centers) and leave it off for quiet-room or headset use.
Both modes apply to managed WebRTC sessions. A phone leg — session.dial(...) or an inbound call — gets a lighter noise suppressor instead, whatever the mode: it removes background noise from the line but does not single out competing voices, so it already behaves as denoise. The local OSS cosmo-server runs neither filter and rejects any audio block at session start (option_unsupported).
It removes background voices, which no client-side processing does: capture-level noise suppression targets steady noise, not other talkers — and the TypeScript SDK keeps the browser's noise suppression and auto-gain control off, because they duck speech during double-talk and break barge-in. Acoustic echo cancellation (below) is a different job again: it removes the agent's own voice from the microphone, and is on by default in every SDK.
Acoustic echo cancellation (AEC)
AEC — the bit of the audio pipeline that subtracts the speaker's output from the mic input so the agent doesn't hear itself — runs inside the browser's WebRTC stack (for TypeScript) or the LiveKit native SDK (for Python and Swift). You don't configure it. The server relies on AEC being active — it never soft-mutes the mic when the agent is speaking. Soft-muting on bot speech events (bot-started-speaking) would defeat AEC and cause interruption artifacts.
Don't gate the mic on bot-started-speaking. Keep the mic track published continuously. The agent's interruption handling relies on real acoustic echo cancellation, not client-side muting.
Continuous streaming
The mic streams continuously for the whole session; server-side VAD decides when the user is speaking. What mute does locally differs by SDK: TypeScript and Swift disable the local microphone track before sending the frame, so capture genuinely stops; Python leaves the track publishing and only signals the server. Either way the server stops processing your audio. If you need manual turn boundaries (push-to-talk) or different barge-in behavior, that's turn-taking configuration, not audio configuration. See Turn-taking for interruption_sensitivity, activity-end, and silence-timeout hooks.
Sample rates
LiveKit negotiates the audio codec automatically, and no SDK asks you for a sample rate. In Python, set_microphone_enabled(True) captures through WebRTC's audio device module, which takes the device's own rate and resamples; the browser WebRTC stack does the same in TypeScript.
Pitfalls
- In the TypeScript SDK, mute calls are usable the moment
agent.start()resolves; they throwSessionStateErrorwith codenot_connectedonce the session has ended. A session reference obtained before the start resolved (theonSessioncallback) canawait session.waitUntilReady()first. - In Python,
set_microphone_enabled()raisesAudioUnavailableErrorwhen no input device can be opened — a headless host, a denied permission, or a device another process holds exclusively.set_speaker_enabled()plays through PortAudio and raises the same error when that library is missing; thesounddevicewheel bundles it on macOS and Windows, but on Linux it comes from the distribution. <RealtimeAudio />must be rendered inside<RealtimeProvider>. It attaches to the provider's session internally.
Transport
The two carriers a session can run on — the WebRTC media room and the single-socket WebSocket lane — and how audio, control messages, and the handshake flow on each.
Turn-taking
Voice activity detection, barge-in, manual turn boundaries, and silence handling — who speaks when, and how to tune it.