快適Web会議！ Windows コアオーディオAPIで自動音量調整


「ラウドネス等化」がないサウンドデバイスにおいて、参加者間で声の大きさが異なるweb会議が多少快適になる、自動音量調整プログラムを作ってみました。
XスペースやYouTube試聴にも使用できます。

Windows コアオーディオAPI の EndpointVolume でリアルタイムの音量レベルを取得し、同じく EndpointVolume で音量調整を設定します。
タスクバーの音量調整を自動で操作するような動作をします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/210035/4ba0bc88-1a53-73f7-81a7-2f722150efc0.png)


## 作成背景

Web会議の自動音量調整です。
Web会議アプリの設定で個々人のマイクの音量調整はされていますが、複数人での通話になるとメンバー間で音量がばらついています。
また、マイクが遠いと補正しきれません。

テレワーク主体から出社主体に戻りましたが、会議室を予約して・・・の不便な働き方には今さら戻れず、「自席でWeb会議」がデファクトスタンダードになっています。
すると、在宅とは違って困ることが起こります。

#### カナル型イヤホンが使えない
オフィスではweb会議中に面直で話しかけられます。
遮音性に優れたカナル型イヤホンは使えず、骨伝導やオープンエア型など、周囲の騒音が聴こえるタイプのヘッドセットを使う必要があります。
みんな自席でweb会議してガヤガヤしています。余計に小さい声の発言者は聞こえません。

#### 一部が会議室から参加
一部のチームが会議室から参加しているパターン。
web会議用のマイクを備えていない会議室の場合、代表者のPCで会議室全体の音声を拾っていますから、PCから遠い人の距離は音量を上げないと聴き取れません。

#### ラウドネス等化がない
Windows PC の自動音量調整機能と言えば「ラウドネス等化」ですが、サウンドデバイスにより非対応の場合があります。
会社で使用しているPCも「ラウドネス等化」は非対応です。

自宅にある2台の内蔵デバイスでも片方は対応しておらず、非対応の場合はそもそも「拡張」タブ自体が出てきません。
PC内蔵デバイスが対応していても、デジタル接続のヘッドセットを使用する場合はヘッドセットのドライバが対応していなければ使用できません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/210035/38e8e0db-09fc-c199-a982-ebb926110f4f.png)

#### ハードウェアAGC(オートゲインコントロール)回路を持ち込めない
コロナ時代は在宅でしたから、アナログ電子回路で作ったAGC回路をヘッドホンアンプの手前に挟んで自動音量調整していました。
会社に電子回路基板を持ち込むわけにはいきません。

### じゃソフトウェアにしてPCの中にAGCを入れてしまおう！
ということでソフトウェアでAGCを模擬しました。

## アーキテクチャと実装

コアオーディオAPIの __EndpointVolume__ を使用しました。

https://learn.microsoft.com/ja-jp/windows/win32/coreaudio/endpointvolume-api

EndpointVolumeとは、スピーカーやヘッドホンなど最終的に音声を再生するデバイスの音量を操作するAPI、分かりやすく言えば __タスクバーの音量調整をプログラムで操作できるAPI__ です。

コアオーディオAPIには、他にも左右それぞれの音量を設定するAPI、アプリケーションごとの音量を調整するAPIも用意されています。サウンド設定画面の音量をプログラムで操作できるAPIです。

自動音量調整の用途では、アプリ問わず「耳に聴こえる音量」を、大きすぎず小さすぎず の状態に維持するよう制御したいです。
言い換えればマウスで行っていたタスクバーの音量操作を自動でやって欲しいということになり、左右一括でマスター音量を調整できる EndpointVolume が適します。

なお無線イヤホン ＋ 内蔵スピーカーのように、EndpointDevice が複数有効化されている場合は EndpoinVolume も複数使用できますが、 ~~デバイス指定は実装が面倒くさいので~~ __既定のデバイス を制御する__ ようにしています。
よって __Zoomなどで規定外のデバイスを手動指定している場合は作用しません__。
本記事では省略していますが、イヤホンのプラグが抜けるなど既定のデバイスが変更された場合は、Mmdevice API で検出して対応しています。

試聴を繰り返し、最終的に下記のようなアーキテクチャになりました。
定期実行タイマーは SetTimer関数や ループ + Sleep関数 を使います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/210035/5f0368cd-e253-f901-061a-da0ee3bf6f3c.png)

APIで取得・設定している情報をGUIの音量ミキサーで当てはめると、下図になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/210035/de761f02-0636-1ac1-0497-ade133b8b8ec.png)


#### フィードフォワード型を採用
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/210035/c479d677-d4a5-17a0-5902-9a2432fea86a.png)

AGCに限らず、自動調整系は調整前の入力側を監視して目標出力が得られるように調整を行う「フィードフォワード型」と、調整後の出力側を監視して目標からずれていたら調整量を修正する「フィードバック型」があります。

電子回路でAGCを作る場合は、音量調整部に非線形な特性を持ち、温度や電圧変動などの影響（外乱）で動作点が変化する素子(MOS-FETなど)を使いますから、外乱や非線形も補正できる「フィードバック型」一択です。

一方、今回はソフトウェアで作成します。
APIドキュメントには以下の説明があります。

>まず、多くのオーディオ アダプター デバイスにハードウェア ボリューム コントロールがありません。 デバイスにハードウェア ボリューム制御がない場合、EndpointVolume API の IAudioEndpointVolume インターフェイスは、そのデバイスとの間のストリームにソフトウェア ボリューム コントロールを自動的に実装します

ソフトウェアの場合、例えば「n dB 減衰」として指定すれば指定通りに正確な音量調整をしてくれますから、フィードフォワード型を採用できます。
デバイスがハードウェアボリュームを備える場合、APIはハードウェアボリュームを制御しますが、ハードウェア部の外乱はデバイス内で補正されているはずですから、ソフトウェアの世界では正確とみなします。

また EndpointVolume API では、出力音量を取得する関数が用意されていません。
出力音量の情報が欲しい場合は、IAudioMeterInformation で入力側で取得したうえで、 IAudioEndpointVolume で音量調整部の設定値を取得して計算で求めることになります。
音量調整部に外乱（指示値と実際の値が違う）はないはずですが、仮に音量調整部に外乱があっても補正できないにも関わらず計算量だけが増えるということになり、フィードバック型を採用するメリットがありません。

#### 復帰付きピークホールドで音量を徐々に上げる動作の実現
「大音量になった際に速やかに音量を下げ、徐々に戻していく」という動作をさせるため、復帰付きのピークホールドを通しています。
大音量時に速やかに下げられないとうるさいですし、復帰動作がないと音量が下がったきり戻りません。
復帰が速すぎると、音飛びのように聴こえます。

#### 目標音量の操作には速やかに反応すること

本ソフト動作中はタスクバーの音量は自動制御されていますから、本ソフトの出力音量目標値つまみがPCの音量つまみの位置づけとなります。
つまみの操作には速やかに聴こえる音量が追従しないと操作性が悪化します。

検出復帰付きピークホールドを通した減衰前レベルと目標音量を比較して減衰量を制御する構造とすることで、目標音量つまみを操作した際は音量上げる側でも速やかにアッテネータ量が応答するようにしています。

アッテネータ制御量側にピークホールドを入れる構造案もありますが、目標音量つまみを操作した場合にも徐々に大きくなる動作になります。

### 主処理部の詳細
#### ヒステリシス制御
ピークメーターで得られた入力音量と、目標音量を比較し、出力が目標音量に近くなるような設定指示値を求めて音量調整部に指示するだけです。
ただし、やみくもに指示するわけにはいきません。

デバイスごとに、最小・最大ボリュームレベル及び1ステップの増分は決まっています。
デバイス仕様は GetVolumeRange 関数により取得できます。
```c++
//GetVolumeRange(最小ボリュームレベル(dB), 最大ボリュームレベル(dB), 増分(dB))
pEndpointVol->GetVolumeRange(&pflVolumeMindB, &pflVolumeMaxdB, &pflVolumeIncrementdB);
```

最小未満・最大超過の設定指示は当然できません。
最小未満の場合は最小+1ステップ、最大超過の場合は最大を設定指示しています。

最小～最大の設定可能範囲内でも、増分より細かな設定指示をした場合は近い値に丸めて反映されます。
そこでヒステリシス制御を入れ、ピークメーター値と目標音量から求めた設定指示値が、現在のボリューム設定値±増分 を超えていた場合のみ設定指示しています。

※現在の設定値は GetMasterVolumeLevel 関数を用いて取得していますが、近い値に丸められた後の値ではなく直前で呼び出した SetMasterVolumeLevel 関数で指示した値が返ってきます。

更新する必要がない場合の不必要な SetMasterVolume 関数呼び出し防止が目的です。
SetMasterVolume 関数を高速で呼び過ぎると、タスクバーの音量バー更新が処理落ちするようで、激しく振動し壊れます(発生した場合 windows 再起動で戻ります。)

