🎹

Web MIDI APIを扱うためのMIDI基礎知識

2021/07/04に公開

@armorik83です。WebAudio Web MIDI API Advent Calendar 2016の18日目は、Web MIDI APIを扱う上での基礎知識となるMIDIについて解説します。

MIDI

MIDIは1981年に策定され一度も改訂されることなく現代まで続いている長寿規格です。DTM(デスクトップミュージック、打ち込み)だけでなくカラオケ店でも日常的に使用されており、(音楽を扱う)舞台で使用されたことをきっかけに舞台照明の操作などに扱われる規格にもなっています。そして、現代ではWeb MIDI APIを通じてブラウザで扱うこともできるようになりました。

照明機器の操作にも使われているということは、音楽のためと思わなくてもよいのです。実際に筆者は、MIDIキーボードをペンタブの側に置いてショートカットの代わりに使っている、というイラストレーターの話を聞いたことがあります。Web MIDI APIを用いて、音楽アプリに限らずとも新しい入力装置を使ったアプリが生まれるかもしれませんね。本稿がその一助になれば幸いです。

MIDIとは

MIDIはMusical Instrument Digital Interfaceの頭文字から取って名付けられたもので、電子楽器の演奏データを機器間でデジタル転送するための世界共通規格を指します。

古いウェブサイトだと、開くと同時に音楽が鳴り出し「MIDIが鳴り出した」という表現をしているものも見受けられますがこれは誤解で、MIDI規格に則って作成されたMIDIデータ(正確にはStandard MIDI File)の再生が始まったというのが正しく、MIDIという語自体に音楽データ、楽譜データというニュアンスは含まれないことに注意してください。

MIDIメッセージとは

MIDI規格上で通信される情報をMIDIメッセージと呼びます。MIDIメッセージは全てバイト列で実現され、書き表す場合には16進数を用います。MIDIメッセージの送受信が可能な機器を、俗にMIDI機器と呼ぶことがあります。

Web MIDI APIに触れる

requestMIDIAccess

すでに探せば解説記事は複数出てきますが、本稿でも述べておきます。まずJavaScript上でMIDI機器を扱うためにMIDIAccessを得る必要があります。MIDIAccessは接続されているMIDI機器を返します。

window.navigator.requestMIDIAccess()

そのためにrequestMIDIAccess()を呼びます。requestMIDIAccess()はPromiseを返すので、そのthen()MIDIAccessが得られます。

window.navigator.requestMIDIAccess().then((midi) => {
  //
})

MIDIAccess

MIDIAccessinputsoutputsというプロパティを持ちます。ここから接続されているMIDI機器のMapを取得します。

window.navigator.requestMIDIAccess().then((midi) => {
  midi.inputs .forEach((input)  => console.log(input))
  midi.outputs.forEach((output) => console.log(output))
})

これで入出力ともに接続可能な機器がログに書き出されます。

MIDIOutput#send()

MIDIメッセージを送信するためには、MIDIOutputに対してsend()を呼びます。まず、MIDIOutputを得ましょう。

window.navigator.requestMIDIAccess().then((midi) => {
  const output = Array.from(midi.outputs).map((output) => output[1])[0]
})

最初のMIDIOutputの取得を1行で書くとこのようになります。Array.from(midi.outputs).map()の辺りは好みなので、for of文でもかまいません。const outputにはMIDIOutputインスタンスが格納されます。

次に、このMIDIOutputに対してsend()を呼びます。このとき、send()の引数に注意しなければなりません。ここでMIDIメッセージに対する基礎知識が活かされます。

MIDIOutput#send()の引数

send()の第一引数datasequence<octet>を受取ります。バイト列とほぼ同義とみてよいです。長大なMIDIメッセージの送信などのためにUint8Arrayの使用が許可されていますが、利便性のためにlengthが3のArrayの使用も可能です。

output.send(new Uint8Array([0x90, 0x45, 0x7f]))

すなわち上記は次のように書くことが許容されます。

output.send([0x90, 0x45, 0x7f])

また、下記のように書くこともできます。

output.send([0x90, 69, 127])

いまここで、index 0のみ16進数で表記しindex 1および2については10進数で表記しましたが、これには理由があります。その理由を次の節にて解説します。

MIDIメッセージ

2種類のバイト

先ほど、MIDIメッセージは全てバイト列で実現されると述べました。このバイト列の仕様について、もう少し詳しく解説します。

MIDIメッセージに使用されるバイトは、ステータスバイトデータバイトの2種類に分けられます。ステータスバイトとは0x80から0xffまでの128個のバイトを指し、データバイトとは0x00から0x7fまでの128個のバイトを指します。

MIDIメッセージは複数のバイトによって連なるバイト列によって表されますが、すべてのMIDIメッセージはステータスバイトを先頭に、その後ろにデータバイトが連なるように策定されています。ステータスバイトによって、その後ろのデータバイト長は確定し、すべて固定長です。(ただしSystem Exclusive Messagesを除く、本稿では割愛)

