Skip to content

リアルタイムとストリーミング

libsonare には、リアルタイム用途の API が 2 系統あります。

  • StreamAnalyzer — 音声ブロックを入力し、解析フレーム(メル、クロマ、オンセット、スペクトル)と逐次的な音楽推定(BPM、キー、コード、コード進行、パターン)を出力します。ビジュアライザーや「今この曲が何をしているか」のライブ表示に使います。
  • RealtimeEngine — トランスポート、オートメーション、クリップ再生、トラックごとのレーンミキサー(レーン、バス、センド、チャンネルストリップ)、MIDI クリップスケジューリング、グラフルーティング、メトロノーム、キャプチャ、オフラインバウンス、フリーズ、テレメトリ。再生エンジン向けです。

このページでいうチャンクブロックは、AudioWorklet などで繰り返し処理する短い音声片のことです。AudioWorklet とは、メイン/UI スレッドとは別の、リアルタイム音声スレッド上で DSP を動かす音声コールバックの実行環境です。リアルタイム処理では、音声コールバック内で重いアロケーションを避けるのが基本です。先にオブジェクトを準備し、コールバックではブロック処理だけを行います。

チャンク・ブロック・フレームの違い

チャンクブロックは、リアルタイム入力から届く短い音声サンプルのまとまりです。フレームは、その音声ブロックを解析して得た時間単位の特徴量です。入力はブロック、UI に描く結果はフレーム、と分けて考えると混乱しにくくなります。

サンプルレートとリサンプリング

サンプルレートは 1 秒が何サンプルでできているか(44100 や 48000 が一般的)です。ストリームのレートがアナライザの想定と一致しないと、処理前に別のレートへ作り直すリサンプリングが必要になり、余分な CPU を消費します。最初からレートを揃えておくほうが高速です。

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

このページを読むと、次のことを判断・実装できるようになります。

  • アプリに StreamAnalyzerRealtimeEngine、ミキシングエンジンのどれが必要かを選べる。
  • 圧縮ファイルのバイト列、デコード済みサンプル、ブロック、フレームを混同せずに音声を渡せる。
  • UI 用の特徴フレームを読み出し、量子化読み出しがなぜあるかを理解できる。
  • 逐次 BPM/キー/コード推定を、初期値から最終結果のように扱わずに使える。
  • 音声コールバック内で安全な処理と、事前準備すべき処理を区別できる。

初学者向けの選び方

作りたいもの最初に使うもの
スペクトログラム、クロマ、オンセット強度、ライブ BPM/キー推定を描くビジュアライザーStreamAnalyzer
ライブのコード/進行/パターン表示StreamAnalyzer.stats()
テンポ、ループ、マーカー、メトロノーム、クリップ、オートメーションを持つ再生エンジンRealtimeEngine
再生するトラックをそのままライブでミックスする再生エンジン(レーン、バス、センド、ストリップ)RealtimeEngineレーンミキサー
単体のミキサー(ワンショットまたはシーン駆動、ストリップ/センド/メーター付き)ミキシングエンジン
単純なオフラインスクリプトこのページではなく はじめに

どちらを使うか

やりたいことAPI
マイク入力や再生中のファイルからメル/クロマ/オンセットのフレームを取り出すStreamAnalyzer
BPM、キー、現在のコード、コード進行、パターンスコアを逐次推定するStreamAnalyzer.stats()
トランスポート、テンポ、ループ、マーカー、メトロノーム、クリップ、オートメーションを扱うRealtimeEngine
再生エンジン内でトラックごとのレーン、バス、センド、チャンネルストリップを扱うRealtimeEngine のレーンミキサー(setTrackLanessetTrackBuses、ストリップ JSON セッター)
エンジン内の楽器へサンプル精度で MIDI クリップを再生するRealtimeEngine.setMidiClips() + sampleAtPpq()
テレメトリ付きの AudioWorklet ブリッジを作るRealtimeEngine または軽量な sonare-rt モジュール
センドやメーター付きでステム/ストリップをミックスするミキシングエンジン

ランタイムごとの入口

このページの中心はブラウザ / WASM の StreamAnalyzerRealtimeEngine、AudioWorklet ブリッジです。Python と CLI は「ライブの音声コールバック内で状態を保つ」ための同一 API ではなく、ファイルや配列をまとめて処理するバッチ API が主な入口です。

実行環境使う入口典型的な用途
ブラウザ / WASMStreamAnalyzerRealtimeEngine@libraz/libsonare/workletライブ可視化、AudioWorklet、逐次 BPM / キー / コード表示
PythonAudio.analyze()onset_envelope(...)tempogram(...) などのバッチ関数ノートブック、オフライン解析、検証用スクリプト
CLIsonare analyzesonare bpmsonare key などファイル単位の確認、バッチ処理、JSON 出力

Python / CLI で同じファイルを解析したい場合は Python APICLI リファレンス を使ってください。音声コールバック内で動かす実装例は、WASM / Worklet 側のコードだけを正として扱います。

StreamAnalyzer

StreamAnalyzer は音声ブロックを処理し、UI 描画用のフレームバッファを出力します。スペクトログラム、クロマ表示、オンセット駆動のビジュアル、逐次的な音楽推定に向いています。一度構築し、受け取ったブロックごとに process() し、たまったフレームを読み出します。