高級なサウンドデバイスで増分が細かい場合は、API呼び出し回数を減らすため、デバイス仕様より粗い値(今は0.5dB)に置き換えています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/210035/630f9769-a860-2379-a6d6-ecc3fa6cb49a.png)

ボリューム動作の詳細は下記のドキュメントの「注釈」に記載されています。

https://learn.microsoft.com/ja-jp/windows/win32/api/endpointvolume/nf-endpointvolume-iaudioendpointvolume-getvolumerange

#### 2024更新 背景雑音への対応

扇風機のような背景雑音がフェードインしてこないように、設定した閾値を下回った時、音量を上げる動作を一時停止する機能を追加しました。

web会議では、USB扇風機のようにある程度の音量で連続するバックグラウンド騒音が入ることがあります。
当初から誰も発言していないときに無駄に音量がMAXにならないよう、入力レベルが閾値以下の場合は動作を一時停止する機構を入れていました。
マイクアンプのノイズのような極小さな入力信号を想定し、一定の入力音量を下回ったらピークホールド更新以降を処理をスキップし、音量上昇・下降動作ともに自動音量調整動作を一時停止する仕様にしていました。

ところが、会議参加者がUSB扇風機を使い始めて問題が起きました。
扇風機の音のような一時停止閾値を超える雑音が入ってきた際は自動音量調整が動作します。
特に扇風機のような連続する雑音は、発言がないタイミングになるたび、発言を聞くのと変わらない音量までフェードインしてきて非常にストレスでした。  


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/210035/575e1460-e5d7-48e8-c688-40253634ac84.png)


そこで、一時停止閾値を可変にしてオプション画面から設定できるように変更し、併せて音量上昇操作のみを一時停止する仕様変更しました。  

上昇も下降も両方とも一時停止する仕様のまま閾値を可変にすると、
目標音量 < 一時低閾値  
に設定した際、目標音量を超えた爆音が出力される問題があります。  

減衰無し(タスクバーの音量MAX)からスタートして徐々に入力音量を上げていくと、
出力目標音量目標値を超えても一時停止閾値を超えるまで自動音量が効かず出力音量が大きくなり続け、一時停止閾値を超えると急に音量が下がる動作となります。

一方、上昇のみ一時停止ならば、一時停止閾値を下回っていても出力音量目標値を超えたら音量を絞る方向は動作しますから、出力音量目標値を超えずに済みます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/210035/063026e5-010c-7f29-cd71-3d404c74e93d.png)

上昇・下降ともに一時停止であっても
一時停止閾値 <= 目標音量  
になるように一時停止閾値設定値を制限するリミッタを入れれば出力音量目標値を超過する問題は解消されます。

はじめこちらを試しましたが、
目標音量 < 扇風機音量  
に設定した際に扇風機が発言と同じ音量までフェードインする問題が解消されず、小さい音で聴きたい時に困りました。  
また出力音量目標値を下げた際、裏で一時停止閾値設定が共連れで更新されるのも、使ってみると不便でした。   

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/210035/d7c2ae2c-da04-12ce-ebba-dc5118e255dc.png)

以上から、下記3つの方法を試して、上昇のみ一時停止　としました。   
・上昇のみ一時停止【採用】   
・上昇・下降ともに一時停止   
・上昇・下降ともに一時停止 with 一時停止閾値 <= 目標音量 に制限   

#### 主処理部の実装

ミュート時の停止機能も搭載し、主処理部の実装は下記のようにしています。
減衰前音量を取得して前処理部したのち、音量(減衰量)を設定しています。

