WebからMIDIファイルをサクッと扱えるライブラリ作って、Web×音楽してみた!


はじめに

PicoAudio.js という、 MIDIファイル(Standard MIDI File ... 以下SMF)をWeb上で再生するJSライブラリを開発しています!

  1. Web Audio API で提供されている基本の音源を利用して、楽器を再現しながら8bitサウンドで楽曲を再生できる
  2. SMFをパースしたデータを参照できる
  3. 再生中の楽曲に対して、発音や音の終了のイベントリスナを設定できる

のような特徴があります。
特に3を利用することで、
再生だけでなく、再生と連携したアプリの開発が簡単にできます!

今回は、そんなPicoAudio.jsの紹介記事です!!
サンプルだけでも見ていってください!!!

簡単な使い方

インストール

CDNとnpmでそれぞれ用意してあります!

<script src="https://unpkg.com/picoaudio/dist/browser/PicoAudio.js"></script>

グローバル変数に PicoAudio が定義されます。
または、

$ npm install picoaudio

こちらはPicoAudioを import して使います。

初期化〜曲の読み込み

// Standard MIDI Fileの準備
const file = /* FileReaderやFetchなどで取得 */
const smfData = new Uint8Array(file);

// PicoAudioの初期化
const picoAudio = new PicoAudio();
picoAudio.init();

// SMF形式のバイナリのパースを行う
const parsedData = picoAudio.parseSMF(smfData);

// パースしたデータをセット
picoAudio.setData(parsedData);

再生

// 再生
picoAudio.play();

その他のメソッド等は PicoAudio.js#readme に書いてます。

とっても簡単に再生できますね!

次は実際に音楽に連動して動くものを作ってみました!

サンプル

音楽に合わせて寿司が流れる

まずはシンプルな例を紹介します

メロディに合わせて寿司が流れるものです
(スクショ撮ったけど地味すぎて掲載から外しましたので、サンプルを覗いてみてください)

サンプル & ソースコード

肝の部分はここです!

picoAudio.addEventListener('noteOn', (note) => {
  // チャンネル0(メロディ)だけ見る
  if (note.channel === 0) {
    x += 5
    sushi.style.translate = `${x % 300}px`
  }
})

picoAudio.addEventListener('noteOn', /* Function */) で、
楽曲中の発音(note on)イベントを受け取ることができます。

サンプルでは noteOn をトリガーに、 sushi を右方向に移動させることで、
メロディのタイミングにあわせて寿司が流れていきます。

このサンプルでは使っていませんが、
受け取っている note オブジェクトには
例えば、音の高さや強さ(ベロシティ)、音の始まりから終わりまでの正確な時間 など、
様々な情報がはいっています。
これを利用することでより豊かな表現も可能です!

次はそれらも使って簡単なピアノロール※を作ってみます!

※ピアノロール ...
楽譜のように、音を並べて表示するものです。
今回実装したピアノロールは、
横方向を音の高さ(左が音が低く、右が音が高い)
縦方向を時系列(上が新しく、下が古い)

簡単なピアノロール

サンプル & ソースコード

一番紹介したい部分はここです!

picoAudio.addEventListener('noteOn', (note) => {
  const key = `${ note.channel }/${ note.pitch }`
  playingNotes.set(key, note.velocity)
})

全体のソースには描画部分のロジックが入っており、サンプルとしては若干見づらくなってしまっているのですが、
(そしてkeyに文字列でデータを仕込んでいて最悪ですが😅)

先ほど紹介した、 noteOn イベントで受け取れるデータ(note)から、
note.channel, note.pitch, note.velocity のプロパティを参照しています。
これは、「MIDIチャンネル(どの楽器か、に近い)」「音の高さ」 「発音の強さ」をそれぞれ表しています。

このサンプルでは、
playingNotes という Map で、
「チャンネル+音の高さ」をkey、「いま鳴っているか(どの強さで鳴っているか)」をvalueとし、
発音のタイミングと音の終わりのタイミングで管理します。

// 再生中の音を管理する(今回は `チャンネル名/音の高さ` というkeyにしてしまう)
const playingNotes = new Map()

// PicoAudioのイベントリスナ 鳴ってる音をMapに入れる
picoAudio.addEventListener('noteOn', (note) => {
  const key = `${ note.channel }/${ note.pitch }`
  playingNotes.set(key, note.velocity)
})
picoAudio.addEventListener('noteOff', (note) => {
  const key = `${ note.channel }/${ note.pitch }`
  playingNotes.set(key, 0)
})

そして、
いまどの音が鳴っているかがリアルタイムで書き換えられている playingNotes を参照して、
CanvasrequestAnimationFrame を用いて描画しています。

描画の際、先ほど紹介した、
note.pitch で横方向の位置を決め、
note.velocity で描画の透明度を決めています。

// canvasの更新
const step = () => {  
  /* 略 */

  // 鳴ってる音を上部に描画する
  playingNotes.forEach((value, key) => {
    if (value) {
      // key から channel と pitch の情報を分離する
      const [channel, pitch] = key.split('/')

      // 低音域はあまり使用しないので100px分シフトする
      const x = NoteWidth * pitch - 100

      // 鳴ってる音の描画
      context.globalAlpha = Math.min(value * 1.2, 1)
      context.fillStyle = ChannelColors[channel]
      context.fillRect(x, 0, NoteWidth, ScrollSize)
    }
  })

  window.requestAnimationFrame(step)
}

window.requestAnimationFrame(step)

おわりに

いかがでしたか?

サンプルのなんとも言えない感じで若干伝えられなかった感もありますが、
MIDIファイルの再生と連携して、Webで何かができる!まあまあ簡単に!
…ということは伝わったのではないでしょうか!

頑張って使うと Picotune のようなサイトを作ることも可能です!

PicoAudio.js の詳しい使い方、
note って、他にはどんなデータを参照できるの?
曲の情報をもっと詳しく見れないの?などは、
PicoAudio.jsのReadme に書いてありますので、気になった方は見てみてください!

よければGitHubのスターもつけていってください!

最後までお付き合いいただきありがとうございました😊