Skip to content

プロジェクト & アレンジ編集

DAW を開かずに、曲のアレンジをコードで組み立てたい—— それを叶えるのが Project です。プロジェクトは、1 曲を構成するすべてを保持するタイムラインです。オーディオトラック、MIDI トラック、そこに置かれたクリップ、テンポマップ、拍子、マーカーが含まれます。libsonare には小さなヘッドレス DAW 編集 API である Project モデルが備わっており、DAW ホストを組み込まずに、自分のアプリの中でそのタイムラインを構築・編集・シリアライズできます。

作業は短いループです。アレンジを組み立て、アンドゥ可能な操作で編集し、コンパイルして再生可能なタイムラインにし、JSON に保存し、最後に音声をレンダリングします。Projectオフラインの制御スレッド向け API(音声スレッドでは決して動きません)で、ブラウザ(WASM)でも Node でも Python でも同じように動作します。

最初に押さえる 3 語

トラックはタイムライン上の 1 本のレーン(オーディオレーンまたは MIDI レーン)です。クリップはトラックに置かれた 1 ブロックの内容で、録音オーディオの一片や MIDI ノートの領域です。PPQ(pulses per quarter note)は libsonare が音楽的な時間を測る単位です。クリップの開始・長さ・イベント位置はすべて 4 分音符を単位として表され、lengthPpq: 4 はテンポに関係なく 4 分音符 4 つ分の長さになります。

ヘッドレス DAW

ヘッドレス DAW は、独自の画面・タイムライン UI・プラグインホストを持たない DAW の中核部分です。libsonare はデータモデルと音声エンジンを提供し、ボタン、波形ビュー、ファイル選択、プロジェクト一覧などはあなたのアプリ側で作ります。

パイプライン内での編集の位置

解析はトラックが「何か」を調べます。編集はタイムライン上にクリップを配置・トリミングし、タイミングを直します。ミキシングは複数トラックをステレオバスへまとめます。マスタリングは仕上がったミックスを配信向けに磨きます。本ページは編集の工程で、「ステムと MIDI のフォルダ」を「構造化されたアレンジ」へ変える段階です。クリップトラックフェードテンポマップ という言葉に馴染みがなければ、先に 編集の基礎 を読んでください。

プロジェクトのモデル

プロジェクトはいくつかの単純な部品を入れ子にした構造で、各部品が次の部品の入れ物になっています。

  • トラッククリップ(タイムライン上に置く内容のブロック)を持ちます。
  • オーディオクリップは代替のテイクと、それらの良い部分を 1 つの演奏につなぐコンプを持てます。
  • トラックはオートメーションレーン——音量やフィルターのカットオフなどのパラメータを時間方向に動かす記録カーブ——を持てます。フェーダーが自動で動くようなものです。
  • MIDI トラックは楽器のデスティネーション——そのノートを実際に音にするシンセやサンプラー——を指します(すぐ下で説明します)。
  • すべてのトラックはミキサーシーンのストリップ——EQ・フェーダー・パン・センドから成る自分のチャンネル——を通ってマスターへ流れます。

MIDI の「デスティネーション」とは

MIDI ノートは「いま音 60 を鳴らせ」といった指示にすぎず、音そのものではありません。デスティネーションは、その指示を送り届ける楽器——指示を音声に変えるシンセやサンプラー——です。MIDI トラックはデスティネーションを名前で指し、レンダリング時に実際の楽器をそこへバインドします。プロジェクトバウンスを参照してください。

プロジェクトの構造
ProjectオーディオトラックMIDI トラックオートメーションレーンオーディオクリップ (テイク / コンプ)MIDI クリップ (ノートイベント)MIDI デスティネーションミキサーシーンのストリップマスターバス
すべてのトラック・クリップ・レーンはプロジェクトの下に入れ子になり、オーディオトラックと MIDI トラックはどちらもミキサーシーンを経てマスターバスへ流れます。

編集の流れを先に見る

API の一覧へ進む前に、この流れを頭に入れておくと迷いにくくなります。Project を編集し、コンパイルでタイムラインを検査し、バウンスで音声サンプルへ変換します。

編集 → コンパイル → バウンス
エラーなしエラーあり修正オーディオ / MIDI ソーストラックとクリップアンドゥ可能な編集compile()診断バウンスクリップ・トラック・ルーティングを修正インターリーブ Float32 音声
診断でエラーが報告された場合、クリップ・トラック・ルーティングの修正は行き止まりではなく、破線で示すように編集ステップへ戻ってループします。

compile() がエラー付きで返ってきても、修正は行き止まりではありません。該当するクリップ・トラック・ルーティングを直せば、アレンジは同じアンドゥ可能な編集ステップに戻り、再びコンパイルできる状態になります。初学者がつまずきやすい点は 2 つです。

  • compile() は音を作りません。アレンジを検査し、レンダリング可能な形へ準備します。
  • 通常の bounce() はオーディオトラックだけをレンダリングします。MIDI トラックを鳴らすには bounceWithSynthInstrument(...)bounceWithSf2Instrument(...) のような楽器つきバウンスが必要です。

このページで身につくこと

このページを読むと、次のことができるようになります。

  • Project を作成し、オーディオ/MIDI トラックを追加してクリップを配置する。
  • クリップ(分割・トリム・移動・ゲイン・フェード・ループ・ソース差し替え・複製・削除)とトラック(追加・名前変更・ルーティング・種別変更・削除)をアンドゥ可能な操作で編集する。
  • PPQ、テンポセグメントを持つテンポマップ、拍子、マーカーを使って音楽的な時間を正しく置く。
  • クリップの重なりポリシーとワープモード(off / repitch / tempo-sync)をワープアンカーとともに選ぶ。
  • キー/コード注釈とオートメーションレーンをプロジェクトへ書き込む。
  • 再生可能なタイムラインへコンパイルし、構造化された診断と致命的でない警告を読む。
  • 決定的な JSON で保存・読み込みし、SMF(標準 MIDI ファイル)と MIDI 2.0 クリップファイル形式で MIDI を交換する。

プロジェクトを作成して内容を追加する

すべてのプロジェクトは空から始まります。サンプルレートを設定し、トラックを追加し、クリップを配置します。addTrackaddClip は安定した整数 ID を返し、以降の編集ではこの ID を使います。位置と長さは PPQ です。

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

await init();

