GEMINI LABJP
NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Articles/API / SDK
API / SDK/2026-06-03Beginner

Gemini Live API Audio Sounds Sped Up — Fixing the Sample Rate Mismatch

When Gemini Live API responses sound high-pitched and sped up, or come back full of noise, the cause is almost always that the 24kHz output is being played at a different sample rate. Here are the concrete fixes for both the browser and iOS.

Gemini API192Live APIaudio7sample rateindie development10troubleshooting82

While prototyping a voice feature for one of my relaxation apps, the speech coming back from Gemini Live API sounded high-pitched and roughly a third too fast — like a fast-forwarded tape. The text response was correct; only the audio was wrong. I first blamed the model and cycled through several voiceConfig options, but nothing changed. The cause wasn't Gemini at all. I had the playback sample rate wrong on my side.

If you're burning time on the same symptom, here are the repro conditions and the fix.

What the symptom looks like

  • The response audio is high-pitched and plays faster than it should (the classic chipmunk voice)
  • Sometimes the opposite: low and dragging
  • A constant hiss, or choppy stop-start playback
  • Text output and transcripts are perfectly fine

A quick rule: if it's "fast and high," your playback rate is too high; if it's "slow and low," it's too low. That alone narrows the cause fast.

Why it happens

Gemini Live API's audio path has two numbers that are easy to confuse.

  • Input (mic to model) is 16,000 Hz (16kHz) 16-bit PCM
  • Output (model to you) is 24,000 Hz (24kHz) 16-bit PCM

There are plenty of samples that use 16kHz for the send path, so it's tempting to push the playback through a 16kHz AudioContext out of habit. Now you're reading 24kHz data as if it were 16kHz, so playback runs at 24000 / 16000 = 1.5x speed, with pitch raised by the same 1.5x. That's the "sped up and high-pitched" sound.

Browsers add another trap. Creating new AudioContext() with no arguments defaults to 48kHz in many environments. Feed raw 24kHz PCM into that "as if it were 48kHz" and you get 2x speed. The rate only matches when the rate the data actually carries and the rate the playback side assumes are the same.

Fix #1: Browser (Web Audio API)

Always load the incoming 24kHz PCM into an AudioBuffer that you explicitly create at 24,000. Live API returns int16 PCM as base64, so converting it correctly to Float32 (-1.0 to 1.0) is the other step people miss.

// Decide the output is 24kHz and build the AudioContext accordingly
const playbackCtx = new AudioContext({ sampleRate: 24000 });
 
// base64 16-bit PCM (little-endian) -> Float32, then play
function playPcm24k(base64Pcm) {
  const bytes = Uint8Array.from(atob(base64Pcm), (c) => c.charCodeAt(0));
  const int16 = new Int16Array(bytes.buffer);
 
  const float32 = new Float32Array(int16.length);
  for (let i = 0; i < int16.length; i++) {
    float32[i] = int16[i] / 32768; // normalize 16-bit to -1.0..1.0
  }
 
  // 1 channel, sample rate 24000
  const buffer = playbackCtx.createBuffer(1, float32.length, 24000);
  buffer.copyToChannel(float32, 0);
 
  const source = playbackCtx.createBufferSource();
  source.buffer = buffer;
  source.connect(playbackCtx.destination);
  source.start();
}

The point is to set both AudioContext({ sampleRate: 24000 }) and createBuffer(1, length, 24000) to 24000. Fix only one and the browser may resample internally and leave you slightly off.

For continuous playback, calling source.start() repeatedly overlaps the audio. In a real app, stack each chunk's start time so they connect seamlessly.

let nextStartTime = 0;
 
function enqueuePcm24k(base64Pcm) {
  const bytes = Uint8Array.from(atob(base64Pcm), (c) => c.charCodeAt(0));
  const int16 = new Int16Array(bytes.buffer);
  const float32 = new Float32Array(int16.length);
  for (let i = 0; i < int16.length; i++) float32[i] = int16[i] / 32768;
 
  const buffer = playbackCtx.createBuffer(1, float32.length, 24000);
  buffer.copyToChannel(float32, 0);
 
  const source = playbackCtx.createBufferSource();
  source.buffer = buffer;
  source.connect(playbackCtx.destination);
 
  const now = playbackCtx.currentTime;
  const startAt = Math.max(now, nextStartTime);
  source.start(startAt);
  nextStartTime = startAt + buffer.duration; // next chunk starts right after this one
}