メル・クロマ・オンセットを一言で

  • メル — 時間ごとの周波数帯域エネルギーを、知覚的な音高スケールで並べたスペクトログラムです。「どんな音か」を示すヒートマップに向きます。
  • クロマ — エネルギーを 12 の音高クラス(C、C#、… B)へ畳み込んだものです。ハーモニーやキーの表示に向きます。
  • オンセット — 新しいノートやビートが始まると跳ね上がる強度曲線です。ビート/テンポの可視化を駆動します。

nFfthopLength を一言で

アナライザは内部で STFT を実行します。

パラメータ意味値を大きく/小さくしたとき
nFft解析窓のサイズ(サンプル数)大きいほど周波数は細かく、時間は粗くなります
hopLengthフレーム間で窓を進める量小さいほど 1 秒あたりのフレームが増え、CPU 負荷も増えます

下記の 2048/512 は一般的な出発点です。なじみがなければ MIR の全体像 を参照してください。

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

await init();

const analyzer = new StreamAnalyzer({
  sampleRate: audioCtx.sampleRate,
  nFft: 2048,
  hopLength: 512,
  nMels: 64,
  computeMel: true,
  computeChroma: true,
  computeOnset: true,
  emitEveryNFrames: 4,   // スロットル: 4 ホップごとに 1 フレーム出力
});

analyzer.process(inputBlock);

const frames = analyzer.readFrames(analyzer.availableFrames());
const stats = analyzer.stats();

if (stats.estimate.updated) {
  console.log(stats.estimate.bpm, stats.estimate.key, stats.estimate.chordRoot);
}

ストリームの既定値はバッチ解析と異なる

StreamAnalyzer の既定サンプルレートは、バッチの 22050 Hz ではなく 44100 Hz です。

リアルタイム音声は再生/キャプチャグラフ(AudioWorklet、デバイスコールバック)から直接届き、ほぼ常に 44100/48000 Hz で動きます。そのレートに合わせるとホットパスでの余分なリサンプルを避けられ、タイムスタンプが音声クロックと揃います。

sampleRate: audioCtx.sampleRate を渡し、推定値とタイムスタンプを実際に再生している音と揃えてください。

フレームの読み出しと出力フォーマット

FrameBufferStructure-of-Arrays です。タイムスタンプ、メル、クロマ、オンセット強度、RMS、スペクトル重心、スペクトル平坦度、コードルート、コードクオリティ、コード信頼度が、それぞれ独立した型付き配列に入ります。この形はスライスも別スレッドへの受け渡しも安価です。

スペクトル重心・平坦度とは?

どちらも 1 フレームのスペクトルのを 1 つの数値にまとめたもので、プロットやしきい値処理に使えます。

指標意味読み方
スペクトル重心エネルギーで重み付けした平均周波数高いほど「明るい」(高域成分が多い)音に聞こえます
スペクトル平坦度エネルギーが周波数全体にどれだけ均等に広がっているか1 に近いとノイズ的、0 に近いと音程的です

組み合わせると、フレームごとの音色を手軽に記述できます。

スレッド間転送や可視化では、しばしば完全な float 精度は不要です。StreamAnalyzer は特徴量配列を量子化し、精度と帯域を引き換えにできます。

読み出しメソッド要素型outputFormat用途
readFrames(n)Float32Array / Int32Array フィールドを持つ FrameBuffer0(既定)完全精度の DSP、さらなる解析
readFramesI16(n)Int16Array フィールドを持つ StreamFramesI161ワーカー/回線への帯域削減転送
readFramesU8(n)Uint8Array フィールドを持つ StreamFramesU82安価な可視化(ヒートマップの 1 画素は 8 ビットで足りる)
typescript
// スペクトログラム描画には 8 ビットのメルで十分 — 出力時点で量子化する。
const analyzer = new StreamAnalyzer({ sampleRate, nMels: 64, outputFormat: 2 });
analyzer.process(block);
const u8 = analyzer.readFramesU8(analyzer.availableFrames());
// u8.mel は Uint8Array [nFrames x nMels]、ImageData にそのまま書き込める

フォーマットは解析側ではなく消費側に合わせる

outputFormatreadFramesU8readFramesI16 が出力時にどう量子化するかを変えるだけで、内部解析は浮動小数点のままです。データが最終的に画素になるなら Uint8、スレッド/ネットワーク境界を越えてバイト数をおよそ半分にしたいなら Int16、下流でさらに計算するなら既定の Float32 を選びます。

マグニチュードフレームは読み出し経路を持たない

StreamAnalyzer はフレームごとのマグニチュードスペクトルを公開しません。readFrames* に対応するフィールドが無いため、コンストラクタは黙って無視するのではなく computeMagnitude: true を拒否します。スペクトログラム表示にはメルを使い、生の単一フレーム FFT が必要なときはバッファした窓に対して meteringSpectrumFrame(...) を実行してください。

量子化レンジのカスタマイズ

量子化読み出しは、いずれもオプションの StreamQuantizeConfig を第 2 引数に取ります。既定値は「ふつう」の信号を前提にしているため、それより大幅に大きい/小さいストリームは、量子化後に全 255 へ飽和したり 0 へつぶれたりします。8 ビット/16 ビットの圧縮で見える情報が残るよう、レンジを広げてください。

typescript
// 入力レベルが高いライブ入力: メルの下限を上げ、オンセット/RMS の上限を引き上げる。
const u8 = analyzer.readFramesU8(analyzer.availableFrames(), {
  melDbMin: -60,    // 既定 -80。大音量ストリームでは下限を上げる
  melDbMax: 0,      // 既定 0
  onsetMax: 80,     // 既定 50。強いトランジェントのクリップを避ける
  rmsMax: 1.5,      // 既定 1
  centroidMax: 11025,
});

同じ StreamQuantizeConfigreadFramesI16(...) にも使えます。引数を省くと既定値のままです。変わるのは出力の写像だけで、内部の浮動小数点解析には影響しません。

逐次推定: BPM、キー、コード、パターン

stats()AnalyzerStats を返し、その estimate フィールドが ProgressiveEstimate です — 音声が届くほど精緻になる、解析器の音楽に対する逐次的な最良推定です。読む前に estimate.updated を確認してください。推定が実際に変化したフレームでのみ true になるので、無駄な UI 更新を避けられます。

typescript
const { estimate } = analyzer.stats();
if (estimate.updated) {
  // テンポとキー(信頼度つき)
  estimate.bpm;            estimate.bpmConfidence;
  estimate.key;            estimate.keyMinor;  estimate.keyConfidence;

  // いま鳴っているコード
  estimate.chordRoot;      // PitchClass(数値 enum、0 = C)
  estimate.chordQuality;   // ChordQuality(数値 enum)
  estimate.chordConfidence;
  estimate.chordStartTime;

  // ここまでの進行
  estimate.chordProgression;     // ChordChange[]  { root, quality, startTime, confidence }
  estimate.barChordProgression;  // BarChord[]     拍同期、1 小節に 1 つ

  // パターン検出(例: I–V–vi–IV のループ)
  estimate.detectedPatternName;  // 最も一致する既知の進行
  estimate.detectedPatternScore;
  estimate.allPatternScores;     // PatternScore[] { name, score }
  estimate.votedPattern;         // BarChord[] ロックされた反復パターン
  estimate.patternLength;        // そのパターンの小節数
  estimate.currentBar;           // estimate.barDuration -> 小節位置/長さ
}

コード出力が 2 層あるのは、答えている問いが違うためです。

フィールド意味
chordProgression検出されたコード変化を、そのまま時系列で並べたもの。
barChordProgressionコードを小節境界にそろえたもの。譜面やチャートとして読みやすくなります。

その上で、パターン検出が小節をまたいで投票し、反復する進行を認識します(votedPattern / detectedPatternName)。これにより、フレームごとにちらつく表示ではなく、曲全体の進行として落ち着いた表示になります。

想定長を与えてパターンをロックしやすくする

パターン投票には十分な小節数が必要です。クリップ長が事前に分かるなら analyzer.setExpectedDuration(seconds) を呼ぶと、タイミングとパターンロックが正しくスケールされます。分からなければ音声が流れるほど推定が精緻になります。非標準チューニングでは analyzer.setTuningRefHz(refHz) でキー/コードの基準を A4 = 440 Hz からずらせます。

オンセット包絡からテンポグラムへ

StreamAnalyzer はライブのオンセット強度ストリームを返します。各フレームの onsetStrength 配列がそれです。

テンポグラムは、オンセット包絡を時間 × テンポの画像に変換します。各時点で、候補となる各テンポがどれだけ強く存在するかを示すものです。逐次 BPM 推定は、この画像を時間方向に最も強いテンポへとつぶしたものに相当し、テンポグラムはその推定の元になる全体像です。

蓄積した包絡、または onsetEnvelope(...) で得た任意のオンセット包絡から計算します。これは音声コールバック内で毎回実行する処理ではなく、バッファした窓に対するバッチ処理です。

typescript
import { init, onsetEnvelope, tempogram, fourierTempogram, cyclicTempogram, tempogramRatio, plp } from '@libraz/libsonare';

await init();

const env = onsetEnvelope(samples, sampleRate, 2048, 512);

const ac  = tempogram(env, sampleRate, 512, 384, 'autocorrelation'); // 既定
const cos = tempogram(env, sampleRate, 512, 384, 'cosine');
const ft  = fourierTempogram(env, sampleRate, 512, 384);
const cyc = cyclicTempogram(env, sampleRate, 512, 384, 60, 60);
const ratio = tempogramRatio(ac.data, 384, sampleRate, 512);
const pulse = plp(env, sampleRate, 512, 30, 300, 384);
関数計算するもの戻り値
tempogram(..., 'autocorrelation')オンセット包絡の局所自己相関(librosa 既定){ nFrames, winLength, data }
tempogram(..., 'cosine')ラグ付きオンセット片どうしの窓内コサイン類似度{ nFrames, winLength, data }
fourierTempogram(...)オンセット包絡の STFT(フーリエテンポグラム){ nBins, nFrames, data }
cyclicTempogram(...)オクターブ畳み込みしたテンポクラス(60・120・240 BPM が同一になる){ nBins, nFrames, data }
tempogramRatio(...)テンポグラムからのテンポ比特徴量Float32Array
plp(...)主要局所パルス曲線Float32Array
自己相関テンポグラムとコサインテンポグラム

既定の自己相関テンポグラムは、オンセット包絡をラグ付きのコピーと相関させ、librosa.feature.tempogram を再現します。コサインモードは代わりに、窓内のラグ付きオンセット片どうしのコサイン類似度を測ります。コサインはオンセットの生エネルギーよりパターンのの一致を強調するため、窓内でオンセット振幅が大きく変動する場合に安定しやすいことがあります。どちらも row i がラグ i の強度である [winLength x nFrames] 行列を生成します。第 5 引数 mode'autocorrelation' | 'cosine')で切り替えます。

