libwebrtc 音声処理

注意

この資料の正確性を保証しません。

重要

この資料についての問い合わせは Sora のサポート範囲には含まれません。

この資料は libwebrtc M99 (4844) 時点の資料です。

libwebrtcでの音声処理全般(前提知識)

ソースコード

音声フレーム

  • 音声フレームは AudioBuffer クラスに格納されて渡ってくる

  • サンプリングレートは 16khz or 32kHz or 48kHz のいずれか

  • 一フレームには 10ms 分のサンプルが格納されている:

    • 16kHz なら 160 個

    • 32kHz なら 320 個

    • 48kHz なら 480 個

  • 基本的には、 AudioBuffer::SplitIntoFrequencyBands() を使ってフレームを周波数帯域毎のバンドに分割した上で、各バンドに対して音声処理が適用される

    • 何分割されるかは、サンプリングレートによって変わり、一つのバンド内のサンプル数は 160 になるようになっている

    • つまり、

      • 16kHz なら一分割 (分割なし)

      • 32kHz なら二分割 (low, high)

      • 48kHz なら三分割 (low, middle, high)

    • 処理適用後は、 AudioBuffer::MergeFrequencyBands() で全てのバンドが結合され、通常の音声フレームの形式に戻される

音声フレームの処理タイミング

  • 音声処理の対象となる音声フレームは audio_processing_impl.cc の中で処理されることになる

  • 各音声フレームの処理タイミングには、音声ストリームの「入口」と「出口」の二つがある

  • なお、これはローカルストリームについての話であり、 WebRTC のようにネットワークを跨いだストリームの「入口(送信側)」と「出口(受信側)」の話とは独立している

    • libwebrtc の音声処理の API としては送信側ストリームと受信側ストリームのどちらにも適用可能

    • ただし、このドキュメントで扱うような処理は、一般に、送信側ストリームに適用されることが多い

各音声処理の適用順序

音声処理な主な適用箇所となる AudioProcessingImpl::ProcessCaptureStreamLocked() メソッド内での、各音声処理の適用順序を記載する。

なお、実際に音声データの更新まで行われる場合には [処理] 、(原則として)データ収集のみの場合には [分析] と記載としている。

また、それぞれの音声処理は、設定や構成によってスキップされることがある。

適用順序:

  1. [処理] ハイパスフィルタ(設定次第で後ろの箇所に移動)

  2. [処理] 前回の AGC の結果を受けての、入力音声データの音量レベル調整

  3. [分析] エコーキャンセル

  4. [分析] AGC マネージャ

  5. [処理] バンド分割

  6. [処理] ハイパスフィルタ

  7. [分析] AGC

  8. [処理] エコーキャンセル

  9. [分析] ノイズ抑制

  10. [処理] ノイズ抑制

  11. [処理] モバイル版エコーキャンセル

  12. [処理] AGC マネージャ

  13. [処理] AGC

  14. [処理] バンド統合

  15. [処理] トランジェント抑制 ※ コメントで「AGC の前後のどちらが良いのかは要調査」との記載がある

  16. [処理] AGC2

ハイパスフィルタ

音声の低周波成分をカットする処理

概要

  • HighPassFiler::Process() によって行われる

  • アルゴリズムは BiQuadFilter (双2次フィルタ) の "Direct form 1"

  • 計算量としては「各音声フレーム内の各サンプルを走査して、数回の算術演算(加算と乗算)を適用する」という極めて軽いもの

補足

  • 音声フレームの各チャネルは独立して処理される

  • HighPassFiler::Process() に渡すフラグによって「音声フレーム全体」ないし「一番下のバンドだけ」に処理が適用されるかどうかが切り替わる

    • デフォルトはおそらく前者

    • 後者の場合には、周波数成分の高いバンドへの処理はスキップされて、オリジナルのデータがそのまま使われる(ハイパスなのでこれでも問題はない)

  • フィルタの実装は CascadedBiQuadFilter クラス

    • "Cascaded"とあるようにフィルタを複数回適用できるようなインタフェースになっているけれど、今回の用途では、各音声フレームの各チャネルに対して、常に一回だけ適用される

    • フィルタの係数は high_pass_filter.cc#21 で定義されている

  • フィルタのイメージとしては 双2次フィルタ (Biquad Filter) の周波数特性(振幅・位相)の一覧 の記事も参考になる

ノイズ抑制

音声フレーム内の雑音を抑制するための処理

主に定常的な雑音に対して効果を発揮する

概要

  • NoiseSuppressor::Analyze() および NoiseSuppressor::Process() によってノイズ抑制が行われる

  • AnalyzeProcess が分かれているのは、他の音声処理(e.g., エコーキャンセル)の影響を受けずに、ノイズ抑制用に必要な情報の収集を行えるようにするため