This also clears up the choppy playback, which usually comes from calling start() on each chunk on the spot and leaving gaps between them.

Fix #2: iOS (AVAudioEngine)

The idea is identical on native. Declare the format you feed into AVAudioPlayerNode at 24,000 Hz. Reuse the recording side's 16,000 Hz here and you get the same sped-up result.

import AVFoundation
 
let engine = AVAudioEngine()
let player = AVAudioPlayerNode()
 
// Output: 24kHz, mono, Float32
let outputFormat = AVAudioFormat(
    commonFormat: .pcmFormatFloat32,
    sampleRate: 24000,
    channels: 1,
    interleaved: false
)!
 
func setupPlayer() {
    engine.attach(player)
    engine.connect(player, to: engine.mainMixerNode, format: outputFormat)
    try? engine.start()
    player.play()
}
 
// Play 16-bit PCM (Data) received from Live API
func play(pcm16: Data) {
    let sampleCount = pcm16.count / 2
    guard let pcmBuffer = AVAudioPCMBuffer(
        pcmFormat: outputFormat, frameCapacity: AVAudioFrameCount(sampleCount)
    ) else { return }
    pcmBuffer.frameLength = AVAudioFrameCount(sampleCount)
 
    pcm16.withUnsafeBytes { raw in
        let int16Ptr = raw.bindMemory(to: Int16.self)
        let dst = pcmBuffer.floatChannelData![0]
        for i in 0..<sampleCount {
            dst[i] = Float(int16Ptr[i]) / 32768.0 // Int16 -> Float32
        }
    }
    player.scheduleBuffer(pcmBuffer)
}

scheduleBuffer plays buffers in order internally, so you don't need the start-time bookkeeping the browser requires. Declare the playback format at 24,000 and the speed-up stops.

When it still isn't fixed

If matching the rate doesn't fix it, suspect these two in order.

First, how you interpret the PCM. If you read the byte array straight into a Float32Array without converting int16 to float32, you get completely different values and wall-to-wall noise. Check the byte count (2 bytes = 1 sample) and the endianness (little-endian).

Second, the channel count. Live API output is mono (1ch). Read it as stereo (2ch) and the samples get split across left and right, breaking both speed and pitch. Confirm channels is 1 in createBuffer or AVAudioFormat.

Avoiding the same trap

I've been building apps as an indie developer since 2014, and audio remains the area where getting a single number wrong throws everything off. To prevent a repeat, I put the meaning into constant names rather than comments.

const GEMINI_INPUT_RATE = 16000;  // mic -> model
const GEMINI_OUTPUT_RATE = 24000; // model -> speaker

Define these two once at the entry point instead of writing 16000 and 24000 inline, and you almost never reuse the send rate for playback by accident.

Start by checking one thing in your own code: whether the sample rate used for playback is 24000. In most cases, that's the single line you need to change.

Share

Thank You for Reading

Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

API / SDK2026-07-19
Still image or short clip? Deciding feature placement from the cost gap between Nano Banana 2 Lite and Omni Flash
When I froze over whether a wallpaper app's hero asset should be a still image or a short moving loop, the deciding factor was not taste but the order of magnitude of the cost. Here is how to normalize Nano Banana 2 Lite and Omni Flash onto the same footing, down to a working decision function.
API / SDK2026-07-04
When Two Managed Agents Fight Over the Same Repo: External Leases and Fencing for Isolated Sandboxes
Every Managed Agents run gets its own isolated sandbox, so a local lock cannot stop two runs from touching the same repo or record. Here is how I serialize them safely with an external lease and a fencing token.
API / SDK2026-06-22
Gemini API on Google Cloud: Diagnosing Production Errors Layer by Layer
Systematically diagnose Gemini API errors in Google Cloud production environments. Covers IAM permissions, Vertex AI vs AI Studio, VPC Service Controls, quota management, service accounts, and multi-region failover with full code examples.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →