◉GEMINI LABEN
●ARGON — gemini-4-argon が 10/13 に GA。複雑なコーディング・エージェント・数学・科学向けです●DEEP RES — deep-research-pro-preview-12-2025 は 10/23 に停止、残り6日。移行先は deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 が GA(10/06)。gemini-3.1-flash-image は非推奨で停止日は未発表です●MCP — ツール結果が 8.5万トークンに膨らんだ事例を、ADK で 310 トークンに包んで抑えた報告が出ています●FLASH — gemini-3.7-flash と 3.5-flash は新しい Flash モデルに自動で回されます(10/08)●NEW — クラッシュ報告を毎回ゼロから読ませない — 既知バグ早見表を先に grep し…●ARGON — gemini-4-argon が 10/13 に GA。複雑なコーディング・エージェント・数学・科学向けです●DEEP RES — deep-research-pro-preview-12-2025 は 10/23 に停止、残り6日。移行先は deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 が GA(10/06)。gemini-3.1-flash-image は非推奨で停止日は未発表です●MCP — ツール結果が 8.5万トークンに膨らんだ事例を、ADK で 310 トークンに包んで抑えた報告が出ています●FLASH — gemini-3.7-flash と 3.5-flash は新しい Flash モデルに自動で回されます(10/08)●NEW — クラッシュ報告を毎回ゼロから読ませない — 既知バグ早見表を先に grep し…
記事一覧/API / SDK
◈ API / SDK/2026-06-03初級

Gemini Live API の応答音声が速回しに聞こえる — サンプルレート取り違えの直し方

Gemini Live API の応答音声が甲高く速回しに聞こえる、あるいはノイズ混じりになる症状は、ほとんどが 24kHz の出力を別のサンプルレートで再生していることが原因です。ブラウザと iOS の両方で、取り違えを直す具体的なコードを記録します。

Gemini API248Live API音声サンプルレート個人開発121トラブルシューティング34

2014 年から個人開発で癒し系・壁紙系のアプリを作り続け、累計 5,000 万ダウンロードほどになりました。そのひとつに音声で話しかけられる機能を試していたとき、Gemini Live API から返ってきた音声が、まるで早送りの再生のように甲高く、3 割ほど速く聞こえました。テキストの応答は正しいのに、音だけがおかしい。最初はモデルの問題かと疑って voiceConfig をいくつも切り替えてみましたが、何も変わりませんでした。原因は Gemini ではなく、私が音声を再生する側のサンプルレートを取り違えていたことでした。

同じ症状で時間を溶かしている方のために、再現条件と直し方を残しておきます。

どんな症状か

  • 応答音声が甲高く、本来より速く聞こえる(いわゆるチップマンクボイス)
  • 逆に低く間延びして聞こえる場合もある
  • ザーッというノイズが乗る、あるいはプチプチと途切れる
  • テキスト出力やトランスクリプトは正常

音が「速い・高い」なら再生レートが本来より高い、「遅い・低い」なら低い、と考えると原因の切り分けが速くなります。

なぜ起きるのか

Gemini Live API の音声まわりには、混同しやすい数字が 2 つあります。

  • 入力(マイクからモデルへ送る音声)は 16,000 Hz(16kHz)の 16bit PCM
  • 出力(モデルが返す音声)は 24,000 Hz(24kHz)の 16bit PCM

送信用に 16kHz を使うコードはサンプルが多く、つい再生側もその流れで 16kHz の AudioContext に流し込んでしまいます。すると 24kHz のデータを 16kHz として読むことになり、再生が 24000 / 16000 = 1.5 倍速、ピッチも 1.5 倍に上がります。これが「速回しで甲高い」の正体です。

ブラウザにはもう一段の落とし穴があります。new AudioContext() を引数なしで作ると、多くの環境で 48kHz が既定になります。ここに 24kHz の生 PCM を「48kHz のつもり」で書き込むと、今度は 2 倍速になります。再生レートは「データが本来持つレート」と「再生する側が解釈するレート」を一致させない限り合いません。

直し方その1:ブラウザ(Web Audio API)

受信した 24kHz の PCM を、必ず 24,000 を指定した AudioBuffer に載せます。Live API は int16 の PCM を base64 で返すので、Float32(-1.0〜1.0)へ正しく変換する点も忘れがちです。

// 出力は 24kHz と決め打ちで AudioContext を作る
const playbackCtx = new AudioContext({ sampleRate: 24000 });
 
