WebRTC の noiseSuppression 効果をチェックできるサイトを作ってみた

# これは何？

WebRTC （正確には `getUserMedia()`）では、ノイズの抑制を行う `noiseSuppression` の設定ができます（デフォルトは `true` の様子）。これ、実際どれぐらい効いているのかなーというのをふと知りたくなり、それのチェックサイトを作ってみました。。。という紹介記事です。

# サイト

サイトは、　[https://noisesuppression-checker.netlify.app/](https://noisesuppression-checker.netlify.app/)。 `start` をクリックすると、マイクから入力された音声を PCM とスペクトラムで表示してくれます。右下の `echoCancellation` や `noiseSuppression` スイッチを切り替えることで、波形からその効果を視覚的に確認できる・・・といった次第です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83466/1488e86c-f6e8-eb4e-a7f2-06193c3c9ba3.png)

構成は、下図の通り。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83466/0852c4ce-e474-bc1d-619d-02f4adf09131.png)


`getUserMedia()` から取得した音声をブラウザ内 WebRTC でローカル内で飛ばし、それを Audio タグに喰わせて再生しつつ、 WebAudio と canvas でビジュアライズしている構成です。（ WebRTC の部分は無くても良いような気はしますが・・・まぁノリでw

`noiseSuppression` とかは、 `getUserMedia()` で音声ストリーム取得時に指定する形です。

## 測定例

筆者の環境での測定例です。生活音がわずかに聞こえる程度の静かな室内でテストしてみました。Macにヘッドセットマイクを挿して計測しています。以下のスクリーンショットは、　Chrome M96　の場合です。

### echoCancellation: true, noiseSuppression: true

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83466/f85523b6-208f-2d55-b806-29d694c92fa3.png)


デフォルトの状態。全体的に軽くノイズがのっています。

## echoCancellation: false, noiseSuppression: true

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83466/5c69222f-222b-3906-b5b5-68bd6f1eee93.png)

ノイズがかなり低減されています。エコーキャンセラーは音声のフィードバックループを発生させるので、その分ノイズがのっちゃうのかもしれないですね。オフにして、 `noiseSuppression`　だけにすると、結構低減されました。

## echoCancellation: true, noiseSuppression: false

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83466/ff644538-e631-b4af-37e9-b1de7d8c7a33.png)

エコーキャンセラーいれた状態で、 `noiseSuppression` を外してみた状態。かなりノイズがのってしまうことが分かります。

ヘッドセットとかした環境であれば、 **エコーキャンセラーを off にする** というのは、音質向上を図るのに検討する価値があるかもしれません。

# 設定方法

## echoCancellation と　noiseSuppression

getUserMedia の引数として指定すれば OK です。例えば、 video を off にして、 `echoCancellation: off`, `noiseSuppression: on` とするのであれば

```js
const stream = await navigator.mediaDevices.getUserMedia({
  video: false,
  audio: {
      echoCancellation: false,
      noiseSuppression: true
  }
})
```

なお、この設定は [applyConstraints](https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/applyConstraints) では変更できません。また、 `noiseSuppression` は [こちらの MDN]( https://developer.mozilla.org/ja/docs/Web/API/MediaTrackSupportedConstraints/noiseSuppression#%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E4%BA%92%E6%8F%9B%E6%80%A7) にあるように safari では対応していないのでご注意を（たぶん、 `echoCancellation` が `true` であれば、　`noiseSuppression` も　　`true` といった感じなんじゃないかなぁ・・・と、波形を見ながら推測）　


通話中、動的に変えたい場合は、以下のように `getUserMedia()` で別の stream を生成し、 `sender.replaceTrack()` で変更してください。（あと、入れ替え前の track を `sender.track.stop()` で止めることを忘れずに）

```js
// RTCPeerConnection オブジェクトに対し、 getSenders() を呼び
// RTCRｔｐSender　オブジェクトを取得する
const [ sender ] = pc.getSenders()

sender.track.stop() // 差し替え前の音声トラックを止める

// エコーキャンセラーと noiseSuppression 両方を外す場合
const settings = {
  echoCancellation: false,
  noiseSuppression: false
}

const newStream = await navigator.mediaDevices.getUserMedia({
  video: false,
  audio: settings
})

// 差し替えるトラックを取り出した後、 replaceTrack() で差し替える
const [ newTrack ] = stream.getAudioTracks()

await sender.replaceTrack( newTrack )
```

これにより、 ice の再ネゴシエーション不要で、さくっと音声トラックを設定変更後のものに差し替えることができます。

## おまけ（ローカルループバックでの WebRTC）

おまけで、今回のサイトで用いた、ローカルループバックでの WebRTC のコーディング方法。 `async/await` が普通に使えるようになったおかげで、だいぶ楽に書けるようになりました。
（WebRTC が出たばかりの頃は、ローカルループバックでも書くのが大変だった・・・（遠い目

```js
// create RTCPeerConnection instance for both peer
//
pc1 = new RTCPeerConnection( {} )
pc2 = new RTCPeerConnection( {} )

// set icecandidate handlers for both peer
//
pc1.addEventListener('icecandidate', ev => {
  pc2.addIceCandidate( ev.candidate )
})
pc2.addEventListener('icecandidate', ev => {
  pc1.addIceCandidate( ev.candidate )
})

// handle media track transmitted by other peer.
//
pc2.addEventListener('track', ev => {
  // ... generate MediaStream, then apply to <Audio/> etc.
})

// set one-way negotiation ( pc1 -> pc2 )
//
const transceiver1 = pc1.addTransceiver('audio')
transceiver1.direction = 'sendonly'
const transceiver2 = pc2.addTransceiver('audio')
transceiver2.direction = 'recvonly'

// set source audio track on pc1
//
stream.getTracks().forEach( track => pc1.addTrack( track ))

// handle offer description
//
const offer = await pc1.createOffer()
await pc1.setLocalDescription( offer )
await pc2.setRemoteDescription( offer )

// handle answer description
//
const answer = await pc2.createAnswer()
await pc2.setLocalDescription( answer )
await pc1.setRemoteDescription( answer )
```

# 最後に

設定の効き具合は、ブラウザや用いるマイクデバイスによって違いますので、様々な環境でチェックしてみると面白いかなーと思います。

コードの全体は、 https://github.com/kokutele/noise-suppression-demo で公開してあります。興味のある方はこちらも確認ください。

あと、最近 safari の autoplay policy には泣かされることが多いですが、今回のケースでは、接続完了直後、送信側で `replaceTrack()` してストリームを入れ替えるコード（設定は同一）をいれないと音声再生がされませんでした(iOS 15 で確認)。ケースによって対応方法が違うので、ほんとやっかい・・・