パラメータ記述子からコントロールを組み立てる
エンジンが構築できるプロセッサは、どれも自分のパラメータを記述しています。名前、型、単位、既定値、受け入れ範囲、表示範囲、軸、兄弟パラメータとの制約、音声の再生中に値を変えられるかどうかです。ホストがこの記述子を読めば、手書きの表を持たずにどのインサートやマスタリングの段の操作画面も描けます。ビルドでキーやバンドが増えれば、画面もそれに追従します。
このページでは、その操作画面を素の JavaScript で順に組み立てます。フィールドごとのリファレンスは別ページにあるので、ここでは繰り返さずにリンクします。各フィールドは パラメータ記述子、min・max・default の出どころは カタログの値域の読み方、ドキュメント全体は 機能カタログ を参照してください。
以下のコード片はすべて 1 本のスクリプトで、同梱の WASM ビルドに対して Node で実行したものです。出力ブロックはそのとき表示された内容です。
用意するもの
記述子を読む
capabilityCatalog() は 1 回の呼び出しでビルド全体を返します。各プロセッサとその params 配列に加え、マスタリングチェーン用の chain セクションも含みます。masteringInsertParamInfo(name, sampleRate?) は 1 つのプロセッサについて同じ記述子を返し、サンプルレートに依存する上限も解決できます。UI は起動時にカタログを 1 回読むのが普通です。
import {
init, capabilityCatalog, masteringInsertParamInfo, masteringInsertTiming,
masteringChain, RealtimeEngine,
} from '@libraz/libsonare';
await init();
const catalog = capabilityCatalog();
const processor = (id) => catalog.processors.find((p) => p.id === id);
const gate = processor('dynamics.gate');
for (const p of gate.params) {
console.log(p.name.padEnd(17), p.type, p.unit, p.scale.padEnd(6),
'min', p.min, 'max', p.max, 'ui', p.uiMin, p.uiMax, 'default', p.default,
'rtSafe', p.rtSafe, p.dependsOn.map((d) => `${d.relation} ${d.key}`).join());
}thresholdDb number dB linear min null max null ui -50 0 default -50 rtSafe true ge closeThresholdDb
attackMs number ms log min 0 max null ui 0.1 100 default 2 rtSafe true
releaseMs number ms log min 0 max null ui 0.1 2000 default 80 rtSafe true
rangeDb number dB linear min null max 0 ui -80 0 default -80 rtSafe true
closeThresholdDb number dB linear min null max null ui null null default -50 rtSafe false le thresholdDb
holdMs number ms linear min 0 max null ui 0 1000 default 0 rtSafe false
keyHpfHz number Hz linear min 0 max null ui null null default 0 rtSafe falseこのページで扱う要素の大半が、ゲートの記述子に出ています。範囲は 2 種類あり、min / max は構築が受け入れる範囲、uiMin / uiMax はコントロールが描くべき範囲です。アタックとリリースの時間は対数軸で描きます。closeThresholdDb には自前の範囲がなく、thresholdDb との関係だけを持っています。最後の 3 行は再生中には変更できません。
パラメータごとに部品を選ぶ
部品は type と choices で決まります。数値がスライダーになるのは、表示範囲か受け入れ範囲から両端が分かるときだけで、それ以外は数値入力欄が必要です。
function controlKind(p) {
if (p.type === 'boolean') return 'toggle';
if (p.choices !== null) return 'select'; // 列挙型、または受け入れる値に飛びがある数値
if (p.type !== 'number') return 'custom'; // 'string' / 'array': インパルス応答、バンドごとのリスト
const lo = p.uiMin ?? p.min;
const hi = p.uiMax ?? p.max;
return lo !== null && hi !== null ? 'slider' : 'field';
}
const kinds = {};
for (const proc of catalog.processors) {
for (const p of proc.params) kinds[controlKind(p)] = (kinds[controlKind(p)] ?? 0) + 1;
}
console.log(kinds);{ slider: 1064, toggle: 1014, select: 745, field: 2751, custom: 5 }field が多いのは意図どおりです。ゲインやオフセットの多くは境界を公開していません。null は構築がその側で値を拒否しないという意味で、測っていないという意味ではありません(null と既定値が教えてくれないこと)。こうしたキーには、単位を添えた数値入力欄が実態に合ったコントロールです。スライダーにするには、エンジンが一度も示していない範囲が必要になります。select は選択肢の数値 value を送り、name はラベルとして使います。
スライダーをスケールに対応させる
scale は軸で、'linear' か 'log' です。対数スライダーには正の下端と有限の上端が必要です。現在のビルドでは、対数スケールの記述子はすべて、uiMin / uiMax か min / max のどちらかで両端を公開しています。uiMin ?? min と uiMax ?? max で必ず見つかります。
function sliderRange(p) {
const lo = p.uiMin ?? p.min;
let hi = p.uiMax ?? p.max;
// 描く範囲の上端が受け入れ上限そのもので、その上限値は拒否される
if (p.uiMax === null && p.maxExclusive) hi *= 0.999;
return [lo, hi];
}
function toValue(p, t) {
const [lo, hi] = sliderRange(p);
return p.scale === 'log' ? lo * (hi / lo) ** t : lo + (hi - lo) * t;
}
function toPosition(p, value) {
const [lo, hi] = sliderRange(p);
const v = Math.min(Math.max(value, lo), hi); // つまみは端に留め、値そのものは保持する
return p.scale === 'log' ? Math.log(v / lo) / Math.log(hi / lo) : (v - lo) / (hi - lo);
}
const attack = processor('dynamics.compressor').params.find((p) => p.name === 'attackMs');
console.log(attack.scale, sliderRange(attack), [0, 0.5, 1].map((t) => +toValue(attack, t).toFixed(3)));
const sampleRate = 44100;
const freq = masteringInsertParamInfo('eq.parametric', sampleRate)
.find((p) => p.name === 'band0.frequencyHz');
console.log(freq.scale, freq.maxRelativeTo, sliderRange(freq), +toValue(freq, 0.5).toFixed(1));
const fast = processor('dynamics.transientShaper').params.find((p) => p.name === 'fastAttackMs');
console.log(fast.default, sliderRange(fast), toPosition(fast, fast.default));log [ 0.1, 200 ] [ 0.1, 4.472, 200 ]
log nyquist [ 20, 22027.95 ] 663.7
0 [ 0.1, 10000 ] 0出力から 3 つのケースが読み取れます。
- コンプレッサーのアタックは 0 ms を受け入れます(
min: 0)。しかし対数軸は 0 に届かないため、記述子はuiMinの 0.1 ms から描くよう指定しています。スライダーの中央は両端の幾何平均の 4.47 ms です。 - EQ バンドの周波数には
uiMaxがありません。上限はナイキスト周波数で、その値自体は含みません。セッションのレートをmasteringInsertParamInfoに渡すと、48 kHz で測った値が 44.1 kHz のセッション用に 22050 へ変わり、スライダーはそのすぐ手前で止まります。このレートでエンジンは 22027.95 Hz を受け入れ、22050 Hz を拒否します。 - 値が表示範囲の外にあることもあります。トランジェントシェイパーの
fastAttackMsは既定値が 0 で、uiMinの 0.1 より下です。toPositionはつまみを端に留め、値は 0 のまま保ちます。値を表示範囲に丸めると、読み込んだ時点で音が変わってしまいます。
min と max は、入力された値を早めに確かめる用途にも使えます。ただし他のパラメータをすべて既定値にして測った範囲なので、兄弟と連動するパラメータでは既定の構成での範囲にすぎません。dependsOn が表すのは単純な大小関係の連動で、それ以外に値域を動かす兄弟は coupledWith が挙げます。ルームリバーブの receiverSpacingM は dependsOn が空で、coupledWith に lengthM、widthM、listenerX、listenerY、sourceX、sourceY を挙げています。2.5 m を公開しているのは、既定の部屋でそこが壁に当たる位置だからです(幾何ベースのルーム系インサート)。ユーザーが coupledWith に挙がったキーを変更したら、既定の構成での境界のままにせず、依存する側のコントロールを再検証する(あるいは検査を緩める)ようにしてください。公開された境界は警告に使い、最終判断はエンジンが拒否するかどうかに任せてください。手順は 値をエンジンへ戻す にあります。
兄弟パラメータで範囲を絞る
dependsOn は、現在値がこのキーの範囲を決める兄弟パラメータの一覧です。各要素は this <relation> factor * sibling と読みます。UI はこれを、兄弟が動くと一緒に動く範囲に変換します。
function liveBounds(p, values) {
let lo = p.min, hi = p.max, loOpen = p.minExclusive, hiOpen = p.maxExclusive;
for (const { key, relation, factor } of p.dependsOn) {
if (values[key] === undefined) continue; // 兄弟パラメータが有効でない
const bound = factor * values[key];
if (relation === 'lt' || relation === 'le') {
if (hi === null || bound < hi) [hi, hiOpen] = [bound, relation === 'lt'];
} else if (lo === null || bound > lo) {
[lo, loOpen] = [bound, relation === 'gt'];
}
}
return { lo, loOpen, hi, hiOpen };
}
const gateValues = { thresholdDb: -40, closeThresholdDb: -50 };
const close = gate.params.find((p) => p.name === 'closeThresholdDb');
console.log('closeThresholdDb', liveBounds(close, gateValues));
const multiband = processor('multiband.compressor');
const cutoff1 = multiband.params.find((p) => p.name === 'cutoff1Hz');
console.log('cutoff1Hz', liveBounds(cutoff1, { cutoff0Hz: 120, cutoff1Hz: 2000 }));
for (const params of [
{ thresholdDb: -40, closeThresholdDb: -40 },
{ thresholdDb: -40, closeThresholdDb: -30 },
]) {
try {
masteringInsertTiming('dynamics.gate', params, 48000);
console.log(params, 'accepted');
} catch (e) {
console.log(params, 'refused:', e.message);
}
}closeThresholdDb { lo: null, loOpen: false, hi: -40, hiOpen: false }
cutoff1Hz { lo: 120, loOpen: true, hi: null, hiOpen: false }
{ thresholdDb: -40, closeThresholdDb: -40 } accepted
{ thresholdDb: -40, closeThresholdDb: -30 } refused: invalid gate configurationゲートのクローズしきい値はオープンしきい値と等しくてもよく(le)、超えることはできません。クロスオーバーのカットオフは厳密に順序づけられている(lt / gt)ので、隣と同じ値にはできません。現在のビルドの関係は、古典的なデノイズとデリバーブの段を除いてすべて係数 1 です。この 2 つでは hopLength が nFft の半分以下でなければなりません(le、係数 0.5)。
兄弟の値が依存側の値を追い越したとき、UI の対応は 2 通り考えられます。依存側の値を一緒に動かすか、ユーザーが直すまでその組を無効と表示するかです。その組をそのまま送ってはいけません。構築が拒否します。
dependsOn は値の範囲を決めるもので、コントロールを隠したり無効にしたりはしません。コントロールが存在するかどうかは、次に扱う slot フィールドが決めます。
存在するバンドだけを表示する
slot が null でないキーは、条件付きで存在する群に属します。プロセッサの slots 配列が、各群の activation、parent、minCrossoverCutoffs を示します(機能カタログ)。プロセッサは次の 2 つのパターンで扱えます。
activation: 'anyKey'の群は、そのキーのどれか 1 つが送られた時点で存在します。パラメトリック EQ のバンドはキーを 1 つ送れば作られるので、UI は「バンドを追加」を用意し、追加後はそのバンドのコントロールを表示します。activation: 'always'とminCrossoverCutoffsの組み合わせでは、有効なクロスオーバーのカットオフが足りている間だけ群が存在します。マルチバンドコンプレッサーのバンド数はカットオフ数より 1 つ多くなります。カットオフのキーを 1 つも送らなければ既定の分割が使われ、既定値が null でないカットオフがそのまま有効になります。
function presentSlots(proc, values) {
const cutoffKeys = proc.params.filter((p) => /^cutoff\d+Hz$/.test(p.name));
const sent = cutoffKeys.filter((p) => values[p.name] !== undefined).length;
const cutoffs = sent > 0 ? sent : cutoffKeys.filter((p) => p.default !== null).length;
const present = new Set();
for (const slot of proc.slots) { // 親は必ず子より前に並ぶ
if (slot.parent !== null && !present.has(slot.parent)) continue;
if (cutoffs < slot.minCrossoverCutoffs) continue;
const keyed = Object.keys(values).some((k) => k.startsWith(`${slot.name}.`));
if (slot.activation === 'always' || keyed) present.add(slot.name);
}
return [...present];
}
console.log(presentSlots(multiband, {}));
console.log(presentSlots(multiband, { cutoff0Hz: 120, cutoff1Hz: 2000, cutoff2Hz: 6000 }));
console.log(presentSlots(processor('eq.parametric'), { 'band0.gainDb': 3, 'band5.frequencyHz': 8000 }));[ 'band0', 'band1', 'band2' ]
[ 'band0', 'band1', 'band2', 'band3' ]
[ 'band0', 'band5' ]カットオフのキーを 1 つでも送ると、分割は送ったカットオフちょうどになります。そのため、バンドを追加する UI は cutoff0Hz から順にカットオフの一覧をすべて送ります。マルチバンドコンプレッサーではエンジンもこれに従います。既定の 2 つのカットオフのままでは、masteringInsertTiming は band3.* のキーをインサートが読まないキーとして拒否し、カットオフを 3 つ送ると受け入れます。
再生中に変えられる値を見分ける
rtSafe はコントロールを 2 つに分けます。rtSafe のパラメータは、リアルタイムエンジンの名前指定セッターかオートメーションレーンを通じて、再生中に変更できます。それ以外のパラメータは、オートメーション id を持つものも含めて、インサートを構築したときにだけ反映されます。変更するにはストリップを組み直す必要があります。現在のビルドの記述子 5,579 個のうち、rtSafe は 1,261 個です。さらに 98 個は id を持っていても rtSafe ではありません。
この違いは、ユーザーが操作する前から画面に出せます。オートメーションレーンは rtSafe の行にだけ描き、構築時専用の変更はドラッグの途中ではなくコントロールを離したときに適用します。
値をエンジンへ戻す
ユーザーの値は、name をキーにした疎なオブジェクトで持ちます。ユーザーが設定したキーだけを入れておけば、残りにはプロセッサ自身の既定値が使われます。構築の前には masteringInsertTiming(name, params, sampleRate) が検証役になります。ストリップと同じ手順でインサートを構築し、構築が拒否する値も、インサートが読まないキーも拒否します。単発の masteringProcess は未知のキーを無視するので、検証には向きません。
const changed = { thresholdDb: -45, closeThresholdDb: -55, holdMs: 20 };
console.log(masteringInsertTiming('dynamics.gate', changed, 48000));
const engine = new RealtimeEngine(48000, 128);
engine.setTrackLanes([1]);
engine.setTrackStripJson(1, JSON.stringify({
version: 1,
strips: [{ id: 'vocal', inserts: [{ processor: 'dynamics.gate', params: changed }] }],
}));
const isLive = (name) => gate.params.find((p) => p.name === name).rtSafe;
for (const [name, value] of [['thresholdDb', -38], ['holdMs', 40]]) {
if (isLive(name)) {
engine.setTrackStripInsertParamByName(1, 0, name, value);
console.log(name, 'set live');
} else {
console.log(name, 'needs a rebuild of the strip');
}
}
try {
engine.setTrackStripInsertParamByName(1, 0, 'holdMs', 40);
} catch (e) {
console.log('holdMs live:', e.message);
}
engine.destroy();{ latencySamples: 0, tailSamples: 0 }
thresholdDb set live
holdMs needs a rebuild of the strip
holdMs live: invalid track strip insert parameter target範囲外の値の扱いは経路によって違います。構築(シーン JSON、setTrackStripJson、masteringProcess、masteringInsertTiming)は受け入れ範囲外の値を拒否し、compressor ratio must be at least 1 のように問題を示すメッセージを返します。ライブ経路は値を丸めます。上限が 0 のゲートの rangeDb にオートメーションで +12 dB を与えると、0 dB と同じサンプルがレンダリングされました。丸めに頼る UI はユーザーへのフィードバックを失うので、ライブのコントロールは描いた範囲の中に収めてください。
ライブ用のセッター、オートメーション id、ライブで変えた値とその後のストリップ再構築の関係は、グループルーティング・サイドチェイン・ライブストリップ操作 と パラメータオートメーション を参照してください。シーンのインサート形式は ミキシングシーン JSON にあります。
マスタリングチェーンの画面を組む
マスタリングチェーン(masteringChain、masterAudio、すべてのプリセット)はドット区切りのフラットなキーを受け取ります。そのキーを記述するのが catalog.chain で、内容は チェーンキーの記述 のとおりです。
stagesは段ごとに 1 要素で、キーのprefix、<prefix>.<leaf>キーが記述子を借りるカタログのprocessor、enabledKeyを持ちます。listsはインデックス付きのリストで、minItems/maxItemsを持ちます。paramsは、どのプロセッサも持たないキーの記述子です。enabledスイッチやloudness.*がここに入ります。aliasesは正規キーの別表記です。UI は正規キーを書けばよく、この一覧は無視して構いません。
段のパネルはこれらを組み合わせて作ります。enabledKey からトグル、段のプロセッサからコントロール、段の prefix に属する params からコントロール、段が持つリストごとに行エディタを用意します。
const { stages, lists, params: chainParams } = catalog.chain;
const chainParam = (key) => chainParams.find((p) => p.name === key);
function stagePanel(stage) {
const owned = lists.filter((l) => l.key.startsWith(`${stage.prefix}.`));
const fromProcessor = stage.processor === null ? [] : processor(stage.processor).params
// リストを持つ段は、インデックス付きキーを cutoff<i>Hz / band<i>.* ではなくリスト経由で編集する
.filter((p) => owned.length === 0 || !/\d/.test(p.name))
.map((p) => ({ ...p, key: `${stage.prefix}.${p.name}` }));
const chainOnly = chainParams
.filter((p) => p.name.startsWith(`${stage.prefix}.`) && p.name !== stage.enabledKey)
.filter((p) => !owned.some((l) => l.key === p.name))
.map((p) => ({ ...p, key: p.name }));
const keys = new Set();
return {
title: stage.prefix,
enabled: chainParam(stage.enabledKey),
controls: [...fromProcessor, ...chainOnly].filter((c) => !keys.has(c.key) && keys.add(c.key)),
lists: owned.map((l) => ({
key: l.key, minItems: l.minItems, maxItems: l.maxItems,
itemFields: l.processor === null ? [chainParam(l.key)] : processor(l.processor).params,
})),
};
}
const panels = stages.map(stagePanel);
for (const panel of panels.filter((p) => /compressor|multiband|loudness/i.test(p.title))) {
console.log(panel.title, panel.enabled.default, panel.controls.map((c) => c.key.slice(panel.title.length + 1)).join(' '));
for (const l of panel.lists) console.log(' list', l.key, `${l.minItems}..${l.maxItems}`, l.itemFields.length, 'fields');
}dynamics.compressor false thresholdDb ratio attackMs releaseMs makeupGainDb kneeDb autoMakeup detector sidechainHpfEnabled sidechainHpfHz pdrTimeMs pdrReleaseScale
dynamics.multibandComp false firKernelSize mode slope
list dynamics.multibandComp.crossover.cutoffsHz 1..63 1 fields
list dynamics.multibandComp.bands 2..64 12 fields
loudness false applyGainAtInputRate ceilingDb maxLimiterGainReductionDb releaseMs targetLufs truePeakOversampleリストが必要になるのはマルチバンドの段です。そのプロセッサの cutoff<i>Hz と band<i>.* キーはリスト要素の別表記で、チェーン経由では既に存在するカットオフとバンドしか指せません。数を決めるのはリストのキーで、crossover.cutoffsHz に 3 要素を入れるとバンドは 4 つになります。bands の各要素は dynamics.compressor のフィールドを取り、カットオフの各要素は params にある dynamics.multibandComp.crossover.cutoffsHz の記述子を使います。20 Hz から 20 kHz の範囲を対数で描く周波数です。loudness の段にはプロセッサがない(processor: null)ので、コントロールはすべて params から来ます。
stages の並びはチェーンの実行順ではありません。配列では repair.declick が repair.declip より前にありますが、チェーンは declip を先に実行します。チェーンを信号の流れとして描く UI は、チェーンの順序 から順序を取ってください。
パネルの適用は 1 つのフラットなオブジェクトで済みます。列挙型は choices の名前で送れます。
const sr = 48000;
const tone = Float32Array.from({ length: sr }, (_, i) => 0.25 * Math.sin(2 * Math.PI * 220 * i / sr));
const result = masteringChain(tone, sr, {
'dynamics.multibandComp.enabled': true,
'dynamics.multibandComp.crossover.cutoffsHz.0': 120,
'dynamics.multibandComp.crossover.cutoffsHz.1': 2000,
'dynamics.multibandComp.crossover.cutoffsHz.2': 6000,
'dynamics.multibandComp.bands.3.ratio': 3,
'dynamics.compressor.enabled': true,
'dynamics.compressor.detector': 'peak',
'loudness.enabled': true,
'loudness.targetLufs': -16,
});
console.log(result.stages, result.outputLufs.toFixed(2));
try {
masteringChain(tone, sr, { 'dynamics.multibandComp.enabled': true, 'dynamics.multibandComp.band3.ratio': 3 });
} catch (e) {
console.log(e.message);
}[
'dynamics.compressor',
'dynamics.multibandComp',
'loudness.optimize'
] -16.00
multiband band index out of range: dynamics.multibandComp.band3.ratio単発のプロセッサ呼び出しと違い、チェーンは知らないキーを拒否します(unknown chain config key: ...)。パネルから組み立てたキーの綴りが間違っていれば、最初のレンダリングで分かります。
関連
- エフェクトインサート:プロセッサ id から操作画面までの、インサート固有の手順
- マスタリングプロセッサ:境界と既定値の測り方
- JavaScript API:
capabilityCatalog()のシグネチャと TypeScript の型 - リアルタイムエンジン:
rtSafeパラメータ向けのライブセッターとオートメーションレーン - ブラウザでミキシングアプリを作る:エンジン経由でストリップとインサートの値を適用するアプリ