虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

Tone.jsとp5.jsで音の発生を可視化する

こんにちは、年末年始に新しくWindows10の自作PCを組んだ虎の穴ラボのおっくんです。

昨年から、Web audioを取り扱うライブラリとしてTone.jsを使って、
ブラウザで音を鳴らしたりということをしていたのですが、一歩進んで再生する音を可視化したいと思いました。
可視化の方法としては、発生させる音を文字で表示するということもできるのですが、
今回は音楽再生ソフトの視覚エフェクトをイメージして作成することにしました。

Web audioを扱うTone.jsとCanvasを扱うp5.jsの2つライブラリを使用して、円環状に配置した音程に対応したバーが伸び縮みするアニメーションをCanvasに表示させます。
完成したページは以下のようになります。

f:id:toranoana-lab:20200113174640p:plain
音楽再生アプリの画面

完成品はこちらアップロードしておりますので、ぜひ見てみてください。

開発環境

  • OS MacOS Mojave バージョン 10.14.6
  • Node.js v12.13.1(ndenvで導入)
  • ブラウザ Chrome 79.0.3945.88

使用ライブラリ

  • Tone.js tonejs.github.io 音の再生には、Tone.jsを使用しています。 Tone.jsはWeb audioを扱うフレームワークです。 一番シンプルな実装では、音程と長さを引数に関数を実行することで1音を鳴らすことができます。 スケジューリング機能も持っており、楽譜を書くようにデータを用意することで楽曲の再生も可能です。

  • p5.js p5js.org Canvasへの描画には、p5.jsを使用しています。 p5.jsはProcessingをJavaScriptに移植したライブラリです。 DOM上でCanvas要素を作成し、その中に点や線・画像を表示してグラフィカルな表現をすることができます。 Canvas要素上でのクリックイベントなども、ライブラリが提供するAPIから扱うことができます。

リポジトリ

今回の制作物は、株式会社 虎の穴 開発室 のリポジトリにアップしています。
ご興味がありましたら、ダウンロードしてお手元でも動かしてみてください。

github.com

参考

  • 画面上でバーを個別の色で円環状に並べるために、色相について調べました。 www.peko-step.com www.petitmonte.com

  • 今回再生した「無伴奏チェロ組曲第1番ト長調 」の楽譜はこちらを参照しました。 www.mukkoo0701music.jp

  • 筆者は楽譜が読めないので、こちらのサイトを参考に楽譜を見比べながら1音ずつ拾いました。 www.print-gakufu.com

Tone.js とp5.js それぞれ単独で使う場合のサンプル

具体的なTone.jsとp5.jsの組み合わせについて示す前に、それぞれ単独でのサンプルを示します。

Tone.jsのサンプル

Tone.jsを利用した連続した音の再生のサンプルは以下の様になります。

const Tone = require("tone");

const score = [
  { note: "C4", dur: "4n" },
  { note: "B4", dur: "8n" },
  { note: "D4", dur: "2n" }
];

//音の種類を設定
const syn = new Tone.PolySynth().toMaster();

// メロディをシーケンス制御の内容を定義
const melody = new Tone.Sequence((time, { note, dur }) => {
  // 音を鳴らす。
  syn.triggerAttackRelease(note, dur, time, 0.3);
}, score).start();

// ループを回数設定
melody.loop = 0; 

const soundstart = () => {
  Tone.Transport.start();
  //2秒たったなら、再生をリセット
  setTimeout(() => {
    Tone.Transport.stop();
  }, 2000);
};

document.getElementById("start").onclick = soundstart;

こちらのサンプルでは読み込まれたhtmlにて、id="start"の要素をクリックすることで音を発生します。 scoreに定義した音程(C4やD4)と長さ(4nや2n)を与えることで、1小節の中での音の長さを定義します。 定義に基づいてTone.Sequenceの第一引数に渡したコールバック関数が呼び出されます。 このときの呼び出されるタイミングは、与えている音の長さの定義に従うので一定ではありません。