const project = Project.create();
try {
  project.setSampleRate(48000);

  // 録音クリップ 1 つを持つオーディオトラック(デコード済みインターリーブ float 音声)
  const audioTrack = project.addTrack({ kind: 'audio', name: 'lead-gtr' });
  const clipId = project.addClip({
    trackId: audioTrack,
    startPpq: 0,          // 先頭に配置
    lengthPpq: 4,         // 4 分音符 4 つ分の長さ
    audio: guitarMono,    // デコード済みサンプルの Float32Array
    audioChannels: 1,
    audioSampleRate: 48000,
  });

  // MIDI トラック + クリップを 1 回で作成
  const { trackId: midiTrack, clipId: midiClip } = project.addMidiClip(0, 8);
} finally {
  project.delete();       // WASM ハンドルは GC されない — 必ず解放する
}
python
import libsonare as sonare

with sonare.Project() as project:
    project.set_sample_rate(48000)

    audio_track = project.add_track("audio", name="lead-gtr")
    clip_id = project.add_clip(
        audio_track,
        start_ppq=0.0,        # 先頭に配置
        length_ppq=4.0,       # 4 分音符 4 つ分の長さ
        audio=guitar_mono,    # インターリーブ float サンプル
        audio_channels=1,
        audio_sample_rate=48000,
    )

    midi_track, midi_clip = project.add_midi_clip(0.0, 8.0)
# with ブロックを抜けるとネイティブハンドルが解放される

プロジェクト概要の更新や、インポートしたアレンジの検証には project.trackCount()project.clipCount() を使えます。シリアライズ済み JSON を走査せず、トラック数とクリップ数を取得できます。Python では track_count()clip_count() です。

プロジェクトは必ず解放する

Project はすべての WASM オブジェクトと同様、JavaScript の GC では回収できないヒープハンドルを保持します。WASM パッケージでは Project.create() で作り、finally ブロックで project.delete() を呼んでください。Node ネイティブでも Project.create() で作り、project.destroy() または project.delete() で解放します。Python ではコンテキストマネージャ(with sonare.Project() as project:)として使うか、project.close() を呼びます。ハンドルをリークすると、長時間のセッションでネイティブまたは WASM メモリが徐々に枯渇します。

クリップを編集する

クリップ操作はいずれも 1 つのアンドゥ可能なコマンドで、クリップ ID で対象を指定します。

操作メソッド内容
分割splitClip(clipId, splitPpq)絶対 PPQ でクリップを切り、新しいクリップの ID を返す
トリムtrimClip(clipId, newStartPpq, newLengthPpq)開始と長さを再設定する
移動moveClip(clipId, newStartPpq, newTrackId?)クリップをずらす。別トラックへも移せる
ゲインsetClipGain(clipId, gain)クリップごとの線形再生ゲイン(>= 0)。オーディオクリップにのみ有効で、MIDI クリップには保存されるがバウンスでは適用されない
フェードsetClipFade(clipId, fadeIn, fadeOut)カーブつきのフェードイン/フェードアウト領域
ループsetClipLoop(clipId, mode, loopLengthPpq?, loopCrossfadePpq?)'off' または 'loop' と、任意のループ継ぎ目クロスフェード
ソース差し替えsetClipSource(clipId, sourceId)クリップを別の登録済みソースへ再バインドする
複製duplicateClip(clipId, newStartPpq)同じトラックにコピーし、新しい ID を返す
削除removeClip(clipId)クリップを削除する
typescript
project.setClipGain(clipId, 0.8);
project.setClipFade(
  clipId,
  { lengthPpq: 0.5, curve: 'equal-power' },  // 半拍でフェードイン
  { lengthPpq: 1.0, curve: 'linear' },       // 1 拍でフェードアウト
);
const tailId = project.splitClip(clipId, 2); // 拍 2 で切り、後半が新クリップになる
project.setClipLoop(tailId, 'loop', 2, 0.05); // 短い継ぎ目クロスフェード付きで 2 拍ごとにループ
const copyId = project.duplicateClip(tailId, 8);

フェードカーブは 'linear''equal-power''exponential''logarithmic' です。各フェード長はクリップ長を上限にクランプされるため、過大なフェードがクリップ開始より前から始まることはありません。負の長さはそのまま拒否されます。ループモードは 'off' または 'loop' で、ループ時は正の loopLengthPpq が必要です。loopCrossfadePpq はループ継ぎ目に入れる任意の equal-power クロスフェードです。0 なら従来どおりのハードループ、正の値ならループ末尾とプリロール側のソース素材をブレンドします。エンジンは使用可能なソースオフセットとループ長の半分を上限にクランプし、ワープ済みクリップではこの継ぎ目クロスフェードを無効にします。

setClipGain / setClipFade はオーディオクリップのみに効く

setClipGainsetClipFade が効くのはオーディオクリップのみです。MIDI クリップでは値が保存され(アンドゥ可能で toJson() でも往復します)が、レンダリングされるノートには反映されません。コンパイラは MIDI クリップのイベントをそのままレンダースケジュールへコピーし、クリップはトラックのミュート/ソロ/ゲインだけでゲートするため、クリップごとのゲインとフェードは音には影響しません。MIDI で鳴る楽器の音量を制御するには、トラックゲインsetTrackGain(trackId, gain)ミキサーシーンのチャンネルストリップのフェーダーに畳み込まれます)を設定してください。トラックゲイン 0 はそのトラックの MIDI ノートを完全に無音にします。

Python では同じ操作が snake_case になり、フェードは長さとカーブを個別の引数で受け取ります。

python
project.set_clip_gain(clip_id, 0.8)
project.set_clip_fade(
    clip_id,
    fade_in_length_ppq=0.5,
    fade_out_length_ppq=1.0,
    fade_in_curve="equal-power",
    fade_out_curve="linear",
)
tail_id = project.split_clip(clip_id, 2.0)
project.set_clip_loop(tail_id, "loop", 2.0, loop_crossfade_ppq=0.05)
copy_id = project.duplicate_clip(tail_id, 8.0)

トラックを編集する

トラック操作も同様にアンドゥ可能です。

