(この文章は作成中ですが、自分のメモ用に公開しています)
SoundCanvas for iOSがリリースされたことで、昔作成したSMFデータを再び聞くことが出来るようになったのですが、音楽を聞くのにiTunesでファイルを転送するのはちょっと面倒かも…と言うことで、Web MIDI APIを使用した簡易MIDI再生環境を作成してみました。
ソースコードはGitHubで公開しています。
SMF & RCPプレイヤー
https://mizunagikb.github.io/miz_music/
まずは環境を用意
まずはWeb MIDI APIを実際に利用できる環境を構築する必要があります。
必要なものは以下の三つです。
- Web MIDI APIの実行環境。
- MIDI楽器。
- (接続方法によっては)接続ケーブル。
Web MIDI APIの実行環境
Web MIDI APIが利用可能なのはいまのところGoogle Chromeのみとなります。(2015年8月現在)もしかするとプラグイン等を導入する事で再生可能な環境があるかもしれませんが、自分は素直にGoogle Chromeを使用しました。
MIDI楽器
MIDI楽器はMIDI信号を受信出来るものであれば何でも良いのですが、今回はSoundCanvas for iOSを使用しました。
接続ケーブル
環境によっては、接続ケーブルが必要になります。参考までに自分が試した組み合わせを記載しておきます。(iMacとiPhone6を使用しています。)
接続方法(iMac側) | 接続方法(iPhone6側) |
---|---|
iRig MIDI2 | Lightning to USB Camera Adapter + YAMAHA UX16 |
YAMAHA UX16 | iRig MIDI2 |
Network | AirPlay |
AirPlayの接続が最も簡単なのですが、無線LAN経由の場合は安定した送信が行えない場合があります。
AirPlayについて補足
MacはAudio MIDI設定にあるネットワークを使用することで、無線LAN経由でMIDI信号を送受信出来ます。(SoundCanvas for iOS側はMENUからAir Play / Bluetoothを有効にします。)
演奏データ構造の決定
SMFを再生するだけであれば、SMFを解析しながらでもなんとかなってしまうのですが、なるべく演奏しやすくするために、Web MIDI APIの仕様で扱い易い形態を取ることにします。
Web MIDI APIの仕様によると、send
メソッドは一つ以上の有効なMIDIメッセージを含むことが出来るとあります。
MIDIメッセージというのは、MIDI規格に沿って演奏情報をやりとりするための最小単位でwww.midi.orgに仕様が掲載されています。
MIDI Messages
http://www.midi.org/techspecs/midimessages.php
MIDIメッセージを分類すると以下の様になります。
メッセージ種別 | メタイベント | . |
---|---|---|
2バイトメッセージ | . | |
3バイトメッセージ | . | |
エクスクルーシブメッセージ | 0xF0, 0xF7 | |
可変長バイトメッセージ | テキストデータ | |
可変長バイトメッセージ | バイナリデータ | |
可変長バイトメッセージ | 0xFF 0x51 | テンポ |
可変長バイトメッセージ | 0xFF 0x2F | エンド |
これらの情報を時間情報(デルタタイム)と組み合わせたる事で音楽情報を記録します。
図で表現すると以下の様になります。
実際に数値を入れるとこんな感じです。
このデルタタイムは音符の長さではなく、後続コマンドを開始するまでの待ち時間であることに注意してください。
ベロシティ0のキーオン情報について
MIDIメッセージには、キーオンとキーオフが用意されているのですが、現行の楽器は大抵がベロシティ0のキーオンをキーオフとして処理します。自分は分けて処理していますが、演奏データを読み込む際にキーオフ情報をベロシティ0のキーオンに変換するとちょっとだけ処理を簡略化出来ます。
ステップ数0の取り扱いについて
ステップ数が0のデータは同一時間で処理するものとみなされます。
このようなデータの場合は、48ステップ待った後にドミソと和音が再生されることになります。
キーオンだけのMIDIデータについて
データの互換性が低くなるため、あまり見かけませんがSMFデータによってはキーオン情報のみしか含まれていない場合があります。(例えばドラム等)
ここはSMFの構造であるデルタタイム
+MIDIメッセージ
という構造に倣いつつ、send
メソッドですぐに使用可能な形式、つまり配列にしておくことにします。
ランニングステータスについて
SMFにはランニングステータスルールというものが存在します。もし後続するメッセージが0x80未満の場合は直前のメッセージを使用する、というルールです。
MIDIの仕様が作成されたのは1982年頃ですので、データサイズをなるべく小さくすることが求められていたのかもしれませんが、現在のPCでは重要度が低いと思われるため、事前に展開してしまうのが良さそうです。
まとめると、
- 時間とMIDIメッセージはセットで管理する。
- デルタタイムは展開しておく。
- ランニングステータスは展開しておく。
- sendに渡しやすいように、MIDIメッセージは配列として保持する。
といった感じでしょうか。
// MIDIメッセージ
export class CMIDIData
{
public m_nStep: number = 0;
public m_eMMsg: E_MIDI_MSG = 0;
public m_eMEvt: E_META_EVT = 0;
public m_aryValue: Array<number> = [];
public m_numValue: number = 0;
public m_strValue: string = "";
}
// トラック情報(MTrk相当)
export class CMIDITrack
{
public m_listData: Array<CMIDIData> = [];
}
// ソング情報(MThd相当)
export class CMIDIMusic
{
public m_nTimeDiv: number = 480;
public m_strTitle: string = "";
public m_listTrack: Array<CMIDITrack> = [];
}
演奏時のタイマーについて
SMFの時間情報は時分秒といった時間ではなく、4分音符の分解能を基準としたタイムベース、例えば4分音符を480としたら8分音符は240といった具合で表現されます。
なぜ素直に時間情報になっていないのかというと、速度が決定されるのはテンポが決定されるまでは実際の演奏速度を決定出来ないためであり、演奏中にテンポが切り替わる場合があるためです。
分解能が途中で切り替わることはないため、1ステップ(演奏時間の最小単位)が現在
の SMFテンポ で何秒に相当するなのかを計算すれば演奏に必要な時間情報を得ることが出来ます。
ちなみにSMFのテンポ情報は4分音符あたりのマイクロ秒として表現されますので、以下の様な式で計算します。
1ステップ当たりの演奏時間
= SMFテンポ
/ タイムベース
SMFテンポが500000で分解能が480の場合は、1ステップは1041マイクロ秒という事になります。ちなみにSMFテンポから音楽としてのテンポを求める場合は1分をSMFテンポで割るだけです。(1分間に四分音符がたたかれる回数なので)
テンポ
= 60 * 1000 * 1000
/ SMFテンポ
演奏処理はJavaScriptのタイマーイベントで行う事になりますが、JavaScriptのタイマーイベントの周期はあまり精度が良くないため、前回処理した時刻から現在の時刻を引き算する事で現在の演奏位置を求めることにします。
動作としては、タイマーが想定時間よりも早く来た場合は殆ど処理を行わず、遅れて来た場合は急いで演奏位置まで追いつくといった挙動になります。
この方法だと、演奏が遅くなったり早くなったりしてしまいそうですが、タイマー周期は安定しないが高い頻度で呼び出すことが出来る環境だとかなりうまく動作します。
欠点は、タイマーイベントの呼び出し間隔がかなり開いてしまうと、時刻に追いつくための処理が高負荷になる事です。回避方法としてはある程度以上の時間が経過してしまった場合は丸め込んでしまうといった方法が考えられます。
今回作成したプレイヤーでは、タイマー周期としては、10ミリ秒毎を設定しました。
再生可能なデータ形式の選定
SMFデータだけであればSoundCanvas for iOS単体で演奏出来る為、わざわざ自作する利点がちょっと乏しいです。それに手持ちのデータにSMF以外にもRCP形式のものがありますのでそちらにも対応させてみます。
RCP形式は標準仕様ではありませんが、Vectorで公開されているCVSという変換ツールに添付されているテキストに、作者の方が独自に解析された情報が記載されていますので、そちらを利用させて頂きます。
プログラムの分離について
単に再生するのが目的ではありますが、ソースコードをある程度管理しやすくするために、プログラムを機能毎に分離しておきます。
表示部はなくても良いのですが、楽曲演奏を行う際に演奏状況の管理が必要になりますので、そこを参照出来れば実装出来そうです。(ピアノロールを実装する場合は事前に楽曲データが必要になります)
プログラムについて
あとはひたすらプログラムをしていくだけですが、ひとつだけ問題があります。
MIDI制御を行う上で必要となるものにエクスクルーシブがあるのですが、この機能を有効にするには、requestMIDIAccess
メソッドの引数に{sysex: true}
が必要となります。
// SysExを有効にする場合
navigator.requestMIDIAccess(
{sysex: true}
).then(evt_midi_success, evt_midi_failure);
さらにhttps通信でなければなりません。({sysex: false}
にすれば、ローカル環境でも動作します。)
httpsの環境を自前で用意するにはちょっと面倒ですが、世の中httpsにしましょうという流れになっているようで、GitHubやMicrosoft Azureといった環境であれば、ファイルさえアップロードしてしまえば、https://と指定するだけでhttps通信が行えます。
特に、GitHubはリポジトリのブランチをウェブページとして公開出来る為、今回の様な用途だとかなり重宝します。