p5.jsのサンプル

p5.jsを利用したCanvasへの描画のサンプルは以下の様になります。

import * as p5 from "p5";

let sketch = p => {
  //Canvasのサイズ設定
  const w = 400;
  const h = 400;

  //初期設定
  p.setup = () => {
    // キャンバス作成
    p.createCanvas(w, h);
    //背景を設定
    p.background("#111");
  };

  //更新処理
  p.draw = () => {
    p.fill("#F00");
    p.rect(10, 10, 50, 50);
  };
};

//p5をsketchを基に実体化
new p5(sketch);

こちらを実行すると、高さと幅が400px黒い背景に、高さと幅が50pxの赤い四角形が描画されます。
p5.jsはsetup関数に定義された内容によって初期設定を行い、draw関数によって表示を更新します。
描画する四角形の座標を更新するといった処理を記述する場合はdraw関数に定義します。
フレームレートを定義しなければ、60fpsで呼び出しされるように動作します。

ディレクトリ構成

今回の制作したソースコードのディレクトリ構成は以下のようになります。

├── node_modules
├── package-lock.json
├── package.json
├── public
│   ├── css
│   └── index.html       #canvasを設置するid=canvas与えたdiv要素を記述しておきます。
├── src
│   ├── index.js             #エントリポイントと主な処理を記述
│   ├── player_icon.js   #画面中央の再生ボタンアイコンを実装
│   ├── score.js            #鳴らす音を定義
│   ├── tools.js             #音の範囲から表示するバーの本数を定義するなど各種関数を定義
│   └── volume_level.js #表示するバーのを実装
└── webpack.config.js

Tone.js とp5.js の組み合わせ

ここからは、具体的なTone.jsとp5.jsの組み合わせ方について取り扱います。

ポイント1:Tone.jsとp5.jsの同期

前述のサンプルで書いた通り、Tone.jsの音の発生のイベントは鳴らすデータの定義次第で変化し、 p5.jsの描画更新は概ね安定しています。
音の発生に描画を合わせるためには、それぞれの同期を取る必要があります。

音の発生に連動したコールバック関数呼び出しに対して、描画の処理のほうが周期が早いので、
Tone.jsのコールバック関数内でパラメータを設定し、p5.jsのdraw関数内でパラメータをチェックすることで同期を試みます。

以下のように実装をしました。
1 . Tone.jsとp5.jsで同期を取るパラメータをnullに初期設定。

let target = null;

2 . Tone.jsでの音の発生でのコールバック関数で、同期を取るパラメータ変数targetに値を設定

let melody = new Tone.Sequence((time, { note, dur }) => {
  // 音を鳴らす。
  syn.triggerAttackRelease(note, dur, time, 0.3);

  //鳴らした音に基づいて描画用のパラメータを与える。
  if (note != null) {
    //鳴らした音に基づいて何番目のバーを主に動かすのかパラメータtargetを設定する
    target = note2number(note, min);
  }

  //省略

}, score).start();

3 . p5.jsのdraw関数での更新処理時にパラメータtargetをチェックし描画処理の後、nullにリセットする。

  p.draw = function() {
    //省略

    let addvols = null;
    //targetの値がnullでないとき、各バーの変位量を設定
    if (target != null) {
      addvols = calcvols(target, hon, 170);
    }
    //各バーの表示を更新
    vols.forEach((vol, index) => {
      // update関数により、各バーのオブジェクトのパラメータを更新
      // パラメータを与えない場合も減衰させる
      if (target != null) {
        vol.update(addvols[index]);
      } else {
        vol.update();
      }
      // 各バーの描画を行う。
      vol.display();
    });

    //パラメータをリセット
    target = null;

    //省略

  };

以上で、Tone.jsによる音の発生に同期してp5.jsでの描画を行うことができます。

ポイント2:描画更新パラメータの円環処理