DETECTOR · ONSET / BEATIDLE
オンセットとビート — 打点から拍へ

オンセット検出は音の打点をすべて捉え、ビート追跡はそこから手拍子を打つような一定の拍を導き出す。表示を切り替え、再生するとプレイヘッドが到達するたびにマーカーが光る。

検出

RealtimeEngine

RealtimeEngine は、より広い意味でのトランスポート/再生エンジンです。パラメータやトランスポートに対するサンプル精度のコマンドを扱い、非リアルタイム書き出し用のオフラインレンダーも提供します。

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

await init();

const caps = engineCapabilities();
if (!caps.abiCompatible) throw new Error('Realtime engine ABI mismatch');

const engine = new RealtimeEngine(48000, 128);
engine.setTempo(128);
engine.setTimeSignature(4, 4);
engine.setLoop(0, 16, true);
engine.play();

const output = engine.process([leftBlock, rightBlock]);
const transport = engine.getTransportState();
const telemetry = engine.drainTelemetry();

engine.stop();
engine.destroy();

RealtimeEngine はトランスポート以外にも、パラメータ情報の登録、オートメーションレーンの設定、マーカーへのシーク、メトロノームクリックの設定、モニター出力付き処理、キャプチャ、オフラインバウンス、クリップのフリーズも扱えます。UI を組むうえで重要なテレメトリは 2 系統あります。

  • メーター — ステレオ高速経路なら drainMeterTelemetry()、サラウンド/オフライン対象のプレーン別レコードなら drainMeterTelemetryWide() を使います。
  • スコープconfigureScopeTelemetry(intervalFrames, bandCount) を 1 度呼んでターゲットごとのスペクトラム+ベクトルスコープ取得を有効化し、drainScopeTelemetry() でスナップショットを読み出します。
    • intervalFrames — スナップショット間の最小レンダーフレーム間隔(0 で取得を無効化)。
    • bandCount — FFT のバンド分解能。1..64 にクランプされ、実際に適用されたバンド数が戻り値として返ります。