操作メソッド内容
追加addTrack({ kind, name })'audio''midi''aux' トラックを追加し、ID を返す
削除removeTrack(trackId)トラックとそのクリップを削除する
名前変更renameTrack(trackId, name)トラック名を変える
種別変更setTrackKind(trackId, kind)トラックを 'audio' / 'midi' / 'aux' 間で切り替える
ルーティングsetTrackRoute(trackId, channelStripRef, outputTarget)トラックをミキサーストリップと出力バスに結びつける
ゲインsetTrackGain(trackId, gain)トラックのリニア出力ゲインを設定する(負値や非有限値は拒否される)
ミュートsetTrackMute(trackId, mute)トラックをミュート/解除する
ソロsetTrackSolo(trackId, solo)トラックをソロにし、他をミュート扱いにする
パンsetTrackPan(trackId, pan)トラックを [-1, 1] でパンする(非有限値は拒否される)
MIDI デスティネーションsetTrackMidiDestination(trackId, destinationId)トラックの MIDI を楽器のデスティネーション ID へルーティングする。内蔵シンセサイザー(NativeSynth)を参照
typescript
const drums = project.addTrack({ kind: 'audio', name: 'drums' });
project.renameTrack(drums, 'drum-bus');
project.setTrackRoute(drums, 'strip-drums', 'master'); // ミキサーシーンのストリップへ配線
python
drums = project.add_track("audio", name="drums")
project.rename_track(drums, "drum-bus")
project.set_track_route(drums, "strip-drums", "master")  # ミキサーシーンのストリップへ配線

aux トラックは自前のクリップを持ちません。内容を録音する場所ではなく、ルーティング/リターン用のレーン(たとえばエフェクトリターンやサブミックス)です。

setTrackRoute はプロジェクトトラックを、プロジェクトのミキサーシーンsetMixerSceneJson で設定)内のストリップへリンクします。これにより、バウンスしたトラックがそのチャンネルストリップの処理を通ります。

アンドゥとリドゥ

プロジェクトは編集履歴を保持します。クリップ・トラック・オートメーション・注釈の各操作は、取り消せるコマンドを積みます。

typescript
project.setClipGain(clipId, 0.3);
project.undo();   // ゲインが元の値に戻る
project.redo();   // ゲイン編集を再適用する
python
project.set_clip_gain(clip_id, 0.3)
project.undo()   # ゲインが元の値に戻る
project.redo()   # ゲイン編集を再適用する

長時間動作するエディターでは、アンドゥとリドゥが保持するメモリを制限したり、アレンジを変えずに編集セッションを切り替えたりできます。setMaxHistoryBytes(bytes) は両方の履歴スタックに共通するバイト上限を設定し、直ちに適用します。0 にすると保持を無効にするため、成功した編集もアンドゥできません。編集件数で制限したい場合は setMaxUndoDepth(depth) も使え、直近 depth 件だけを残します。WASM では depth1 以上の整数でなければなりません。clearHistory() は現在のプロジェクト状態を変えずに、アンドゥとリドゥの両方を消去します。Node も同じ camelCase 名、Python では set_max_history_bytes(...)set_max_undo_depth(...)clear_history() を使います。

typescript
project.setMaxUndoDepth(100); // 直近 100 件の編集だけを保持
project.setMaxHistoryBytes(8 * 1024 * 1024); // アンドゥ/リドゥ共通の上限
// ... 保存する、または別の編集セッションへ渡す ...
project.clearHistory();       // アレンジはそのまま。アンドゥ/リドゥだけが空になる

履歴は厳密なので、編集前に toJson() を呼び、アンドゥしてから再び toJson() を呼ぶと、バイト単位で同一の JSON になります。テストやエディタ UI の変更検出に役立つ不変条件です。

複数クリップを変更する複合編集は、履歴上では 1 トランザクションです。アンドゥ/リドゥは 1 ステップで完了し、アレンジが途中状態で残りません。

音楽的な時間: PPQ・テンポ・拍子・マーカー

すべての位置は PPQ(浮動小数点値としての 4 分音符。分数拍も正確に表せます)です。テンポと拍子は、順序づけられたセグメントのリストとしてプロジェクトに保持されます。

テンポマップとテンポセグメント

テンポマップはテンポセグメントのリストです。各セグメントは PPQ 位置から始まり BPM を設定します。任意の endBpm を指定すると、そのセグメントで新しいテンポへ直線的に変化します。

typescript
project.setTempoSegments([
  { startPpq: 0,  bpm: 120 },                 // 先頭から一定の 120 BPM
  { startPpq: 16, bpm: 120, endBpm: 140 },    // このセグメントで 120 -> 140 へランプ
  { startPpq: 32, bpm: 140 },
]);
project.tempoSegmentCount(); // 3
python
project.set_tempo_segments([
    {"start_ppq": 0.0, "bpm": 120},                     # 先頭から一定の 120 BPM
    {"start_ppq": 16.0, "bpm": 120, "end_bpm": 140},    # このセグメントで 120 -> 140 へランプ
    {"start_ppq": 32.0, "bpm": 140},
])
project.tempo_segment_count()  # 3

拍子

拍子は並列のセグメントリストで、各セグメントは分子(1 小節あたりの拍数)と分母(拍の単位)を持ちます。

typescript
project.setTimeSignatures([
  { startPpq: 0,  numerator: 4, denominator: 4 },
  { startPpq: 64, numerator: 3, denominator: 4 },  // 後半で 3/4 へ切り替え
]);
python
project.set_time_signatures([
    {"start_ppq": 0.0, "numerator": 4, "denominator": 4},
    {"start_ppq": 64.0, "numerator": 3, "denominator": 4},  # 後半で 3/4 へ切り替え
])

マーカー

マーカーはタイムライン上の位置にラベルを付けます。マーカー ID に 0 を渡すと新しい ID が割り当てられ、安定した ID が返ります。

typescript
const introId = project.setMarker(0, 0,  'intro');
project.setMarker(0, 16, 'verse');
project.setMarker(introId, 0, 'intro (edited)'); // ID を再利用して更新

構造化マーカーには、ProjectMarker オブジェクト全体を渡す setMarkerEx(...) を使います。MarkerKind は通常マーカー、テキスト、歌詞、キューポイント、調号を表します。調号マーカーでは keyFifths-7...+7、シャープが正)と keyMinor を使います。

typescript
import { MarkerKind } from '@libraz/libsonare';

project.setMarkerEx({
  id: 0,
  ppq: 32,
  name: 'drop cue',
  kind: MarkerKind.cuePoint,
  keyFifths: 0,
  keyMinor: false,
});

project.setMarkerEx({
  id: 0,
  ppq: 64,
  name: 'E minor',
  kind: MarkerKind.keySignature,
  keyFifths: 1,
  keyMinor: true,
});

for (let i = 0; i < project.markerCount(); i += 1) {
  console.log(project.markerByIndex(i));
}
python
from libsonare import MarkerKind, ProjectMarker

