MIDI クリップを編集する
MIDI クリップは、プロジェクトのタイムライン上にある記号的なイベントリストです。このページでは、チャンネルボイスイベントの作成、クリップの置換、ノート対の検証、キャプチャした入力のルーティング、MIDI-FX を非破壊のまま使うか焼き込むかを扱います。フル解像度の MIDI 2.0 UMP パッカーとライブの複数ワードメッセージは、MIDI 2.0・UMP・クリップファイルを参照してください。
MIDI イベントは音声ではありません
setMidiEvents が変更するのは保存されたノートとコントローラのイベントです。PCM はレンダリングせず、変更もしません。クリップを音にするには楽器をバインドしてプロジェクトをバウンスします。プロジェクト MIDI とプロジェクトのバウンスを参照してください。
MIDI の内容
MIDI クリップはフラットなイベントリストを保持します。Project.midi* 静的パッカー(正規の MIDI 1.0 ワードを生成します)でイベントを作り、setMidiEvents でクリップのリストを置き換えます。
番号は 0 から数える
どのパッカーも (ppq, group, channel, …) を受け取り、数値は通信上の値そのままで、0 から数えます。
| 引数 | 範囲 | MIDI 機器や GM の一覧表での呼び方 |
|---|---|---|
group | 0〜15 | UMP グループ。MIDI 2.0 パケットで 16 チャンネルをまとめた単位です。複数のグループを使い分けない限り 0 |
channel | 0〜15 | MIDI チャンネル 1〜16。チャンネル 9 が GM のドラムチャンネル 10 |
program | 0〜127 | プログラム 1〜128。4 は GM のプログラム 5、Electric Piano 1 |
note | 0〜127 | 60 が中央のド(C4) |
setMidiEvents はクリップの SysEx を捨てる
クリップの SysEx ペイロード(GS リセットや、メーカー固有 SysEx の「データセット」メッセージで作る GS のセットアップブロック)はイベントリストの脇に置かれ、ProjectMidiEvent が持たないハンドルで参照されています。importSmf() は保持し、exportSmf() はバイト単位でそのまま書き戻し、toJson() も運びます。オフラインバウンスでも実際に反映され、インポートした SMF に埋め込まれた GS インサーションエフェクトのタイプ選択はレンダリング結果の音を変えます。setMidiEvents() はリストを置き換えるためペイロードを参照するものが何も残らず、1 回の呼び出しで全フレームが消えます。読み戻す入口もないので、先に退避して復元することもできません。GS のセットアップブロックを残す必要があるなら、クリップのイベントリストではなく書き出したファイルの側を編集してください。
import { Project, init } from '@libraz/libsonare';
await init();
const project = new Project();
project.setSampleRate(48000);
const { clipId: midiClip } = project.addMidiClip(0, 4);
project.setMidiEvents(midiClip, [
Project.midiNoteOn(0, 0, 0, 60, 100), // (ppq, group, channel, note, velocity)
Project.midiNoteOff(2, 0, 0, 60),
Project.midiNoteOn(2, 0, 0, 64, 100),
Project.midiNoteOff(3.75, 0, 0, 64),
]);
project.setProgram(midiClip, 4); // GM プログラム(例: 4 = エレクトリックピアノ)
// 次の編集手順でも `project` を使い、終わったら project.delete() を呼ぶ。import libsonare as sonare
project = sonare.Project()
project.set_sample_rate(48000)
_track_id, midi_clip = project.add_midi_clip(0.0, 4.0)
project.set_midi_events(midi_clip, [
sonare.Project.midi_note_on(0.0, 0, 0, 60, 100), # (ppq, group, channel, note, velocity)
sonare.Project.midi_note_off(2.0, 0, 0, 60),
sonare.Project.midi_note_on(2.0, 0, 0, 64, 100),
sonare.Project.midi_note_off(3.75, 0, 0, 64),
])
project.set_program(midi_clip, 4) # GM プログラム(例: 4 = エレクトリックピアノ)
# 次の編集手順でも `project` を使い、終わったら project.close() を呼ぶ。このようなクリップをピアノロールで描き、再生した例です。
Python では静的パッカーが Project.midi_note_on(...) / Project.midi_note_off(...) で、それぞれ (ppq, data0, data1) タプルを返します。イベントリストはそのタプルの任意のシーケンスです。
setProgram は 3 つ目の任意引数 bank を取ります。setProgram(clipId, program, bank?) の形です。省略するとバンクセレクトは送出されず、>= 0 を渡すとプログラムチェンジの前にバンクセレクトを送出します。負の bank は拒否されます(Node と WASM は RangeError、Python は SonareValueError)。-1 は「なし」を表す値ではありません。クリップ既定ではなく特定の UMP(Universal MIDI Packet)グループ・チャンネルでプログラムを変えるには setProgramOnChannel(clipId, group, channel, program, bank?) を使います。どちらも WASM・Node・Python すべてで同じ任意の bank を取ります(set_program(clip_id, program, bank=None)、set_program_on_channel(clip_id, group, channel, program, bank=None))。
ppq はティックではなく 4 分音符単位
ppq 引数は 4 分音符単位の位置(浮動小数点)であり、MIDI のティック数ではありません。Project.midiNoteOn(1, …) は 4 分音符 1 つ分あと、Project.midiNoteOn(0.5, …) は 8 分音符 1 つ分あとを指します。名前に反して 480 ティック/4 分音符ではありません。Project.midiNoteOn(480, …) は 4 分音符 480 個分(120 小節)先にノートを置くため、ほぼ常にレンダリング範囲のはるか外となり、何も鳴らずに終わります。ティックベースのソース(480 PPQ の SMF など)から変換する場合は、まずソースの「4 分音符あたりのティック数」で割ってください(タイミング: ティックと PPQを参照)。同じ単位が addMidiClip(startPpq, lengthPpq) と、本ページのすべてのクリップ/オートメーション位置に適用されます。
各静的パッカーは、setMidiEvents のリストにそのまま渡せる MIDI 1.0 の UMP ワード(1 つまたは複数)を返します。
| パッカー | シグネチャ | イベント |
|---|---|---|
| ノートオン | Project.midiNoteOn(ppq, group, channel, note, velocity) | ノートオン |
| ノートオフ | Project.midiNoteOff(ppq, group, channel, note, velocity?=0) | ノートオフ |
| コントロールチェンジ | Project.midiCc(ppq, group, channel, controller, value) | CC |
| プログラムチェンジ | Project.midiProgram(ppq, group, channel, program) | プログラムチェンジ |
| バンク + プログラム | Project.midiBankProgram(ppq, group, channel, bankMsb, bankLsb, program) | バンクセレクト + プログラムチェンジ(複数イベントを返す) |
| ポリプレッシャー | Project.midiPolyPressure(ppq, group, channel, note, pressure) | ノート単位アフタータッチ |
| チャンネルプレッシャー | Project.midiChannelPressure(ppq, group, channel, pressure) | チャンネルアフタータッチ |
| ピッチベンド | Project.midiPitchBend(ppq, group, channel, bend) | ピッチベンド。bend は符号なし 14 ビット(0..16383、中央 8192)で、範囲外は RangeError を送出 |
イベントレベルの Project.midiProgram(...) パッカーはプログラムチェンジワードをクリップのイベントリスト内に置きます。上で示したクリップレベルの project.setProgram(midiClip, program)(クリップの既定プログラムを直接設定する便利メソッド)とは別物です。
validateMidiNotes
バウンス前に、MIDI クリップのハングノート、つまり対応するノートオフのないノートオン(またはその逆)を調べます。放置すると音が鳴りっぱなしになります。validateMidiNotes は、クリップの再生範囲の中でチャンネル + ノートごとに FIFO でノートオンとノートオフを対応づけ、結果を報告します。クリップの終端やループ境界で途切れたノートは、そこにノートオフが補われて閉じられるので、クリップをはみ出すノートも対のものとして検証を通ります。ノートオフが一度もないノートオンは、これまでどおり報告されます。
const check = project.validateMidiNotes(midiClip);
// { ok: true, unmatchedNoteOns: 0, unmatchedNoteOffs: 0 }
if (!check.ok) {
console.warn(`ハングノート: オン ${check.unmatchedNoteOns} / オフ ${check.unmatchedNoteOffs}`);
}check = project.validate_midi_notes(midi_clip)
# check.ok, check.unmatched_note_ons, check.unmatched_note_offs
if not check.ok:
print(f"ハングノート: オン {check.unmatched_note_ons} / オフ {check.unmatched_note_offs}")MIDI アレンジを鳴らすには、レンダリング時に楽器をバインドします。プロジェクトバウンス、内蔵シンセサイザー、SoundFont プレイヤーを参照してください。コントローラからプロジェクトをライブ演奏するには、MIDI 入力を参照してください。
キャプチャした MIDI ストリームをルーティングする
Project.midiRouteEvents(events, config?) は静的ヘルパーで、キャプチャした ProjectMidiEvent ストリームをネイティブの MidiRouter(フィルター/リマップ/チャンネルスルー。ライブランタイムが使うものと同じルーターです)に通し、ProjectMidiRouteResult を返します。録音した入力をクリップにする前に、オフラインで事前フィルターやリマップを行う用途に使えます。
const routed = Project.midiRouteEvents(capturedEvents, {
filterGroup: 0, // グループ 0 だけ残す(省略 / null = 任意)
filterChannel: 9, // チャンネル 9(ドラムチャンネル)だけ残す
remapChannel: 0, // 残ったイベントをチャンネル 0 へ書き換える
thru: true, // 一致したイベントを通す
});
// routed.events -> ProjectMidiEvent[]
// routed.overflowed -> ルーターのバッファがイベントを取りこぼすと true
// routed.overflowCount-> 取りこぼしたイベント数
project.setMidiEvents(midiClip, routed.events);config のフィールドはすべて任意で、JS/WASM では camelCase(filterGroup・filterChannel・remapChannel・thru)です。フィルターフィールドが null または省略なら「任意」を意味し、remapChannel を省略するとチャンネルは変更されません。Python では snake_case(filter_group・filter_channel・remap_channel・thru)です。このヘルパーは WASM・Node・Python すべてで利用できます。下の CC ラーンと組み合わせて使えます。
CC をラーンしてオートメーションにする
オフラインで「ツマミを動かし、どの CC が動いたかを取り込む」流れには、Project.midiCcLearn(events, paramId, options?) を使います。取り込んだイベント列からどのコントローラーが動いたかを見つけ、パラメータ paramId へのバインド記述子を返します(該当がなければ null)。続けて midiCcToBreakpoint(bindings, event) が CC イベントをオートメーションのブレークポイントに変換し、midiParamToCc(bindings, paramId, unitValue, group, ppq?) がパラメータ値を CC イベントに戻します。3 つとも、ライブエンジンではなく取り込んだ ProjectMidiEvent データを対象とします。ライブで CC をパラメータにバインドするには MIDI 入力を参照してください。
const cc = (ppq: number, value: number) => Project.midiCc(ppq, 0, 0, 1, value); // チャンネル 0 の CC1
const events = [cc(0, 10), cc(0.5, 100)];
const binding = Project.midiCcLearn(events, /* paramId */ 42, { minMovement: 20 });
// { ccNumber: 1, channel: 0, kind: 0, paramId: 42, minValue: 0, maxValue: 1, ... }
const point = Project.midiCcToBreakpoint([binding!], events[1]);
// { ppq: 0.5, value: 0.787, curveToNext: 0 }
const back = Project.midiParamToCc([binding!], 42, 0.5, /* group */ 0, /* ppq */ 1);
// ppq 1 の CC イベントcc = lambda ppq, value: sonare.Project.midi_cc(ppq, 0, 0, 1, value) # チャンネル 0 の CC1
events = [cc(0.0, 10), cc(0.5, 100)]
binding = sonare.Project.midi_cc_learn(events, 42, min_movement=20)
point = sonare.Project.midi_cc_to_breakpoint([binding], events[1]) # (ppq, value, curve_to_next)
back = sonare.Project.midi_param_to_cc([binding], 42, 0.5, 0, 1.0) # (ppq, data0, data1)minMovement(Python は min_movement)は 7 ビット CC 単位で測る 0〜127 の整数(既定 0)で、範囲外の値は例外になります。0 なら最初に観測したスタンドアロンのコントローラー値ですぐにバインドします。ただし番号の小さい CC(0..31)は、次のメッセージと 14 ビットペアを作れるよう一時的に保持します(14 ビットペアとは、番号の小さい CC 0..31 と、それより 32 大きい番号の CC の組です。詳しくは MIDI の基礎を参照)。正のしきい値を指定すると、同じ UMP グループとチャンネルで最初に観測した値から、その値以上動いたときにバインドします。MIDI 2.0 の 32 ビット CC 値も 7 ビット単位へ縮小して比較します。このしきい値はスタンドアロンの 7 ビットコントローラーだけに適用し、14 ビットペアと RPN/NRPN シーケンスはこのゲートを通らずに組み立てます。
MIDI-FX チェーンをクリップに焼き込む
MIDI-FX チェーン(トランスポーズ、ベロシティカーブ、ヒューマナイズなど)は通常、クリップのイベントに重なる非破壊のレイヤーとして働きます。bakeMidiFx はその逆で、チェーンを 1 回実行し、その結果でクリップに保存された MIDI イベントを書き換えます。これにより変換後のノートがクリップの実体になります。エフェクトをアレンジに固定したいときは焼き込み、まだ調整したいときは非破壊のままにしておきます。
const configJson = JSON.stringify({ transpose_semitones: 12 }); // 1 オクターブ上げる
project.bakeMidiFx(midiClip, configJson); // イベントがその場でトランスポーズされる編集時の選択範囲や注釈を焼き込み後にも対応付けたいときは、リクエスト形式を使います。sourceIndex は正規順に並ぶ変換後の各イベントに対応する入力イベントの index です。各 index は必ず 0..入力イベント数-1 の範囲にあり、コードやアルペジエーターのように 1 つの入力から複数のイベントが出る場合は同じ index が複数回現れます。
const count = project.previewMidiFxCount({ clipId: midiClip, configJson });
const { sourceIndex } = project.bakeMidiFx({
clipId: midiClip,
configJson,
withSourceIndex: true,
});previewMidiFxCount(...) はプロジェクトを変更せず、同じ決定的な変換を実行します。返る件数は続けて焼き込むイベント数と一致するため、出力バッファを正確に確保できます。従来の bakeMidiFx(clipId, configJson) 形式も使えますが、由来情報は返しません。Python では project.preview_midi_fx_count(clip_id, config_json) が件数を、project.bake_midi_fx(clip_id, config_json, with_source_index=True) が由来の index を返します(フラグなしの bake_midi_fx は None を返します)。
config は JSON オブジェクトで、各ステージの有効/無効はそのパラメータのキーの有無で決まります。ステージのキーを含めれば有効になり、省けばスキップされます。未知のキーは無視されるため、キーを打ち間違えてもエラーにならず、何も起きません。
| ステージ | キー |
|---|---|
| トランスポーズ | transpose_semitones |
| ベロシティカーブ | velocity_scale、velocity_offset、velocity_gamma(>0)。下記参照 |
| クオンタイズ | quantize_ppq(>0)、quantize_strength(0–1、既定 1) |
| ヒューマナイズ | humanize_ppq(>=0)、humanize_velocity(0〜127)、seed(非負の整数)。タイミングとベロシティへ再現可能な揺らぎを加える |
| コード | chord_intervals(半音オフセットの配列、1〜8 要素) |
| アルペジエーター | arpeggiator_intervals(半音オフセットの配列、1〜16 要素)、arpeggiator_step_ppq(>0)、arpeggiator_gate_ppq(既定はステップ長で、それに丸められる) |
ベロシティカーブはノートオンだけに、次の順で働きます。ベロシティを 0〜1 に正規化し、velocity_gamma 乗し、velocity_scale を掛け、velocity_offset(7 ビットのベロシティ単位)を足して、丸めて 1〜127 に収めます(ノートオンが 0 になることはありません)。式にすると v' = clamp(127 · (v/127)^gamma · scale + offset) です。gamma が 1 より大きいと弱い音が下がって強い音は残り、1 より小さいと弱い音が持ち上がります。MIDI 2.0 のノートオンは、先に 7 ビットへ丸めず、16 ビットの全範囲で整形されます。
クオンタイズは、quantize_ppq 個ぶんの 4 分音符を間隔とするグリッド(0.25 なら 16 分音符のグリッド)へイベントを寄せます。グリッドはクリップ自身の先頭から数えます。ノートオンは最も近いグリッド線へ、距離の quantize_strength 倍だけ動き(1 なら線の上、0.5 なら半分)、対応するノートオフも同じ量だけ動くので、ノートの長さは保たれます。コントローラーなどほかのイベントは位置を保ちます。16 分音符のグリッドで強さ 1 なら、0.3 から 0.9 までのノートは 0.25 から 0.85 になります。
chord_intervals は 8 要素、arpeggiator_intervals は 16 要素が上限です。空配列、またはいずれかの上限を超える配列を渡すと、bakeMidiFx は黙って切り詰めるのではなく RangeError(Python は SonareValueError)を投げます。
// 押さえた各ノートを 3 ステップの上昇アルペジオにする(1 ステップ 16 分音符)。
project.bakeMidiFx(midiClip, JSON.stringify({
arpeggiator_intervals: [0, 4, 7],
arpeggiator_step_ppq: 0.25,
arpeggiator_gate_ppq: 0.2,
}));書き換えは破壊的ですが、ほかの編集と同様にアンドゥ可能です。undo() で元のイベントに戻ります。
関連
- 音声を MIDI に変換する — モノラル録音をプロジェクトの MIDI イベントに変換します
- MIDI 2.0・UMP・クリップファイル — フル解像度のパッカー、ライブ UMP、忠実度を保つ交換を扱います
- MIDI 入力 — ライブのコントローラ入力を楽器へルーティングします