libwebrtc 音声処理¶
注意
この資料の正確性を保証しません。
重要
この資料についての問い合わせは Sora のサポート範囲には含まれません。
この資料は libwebrtc M99 (4844) 時点の資料です。
libwebrtcでの音声処理全般(前提知識)¶
ソースコード¶
音声処理関連は webrtc/modules/audio_processing/ 以下にまとまっている
音声フレーム¶
音声フレームは AudioBuffer クラスに格納されて渡ってくる
サンプリングレートは 16khz or 32kHz or 48kHz のいずれか
1 フレームには 10ms 分のサンプルが格納されている:
16kHz なら 160 個
32kHz なら 320 個
48kHz なら 480 個
基本的には、 AudioBuffer::SplitIntoFrequencyBands() を使ってフレームを周波数帯域毎のバンドに分割した上で、各バンドに対して音声処理が適用される
何分割されるかは、サンプリングレートによって変わり、ひとつのバンド内のサンプル数は 160 になるようになっている
つまり、
16kHz なら一分割 (分割なし)
32kHz なら二分割 (low, high)
48kHz なら三分割 (low, middle, high)
処理適用後は、 AudioBuffer::MergeFrequencyBands() ですべてのバンドが結合され、通常の音声フレームの形式に戻される
音声フレームの処理タイミング¶
音声処理の対象となる音声フレームは audio_processing_impl.cc の中で処理されることになる
各音声フレームの処理タイミングには、音声ストリームの「入口」と「出口」の 2 つがある
前者の場合には AudioProcessingImpl::ProcessCaptureStreamLocked() 内で処理が適用される
大半の音声処理はここで実行される
後者の場合には AudioProcessingImpl::ProcessRenderStreamLocked() 内で処理が適用される
ほとんどの音声処理は、こちら側では何も行わない
エコーキャンセルでは「実際に出力される音声フレームのバッファリング」といった軽い処理が実施される
なお、これはローカルストリームについての話であり、 WebRTC のようにネットワークを跨いだストリームの「入口(送信側)」と「出口(受信側)」の話とは独立している
libwebrtc の音声処理の API としては送信側ストリームと受信側ストリームのどちらにも適用できる
ただし、このドキュメントで扱うような処理は、一般に、送信側ストリームに適用されることが多い
各音声処理の適用順序¶
音声処理の主な適用箇所となる AudioProcessingImpl::ProcessCaptureStreamLocked() メソッド内での、各音声処理の適用順序を記載する。
なお、実際に音声データの更新まで行われる場合には [処理]
、(原則として)データ収集のみの場合には [分析]
と記載としている。
また、それぞれの音声処理は、設定や構成によってスキップされることがある。
適用順序:
[処理] ハイパスフィルタ(設定次第で後ろの箇所に移動)
[処理] 前回の AGC の結果を受けての、入力音声データの音量レベル調整
[分析] エコーキャンセル
[分析] AGC マネージャ
[処理] バンド分割
[処理] ハイパスフィルタ
[分析] AGC
[処理] エコーキャンセル
[分析] ノイズ抑制
[処理] ノイズ抑制
[処理] モバイル版エコーキャンセル
[処理] AGC マネージャ
[処理] AGC
[処理] バンド統合
[処理] トランジェント抑制 ※ コメントで「AGC の前後のどちらが良いのかは要調査」との記載がある
[処理] AGC2
以下では、具体的な例として Chromium での Android および iOS の場合に適用される処理と、その順序を記載する。
ソースコードとしては media/webrtc/helpers.cc の CreateWebRtcAudioProcessingModule()
および ConfigAutomaticGainControl()
関数でこれらの構成が決定されている。
なお、ハイパスフィルタやノイズ抑制、エコーキャンセル、AGC の有効・無効はユーザが利用時に指定できるが、それらはすべて有効になっているものとする。 また、実験的なフィールドトライアルの機能はすべて無効になっているものとする。
Android の場合の適用順序¶
[処理] ハイパスフィルタ
[処理] バンド分割
[分析] AGC
[分析] ノイズ抑制
[処理] ノイズ抑制
[処理] モバイル版エコーキャンセル
[処理] AGC
[処理] バンド統合
なお AGC のモードは kFixedDigital となっている(モードの意味については AGC の節を参照)。
iOS の場合の適用順序¶
[処理] ハイパスフィルタ
[分析] エコーキャンセル
[処理] バンド分割
[分析] AGC
[処理] エコーキャンセル
[分析] ノイズ抑制
入力音声データ(
capture_buffer
)ではなく、 エコーキャンセル処理の結果得られたliear_aec_buffer
を分析対象とする
[処理] ノイズ抑制
[処理] AGC
[処理] バンド統合
なお AGC のモードは kAdaptiveAnalog となっている。
ハイパスフィルタ¶
音声の低周波成分をカットする処理
概要¶
HighPassFiler::Process() によって行われる
アルゴリズムは BiQuadFilter (双2次フィルタ) の "Direct form 1"
計算量としては「各音声フレーム内の各サンプルを走査して、数回の算術演算(加算と乗算)を適用する」という極めて軽いもの
補足¶
音声フレームの各チャネルは独立して処理される
HighPassFiler::Process() に渡すフラグによって「音声フレーム全体」ないし「一番下のバンドだけ」に処理が適用されるかどうかが切り替わる
デフォルトはおそらく前者
後者の場合には、周波数成分の高いバンドへの処理はスキップされて、オリジナルのデータがそのまま使われる(ハイパスなのでこれでも問題はない)
フィルタの実装は CascadedBiQuadFilter クラス
"Cascaded"とあるようにフィルタを複数回適用できるようなインタフェースになっているけれど、今回の用途では、各音声フレームの各チャネルに対して、常に 1 回だけ適用される
フィルタの係数は high_pass_filter.cc#21 で定義されている
フィルタのイメージとしては 双2次フィルタ (Biquad Filter) の周波数特性(振幅・位相)の一覧 の記事も参考になる
ノイズ抑制¶
音声フレーム内の雑音を抑制するための処理
主に定常的な雑音に対して効果を発揮する
概要¶
NoiseSuppressor::Analyze() および NoiseSuppressor::Process() によってノイズ抑制が行われる
Analyze
とProcess
が分かれているのは、他の音声処理(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(およびその逆変換)が 2 回適用されている
O(N log N)
N
は 256ノイズ抑制の際には「現在の音声フレーム(バンド単位で160サンプル)」の前方に「前回の音声フレームの末尾部分(96サンプル)」を結合した上で処理が行われるため、
N
の値はその合算となる
Analyze
とProcess
のそれぞれで音声フレームはスペクトルに変換して処理されるので、その前後で FFT (とその逆変換)が走る
また、音声フレーム全体を走査するような O(N) の処理が十数回程度実施されている(回数はあくまでも感覚値)
ノイズ抑制によって発生する(理論上の)遅延¶
6ms
NoiseSuppressor::Process() では、入力音声フレーム内の 4/10 のデータだけが出力フレームに反映されて、残りは次回の処理時まで遅らせられる
上述の通り音声フレームの長さは 10 ms で、その内の 6/10 の出力が次回まで遅らせられるので 6 ms と遅延となる
一部データの出力を遅らせているのは、おそらくフレームの繋ぎ目部分の音声をスムーズにするため
Analyze
の際にも「前回の入力フレームの末尾(遅延)部分」と「今回の入力フレーム」を結合した上で分析が行われる
補足¶
マルチチャネルの扱い¶
NoiseSuppressor::Analyze() では、各チャネルが独立に処理される
NoiseSuppressor::Process() では、各チャネルのフィルタの値(ゲイン)が集約される
やっていることとしては、各周波数成分について、チャネル間で一番小さなフィルタの値を採用しているだけ
マルチバンドの扱い¶
音声フレーム で触れたように、入力音声フレームのサンプリングレートが 32 kHz 以上の場合は、フレームが周波数帯域(バンド)によって分割されて、バンド単位で処理されることになる
NoiseSuppressor::Analyze() では、一番低帯域のバンドのみを使って分析が行われる
NoiseSuppressor::Process() では、一番低帯域のバンドのみにノイズ抑制処理が適用される
それよりも大きなバンドについては、ノイズ抑制は行わずに、ローバンドの抑制結果に応じた、全体の音量調整のみを実施する
コード的には
upper_band_gain
というスカラ値 (float
)を、音声フレームに対してひとつ算出し、それをフレーム内の各サンプルに乗算している
FFT用の窓関数¶
ハニングとフラットのハイブリッドが使用される
処理のスキップ¶
音声フレームのローバンドが、値が
0
のサンプルしか含んでいない場合には NoiseSuppressor::Analyze() でのノイズスペクトルの分析および更新はスキップされるこのケース(無音)で各種統計値を更新してしまうと、無音ではなくなった後のしばらくは、音声データのすべてが「スピーチ(ノイズ無し)」と判定されてしまうため
音声フレームが実際に使われることがない場合(
capture_output_used_ == false
)は、 NoiseSuppressor::Process() はフィルタの更新はするが、適用は行わないフィルタの情報は、 NoiseSuppressor::Analyze() 呼び出しでも参照されているので、フィルタ更新までは必要
各種オプション¶
libwebrtcには
noise_suppression.level
というオプションがあり、それによってノイズ抑制時の各処理で使用されるパラメーターが変化する値は
kLow
orkModerate
orkHigh
orkVeryHigh
詳細は、以下のメソッドを参照のこと
モバイル版エコーキャンセル¶
モバイル端末向けの軽量なエコーキャンセル処理
コード中では "AECM (acoustic echo control for mobile)" と呼称されている
概要¶
AECM では、音声データの 入力時(capture) と 出力時(render) のそれぞれで処理が実行される
用語としては「入力側(の音声フレーム)は nearend 」、「出力側(の音声フレーム)は farend 」と呼ばれる
全体の流れとしては、以下のようになる:
[出力時] 実際に出力される音声フレームをバッファ(履歴)に保持しておく
[入力時] 入力音声フレーム内のエコー部分に対応する出力済み音声フレームを特定する(遅延推定)
[入力時] 上の遅延情報を用いて、エコー除去フィルタを適用する(ざっくりと言えば、入力音声から、対応する過去の出力音声成分を引く)
AECM 向けのAPIを提供しているのは EchoControlMobileImpl クラス
内部的には echo_control_mobile.h および aecm_core.h に存在する
WebRtcAecm_*
関数群によって、実際の処理が実装されている
音声出力時の処理 (AudioProcessingImpl::ProcessRenderStreamLocked())¶
基本的には「実際に出力される音声フレームのコピーを、後続の入力時処理の際に参照できるようにバッファに保存」しているだけとなる
主な登場人物は、以下の 2 つ:
EchoControlMobileImpl::PackRenderAudioBuffer() 関数
出力音声フレームを AudioProcessingImpl クラスが管理している一時バッファに書き込む
モノラルチャネル かつ サンプリングレートが 16 kHz の場合には、単純なデータコピーとなる
マルチチャネル あるいは マルチバンド (サンプリングレートが 16 kHz 以上) の場合には、プラスアルファの処理が加わるので 補足 の節を参照のこと
EchoControlMobileImpl::ProcessRenderAudio() メソッド
EchoControlMobileImpl が管理するリングバッファに EchoControlMobileImpl::PackRenderAudioBuffer() の結果を書き込むだけ
音声入力時の処理 (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() 関数の処理の大まかな流れは以下の通り:
farend
引数およびnearendNoisy
引数に FFT を適用して周波数スペクトルに変換する ( TimeTofrequencyDomain() )farend
引数の周波数スペクトルをaecm
引数が管理している履歴 (リングバッファ) に追記する ( WebRtcAecm_UpdateFarHistory() )上の履歴と
nearendNoisy
引数の周波数スペクトルを照会して、出力音声(エコー)が入力音声に含まれるまでの遅延を推定するWebRtc_AddFarSpectrumFix() および WebRtc_DelayEstimatorProcessFix()
この部分の詳細は 遅延推定 を参照
推定遅延に対応する出力音声フレーム(スペクトル)を取得する ( WebRtc_AlignedFarend() )
これによって、現在の入力音声フレーム と そのエコー成分に対応する出力音声フレーム が得られたので、以後の処理ではこの 2 つのフレーム(のスペクトル)が使用される
エコーキャンセル用のフィルタ構築の前段として
aecm
が管理しているエコー音声チャネルを更新するチャネル更新には NLMS というアルゴリズムを使用 ( Wikipedia: Normalizecd least squares filter (NLMS) )
関連する関数は WebRtcAecm_CalcEnergies() と WebRtcAecm_CalcStepSize() 、 WebRtcAecm_UpdateChannel()
この部分の詳細は チャネル更新 を参照
入力音声フレーム(スペクトル)にフィルタを適用して、エコー部分を除去する
フィルタには Wiener filter (Wikipedia) を使用
基本的には「入力フレームから、ひとつ上で求めたエコーチャネル成分を除去する」イメージ
フィルタの強弱を調整するために WebRtcAecm_CalcSuppressionGain() でゲイン値(重み)が計算される
入力とエコーのエナジーの差分をもとに、無音、ダブルトーク、等の簡易的な状況判定を行い、それに応じてゲイン値を決定
NLP (non-linear processor) でフィルタの値がさらに調整される
外れ値の処理(大き過ぎる or 小さ過ぎるフィルタの値を 1 or 0 に丸める)等がここで行われる
コンフォートノイズを生成( ComfortNoise() )
オプショナルで、入力音声フレームにコンフォートノイズ(背景ノイズ)を生成する
デフォルトは無効
ノイズキャンセルの他の箇所の処理とはほぼ独立している
処理適用後の入力音声フレーム(スペクトル)に逆FFTを適用して、時間波形に戻す
遅延推定¶
出力音声が入力音声に含まれるまでの遅延の推定は WebRtc_AddFarSpectrumFix() および WebRtc_DelayEstimatorProcessFix() で行われている:
WebRtc_AddFarSpectrumFix(): 出力音声フレーム (farend) の形式変換と履歴への追加
uint16_t[64]
の形式で保持されている現在の出力音声フレーム(スペクトル)の中央部分を取り出してuint32_t
(32 要素のビット配列)に変換する各周波数成分のこれまでの平均値を別途保持しており、それに比べて大きいなら 1 、小さいなら 0 となる
上のビット配列を、履歴配列の先頭に挿入する
配列の要素数は 100 で、各要素は時系列順に並んでいる
履歴配列の位置(インデックス)が遅延値に対応する(i.e., 先頭なら遅延なしで、末尾なら 400 ms 程度)
WebRtc_DelayEstimatorProcessFix(): 遅延推定
現在の入力音声フレーム(スペクトル)を、上と同様の方法で
uint32_t
に変換するnearendの変換後のビット配列 と farendの履歴の各要素 の一致度を計測する
イメージ的には
bitcount(nearend xor history[i])
といった処理で、値が小さいほど、より一致度が高いことを示す
別途管理している 履歴の各位置(= 遅延)での一致度の平均 を更新する
一致度の平均が最も高い位置を遅延のベスト候補とする
その候補を採用できる条件が揃っている場合には、その遅延値を採用し、そうではない場合には前回の遅延値を使用する
e.g., ベスト候補とワースト候補の差が小さい場合には、不安定な状況だと判断して見送る
条件の詳細については WebRtc_ProcessBinarySpectrum() のコードを参照のこと
チャネル更新¶
チャネル更新は WebRtcAecm_UpdateChannel() および、その前段の WebRtcAecm_CalcEnergies() と WebRtcAecm_CalcStepSize() によって実施される
これらの関数によって AecmCore
構造体が管理しているエコー音声用のチャネル情報が更新される
まずは、出力音声フレームのVADレベル(フラグ)を推定し、もしこれが 0 (エナジーが低く無音に近い) なら後続の処理はスキップする ( WebRtcAecm_CalcEnergies() )
次に WebRtcAecm_CalcStepSize() で、 NLMS のステップサイズパラメーターである
mu
を計算する最後は WebRtcAecm_UpdateChannel() で、 NLMS を使ってエコーチャネルを更新する
AecmCore
は、毎フレームで更新されるchannelAdapt
と、それよりは安定したchannelStored
をフィールドとして保持している
これらは出力音声フレーム(スペクトル)からエコー音声フレーム(スペクトル)を算出するための重み配列のようなもの (イメージとしては
nearend_echo[i] = farend[i] * channelXXX[i]
)入出力フレーム(スペクトル) の情報を使って、
channelAdapt
を更新十分なエナジーのフレームが一定数 (20個) 続いたら、
channelAdapt
でchannelStored
の置換を試みる
channelAdapt
とchannelStored
のそれぞれを用いて計算した「エコー音声フレームのエナジー」と「対応する入力音声フレームのエナジー」の差分(絶対値)を求めるその差分の20フレーム分の合計値が
channelAdapt
を使った場合の方が小さければ、より適切なものだと判断してchannelStored
を置換する (逆の場合にはchannelAdapt
が置換される)後続の処理では
channelStored
の値が使用される
バッファリングによる遅延¶
呼び出し階層の上と下で対象とする音声フレームのサンプル数は異なっている
上の方( EchoControlMobileImpl::ProcessCaptureAudio() )では 160 サンプル(10ms分)
下の方( WebRtcAecm_ProcessBlock() )では 64 サンプル
そのため入力音声フレームは、まずリングバッファに追記され、 WebRtcAecm_ProcessBlock() 呼び出しの際には、そこから 64 サンプルずつ取り出されて処理されることになる
その際にキリが悪かったサンプル群は次回の入力音声処理時に持ち越されることになるので、 32 サンプル分(2ms分)の遅延が発生する
計算量特性¶
典型的には、一つの音声フレームを処理する際には、以下の関数群が1回ずつ呼び出されることになる:
それぞれに対して、以下の処理が、チャネル数の二乗分だけ繰り返されることになる( マルチチャネルの扱い ):
EchoControlMobileImpl::PackRenderAudioBuffer() および EchoControlMobileImpl::ProcessRenderAudio()
一フレーム分のメモリコピー
EchoControlMobileImpl::ProcessCaptureAudio()
一フレーム分の FFT と 逆FFT
フレーム単位のメモリコピーやループを十数回~数十回
注意: 一フレームに含まれる具体的なサンプル数は、関数によって 160 あるいは 64 と変動するが、ここでは簡単のためにそれらの違いは考慮しないものとする
補足¶
マルチチャネルの扱い¶
各チャネルは、一番下のレイヤー( WebRtcAecm_ProcessBlock() )では独立して処理される
上のレイヤー( EchoControlMobileImpl::ProcessCaptureAudio() )では、以下のような処理される
入力音声フレームのチャネル数 の他に、 出力音声フレームのチャネル数 の概念がある
後者はソースコード中では
reverse_channel
と呼称されているEchoControlMobileImpl
クラスの初期化時には、 WebRtcAecm_ProcessBlock() に渡されるAecm
構造値インスタンスは入力音声フレームチャネル数 * 出力音声フレームチャネル数
個だけ生成される(おそらく典型的にはチャネル数の二乗となる)
入力音声フレームの各チャネルの処理は、イメージとしては次のようになる( EchoControlMobileImpl::ProcessCaptureAudio()#L173 )
入力音声フレームから該当チャネル部分のデータを取得する (nearend)
出力音声フレームの各チャネルデータ(farend)に対して for ループを回す( EchoControlMobileImpl::ProcessCaptureAudio()#L198 )
nearend と farend を引数として
WebRtcAecm_Process()
経由で WebRtcAecm_ProcessBlock() を呼び出す次のイテレーションに進む前に nearend のデータは、ノイズキャンセル結果で置換される
つまりざっくりと言えば、チャネル数に対する二重ループを回して、各入力チャネルから各出力チャネルのエコー成分を順々に取り除いている感じとなる
マルチバンドの扱い¶
マルチバンド自体については 音声フレーム を参照
音声フレームがマルチバンドに分割された際には、一番下のバンドのみが処理され、残りのバンドについては 0 で埋められる(ローパス)
各種オプション¶
ルーティングモードの指定 ( 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 を適用する際の論理的な流れは、以下のようになっている:
適切な入力音量レベルを推定(e.g., 入力音声フレームでクリッピングが発生しているならレベルを下げる)
上で求めた適切な音量レベルに基づいて、音声フレームの全体音量レベルを変更
音声フレーム内での音量の均一化
また、一番上の階層での構成要素としては、以下の 3 つのクラスが存在する:
GainControlImpl: 上述の三つの処理のすべてを担当できるクラス(後述のモードに応じて、一部処理を他のクラスに委譲する)
AgcManagerDirect: 「適切な音量レベルの推定」に特化した処理を担当するクラス(オプショナル)
CaptureLevelAdjuster: 「全体音量レベルの変更」に特化した処理を担当するクラス(オプショナル)
これらのクラスの使い分けは GainControlImpl に指定される三つのモードによって説明できる:
モードは AudioProcessing::GainController1::Mode 列挙型によって定義され、
kAdaptiveAnalog
、kAdaptiveDigital
、kFixedDigital
のいずれかとなるkAdaptiveAnalog
の場合:このモードでは「アナログ(マイク)音量が制御できる」という想定のもとでゲインコントールが行われる
"Adaptive" は「適切な音量レベル推定」と、それに応じた「全体音量レベルの変更」が行われることを示す
この場合は GainControlImpl が「適切な音量レベルの推定」と「フレーム内での音量均一化」の両方を実施する
CaptureLevelAdjuster が有効な場合には、 このクラスが「全体音量レベルの変更」(適切な音量レベルの反映)を行う
CaptureLevelAdjuster は、入力マイクの音量調整の挙動をエミューレートするためのクラス
CaptureLevelAdjuster が無効な場合には、入力音量レベルの調整は libwebrtc の音声処理機能の利用側の責任となる
AudioProcessing::recommended_stream_analog_level() メソッドで、期待される音量レベルは取得できる
kAdaptiveDigital
の場合:このモードでは「アナログ(マイク)音量は制御できない」という想定のもとでゲインコントールが行われる
GainControlImpl の役割は、基本的には
kAdaptiveAnalog
の場合と同様だが、「全体音量レベルの変更」まで一緒に行ってしまう点が異なるアナログ音量調整を利用側に任せることができないので、自分で入力音声フレームに手を入れて、音量調整をしてしまう
そのため GainControlImpl が一番多くのことを行うモードとなる
kFixedDigital
の場合:このモードでは GainControlImpl は「適切な音量レベルの推定」と「全体音量レベルの変更」に関する処理を行わなくなる
「フレーム内での音量均一化」のみを担当
「適切な音量レベルの推定」に関しては、 AgcManagerDirect が代わりに担当する(有効になっている場合)
AgcManagerDirect と GainControlImpl を併用する場合は、常にこのモードとなる
「全体音量レベルの変更」に関しては、
kAdaptiveAnalog
の場合と同様( CaptureLevelAdjuster が有効ならそれが使用され、無効なら利用者責任)なお列挙的のコメントを意訳すると「組み込みデバイスなどで、入力音量レベルが予測できる(i.e., 事前に適切なゲインが判明している)場合には、全体音量レベル推定および変更処理そのものを省略できる」とのこと
今回は簡単のために、基本的には、以下の構成を想定して説明を行うこととする:
GainControlImpl に指定されるモードは
kFixedDigital
AgcManagerDirect は有効
CaptureLevelAdjuster は無効(このクラスの役割は「物理マイク音量のエミューレート」で、厳密には AGC とは独立した処理であるため)
GainControlImpl と AgcManagerDirect は、それぞれ Analyze と Process フェーズが存在する
前者は、他の音声処理が適用される前に実施されて、(極力)生の入力音声フレームから、各種情報が収集される
音声フレーム処理の入り口となる AudioProcessingImpl::ProcessCaptureStreamLocked() メソッドの中では、以下の順番で適用されている:
AgcManagerDirect の Analyze ( AgcManagerDirect::AnalyzePreProcess() メソッド)
GainControlImpl の Analyze ( GainControlImpl::AnalyzeCaptureAudio() メソッド)
AgcManagerDirect の Process ( AgcManagerDirect::Process() メソッド)
GainControlImpl の Process ( GainControlImpl::ProcessCaptureAudio() メソッド)
以降のサブセクションでは、上記のメソッドそれぞれの詳細について記載する
AgcManagerDirect の Analyze ( AgcManagerDirect::AnalyzePreProcess() メソッド)¶
このメソッドが行なっていることは、主に以下の 2 つ:
入力音声フレームのクリッピング判定
クリッピング判定 = 大音量領域での音声の欠損が発生するかどうか (c.f. Audio Clipping (Wikipedia))
クリッピングが発生する場合には、入力音量レベルを下げる必要がある
上述の通り、実際に音量レベルを変更するのは、このクラスの呼び出し側の責務( AgcManagerDirect は適切なレベルを推定するのみ)
「代表チャネル」を更新する
「代表チャネル」は、ゲインコントロールの際の基準となるチャネル
「適切な音量レベルの推定」はチャネル毎に独立して行われるが、実際に音量調整をする際には、代表チャネルの値が使用される
代表チャネルは AgcManagerDirect::AnalyzePreProcess() メソッドによって選択される
実装的には、単純に「適切な音量レベル」が一番小さなチャネルを選択しているだけ
このメソッドの大半はクリッピング関連処理に費やされている。
また、オプショナルな構成要素として ClippingPredictor クラスが存在する。 これが有効となっている場合には「現在のフレームにクリッピングが存在するかどうか」の判定だけでなく、 「近い将来に発生しそうかどうか」の予測まで行われるようになる。 ただし、今回は簡単のために、このクラスの詳細は割愛し、無効となっているものと想定する。
クリッピング判定処理の流れは次の通り:
入力音声フレームで、値が上端ないし下端に達しているサンプルの割合を計算( AgcManagerDirect::ComputeClippedRatio() )
これが規定値(デフォルトでは一割)を超えている場合には「クリッピング有り」と判定される
「クリッピング有り」判定、かつ、前回の判定から一定時間(デフォルトでは 3 秒)経過している場合には、以下を実施する:
各チャネルに対して MonoAgc::HandleClipping() を呼び出す
MonoAgc は、各チャネルに対して独立に「適切な音量レベルの推定」処理を適用するためのクラス
MonoAgc::HandleClipping() の中では(ざっくり言えば)推定適切音量レベルの定数分の減少が行われる
GainControlImpl の Analyze ( GainControlImpl::AnalyzeCaptureAudio() メソッド)¶
このメソッドの中では主に、以下が実施される:
入力音声フレームのエナジーなどの統計情報の取得(モードが
kFixedDigital
以外の場合)取得した値は Process フェーズで利用される
入力音声フレームの「全体音量レベルの変更」(モードが
kAdaptiveDigital
の場合)
モード毎に適用される処理の概要は以下の通り:
kAdaptiveAnalog
の場合:以下の処理を行う WebRtcAgc_AddMic() 関数をチャネル毎に適用:
入力音量レベルが小さすぎる場合には、入力フレームの各サンプルに一律のゲイン値を適用して調整(増幅)
一フレームで適用されるゲイン値には上限がり、複数フレームを跨いで徐々に大きくすることで、ターゲット音量に近づけていく
入力フレームのバンド 0 部分を数 ms 間隔に区切って、それぞれのエナジーとエンベロープ( Envelope (Wikipedia) )を計算
最後に WebRtcAgc_ProcessVad() 関数を使って、入力音声フレームにスピーチが含まれているかどうかの情報を取得
VAD 判定自体は簡易的なもの
ざっくりと言えば「現在のフレームのエナジー」と「長期的なエナジーの統計(平均と標準偏差)」を比較して、前者が十分に高ければ「ボイスが含まれている」と判定
kAdaptiveDigital
の場合:以下の処理を行う WebRtcAgc_VirtualMic() 関数をチャネル毎に適用:
入力音声フレームのバンド 0 を対象として、エナジーを計算して
lowLevelSignal
かどうかを判定入力音量フレームの音量レベルが「適切な音量レベル」に合うように、一律のゲイン値を適用する(全体音量を上げる or 下げる)
「全体音量レベルの変更」を行なっている箇所
調整結果に応じて「現在の入力音声レベル」の値も修正する
「適切な音量レベル」を求めること自体は Process フェーズで行われる
最後に WebRtcAgc_AddMic() 関数を呼び出す
つまり前処理を除けば
kAdaptiveAnalog
と同じとなる
kFixedDigital
の場合:特に何も行わない
AgcManagerDirect の Process ( AgcManagerDirect::Process() メソッド)¶
このメソッドでは、主に以下が行われる:
「適切な全体音量レベル」の推定
「 後続の GainControlImpl クラスの Process フェーズで使用されるゲイン値("compression gain")」の算出
大まかな処理の流れは次の通り:
入力音声フレームのバンド 0 に対して Agc::Process() メソッドを呼び出す
内部ではまず VoiceActivityDetector::ProcessChunk() を使って、細かいチャンク単位(数ms単位)でのボイス確率と RMS を求めている
RMS は "Root Mean Square" の略で、音圧に近い指標
その後、その二つの値を LoudnessHistogram::Update() を使って、ヒストグラムに格納している(RMS に対応するボイス確立を保持)
上の結果を使って MonoAgc::UpdateGain() メソッドで「適切な音量レベル」を更新する
RMS のヒストグラムから、現在の loudness を求め、それをターゲットとする loudness と比較する
現在とターゲットの loudness の差から、デジタルゲインコントールのゲイン値(対象となる調整量)を求める
デジタルゲインコントールで調整できる範囲を超えている場合には、その超過分を(物理マイク or エミューレートマイクでの)アナログ調整でカバーするために「適切な音量レベル」の値を更新する
次に MonoAgc::UpdateCompressor() で「デジタルゲインコントロールで使用されるゲイン値」を更新する
一つ上で求めたデジタルゲインコントロールのゲイン値をそのまま適用すると、音量が急変してしまう可能性があるので、徐々にターゲットに近づくように、ゲイン値を調整する
調整後のゲイン値は
GainControlImpl::set_compression_gain_db()
経由でデジタルゲインコントロールに渡されるただし現在の実装では
WebRTC-UseLegacyDigitalGainApplier
フラグが有効になっていないと、この値は使用されない(のであまり気にしなくても良い)
最後は、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
以外の場合)
大まかな処理の流れは次の通り:
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" という記載があるので、それをチェックしているものと思われる
ゲイン配列の適用
ApplyDigitalGain() 関数を用いて、上で求めたゲイン配列を入力音声フレームに適用する
基本的には、入力音声フレームの各サンプルにゲイン値を掛けているだけの単純な関数
1ms 区間の区切りで不自然にならないように、ゲイン値を徐々に調整していく、などの補助的な処理がいくつか入っている
「適切な入力音声レベル」の更新(モードが
kAdaptiveAnalog
の場合のみ)アナログマイク(ないしエミューレートされたマイク)による音量変更用に、チャネル毎に求めた「適切な入力音声レベル」から代表値を選択する
行なっていることとしては、上述の AgcManagerDirect::AggregateChannelLevels() と同様(最小値を選択しているだけ)
補足¶
マルチチャネルの扱い¶
基本的には各チャネルは独立して扱われる(特に分析系の処理では)
ただし、最終的に音量調整を行う際には、一番音量レベルが小さいチャネルを代表チャネルとして、それを基準として処理が適用される
マルチバンドの扱い¶
以下の処理は、入力音声フレームがマルチバンドに分割される前に適用される:
CaptureLevelAdjuster による入力音声フレームの音量調整
AgcManagerDirect の Analyze ( AgcManagerDirect::AnalyzePreProcess() メソッド)
入力音声フレームの分析・情報収集の際には、バンド 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 と異なり単体で使われることはない。
各種オプション¶
一番挙動に大きな影響を与えるのは、おそらく AudioProcessing::GainController1::Mode の値
それ以外にもパラメーターはたくさんあるがここでは割愛
設定できる項目については AudioProcessing::GainController1 構造体にまとまっている