読み出した各スコープスナップショットは targetId(マスター・レーン・バスのいずれか)で識別され、2 本の配列を持ちます。bands は線形バンドの FFT マグニチュード(dB、長さ=適用されたバンド数)、points はベクトルスコープ表示用のインターリーブステレオのゴニオメータ点群 [l0, r0, l1, r1, …](最大 32 ステレオ点)です。バンドのレベルはブロックサイズに依存しません。振幅の正規化が短いブロックを考慮するため、AudioWorklet のブロックサイズによらず dB 値が安定します。

drainMeterTelemetry()drainMeterTelemetryWide()drainScopeTelemetry() が返す各レコードには droppedRecords が含まれます。これは前回のドレイン以降にロックフリーのテレメトリリングから失われたスナップショット数です。値が 0 以外なら、消費側のドレインが追いついておらず(バックプレッシャー)、メーターやスコープがちらつかないようにポーリング頻度を上げる必要があります。

メーターレコードの dB 値を持つレベル/ラウドネスのフィールド(peakDbLRrmsDbLRtruePeakDbLRmaxTruePeakDbmomentaryLufsshortTermLufsintegratedLufs)はすべて −120 dBFS のフロアを持ち、NaN-Infinity を返しません。未初期化・無音・未書き込みのプレーン(例: モノラルレーンの右チャンネル)は 0 dBFS ではなく −120 dBFS を返すので、レコードは常に JSON セーフです(correlationmonoCompatWidthgainReductionDb などの非 dB フィールドは 0 が既定)。積分系メーターのフィールド(momentaryLufsshortTermLufsintegratedLufs と true-peak 系)は、ストリーミングが一定時間続いて初めてフロアより上に上がります。短いレンダーやワンショットのレンダーでは −120 のままです。

スケジュール済みのクリップとシーケンス MIDI が鳴るのは、トランスポートが走っている間だけです。停止中のエンジンでは音が漏れず無音のままです。オフラインヘルパー(renderOfflinebounceOfflinefreezeOffline)はレンダー期間だけトランスポートを走らせ、終了後に元の状態へ戻すので、オフラインのクリップ/MIDI レンダリングに手動の play() は不要です。

手動でオフラインレンダーする場合、つまりこれらのヘルパーを使わず自分で process() を回す場合は、シーク後にまずプライミング用の process() ブロックを 1 回流し(これでキュー済みコマンドが排出され、シーク位置のオートメーションが適用されます)、続いて engine.settleParameters() を呼んで、進行中のあらゆるパラメータランプ(エンジンレベルのスムーズ化パラメータ、ミキサーレーンのフェーダー/パン/ゲート、バスゲイン)をターゲット値へスナップさせてください。これで最初に聴こえるブロックが、既定値からランプインせず確定値でレンダーされます。settleParameters() はライブ音声スレッドと同時に実行してはならず、オフライン/メインスレッド専用です。

typescript
// プライミング: キュー済みコマンドを排出し、シーク位置のオートメーションを適用する。
engine.process([new Float32Array(blockSize), new Float32Array(blockSize)]);
engine.settleParameters(); // 最初に聴こえるブロックの前に、全スムーズ化ランプをターゲットへスナップ

録音まわりでは、キャプチャ面にいくつかのコントロールが加わります。

  • setCaptureSource('output' | 'input') — エンジンのレンダー済み出力バスを録るか、process(...) に渡す生の入力を録るかを選びます。
  • setRecordOffsetSamples(offset) — モニタリングの往復レイテンシを補正するため、キャプチャ音声をずらします。
  • setInputMonitor(enabled, gain?) — 演奏者が自分の音を聞けるように、ライブ入力を出力へミックスします。

captureStatus() は、現在のキャプチャ元 source'output' または 'input')と現在の recordOffsetSamples の両方を返すので、何を録っているかを UI 側で確認できます。全体の流れは 録音とテイク を参照してください。

ライブ MIDI と録音

エンジンは、楽器へのライブ MIDI 入力と、再生されている内容の録音も受け付けます。これらには専用ページがあります。Web MIDI からエンジンへのブリッジ(ポート管理、CC バインド、NativeSynth/SF2 の宛先)は MIDI 入力、キャプチャ・ループ録音のテイク/コンプレーン・getUserMedia をエンジンノードへつなぐブラウザマイクヘルパー bindMicrophoneInput(...)録音とテイク を参照してください。

構築前にエンジン ABI を確認する

engineCapabilities().abiCompatible は、読み込んだ WASM が JS ラッパーの期待するエンジン ABI と一致するかを確認します。リアルタイムエンジンはライブラリ中で最もバージョンに敏感な面で、不一致のバイナリに対して構築すると未定義です。上記のチェックでガードし、失敗したら @libraz/libsonare パッケージを更新して、WASM バイナリと JS ラッパーを同じリリースに揃えてください。

レーンミキサー

エンジンはリアルタイムセーフなレーンミキサーを内蔵しており、再生エンジンが自分の再生するトラックを別のミキシングパスなしでそのままミックスできます。各トラックはレーンを 1 つ占有し、レーンはAuxセンドで番号付きのバスへ送れます。トラック・バス・マスターはそれぞれ EQ、インサート、フェーダー、パン、センドを備えた完全なチャンネルストリップを持ち、これはミキシングエンジンと同じストリップモデルです。レーン構成を再発行するたびに、プラグインのディレイ補正は自動で再計算されます。

