SkyWayとTone.jsを使って、ボイスチェンジャー付きボイスチャットアプリを作る

最近ちまたでは、リモート会議や飲み会で、自分のカメラ映像をsnapCameraで面白おかしく加工するのが流行っています。

映像だけでなく、声も加工したくなるとき、ありますよね？

・・・ありますよね？（・・・きっとあるから読んで頂けているはず。）

自分の声を加工するためには「バ美声」などのボイスチェンジャーアプリと仮想オーディオデバイスが必要です。しかし、これらを用意するには手間がかかるし、仮想オーディオデバイスは割とトラブルがつきものです。。。

そこで、手間無しで、トラブルに悩まされることも無く、誰でも簡単にボイスチェンジ出来る、**ボイスチェンジャー付きボイスチャットアプリ**を作ってみることにしました！！

## 必要な材料

- [SkyWay](https://webrtc.ecl.ntt.com/)
ボイスチャットアプリを簡単に実装できるSDK & API

- [Tone.js](https://tonejs.github.io/)
Web Audio APIを簡易に扱うことができるフレームワーク。音の生成や加工が簡単にできます。


## 作り方

3STEPで作ります。各STEPで動作を確認できるCODEPENを用意しました。

では、やっていきましょう。

### STEP1. ただのボイスチャットアプリを作る

[SkyWay Javascript-SDKのチュートリアル](https://webrtc.ecl.ntt.com/documents/javascript-sdk.html#%E3%83%81%E3%83%A5%E3%83%BC%E3%83%88%E3%83%AA%E3%82%A2%E3%83%AB)のとおりにビデオチャットアプリを作ります。

コピペで作ったコード全文がこちら。

```js
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SkyWayチュートリアル</title>
    <script src="https://cdn.webrtc.ecl.ntt.com/skyway-latest.js"></script>
  </head>
  <body>
    <p id="my-id"></p>
    <video id="my-video" width="400px" autoplay muted playsinline></video>
    <textarea id="their-id"></textarea>
    <button id="make-call">発信</button>
    <video id="their-video" width="400px" autoplay playsinline></video>
    <script>
      let localStream;

      // カメラ映像取得
      navigator.mediaDevices.getUserMedia({video: true, audio: true})
        .then( stream => {
        // 成功時にvideo要素にカメラ映像をセットし、再生
        const videoElm = document.getElementById('my-video')
        videoElm.srcObject = stream;
        videoElm.play();
        // 着信時に相手にカメラ映像を返せるように、グローバル変数に保存しておく
        localStream = stream;
      }).catch( error => {
        // 失敗時にはエラーログを出力
        console.error('mediaDevice.getUserMedia() error:', error);
        return;
      });

      const peer = new Peer({
        key: 'あなたのAPIキー',
        debug: 3
      });

      peer.on('open', () => {
        document.getElementById('my-id').textContent = peer.id;
      });

      // 発信処理
      document.getElementById('make-call').onclick = () => {
        const theirID = document.getElementById('their-id').value;
        const mediaConnection = peer.call(theirID, localStream);
        setEventListener(mediaConnection);
      };

      // イベントリスナを設置する関数
      const setEventListener = mediaConnection => {
        mediaConnection.on('stream', stream => {
          // video要素にカメラ映像をセットして再生
          const videoElm = document.getElementById('their-video')
          videoElm.srcObject = stream;
          videoElm.play();
        });
      }
      peer.on('call', mediaConnection => {
        mediaConnection.answer(localStream);
        setEventListener(mediaConnection);
      });
    </script>
  </body>
</html>
```

[動作確認用CODEPEN](https://codepen.io/ibuibu/pen/pogbddZ)
別タブで２つページを開いて、テキストエリアに他方のIDをコピペして発信ボタンを押すと通信が開始します。（ヘッドホンなしだとハウリングします。注意してください。）

### STEP2. ボイスチェンジャー機能のテストをする

最終的には、自分の音声を加工して相手に送信するように実装するのですが、そうすると通信開始しないと声の変化を確認できないので、ちょっとテストが面倒です。

まずはテストとして、ローカルで自分の声の変化を確認してみます。

- **STEP2-1.** Tone.jsを[GitHub](https://github.com/Tonejs/Tone.js)よりダウンロードし、scriptタグでインポートします。自作のjsコードよりも上部に配置してください。

```html
<script src='./tone.js'></script>
```

- **STEP2-2.** ボイスチェンジ機能を実装します。マイク入力をピッチシフター（音程を変更するエフェクタ）とリバーブ（残響効果エフェクタ）に順に接続します。

```javascript
// マイクを用意し、ONにする
const micAudio = new Tone.UserMedia();
micAudio.open();

// testボタン押下のイベントリスナ
document.getElementById('test').addEventListener('click', () => {
  // ピッチシフター（音程を変更するエフェクタ）を用意。音の高さは5。
  const shifter = new Tone.PitchShift(5);
  // マスターオーディオ（スピーカー）に接続された、リバーブ（残響効果エフェクタ）を用意。
  const reverb = new Tone.Freeverb().toMaster();
  // マイク音声をピッチシフターに接続
  micAudio.connect(shifter);
  // ピッチシフターをリバーブに接続
  shifter.connect(reverb);
});
```

[動作確認用CODEPEN](https://codepen.io/ibuibu/pen/jOWrYRE)
testボタンを押すと、自分の声の音程が高くなっていることを確認できます。
*これだけChrome84だとうまく動かない可能性があります。他のブラウザを試してみてください。

今回は、ボイスチェンジャーらしくピッチシフターとリバーブを使用しましたが、Tone.jsには他にも様々なエフェクタが用意されています。
[（参考）Tone.js 公式Docs](https://tonejs.github.io/docs/)

エフェクタ同士をconnect関数で繋ぐことで、幾つもの効果を重ねてかけることができます。


### STEP3. ボイスチェンジャー機能を、相手に送る自分の声に適用する

STEP1で作ったボイスチャットに、STEP2のボイスチェンジャー機能を実装します。


```javascript
let localStream;

const micAudio = new Tone.UserMedia();
// マイクがオープンしたときのコールバック関数にgetUserMediaを格納
micAudio.open().then( () => {
  const shifter = new Tone.PitchShift(5);
  const reverb = new Tone.Freeverb();
  // 加工済みの音声を受け取る空のノードを用意
  const effectedDest = Tone.context.createMediaStreamDestination();
  micAudio.connect(shifter);
  shifter.connect(reverb);
  // リバーブを空のノードに接続
  reverb.connect(effectedDest);
  
  // カメラ映像取得
  navigator.mediaDevices.getUserMedia({video: true, audio: true})
    .then( stream => {

    // ストリームから音声トラックを削除
    const oldTrack = stream.getAudioTracks()[0];
    stream.removeTrack(oldTrack);
    
    // ストリームにエフェクトがかかった音声トラックを追加
    const effectedTrack = effectedDest.stream.getAudioTracks()[0];
    stream.addTrack(effectedTrack); 

    // 成功時にvideo要素にカメラ映像をセットし、再生
    const videoElm = document.getElementById('my-video')
    videoElm.srcObject = stream;
    videoElm.play();
    // 着信時に相手にカメラ映像を返せるように、グローバル変数に保存しておく
    localStream = stream;
  }).catch( error => {
    // 失敗時にはエラーログを出力
    console.error('mediaDevice.getUserMedia() error:', error);
    return;
  });
});

```

[動作確認用CODEPEN](https://codepen.io/ibuibu/pen/rNxLJzG)
STEP1と同様に、発信ボタンで通信をしてください。相手の加工済みの音声が聞こえます。

実装上のポイントは以下３点です。

- リバーブ等のエフェクタのノードからはストリーム（MediaStream）を取得できないため、Tone.context.createMediaStreamDestination()で空のノードを作成し、これを経由した
- micAudioがオープンして音声の加工が完了してから、getUserMedia関数を呼び出したいので、micAudio.open()後のコールバック関数の中にgetUserMedia関数を格納した
- getUserMedia関数の中で、相手に渡すストリームの音声を加工後のものに差し替えるため、既存のオーディオトラックを削除　→　加工後の音声トラックを追加　を行った。

・・・これで完成です！

### まとめ

Tone.jsでボイスチェンジャーが簡単に実装できました！

本来Web Audio APIでピッチシフターを自作するには高度な音響プログラミング技術が必要になりますが、Tone.jsによりあっさり出来ました。

他にも様々なエフェクタがあるのでぜひ試してみてください。

また、UI上でエフェクタの選択やパラメータの変更をできるようにすると、さらに面白くなると思います。