Skip to content

MIDI 2.0・UMP・クリップファイル ​

MIDI 2.0 のメッセージは、libsonare では Universal MIDI Packet(UMP)として通ります。UMP はパケットの運搬形式であり、MIDI 2.0 の意味論そのものではありません。メッセージタイプとステータスが MIDI の意味を定めます。MIDI 1.0 のチャンネルボイスを表す MT 0x2 は 1 ワード、MIDI 2.0 のチャンネルボイスを表す MT 0x4 は 2 ワードです。Project のイベントは PPQ 位置と最初の 2 UMP ワードを持つため、MIDI 1.0 と MIDI 2.0 のチャンネルボイスイベントを同じ setMidiEvents リストで扱えます。Project.midi* は MIDI 1.0 ワードを、Project.midi2* はフル解像度の MIDI 2.0 ワードを作ります。

プロトコルの定義は MIDI Association の Universal MIDI Packet・MIDI 2.0 Protocol 仕様を参照してください。以下の受信側とバインディングの制限は libsonare の実装についての説明です。

値の幅と受信側の挙動

MIDI 2.0 UMP には 16 ビットのノートベロシティ、32 ビットのコントローラ/プレッシャー、32 ビットのピッチベンド、ノート単位メッセージを入れられます。生 UMP と MIDI 2.0 クリップファイルの経路では、その値の幅を保ちます。ただし受信する楽器が対応する表現は一部に限られることがあります。下の受信側の表を参照してください。

UMP を通る MIDI 1.0 と MIDI 2.0
MIDI 1.0 / MT2MIDI 2.0 / MT4ProjectMidiEvent 列RealtimeEngine UMPSMF2CLIPSMF変換(損失の可能性)
UMP は 1 ワードの MIDI 1.0 または 2 ワードの MIDI 2.0 チャンネルボイスを運びます。プロジェクト列は受信側やファイル変換で制限される前に両方を保持できます。

MIDI 1.0 から MIDI 2.0 へ ​

UMP のチャンネルボイスワードは 4 ビットのグループと 4 ビットの MIDI チャンネルを持ちます。グループは 16 チャンネルずつの独立した組で、16 組あるので 1 本の UMP ストリームで 256 チャンネルを指定できます。普通の MIDI 1.0 の構成ではグループ 0 を使います(MIDI の基礎)。チャンネルボイスのメッセージタイプが値の配置を分けます。MT 0x2 は 1 ワードの MIDI 1.0 メッセージ、MT 0x4 は 2 ワードの MIDI 2.0 メッセージです。広い値のフィールドは 2 番目のワードに入ります。どちらの形式も 7 ビットのノート番号と同じグループ/チャンネル範囲を使います。

値または表現MIDI 1.0 チャンネルボイスMIDI 2.0 チャンネルボイス
ノートベロシティ7 ビット16 ビット
CC、チャンネルプレッシャー、ポリフォニックプレッシャー7 ビット32 ビット
ピッチベンド14 ビット32 ビット
ノート単位コントローラとノート単位ピッチベンドチャンネルボイスに相当する形式なしMIDI 2.0 固有のメッセージ形式
ノートオン属性相当するフィールドなしtype と任意の 16 ビット属性データ

MIDI 2.0 は値の精度を上げ、ノート単位表現を追加します。ただし、すべての受信側がすべてのメッセージを適用するわけではありません。別々のノート単位メッセージで、同じチャンネル上で同時に鳴る、ノート番号が異なる音符へ独立した変化を指定できますが、どの形式を実装するかは楽器側が決めます。

MIDI 1.0 でも CC メッセージを組み合わせれば 14 ビット値を表せます。表は単一の CC メッセージの値幅です。MIDI 2.0 には、機能を確認する MIDI-CI、コントローラの共通動作を定める Profiles、機器の情報を交換する Property Exchange もあります。これらは UMP の符号化とは別の仕組みです。ここで説明する libsonare の API はメッセージの作成・保存・送信を扱い、外部機器との機能交渉は行いません。

Project イベントの形 ​

ProjectMidiEvent は JavaScript では { ppq, data0, data1? }、Python では (ppq, data0, data1) です。data0 は最初の UMP ワード、data1 は 2 番目のワードです(省略時は 0)。Project のイベントリストが運ぶのはチャンネルボイス UMP です。SysEx ペイロードは別のストアにあり、このフラットな型には含まれません。setMidiEvents はリストを置き換え、そのストアを破棄します。

