0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Voice Live の音声入力の品質を上げる ― ノイズ抑制・エコー除去・Live-Reference AEC の選び方

0
Last updated at Posted at 2026-11-10

はじめに

音声エージェントでは、マイクに入る音の品質が思っている以上に効きます。雑音やスピーカーからの回り込みは、音声認識の精度だけでなく、発話区間の検出(VAD)、割り込みの判定、発話終端(EOU)の判定まで狂わせます。エージェントが自分の声を「ユーザーの発話」と誤認して、自分で自分の話を遮る、というのが典型的な失敗です。

Voice Live には、入力音声を整える仕組みが3段階あります。

  1. ノイズ抑制(azure_deep_noise_suppression)
  2. サーバ側のエコー除去(server_echo_cancellation)
  3. Live-Reference AEC(エコー除去の参照に、クライアントが実際に再生した音を使う)

本記事では「どこまで入れればよいか」の判断の流れと、一番手間のかかる 3 の実装の勘所を整理します。

本記事は Voice Live 機能解説シリーズの第2回です。

シリーズの記事一覧(SDK 1.2.0 / API 2026-07-15)

  1. Voice Live SDK 1.2.0の変更点とAPI 2026-07-15 GA
    SDKに型が無い機能の分類
  2. Voice Live APIで会話の「間」を扱う
    先に話す・言いよどみを待つ・割り込む・待ち時間に話す
  3. Voice Live の音声入力の品質を上げる(本記事)
    ノイズ抑制・エコー除去・Live-Reference AEC の選び方
  4. Voice LiveでMCPサーバのツールを使う
    function callingとの違いと並列ツール呼び出し
  5. Voice LiveでFoundryのエージェントをツールとして呼ぶ
    chat-supervisorパターンの使い方と前段モデルの選び方
  6. 30秒の音声で「自分の声」を作って Microsoft Foundry エージェントを喋らせる
    Personal Voice の申請から実装まで
  7. Voice Live APIのアバターを使い分ける
    標準・カスタム写真・カスタムビデオの設定と実装
  8. Voice Liveのアバター映像をWebSocketで受ける
    WebRTCとの違いと映像の形式

番外編:Voice LiveのWebRTC音声接続に.NETのSIPSorceryでつなぐ
SDPを1行ずつ突き合わせた記録

検証に使ったコードはこちらです。

本記事は 2026年10月時点の情報です。対象は Voice Live API 2026-07-15(GA)、Live-Reference AEC の実機検証は 2026-06-01-preview で行っています(理由は後述)。

3段階の全体像

段階 設定 何を取り除くか 実装の手間
1. ノイズ抑制 input_audio_noise_reduction 周囲の雑音 設定のみ
2. サーバ側エコー除去 input_audio_echo_cancellation(参照はサーバ) エージェント自身の声の回り込み 設定のみ
3. Live-Reference AEC 同上(参照はクライアント) 実際にスピーカーから出た音の回り込み 大きい(ステレオ参照の生成)

1 と 2 は session.update に1行ずつ足すだけです。

session.update
{
  "type": "session.update",
  "session": {
    "input_audio_sampling_rate": 24000,
    "input_audio_noise_reduction": { "type": "azure_deep_noise_suppression" },
    "input_audio_echo_cancellation": { "type": "server_echo_cancellation" }
  }
}

サンプルでは 1 と 2 を常に有効にして使っていますが、有効・無効で認識精度などを比較する検証はしていません。本記事で実測を伴うのは 3 です。

ノイズ抑制は3つのモードから選ぶ

ノイズ抑制は当初 azure_deep_noise_suppression の1種類だけでしたが、現在は3つのモードから選べます。「どの声を残したいか」で決まります。

type 挙動 使う場面
azure_deep_noise_suppression 背景の雑音を抑え、マイクに一番近い話者に最適化する 従来の汎用的なノイズ抑制
azure_deep_noise_suppression_headset 近くの話者の声を保ちつつ、背景の「声」とノイズを抑える 周囲に人の話し声がある環境で、目の前の話者だけを通したい
azure_personal_noise_suppression 対象話者の情報を使って、その人の声だけを残す(手前にいる他の話者の声も除去する) 前後の分離だけでは足りず、話者単位で絞りたい

違いは「背景か手前か」で切るのか、「誰の声か」で切るのかです。カフェで隣の席の会話を消したいなら headset、複数人が同じ場所にいて特定の1人だけを拾いたいなら personal、という使い分けになります。

