Skip to content

プロジェクトのコンパイル・保存・読み込み ​

プロジェクトには 3 つの境界があります。compile() はアレンジをレンダリング可能なタイムラインへ変換できるか確認します。toJson() / fromJson() はアレンジモデルを保存します。読み込みは PCM を復元しないため、バウンス前にデコード済み音声を再バインドします。ホスト側で分離したステムは、通常のオーディオトラックとクリップを作る全件成功または全件失敗のインポートで追加します。

目的API
アレンジを確認するproject.compile()
モデルを保存するproject.toJson() と Project.fromJson(...)
読み込み警告を回収するProject.fromJsonWithDiagnostics(...)
デコード済み音声を戻すunresolvedAudioSourceIds() と setSourceAudio(...)
分離済みステムを追加するimportExternalStems(...)
コンパイル、保存、再バインド、レンダリング
Project の編集compile()決定的な JSONfromJson()PCM を再バインドバウンス
保存したモデルはアレンジを復元します。レンダリング前にホストがデコード済み PCM をもう一度渡します。

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

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_timeline・diagnostic_count・diagnostics・messages)を返します。

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

toJson() はプロジェクト全体(トラック、クリップ、MIDI の内容、ループクロスフェード、テンポマップ、拍子、マーカー、注釈、ワープマップ、オートメーション、パートリグの選択)を決定的な JSON にシリアライズします。同じプロジェクトは常にバイト単位で同一のテキストになります。Project.fromJson(...) で復元します。ループクロスフェードは 0 のときフィールドを省略するため、ハードループのプロジェクトは簡潔な JSON 形状を保ちます。読み込みには 1 からサポートする 5 までの整数 version が必要です。

シリアライザーはモデルに必要な最も低いスキーマを選びます。型付きフィールドや物理フィールドがないプロジェクトはスキーマ 1 を使います。型付きオートメーションレーンがあるとスキーマ 2 になり、クリップまたはテイクに秒単位の物理ソースオフセット、またはクリップにループアンカーがあるとスキーマ 3 になります。コンプのレンダーパーツにはスキーマ 4 を使い、パートリグが 1 件以上あるとスキーマ 5 を使います。物理ソースオフセット、ループアンカー、コンプのレンダーパーツ、パートリグは JSON/ネイティブモデルのデータです。現行 JavaScript サーフェスには最初の 3 種類のセッターがないため、プロジェクトを交換するときは JSON/ネイティブモデルを通して保持してください。これらのフィールドがないプロジェクトは下位スキーマと既存の内容を保ちます。

パートリグは destination_id と part をキーにして part_rigs へ保存します。part は 0〜15 またはデスティネーション全体を表す 255(PART_RIG_ALL_PARTS)です。各エントリのモードは bank、none、chain のいずれかです。chain には 1〜8 個の { processor, params } オブジェクトを入れ、params は JSON オブジェクトを表す文字列として保存します。Project.setPartRig(...) は、そのデスティネーションへトラックがまだルーティングされていなくてもエントリを保存し、Project.clearPartRig(...) は削除します。プロジェクトのバウンスでは SoundFont と物理モデルのデスティネーションへ適用されます。SMF の書き出しには含まれません。

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) が対応します。

読み込めない文書は、どのサーフェスでも C ABI の InvalidFormat コードで失敗し、メッセージは診断名で始まります。WASM と Node は SonareError を投げ、Python は同じ code を持つ SonareValueError を送出します。Python CLI も、ネイティブ CLI と同様に、壊れたプロジェクト文書では終了コード 5 で終了します。

診断名原因致命的か
malformed_jsonテキストが JSON オブジェクトではないはい
missing_versionversion がない、または数値ではないはい
invalid_versionversion が 0、または 0 以上の整数ではないはい
unsupported_schema_versionversion がサポートする 5 より新しいはい
invalid_sample_ratesample_rate が 8000〜384000 の整数ヘルツではないはい
invalid_formatコンテナまたは配列要素の型が違う(例: tracks が配列でない、clips の要素がオブジェクトでない)。メッセージにそのパスが入るはい
invalid_clip_take_structureクリップのテイクが id を重複させる、または id 0 を使う、active_take_id がどのテイクも指さない、MIDI トラックにテイクがある、コンプセグメントが重なる、クリップ終端を超える、またはクロスフェードが負はい
unknown_scene_key埋め込まれたミキサーシーンのうち、シーンリーダーが消費しないキーいいえ。diagnostics に返る