ppq は 4 分音符単位の位置であり、480 ticks/4 分音符の整数ではありません。MT 0x2 の MIDI 1.0 チャンネルボイスは 1 ワード、MT 0x4 の MIDI 2.0 チャンネルボイスは 2 ワードです。

MIDI 2.0 イベントを作る ​

JavaScript と Python の Project バインディングは同じパッカー群を公開します。

系統JavaScriptPythonデータ
ノートmidi2NoteOn、midi2NoteOffmidi2_note_on、midi2_note_off16 ビットの発音/リリースベロシティ。ノートオンのベロシティ 0 もノートオンです
チャンネル表現midi2Cc、midi2ChannelPressure、midi2PitchBend対応する snake_case32 ビットの CC、プレッシャー、ベンド。ベンド中心は 0x80000000 です
ポリフォニックプレッシャーmidi2PolyPressuremidi2_poly_pressure1 ノートに対する 32 ビットプレッシャー
プログラムmidi2Programmidi2_program同じメッセージ内のプログラムと任意のバンク MSB/LSB。バンク有効が false のまま MSB/LSB に 0 以外を渡すと拒否される
RPN / NRPNregistered / assignable と relative のパッカー対応する snake_case32 ビット値または符号付き差分
ノート単位registered / assignable コントローラ、ノート単位ベンド対応する snake_caseノート単位のインデックス 0..255 または 32 ビットベンド
管理midi2PerNoteManagementmidi2_per_note_management1 ノートの detach / reset フラグ

ノート、グループ、チャンネル、コントローラ、プログラム、バンク、ノートコントローラのインデックスはプロトコルの範囲を使います。midi2NoteOn の attributeType と attributeData は任意です。type 3 は pitch 7.9 を運びます。これは 16 ビットのピッチで、7 ビットが半音、9 ビットが端数です(1/512 半音刻み)。

幅の広い値は同じ量をより細かく表したものです。フルスケールは 16 ビットのベロシティで 0xFFFF、32 ビットの値で 0xFFFFFFFF です。0xC000 はフルスケールの 75%(MIDI 1.0 の目盛りでは 127 のうち約 95)、0x8000 は約 64 です。7 ビットの値を広げるときはより広い範囲に引き伸ばし、戻すときは上位 7 ビットだけを残します。

typescript
import { Project, init } from '@libraz/libsonare';

await init(); // ブラウザ/WASM では必要。この WASM パッケージは Node でも初期化が必要です
const project = new Project();
project.setSampleRate(48000);
const { clipId } = project.addMidiClip(0, 4);
const on = Project.midi2NoteOn(0, 0, 0, 60, 0xc000);
const bend = Project.midi2PerNotePitchBend(0.25, 0, 0, 60, 0x80000000);
const off = Project.midi2NoteOff(2, 0, 0, 60, 0x8000);
try {
  project.setMidiEvents(clipId, [on, bend, off]);
} finally {
  project.delete();
}
python
import libsonare as sonare

with sonare.Project() as project:
    project.set_sample_rate(48000)
    _track_id, clip_id = project.add_midi_clip(0.0, 4.0)
    on = sonare.Project.midi2_note_on(0.0, 0, 0, 60, 0xC000)
    bend = sonare.Project.midi2_per_note_pitch_bend(0.25, 0, 0, 60, 0x80000000)
    off = sonare.Project.midi2_note_off(2.0, 0, 0, 60, 0x8000)
    project.set_midi_events(clip_id, [on, bend, off])

MIDI 1.0 の対象には、MIDI 1.0 パッカーも使えます。midiNoteOn、midiCc、midiChannelPressure などの midi* メソッドは、7 ビット値と 14 ビットのピッチベンドを持つ正規の MIDI 1.0 UMP を作ります。MIDI 2.0 イベントを MIDI 1.0 へ変換すると値を縮小し、単一の MIDI 1.0 メッセージに対応しない形式を捨てます。その損失を許容できる場合だけ MIDI 1.0 経路を選んでください。

リアルタイムで生 UMP をキューへ入れる ​

RealtimeEngine.pushMidiUmp(destinationId, words, renderFrame?) は、最上位ワードから並べた 1〜4 個の 32 ビットワードを受け取ります。配列の長さは先頭ワードのメッセージタイプと一致する必要があります。MT 0x4 の MIDI 2.0 チャンネルボイスはフル解像度で渡されます。setMidiInputSource(...) の後で使う pushMidiInputUmp(words, offsetInBlockSamples = 0) も同じ規則でエンジン所有の入力ソースへ入れます。