project.set_marker_ex(ProjectMarker(0, 32.0, "drop cue", MarkerKind.CUE_POINT))
project.set_marker_ex(
    ProjectMarker(0, 64.0, "E minor", MarkerKind.KEY_SIGNATURE, key_fifths=1, key_minor=True)
)

for index in range(project.marker_count()):
    print(project.marker_by_index(index))

Python ではセグメントリストに、上のマッピングの代わりに素のタプルも渡せます(テンポは (start_ppq, bpm)、拍子は (start_ppq, numerator, denominator))。単純なマーカー呼び出しは set_marker(marker_id, ppq, name) です。

重なりポリシー

重なりポリシーは、同じトラック上の 2 つのクリップが同じ時間範囲を占めてよいかを決めます。プロジェクト全体に適用されます。

typescript
project.setOverlapPolicy(0); // クリップの重なりを禁止(既定)
project.setOverlapPolicy(1); // 重なりを許可(クロスフェードや重ねたテイクなど)
project.getOverlapPolicy();  // 読み戻す

0 は重なりを禁止し、1 は許可します。重ねたクリップやクロスフェードを意図する場合は許可し、トラックを厳密に逐次にしたい場合は禁止します。このポリシーが素の整数なのは、ネイティブの列挙体をそのまま反映しているためです。定義されているのは 0(禁止)と 1(許可)だけで、それ以外の値は不正なパラメータとして拒否されます。

ワープ: クリップをグリッドに合わせて伸縮する

ワープは、録音したオーディオクリップを固定の元の速度で再生する代わりに、プロジェクトのグリッドへ追従させる機能です。録音を少し前後させたり伸縮させたりして、拍を狙った位置に合わせるイメージです。内部的には、クリップは自身の録音タイムラインを保ったまま、ワープマップがその録音タイムライン上の位置をプロジェクト時間上の位置へピン留めします。各クリップはワープモードを持ち、'off' 以外のモードが実際に効くにはアンカーから成るワープマップが必要です。

ワープモード意味
'off'音声をネイティブのレートで再生し、テンポを無視する
'repitch'テンポに合わせて速度を変える(テープのようにピッチも動く)
'tempo-sync'ピッチを保ったままテンポに追従するようタイムストレッチする

tempo-sync がピッチを保つしくみ

'tempo-sync' は音声をフェーズボコーダーでタイムストレッチします。これは STFT ベースのタイムストレッチで、ピッチを変えずにタイミングだけを変えます('repitch' がテープのように両方を動かすのとは対照的です)。同じアルゴリズムがリアルタイム再生でもオフラインのバウンスでも動くため、ワープしたクリップはどちらでレンダリングしても同じ音になります。ステレオやマルチチャンネルのクリップでは、全チャンネルをピークロック付きの 1 回のボコーダーパスで伸縮するため、チャンネル間で位相が揃ったままになり、ステレオイメージがずれません。

PARAM SWEEP · TIME STRETCHIDLE
タイムストレッチ — 音程はそのまま、長さを変える

タイムストレッチはピッチシフトのちょうど逆で、音程はそのままに、音の長さを変えます。レート(rate)をドラッグするとドラムの打点が広がったり詰まったりして波形がパネルを占める幅も変わりますが、下のスペクトルはほとんど動きません。1.0 より下では遅く長く、1.0 より上では速く短くなります。レンダーごとにピークを揃えているので、速いレートがそのまま小さく聞こえることはありません。聞こえるレベルを決めているのはデモ側で、ストレッチではありません。再生すると、チップマンク効果なしにグルーヴのテンポだけが変わるのが聴けます。

レート
1 ×

ワープマップはアンカーのリストで、各アンカーは「録音中のこの瞬間をタイムライン上のここに置く」というピンです。具体的には、各 ProjectWarpAnchorwarpSample(プロジェクト/ワープ後タイムライン上の位置)を sourceSample(録音音声内の対応位置)に結びつけ、エンジンは隣り合うアンカーの間で音声を滑らかに伸縮させます。

ワープアンカーは録音時間をプロジェクト時間へ対応づける
録音タイムライン(SOURCESAMPLE)プロジェクトタイムライン(WARPSAMPLE)1×(録音のまま)2× 伸長0.5×(速く)01200024000480000120003600048000
破線のピンが各 sourceSample を warpSample に結びつけ、隣り合うアンカーの間ではその区間の比率で音声を伸縮します。
typescript
// 再利用できるワープマップを定義し、クリップに割り当てる
project.setWarpMap({
  id: 1,
  name: 'groove',
  anchors: [
    { warpSample: 0,     sourceSample: 0 },
    { warpSample: 24000, sourceSample: 12000 }, // 小節前半をソースの 2 倍速で再生
  ],
});
project.setClipWarpRef(clipId, 1);          // マップを参照(0 で解除)
project.setClipWarpMode(clipId, 'tempo-sync');
// project.removeWarpMap(1);                 // 不要になったら ID でマップを削除

ワープマップは ID で管理される第一級オブジェクトです。setWarpMap({ id, name, anchors }) で追加・置換し、setClipWarpRef(clipId, id)0 で解除)でクリップに割り当て、project.removeWarpMap(id) で ID を指定して削除します。クリップがまだ参照しているマップを削除すると、そのクリップにはワープ参照が宙ぶらりんで残るため、先に setClipWarpRef(clipId, 0) で参照を解除してください。

アンカーはアプリ側で維持する。最低 2 個が必要

アンカーはサンプル単位の絶対的な対応表であり、エンジンがテンポマップから導き直すことはありません。したがって setTempoSegments(...) でテンポを変えても、ワープ済みクリップが伸縮し直されることはありません。変わるのはタイムライン上のクリップの開始位置と長さだけで、固定されたワープ曲線のうち再生される範囲が変わるにすぎません。テンポ変更に音声を追従させたいときは、アプリ側でアンカーを計算し直し、setWarpMap(...) で新しいマップを渡してください。

また、マップがストレッチを表すにはアンカーが 2 個以上必要です。ワープマップも事前ベイク済み音声も持たない 'tempo-sync' クリップは、compile() が参照切れとして報告するコンパイルエラーになります。'repitch' クリップでマップが無い、あるいはアンカーが 1 個だけの場合はエラーにならず、'off' と同じくネイティブ速度で静かに再生されます。

テイクとコンプレーン

クリップは代替のテイクと、複数テイクの良い箇所をつなぐコンプ(合成)を持てます。これらは Project の第一級機能(setClipTakessetClipCompSegmentsaddLoopRecordingTakes)で、ループ録音のキャプチャを含めて専用ページで詳しく扱います。録音とテイクを参照してください。