typescript
// まずバスを宣言し、次にセンド付きでレーン順を宣言する。
engine.setTrackBuses([{ busId: 1, gainDb: 0 }]);
engine.setTrackLanes([
  { trackId: 1, sends: [{ busId: 1, levelDb: -12, enabled: true }] },
  2, // トラック id だけを書くと、センドなしのレーンを追加する
]);

// ストリップはミキサーシーン JSON を再利用する:
// シーンの最初の strips[0] エントリーがストリップ仕様になる。
engine.setTrackStripJson(1, vocalSceneJson);
engine.setBusStripJson(1, reverbSceneJson);   // バスは setTrackBuses で先に存在させる
engine.setMasterStripJson(masterSceneJson);

// ストリップを作り直さずに内蔵 EQ の 1 バンドだけ更新する
// (バンド JSON のスキーマは eq.parametric / StreamingEqualizer と同じ):
engine.setTrackStripEqBandJson(1, 0,
  JSON.stringify({ type: 'peak', frequencyHz: 250, gainDb: -2, q: 1.0 }));

// インサートをその場でバイパスする。第 4 引数に true を渡すと状態もリセットする。
engine.setTrackStripInsertBypassed(1, 0, true);

// キュー可能なソロ/ミュート: レーンインデックスと renderFrame を取る
// (-1 = 即時適用、将来のフレームを渡すとサンプル精度で適用)。
engine.setSoloMute(0, true, false, -1);

レーンインデックスは追加専用

あるトラック id が一度レーンを占有すると、そのレーンインデックスはエンジンの生存期間中固定されます。setTrackLanes(...) を呼ぶたびに、宣言済みのレーン id を現在の順序どおりに並べ、新しいトラック id はその後ろにのみ追加できます。sends を持つエントリーはそのトラックのセンドリストを置き換え、sends のないエントリー(id だけの指定を含む)は既存のセンドに触れません。setSoloMute はこの固定インデックスでレーンを指定します。

構造を変えるストリップ呼び出しはコントロールスレッドで

setTrackLanessetTrackBuses、ストリップ JSON セッターは内部構造を構築するため、process(...) と同時に実行してはいけません。レンダーの合間か停止中に発行してください。ライブ操作向けの軽量なコントロールは、サンプル精度でキューされる setSoloMute と、1 バンドをその場で書き換える EQ バンド更新です。

ENGINE · LANE MIXERIDLE
エンジンのレーンミキサー — 再生エンジン内のフェーダーとミュート

3 つの MIDI クリップがリアルタイムエンジンでループします。各トラックはレーンを 1 つ占有し、専用のチャンネルストリップを持ちます。フェーダーはストリップのセッターを、ミュートは setSoloMute を呼びます。下の各バンドはエンジンの実際のレーン別出力で、操作のたびに renderOffline で描き直されます。

リードのフェーダー
0 dB
ベースのフェーダー
0 dB
ドラムのフェーダー
0 dB
リードをミュート
ベースをミュート
ドラムをミュート

グループルーティング・サイドチェイン・ライブストリップ操作

レーン/センドのグラフ以外にも、ストリップを作り直さずにルーティングとパンを変えるリアルタイムセーフな操作がいくつかあります。

目的生の RealtimeEngineSonareEngine ワークレットファサード
レーンをグループバスへ折り込む(busId 0 でマスターミックスへ戻す)setTrackLanes(...) のレーン outputBusId0 または未指定でマスターミックス)setTrackOutputBus(target, busId)busId 0 でマスターミックスへ戻す)
あるレーンのインサートを別レーンでキーイング(ダッキング)setLaneSidechain(trackId, insertIndex, sourceTrackId)0 で解除)setLaneSidechain(target, insertIndex, sourceTarget)null で解除)
レーンをパンするsetTrackStripPan(trackId, pan)setTrackStripPan(target, pan)
パンロー/パンモードsetTrackStripPanLaw(...)setTrackStripPanMode(...)同名
左右独立(デュアル)パンsetTrackStripDualPan(trackId, left, right)setTrackStripDualPan(target, left, right)
レーンごとのサンプル遅延setTrackStripChannelDelaySamples(trackId, samples)同名
インサートパラメータを名前で設定setTrackStripInsertParamByName(trackId, insertIndex, paramName, value)(マスター: setMasterStripInsertParamByName(...)同名、加えて setStripInsertParamByName(target, ...)

setTrackStripInsertParamByName(...) はリアルタイムオートメーションの入り口です。masteringInsertParamInfo(name) が返す JSON キーでパラメータを指定するため、ホストはストリップ JSON を作り直さずにインサートの自動化可能なパラメータをライブで駆動できます。ファサードでは target はトラック id または名前です。

パラメータオートメーション

RealtimeEngine は、setTrackStripInsertParamByName のストリップインサートパラメータとは別に、エンジンレベルのパラメータレジストリを持ちます。addParameter(info) でパラメータを 1 度登録し、setParameter(id, value, renderFrame?)(ランプには setParameterSmoothed(...))でライブに駆動するか、setAutomationLane(id, points) でタイムライン上にスケジュールします。

typescript
// EngineParameterInfo: id, name, unit, min/max/default, rtSafe, defaultCurve(0=linear)
engine.addParameter({
  id: 1, name: 'volume', unit: 'lin',
  minValue: 0, maxValue: 1, defaultValue: 1,
  rtSafe: true, defaultCurve: 0,
});

// オートメーション点は PPQ(4 分音符単位)で位置づけ、任意で curveToNext コード
// (0=linear、1=exponential、2=hold、3=s-curve)を持ちます。
engine.setAutomationLane(1, [
  { ppq: 0, value: 1, curveToNext: 0 },
  { ppq: 4, value: 0 },
]);

// あるいはコントロールスレッドから命令的に設定する(renderFrame -1 = 即時)。
engine.setParameter(1, 0.5);

SonareEngine ファサードでは、パラメータを登録せずにミキサーのフェーダー/パンを自動化することもできます。automationParamId(target, 'faderDb' | 'pan')busAutomationParamId(busId) はミキサー名前空間の予約済みエンジンパラメータ id を返すので、それをそのまま setAutomationLane(paramId, points) に渡してトラック/マスターのフェーダーやパン、あるいはバスのフェーダー(バス id はそのフェーダーゲイン dB に解決されます)を自動化できます。targetbusId は初回利用時にミキサーのレーン/バスを宣言します。

サラウンドグループバスとワイドメーター

サラウンドの channelLayoutSonareChannelLayout: 0 モノラル、1 ステレオ、2 5.1、3 7.1)で宣言したバスはサラウンドグループバスになります。バスはプレーンごとにマスターへ合算し、プレーン別メーターを公開します。レーンごとのサラウンドパン DSP(各レーンをストリップの surroundPan 位置へ配置するもの)は段階導入中で、surroundPan 値(およびレーン自身の sourceChannelLayout)はシーンに保存され config JSON を往復しますが、まだ音声のパンニングには反映されません。現状はバスの channelLayout を宣言し、レーンごとのパンはサラウンド DSP パスが入った時点で有効になります。