NoiseSuppressor::Analyze() での処理:

  • ここでは 音声フレーム中に含まれるノイズ(スペクトル)の分析 が行われる

  • ノイズスペクトルの判定には NoiseEstimator クラスが使用される

    • アルゴリズムは "quantile noise estimation"

      • ざっくりと言えば「ある時間範囲内の音声フレームのスペクトルを、各周波数成分単位でソートして、そのquantile (e.g, 50%タイル)以下のものをノイズと判定する」というもの

      • 対象とするノイズが「定常的なもの」であるという仮定があるので、突発的に発生したような雑音は苦手

      • libwebrtcでの実装と完全に一致している訳ではないが "Quantile based noise estimation for spectral subtraction and Wiener filtering" の論文が参考になる

  • また、 NoiseEstimator の結果のノイズスペクトルは「スペクトルの各周波数成分のスピーチ確率」によって調整される

    • ざっくりと言えば「ある周波数成分にスピーチ(人の声)が含まれている確率が高い場合には、前回音声フレームのノイズ推定結果を重視し、そうではない場合は今回のものを重視する」といった重み付けが行われる

    • スピーチ確率の推定は SpeechProbabilityEstimator クラスによって行われる

      • その際には、まず音声フレームのスペクトルやS/N比を入力としてシグナルモデル ( SignalModel ) を構築する

      • その後、そのモデルに含まれる各特徴量 (e.g., spectral_flatness, lrt (likelihood ratio test)) を使って、スペクトル内の角周波数成分のスピーチ確率が算出される

      • 用語としては "Features for voice activity detection: a comparative analysis" の記事などが参考となる

      • ※ この部分は理解が比較的浅い

NoiseSuppressor::Process() での処理:

  • ここでは Analyze で得られたノイズスペクトルを使って ノイズを除去するためのフィルタの構築およびその適用 が実施される

  • フィルタには Wiener filter (Wikipedia) が使用される (WienerFilter クラス)

  • その他細々とした色々な処理が行われているけれど、それらについては後続の「補足」を参照のこと

計算量

  • FFT(およびその逆変換)が二回適用されている

    • O(N log N)

      • N は 256

      • ノイズ抑制の際には「現在の音声フレーム(バンド単位で160サンプル)」の前方に「前回の音声フレームの末尾部分(96サンプル)」を結合した上で処理が行われるため、 N の値はその合算となる

    • AnalyzeProcess のそれぞれで音声フレームはスペクトルに変換して処理されるので、その前後で FFT (とその逆変換)が走る

  • また、音声フレーム全体を走査するような O(N) の処理が十数回程度実施されている(回数はあくまでも感覚値)

ノイズ抑制によって発生する(理論上の)遅延

  • 6ms

  • NoiseSuppressor::Process() では、入力音声フレーム内の 4/10 のデータだけが出力フレームに反映されて、残りは次回の処理時まで遅らせられる

    • 上述の通り音声フレームの長さは 10ms で、その内の 6/10 の出力が次回まで遅らせられるので 6ms と遅延となる

    • 一部データの出力を遅らせているのは、おそらくフレームの繋ぎ目部分の音声をスムーズにするため

      • Analyze の際にも「前回の入力フレームの末尾(遅延)部分」と「今回の入力フレーム」を結合した上で分析が行われる

補足

マルチチャネルの扱い

マルチバンドの扱い

  • 音声フレーム で触れたように、入力音声フレームのサンプリングレートが 32kHz 以上の場合は、フレームが周波数帯域(バンド)によって分割されて、バンド単位で処理されることになる

  • NoiseSuppressor::Analyze() では、一番低帯域のバンドのみを使って分析が行われる

  • NoiseSuppressor::Process() では、一番低帯域のバンドのみにノイズ抑制処理が適用される

    • それよりも大きなバンドについては、ノイズ抑制は行わずに、ローバンドの抑制結果に応じた、全体の音量調整のみを実施する

    • コード的には upper_band_gain というスカラ値 ( float )を、音声フレームに対して一つ算出し、それをフレーム内の各サンプルに乗算している

FFT用の窓関数

処理のスキップ

  • 音声フレームのローバンドが、値が 0 のサンプルしか含んでいない場合には NoiseSuppressor::Analyze() でのノイズスペクトルの分析および更新はスキップされる

    • このケース(無音)で各種統計値を更新してしまうと、無音ではなくなった後のしばらくは、音声データの全てが「スピーチ(ノイズ無し)」と判定されてしまうため

  • 音声フレームが実際に使われることがない場合( capture_output_used_ == false )は、 NoiseSuppressor::Process() はフィルタの更新はするが、適用は行わない

    • フィルタの情報は、 NoiseSuppressor::Analyze() 呼び出しでも参照されているので、フィルタ更新までは必要