typescript
import { Project, RealtimeEngine, init } from '@libraz/libsonare';

await init();
const engine = new RealtimeEngine(48000, 128); // 制御スレッドのキュー。音声スレッドがレンダリングします
engine.setBuiltinInstrument({}, 0);
engine.setMidiInputSource(0);
const note = Project.midi2NoteOn(0, 0, 0, 60, 0xc000);
try {
  engine.pushMidiUmp(0, [note.data0, note.data1 ?? 0]);
  engine.pushMidiInputUmp([note.data0, note.data1 ?? 0], 0);
} finally {
  engine.destroy();
}
python
import libsonare as sonare

engine = sonare.RealtimeEngine(48000, 128)  # コマンドをキューし、process() のブロックでレンダリングします
engine.set_builtin_instrument()
engine.set_midi_input_source(0)
try:
    note = sonare.Project.midi2_note_on(0.0, 0, 0, 60, 0xC000)
    engine.push_midi_ump(0, [note[1], note[2]])
finally:
    engine.close()

AudioWorklet の SonareEngine ファサードは pushMidiUmp(trackId, words, renderFrame?) で完全なパケットを受け取ります。words は従来の 1 ワードの数値、または 1〜4 ワードの配列/Uint32Array にできます。エンジン所有の入力ソースに入れる pushMidiInputUmp(words, offsetInBlockSamples = 0) も同じ完全なパケットを受け取ります。生の MT 0x3/0x5 データメッセージはどちらの生 UMP 呼び出しでも拒否されるため、完全な SysEx フレームを pushMidiSysex で送ります。キュー満杯や不正なワードはバインディングからエラーとして返ります。

ライブのプログラムチェンジは、MIDI 2.0 メッセージ 1 件として送ります。デスティネーションには pushMidiProgram(...)、入力ソースには pushMidiInputProgram(...) を使います。従う受信側は一部だけです。シグネチャ、引数の範囲、受信側の表は MIDI 入力 を参照してください。

内蔵受信側が適用するもの ​

3 種類の Project/リアルタイム楽器バインディングはいずれも、渡された値の幅を保った MIDI 2.0 チャンネルボイス UMP を受け取ります。表現の対応は楽器ごとに異なります。

受信側適用するもの境界
内蔵波形シンセフル幅のノートベロシティ、チャンネルプレッシャー、ポリフォニックプレッシャー、ノート単位ベンドMPE のベンド処理は内蔵シンセのゾーン設定に従います
NativeSynthフル幅のノートベロシティ、チャンネルプレッシャー、ポリフォニックプレッシャー、ノート単位ベンドRPN 0 がベンド範囲を制御します。ほかの MIDI 2.0 ステータスがすべてのパッチへ効くとは限りません
SoundFont プレイヤーフル幅のノートベロシティ、チャンネルプレッシャー、ピッチベンド、MIDI 2.0 ノート単位ピッチポリフォニックキープレッシャーは意図的に無視します。MPE のメンバーチャンネルはチャンネル表現を運べます

受信側が表現の種類を無視しても、UMP には元のメッセージが残ります。生 UMP の経路と Web MIDI の便利な MIDI 1.0 メソッドはMIDI 入力を参照してください。

MIDI 2.0 クリップファイル (SMF2CLIP) ​

MIDI 2.0 クリップファイルは、SMF2CLIP ヘッダを持つメモリ上の UMP コンテナです。Project.exportClipFile() はプロジェクトのテンポマップと MIDI クリップを 1 つのクリップコンテナへ書き出し、importClipFile(...) はインポートしたクリップを追加します。この形式は MIDI 2.0 のチャンネルボイスメッセージを値変換なしで保持します。

16 ビットベロシティ、32 ビット CC、ノート単位/レジスタードコントローラ、バンク有効な Program Change を保持します。これらの値が重要な交換ではこの経路を使います。クリップファイルは SysEx7/8 データも運びます。書き出し時はプロジェクトの別ストアにある SysEx をファイルへ直列化し、読み込み時にそのストアへ戻します。プロジェクトの読み込みでは、どの MIDI イベントからも参照されない SysEx ペイロードは破棄されます。SysEx8 ペイロードの先頭 F0 や末尾 F7 のデータバイトは、SMF とクリップファイルのどちらの書き出しでも保たれ、SMF の読み込みではメタイベントをまたいで分割された SysEx メッセージが 1 つに結合されます。