前節の例をもう一度取り上げましょう。

output.send([0x90, 0x45, 0x7f])

このうち0x90はステータスバイト、0x45, 0x7fはデータバイトです。データバイトは0から127の128段階の値とおおむね見なしてよいので、これらを次のように10進数に書き直すと便利なのです。

output.send([0x90, 69, 127])

では10進数に書き直さなかったステータスバイトとはなんでしょうか。

ステータスバイト

ステータスバイトは0x80から0xffまでの128種類存在すると述べました。これらには1つずつ意味が定義されています(欠番も存在します)。

ステータスバイトはいくつかの種類に分けられます。システムメッセージチャンネルボイスメッセージチャンネルモードメッセージという大きく3種類に分けることができます(仕様書上の分類を解説用に簡略化しています)

0xfn システムメッセージ

0xf0から0xffのバイトはシステムメッセージを扱います。高等な機能を扱えますが、本稿では紹介しません。

0x8nから0xen チャンネルボイスメッセージ

0x8nから0xenはチャンネルボイスメッセージを表します(例外あり、後述)。nは任意の数で0からfを取りうると解釈してください。

上位4ビットがチャンネルボイスメッセージの種類を表し、下位4ビット(nの部分)はチャンネル番号を表します。例を挙げましょう。

0x9nは全てノートオンメッセージです。ここで0x90であればチャンネル1に対するノートオン、0x91であればチャンネル2に対するノートオンです。これが16チャンネル分存在するので、0x9fはチャンネル16に対するノートオンとなります。

いくつか代表的なチャンネルボイスメッセージを紹介します。

0x8n ノートオフ
nに指定したチャンネル内の対象の音を止める。止める対象の音は後続のデータバイトにて指定する。データバイト長は2。

  • 第1データバイト
    • ノートナンバー
  • 第2データバイト
    • オフベロシティ

0x9n ノートオン
nに指定したチャンネル内の対象の音を鳴らす。鳴らす対象の音は後続のデータバイトにて指定する。データバイト長は2。

  • 第1データバイト
    • ノートナンバー
  • 第2データバイト
    • ベロシティ

0xen ピッチベンド
nに指定したチャンネルで鳴っている音のピッチを変える。MSB (Most Significant Byte) 128段階の1段階ずつをさらにLSB (Least Significant Byte) で128分割しているので、計16384段階の細かい指定ができる。データバイト長は2。

  • 第1データバイト
    • ピッチベンド値MSB
  • 第2データバイト
    • ピッチベンド値LSB

0xbn 0x78から0xbn 0x7f チャンネルモードメッセージ

チャンネルモードメッセージだけ特殊で、奇妙な仕様をしています。0xbnのあと第1データバイトが0x78から0x7fだったときに限り、それらはチャンネルモードメッセージとなります。0xbnのあとが0x00から0x77だった場合は前項でのチャンネルボイスメッセージとなり、コントロールチェンジとして扱われます。

チャンネルモードメッセージは歴史的経緯のような雰囲気を感じるため、あまり覚えなくてよいです。

ノートナンバーとベロシティ

最後にノートオン、ノートオフのデータバイトとして登場したノートナンバーとベロシティについて説明しましょう。

ノートナンバーは中央のドの音を60と定め、(MIDIメッセージとして表現できうる)一番低い音を0、一番高い音を127と定義している音程の番号です。なお、グランドピアノは88鍵盤が備わっており、これらの範囲は21から108となっているため、128段階の分け方でも十分賄えているということです。

ベロシティは強弱を表します。127が最も強く、1が最も弱いです。0の場合は無音、すなわちノートオフと同義とみなされます。強さについては主観的解釈が大きいですが、仕様書上ではmp(メゾピアノ)からmf(メゾフォルテ)の間の強さが64であると定義されています。

まとめ

これらの知識をもとに再度次の処理を読んでみましょう。

output.send([0x90, 69, 127])
  • 0x90
    • 0x9nはノートオン
    • 0x90はチャンネル1に対してのメッセージ
  • 69
    • ノートナンバー
    • 69は中央ドから少し上のラの音
  • 127
    • ベロシティ
    • 127は最も強く鳴らすということ

つまりこれは、outputに対して「チャンネル1はラの音を最も強く鳴らせ」という命令を.send()していると解釈できるのです。

ノートナンバーやベロシティは若干の音楽知識を求められるようにも見えますが、ノートナンバーは結局キーコードと同じようなもので、ベロシティは単なる強弱です。なので、ここまでで解説した話はほとんどがバイト列に関するものであるため、エンジニアにも親しみやすいですね。

デバッグに便利なアプリケーション

MIDIMonitorというアプリケーションをお勧めします(macOS用)。macOSには最初からIAC Driverという仮想MIDIドライバが備わっているため、MIDI機器をひとつも所持していなくてもWeb MIDI APIを楽しむことができます。


それでは。

Discussion