typescript
engine.setTrackBuses([{ busId: 1, channelLayout: 2 }]); // 5.1 のグループバス
engine.setTrackOutputBus(1, 1);                          // レーンをそこへルーティング

setTrackOutputBus(1, 0)(または setTrackLanesoutputBusId: 0 を指定)を呼ぶと、レーンをマスターミックスへ戻せます。

サラウンドメーターはライブのワークレットメーターリングを通りません。オフラインまたはメインスレッドのエンジンで drainMeterTelemetryWide(maxRecords?) を使って読み取ると、プレーンごとの(ワイドな)レコードが返ります。drainMeterTelemetry() はステレオの高速パスのままです。この 2 つのドレインは同じシングルコンシューマのテレメトリキューを消費するため、1 つのエンジンインスタンスにつきどちらか一方だけを呼んでください。ライブの AudioWorklet 経路はステレオドレインでキューを所有しており、そのため drainMeterTelemetryWide() はオフライン(非ワークレット)エンジン向けです。両方を 1 つのエンジンで回すと、互いのレコードを奪い合います。

MIDI クリップスケジューリングと sampleAtPpq

音声クリップにはクリップスケジュールとページプロバイダがあり、MIDI クリップには専用のリアルタイムスケジュールがあります。setMidiClips(clips) はエンジンの MIDI クリップスケジュール全体を 1 回の呼び出しで置き換え、各クリップはイベントを MIDI の宛先 id に従って楽器へルーティングします(setBuiltinInstrumentsetSynthInstrumentsetSf2Instrument でバインドした楽器。宛先モデルは MIDI 入力を参照)。

このスケジュールはコンパイル済みです。タイミングは PPQ ではなくエンジンタイムライン上の絶対サンプルで表します。音楽的な位置の変換には sampleAtPpq(ppq) を使ってください。エンジンのテンポマップ(setTempo / setTempoSegments のすべての変更)を積分するため、テンポが途中で変わっても正しい位置が得られます。

typescript
// UMP MIDI 1.0 チャンネルボイスワード(ノートオン = ステータス 0x9、ノートオフ = 0x8)。
const noteOn  = (ch: number, note: number, vel: number) =>
  (0x2 << 28) | (0x9 << 20) | (ch << 16) | (note << 8) | vel;
const noteOff = (ch: number, note: number) =>
  (0x2 << 28) | (0x8 << 20) | (ch << 16) | (note << 8);

const start = engine.sampleAtPpq(8);                  // テンポマップを考慮した変換
const length = engine.sampleAtPpq(16) - start;

engine.setMidiClips([{
  id: 1,
  trackId: 1,
  destinationId: 0,            // このイベントをレンダーする楽器の宛先
  startSample: start,
  startPpq: 8,
  lengthSamples: length,
  loop: true,
  loopLengthSamples: length,
  events: [
    // renderFrame はエンジンタイムライン上の絶対サンプル。1 ワードの
    // MIDI 1.0 イベントでは wordCount を省略できる(word0 から推論される)。
    { renderFrame: start,                          word0: noteOn(0, 60, 100) },
    { renderFrame: start + Math.floor(length / 2), word0: noteOff(0, 60) },
  ],
}]);

ループするクリップは loopLengthSamples ごとにイベントリストを繰り返します。スケジュールを空にするには setMidiClips([]) を呼びます。プロジェクトレベル(PPQ 単位のノート、テイク、コンピング)で作業したい場合は、プロジェクト編集でアレンジを組んでバウンスしてください。このリアルタイムスケジュールは、DAW フロントエンドがコンパイルして渡す低レベル側の面です。

AudioWorklet での使い分け

通常の WASM パッケージは完全な RealtimeEngine クラスを公開します。任意の Worklet ブリッジでは、SonareRealtimeEngineNode.create(...) から完全版の embind ランタイムと専用の sonare-rt ランタイムのどちらかを選べます。

ブリッジヘルパーは @libraz/libsonare/worklet サブパスにあります。Worklet モジュール側でプロセッサを登録し、メインスレッド側でノードを作ります。moduleUrlregisterSonareRealtimeEngineWorkletProcessor() を呼ぶコンパイル済み Worklet モジュールです。

typescript
// sonare-engine-worklet.ts
import { registerSonareRealtimeEngineWorkletProcessor } from '@libraz/libsonare/worklet';

registerSonareRealtimeEngineWorkletProcessor();
typescript
// main.ts
import { SonareRealtimeEngineNode } from '@libraz/libsonare/worklet';

const audioCtx = new AudioContext();
const engineNode = await SonareRealtimeEngineNode.create(audioCtx, {
  moduleUrl: '/sonare-engine-worklet.js',
  runtimeTarget: 'embind', // Worklet 内で完全版 RealtimeEngine を使う
  channelCount: 2,
  mode: 'auto',            // 可能なら SAB、なければ postMessage
});