headset と personal には、さらに detection_input という指定があります。

detection_input 想定する環境
raw_ec(既定) 電話など一般的な環境
mas_dns 雑音が多く、音響的に厳しい環境
session.update(headset モード)
{
  "type": "session.update",
  "session": {
    "input_audio_noise_reduction": {
      "type": "azure_deep_noise_suppression_headset"
    }
  }
}
session.update(personal モード・厳しい環境)
{
  "type": "session.update",
  "session": {
    "input_audio_noise_reduction": {
      "type": "azure_personal_noise_suppression",
      "detection_input": "mas_dns"
    }
  }
}

使う前に押さえておきたい点が3つあります。

  • ノイズ抑制のモードを選ぶことは、エコー除去の有効化・無効化とは独立です。input_audio_echo_cancellation は別に設定します
  • session.updated は「要求した設定」を返すだけで、パーソナライズが実際に効いているかは示しません(この「受理されたのに効いているか分からない」は Voice Live のあちこちで出てくるパターンです)
  • 話者ターゲティングは本人確認の仕組みではありません。 また、ノイズ抑制は他の話者が文字起こしに出ないことを保証しません

なお、筆者はまだ3モードの比較検証をしていないため、本記事で実測を伴うのは引き続き Live-Reference AEC です。

もう1つの選択:文字起こしモデル

入力音声の品質という観点では、どの文字起こしモデルを使うかも効いてきます。こちらも選択肢が増えました。input_audio_transcription.model で指定します。

文字起こしモデル 組み合わせられるチャットモデル
azure-speech すべての非マルチモーダルモデルとエージェント(既定で有効)
mai-transcribe / mai-transcribe-2(preview) gpt-realtime、gpt-realtime-mini、すべての非マルチモーダルモデル、エージェント
whisper-1 / gpt-4o-transcribe / gpt-4o-mini-transcribe / gpt-4o-transcribe-diarize gpt-realtime と gpt-realtime-mini のみ

組み合わせを間違えるとセッションが拒否されます。 OpenAI 系の文字起こしモデルは realtime 系のモデルとしか組み合わせられず、逆に gpt-4o のようなカスケードのモデルに指定すると弾かれます。筆者は以前これで「このリージョンでは realtime モデルが使えない」と誤判定しました。

mai-transcribe 系は phrase_list(認識させたい語を並べて精度を上げる)に対応しています。より低遅延な mai-transcribe-2-streaming(preview)もありますが、使えるリージョンが mai-transcribe-2 とは異なるので、選ぶ前にリージョンの表を確認してください。

どこまで入れるかの判断

まずサーバ側のエコー除去で足りるか

サーバ側のエコー除去は、サーバが送った TTS の音声を参照信号にして、マイク入力からエージェント自身の声を引き算します。つまり「サーバが送った音 = スピーカーから実際に鳴った音」という前提に立っています。

公式ドキュメントには、この前提の限界も書かれています。

The service assumes the client plays response audio as soon as it receives it. If playback is delayed for more than two seconds, echo cancellation quality is impacted.
― How to use the Voice Live API

逆に言えば、この前提が成り立つ環境ならサーバ側で十分です。

サーバ側で十分な環境 理由
ヘッドセット・イヤホン そもそも回り込みが小さい
受け取った TTS をそのまま鳴らすだけ 参照と実際の再生音が一致する
クライアントを単純に保ちたい 3 の実装コストに見合わない

Live-Reference AEC が要る環境

前提が崩れるのは、スピーカーから出る音がサーバの TTS と一致しない場合です。

環境 サーバ側の参照では足りない理由
音量調整・EQ・ノーマライズ・デバイスの DSP(ラウドネス補正、空間オーディオなど) 振幅や周波数特性が変わると引き算が成立しない
Bluetooth などでの再エンコード・リサンプリング 波形が元の TTS と一致しない
効果音・BGM・通知音、動画やゲームの音を同時に鳴らす サーバの参照にアプリ独自の音が入っていない
複数のエージェントやアバターが同時に話す スピーカーから出るのはミックス後の音
再生の遅延・ジッタ・途中停止(バージインなど) 「送った通りに鳴った」が崩れる
スピーカーとマイクが近い、大音量(キオスク、車載、ロボット、スマートスピーカー、XR ヘッドセット) 回り込みが大きく、参照の正確さが結果に直結する