拍子の Flex Data(テンポ・拍子・テキストを運ぶ UMP メッセージで、SMF のメタイベントに相当します)は分母を 2 の指数 e として格納し、2^e にデコードします。クリップファイルでは e = 0 を非標準分母用に予約するため、表現できる指数は 1..7(分母 2..128)です。2 のべき乗でない分母を書き出すと、128 を上限として次に表現できるべき乗へ切り上げ、スキップイベント数を増やします。読み込んだ指数が 1..7 の範囲外ならスキップし、有効な拍子が PPQ 0 から始まらない場合は PPQ 0 に 4/4 を補います。SMF2CLIP ヘッダにはプロジェクト履歴やスキーマバージョンのフィールドがないため、現在のクリップファイル形式の交換データだけを保持し、プロジェクト履歴やバージョン交渉は保持しません。

読み込みには、時刻付きクリップデータの前に設定ヘッダ(最初のゼロ DCS、つまり 0 ティックのデルタクロックスタンプと、それに続く 1 個の非ゼロ DCTPQ、つまり 4 分音符あたりのティック数の設定。タイミング: ティックと PPQ を参照)が必要です。その後に完全な Start-of-Clip マーカーが 1 個、続いて完全な End-of-Clip マーカーが 1 個だけ現れなければなりません。マーカーの欠落、重複、逆順、後続データは拒否されます。メッセージ種別が要求するワード数に達する前に終わる切り詰められた UMP も拒否します。書き出しでは、明示した UMP の word_count がメッセージ種別と一致しない場合、出力を生成する前に拒否するため、壊れたパケットが部分的なクリップファイルとして公開されることはありません。

typescript
import { Project, init } from '@libraz/libsonare';

await init();
const project = new Project();
const imported = new Project();
try {
  const clipFile = project.exportClipFile(); // "SMF2CLIP" で始まる Uint8Array
  const clipId = imported.importClipFile(clipFile);
  console.log(clipId);
} finally {
  imported.delete();
  project.delete();
}
python
import libsonare as sonare

with sonare.Project() as project, sonare.Project() as imported:
    clip_file = project.export_clip_file()  # b"SMF2CLIP" で始まる bytes
    clip_id = imported.import_clip_file(clip_file)

標準 MIDI ファイル は MIDI 1.0 ツールとの互換性に適しています。SMF はすべての MIDI 2.0 の値やノート単位メッセージを損失なく表現できません。プロジェクトの書き出しでは MIDI 1.0 に表現できない MIDI 2.0 専用イベントを捨てます。SMF はテンポ/拍子トラックを持つフォーマット 1、4 分音符あたり 480 ticks なので、MIDI 2.0 の完全なアーカイブではなく互換形式です。どちらの書き出しもテンポマップに従い(ランプは階段状のステップとして書かれ、最初のテンポは tick 0 に置かれます)、テンポ変更のあとにあるループ MIDI クリップの繰り返しをすべて書き出します。SMF の読み込みでは、長さゼロのノートを破棄します。

境界を明示する ​

  • ProjectMidiEvent は最初の 2 UMP ワードを持ち、プロジェクトイベントとして表現できるのはチャンネルボイスだけです。一般の UMP や SysEx のコンテナではありません。
  • 生のリアルタイム pushMidiUmp は 1〜4 ワードの完全なパケットを受け取りますが、MT 0x3 と MT 0x5 のデータメッセージを拒否します。SysEx には pushMidiSysex を使います。
  • MIDI 1.0 パッカーは 7 ビットのチャンネル値と 14 ビットのピッチベンドを使います。MIDI 2.0 パッカーは上の表に示した 16/32 ビットのフィールドを使います。
  • ループの折り返し、シーク、停止のあと、次に再生が進むときに、エンジンは新しい位置より前にクリップが設定したコントローラー、プログラム、チャンネルプレッシャー、ピッチベンド、MIDI 2.0 のレジスタード/アサイナブルコントローラの値を再送します。データエントリー、RPN/NRPN の選択、ポルタメント制御(CC 84)、チャンネルモードメッセージは再送されません。
  • パケットの精度と受信側の対応は別です。特定の楽器が無視するポリフォニックプレッシャーやノート単位コントローラも、パケットには保持できます。

関連 ​