Web Audio APIで音声ノイズを軽減する

#背景
Opentokを使ったビデオチャットで環境ノイズ音を軽減したくて調べた。
MediaStreamのaudioSourceに対して予めフィルタをかけたaudioトラックをセットすれば
ノイズを軽減できそうとわかった。

本気でノイズ除去やるならtensorflowとか使うんでしょ？
でもそこまでじゃない！そんな時はコレだぁぁ！みたいなものを投稿してみる。

#手順
1. audioStreamを作って
2. audioStreamにフィルタをかける

以上！

(Opentok部分は関係がないため触れません:wink:)


#1. audioStreamを作る

`navigator.mediaDevices.getUserMedia({constraint})`

constraintではvideoとaudioの設定ができる。

一番シンプルな形としては

```javascript

constraint = {audio: true, video: true}
```

になるが、今回videoは使用しないのでfalseを指定しておき、
audioはこだわりたいので以下で細かく設定を入れていく。

ちなみにaudioで指定できるパラメータについては以下の記事をかなり参考にしました。
https://qiita.com/yusuke84/items/842ef495d2e6eca05ae6

```javascript
constraint = {
    audio: {deviceId: micId},
    video: false,
    sampleRate: {ideal: 32000},
    sampleSize: {ideal: 16},
    echoCancellation: true,
    echoCancellationType: 'system',
    noiseSuppression: false,
    latency: 0.01
}
```
- audio
 - micId=マイクのデバイスIDです。事前に取得しておきます。
- sampleRate
 - 音の周波数です。Chromeのデフォルトは48kHz
 - 今回は32kHzはFM音源並の音質に変更しています。
 - 人の会話では主に125Hz~8000Hz程度が使用されているらしいので最低限の場合は8000*2(STEREO)で16kHzあれば足りますが、検証の際に明らかに音質が低かったため何度か検証を行い32kHzに設定しました。
 - 参考にした聴力のサイト：http://www.inoko.com/lijac/hocyouki/hearing-aids/test.html

- sampleSize
 - 音をデータ化する時の細かさです。Chromeのデフォルトは16bit
 - chromeのバージョンアップによって勝手に変更されるとのは好ましくないためハンドリングのためにあえて指定しています。
 - 検証の際に24bitまで上げてみたところ女性の声の一部の潰れてしまう周波数域が改善されたように感じました。
 - ただ、この数値を上げた場合にメモリの消費が増えるという記事をどこかで見たため、検証時にはとくに負荷的な変化は感じなかったのですが、、念のためデフォルト値に設定してます。

- echoCancellation
 - エコーを制御します。デフォルトはtrue
 - これもハンドリングのため指定してます。

- echoCancellationType
 - エコーキャンセルの種類です。デフォルトはsystem
 - Chrome独自の設定のようです。こちらもハンドリングのため指定してます。

- noiseSuppression
 - ノイズ軽減処理です。デフォルトはtrue
 - 今回手動でノイズ軽減を行いたいためfalseでそのまま通してもらいます。

- latency
 - そのままレイテンシです。デフォルトは不明
 - 0.001というような人に感知できないレベルの遅延も設定できますが、
 - ストリーミング中に音声が途切れたり急に速くなったりすることを考慮し一定の遅延を設定し安定させるような調整を行っております。（ここは調整の余地ありです）


#2. audioStreamにフィルタをかける

初心者なので[使い方](https://developer.mozilla.org/ja/docs/Web/API/AudioContext/createBiquadFilter)と[サンプル](https://weblike-curtaincall.ssl-lolipop.jp/portfolio-web-sounder/webaudioapi-effectors/demos/demo-23) を参考にしました。

```javascript

let context = new (window.AudioContext || window.webkitAudioContext)();
let mic = context.createMediaStreamSource(stream);
let output = context.createMediaStreamDestination();

// Create the instance of BiquadFilterNode
let bass = context.createBiquadFilter();
let middle1 = context.createBiquadFilter();
let middle2 = context.createBiquadFilter();
let treble1 = context.createBiquadFilter();
let treble2 = context.createBiquadFilter();
let treble3 = context.createBiquadFilter();
let lowpass = context.createBiquadFilter();
let highpass = context.createBiquadFilter();

// Set type
bass.type = (typeof bass.type === 'string') ? 'lowshelf' : 3;
middle1.type = (typeof middle1.type === 'string') ? 'peaking' : 5;
middle2.type = (typeof middle2.type === 'string') ? 'peaking' : 5;
treble1.type = (typeof treble1.type === 'string') ? 'highshelf' : 4;
treble2.type = (typeof treble2.type === 'string') ? 'highshelf' : 4;
treble3.type = (typeof treble3.type === 'string') ? 'highshelf' : 4;
lowpass.type = (typeof lowpass.type === 'string') ? 'lowpass' : 0;
highpass.type = (typeof highpass.type === 'string') ? 'highpass' : 1;

// Set frequency
lowpass.frequency.value = 8000;
highpass.frequency.value = 100;
bass.frequency.value = 200;
middle1.frequency.value = 600;
middle2.frequency.value = 1000;
treble1.frequency.value = 1800;
treble2.frequency.value = 4000;
treble3.frequency.value = 6000;

// Set Q (Quality Factor)
// bass.Q.value   = Math.SQRT1_2;  // Not used
middle1.Q.value = Math.SQRT1_2;
middle2.Q.value = Math.SQRT1_2;
// treble.Q.value = Math.SQRT1_2;  // Not used
lowpass.Q.value = 0.2;
highpass.Q.value = 0.7;

// Initialize Gain
bass.gain.value = -1;
middle1.gain.value = 3;
middle2.gain.value = -2;
treble1.gain.value = -8;
treble2.gain.value = -14;
treble3.gain.value = -26;

mic.connect(lowpass);
lowpass.connect(highpass);
highpass.connect(bass);
bass.connect(middle1);
middle1.connect(middle2);
middle2.connect(treble1);
treble1.connect(treble2);
treble2.connect(treble3);
treble3.connect(output);

audioSource = output.stream.getAudioTracks()[0];
```
操作する周波数の目安として先ほどaudioStreamの項目で貼っていた[聴力のサイト](http://www.inoko.com/lijac/hocyouki/hearing-aids/test.html)を参考にしました。

ポイントは２つ

- LPF＋HPFを使って100Hz~8000Hzを通してそれ以外をカットしています。
- 声のメインの周波数帯域の200Hz~1000Hzを保持して、それ以上の周波数はgain値にマイナスを振り徐々に減衰させています。

最終的にoutputのaudioトラック返して完了です。


余談ですが、
micからoutputまでのconnectの実装はギターの単品のエフェクターを繋いでるような感じしますね:guitar:

#課題
- まずは作ってみたというレベルなのでgain値は調整が必要と思う。
- 一部の音をカットしてる分の音圧が下がっているため、コンプレッサーやgainで増幅するように調整できればもっと良いのかもしれません。

本日はここまで！