各種オプション

モバイル版エコーキャンセル

モバイル端末向けの軽量なエコーキャンセル処理

コード中では "AECM (acoustic echo control for mobile)" と呼称されている

概要

  • AECM では、音声データの 入力時(capture)出力時(render) のそれぞれで処理が実行される

    • 用語としては「入力側(の音声フレーム)は nearend 」、「出力側(の音声フレーム)は farend 」と呼ばれる

  • 全体の流れとしては、以下のようになる:

    1. [出力時] 実際に出力される音声フレームをバッファ(履歴)に保持しておく

    2. [入力時] 入力音声フレーム内のエコー部分に対応する出力済み音声フレームを特定する(遅延推定)

    3. [入力時] 上の遅延情報を用いて、エコー除去フィルタを適用する(ざっくりと言えば、入力音声から、対応する過去の出力音声成分を引く)

  • AECM 向けのAPIを提供しているのは EchoControlMobileImpl クラス

音声出力時の処理 (AudioProcessingImpl::ProcessRenderStreamLocked())

基本的には「実際に出力される音声フレームのコピーを、後続の入力時処理の際に参照できるようにバッファに保存」しているだけとなる

主な登場人物は、以下の二つ:

音声入力時の処理 (AudioProcessingImpl::ProcessCaptureStreamLocked())

関数の呼び出し階層としては、以下のようになっている:

- EchoControlMobileImpl::ProcessCaptureAudio()  # AudioProcessingImpl::ProcessCaptureStreamLocked()から呼び出されるメソッド
  - WebRtcAecm_Process()
    - WebRtcAecm_EstBufDelay()  # ここで推定した遅延情報はデバッグ用途でしか使用されていない
    - WebRtcAecm_ProcessFrame()
      - WebRtcAecm_BufferFarFrame()
      - WebRtcAecm_FetchFarFrame()
      - WebRtcAecm_ProcessBlock()  # 主要な処理はこの関数内で実施される
        - WebRtcAecm_UpdateFarHistory()
        - WebRtc_AddFarSpectrumFix()
        - WebRtc_DelayEstimatorProcessFix()
        - WebRtcAecm_AlignedFarend()
        - WebRtcAecm_CalcEnergies()
        - WebRtcAecm_CalcStepSize()
        - WebRtcAecm_UpdateChannel()
        - WebRtcAecm_CalcSuppressionGain()
        - ComfortNoise()

重要なのは WebRtcAecm_ProcessBlock() で、それより上の関数群は、基本的にはバッファ管理しか行っていないので、今回はあまり気にする必要はない。

WebRtcAecm_ProcessBlock() 関数のシグネチャは次の通り:

int WebRtcAecm_ProcessBlock(AecmCore* aecm,

                            // farend の音声フレーム (64サンプル)
                            const int16_t* farend,

                            // nearend の音声フレーム (64サンプル)
                            const int16_t* nearendNoisy,

                            // nearendNoisy から雑音を除いたもの
                            // 今回の用途では常に NULL になるので無視して良い
                            const int16_t* nearendClean,

                            // nearendNoisy に対してエコーキャンセルを適用した結果の格納先
                            // 今回の用途では nearendNoisy と同じポインタが使用される
                            int16_t* output)