engineNode.node.connect(audioCtx.destination);
engineNode.play();
engineNode.onTelemetry((telemetry) => console.log(telemetry));
console.log(engineNode.capabilities.mode, engineNode.capabilities.degradedReason);

// SAB テレメトリを使う場合は、requestAnimationFrame など UI tick で回収する。
engineNode.pollTelemetry();

// 後始末:
engineNode.destroy();

アプリ側でより高いレベルの facade が欲しい場合は、SonareEngine を使えます。

部品役割
Worklet ノードリアルタイム音声側を実行します。
main thread 側の RealtimeEngineオフライン処理やタイムライン操作を扱います。

transport ファサードは play/stop、秒または PPQ への seek、テンポ、ループ更新を扱います。トランスポート以外も、ファサードはエンジンのほぼ全面をコントロールメッセージ経由で Worklet にミラーします。メインスレッドが唯一の正であり、音声スレッドは同期済みスナップショットを受け取るだけです。

やりたいことファサード API
トラックルーティング、フェーダー、パン、ソロ/ミュートsetTrackLanessetStripGainsetStripPansetTrackStripPansetTrackStripPanLawsetTrackStripPanModesetTrackStripDualPansetTrackStripChannelDelaySamplessetSoloMute
トラック/マスターのインサートと EQsetTrackStripJsonsetMasterStripJsonsetTrackStripEqBandsetMasterStripEqBandsetTrackStripInsertParamByNamesetMasterStripInsertParamByName、インサートバイパス系メソッド
センドとバスsetSendssetBusGainsetBusStripJson
MIDI クリップとライブ MIDIsetMidiClipspushMidiNoteOnpushMidiNoteOffpushMidiCcpushMidiPanic
パラメータオートメーションsetAutomationLaneaddAutomationPointautomationParamId(target, kind)busAutomationParamId(busId)listParametersautomationLaneCount
楽器setBuiltinInstrumentsetSynthInstrumentloadSoundFontsetSf2Instrument
録音とモニタリングconfigureCaptureinputMonitor を含む)、armRecordpunchcapturedAudiocaptureStatus
トランスポート、テンポ、マーカーgetTransportStatecachedTransportStatesetTempoSegmentssetTimeSignatureSegments、マーカー系メソッド(全置換の setMarkers を含む。解決後のマーカー一覧(各エントリがエンジン id を持つ)を返す)、setLoopFromMarkers
クリップ更新addClipremoveClip — ファサードは差分(クリップデルタ)を Worklet へ送る
メーターとテレメトリonMeter / onTelemetry / onScopepollMeters / pollTelemetry / pollScope。メーターレコードはマスター・レーン・バス・入力のターゲット id を持つ。オフライン/メインスレッド側エンジンではワイドメーターレコードとスコープスナップショットも読み出せる
オフライン書き出しメインスレッド側ミラーの renderOffline

ファサードでは、ストリップを指定するメソッドはトラック id または名前target: string | number)を受け取り、setSoloMute(target, solo, mute) はレーンインデックスの解決まで行います。setTrackStripEqBandEqBand オブジェクトを直接受け取るので、バンド JSON を手書きする必要はほとんどありません。

automationParamId(target, 'faderDb' | 'pan')busAutomationParamId(busId) はミキサー名前空間の予約済みエンジンパラメータ id を返すので、addParameter でカスタムパラメータを登録しなくても、それらをそのまま setAutomationLane(paramId, points) に渡してトラック/マスターのフェーダーやパン、あるいはバスのフェーダー(そのフェーダーゲイン dB に解決されます)を自動化できます。targetbusId は初回利用時にミキサーのレーン/バスを宣言します。

Worklet 側でスコープスナップショットを流す場合は、SonareRealtimeEngineNode.create(...)scopeIntervalFramesscopeBands、必要に応じて scopeSharedBuffer / scopeRingCapacity を渡します。独自ブリッジでスペクトラムとベクトルスコープのスナップショットを共有リングに流したい場合は、低レベルの createSonareScopeRingBuffer(...)readSonareScopeRingBuffer(...) を使えます。

typescript
import { SonareEngine } from '@libraz/libsonare/worklet';

const engine = await SonareEngine.create(audioCtx, {
  moduleUrl: '/sonare-engine-worklet.js',
  mode: 'auto',
  channelCount: 2,
});

engine.setTrackLanes([{ trackId: 1 }]);
engine.setTrackStripJson(1, trackSceneJson);
engine.addClip(1, [clipL, clipR], 0);
engine.setTempoSegments([{ startPpq: 0, bpm: 120 }]);
engine.transport.setLoop(0, 4, true);
engine.transport.play();
engine.onMeter((meter) => console.log(meter.rmsDbL, meter.rmsDbR));

const offline = await engine.renderOffline(48000);
console.log(offline[0].length);

engine.destroy();

既定のランタイムターゲットは embind

SonareEngine の既定は Worklet 内の完全版 embind エンジン(runtimeTarget: 'embind')で、縮小版 sonare-rt ランタイムはトランスポート中心のフォールバックという位置づけです。ホスト側の Worklet エントリーがメッセージ名をフィルタしてから転送している場合は、sync*captureRequesttransportRequest のすべてを許可リストに加えてください。そうしないとレーン/ストリップ/MIDI の同期メッセージが黙って落ちます。

別の sonare-rt モジュールは AudioWorklet のホットパス用に意図的に小さくしてあります。対象はトランスポート、テンポ/ループ、マーカーシーク、メトロノーム有効化、キャプチャのアーム/パンチコマンド、ブロック処理、基本テレメトリ読み出しです。メインスレッド側に置く方が自然な embind の重い機能は省かれています。

typescript
const rtNode = await SonareRealtimeEngineNode.create(audioCtx, {
  moduleUrl: '/sonare-engine-worklet.js',
  runtimeTarget: 'sonare-rt',
  rtModuleUrl: '/sonare-rt.js', // 縮小版ランタイムでは必須
  channelCount: 2,
});