unknown_scene_key の警告は、ドキュメントのルートからのパス付き(例: scene.strips[0].faderDB)で報告されます。$ または x- で始まるキーは対象外です。プロジェクト文書のそれ以外の部分は、未知のキーを検査しません。単体のシーンに対する同じ検査はミキシングシーン JSONを参照してください。

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

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

JSON が保持するもの
オーディオトラックテイク 1テイク 2テイク 3コンプMIDI トラックオートメーションレーン小節 1小節 2小節 3小節 40481216PPQ 単位の位置 · 1.0 = 4 分音符 1 つクリップ startPpq 0 · lengthPpq 8アクティブテイク(空白区間を埋める)テイク 1テイク 3テイク 2テイクを選ぶだけ(コピーはしない)MIDI クリップ
  • テイク 1
  • テイク 2
  • テイク 3
JSON が保持するのは、トラック、クリップ、テイク、オートメーションからなるこのアレンジです。オーディオクリップの下にある音声は含まれず、ホストがデコードして戻します。
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, … }, 4 分音符単位
  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 });
python
loaded = sonare.Project.from_json(json_text)

for i in range(loaded.track_count()):
    track = loaded.track_by_index(i)
    print(track.id, track.name)
for i in range(loaded.clip_count()):
    clip = loaded.clip_by_index(i)
    print(clip.id, clip.start_ppq, clip.length_ppq)

unresolved = set(loaded.unresolved_audio_source_ids())
for i in range(loaded.source_count()):
    source = loaded.source_by_index(i)
    if source.kind != 0 or source.id not in unresolved:  # 0 = audio、MIDI は除外
        continue
    pcm = decode_from_your_storage(source.name_or_uri)
    loaded.set_source_audio(source.id, pcm, source.channel_count, int(source.sample_rate_hint))
    loaded.set_audio_source_metadata(source.id, "sha256:...", "lead-vocal")

audio = loaded.bounce(sample_rate=48000)

コマンドラインからは sonare project bounce --resolve-audio が、ソースの持つ file:// URI を開きます(CLI からバウンスするを参照)。

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

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

startPpq と lengthPpq は 4 分音符を単位とする位置と長さ(1.0 が 4 分音符 1 つ分)で、MIDI のティックではありません。ティックと PPQ と クリップとトラック を参照してください。

sampleRate 引数には渡した PCM の実際のレートを指定するので、デコード後のレートを渡してください。保存されている sampleRateHint はヒントにすぎず、不明なときは 0 です。プロジェクトのサンプルレートと異なる場合は、コンパイル時にソースがプロジェクトのレートへリサンプリングされます。bounce({ sampleRate }) は別です。リサンプリングせず、プロジェクトのレート(上の例では 48000)と一致している必要があります。後述のステム取り込みもリサンプリングしません。

unresolvedAudioSources()(Python は unresolved_audio_sources())は、同じ順序でそれらのソースの完全なディスクリプタを返すので、ローダーは追加の問い合わせなしに各 nameOrUri を読めます。sourceByIndex とディスクリプタは URI を切り詰めずに返します。

unresolvedAudioSourceIds() は、デシリアライズ後もデコード済み PCM が必要なソース ID の公開リストです。ディスクリプタを走査するときの上の kind !== 0 ガードは防御的なもので、0 がオーディオ、1 が MIDI です。MIDI ソースにはバインドする PCM も更新するソースメタデータもありません。オーディオソースのディスクリプタには所有メタデータ contentHash と externalStemRole もあり、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_hash と external_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 には影響しません。

レンダリングへ進む ​

プロジェクトにタイムラインがあり、必要なオーディオソースをすべてバインドできたら、プロジェクトのバウンスへ進みます。このページでは楽器のバインドやレンダーオプションの詳細を重複して説明しません。

関連 ​