WebRtcAecm_ProcessBlock() 関数の処理の大まかな流れは以下の通り:

  1. farend 引数および nearendNoisy 引数に FFT を適用して周波数スペクトルに変換する ( TimeTofrequencyDomain() )

  2. farend 引数の周波数スペクトルを aecm 引数が管理している履歴 (リングバッファ) に追記する ( WebRtcAecm_UpdateFarHistory() )

  3. 上の履歴と nearendNoisy 引数の周波数スペクトルを照会して、出力音声(エコー)が入力音声に含まれるまでの遅延を推定する

  4. 推定遅延に対応する出力音声フレーム(スペクトル)を取得する ( WebRtc_AlignedFarend() )

    • これによって、現在の入力音声フレームそのエコー成分に対応する出力音声フレーム が得られたので、以後の処理ではこの二つのフレーム(のスペクトル)が使用される

  5. エコーキャンセル用のフィルタ構築の前段として aecm が管理しているエコー音声チャネルを更新する

  6. 入力音声フレーム(スペクトル)にフィルタを適用して、エコー部分を除去する

    • フィルタには Wiener filter (Wikipedia) を使用

    • 基本的には「入力フレームから、一つ上で求めたエコーチャネル成分を除去する」イメージ

    • フィルタの強弱を調整するために WebRtcAecm_CalcSuppressionGain() でゲイン値(重み)が計算される

      • 入力とエコーのエナジーの差分をもとに、無音、ダブルトーク、等の簡易的な状況判定を行い、それに応じてゲイン値を決定

    • NLP (non-linear processor) でフィルタの値がさらに調整される

      • 外れ値の処理(大き過ぎる or 小さ過ぎるフィルタの値を 1 or 0 に丸める)等がここで行われる

  7. コンフォートノイズを生成( ComfortNoise()

    • オプショナルで、入力音声フレームにコンフォートノイズ(背景ノイズ)を生成する

    • デフォルトは無効

    • ノイズキャンセルの他の箇所の処理とはほぼ独立している

  8. 処理適用後の入力音声フレーム(スペクトル)に逆FFTを適用して、時間波形に戻す

遅延推定

出力音声が入力音声に含まれるまでの遅延の推定は WebRtc_AddFarSpectrumFix() および WebRtc_DelayEstimatorProcessFix() で行われている:

  • WebRtc_AddFarSpectrumFix(): 出力音声フレーム (farend) の形式変換と履歴への追加

    1. uint16_t[64] の形式で保持されている現在の出力音声フレーム(スペクトル)の中央部分を取り出して uint32_t (32 要素のビット配列)に変換する

      • 各周波数成分のこれまでの平均値を別途保持しており、それに比べて大きいなら 1 、小さいなら 0 となる

    2. 上のビット配列を、履歴配列の先頭に挿入する

      • 配列の要素数は 100 で、各要素は時系列順に並んでいる

      • 履歴配列の位置(インデックス)が遅延値に対応する(i.e., 先頭なら遅延なしで、末尾なら 400 ms程度)

  • WebRtc_DelayEstimatorProcessFix(): 遅延推定

    1. 現在の入力音声フレーム(スペクトル)を、上と同様の方法で uint32_t に変換する

    2. nearendの変換後のビット配列farendの履歴の各要素 の一致度を計測する

      • イメージ的には bitcount(nearend xor history[i]) といった処理で、値が小さいほど、より一致度が高いことを示す

    3. 別途管理している 履歴の各位置(= 遅延)での一致度の平均 を更新する

    4. 一致度の平均が最も高い位置を遅延のベスト候補とする

    5. その候補を採用可能な条件が揃っている場合には、その遅延値を採用し、そうではない場合には前回の遅延値を使用する

      • e.g., ベスト候補とワースト候補の差が小さい場合には、不安定な状況だと判断して見送る

      • 条件の詳細については WebRtc_ProcessBinarySpectrum() のコードを参照のこと

チャネル更新

チャネル更新は WebRtcAecm_UpdateChannel() および、その前段の WebRtcAecm_CalcEnergies()WebRtcAecm_CalcStepSize() によって実施される

これらの関数によって AecmCore 構造体が管理しているエコー音声用のチャネル情報が更新される

  1. まずは、出力音声フレームのVADレベル(フラグ)を推定し、もしこれが 0 (エナジーが低く無音に近い) なら後続の処理はスキップする ( WebRtcAecm_CalcEnergies() )

  2. 次に WebRtcAecm_CalcStepSize() で、 NLMS のステップサイズパラメータである mu を計算する

  3. 最後は WebRtcAecm_UpdateChannel() で、 NLMS を使ってエコーチャネルを更新する

  • AecmCore は、毎フレームで更新される channelAdapt と、それよりは安定した channelStored をフィールドとして保持している

    • これらは出力音声フレーム(スペクトル)からエコー音声フレーム(スペクトル)を算出するための重み配列のようなもの (イメージとしては nearend_echo[i] = farend[i] * channelXXX[i])

  • 入出力フレーム(スペクトル) の情報を使って、 channelAdapt を更新

  • 十分なエナジーのフレームが一定数 (20個) 続いたら、 channelAdaptchannelStored の置換を試みる

    • channelAdaptchannelStored のそれぞれを用いて計算した「エコー音声フレームのエナジー」と「対応する入力音声フレームのエナジー」の差分(絶対値)を求める

    • その差分の20フレーム分の合計値が channelAdapt を使った場合の方が小さければ、より適切なものだと判断して channelStored を置換する (逆の場合には channelAdapt が置換される)

  • 後続の処理では channelStored の値が使用される

バッファリングによる遅延

  • 呼び出し階層の上と下で対象とする音声フレームのサンプル数は異なっている

  • そのため入力音声フレームは、まずリングバッファに追記され、 WebRtcAecm_ProcessBlock() 呼び出しの際には、そこから 64 サンプルずつ取り出されて処理されることになる

  • その際にキリが悪かったサンプル群は次回の入力音声処理時に持ち越されることになるので、 32 サンプル分(2ms分)の遅延が発生する

計算量特性

典型的には、一つの音声フレームを処理する際には、以下の関数群が1回ずつ呼び出されることになる:

それぞれに対して、以下の処理が、チャネル数の二乗分だけ繰り返されることになる( マルチチャネルの扱い ):

注意: 一フレームに含まれるサンプル数は 160 だったり 64 だったりするけど、ここでは簡単のためにそれらの違いは考慮しないものとする

補足

マルチチャネルの扱い

  • 各チャネルは、一番下のレイヤー( WebRtcAecm_ProcessBlock() )では独立して処理される

  • 上のレイヤー( EchoControlMobileImpl::ProcessCaptureAudio() )では、以下のような処理される

    • 入力音声フレームのチャネル数 の他に、 出力音声フレームのチャネル数 の概念がある

      • 後者はソースコード中では reverse_channel と呼称されている

      • EchoControlMobileImpl クラスの初期化時には、 WebRtcAecm_ProcessBlock() に渡される Aecm 構造値インスタンスは 入力音声フレームチャネル数 * 出力音声フレームチャネル数 個だけ生成される(おそらく典型的にはチャネル数の二乗となる)

    • 入力音声フレームの各チャネルの処理は、イメージとしては次のようになる( EchoControlMobileImpl::ProcessCaptureAudio()#L173

      1. 入力音声フレームから該当チャネル部分のデータを取得する (nearend)

      2. 出力音声フレームの各チャネルデータ(farend)に対して for ループを回す( EchoControlMobileImpl::ProcessCaptureAudio()#L198

        • nearend と farend を引数として WebRtcAecm_Process() 経由で WebRtcAecm_ProcessBlock() を呼び出す

        • 次のイテレーションに進む前に nearend のデータは、ノイズキャンセル結果で置換される

    • つまりざっくりと言えば、チャネル数に対する二重ループを回して、各入力チャネルから各出力チャネルのエコー成分を順々に取り除いている感じとなる

マルチバンドの扱い

各種オプション

  • ルーティングモードの指定 ( RoutingMode )

    • kQuietEarpieceOrHeadset, kEarpiece, kLoudEarpiece, kSpeakerphone, kLoudSpeakerphone の中から選べる

    • この値によって、エコーキャンセル処理時の各種パラメータが変化する

    • デフォルト値は kSpeakerphone

  • NLP の ON/OFF ( AecmCore::nlpFlag 、デフォルトは有効)

  • コンフォートノイズの ON/OFF ( EchoControlMobileImpl::comfort_noise_enabled_ 、デフォルトは無効)

オートゲインコントロール(AGC)

出力音量レベルを適切な範囲内で一定に保つための処理

libwebrtc には「無印の AGC (ないし AGC1)」と「AGC2」が存在するが、ここでは前者を扱っている(後者については補足節を参照のこと)

概要

  • libwebrtc で、入力音声フレームに AGC を適用する際の論理的な流れは、以下のようになっている:

    1. 適切な入力音量レベルを推定(e.g., 入力音声フレームでクリッピングが発生しているならレベルを下げる)

    2. 上で求めた適切な音量レベルに基づいて、音声フレームの全体音量レベルを変更

    3. 音声フレーム内での音量の均一化

  • また、一番上の階層での構成要素としては、以下の三つのクラスが存在する:

    • GainControlImpl: 上述の三つの処理の全てを担当可能なクラス(後述のモードに応じて、一部処理を他のクラスに委譲する)

    • AgcManagerDirect: 「適切な音量レベルの推定」に特化した処理を担当するクラス(オプショナル)

    • CaptureLevelAdjuster: 「全体音量レベルの変更」に特化した処理を担当するクラス(オプショナル)

  • これらのクラスの使い分けは GainControlImpl に指定される三つのモードによって説明できる:

    • モードは AudioProcessing::GainController1::Mode 列挙型によって定義され、 kAdaptiveAnalogkAdaptiveDigitalkFixedDigital のいずれかとなる

    • kAdaptiveAnalog の場合:

      • このモードでは「アナログ(マイク)音量が制御可能」という想定のもとでゲインコントールが行われる

        • "Adaptive" は「適切な音量レベル推定」と、それに応じた「全体音量レベルの変更」が行われることを示す

      • この場合は GainControlImpl が「適切な音量レベルの推定」と「フレーム内での音量均一化」の両方を実施する

      • CaptureLevelAdjuster が有効な場合には、 このクラスが「全体音量レベルの変更」(適切な音量レベルの反映)を行う

        • CaptureLevelAdjuster は、入力マイクの音量調整の挙動をエミューレートするためのクラス

      • CaptureLevelAdjuster が無効な場合には、入力音量レベルの調整は libwebrtc の音声処理機能の利用側の責任となる

    • kAdaptiveDigital の場合:

      • このモードでは「アナログ(マイク)音量は制御不可能」という想定のもとでゲインコントールが行われる

      • GainControlImpl の役割は、基本的には kAdaptiveAnalog の場合と同様だが、「全体音量レベルの変更」まで一緒に行ってしまう点が異なる

        • アナログ音量調整を利用側に任せることができないので、自分で入力音声フレームに手を入れて、音量調整をしてしまう

        • そのため GainControlImpl が一番多くのことを行うモードとなる

    • kFixedDigital の場合:

      • このモードでは GainControlImpl は「適切な音量レベルの推定」と「全体音量レベルの変更」に関する処理を行わなくなる

        • 「フレーム内での音量均一化」のみを担当

      • 「適切な音量レベルの推定」に関しては、 AgcManagerDirect が代わりに担当する(有効になっている場合)

      • 「全体音量レベルの変更」に関しては、 kAdaptiveAnalog の場合と同様( CaptureLevelAdjuster が有効ならそれが使用され、無効なら利用者責任)

      • なお列挙的のコメントを意訳すると「組み込みデバイスなどで、入力音量レベルが予測可能な(i.e., 事前に適切なゲインが判明している)場合には、全体音量レベル推定および変更処理そのものを省略可能」とのこと

  • 今回は簡単のために、基本的には、以下の構成を想定して説明を行うこととする:

    • GainControlImpl に指定されるモードは kFixedDigital

    • AgcManagerDirect は有効

    • CaptureLevelAdjuster は無効(このクラスの役割は「物理マイク音量のエミューレート」で、厳密には AGC とは独立した処理であるため)

  • GainControlImplAgcManagerDirect は、それぞれ Analyze と Process フェーズが存在する

  • 以降のサブセクションでは、上記のメソッドそれぞれの詳細について記載する

AgcManagerDirect の Analyze ( AgcManagerDirect::AnalyzePreProcess() メソッド)

このメソッドが行なっていることは、主に以下の二つ:

  1. 入力音声フレームのクリッピング判定

    • クリッピング判定 = 大音量領域での音声の欠損が発生するかどうか (c.f. Audio Clipping (Wikipedia))

    • クリッピングが発生する場合には、入力音量レベルを下げる必要がある

      • 上述の通り、実際に音量レベルを変更するのは、このクラスの呼び出し側の責務( AgcManagerDirect は適切なレベルを推定するのみ)

  2. 「代表チャンネル」を更新する

    • 「代表チャンネル」は、ゲインコントロールの際の基準となるチャンネル

    • 「適切な音量レベルの推定」はチャンネル毎に独立して行われるが、実際に音量調整をする際には、代表チャンネルの値が使用される

    • 代表チャンネルは AgcManagerDirect::AnalyzePreProcess() メソッドによって選択される

      • 実装的には、単純に「適切な音量レベル」が一番小さなチャンネルを選択しているだけ

このメソッドの大半はクリッピング関連処理に費やされている。

また、オプショナルな構成要素として ClippingPredictor クラスが存在する。 これが有効となっている場合には「現在のフレームにクリッピングが存在するかどうか」の判定だけでなく、 「近い将来に発生しそうかどうか」の予測まで行われるようになる。 ただし、今回は簡単のために、このクラスの詳細は割愛し、無効となっているものと想定する。

クリッピング判定処理の流れは次の通り:

  1. 入力音声フレームで、値が上端ないし下端に達しているサンプルの割合を計算( AgcManagerDirect::ComputeClippedRatio()

    • これが規定値(デフォルトでは一割)を超えている場合には「クリッピング有り」と判定される

  2. 「クリッピング有り」判定、かつ、前回の判定から一定時間(デフォルトでは三秒)経過している場合には、以下を実施する:

    • 各チャンネルに対して MonoAgc::HandleClipping() を呼び出す

    • MonoAgc は、各チャンネルに対して独立に「適切な音量レベルの推定」処理を適用するためのクラス

    • MonoAgc::HandleClipping() の中では(ざっくり言えば)推定適切音量レベルの定数分の減少が行われる

GainControlImpl の Analyze ( GainControlImpl::AnalyzeCaptureAudio() メソッド)

このメソッドの中では主に、以下が実施される:

  • 入力音声フレームのエナジーなどの統計情報の取得(モードが kFixedDigital 以外の場合)

    • 取得した値は Process フェーズで利用される

  • 入力音声フレームの「全体音量レベルの変更」(モードが kAdaptiveDigital の場合)

モード毎に適用される処理の概要は以下の通り:

  • kAdaptiveAnalog の場合:

    • 以下の処理を行う WebRtcAgc_AddMic() 関数をチャンネル毎に適用:

      1. 入力音量レベルが小さすぎる場合には、入力フレームの各サンプルに一律のゲイン値を適用して調整(増幅)

        • 一フレームで適用されるゲイン値には上限がり、複数フレームを跨いで徐々に大きくすることで、ターゲット音量に近づけていく

      2. 入力フレームのバンド0部分を数ms間隔に区切って、それぞれのエナジーとエンベロープ( Envelope (Wikipedia) )を計算

      3. 最後に WebRtcAgc_ProcessVad() 関数を使って、入力音声フレームにスピーチが含まれているかどうかの情報を取得

        • VAD 判定自体は簡易的なもの

        • ざっくりと言えば「現在のフレームのエナジー」と「長期的なエナジーの統計(平均と標準偏差)」を比較して、前者が十分に高ければ「ボイスが含まれている」と判定

  • kAdaptiveDigital の場合:

    • 以下の処理を行う WebRtcAgc_VirtualMic() 関数をチャンネル毎に適用:

      1. 入力音声フレームのバンド0を対象として、エナジーを計算して lowLevelSignal かどうかを判定

      2. 入力音量フレームの音量レベルが「適切な音量レベル」に合うように、一律のゲイン値を適用する(全体音量を上げる or 下げる)

        • 「全体音量レベルの変更」を行なっている箇所

        • 調整結果に応じて「現在の入力音声レベル」の値も修正する

        • 「適切な音量レベル」を求めること自体は Process フェーズで行われる

      3. 最後に WebRtcAgc_AddMic() 関数を呼び出す

        • つまり前処理を除けば kAdaptiveAnalog と同じとなる

  • kFixedDigital の場合:

    • 特に何も行わない

AgcManagerDirect の Process ( AgcManagerDirect::Process() メソッド)

このメソッドでは、主に以下が行われる:

  • 「適切な全体音量レベル」の推定

  • 「 後続の GainControlImpl クラスの Process フェーズで使用されるゲイン値("compression gain")」の算出

大まかな処理の流れは次の通り:

  1. 入力音声フレームのバンド0に対して Agc::Process() メソッドを呼び出す

    • 内部ではまず VoiceActivityDetector::ProcessChunk() を使って、細かいチャンク単位(数ms単位)でのボイス確率と RMS を求めている

      • RMS は "Root Mean Square" の略で、音圧に近い指標

    • その後、その二つの値を LoudnessHistogram::Update() を使って、ヒストグラムに格納している(RMS に対応するボイス確立を保持)

  2. 上の結果を使って MonoAgc::UpdateGain() メソッドで「適切な音量レベル」を更新する

    • RMS のヒストグラムから、現在の loudness を求め、それをターゲットとする loudness と比較する

    • 現在とターゲットの loudness の差から、デジタルゲインコントールのゲイン値(対象となる調整量)を求める

    • デジタルゲインコントールで調整可能な範囲を超えている場合には、その超過分を(物理マイク or エミューレートマイクでの)アナログ調整でカバーするために「適切な音量レベル」の値を更新する

  3. 次に MonoAgc::UpdateCompressor() で「デジタルゲインコントロールで使用されるゲイン値」を更新する

    • 一つ上で求めたデジタルゲインコントロールのゲイン値をそのまま適用すると、音量が急変してしまう可能性があるので、徐々にターゲットに近づくように、ゲイン値を調整する

    • 調整後のゲイン値は GainControlImpl::set_compression_gain_db() 経由でデジタルゲインコントロールに渡される

      • ただし現在の実装では WebRTC-UseLegacyDigitalGainApplier フラグが有効になっていないと、この値は使用されない(のであまり気にしなくても良い)

  4. 最後は、Analyze フェーズと同様に AgcManagerDirect::AggregateChannelLevels() メソッドを用いた代表チャンネルが選出される

GainControlImpl の Process ( GainControlImpl::ProcessCaptureAudio() メソッド)

このメソッドでは、主に以下が行われる:

  • 「入力音声フレーム内の音量の均一化」

    • 参考までに analog_processing.h ファイル内のコメントでは、この処理は次のように説明されている

      It applies a fixed gain through most of the input level range, and compresses (gradually reduces gain with increasing level) the input signal at higher levels.

  • 「適切な入力音声レベル」の推定(モードが kFixedDigital 以外の場合)

大まかな処理の流れは次の通り:

  1. WebRtcAgc_Analyze() 関数を呼び出して「適用するゲイン配列」と「適切な入力音声レベル」を計算

    • 内部的には WebRtcAgc_ComputeDigitalGains() 関数と WebRtcAgc_ProcessAnalog() 関数を使用している

    • WebRtcAgc_ComputeDigitalGains() 関数は「適用するゲイン配列」を求める

      • ゲイン配列は 11 要素: 音声フレームの 1ms 毎のゲイン値(10個) + 次のフレームの冒頭部分のゲイン値(1個)

      • この関数は、入力音声フレームの VAD や各区間(1ms単位)のエナジーの情報を使って、それぞれの区間に適用するゲイン値を求める

    • WebRtcAgc_ProcessAnalog() では「適切な入力音声レベル」を求めている(音声フレームの更新は、ここでは行わない)

      • かなり色々なヒューリスティックを用いて、期待されるアナログ音量レベルを決定しているので、詳細は実装を参照のこと

        • 入力音声フレームの saturation や VAD を判定したり、 etc

      • なお、この関数は以下のいずれかの条件に該当する場合には呼び出されない:

        • モードが kFixedDigital

          • GainControlImpl が感知する範囲では「入力音声レベルの変更」が行われないため、「適切な入力音声レベル」を求めることが不要なため

        • モードが kAdaptiveDigital かつ Analyze フェーズで求めた lowLevelSignal フラグが真

          • lowLevelSignal の判定箇所には "digital AGC will not adapt to low-level signals" という記載があるので、それをチェックしているものと思われる

  2. ゲイン配列の適用

    • ApplyDigitalGain() 関数を用いて、上で求めたゲイン配列を入力音声フレームに適用する

    • 基本的には、入力音声フレームの各サンプルにゲイン値を掛けているだけの単純な関数

    • 1ms 区間の区切りで不自然にならないように、ゲイン値を徐々に調整していく、などの補助的な処理がいくつか入っている

  3. 「適切な入力音声レベル」の更新(モードが kAdaptiveAnalog の場合のみ)

    • アナログマイク(ないしエミューレートされたマイク)による音量変更用に、チャンネル毎に求めた「適切な入力音声レベル」から代表値を選択する

    • 行なっていることとしては、上述の AgcManagerDirect::AggregateChannelLevels() と同様(最小値を選択しているだけ)

補足

マルチチャネルの扱い

  • 基本的には各チャンネルは独立して扱われる(特に分析系の処理では)

  • ただし、最終的に音量調整を行う際には、一番音量レベルが小さいチャンネルを代表チャンネルとして、それを基準として処理が適用される

マルチバンドの扱い

  • 以下の処理は、入力音声フレームがマルチバンドに分割される前に適用される:

  • 入力音声フレームの分析・情報収集の際には、バンド0が対象となることが多い

バッファリングによる遅延

バッファリングを行なっていないので、それに伴う遅延も特になし

計算量特性

他の音声処理の多くと同様の傾向:

  • 入力音声フレームの走査やコピーが十数回から数十回程度行われる

  • 他の音声処理の多くとは異なり FFT を行なっていないのは特徴的

AGC2 について

ここで取り上げているのは AGC1 (ないし無印の AGC )と呼ばれるもので、 libwebrtc にはそれとは別に AGC2 も存在する ( GainController2 クラス)。

AGC1 と AGC2 は排他的なものではなく、両方を併用することも可能な模様。 例えば、 audio_processing.h ファイルに掲載されている example コードには、以下のような記載があり、両方が有効化されている:

AudioProcessing::Config config;
..省略..

// AGC1 を有効化
config.gain_controller1.enabled = true;
config.gain_controller1.mode =
AudioProcessing::Config::GainController1::kAdaptiveAnalog;
config.gain_controller1.analog_level_minimum = 0;
config.gain_controller1.analog_level_maximum = 255;

// AGC2 を有効化
config.gain_controller2.enabled = true;

また Chromium (利用側)の media/webrtc/helpers.cc ファイルにある ConfigureAutomaticGainControl() 関数で ゲインコントロールを設定している箇所を見ても AGC1 と AGC2 が併用されるようなコードとなっている。 ただし AGC2 については features::kWebRtcAnalogAgcClippingControl というフラグが有効になっている場合にのみ利用され、 AGC1 と異なり単体で使われることはない。

各種オプション

© Copyright 2022, Shiguredo Inc Created using Sphinx 5.0.0b1