// base64 の 16bit PCM(little-endian) → Float32 に変換して再生する
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; // 16bit を -1.0〜1.0 に正規化
  }
 
  // チャンネル数1・サンプルレート24000 で AudioBuffer を作る
  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();
}

AudioContext({ sampleRate: 24000 }) と createBuffer(1, length, 24000) の両方を 24000 で揃えるのがポイントです。片方だけ直しても、ブラウザが内部で勝手にリサンプルして微妙にズレることがあります。

ただし連続再生では source.start() を呼びっぱなしにすると音が重なります。実際のアプリでは、次のチャンクの開始時刻を積み上げて隙間なくつなぐキューにします。

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; // 次チャンクは現チャンクの直後から
}

これでプチプチ途切れる症状も同時に解消します。途切れは多くの場合、チャンクごとに start() をその場で呼んでいて再生の隙間が空くことが原因だからです。

直し方その2:iOS(AVAudioEngine)

ネイティブでも考え方は同じです。AVAudioPlayerNode に流すフォーマットを 24,000 Hz で宣言します。ここを録音側の 16,000 Hz のまま使い回すと、やはり速回しになります。

import AVFoundation
 
let engine = AVAudioEngine()
let player = AVAudioPlayerNode()
 
// 出力は 24kHz・モノラル・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()
}
 
// Live API から受け取った 16bit PCM(Data) を再生
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 は内部で順番に再生してくれるので、ブラウザのような開始時刻の管理は不要です。再生フォーマットを 24,000 で宣言しさえすれば、速回しは止まります。

それでも直らないときの切り分け

レートを揃えても直らない場合は、次の 2 つを順に疑います。

ひとつは PCM の解釈です。int16 を float32 に変換せず、バイト列をそのまま Float32Array として読むと、まったく別の数値になりノイズだらけになります。バイト数(2 バイト = 1 サンプル)とエンディアン(リトルエンディアン)を確認してください。

もうひとつはチャンネル数です。Live API の出力はモノラル(1ch)です。ステレオ(2ch)として読むと、左右に半分ずつ振り分けられて速さもピッチも崩れます。createBuffer や AVAudioFormat の channels を 1 にしているか確認します。

同じ轍を踏まないために

AdMob で収益を立てる無料アプリを長く運用してきましたが、音声まわりは「数字を 1 つ取り違えるだけで全部おかしくなる」領域だと改めて感じました。広告と違ってユーザーの耳に直接届くので、速回しの音は一発で「壊れている」と伝わってしまいます。再発を防ぐために、コードのコメントではなく定数名に意味を込めるようにしています。

const GEMINI_INPUT_RATE = 16000;  // マイク → モデル
const GEMINI_OUTPUT_RATE = 24000; // モデル → スピーカー

16000 と 24000 を直接書かず、この 2 つの定数を入口で 1 度だけ定義しておくと、送信用のレートを再生に流用する事故がほぼ起きなくなります。

まず手元のコードで、再生に使っているサンプルレートが 24000 になっているかどうかを 1 箇所確認してみてください。多くの場合、直すのはその 1 行です。

シェア

お読みいただきありがとうございます

Gemini Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

◈ API / SDK2026-05-15
Gemini API Embedding で詰まった3か所 — 壁紙アプリのカテゴリ自動分類で踏んだエラーと対処
Gemini API の Embedding をカテゴリ自動分類に組み込んで踏んだ INVALID_ARGUMENT・429・精度不足の3点を、次元の固定・切り詰め後の正規化・チェックポイント付きバッチ・recall@5 の計測台という形で整理した記録です。
◈ API / SDK2026-10-14
config に「Extra inputs are not permitted」が出たら、キーの綴りを一つずつ疑います
google-genai SDK で config を渡した途端に pydantic の ValidationError(extra_forbidden)が出る原因は、ほぼ綴りの一字違いです。手元で再現した出力と、呼び出し前に怪しいキーを洗う関数、text が None になる場合の見分け方を書き残します。
◈ API / SDK2026-09-29
『F6号』と『A4くらい』が同じ列に並ぶ作品台帳を、Gemini の読み取りと号数表の換算で cm に揃えるまで
作家さんの作品台帳に『F6号』『41×31.8cm』『A4くらい』が混ざっていました。Gemini API には読み取りだけを頼み、換算は号数表、向きは画像で決める小さな道具を、動くコードとともに一本ずつ組み立てます。