Audio & permissions
Voice is the only part of the SDK that uses native code. The text path uses none. The native audio engine does one job: capture the microphone as PCM16 and play the bot's PCM back, with hardware echo cancellation.
No AI model runs on the device. The SDK only moves audio bytes — Gemini stays server-side. See Privacy & store compliance.
Sample rates (fixed)
The Gemini bridge fixes the rates and the WebSocket path does not negotiate them (there is no
voice_config frame). The SDK hardcodes both in one documented constant:
| Direction | Format |
|---|---|
| Uplink (mic → server) | PCM16, mono, signed LE, 16000 Hz |
| Downlink (server → speaker) | PCM16, mono, signed LE, 24000 Hz |
If the device's native audio rate differs (commonly 44.1 / 48 kHz), the SDK resamples at the audio-engine boundary — you don't handle this. Uplink frames are 20–40 ms (20 ms = 640 bytes).
Native audio engine
| Platform | Capture / playback | Echo cancellation |
|---|---|---|
| iOS | AVAudioEngine in .voiceChat mode | hardware AEC via the voice-chat audio session |
| Android | AudioRecord with source VOICE_COMMUNICATION | AcousticEchoCanceler |
The engine also handles interruptions and route changes (a call comes in, headphones are plugged/unplugged)
with auto-resume, and barge-in flush — when the backend sends { "type": "barge_in" } (the user started
talking over the bot), the SDK clears the playback buffer immediately.
Microphone permission
The SDK requests the microphone permission for you the first time you start voice — on both platforms —
and resolves a structured result (denial never crashes the app). You still declare the permission strings in
your app (iOS NSMicrophoneUsageDescription, Android RECORD_AUDIO); the SDK owns the runtime prompt.
- React Native
- Flutter
// startVoice() requests RECORD_AUDIO (Android) / the mic (iOS) for you, then captures.
// Denial is non-fatal — it rejects with a typed error and text keeps working.
session.startVoice().catch((err) => console.warn('voice unavailable', err));
The Expo config plugin (or, on bare React Native, your Info.plist) supplies NSMicrophoneUsageDescription
(iOS); the package ships the RECORD_AUDIO declaration (Android) — see Quickstart → Install.
// startVoiceSession() requests the mic permission for you before capture.
final session = client.startVoiceSession(config: const SessionConfig(lang: 'uk'));
session.audioState.listen((a) => print(a.name)); // inactive | … | active
await session.setMuted(true); // mute the mic mid-session
Declare NSMicrophoneUsageDescription in ios/Runner/Info.plist and rely on the plugin's RECORD_AUDIO
declaration on Android — see Quickstart → Install.
Graceful degradation
The voice path never crashes the app. Each failure resolves to a structured state:
| Situation | Behavior |
|---|---|
| Microphone denied | voice does not start; text keeps working; the bot adapts |
| Native module missing (e.g. running in Expo Go or a simulator) | startVoice() rejects; text unaffected |
Voice provider down (4503) | session ends with a fatal close; surface it and offer text/retry |
| Hard offline | the SDK settles into a graceful state after ≤3 reconnect attempts; no crash |
Foreground-only (v1)
v1 is foreground-only. There is no background audio — this avoids the iOS UIBackgroundModes: audio
review friction and keeps the store path simple. When the app is backgrounded, the socket may drop and the
60s reattach window applies (see Lifecycle); voice resumes when the app returns to the
foreground.
Minimum versions
| Package floor | Voice / audio path | |
|---|---|---|
| iOS | 13+ | 16+ |
| Android | API 21+ | API 24+ |
App Attest needs iOS 14+; Play Integrity needs recent Play Services — gate attestation accordingly.