```c++
// 音量調整処理　ループまたは繰り返しタイマーで呼ばれる

//ピークホールドの復帰一時停止用定数 
#define STOP_P_HOLD_DEC 0.0f
 
void drive_autoVol() {
//略
   //ミュート時動作停止
   pEndpointVol->GetMute(&endVol_isMute);
   //純粋boolじゃないのであえて冗長に==実装
   if (endVol_isMute == TRUE) {
       return;
   }

   // マスターボリューム前の音量をリニア値で取得
   pMeterInfo->GetPeakValue(&beforeATTLevelRealT);

   // マスターボリューム前の音量をdB値に
   beforeATTLevelRealT = preTrt::to_dB(beforeATTLevelRealT);

   // 無音時動作停止
   if (beforeATTLevelRealT < stopTh_dB) {
       return;
   }

    // ATT前の音量をピークホールド処理
    if (beforeATTLevelRealT < stopTh_dB) {
        //入力音量が小さいときはピークホールドの復帰をしない(音量上昇の一時停止)
        beforeATTLevelPeakH = preTrt::pk_Hold(beforeATTLevelRealT, pflVolumeMindB, STOP_P_HOLD_DEC);
    }
    else {
        //入力音量が閾値以上の時は復帰付きピークホールド処理
        beforeATTLevelPeakH = preTrt::pk_Hold(beforeATTLevelRealT, pflVolumeMindB, p_hold_dec);
    }

   // 現在のATT設定を取得 
   pEndpointVol->GetMasterVolumeLevel(&currentATT);

   // ATT量を計算 
   nextATT = targetLeveldB - beforeATTLevelPeakH;

   if (nextATT < pflVolumeMindB + pflVolumeIncrementdB) {
       // －∞はできない → MUTEしない最小ボリュームに設定 
       nextATT = pflVolumeMindB + pflVolumeIncrementdB;
   }
   if (pflVolumeMaxdB < nextATT) {
       // 増幅はできない → 最大に設定 
       nextATT = pflVolumeMaxdB;
   }
   
   //ヒステリシス制御
   if (pflVolumeIncrementdB < abs(currentATT - nextATT)) {
       // 現在の設定値と新しい設定の差が増分を超えていたら反映 
       pEndpointVol->SetMasterVolumeLevel(nextATT, NULL);
   }
   else {
       //音量維持
   }
   return;
}
```

GitHub:
https://github.com/hmpow/AutoVolume/blob/master/AutoVolume/src/AutoVolumeCore.cpp

### 前処理部の詳細

主処理部に全て詰め込むと関数が長くなるため、前処理部をネームスペースでまとめて外へ出しています。
~~C → C++ 移植時にクラス設計が面倒くさかった~~

#### 内部処理はdB値で
ピークメーターは GetPeakValue により無音から最大音量を 0.0～1.0 で正規化されたリニア値で取得され、音量設定は正規化値を用いる SetMasterVolumeLevelScalar と dB 値を用いる SetMasterVolumeLevel の2つが用意されています。

聴覚は対数になっており、騒音測定や音響機器など、聴覚に関する機材では dB が多く用いられています。
目標値を dB 値で制御した方が直感的に操作できるのと、処理も可減算になりわかりやすいため、ピークメータ値を dB 値に変換してから扱っています。

ピークメーターは電子回路的には電圧計とみなせますから、
dB値 = 20 × log10(正規価値) です。
最大が 0dB、無音が -∞dB になります。

#### 小音量時音量上昇一時停止の実現

小音量時音量上昇一時停止は、一時停止閾値を下回っている場合は入力部ピークホールドの復帰をしないことにより実現しています。
ピークホールドされている値より大きな音が入ってきた場合は、一時停止閾値にかかわらず新たな値がメモリされ出力音量はすぐに絞られます。
ピークホールドされた値と目標音量を比較してアッテネータ制御していますから、ピークホールドがリリースしなければ出力音量は絞られた状態が維持されます。
一時停止し閾値を上回る音量が入力されるとピークホールドのリリース操作が再開され、目標音量より小さければ徐々に出力音量が上がっていきます。

#### ピークホールド部

電子回路のAGCでは、音量検出の整流回路の後に、コンデンサにより平滑、放電回路により徐々に減衰量を減らしていくという構成になっています。
このコンデンサと放電回路の働きをソフトウェアで模擬しています。
メモリ値から毎周期一定の値を引いていき、メモリ値より大きな値が来たら更新します。
無音ですと-∞ になってしまいますから、最小値を制限できるような関数にしています。

1周期当たりの復帰量は引数で受け取れるようにしています。
小音量時音量上昇一時停止は、呼び出し側から復帰量に 0.0dB を渡して復帰機能は無効とし、大きな値が入ってきたときのメモリ更新は行うが復帰しない純粋なピークホールド動作をさせています。  

```c++
float pk_Hold(float input, float min, float decrement) {
    // 初回はminで初期化 
    static float peak = min;

    // decrement値チェックと減衰 
    if (decrement > 0.0001f && peak > (min + decrement)) {
        peak = peak - decrement;
    }

    // data値チェックとピーク値更新 
    if (input > min && input > peak) {
        peak = input;
    }
    return peak;
}
```


## ソースコード(github)
全てのソースコートはgithubで公開しております。
https://github.com/hmpow/AutoVolume

下記で確認しています
OS  : Windows 11 Pro 64bit
IDE : Visulal Studio Community 2022 Version 17.5.0
      ※C++によるデスクトップ開発 でセットアップ