オートメーションレーン

オートメーションレーンは、ホスト定義のパラメータ 1 つを時間に沿って変化させます。各ブレークポイントは PPQ 位置・値・次の点へのカーブ('linear''exponential''hold''scurve')を持ちます。

typescript
// addAutomationLane はレーンのターゲットパラメータ ID を返します。
// これが編集・削除に渡すハンドルです。targetKind を省略すると従来の opaque レーンです。
const laneParamId = project.addAutomationLane(trackId, {
  targetParamId: 1,                                   // 変化させるパラメータのホスト ID
  points: [
    { ppq: 0, value: 0.0, curve: 'linear' },
    { ppq: 4, value: 1.0, curve: 'exponential' },
  ],
});
project.editAutomationLane(trackId, laneParamId, { targetParamId: 1, points: [/* … */] });
project.removeAutomationLane(trackId, laneParamId);

const faderLaneId = project.addAutomationLane(trackId, {
  targetParamId: 2,
  targetKind: 'track-fader-db',                   // または 'track-pan'
  points: [
    { ppq: 0, value: 0, curve: 'linear' },       // フェーダー値は dB
    { ppq: 4, value: -6, curve: 'linear' },
  ],
});
project.editAutomationLane(trackId, faderLaneId, {
  targetParamId: 2,
  targetKind: 'track-fader-db',
  points: [{ ppq: 0, value: -3, curve: 'linear' }],
});
python
lane_param_id = project.add_automation_lane(
    track_id,
    target_param_id=1,                # 変化させるパラメータのホスト ID
    points=[
        (0.0, 0.0, "linear"),         # (ppq, value, curve)
        (4.0, 1.0, "exponential"),
    ],
)
project.edit_automation_lane(track_id, lane_param_id, points=[])
project.remove_automation_lane(track_id, lane_param_id)

fader_lane_id = project.add_automation_lane(
    track_id,
    target_param_id=2,
    target_kind="track-fader-db",     # または "track-pan"
    points=[(0.0, 0.0, "linear"), (4.0, -6.0, "linear")],
)

Python ではブレークポイントがオブジェクトではなく (ppq, value, curve) タプルで、add_automation_lane / edit_automation_lanetarget_param_idpoints を別々の引数として受け取ります。従来のレーンは target_kind="opaque"(または省略)、ミキサーの型付きターゲットは "track-fader-db""track-pan" を渡します。Python は名前のほか序数 012 も受け付け、snake_case または camelCase のキーを持つマッピング記述子も使えます。

レーンの targetParamId は自分のパラメータ ID です。プロジェクトはブレークポイントをそのまま保存し、コンパイル済みタイムラインで再生します。この ID はレーンの識別子でもあります。1 トラックにつき同じターゲットのレーンは 1 本までで、addAutomationLane はその ID を返し、編集・削除もこの ID でレーンを指定します。レーンが動かすパラメータを変えるには、編集ではなく削除してから追加してください。

型付きレーンでは targetKind: 'track-fader-db' または 'track-pan' を指定し、レーンを所有するトラックのミキサーフェーダー/パンを対象にします。JavaScript は名前のほか序数 012 も受け付けます。コンパイル/インストール時にプロジェクトがエンジンの予約パラメータ名前空間へ解決するため、永続化された targetParamId はリアルタイム用 ID ではありません。オフラインバウンスではトラックミキサーを通って適用されます。1 トラックにつき各型のレーンは 1 本までです。targetKind を省略した場合は targetKind: 'opaque' と同じ従来のホスト定義ターゲットになります。JSON のフィールド名は target_kind で、型付きレーンを 1 本でも含むプロジェクトはスキーマバージョン 2、opaque だけのプロジェクトはスキーマバージョン 1 のまま既存バイト列を維持します。C の拡張エントリーポイントは sonare_project_add_automation_lane_exsonare_project_edit_automation_lane_ex で、従来の C 呼び出しは opaque/既存種別維持の経路です。

レーンの指定は位置ではなくターゲットパラメータ ID

editAutomationLaneremoveAutomationLane は、以前の位置インデックスに代わってターゲットパラメータ ID を受け取ります。どちらも数値で引数の個数も同じなので、インデックスを渡す既存コードはエラーにならず、別のレーンを編集してしまいます。インデックスを保持して渡している箇所は見直してください。

キーとコードの注釈書き戻し

プロジェクトは音楽的な注釈、すなわち解析器が生成したキー領域とコードシンボルを保持できます。これによりアレンジとともに移動し、保存/読み込みでも残ります。どちらのストリームも全置換で、アンドゥ可能です。

typescript
project.annotateKeys([
  { startPpq: 0, endPpq: 16, tonicPc: 0, mode: 1 }, // C メジャー(tonicPc 0、mode 1 = major)
]);
project.annotateChords([
  { startPpq: 0, endPpq: 4, rootPc: 0, quality: 1, romanNumeral: 'I' },
  { startPpq: 4, endPpq: 8, rootPc: 7, quality: 1, romanNumeral: 'V' },
]);
python
project.annotate_keys([
    (0.0, 16.0, 0, 1),  # (start_ppq, end_ppq, tonic_pc, mode) — C メジャー
])
project.annotate_chords([
    {"start_ppq": 0.0, "end_ppq": 4.0, "root_pc": 0, "quality": 1, "roman_numeral": "I"},
    {"start_ppq": 4.0, "end_ppq": 8.0, "root_pc": 7, "quality": 1, "roman_numeral": "V"},
])

Python では annotate_keys(start_ppq, end_ppq, tonic_pc, mode) タプルを、annotate_chords が WASM のオブジェクトと同じフィールド(snake_case キー)のマッピングを受け取ります。

数値フィールドはいずれも小さな固定エンコーディングです。

  • ピッチクラスtonicPcrootPc): 0..11 で、C = 0、C#/Db = 1、… B = 11。255 は不明を表します。
  • キーモードmode): 1 = major、2 = minor。
  • コードクオリティquality): 1 = major、2 = minor、3 = diminished、4 = augmented(全リストはコード認識を参照)。

つまり { tonicPc: 0, mode: 1 } は C メジャー、{ rootPc: 7, quality: 1 } は G メジャーコードです。

これらは解析 API の列挙体ではなくアレンジ側の序数

