AMBE ボコーダとホワイトノイズの相性問題 ─ 100Hz トーンにたどり着くまで

カテゴリ: ハマりどころ / OpenCCVoice / DVSwitch タグ: AMBE, DMR, TGIF, 音声信号処理, SoX


前回、TGIF が「デジタル完全無音」を転送しないことを突き止め、対策として「無音を微小なノイズに置き換える」設計(V1.84)で MD-619 のハングを解消しました。ところが今度は音質の問題が出ます。この記事は、その音質改善に4版を費やした話です。

V1.84 の副作用:シャー音

応答の頭に、約3秒間の「シャー」というノイズが聞こえるようになりました。V1.84 では前パディング1.5秒+焼き込み頭無音1.5秒の全部にノイズを充填していたためです。

パケット解析で TGIF ゲートの挙動を測ってみると、スキップされるフレーム数は 51 → 22 に減っていました。ゲートが開くタイミングが 3.06秒 → 1.32秒に早まった。だが、まだヘッダは食われる。しかも聞こえるノイズは実用に耐えない大きさです。

V1.85:ゲートを早く開けさせようとして失敗

「エネルギー積算型のゲート」なら、音量を上げれば早く開くはず、という仮説を立てました。V1.85 で試したのは:

  • ノイズ振幅を 150 → 600(-47dBFS → -35dBFS)に引き上げ
  • 先頭300msだけ 2500(-22dBFS)の強めバースト

実機テスト、スキップは 22 フレームで変化なし。ヘッダも通らない。

これで分かりました。TGIF のゲートには「約 1.3 秒の固定床」がある。 音量を上げても開放時間は縮まない。バーストは無意味です。仮説から外して、次に進みました。

構造の再設計

ゲート開放後の無音は正常に転送されることが分かったので、ノイズは先頭だけあればいい。V1.86〜V1.87 で、応答音声の構造を再設計しました。

[TX lead 1.0s(実時間の待ち・無音)]
[ノイズ 1.3s(TGIF ゲート開放用、実際に届くのは0.2秒だけ)]
[真の無音 1.5s(受信機の途中参加同期の助走)]
[実音声(イントロ「こちらは」〜本文)]
[真の無音 1.5s(後パディング)]
[終端 1発]

これで音の全体構造は良くなりました。しかし新しい報告:

「こちらは」の前に、ゲロゲロ・プツプツ音、時々シャー。

AMBE ボコーダの性質

この症状、AMBE の性質から予測できるものでした。

AMBE は音声用ボコーダです。 人間の声を効率よく圧縮するために「ピッチ + スペクトル包絡」というモデルで信号を扱う設計になっています。声帯の振動をピッチで、口腔の共鳴をスペクトル包絡で捉える、と考えると分かりやすいです。ホワイトノイズを入れると、この声モデルに無理やり当てはめて符号化するので、「うがい声」様の歪みに化けます。これがゲロゲロ音の正体。

プツプツ音は無音判定境界での振動。 振幅を下げすぎると、AMBE が「これは無音」と判定するフレームと「これは音」と判定するフレームが交互に出現し、ポツポツと切れる音になります。V1.88 でノイズ振幅を 100 に下げていましたが、これが AMBE の無音判定境界にあたっていたのです。

「時々シャー」については、ゲート開放タイミングの揺れが原因でした。TGIF ゲートが開くタイミングが 1.0〜1.3 秒の間で揺れるので、届くノイズの尻尾の長さも変わる。これがランダムな「シャー」に聞こえます。

V1.89:ノイズをトーンに

対策は明快でした。リード音をホワイトノイズから 100Hz の微小トーンに変更する。

なぜ 100Hz か。20ms のパケットブロックにちょうど 2 周期入るので、ブロック連結時に位相が完全に連続します。飛びのない滑らかな周期信号は、AMBE のピッチ検出モデルにきれいに乗ります。声帯の振動と同じ形をしているので、ボコーダは「これは声の低音成分」として扱ってくれる、という理屈です。

実装はこう。

def _init_noise_blocks():
    global _TONE_BLOCK, _TONE_FADES
    base = [math.sin(2 * math.pi * 100.0 * i / 8000.0) for i in range(160)]
    _TONE_BLOCK = struct.pack("<160h", *[int(NOISE_FILL_AMP * s) for s in base])
    # 末尾フェード(無音への遷移ポップを防ぐ)
    _TONE_FADES = [struct.pack("<160h", *[int(NOISE_FILL_AMP * f * s) for s in base])
                   for f in (0.75, 0.55, 0.35, 0.20, 0.08)]

検証で「サンプル間ジャンプ最大12(理論値どおり)」を確認。位相は完全連続です。末尾5ブロック(0.1秒)はフェードアウトして、真の無音への遷移ポップも防いでいます。

実機テスト結果、ゲロゲロ・プツプツ音が消失。 聞こえるとしてもごく薄いハムのみ。

教訓

ボコーダは「声のためのコーデック」であり、声でないもの(ホワイトノイズ)を通すと予測不能な出力になります。周期信号なら綺麗に載る。信号処理の基礎を思い出せば当然のことでしたが、ボット開発のコンテキストからは見落としがちでした。

「無音の代わりに音を入れる」というアイデア自体は正しかった。ただし何の音を入れるかまでは、実測して初めて分かることでした。

V1.84 でホワイトノイズを選んだのは、なんとなく「無音でなければ何でもいいはず」という思い込みからでした。しかし AMBE を通した先で何が起きるかは、実機で聞いてみるまで分からない。信号処理の教科書に書いてある内容(ボコーダの動作原理)を、実務のトラブルシュートで再発見した格好です。

📝 執筆:JI2TAB(尾張旭 DMR デジピーター 管理人)
🏛 JJ2YYK あいちデジタルコミュニケーションハムクラブ

振り返って: 「無音を非無音に置き換える」という抽象的な設計指針は、V1.84 の時点で正しく決まっていました。ここから V1.89 まで4版を費やしたのは、具体的な信号の形をどうするかを実測で追い込むためでした。設計と実装の間には、ボコーダの性質という技術的な溝がある。この溝を埋めるのは机上の議論ではなく、実測です。

続きの話(Talker Alias 引きずりの謎、並行制御の落とし穴)は次回以降で。

JJ2YYK
  • JJ2YYK

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です