判断の目安は「スピーカーの実際の出力がサーバの TTS と違う環境ほど、3 の価値が上がる」です。ただし後述のとおり、参照の時間合わせを正しく作れないと逆効果になります。作り込めるなら 3、無理なら 2 が現実的な線引きです。

Live-Reference AEC

仕組み

マイクの音声を送る代わりに、マイク(チャンネル0)と実際の再生音(チャンネル1)を交互に並べたステレオ PCM16 を送ります。

[mic₀, ref₀, mic₁, ref₁, mic₂, ref₂, ...]

サーバは内部の TTS ループバックの代わりに、チャンネル1をエコー除去の参照に使います。エコー除去の処理自体はサーバで行われ、クライアントは参照を渡すだけです。

設定

session.update
{
  "type": "session.update",
  "session": {
    "input_audio_format": "pcm16",
    "input_audio_echo_cancellation": {
      "type": "server_echo_cancellation",
      "reference_source": "client",
      "channels": 2
    }
  }
}

reference_source と channels の組み合わせは2通りしか許されません。

reference_source channels 結果
server(既定) 1(既定) OK。従来のサーバ側エコー除去
server 2 エラー invalid_ec_channels_requires_client
client 1 エラー invalid_ec_reference_channels
client 2 OK。Live-Reference AEC

また、reference_source、channels、input_audio_format、input_audio_sampling_rate はセッションの途中で変更できません。この機能は API 2026-06-01-preview で追加され、2026-07-15(GA)でも使えます。

帯域はステレオになるぶんほぼ倍になります。公式の目安では 24 kHz PCM16 で生データ約 94 KB/s、base64 後に約 125 KB/s です。参照チャンネルは課金の計測前に取り除かれるので、音声の課金は増えません。

SDK 1.2.0 では AudioEchoCancellation に ReferenceSource と Channels が入ったので、設定は型で書けます。

options.InputAudioFormat = InputAudioFormat.Pcm16;
options.InputAudioEchoCancellation = new AudioEchoCancellation
{
    ReferenceSource = EchoCancellationReferenceSource.Client,
    Channels = 2
};

feature flag を付けて接続する

reference_source と channels を使うには、WebSocket の URL に &features=client_ec_reference:true を付ける必要があります(API リファレンスに記載があります)。

本記事の実機検証は 2026-06-01-preview + このフラグで行いました。フラグなしで通るかは確認していないので、まずフラグ付きで試すのが確実です。

wss://<resource>.cognitiveservices.azure.com/voice-live/realtime?api-version=2026-06-01-preview&model=gpt-4o&features=client_ec_reference:true

公式にもブラウザ向けのサンプル(Live-Reference AEC sample)が用意されています。Web アプリで試すならこちらが近道です。
https://github.com/microsoft-foundry/voicelive-samples/tree/main/javascript/live-reference-aec

実装の勘所:参照の時間合わせ

設定は数行ですが、動かすのは難しいです。サーバは渡された参照をそのまま引き算に使うので、参照が実際にスピーカーから出ている音とずれていると、エコーは消えません。サンプルで最初に作った素朴な実装(再生した音を FIFO に積み、マイクのフレームごとに取り出す)は、しばらくするとずれて戻らなくなりました。

最終的に安定させるまでに効いたのは次の4点です。

1. 再生バッファで音声を捨てない

応答の音声は実時間より速くまとめて届きます。サンプルの再生バッファ(NAudio の BufferedWaveProvider)は当初 10秒で、溢れた音声を捨てる設定(DiscardOnBufferOverflow)でした。これだと長い応答で音声が黙って捨てられ、参照に積んだ音と実際に鳴った音が一致しなくなります。

120秒に広げて、捨てない状態にしました。

これはエコー除去に限らず全モードに効く不具合でした。10秒を超える応答が途中でぷつぷつ切れる場合は、再生バッファの溢れを疑ってください。

2. 参照は「最新 − 再生待ち」の位置を読むだけにする

いまマイクが拾っているのは、いまスピーカーから出ている音です。それは「クライアントが再生キューに積んだ最新の音」から「まだ再生バッファに残っている分(再生待ち)」だけ前の位置にあります。

再生キューに積んだ音  : ────────────────────────▶ 最新
                                   ▲
                    いま鳴っている位置 = 最新 − 再生待ち

ポイントは、この位置をマイクのフレームごとに毎回計算し直して、読むだけにすることです。取り出して消費すると、読み位置がマイクの速度で進みます。再生が遅れている局面ではその読み位置が実際の再生より先行し、一度先行すると戻れません。毎回計算し直せば、ずれても次のフレームで自然に戻ります。

