web audio api を使い、audioタグから取得した音声の音波を表示する ( javascript )

タイトルのとおり、audioタグから音声を取得し、web audio api を用いて音波を表示できるようにします。（ついでに、音量、再生位置、panをユーザーがブラウザを通して動的に操作できるようにします。）
本記事の大まかな流れは以下の通りです。
　１，　開発環境の準備
　２，　web audio api を使った音声の再生、音波の表示

最終的なディレクトリは次の通りとなります。

```
 root/
   ├ html/
   │  ├ index.html
   │  └ index.js
   │
   ├ audio/
   │  └ audio.mp3
   │
   ├ nginx/
   │   └ default.conf
   │ 
   ├ DockerfileNginx
   └ docker-compose.yml
```

<h2>1　開発環境の準備</h2>
　最初にwebサーバーを立てて、httpによってhtmlファイル、オーディオファイルを開けるようにします。web audio apiということだけあって、webサーバーを立てないと、オーディオファイルを取り扱えません。web audio api を使うときには、まず後述するAudioContextを作成してオーディオファイルと結びつけるのですが、その際オーディオファイルをhttpで取得しないと、音が正常に再生されません。
　私は最初、webサーバーを立てずに、audioタグのオーディオファイルをファイルパスから取得していたのですが、そのために少し時間を溶かしました。


<h3>DockerfileNginx</h3>
まずはDockerfileを作成します。

```:dockerfile
FROM nginx:1.19.0-alpine
```
とりあえずこれだけです。必要最小限です。

<h3>default.conf</h3>
次にnginxの設定ファイルを書きます。