ここでの modequality の数値は1 始まりのアレンジ側の序数(major = 1)で、detectKey / detectChords が返す0 始まりModeChordQuality 列挙体(major = 0、minor = 1、diminished = 2、augmented = 3)とは別物です。両者は 1 つずれており、そのまま渡せません。解析 API の ChordQuality.Minor(= 1)を annotateChordsquality へ直接渡すと、ここではメジャーとして扱われてしまいます。注釈へ書き込む前に解析 API の結果を変換してください(たとえば quality = analysisQuality + 1)。

アシストサイドカー

アシストサイドカーは、プロジェクトごとの不透明でアンドゥ可能なメタデータブロブです。AI アシストの提案、ツール用ペイロード、その他アレンジとともに運びたいバイナリ注釈を格納する場所になります。各サイドカーはモジュール IDターゲットスコープ(トラック ID と PPQ 領域)でキー付けされ、ストア全体はプロジェクト JSON の assist_sidecars キーの下にシリアライズされるため、toJson() / fromJson() の往復でも残ります。

typescript
const payload = new TextEncoder().encode(JSON.stringify({ suggestion: 'tighten chorus' }));
project.setAssistSidecar({
  moduleId: 'my-assistant',  // 空にできない
  schemaVersion: 1,
  targetTrackId: 0,          // 0 = プロジェクトスコープ
  regionStartPpq: 0,
  regionEndPpq: 16,
  payload,                    // Uint8Array(コピーされる)
});

project.assistSidecars();     // プロジェクト順の全記述子
project.getAssistSidecar(0);  // { moduleId, schemaVersion, targetTrackId,
                              //   regionStartPpq, regionEndPpq, payload }

moduleId + targetTrackId + 領域スコープが既存のものと同じサイドカーは置換され、それ以外は追加されます。targetTrackId 0 はプロジェクトスコープを意味します。書き込みはアンドゥ可能な編集なので、undo() / redo() で取り消し・やり直しできます。

上の記述子形式が WASM と Node の標準 JavaScript API です。WASM には互換性のため、従来の位置引数形式 setAssistSidecar(moduleId, schemaVersion, targetTrackId, regionStartPpq, regionEndPpq, payload) も残っています。どちらの JavaScript バインディングにも件数、インデックスアクセサー、全件をまとめて読む assistSidecars() があります。Pythonset_assist_sidecar(module_id, payload, *, schema_version=0, target_track_id=0, region_start_ppq=0.0, region_end_ppq=0.0) を使い(マッピング記述子も受け付けます)、assist_sidecar_count()get_assist_sidecar(index)assist_sidecars() で読み取ります。C ABI は位置引数の sonare_project_set_assist_sidecar(...) と、対応する count/get/free 関数です。

MIDI の内容

MIDI クリップはフラットなイベントリストを保持します。Project.midi* 静的パッカー(正規の MIDI 1.0 ワードを生成します)でイベントを作り、setMidiEvents でクリップのリストを置き換えます。

typescript
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(4, 0, 0, 64),
]);
project.setProgram(midiClip, 4);          // GM プログラム(例: 4 = エレクトリックピアノ)
python
project.set_midi_events(midi_clip, [
    Project.midi_note_on(0.0, 0, 0, 60, 100),  # (ppq, group, channel, note, velocity)
    Project.midi_note_off(2.0, 0, 0, 60),
    Project.midi_note_on(2.0, 0, 0, 64, 100),
    Project.midi_note_off(4.0, 0, 0, 64),
])
project.set_program(midi_clip, 4)          # GM プログラム(例: 4 = エレクトリックピアノ)

Python では静的パッカーが Project.midi_note_on(...) / Project.midi_note_off(...) で、それぞれ (ppq, data0, data1) タプルを返します。イベントリストはそのタプルの任意のシーケンスです。

setProgram は 3 つ目の任意引数 bank を取ります——setProgram(clipId, program, bank = -1)。既定は -1(バンクセレクトを送出しない)で、>= 0 を渡すとプログラムチェンジの前にバンクセレクトを送出します。クリップ既定ではなく特定の UMP(Universal MIDI Packet)グループ・チャンネルでプログラムを変えるには setProgramOnChannel(clipId, group, channel, program, bank?) を使います。どちらも WASM・Node・Python すべてで同じ任意の bank を取ります(set_program(clip_id, program, bank=-1)set_program_on_channel(clip_id, group, channel, program, bank=-1))。

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 分音符あたりのティック数」で割ってください。同じ単位が 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 でノートオンとノートオフを対応づけ、結果を報告します。

typescript
const check = project.validateMidiNotes(midiClip);
// { ok: true, unmatchedNoteOns: 0, unmatchedNoteOffs: 0 }
if (!check.ok) {
  console.warn(`ハングノート: オン ${check.unmatchedNoteOns} / オフ ${check.unmatchedNoteOffs}`);
}

MIDI アレンジを鳴らすには、レンダリング時に楽器をバインドします。音声をレンダリングする内蔵シンセサイザーSoundFont プレイヤーを参照してください。コントローラからプロジェクトをライブ演奏するには、MIDI 入力を参照してください。

キャプチャした MIDI ストリームをルーティングする

Project.midiRouteEvents(events, config?) は静的ヘルパーで、キャプチャした ProjectMidiEvent ストリームをネイティブの MidiRouter(フィルター/リマップ/チャンネルスルー)——ライブランタイムが使うものと同じルーター——に通し、ProjectMidiRouteResult を返します。録音した入力をクリップにする前に、オフラインで事前フィルターやリマップを行う用途に使えます。

typescript
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(filterGroupfilterChannelremapChannelthru)です。フィルターフィールドが null または省略なら「任意」を意味し、remapChannel を省略するとチャンネルは変更されません。Python では snake_case(filter_groupfilter_channelremap_channelthru)です。このヘルパーは WASM・Node・Python すべてで利用できます。オフラインの MIDI ラーン(Project.midiCcLearnMIDI 入力で解説)と組み合わせて使えます。

MIDI-FX チェーンをクリップに焼き込む

MIDI-FX チェーン(トランスポーズ、ベロシティカーブ、ヒューマナイズなど)は通常、クリップのイベントに重なる非破壊のレイヤーとして働きます。bakeMidiFx はその逆で、チェーンを 1 回実行し、その結果でクリップに保存された MIDI イベントを書き換えます。これにより変換後のノートがクリップの実体になります。エフェクトをアレンジに固定したいときは焼き込み、まだ調整したいときは非破壊のままにしておきます。