今回は発生させた音に基づいて、円環状に並べたバーが伸び縮みします。
各バーの伸びのパラメータは配列一次元の配列で管理し、
発生させた音に基づくバー以外も、一定のルールで動作させたいと考えました。
この処理をポイント1のソースコードに記載のあるcalcvols関数で行います。
関数の実装は以下のように行いました。

// target :鳴らした音と対応するバーの番号
// length:バーの総本数
// param:鳴らした音に対応するバーの伸びの値
export const calcvols = (target, length, param) => {
  let arr = [];
  arr[0] = param;

  for (let i = 0; i < length / 2 - 1; i++) {
    //arr[0]を基準に、離れるにつれて値が減衰させる
    let value = param / (i + 2);
    arr.push(value);
    arr.unshift(value);
  }

  // 配列末尾を抜き出し配列先頭に入れることによって、最初に設定したarr[0]の位置を変更する
  for (let i = 0; i < target; i++) {
    let buf = arr.pop();
    arr.unshift(buf);
  }

  return arr;
};

表示するバーが円環になっているので配列の0番目と末尾も連続した関係であり、視覚的に隣り合う関係になります。
途中挫折しながらでしたが最終的に、配列の末尾を配列の先頭に入れ直す処理とすることで対応しました。

同じものを複数描画させたいときの対応。

同じルールで伸び縮みするバーを動作させます。 同一の動きをする場合には、切り出してclassにしておきます。 volume_level.jsにVolumeLevelクラスとして分離したので以下に示します。

export default class VolumeLevel {
  constructor(p, x, y, t, c) {
    this.p = p;
    this.position = p.createVector(x, y);
    this.theta = t;
    this.color = c;
    this.height = 100;
    this.width = 10;
  }
  update(vol) {
    if (vol != null) {
      this.height = this.height + vol > 155 ? 155 : this.height + vol;
    }
    this.height -= 3;
    this.height = this.height < 9 ? 9 : this.height;
  }
  display() {
    this.p.push();
    this.p.noStroke();
    this.p.translate(this.position.x, this.position.y);
    this.p.rotate(this.theta);
    this.p.fill(this.color);
    this.p.beginShape();
    if (this.height > 30) {
      this.p.vertex(0 - this.width / 2, 0);
      this.p.vertex(0 - this.width / 2, this.height);
      this.p.vertex(this.width / 2, this.height);
      this.p.vertex(this.width / 2, 0);
    } else {
      this.p.vertex(0 - this.width / 2 - 5, 0);
      this.p.vertex(0 - this.width / 2 - 5, this.height);
      this.p.vertex(this.width / 2 + 5, this.height);
      this.p.vertex(this.width / 2 + 5, 0);
    }
    this.p.endShape();
    this.p.pop();
  }
}

複数の同じルールで動作するもの以外に、 単独でも複雑な動きをさせるものは、クラスに分離させた方が見通しが良くなります。
そのため、画面中央に表示するアイコンは一つしか表示しませんが、player_icon.jsにPlayerIconクラスとして分離しています。

実行

コンソールでnpm run devを実行するか、npm run buildしてビルドしたファイルを任意のサーバーに置くことで実行できます。

最後に

Tone.jsとp5.jsを使って、再生する音を可視化してみました。
筆者に音楽の素養はあまり無いのですが、Tone.jsを使うと割合容易に音を鳴らすことができます。
Tone.jsでは鳴らす音を加工するエフェクターなども用意されているのですが、使いこなせないので筆者は触っていません。
音楽経験が有る方だと、より理解して様々なアイデアを実現できる題材ではないかと思います。

p5.jsは、公式が用意しているサンプルも豊富ですしデジタルアートを始めるのに最適だと思っています。
CDNからp5.jsを読み込めば、エディタとブラウザだけでインストールフリーで始めることができます。

それでは、楽しいTone.js+p5.jsライフを!

P.S.

虎の穴では一緒に働いて、一緒に同人誌を書く仲間を絶賛募集中です!
興味のある方は是非採用サイトを御覧ください! yumenosora.co.jp