```:default.conf
server {
  listen 80;
  client_max_body_size 20M;

  location / {
      root /usr/share/nginx/html/;
      index index.html
      try_files $uri $uri/ /index.html;
  }
}
```
上記簡単な解説は、
<b>listen 80;</b>
　ポート80番でrequestを待ち受けます。80番はhttpのデフォルトのポート番号。
<b>client_max_body_size 20M </b>
　これはデフォルトで1Mですが、1Mだとオーディオファイルを取り扱えないので、20Mで設定しました。
<b>location /  {  ...　</b>
・root以下は、htmlファイルを格納するパスを記載します。
・index index.html　→　/usr/share/nginx/html/内に格納されていて、これからブラウザで表示したいファイル名を指定します。

<h3>docker-compose.yml</h3>
最後にdocker-compose.ymlを作成します。

```:docker-compose.yml
version: '3.7'

services:
  nginx:
    build:
      context: .
      dockerfile: DockerfileNginx
    ports:
      - "80:80"
    volumes:
      - ./html:/usr/share/nginx/html
      - ./nginx/:/etc/nginx/conf.d/
```
上記簡単な解説は、
<b> - ./html:/usr/share/nginx/html</b>
　ローカルのhtml/ディレクトリ内のhtmlファイルをdockerのnginxコンテナ内（usr/share/nginx/html）にマウントしています。
<b>- ./nginx/:/etc/nginx/conf.d/</b>
　先ほど作成したdefault.confファイルの設定をdockerのnginxコンテナに反映させるために、ローカルのnginx/ディレクトリをdockerの/etc/nginx/conf.dディレクトリにマウントしています。

<h3>webサーバー動作確認</h3>
試しにnginxを動かしてみます。
まず以下のようなhtmlファイルを作ります。


```html:index.html
<html>
  <head>
  </head>
  <body>
    <h1>index.html</h1>
    <hr/>
  </body>
</html>
```
そしてターミナルで以下のコマンドを打ちます。

```terminal:
$ docker-compose up
```
そして、ブラウザでhttp://127.0.0.1にアクセスし、以下のような画面が表示されれば動作確認終了です。
![スクリーンショット 2021-03-22 23.32.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/476574/d4311812-9408-a183-21e0-984609e69900.png)

開発環境の準備は以上です。
<h2>2　web audio api を使った音声の再生</h2>
これからjavascriptを書いていきますが、その前にhtmlファイルを作成します。
先ほどのindex.htmlを次のように書き換えます。

```html:index.html
<html>
  <head>
      <meta charset='UTF-8'>
      <title>web audio api demo</title>
      <script src='./index.js' defer></script>
  </head>
  <body>
      <h1>web audio api</h1>
      <hr/>
      <audio src='./audio/audio.mp3' 
             crossOrigin='anonymous'
             ></audio><br/>
      <button>
          <span>Play/Pause</span>
      </button><br/>
      time
      <input type='range' id='time' min='0' value='0' step='1' /><br/>
      volume
      <input type='range' id='volume' min='0' max='0.16' value='0.08' step='0.001' /><br/>
      panner
      <input type='range' id='panner' min='-1' max='1' value='0' step='0.01'/><br/>
      <hr/>
      <div>canvas</div>
      <canvas id='mycanvas' width='640' height='400'></canvas>
  </body>
</html>

```
本当はaudioタグとcanvasタグが重要でinputタグは飾りですが、あとで使うので、今まとめて書いてます。

次に、javascriptを書いていきます。少し長めです。

```javascript:index.js
console.clear()

//audioタグから、オーディオエレメントを作ります。
const audioElement = document.querySelector('audio');

//AudioContextを作ります。
const audioContext = new AudioContext();
//異なるドメインから音声ファイルを取得する時に必要な設定です。本当は、今回は設定する必要がありません。
audioContext.crossOrigin='anonymous';

//AudioContextにaudioElementを紐づけます。
const track = audioContext.createMediaElementSource(audioElement);

//Gain Nodeを作ります。再生時に音量を設定するためのノードです。
const gainNode = audioContext.createGain();
gainNode.gain.setValueAtTime(0.08,0)

//Stereo Panner Nodeを作ります。パンを設定するためのノードです。
const pannerOptions = { pan : 0 };
const pannerNode = new StereoPannerNode(audioContext,pannerOptions);

//Analyzer Nodeを作ります。音波のビジュアライズをするために必要なノードです。
const analyserNode = audioContext.createAnalyser();
analyserNode.fftSize = 2048;//use for fast fourier transformation
const bufferLength = analyserNode.frequencyBinCount;//half the size of analyserNode.fftSize
const dataArray = new Uint8Array(bufferLength);

//今まで作成してきたノードを全て繋ぎます。
track.connect(gainNode).connect(pannerNode).connect(analyserNode).connect(audioContext.destination);

//音波の壁画のために、canvasを作成します。 
//またfunction draw()以下でcanvas常に音波を表示できるようにします。
const canvas = document.getElementById('mycanvas');
const canvasContext = canvas.getContext('2d');
const WIDTH=canvas.width
const HEIGHT=canvas.height
canvasContext.strokeRect(0,0,WIDTH,HEIGHT);
function draw(){
  drawVisual = requestAnimationFrame(draw);
  analyserNode.getByteTimeDomainData(dataArray);//音波データをUint8Array配列(dataArray)にコピー
  canvasContext.fillStyle = 'rgb(200,200,200)';
  canvasContext.fillRect(0,0,WIDTH,HEIGHT);
  canvasContext.lineWidth = 3;
  canvasContext.strokeStyle = 'rgb(0,0,0)';

  canvasContext.beginPath();

  const sliceWidth = WIDTH * (1.0/bufferLength);
  var x = 0;

  for(var i=0; i<bufferLength; i++){
    var v = dataArray[i] / 128.0;
    var y = v * HEIGHT/2 ;
    if(i ===0){
      canvasContext.moveTo(x,y);
    }else{
      canvasContext.lineTo(x,y);
    }
    x+=sliceWidth;
  }

  canvasContext.lineTo(WIDTH,HEIGHT/2);
  canvasContext.stroke();
};

draw();

//おまけ。再生位置、音量、パンをユーザーが画面上から設定できるようにします。
//再生ボタン作成
const playButton = document.querySelector('button');

//再生位置を自由に動かせるように、スライダーを作成。
const timeControl = document.querySelector('#time')
timeControl.setAttribute('max',audioElement.duration)

timeControl.addEventListener('input',function(){
  audioElement.currentTime=this.value;
},false)

audioElement.addEventListener('timeupdate',function(){
  timeControl.value = audioElement.currentTime
})

//音量を調整できるように、スライダーを作成。
const volumeControl = document.querySelector('#volume');
volumeControl.addEventListener('input',function(){
  gainNode.gain.value=this.value;
},false)

playButton.addEventListener('click',function(){
  //ユーザーのアクションがあるまで、オーディをファイルは'suspended'状態となり自動再生されません。
　//なのでボタンクリックと同時に、resume()で再生されるようにしています。
  if (audioContext.state==='suspended'){
    audioContext.resume();
  }

  // 再生した時と一時停止した時のロジック
  if (this.dataset.playing === 'false'){
    audioElement.play();
    this.dataset.playing='true'
  } else if (this.dataset.playing==='true'){
    audioElement.pause();
    this.dataset.playing = 'false'
  }
},false)

audioElement.addEventListener('ended',()=>{
  playButton.dataset.playing='false';
})

//パンを作成
const pannerControl = document.querySelector('#panner')
pannerControl.addEventListener('input',function(){
  pannerNode.pan.value=this.value;
},false)
```

上記jsファイルを書き終えた後は、http://127.0.0.1にアクセスして、オーディオ再生ボタンを押すだけです。
以下のようになります。
<b>音声再生前</b>
![スクリーンショット 2021-03-23 0.05.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/476574/945a2ff2-c3be-c055-6e53-95815ecc7467.png)
<b>音声再生中</b>
![スクリーンショット 2021-03-23 0.06.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/476574/07561b69-92b3-fe0f-5696-0f7c08297b5c.png)

音波が表示されています。また、スライダーの操作を通して、再生位置、音量、パンを調整できるようになっています。