Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
66
Help us understand the problem. What is going on with this article?
@soarflat

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

More than 3 years have passed since last update.

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

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

再生中の音から波形データを取得して描画する

wave.jpg

デモ(ページを開くと音が流れるので音量注意)

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

時間領域の波形データ

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

周波数領域の波形データ

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

波形データの取得方法

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

AnalyserNode

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

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

デモ(ページを開くと音が流れるので音量注意)

ディレクトリ構成

.
├── index.html
├── audio.js
└── sample.mp3

index.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を生成し、AudioBufferSourceNodeAudioDestinationNodeに接続する。
  • 再生とrequestAnimationFrameでの描画処理を開始する。
  • 再描画する前に再生中の波形データを取得して再描画する。
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で取得した波形データを描画する。

デモ(ページを開くと音が流れるので音量注意)
getByteTimeDomainDataで取得した波形データを描画するための記述とほとんど変わらないです。

ディレクトリ構成

.
├── index.html
├── audio.js
└── sample.mp3

index.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を生成し、AudioBufferSourceNodeAudioDestinationNodeに接続する。
  • 再生とrequestAnimationFrameでの描画処理を開始する。
  • 再描画する前に再生中の波形データを取得して再描画する。
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();

まとめ

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

関連

66
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
soarflat
フロントエンドエンジニア。Udemy で webpack の講座を公開しています。https://www.udemy.com/course/practical-webpack/?couponCode=E09894A14502565499BA

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
66
Help us understand the problem. What is going on with this article?