EchoReferenceStereoCapture.cs(抜粋)
public byte[] BuildStereoFrame(byte[] micMonoPcm, int playbackBacklogBytes)
{
    int sampleCount = micMonoPcm.Length / 2;
    var stereo = new byte[sampleCount * 4];

    lock (gate)
    {
        // いま鳴っている位置 = 最新 − 再生待ち。毎フレーム計算し直し、読むだけ(消費しない)
        int readOffset = count - playbackBacklogBytes;
        if (readOffset < 0) readOffset = 0; // 履歴が足りない(参照が再生より先行している)

        for (int i = 0; i < sampleCount; i++)
        {
            // ch0: マイク
            stereo[i * 4]     = micMonoPcm[i * 2];
            stereo[i * 4 + 1] = micMonoPcm[i * 2 + 1];

            // ch1: 実際に鳴っている音(無ければ無音)
            int pos = readOffset + i * 2;
            if (pos + 1 < count)
            {
                stereo[i * 4 + 2] = reference[(head + pos) % reference.Length];
                stereo[i * 4 + 3] = reference[(head + pos + 1) % reference.Length];
            }
        }

        // このフレームの分は再生済みになったので、古い側から捨ててバッファを一定に保つ
        int retire = Math.Min(readOffset, micMonoPcm.Length);
        head = (head + retire) % reference.Length;
        count -= retire;
    }
    return stereo;
}

「再生待ち」は再生バッファの残量(BufferedWaveProvider.BufferedBytes)から取ります。参照用の履歴はこの再生待ちより長く持つ必要があるので、サンプルでは30秒分を確保しています。

3. マイクのコールバックで再生バッファをロックしない

再生待ちの量を正確に読もうとして、マイクのコールバックの中で再生バッファをロックすると、再生が途切れます。残量の読み取りはロックせずに行い、多少の誤差は 2 の「毎回計算し直す」で吸収させます。

4. バージインで参照もリセットする

ユーザーが割り込んで再生バッファを捨てたときは、参照の履歴も一緒に捨てます。捨てないと、もう鳴らない音が参照に残り、以降の位置計算がずれます。

動いているかの確かめ方

エコー除去の効果は耳では判断しにくいので、サンプルでは5秒ごとに統計を出しています。

出力の形式
[EC ref] sent <送信秒数>s stereo | ref audible <参照に音があった割合>% / silence <無音の割合>% | playback backlog <再生待ち>ms | ref buffered <参照の履歴>ms | skipped <読み飛ばした秒数>s | aligned

見るのは次の2点です。

  • playback backlog(再生待ち)と ref buffered(参照の履歴)が ±100ms 程度で追随している
  • 末尾が aligned のまま(履歴が足りず参照が再生より先行したフレームが無い)

実機では120秒連続で aligned を維持し、参照が再生待ちに ±100ms で追随しました。エージェントの声をマイクが拾って自分の発話を遮る自己トリガーは起きなくなりました。

この実装は、再生バッファより後ろ(デバイスやドライバの遅延)はモデル化していません。完全な音響的整合ではなく、ワイヤ上の仕組みを確かめるための best effort です。それでも自己トリガーを消すには十分でした。

まとめ

状況 選ぶもの
周囲に人の話し声がある ノイズ抑制を headset モードに
複数人の中から特定の1人だけ拾いたい ノイズ抑制を personal モードに
ヘッドセット、TTS をそのまま鳴らすだけ ノイズ抑制 + サーバ側エコー除去
スピーカーの音が TTS と一致しない(DSP、BGM、ミックス、Bluetooth、大音量) Live-Reference AEC
Live-Reference AEC の時間合わせを作り込めない サーバ側エコー除去のまま
  • ノイズ抑制とサーバ側エコー除去は設定だけで入る。ノイズ抑制は3モードから「どの声を残すか」で選ぶ
  • 文字起こしモデルの組み合わせに制約がある。OpenAI 系は realtime 系のモデルとだけ
  • Live-Reference AEC は設定は数行、実装の大半は参照の時間合わせ
    • 再生バッファで音声を捨てない
    • 参照は「最新 − 再生待ち」を毎回計算し直して読むだけ
    • マイクのコールバックで再生バッファをロックしない
    • バージインで参照もリセットする
  • 接続 URL に &features=client_ec_reference:true を付ける

次回は MCP サーバのツールと並列ツール呼び出し を扱います。

参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?