await rtNode.ready; // Worklet 内で縮小版モジュールのロードが完了すると resolve
完全版 RealtimeEngine のみsonare-rt から省く理由
パラメータレジストリとオートメーションレーン(addParameterparameterInfosetAutomationLanesetParameter*レンダーコールバック内の JS オブジェクト / 文字列マーシャリングを避けるため
トランスポートの読み戻し(getTransportStateWorklet ブリッジ側で状態をミラーするため
完全なマーカー / 拍子ヘルパーsonare-rt はマーカーシークとトランスポートコマンドに絞るため
グラフトポロジーとクリップスケジューリンググラフ / クリップ編集は完全版 embind ランタイムの責務
キャプチャの読み戻しとオフラインレンダー(capturedAudiorenderOfflinebounceOfflinefreezeOffline非リアルタイム処理のため
メーターテレメトリの回収縮小版ランタイムは基本テレメトリ経路だけを公開するため

シーン編集、パラメータ確認、グラフ操作、キャプチャの読み戻し、オフライン書き出しが必要な場合はメインスレッドで通常パッケージを使います。レンダーコールバック内でアロケーションや GC を抑えたい場合は、AudioWorklet 側で sonare-rt を使うのが自然です。

クリップ音声のページストリーミング

長いアレンジは、一度にメモリへ載せきれないほどのクリップ音声を持つことがあります。エンジンはクリップ音声をページ単位でストリーミングします。1 つの巨大なバッファの代わりに、クリップはクリップページプロバイダに支えられ、必要に応じて固定サイズのページをエンジンへ渡します。

この流れは設計上ロックフリーです。

  1. レンダースレッドがまだ持っていないページを必要とし、ウェイトフリーのページ要求キューへ要求を積みます。
  2. メインスレッドが engine.popClipPageRequest() でそのキューを排出し、要求されたページの音声をストレージから読み、provider.supply(pageIndex, channels) を呼びます。
  3. 不要になったページは provider.clear(pageIndex) で解放します。

音声スレッドは要求を積むことと、供給済みページを読むことしか行わないため、ストレージやアロケーションでブロックしません。

ロックフリーとウェイトフリー(初心者向け)

リアルタイム音声スレッドは止まってはいけません。ほんの一瞬でも停滞すると出力にノイズが乗ります。ロックフリーとは、音声スレッドが他スレッドの保持するロックの解放を待たないことです。ここで使うより強い保証がウェイトフリーで、ページ欠落要求をページ要求キューへ積む処理は必ず有限ステップで完了します。そのため音声スレッドがそこで待ったりスピンしたりすることがありません。遅い部分(ストレージからのページ読み取り)は、代わりにメインスレッドで行われます。

ブラウザの OPFS バックエンドプロバイダ

パッケージには、Origin Private File System(OPFS) からワーカー上でページを読む既製プロバイダが同梱されており、ディスク読み取りをメインスレッドの外に保ちます。

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

const binding = createOpfsClipPageProvider(engine, {
  path: 'clips/long-take.f32',  // OPFS 上のインターリーブ Float32
  numChannels: 2,
  numSamples: totalFrames,      // ファイルの総フレーム数
  pageFrames: 65536,            // フレーム単位のページサイズ
  // dataOffsetBytes?: ヘッダーをスキップ。worker?: 自前の Worker を再利用
});

// UI tick で、レンダースレッドが要求した分を処理する:
let request;
while ((request = engine.popClipPageRequest()) !== null) {
  await binding.supplyRequest(request);  // 該当ページを読み込んで供給
}

// 後始末:
binding.close();  // プロバイダを解放し、所有していればワーカーも終了

createOpfsClipPageProvider(...) はエンジン側の ClipPageProvider を作り、ワーカーと組み合わせます。既定では createOpfsClipPageWorker() でインラインワーカーを起動します。そのワーカー本体は opfsClipPageWorkerSource として公開されているので、自前でバンドルしたり、独自の Worker を渡したりもできます。supplyRequest(request) は排出した要求のサンプル位置をページインデックスへ写像し、supplyPage(pageIndex) はページを直接プリフェッチできます。

OPFS 対応はブラウザによって異なる

OPFS プロバイダは navigator.storage.getDirectory() と同期アクセスハンドルに依存します。これらは現行の Chromium・Firefox と最近の Safari の WebKit では利用できますが、古いブラウザでは使えません。利用前に機能検出し、OPFS が無い環境向けに完全インメモリのフォールバック(または任意のソースから供給する自前の ClipPageProvider)を用意してください。

表示用の波形ピーク

任意のズームでクリップを描くのに、全サンプルは必要ありません。必要なのは画面の列ごとの最小/最大包絡です。waveformPeaks(...) はインターリーブ音声をチャンネルごとの最小/最大のバケットへ縮約し、そのまま描けます。

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

await init();

// インターリーブステレオ(L0,R0,L1,R1,...)。ここでは mono なので channels = 1
const peaks = waveformPeaks(samples, /* channels */ 1, { samplesPerBucket: 512 });
// peaks.min / peaks.max は長さ peaks.channels * peaks.bucketCount の
// チャンネルメジャー Float32Array。バケットごとに縦線を描く
for (let b = 0; b < peaks.bucketCount; b++) {
  drawColumn(b, peaks.min[b], peaks.max[b]);
}

ユーザーが自由にズームできるクリップでは、waveformPeakPyramid(...) で複数のバケットサイズを一度に前計算し、現在の pixels-per-second に最も近いレベルを選びます。

typescript
const pyramid = waveformPeakPyramid(samples, 1, {
  samplesPerBucketLevels: [512, 1024, 2048, 4096],
});
// pyramid[i] はそのバケットサイズの WaveformPeaksReport。粗いレベルほど
// バケット数が少なく、ズームアウト時に描画が軽い

どちらもバッファしたクリップに対するバッチ縮約で、音声コールバック内の処理ではありません。samplesPerBucket はフレーム単位のバケット幅で、小さいほど詳細でバケット数も増えます。

関連