Web Audio APIを利用してオーディオビジュアライザを作成する ~その2 再生中の音から波形データを取得して描画する~


Web Audio APIを利用してオーディオビジュアライザを制作したので、それに関する備忘録です。
まとめて書くと内容が長くなり、複雑で解りにくなりそうなので分けて書いています。

その2では再生中の音から波形データを取得してcanvasに描画します。
Web Audio APIで音の出し方が不明なかたはその1をご覧ください。

- [Web Audio APIを利用してオーディオビジュアライザを作成する ~その1 音声データを読み込んで音を出す~](http://qiita.com/soarflat/items/b60649195c16c7a43868)

# 再生中の音から波形データを取得して描画する
![wave.jpg](https://qiita-image-store.s3.amazonaws.com/0/69667/b81c6c99-17dc-322c-55de-8857b85353c7.jpeg)

[デモ](http://hira777.github.io/qiita/web-audio/demo4/index.html)**(ページを開くと音が流れるので音量注意)**

デモでは`OscillatorNode`で生成した音を再生し、波形データを取得してcanvasに描画しています。
上下2種類のグラフが描画されていますが、上のグラフが**時間領域の波形データ**を描画し、下のグラフが
**周波数領域の波形データ(振幅スペクトル)**を描画したものになります。
右上のバーで周波数と音量を調節でき、数値を変更すると描画も変更されます。

## 時間領域の波形データ
時間領域はx軸が時間、y軸は振幅で表された領域です。
デモの上のグラフでは0ms~約23ms秒までの時間毎の振幅が描画されています。
音量を大きくすれば、振幅が大きくなり、周波数を大きくすれば、波が細かくなります。
デモでは`OscillatorNode`で生成した一定の周波数、音量である音の波形データを
描画しているため、同じ周期の波が描画されています。

## 周波数領域の波形データ
時間領域はx軸が周波数、y軸はdbで表された領域です。
デモの下のグラフでは0Hz~約21579Hzまでの周波数毎のdbが描画されています。
周波数を変更すれば、描画される位置が変わります。

## 波形データの取得方法
これらの波形データを取得するために、`AnalyserNode`を生成する必要があります。

##AnalyserNode
リアルタイムの波形データを取得できるノードです。
時間領域の波形データは`getByteTimeDomainData`メソッド、
周波数領域の波形データは`getByteFrequencyData`メソッドで取得できます。

# getByteTimeDomainDataで取得した波形データを描画する。
[デモ](http://hira777.github.io/qiita/web-audio/demo5/index.html)**(ページを開くと音が流れるので音量注意)**

## ディレクトリ構成

```lang:
.
├── index.html
├── audio.js
└── sample.mp3
```

## index.html

```html:index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>getByteTimeDomainDataで取得した波形データを描画する。</title>
<style>
body {
  margin: 0;
  color: #fff;
  background: #000;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script src="audio.js"></script>
</body>
</html>
```

## audio.js

`audio.js`は以下のような処理になる。

- canvas要素を取得し、コンテキストを取得する。
- 読み込んだ音声データを`AudioBufferSourceNode`で音源に設定する。
- `AnalyserNode`を生成し、`AudioBufferSourceNode`と`AudioDestinationNode`に接続する。
- 再生とrequestAnimationFrameでの描画処理を開始する。
- 再描画する前に再生中の波形データを取得して再描画する。

```js:audio.js
// canvas要素を取得
var c = document.getElementById('canvas');
var cw;
var ch;

// canvasサイズをwindowサイズにする
c.width = cw = window.innerWidth;
c.height = ch = window.innerHeight;

// 描画に必要なコンテキスト(canvasに描画するためのAPIにアクセスできるオブジェクト)を取得
var ctx = c.getContext('2d');

// AudioNodeを管理するAudioContextの生成
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();


/**
 * 音声ファイルローダー
 */
var Loader = function(url) {
  this.url = url;  // 読み込む音声データのURL
};

// XMLHttpRequestを利用して音声データ(バッファ)を読み込む。
Loader.prototype.loadBuffer = function() {
  var loader, request;
  loader = this;
  request = new XMLHttpRequest();
  request.open('GET', this.url, true);
  request.responseType = 'arraybuffer';

  request.onload = function() {
    // 取得したデータをデコードする。
    audioCtx.decodeAudioData(this.response, function(buffer) {
      if (!buffer) {
        console.log('error');
        return;
      }
      loader.playSound(buffer);  // デコードされたデータを再生する。
    }, function(error) {
      console.log('decodeAudioData error');
    });
  };

  request.onerror = function() {
    console.log('Loader: XHR error');
  };

  request.send();
};

// 読み込んだ音声データ(バッファ)を再生と波形データの描画を開始する。
Loader.prototype.playSound = function(buffer) {
  var visualizer = new Visualizer(buffer);
};


/**
 * ビジュアライザー
 */
var Visualizer = function(buffer) {
  this.sourceNode = audioCtx.createBufferSource();  // AudioBufferSourceNodeを作成
  this.sourceNode.buffer = buffer;                  // 取得した音声データ(バッファ)を音源に設定
  this.analyserNode = audioCtx.createAnalyser();    // AnalyserNodeを作成
  this.times = new Uint8Array(this.analyserNode.frequencyBinCount);  // 時間領域の波形データを格納する配列を生成 
  this.sourceNode.connect(this.analyserNode);       // AudioBufferSourceNodeをAnalyserNodeに接続
  this.analyserNode.connect(audioCtx.destination);  // AnalyserNodeをAudioDestinationNodeに接続
  this.sourceNode.start(0);                         // 再生開始
  this.draw();                                      // 描画開始
};

Visualizer.prototype.draw = function() {
  // 0~1まで設定でき、0に近いほど描画の更新がスムーズになり, 1に近いほど描画の更新が鈍くなる。
  this.analyserNode.smoothingTimeConstant = 0.5;

  // FFTサイズを指定する。デフォルトは2048。
  this.analyserNode.fftSize = 2048;

  // 時間領域の波形データを引数の配列に格納するメソッド。
  // analyserNode.fftSize / 2の要素がthis.timesに格納される。今回の配列の要素数は1024。
  this.analyserNode.getByteTimeDomainData(this.times);

  // 全ての波形データを描画するために、一つの波形データのwidthを算出する。
  var barWidth = cw / this.analyserNode.frequencyBinCount;

  ctx.fillStyle = 'rgba(0, 0, 0, 1)';
  ctx.fillRect(0, 0, cw, ch);

  // analyserNode.frequencyBinCountはanalyserNode.fftSize / 2の数値。よって今回は1024。
  for (var i = 0; i < this.analyserNode.frequencyBinCount; ++i) {
    var value = this.times[i]; // 波形データ 0 ~ 255までの数値が格納されている。
    var percent = value / 255; // 255が最大値なので波形データの%が算出できる。
    var height = ch * percent; // %に基づく高さを算出
    var offset = ch - height;  // y座標の描画開始位置を算出

    ctx.fillStyle = '#fff';
    ctx.fillRect(i * barWidth, offset, barWidth, 2);
  }

  window.requestAnimationFrame(this.draw.bind(this));
};

// requestAnimationFrameを多くのブラウザで利用するためにprefixの記載
var setUpRAF = function() {
  var requestAnimationFrame = window.requestAnimationFrame ||
                              window.mozRequestAnimationFrame ||
                              window.webkitRequestAnimationFrame ||
                              window.msRequestAnimationFrame;
  window.requestAnimationFrame = requestAnimationFrame;
};

setUpRAF();
var loader = new Loader('sample.mp3');
loader.loadBuffer();
```

# getByteFrequencyDataで取得した波形データを描画する。

[デモ](https://hira777.github.io/qiita/web-audio/demo6/index.html)**(ページを開くと音が流れるので音量注意)**
getByteTimeDomainDataで取得した波形データを描画するための記述とほとんど変わらないです。

## ディレクトリ構成

```lang:
.
├── index.html
├── audio.js
└── sample.mp3
```

## index.html

```html:index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>getByteTimeDomainDataで取得した波形データを描画する。</title>
<style>
body {
  margin: 0;
  color: #fff;
  background: #000;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script src="audio.js"></script>
</body>
</html>
```

## audio.js

`audio.js`は以下のような処理になる。

- canvas要素を取得し、コンテキストを取得する。
- 読み込んだ音声データを`AudioBufferSourceNode`で音源に設定する。
- `AnalyserNode`を生成し、`AudioBufferSourceNode`と`AudioDestinationNode`に接続する。
- 再生とrequestAnimationFrameでの描画処理を開始する。
- 再描画する前に再生中の波形データを取得して再描画する。

```js:audio.js
// canvas要素を取得
var c = document.getElementById('canvas');
var cw;
var ch;

// canvasサイズをwindowサイズにする
c.width = cw = window.innerWidth;
c.height = ch = window.innerHeight;

// 描画に必要なコンテキスト(canvasに描画するためのAPIにアクセスできるオブジェクト)を取得
var ctx = c.getContext('2d');

// AudioNodeを管理するAudioContextの生成
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();


/**
 * 音声ファイルローダー
 */
var Loader = function(url) {
  this.url = url;  // 読み込む音声データのURL
};

// XMLHttpRequestを利用して音声データ(バッファ)を読み込む。
Loader.prototype.loadBuffer = function() {
  var loader, request;
  loader = this;
  request = new XMLHttpRequest();
  request.open('GET', this.url, true);
  request.responseType = 'arraybuffer';

  request.onload = function() {
    // 取得したデータをデコードする。
    audioCtx.decodeAudioData(this.response, function(buffer) {
      if (!buffer) {
        console.log('error');
        return;
      }
      loader.playSound(buffer);  // デコードされたデータを再生する。
    }, function(error) {
      console.log('decodeAudioData error');
    });
  };

  request.onerror = function() {
    console.log('Loader: XHR error');
  };

  request.send();
};

// 読み込んだ音声データ(バッファ)を再生と波形データの描画を開始する。
Loader.prototype.playSound = function(buffer) {
  var visualizer = new Visualizer(buffer);
};


/**
 * ビジュアライザー
 */
var Visualizer = function(buffer) {
  this.sourceNode = audioCtx.createBufferSource();  // AudioBufferSourceNodeを作成
  this.sourceNode.buffer = buffer;                  // 取得した音声データ(バッファ)を音源に設定
  this.analyserNode = audioCtx.createAnalyser();    // AnalyserNodeを作成
  this.freqs = new Uint8Array(this.analyserNode.frequencyBinCount);  // 周波数領域の波形データを格納する配列を生成 
  this.sourceNode.connect(this.analyserNode);       // AudioBufferSourceNodeをAnalyserNodeに接続
  this.analyserNode.connect(audioCtx.destination);  // AnalyserNodeをAudioDestinationNodeに接続
  this.sourceNode.start(0);                         // 再生開始
  this.draw();                                      // 描画開始
};

Visualizer.prototype.draw = function() {
  // 0~1まで設定でき、0に近いほど描画の更新がスムーズになり, 1に近いほど描画の更新が鈍くなる。
  this.analyserNode.smoothingTimeConstant = 0.5;

  // FFTサイズを指定する。デフォルトは2048。
  this.analyserNode.fftSize = 2048;

  // 周波数領域の波形データを引数の配列に格納するメソッド。
  // analyserNode.fftSize / 2の要素がthis.freqsに格納される。今回の配列の要素数は1024。
  this.analyserNode.getByteFrequencyData(this.freqs);

  // 全ての波形データを描画するために、一つの波形データのwidthを算出する。
  var barWidth = cw / this.analyserNode.frequencyBinCount;

  ctx.fillStyle = 'rgba(0, 0, 0, 1)';
  ctx.fillRect(0, 0, cw, ch);

  // analyserNode.frequencyBinCountはanalyserNode.fftSize / 2の数値。よって今回は1024。
  for (var i = 0; i < this.analyserNode.frequencyBinCount; ++i) {
    var value = this.freqs[i]; // 配列には波形データ 0 ~ 255までの数値が格納されている。
    var percent = value / 255; // 255が最大値なので波形データの%が算出できる。
    var height = ch * percent; // %に基づく描画する高さを算出

    ctx.fillStyle = '#fff';
    ctx.fillRect(i * barWidth, ch, barWidth, -height);  // -をつけないと下に描画されてしまう。
  }

  window.requestAnimationFrame(this.draw.bind(this));
};

// requestAnimationFrameを多くのブラウザで利用するためにprefixの記載
var setUpRAF = function() {
  var requestAnimationFrame = window.requestAnimationFrame ||
                              window.mozRequestAnimationFrame ||
                              window.webkitRequestAnimationFrame ||
                              window.msRequestAnimationFrame;
  window.requestAnimationFrame = requestAnimationFrame;
};

setUpRAF();
var loader = new Loader('sample.mp3');
loader.loadBuffer();
```

# まとめ
波形データを取得して描画ができたら、あとは描画を調整して素敵なビジュアライザーを作るだけです。
データの特性上、時間領域より周波数領域の波形データを描画に利用することが多いと思います。
上記のデモは単に波形データを描画しているだけであり、ビジュアライザーっぽくないので、
描画する周波数領域を狭めて描画したりと調整が必要です。
特定の範囲の周波数領域の描画をしたい場合、以下をご参考にしてください。


- [Web Audio APIを利用してオーディオビジュアライザを作成する ~getByteFrequencyDataで取得する配列のどの要素がどの周波数(Hz)の波形データを格納しているか確認する~](http://qiita.com/soarflat/items/004cac51b818b9483764)

# 関連

- [Web Audio APIを利用してオーディオビジュアライザを作成する ~その1 音声データを読み込んで音を出す~](http://qiita.com/soarflat/items/b60649195c16c7a43868)