【JavaScript 2020】音声ファイルの可視化をブラウザ上で簡単に試す（Web Audio API）

タイトル通りの内容です。
JavaScriptネタなので、後付けで [JavaScript のアドベントカレンダー](https://qiita.com/advent-calendar/2020/javascript2)の 23日目に登録をしてみたりもしました。

## さくっと可視化を試す
### ソースコードを探す
サンプルは探せばいろいろありそうでしたが、とりあえずサクッと試せそうで分かりやすそうなものを選んでみました。JavaScript関連でよくお世話になる MDN の以下のページのものをそのまま利用できました。

●AnalyserNode.getFloatFrequencyData() - Web APIs | MDN
　https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode/getFloatFrequencyData

ほぼサンプルそのまま、という利用方法です。ただし注意点があるので補足します。
まずは、以下が手を加える前のサンプルです。

```html
<!doctype html>
<body>
<script>
const audioCtx = new AudioContext();

//Create audio source
//Here, we use an audio file, but this could also be e.g. microphone input
const audioEle = new Audio();
audioEle.src = 'my-audio.mp3';//insert file name here
audioEle.autoplay = true;
audioEle.preload = 'auto';
const audioSourceNode = audioCtx.createMediaElementSource(audioEle);

//Create analyser node
const analyserNode = audioCtx.createAnalyser();
analyserNode.fftSize = 256;
const bufferLength = analyserNode.frequencyBinCount;
const dataArray = new Float32Array(bufferLength);

//Set up audio node network
audioSourceNode.connect(analyserNode);
analyserNode.connect(audioCtx.destination);

//Create 2D canvas
const canvas = document.createElement('canvas');
canvas.style.position = 'absolute';
canvas.style.top = 0;
canvas.style.left = 0;
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
document.body.appendChild(canvas);
const canvasCtx = canvas.getContext('2d');
canvasCtx.clearRect(0, 0, canvas.width, canvas.height);

function draw() {
  //Schedule next redraw
  requestAnimationFrame(draw);

  //Get spectrum data
  analyserNode.getFloatFrequencyData(dataArray);

  //Draw black background
  canvasCtx.fillStyle = 'rgb(0, 0, 0)';
  canvasCtx.fillRect(0, 0, canvas.width, canvas.height);

  //Draw spectrum
  const barWidth = (canvas.width / bufferLength) * 2.5;
  let posX = 0;
  for (let i = 0; i < bufferLength; i++) {
    const barHeight = (dataArray[i] + 140) * 2;
    canvasCtx.fillStyle = 'rgb(' + Math.floor(barHeight + 100) + ', 50, 50)';
    canvasCtx.fillRect(posX, canvas.height - barHeight / 2, barWidth, barHeight / 2);
    posX += barWidth + 1;
  }
};

draw();
</script>
</body>
```

### 動作させるために必要なこと（2つ）
Web上から利用可能な音声ファイルをダウンロードしてきて、「my-audio.mp3」という名前にリネームをして、上記のサンプルを動かそうとしたところ動作せず、以下のメッセージがコンソールに表示されていました。

![audio_html.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/d8f8e01e-98bc-6ec3-b666-f7d43d7be75e.jpeg)

少し補足すると、サンプルコードを適当に「audio.html」などという名前で保存し、ブラウザにそのままドラッグ＆ドロップして動かそうとしていました。

#### ローカルでサーバを動作させる
上記の解決策について説明します。

対応が必要な内容の 1つは、ローカルでサーバを動作させて HTMLファイルにアクセスする形にすることです。手軽に試すには、以下のどれかを使うのが個人的にはオススメです。今回は、Python のもの（`python -m SimpleHTTPServer`）を使いました。

●ワンライナーWebサーバを集めてみた - Qiita
　https://qiita.com/sudahiroshi/items/e74d61d939f18779970d

#### ユーザのアクションをトリガーにして音源を再生させる
もう 1つは、コンソールのメッセージに出ていた URL にアクセスした先、以下のページに書かれた内容に対応することです。

●Autoplay Policy Changes  |  Web  |  Google Developers
　https://developers.google.com/web/updates/2017/09/autoplay-policy-changes#webaudio

大まかに言うと、ユーザのアクションが行われてから処理を行う、というものです。以前から、スマホだと自動再生がされてしまわないようにこのような挙動をさせる仕様があったのは把握していたのですが、デスクトップ版のブラウザ（今回は Mac上で Chrome を利用）でも同様の状況になっていたとは・・・。

とりあえず、画面をクリックしたら初回のみサンプルのコードにあった処理を実行する形に書きかえました。以下がソースコードです。

```html
<!DOCTYPE html>
<body>
  <script>
    let isStarted = false;

    document.documentElement.addEventListener("mousedown", function () {
      if (!isStarted) {
        isStarted = true;

        mouse_IsDown = true;
        console.log("mouse down");

        const audioCtx = new AudioContext();

        //Create audio source
        //Here, we use an audio file, but this could also be e.g. microphone input
        const audioEle = new Audio();
        audioEle.src = "my-audio.mp3"; //insert file name here
        audioEle.autoplay = true;
        audioEle.preload = "auto";
        const audioSourceNode = audioCtx.createMediaElementSource(audioEle);

        //Create analyser node
        const analyserNode = audioCtx.createAnalyser();
        analyserNode.fftSize = 256;
        const bufferLength = analyserNode.frequencyBinCount;
        const dataArray = new Float32Array(bufferLength);

        //Set up audio node network
        audioSourceNode.connect(analyserNode);
        analyserNode.connect(audioCtx.destination);

        //Create 2D canvas
        const canvas = document.createElement("canvas");
        canvas.style.position = "absolute";
        canvas.style.top = 0;
        canvas.style.left = 0;
        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;
        document.body.appendChild(canvas);
        const canvasCtx = canvas.getContext("2d");
        canvasCtx.clearRect(0, 0, canvas.width, canvas.height);

        function draw() {
          //Schedule next redraw
          requestAnimationFrame(draw);

          //Get spectrum data
          analyserNode.getFloatFrequencyData(dataArray);

          //Draw black background
          canvasCtx.fillStyle = "rgb(0, 0, 0)";
          canvasCtx.fillRect(0, 0, canvas.width, canvas.height);

          //Draw spectrum
          const barWidth = (canvas.width / bufferLength) * 2.5;
          let posX = 0;
          for (let i = 0; i < bufferLength; i++) {
            const barHeight = (dataArray[i] + 140) * 2;
            canvasCtx.fillStyle =
              "rgb(" + Math.floor(barHeight + 100) + ", 50, 50)";
            canvasCtx.fillRect(
              posX,
              canvas.height - barHeight / 2,
              barWidth,
              barHeight / 2
            );
            posX += barWidth + 1;
          }
        }

        draw();
      }
    });
  </script>
</body>

```

### 実際に動作させた様子
こちらが、実際に動作させた時の画面です。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">久しぶりに Web Audio API を触った。<br><br>以下のサンプルにちょっとだけ手を加えたもの。それと、音源は「効果音ラボ」にあったピアノの音のファイルより。<br><br>●AnalyserNode.getFloatFrequencyData() - Web APIs | MDN<br>　<a href="https://t.co/Xrts0XTTLC">https://t.co/Xrts0XTTLC</a> <a href="https://t.co/HqQkxRBm48">pic.twitter.com/HqQkxRBm48</a></p>&mdash; you (@youtoy) <a href="https://twitter.com/youtoy/status/1343729936482226176?ref_src=twsrc%5Etfw">December 29, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

### 音声ファイルの入手元
なお今回用いた音源は、よくお世話になっているサイト「[効果音ラボ](https://soundeffect-lab.info/)」にあるものを使わせていただきました。