typescript
const configJson = JSON.stringify({ transpose_semitones: 12 }); // 1 オクターブ上げる
project.bakeMidiFx(midiClip, configJson);                        // イベントがその場でトランスポーズされる

編集時の選択範囲や注釈を焼き込み後にも対応付けたいときは、リクエスト形式を使います。sourceIndex は正規順に並ぶ変換後の各イベントに対応する入力イベントの index です。入力に由来しないイベントは -1 で、コードやアルペジエーターのように 1 つの入力から複数のイベントが出る場合は同じ index が複数回現れます。

typescript
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) を使います。

config は JSON オブジェクトで、各ステージはそのパラメータをキーにします。ステージのキーを含めれば有効になり、省けばスキップされます。未知のキーは無視されるため、打ち間違いは静かに何もしません。

ステージキー
トランスポーズtranspose_semitones
ベロシティカーブvelocity_scalevelocity_offsetvelocity_gamma(>0)
クオンタイズquantize_ppq(>0)、quantize_strength(0–1、既定 1)
コードchord_intervals(半音オフセットの配列、1〜8 要素)
アルペジエーターarpeggiator_intervals(半音オフセットの配列、1〜16 要素)、arpeggiator_step_ppq(>0)、arpeggiator_gate_ppq(既定はステップ長で、それに丸められる)

chord_intervals は 8 要素、arpeggiator_intervals は 16 要素が上限です。空配列、またはいずれかの上限を超える配列を渡すと、bakeMidiFx は黙って切り詰めるのではなく SONARE_ERROR_INVALID_PARAMETER を投げます。

typescript
// 押さえた各ノートを 3 ステップの上昇アルペジオにする(1 ステップ 16 分音符)。
project.bakeMidiFx(midiClip, JSON.stringify({
  arpeggiator_intervals: [0, 4, 7],
  arpeggiator_step_ppq: 0.25,
  arpeggiator_gate_ppq: 0.2,
}));

書き換えは破壊的ですが、ほかの編集と同様にアンドゥ可能です。undo() で元のイベントに戻ります。

自動テンポとグリッドスナップ

編集を拍に合わせる 2 つのヘルパーがあります。

  • autoTempo(audio, sampleRate) はモノラルバッファからテンポを検出し、テンポマップとして設定し、主要な BPM を返します。
  • snapToGrid(ppq, strength) は PPQ 座標をプロジェクトグリッドの最近接拍へスナップします。strength0..1(1 で完全にスナップ)です。
typescript
const bpm = project.autoTempo(monoMix, 48000); // テンポを検出して設定し、約 120 を返す
const snapped = project.snapToGrid(1.2, 1.0);  // 1.2 -> 1(最近接拍)

アレンジをコンパイルする

compile() は編集済みプロジェクトを再生可能なタイムラインへ変換し、構造化された診断を報告します。エラー(重大度 0)はタイムラインを構築できなかったことを意味し、警告(重大度 1)は致命的でなく、タイムラインは依然として再生可能です。

typescript
const result = project.compile();
// result.hasTimeline     -> エラーなしで再生可能なタイムラインが生成されたとき true
// result.diagnosticCount -> 診断の数
// result.diagnostics     -> [{ code, severity, targetId, message }, …]
// result.messages        -> 改行で連結した人間可読の詳細

if (!result.hasTimeline) {
  for (const d of result.diagnostics) {
    if (d.severity === 0) console.error(`コンパイルエラー (clip/track ${d.targetId}): ${d.message}`);
  }
}

よくある致命的でない警告として、MIDI クリップを含むが楽器がバインドされていないプロジェクトは正常にコンパイルされますが、無音でバウンスされます。バウンス後に、そのレンダリングが生成した警告を lastBounceCompileResult() で読めます。

typescript
project.bounce({ numChannels: 2 });
const last = project.lastBounceCompileResult();
// last.diagnostics[0].message ->
//   "project contains MIDI clips; bounce is silent unless an instrument is bound"  (重大度 1)

Python では project.compile() が同じ形(has_timelinediagnostic_countdiagnosticsmessages)を返します。

保存と読み込み: 決定的な JSON

toJson() はプロジェクト全体(トラック、クリップ、MIDI の内容、ループクロスフェード、テンポマップ、拍子、マーカー、注釈、ワープマップ、オートメーション)を決定的な JSON にシリアライズします。同じプロジェクトは常にバイト単位で同一のテキストになります。Project.fromJson(...) で復元します。ループクロスフェードは 0 のときフィールドを省略するため、従来のハードループプロジェクトは同じ JSON 形状を保ちます。

typescript
const json = project.toJson();
// … `json` をディスク・データベース・postMessage に保存 …

const restored = Project.fromJson(json);
try {
  // restored.toJson() === json
} finally {
  restored.delete();
}

致命的でない読み込み警告(たとえば修復のために保持された宙ぶらりんのソース参照)を取得したい場合は Project.fromJsonWithDiagnostics(json) を使います。

typescript
const { project: loaded, diagnostics } = Project.fromJsonWithDiagnostics(json);
try {
  if (diagnostics) console.warn(diagnostics);
} finally {
  loaded.delete();
}

Python では project.to_json()Project.from_json(json)Project.from_json_with_diagnostics(json) が対応します。

モデルを読み戻し、読み込み後に音声を再バインドする

プロジェクト JSON が保存するのはアレンジであって PCM ではありません。そのため読み込んだプロジェクトはソースの存在は分かっていても、その実体となるサンプルを持っていません。読み取り専用のディスクリプタ 3 系統と PCM/ソースメタデータのセッターで、この輪を閉じます。

typescript
const loaded = Project.fromJson(json);

for (let i = 0; i < loaded.trackCount(); i++) {
  const track = loaded.trackByIndex(i);      // { id, kind, midiDestinationId, gain, pan, mute, solo, name }
  console.log(track.id, track.name);
}
for (let i = 0; i < loaded.clipCount(); i++) {
  const clip = loaded.clipByIndex(i);        // { id, trackId, sourceId, startPpq, lengthPpq, … }
  console.log(clip.id, clip.startPpq, clip.lengthPpq);
}
const unresolvedAudioIds = new Set(loaded.unresolvedAudioSourceIds());
for (let i = 0; i < loaded.sourceCount(); i++) {
  const source = loaded.sourceByIndex(i);    // { id, kind, channelCount, sampleRateHint,
                                             //   nameOrUri, contentHash, externalStemRole }
  if (source.kind !== 0 || !unresolvedAudioIds.has(source.id)) continue; // 0 = audio、MIDI は除外
  const pcm = await decodeFromYourStorage(source.nameOrUri);
  loaded.setSourceAudio(source.id, pcm, source.channelCount, source.sampleRateHint);
  loaded.setAudioSourceMetadata(source.id, 'sha256:...', 'lead-vocal');
}

