音に合わせて波形が動くビジュアライザーをブラウザ上で作ってみる【p5.js】

この記事は[DeNA 21 新卒 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/dena-21-shinsotsu)の19日目の記事です。

# はじめに

YouTubeにあるBGM系の動画で、音楽に合わせてかっこよく波形が動くビジュアルエフェクトを見かけたことはありませんか？

その多くは動画編集ソフトで作成されているものと思います。
しかし僕は、もっと手軽に作ることはできないものかと考えました。

自分の好きな曲に合わせて動くビジュアライザーが欲しい！
今回はそんな思いで、マイクに入力された音に合わせてブラウザ上で動作するオーディオビジュアライザーを作ってみました。

今回作ったもの（GIF画像）
![spectrum.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269486/9e5291f7-0f10-52c9-6b2c-63358fce41bc.gif)

# [準備] p5.jsの導入

今回は[p5.js](https://p5js.org/)というOSSのJavaScriptライブラリを使用して実装しました。
p5.jsは、図形の描画が得意なProcessingというプログラミング言語のJavaScript版ライブラリです。
[こちら](https://cdnjs.com/libraries/p5.js)からCDNのURLをコピーして、scriptタグで読み込めば導入は完了です。
今回はサウンドの処理も扱うので、アドオンの`p5.sound.js`も読み込みます。

```index.html
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.min.js" integrity="sha512-WIklPM6qPCIp6d3fSSr90j+1unQHUOoWDS4sdTiR8gxUTnyZ8S2Mr8e10sKKJ/bhJgpAa/qG068RDkg6fIlNFA==" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/addons/p5.sound.min.js" integrity="sha512-wM+t5MzLiNHl2fwT5rWSXr2JMeymTtixiw2lWyVk1JK/jDM4RBSFoH4J8LjucwlDdY6Mu84Kj0gPXp7rLGaDyA==" crossorigin="anonymous"></script>
```

詳しいチュートリアルは[公式のGet Started](https://p5js.org/get-started/)を参照してください。

# シンプルなオーディオスペクトラムを作る

初めに、マイクに入力された音に反応するスペクトラムを実装してみます。

![spectrum.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269486/2a7c1320-31a1-8ca4-584c-2a75dec408c2.png)

p5.jsが読み込まれると、初めに`setup()`が実行されます。
setup関数の中に、初期設定について書いていきます。

```js
let mic, fft;

// 最初に実行される
function setup() {
  // 描画範囲を画面サイズいっぱいに設定する
  let cnv = createCanvas(windowWidth, windowHeight);

  // 画面がクリックされたらAudioの取り扱いを開始する。
  cnv.mouseClicked(userStartAudio);

  // マイクを利用するための初期設定
  mic = new p5.AudioIn();
  mic.start();

  // fftを利用するための初期設定
  fft = new p5.FFT(0.9, 256);
  fft.setInput(mic);

  // 図形の塗りつぶしを無効にする設定
  noFill();
  // 図形の線の色を255(白)に設定
  stroke(255);
}
```

最初に`setup()`が実行されたあとは、`draw()`が繰り返し実行されます。
アニメーションのフレームが更新されていくようなイメージです。
draw関数の中に、描画の処理を書いていきます。

```js
function draw() {
  // 背景を0(黒)で塗りつぶす
  background(0);
  // スペクトラムを描画する関数を呼び出す
  showSpectrum(fft);
}

function showSpectrum(fft) {
  const spectrum = fft.analyze();

  beginShape();
  for (let i = 0; i < spectrum.length; i++) {
    // map関数でiを0からspectrum.lengthの範囲から、0からwidthの範囲に置き換える
    // x = i / (spectrum.length - 0) * (width - 0) と同義
    // 詳細：https://p5js.org/reference/#/p5/map
    const x = map(i, 0, spectrum.length, 0, width);

    const amp = spectrum[i];
    const y = map(amp, 0, 255, height / 2, 0);

    // x, yの位置に頂点を打つ
    vertex(x, y);
  }
  // beginShape()からendShape()の間で打たれた頂点は線で結ばれる
  endShape();
}
```

**[実行例(CodePenリンク)](https://codepen.io/izm51/pen/VwKpOzz)**
※ブラウザからマイクの利用許可を求めるメッセージが表示されたら「許可」を押してください。
黒い画面をクリックするとスペクトラムが動作開始します。

# スペクトラムを円形にしてみる

![circle_spectrum.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269486/ed6c162c-0491-7dcc-d5fe-93c678ef592e.png)

`showSpectrum`関数を書き換えて、スペクトラムを円形に描画してみます。

```js
// setup()は変更なし

function draw() {
  background(0);
  // 座標(x, y)=(0, 0)の位置を画面中央に設定
  translate(width / 2, height / 2);
  showSpectrum(fft);
}

function showSpectrum(fft) {
  const spectrum = fft.analyze();
  
  beginShape();
  for (let i = 0; i < spectrum.length; i++) {
    const angle = map(i, 0, spectrum.length, radians(0), radians(360));
    const amp = spectrum[i];
    const r = map(amp, 0, 255, 50, 200);
    const x = r * cos(angle);
    const y = r * sin(angle);
    vertex(x, y);
  }
  endShape();
}
```

**[実行例(CodePenリンク)](https://codepen.io/izm51/pen/LYRyEVv)**
※ブラウザからマイクの利用許可を求めるメッセージが表示されたら「許可」を押してください。
黒い画面をクリックするとスペクトラムが動作開始します。

# スペクトラムを反転させてくっつけてみる

![mirror_circle_spectrum.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269486/dd09260f-df48-5390-54d5-3f0fb0d549d1.png)

直線状のスペクトラムを円形に丸めただけだと、開始地点と終了地点が繋がらず分断されてしまいました。
無理やり線でつなぐこともできますが、今回は180度分の円弧2つを左右反転させてくっつけてみることにします。

```js
// setup()は変更なし

function draw() {
  background(0);
  translate(width / 2, height / 2);
  // スペクトルの開始角と終了角を引数として渡す
  showSpectrum(fft, -90, 90);
  showSpectrum(fft, 270, 90);
}

function showSpectrum(fft, startAngle, endAngle) {
  const spectrum = fft.analyze();
  
  beginShape();
  for (let i = 0; i < spectrum.length; i++) {
    const angle = map(i, 0, spectrum.length, radians(startAngle), radians(endAngle));
    const amp = spectrum[i];
    const r = map(amp, 0, 255, 50, 200);
    const x = r * cos(angle);
    const y = r * sin(angle);
    vertex(x, y);
  }
  endShape();
}
```

**[実行例(CodePenリンク)](https://codepen.io/izm51/pen/JjRNoNq)**
※ブラウザからマイクの利用許可を求めるメッセージが表示されたら「許可」を押してください。
黒い画面をクリックするとスペクトラムが動作開始します。

# ビートに合わせてスペクトラムをバウンスさせてみる

![bounce_mirror_circle_spectrum.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/269486/26ef585b-e370-e7dd-c3cd-71c0bab2541c.png)

曲の盛り上がりに合わせて、スペクトルの円の半径を大きくしたり小さくしたりしてみます。
静止画像ですとイメージが伝わりづらいですが、このエフェクトによって臨場感が増すと思います。

ビートの検出にはp5.jsの`peakDetect`を利用します。（[p5.PeakDetect](https://p5js.org/reference/#/p5.PeakDetect)）

```js
let mic, fft, peakDetect;
let minRadius = 50;
let radius = minRadius;

function setup() {
  let cnv = createCanvas(windowWidth, windowHeight);
  cnv.mouseClicked(userStartAudio);

  mic = new p5.AudioIn();
  mic.start();

  fft = new p5.FFT(0.9, 256);
  fft.setInput(mic);
  
  peakDetect = new p5.PeakDetect(20, 2000, 0.5, 10);

  noFill();
  stroke(255);
}

function draw() {
  background(0);
  translate(width / 2, height / 2);
  
  peakDetect.update(fft);
  
  if ( peakDetect.isDetected ) {
    // ビートが検知されている場合、半径を90に設定
    radius = 90;
  } else {
    // ビートが検知されていない場合、半径を毎フレーム1ずつ小さくする
    radius -= 1;
    // minRadiusよりは小さくしない
    radius = (radius > minRadius) ?  radius : minRadius;
  }

  showSpectrum(fft, radius, radius+60, -90, 90);
  showSpectrum(fft, radius, radius+60, 270, 90);
}

function showSpectrum(fft, minR, maxR, startAngle, endAngle) {
  const spectrum = fft.analyze();
  
  beginShape();
  for (let i = 0; i < spectrum.length; i++) {
    const angle = map(i, 0, spectrum.length, radians(startAngle), radians(endAngle));
    const amp = spectrum[i];
    const r = map(amp, 0, 255, minR, maxR);
    const x = r * cos(angle);
    const y = r * sin(angle);
    vertex(x, y);
  }
  endShape();
}
```

# 今回の完成品

**[実行例(CodePenリンク)](https://codepen.io/izm51/pen/dypWPKo)**
※ブラウザからマイクの利用許可を求めるメッセージが表示されたら「許可」を押してください。
黒い画面をクリックするとスペクトラムが動作開始します。

# 終わりに

本記事ではp5.jsを使って、ブラウザでリアルタイムに動作するオーディオビジュアライザーを作ってみました。
今回はシンプルな実装にとどまりましたが、かっこいい背景画像を設定したり、カラフルなエフェクトを追加したりで自分好みのビジュアライザーを作成できそうです。
もし何かの参考にしていただけたら嬉しい限りです。

この記事を読んで「面白かった」「学びがあった」と思っていただけた方、よろしければ LGTM、Twitter や Facebook、はてなブックマークにてコメントをお願いします！

また [DeNA 公式 Twitter アカウント @DeNAxTech](https://twitter.com/DeNAxTech) では、 Blog 記事だけでなく色々な勉強会での登壇資料も発信しています。ぜひフォローして下さい！
<a href="https://twitter.com/DeNAxTech?ref_src=twsrc%5Etfw" class="twitter-follow-button" data-size="large" data-lang="ja" data-show-count="false">Follow @DeNAxTech</a><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>