const audio = loaded.bounce({ sampleRate: 48000 });

trackByIndex / clipByIndex / sourceByIndex は保存順に対する 0 始まりのインデックスで、 trackCount() / clipCount() / sourceCount() と対になります。返るのはハンドルではなく ディスクリプタなので、返り値を書き換えても何も変わりません。ホストがディスクから読み込んだ プロジェクトをレンダリングしたいときや、自前のコードが構築したのではないプロジェクトに対して UI を組むときに使います。

setSourceAudio(sourceId, samples, channels, sampleRate) は、バウンス前にデコード済み PCM を ソースへ再バインドします。「読み込んだだけのアレンジ」を「レンダリングできるプロジェクト」に 変えるのがこの一手です。

unresolvedAudioSourceIds() は、デシリアライズ後もデコード済み PCM が必要なソース ID の公開リストです。ディスクリプタを走査するときの上の kind !== 0 ガードは防御的なもので、0 がオーディオ、1 が MIDI です。MIDI ソースにはバインドする PCM も更新するソースメタデータもありません。オーディオソースのディスクリプタには所有メタデータ contentHashexternalStemRole もあり、MIDI ソースでは空文字列です。setAudioSourceMetadata(sourceId, contentHash, externalStemRole) は両方の文字列を 1 つのアンドゥ可能な編集として置き換え、空文字列で個別にクリアできます。WASM はこの位置引数形式を使い、Node は第 2 引数に { contentHash, externalStemRole } のオブジェクトも受け付けます。Python は set_audio_source_metadata(source_id, content_hash, external_stem_role)(C ABI は sonare_project_set_audio_source_metadata)です。Python には unresolved_audio_source_ids() があり、ソースディスクリプタの名前は content_hashexternal_stem_role です。C の getter が返すヒープ文字列は対応する free 関数で解放します。

ホスト側で分離したステムを取り込む

アプリ側ですでに音源分離を済ませている場合(あるいは単に楽器ごとの WAV がある場合)、 importExternalStems はそれらを 1 トランザクションで、それぞれ 1 本のオーディオトラックと クリップに変換します。

typescript
const { trackIds, clipIds } = project.importExternalStems({
  sampleRate: 48000,
  stems: [
    { name: 'vocals', layout: 'stereo', planarSamples: [vocalL, vocalR], startFrame: 0 },
    { name: 'drums',  layout: 'stereo', planarSamples: [drumL, drumR],   startFrame: 0 },
    { name: 'bass',   layout: 'mono',   planarSamples: [bassMono],       startFrame: 0, role: 'bass' },
  ],
});

取り込みは全件成功か全件失敗です。1 つでも拒否されればプロジェクトは中途半端に埋まらず、 まったく変更されません。リサンプリング・タイミング調整・ゲイン補正は一切行いません。 すべてのステムはあらかじめ sampleRate に揃っている必要があり、startFrame はそのままの値で プロジェクトのタイムライン上に配置されます。ステムごとの任意の role はホスト用メタデータで、 シリアライザーを往復しますが DSP には影響しません。

MIDI 交換: SMF と MIDI 2.0 クリップファイル

プロジェクトのテンポマップと MIDI クリップは 2 つの形式で往復できます。

標準 MIDI ファイル (SMF)

exportSmf は常にフォーマット 1(マルチトラック)で書き出します。トラック 0 がテンポ/拍子マップを、以降は各クリップが 1 つの MTrk となり、4 分音符あたり 480 ティックに量子化されます。

typescript
const smf = project.exportSmf();        // Uint8Array — SMF フォーマット1、480 PPQN
// … `smf` を .mid ファイルへ書き出す …

const fresh = new Project();
try {
  const firstClip = fresh.importSmf(smf); // 最初に追加されたクリップ ID を返す
} finally {
  fresh.delete();
}

インポーターは破損をトラック内に封じ込めます。ある SMF トラックの可変長数値やペイロードが宣言境界を越えていても、そのトラック末尾へ再同期するため、ファイル全体を失敗させず後続の正常なトラックを引き続き取り込めます。

SMF が往復させているのは 演奏 です。それを記譜すれば、その同じノートのリストが楽譜になります。下の大譜表は MIDI クリップの記譜ビューです。再生すると、そこに保存されたイベントが鳴ります。

MIDI · SCOREIDLE
同じ MIDI を、楽譜で読む

上のピアノロールとまったく同じ3声フレーズ(旋律・分散和音・低音)を、大譜表に標準記譜したものです。音符・音価・連桁・音部記号は、エンジンが鳴らす MIDI からそのまま起こしています。楽器を切り替えれば同一の音符が別の内蔵音色で鳴り、テンポを動かせば演奏が速くなります。再生すると、鳴っている音符が鳴った瞬間に光ります。

楽器
テンポ
96 BPM

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

SMF は MIDI 2.0 より前の形式なので、16 ビットベロシティ・32 ビット CC・パーノートコントローラ・バンク有効なプログラムチェンジを欠落なく運べません。MIDI 2.0 クリップファイルSMF2CLIP)はこれらすべてを保持します。MIDI 2.0 の忠実度が重要なときはこちらを選んでください。

typescript
const clipFile = project.exportClipFile();   // Uint8Array、"SMF2CLIP" ヘッダ
const firstClip = otherProject.importClipFile(clipFile);

Python ではこれらが export_smf / import_smfexport_clip_file / import_clip_file で、bytes を返し受け取ります。

音声をレンダリングする

編集はタイムラインを生み、レンダリングはそれをサンプルへ変換します。Projectbounce(...)(オーディオトラックのみ)か、MIDI トラックを鳴らす楽器バインド付きバウンス(bounceWithBuiltinInstrumentbounceWithSynthInstrumentbounceWithSf2Instrument)でオフラインバウンスします。レンダーオプション一式、楽器バインド、SoundFont 読み込み、バウンスが報告する診断は プロジェクトバウンス & レンダリング で扱います。

typescript
// オーディオのみの簡易レンダー。ここでは MIDI トラックは無音です。
const audio = project.bounce({ numChannels: 2 });

アレンジがエラーなくコンパイルできたら、次は MIDI トラックを鳴らすことも含めて音声へ変換する番です。プロジェクトバウンス & レンダリングへ進んでください。

関連