@libraz/libsonare 1.8.0 → 1.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/_effects_common.d.ts +2 -1
- package/dist/_effects_common.d.ts.map +1 -1
- package/dist/analysis.js +3 -0
- package/dist/analysis.js.map +1 -1
- package/dist/effects_mastering.d.ts +2 -2
- package/dist/effects_mastering.d.ts.map +1 -1
- package/dist/effects_note_ops.d.ts +55 -30
- package/dist/effects_note_ops.d.ts.map +1 -1
- package/dist/effects_timepitch.d.ts +17 -12
- package/dist/effects_timepitch.d.ts.map +1 -1
- package/dist/feature_decompose.d.ts +1 -1
- package/dist/feature_decompose.d.ts.map +1 -1
- package/dist/index.d.ts +11 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2098 -155
- package/dist/index.js.map +1 -1
- package/dist/instrument_types.d.ts +27 -3
- package/dist/instrument_types.d.ts.map +1 -1
- package/dist/mastering_core.d.ts +30 -3
- package/dist/mastering_core.d.ts.map +1 -1
- package/dist/mixer.d.ts +2 -2
- package/dist/mixer.d.ts.map +1 -1
- package/dist/project.d.ts +5 -4
- package/dist/project.d.ts.map +1 -1
- package/dist/project_class.d.ts +57 -3
- package/dist/project_class.d.ts.map +1 -1
- package/dist/project_internal.d.ts +35 -1
- package/dist/project_internal.d.ts.map +1 -1
- package/dist/project_types.d.ts +17 -6
- package/dist/project_types.d.ts.map +1 -1
- package/dist/public_types.d.ts +1 -0
- package/dist/public_types.d.ts.map +1 -1
- package/dist/public_types_mastering.d.ts +19 -1
- package/dist/public_types_mastering.d.ts.map +1 -1
- package/dist/public_types_playback.d.ts +1 -0
- package/dist/public_types_playback.d.ts.map +1 -1
- package/dist/public_types_spectral.d.ts +5 -5
- package/dist/public_types_spectral.d.ts.map +1 -1
- package/dist/public_types_vocal_edit.d.ts +250 -0
- package/dist/public_types_vocal_edit.d.ts.map +1 -0
- package/dist/public_types_vocal_project.d.ts +63 -0
- package/dist/public_types_vocal_project.d.ts.map +1 -0
- package/dist/realtime_engine.d.ts +106 -16
- package/dist/realtime_engine.d.ts.map +1 -1
- package/dist/repair_dereverb.d.ts +1 -0
- package/dist/repair_dereverb.d.ts.map +1 -1
- package/dist/repair_noise.d.ts +1 -0
- package/dist/repair_noise.d.ts.map +1 -1
- package/dist/sonare-analysis.wasm +0 -0
- package/dist/sonare.d.ts +89 -0
- package/dist/sonare.js +1 -1
- package/dist/sonare.wasm +0 -0
- package/dist/streaming_processors.d.ts +5 -0
- package/dist/streaming_processors.d.ts.map +1 -1
- package/dist/vocal_edit.d.ts +131 -0
- package/dist/vocal_edit.d.ts.map +1 -0
- package/dist/vocal_edit_worker.d.ts +51 -0
- package/dist/vocal_edit_worker.d.ts.map +1 -0
- package/dist/vocal_edit_worker.js +1462 -0
- package/dist/vocal_edit_worker.js.map +1 -0
- package/dist/vocal_edit_worker_client.d.ts +118 -0
- package/dist/vocal_edit_worker_client.d.ts.map +1 -0
- package/dist/vocal_edit_worker_protocol.d.ts +139 -0
- package/dist/vocal_edit_worker_protocol.d.ts.map +1 -0
- package/dist/vocal_project.d.ts +17 -0
- package/dist/vocal_project.d.ts.map +1 -0
- package/dist/web_midi.d.ts +4 -0
- package/dist/web_midi.d.ts.map +1 -1
- package/dist/worker.js.map +1 -1
- package/dist/worklet/engine-mixer-facade.d.ts +42 -5
- package/dist/worklet/engine-mixer-facade.d.ts.map +1 -1
- package/dist/worklet/engine-node.d.ts +10 -0
- package/dist/worklet/engine-node.d.ts.map +1 -1
- package/dist/worklet/engine-processor.d.ts +6 -1
- package/dist/worklet/engine-processor.d.ts.map +1 -1
- package/dist/worklet/engine-register.d.ts.map +1 -1
- package/dist/worklet/engine-strips.d.ts +8 -4
- package/dist/worklet/engine-strips.d.ts.map +1 -1
- package/dist/worklet/engine-sync.d.ts +4 -2
- package/dist/worklet/engine-sync.d.ts.map +1 -1
- package/dist/worklet/engine.d.ts +42 -9
- package/dist/worklet/engine.d.ts.map +1 -1
- package/dist/worklet/guards.d.ts +10 -2
- package/dist/worklet/guards.d.ts.map +1 -1
- package/dist/worklet/messages.d.ts +54 -9
- package/dist/worklet/messages.d.ts.map +1 -1
- package/dist/worklet/mixer-processor.d.ts +2 -2
- package/dist/worklet/mixer-processor.d.ts.map +1 -1
- package/dist/worklet/protocol.d.ts +20 -2
- package/dist/worklet/protocol.d.ts.map +1 -1
- package/dist/worklet.d.ts +1 -1
- package/dist/worklet.d.ts.map +1 -1
- package/dist/worklet.js +808 -204
- package/dist/worklet.js.map +1 -1
- package/package.json +6 -1
- package/src/_effects_common.ts +36 -6
- package/src/effects_mastering.ts +4 -0
- package/src/effects_note_ops.ts +92 -44
- package/src/effects_timepitch.ts +42 -31
- package/src/feature_decompose.ts +2 -2
- package/src/index.ts +39 -0
- package/src/instrument_types.ts +29 -3
- package/src/mastering_core.ts +98 -2
- package/src/mixer.ts +41 -21
- package/src/project.ts +8 -1
- package/src/project_class.ts +136 -1
- package/src/project_internal.ts +90 -0
- package/src/project_types.ts +18 -6
- package/src/public_types.ts +1 -0
- package/src/public_types_mastering.ts +23 -0
- package/src/public_types_playback.ts +1 -0
- package/src/public_types_spectral.ts +5 -5
- package/src/public_types_vocal_edit.ts +289 -0
- package/src/public_types_vocal_project.ts +69 -0
- package/src/realtime_engine.ts +157 -24
- package/src/repair_dereverb.ts +1 -0
- package/src/repair_noise.ts +1 -0
- package/src/sonare.js.d.ts +89 -0
- package/src/streaming_processors.ts +8 -0
- package/src/vocal_edit.ts +885 -0
- package/src/vocal_edit_worker.ts +832 -0
- package/src/vocal_edit_worker_client.ts +812 -0
- package/src/vocal_edit_worker_protocol.ts +206 -0
- package/src/vocal_project.ts +327 -0
- package/src/web_midi.ts +100 -5
- package/src/worklet/engine-mixer-facade.ts +422 -87
- package/src/worklet/engine-node.ts +69 -4
- package/src/worklet/engine-processor.ts +129 -34
- package/src/worklet/engine-register.ts +3 -0
- package/src/worklet/engine-strips.ts +36 -14
- package/src/worklet/engine-sync.ts +6 -1
- package/src/worklet/engine.ts +76 -40
- package/src/worklet/guards.ts +81 -0
- package/src/worklet/messages.ts +63 -4
- package/src/worklet/mixer-processor.ts +10 -3
- package/src/worklet/protocol.ts +39 -8
- package/src/worklet.ts +2 -0
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
SidechainSourceKind,
|
|
8
8
|
} from '../index';
|
|
9
9
|
import { normalizeTrackLanes } from './engine-offline';
|
|
10
|
-
import { buildMixerLanes } from './engine-sync';
|
|
10
|
+
import { buildMixerLanes, resolveTargetId } from './engine-sync';
|
|
11
11
|
import type { SonareEngineSyncMessage, SonareEngineSyncMixerInsertParamOverride } from './messages';
|
|
12
12
|
import { ENGINE_MIXER_PARAM_FADER_DB, engineMixerBusTarget } from './protocol';
|
|
13
13
|
|
|
@@ -32,6 +32,7 @@ export interface EngineMixerContext {
|
|
|
32
32
|
readonly trackLaneIds: number[];
|
|
33
33
|
readonly trackSends: Map<number, EngineTrackSend[]>;
|
|
34
34
|
readonly trackOutputBus: Map<number, number>;
|
|
35
|
+
readonly trackSourceChannelLayout: Map<number, number>;
|
|
35
36
|
readonly laneSidechains: Map<
|
|
36
37
|
string,
|
|
37
38
|
{ trackId: number; insertIndex: number; sourceTrackId: number }
|
|
@@ -54,7 +55,10 @@ export interface EngineMixerContext {
|
|
|
54
55
|
ensureTrackLane(target: string | number): number;
|
|
55
56
|
ensureBus(busId: number): number;
|
|
56
57
|
mixerLanes(): EngineTrackLane[];
|
|
57
|
-
syncMixer(
|
|
58
|
+
syncMixer(
|
|
59
|
+
insertBaseResets?: StripJsonTarget[],
|
|
60
|
+
oneShotInsertParamOverrides?: InsertParamOverride[],
|
|
61
|
+
): void;
|
|
58
62
|
sendSmoothedParam(paramId: number, value: number): boolean;
|
|
59
63
|
getMasterStripJson(): string | undefined;
|
|
60
64
|
cacheMasterStripJson(sceneJson: string): void;
|
|
@@ -66,6 +70,212 @@ export type StripJsonTarget =
|
|
|
66
70
|
| { kind: 'bus'; busId: number }
|
|
67
71
|
| { kind: 'master' };
|
|
68
72
|
|
|
73
|
+
type SceneObject = Record<string, unknown>;
|
|
74
|
+
|
|
75
|
+
function isSceneObject(value: unknown): value is SceneObject {
|
|
76
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function hasOwn(object: SceneObject, key: string): boolean {
|
|
80
|
+
return Object.getOwnPropertyDescriptor(object, key) !== undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Canonicalizes JSON objects without changing array order or string contents. */
|
|
84
|
+
function canonicalJson(value: unknown): string {
|
|
85
|
+
if (Array.isArray(value)) {
|
|
86
|
+
return `[${value.map((item) => canonicalJson(item)).join(',')}]`;
|
|
87
|
+
}
|
|
88
|
+
if (value !== null && typeof value === 'object') {
|
|
89
|
+
const object = value as SceneObject;
|
|
90
|
+
return `{${Object.keys(object)
|
|
91
|
+
.sort()
|
|
92
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJson(object[key])}`)
|
|
93
|
+
.join(',')}}`;
|
|
94
|
+
}
|
|
95
|
+
return JSON.stringify(value);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function aliasedValue(
|
|
99
|
+
object: SceneObject,
|
|
100
|
+
primary: string,
|
|
101
|
+
legacy: string,
|
|
102
|
+
): { present: boolean; value: unknown } {
|
|
103
|
+
if (hasOwn(object, primary)) {
|
|
104
|
+
return { present: true, value: object[primary] };
|
|
105
|
+
}
|
|
106
|
+
if (hasOwn(object, legacy)) {
|
|
107
|
+
return { present: true, value: object[legacy] };
|
|
108
|
+
}
|
|
109
|
+
return { present: false, value: undefined };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function insertSignature(insert: unknown): string {
|
|
113
|
+
const object = insert !== null && typeof insert === 'object' ? (insert as SceneObject) : {};
|
|
114
|
+
const slot = object.slot === undefined ? 'pre' : object.slot;
|
|
115
|
+
const processor = aliasedValue(object, 'processor', 'processor_name');
|
|
116
|
+
const params = aliasedValue(object, 'params', 'params_json');
|
|
117
|
+
const sidechain = aliasedValue(object, 'sidechainKey', 'sidechain_key');
|
|
118
|
+
const processorValue = typeof processor.value === 'string' ? processor.value : '';
|
|
119
|
+
const sidechainValue = typeof sidechain.value === 'string' ? sidechain.value : '';
|
|
120
|
+
let paramsValue: unknown;
|
|
121
|
+
// A missing params bag and an empty legacy blob decode alike; "{}" is distinct.
|
|
122
|
+
if (!params.present) {
|
|
123
|
+
paramsValue = ['absent'];
|
|
124
|
+
} else if (typeof params.value === 'string') {
|
|
125
|
+
// Canonicalize legacy text like object params; invalid text stays byte-exact.
|
|
126
|
+
if (params.value.length === 0) {
|
|
127
|
+
paramsValue = ['absent'];
|
|
128
|
+
} else {
|
|
129
|
+
try {
|
|
130
|
+
paramsValue = ['params', canonicalJson(JSON.parse(params.value))];
|
|
131
|
+
} catch {
|
|
132
|
+
paramsValue = ['params-invalid', params.value];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
paramsValue = ['params', canonicalJson(params.value)];
|
|
137
|
+
}
|
|
138
|
+
return JSON.stringify([slot, processorValue, paramsValue, sidechainValue]);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Returns the insert identity used to decide whether native state can stay
|
|
143
|
+
* alive across a strip-scene update. Scalar strip fields are intentionally
|
|
144
|
+
* excluded. Track and master inserts use the native pre-then-post indexing;
|
|
145
|
+
* buses retain their scene order.
|
|
146
|
+
*/
|
|
147
|
+
export function decodedInsertSignature(sceneJson: string, target: StripJsonTarget): string {
|
|
148
|
+
const scene = JSON.parse(sceneJson) as {
|
|
149
|
+
strips?: unknown[];
|
|
150
|
+
buses?: unknown[];
|
|
151
|
+
};
|
|
152
|
+
const entries = target.kind === 'bus' ? scene.buses : scene.strips;
|
|
153
|
+
const strip = Array.isArray(entries) ? entries.find(isSceneObject) : undefined;
|
|
154
|
+
const object = isSceneObject(strip) ? strip : {};
|
|
155
|
+
const inserts = Array.isArray(object.inserts) ? object.inserts.filter(isSceneObject) : [];
|
|
156
|
+
const ordered =
|
|
157
|
+
target.kind === 'bus'
|
|
158
|
+
? inserts
|
|
159
|
+
: [
|
|
160
|
+
...inserts.filter((insert) => insert.slot !== 'post'),
|
|
161
|
+
...inserts.filter((insert) => insert.slot === 'post'),
|
|
162
|
+
];
|
|
163
|
+
return JSON.stringify(ordered.map(insertSignature));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function constructionValue(
|
|
167
|
+
engine: RealtimeEngine,
|
|
168
|
+
target: StripJsonTarget,
|
|
169
|
+
override: InsertParamOverride,
|
|
170
|
+
): number | undefined {
|
|
171
|
+
let parameterId = -1;
|
|
172
|
+
switch (target.kind) {
|
|
173
|
+
case 'track':
|
|
174
|
+
parameterId = engine.resolveTrackInsertAutomationId(
|
|
175
|
+
target.trackId,
|
|
176
|
+
override.insertIndex,
|
|
177
|
+
override.paramName,
|
|
178
|
+
);
|
|
179
|
+
break;
|
|
180
|
+
case 'bus':
|
|
181
|
+
parameterId = engine.resolveBusInsertAutomationId(
|
|
182
|
+
target.busId,
|
|
183
|
+
override.insertIndex,
|
|
184
|
+
override.paramName,
|
|
185
|
+
);
|
|
186
|
+
break;
|
|
187
|
+
case 'master':
|
|
188
|
+
parameterId = engine.resolveMasterInsertAutomationId(
|
|
189
|
+
override.insertIndex,
|
|
190
|
+
override.paramName,
|
|
191
|
+
);
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
if (parameterId < 0) {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
return engine.insertParameterConstructedValue(parameterId);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Builds one-shot restores for manual insert edits before their bookkeeping is forgotten. */
|
|
201
|
+
export function constructionInsertOverrides(
|
|
202
|
+
engine: RealtimeEngine,
|
|
203
|
+
target: StripJsonTarget,
|
|
204
|
+
overrides: InsertParamOverrideMap,
|
|
205
|
+
): InsertParamOverride[] {
|
|
206
|
+
const restored: InsertParamOverride[] = [];
|
|
207
|
+
for (const override of overrides.values()) {
|
|
208
|
+
if (!sameStripTarget(override.target, target)) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const value = constructionValue(engine, target, override);
|
|
212
|
+
if (value !== undefined) {
|
|
213
|
+
restored.push({ ...override, value });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return restored;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function applyInsertParamOverride(engine: RealtimeEngine, override: InsertParamOverride): void {
|
|
220
|
+
switch (override.target.kind) {
|
|
221
|
+
case 'track':
|
|
222
|
+
engine.restoreTrackStripInsertParamByName(
|
|
223
|
+
override.target.trackId,
|
|
224
|
+
override.insertIndex,
|
|
225
|
+
override.paramName,
|
|
226
|
+
override.value,
|
|
227
|
+
);
|
|
228
|
+
break;
|
|
229
|
+
case 'bus':
|
|
230
|
+
engine.restoreBusStripInsertParamByName(
|
|
231
|
+
override.target.busId,
|
|
232
|
+
override.insertIndex,
|
|
233
|
+
override.paramName,
|
|
234
|
+
override.value,
|
|
235
|
+
);
|
|
236
|
+
break;
|
|
237
|
+
case 'master':
|
|
238
|
+
engine.restoreMasterStripInsertParamByName(
|
|
239
|
+
override.insertIndex,
|
|
240
|
+
override.paramName,
|
|
241
|
+
override.value,
|
|
242
|
+
);
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Applies construction restores to the control-side mirror before posting them. */
|
|
248
|
+
export function applyConstructionInsertOverrides(
|
|
249
|
+
engine: RealtimeEngine,
|
|
250
|
+
overrides: readonly InsertParamOverride[],
|
|
251
|
+
): void {
|
|
252
|
+
for (const override of overrides) {
|
|
253
|
+
applyInsertParamOverride(engine, override);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Clears the native manual parameter bases for one strip after a replacement. */
|
|
258
|
+
export function clearInsertParameterBases(engine: RealtimeEngine, target: StripJsonTarget): void {
|
|
259
|
+
switch (target.kind) {
|
|
260
|
+
case 'track':
|
|
261
|
+
engine.clearTrackInsertParameterBases(target.trackId);
|
|
262
|
+
break;
|
|
263
|
+
case 'bus':
|
|
264
|
+
engine.clearBusInsertParameterBases(target.busId);
|
|
265
|
+
break;
|
|
266
|
+
case 'master':
|
|
267
|
+
engine.clearMasterInsertParameterBases();
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
interface MixerRoutingDraft {
|
|
273
|
+
trackLaneIds: number[];
|
|
274
|
+
trackSends: Map<number, EngineTrackSend[]>;
|
|
275
|
+
trackOutputBus: Map<number, number>;
|
|
276
|
+
trackSourceChannelLayout: Map<number, number>;
|
|
277
|
+
}
|
|
278
|
+
|
|
69
279
|
/** An empty construction scene used only within an explicit strip replacement. */
|
|
70
280
|
export function emptyStripJson(target: StripJsonTarget): string {
|
|
71
281
|
switch (target.kind) {
|
|
@@ -123,12 +333,11 @@ export function hasInsertParamOverrides(
|
|
|
123
333
|
return false;
|
|
124
334
|
}
|
|
125
335
|
|
|
126
|
-
/** Apply
|
|
336
|
+
/** Apply and validate one candidate scene without rebuilding it through an empty scene. */
|
|
127
337
|
export function applyFullStripJson(
|
|
128
338
|
engine: RealtimeEngine,
|
|
129
339
|
target: StripJsonTarget,
|
|
130
340
|
sceneJson: string,
|
|
131
|
-
resetInserts: boolean,
|
|
132
341
|
): void {
|
|
133
342
|
const apply = (json: string): void => {
|
|
134
343
|
switch (target.kind) {
|
|
@@ -145,21 +354,6 @@ export function applyFullStripJson(
|
|
|
145
354
|
};
|
|
146
355
|
engine.applyCommandsDueNowPreservingFuture();
|
|
147
356
|
apply(sceneJson);
|
|
148
|
-
if (resetInserts) {
|
|
149
|
-
switch (target.kind) {
|
|
150
|
-
case 'track':
|
|
151
|
-
engine.clearTrackInsertParameterBases(target.trackId);
|
|
152
|
-
break;
|
|
153
|
-
case 'bus':
|
|
154
|
-
engine.clearBusInsertParameterBases(target.busId);
|
|
155
|
-
break;
|
|
156
|
-
case 'master':
|
|
157
|
-
engine.clearMasterInsertParameterBases();
|
|
158
|
-
break;
|
|
159
|
-
}
|
|
160
|
-
apply(emptyStripJson(target));
|
|
161
|
-
apply(sceneJson);
|
|
162
|
-
}
|
|
163
357
|
}
|
|
164
358
|
|
|
165
359
|
/** Builds a collision-safe key for one target/insert/parameter tuple. */
|
|
@@ -264,53 +458,80 @@ export function cacheStripJson(
|
|
|
264
458
|
|
|
265
459
|
/** Builds the engine's track-lane descriptors from the current routing stores. */
|
|
266
460
|
export function mixerLanes(ctx: EngineMixerContext): EngineTrackLane[] {
|
|
267
|
-
return buildMixerLanes(
|
|
461
|
+
return buildMixerLanes(
|
|
462
|
+
ctx.trackLaneIds,
|
|
463
|
+
ctx.trackSends,
|
|
464
|
+
ctx.trackOutputBus,
|
|
465
|
+
ctx.trackSourceChannelLayout,
|
|
466
|
+
);
|
|
268
467
|
}
|
|
269
468
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
469
|
+
function cloneMixerRouting(ctx: EngineMixerContext): MixerRoutingDraft {
|
|
470
|
+
return {
|
|
471
|
+
trackLaneIds: [...ctx.trackLaneIds],
|
|
472
|
+
trackSends: new Map(
|
|
473
|
+
Array.from(ctx.trackSends, ([trackId, sends]) => [
|
|
474
|
+
trackId,
|
|
475
|
+
sends.map((send) => ({ ...send })),
|
|
476
|
+
]),
|
|
477
|
+
),
|
|
478
|
+
trackOutputBus: new Map(ctx.trackOutputBus),
|
|
479
|
+
trackSourceChannelLayout: new Map(ctx.trackSourceChannelLayout),
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function commitMixerRouting(ctx: EngineMixerContext, draft: MixerRoutingDraft): void {
|
|
484
|
+
ctx.trackLaneIds.splice(0, ctx.trackLaneIds.length, ...draft.trackLaneIds);
|
|
485
|
+
ctx.trackSends.clear();
|
|
486
|
+
for (const [trackId, sends] of draft.trackSends) {
|
|
487
|
+
ctx.trackSends.set(
|
|
488
|
+
trackId,
|
|
489
|
+
sends.map((send) => ({ ...send })),
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
ctx.trackOutputBus.clear();
|
|
493
|
+
for (const [trackId, busId] of draft.trackOutputBus) {
|
|
494
|
+
ctx.trackOutputBus.set(trackId, busId);
|
|
495
|
+
}
|
|
496
|
+
ctx.trackSourceChannelLayout.clear();
|
|
497
|
+
for (const [trackId, layout] of draft.trackSourceChannelLayout) {
|
|
498
|
+
ctx.trackSourceChannelLayout.set(trackId, layout);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function buildDraftMixerLanes(draft: MixerRoutingDraft): EngineTrackLane[] {
|
|
503
|
+
return buildMixerLanes(
|
|
504
|
+
draft.trackLaneIds,
|
|
505
|
+
draft.trackSends,
|
|
506
|
+
draft.trackOutputBus,
|
|
507
|
+
draft.trackSourceChannelLayout,
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function applyMixerRouting(
|
|
275
512
|
ctx: EngineMixerContext,
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
):
|
|
279
|
-
const lanes =
|
|
513
|
+
draft: MixerRoutingDraft,
|
|
514
|
+
busesAlreadyApplied: boolean,
|
|
515
|
+
): { lanes: EngineTrackLane[]; buses: EngineBus[] } {
|
|
516
|
+
const lanes = buildDraftMixerLanes(draft);
|
|
280
517
|
const buses = ctx.buses.map((bus) => ({ ...bus }));
|
|
281
518
|
if (!busesAlreadyApplied) {
|
|
282
519
|
ctx.offlineEngine.setTrackBuses(buses);
|
|
283
520
|
}
|
|
284
521
|
if (lanes.length > 0) {
|
|
285
|
-
|
|
522
|
+
// Not settled: that would snap unrelated insert ramps on a scalar edit.
|
|
286
523
|
ctx.offlineEngine.setTrackLanes(lanes);
|
|
287
524
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
for (const binding of ctx.busSidechains.values()) {
|
|
299
|
-
ctx.offlineEngine.setBusSidechain(
|
|
300
|
-
binding.busId,
|
|
301
|
-
binding.insertIndex,
|
|
302
|
-
binding.sourceKind,
|
|
303
|
-
binding.sourceId,
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
|
-
for (const binding of ctx.masterSidechains.values()) {
|
|
307
|
-
ctx.offlineEngine.setMasterSidechain(
|
|
308
|
-
binding.insertIndex,
|
|
309
|
-
binding.sourceKind,
|
|
310
|
-
binding.sourceId,
|
|
311
|
-
);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
525
|
+
return { lanes, buses };
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function postMixerSync(
|
|
529
|
+
ctx: EngineMixerContext,
|
|
530
|
+
lanes: EngineTrackLane[],
|
|
531
|
+
buses: EngineBus[],
|
|
532
|
+
insertBaseResets: StripJsonTarget[],
|
|
533
|
+
oneShotInsertParamOverrides: InsertParamOverride[],
|
|
534
|
+
): void {
|
|
314
535
|
replayInsertParamOverrides(ctx, lanes, buses);
|
|
315
536
|
const trackStrips = Array.from(ctx.trackStripJson, ([trackId, sceneJson]) => ({
|
|
316
537
|
trackId,
|
|
@@ -320,10 +541,10 @@ export function syncMixer(
|
|
|
320
541
|
busId,
|
|
321
542
|
sceneJson,
|
|
322
543
|
}));
|
|
323
|
-
const insertParamOverrides =
|
|
324
|
-
ctx.insertParamOverrides.values(),
|
|
325
|
-
flattenInsertParamOverride,
|
|
326
|
-
|
|
544
|
+
const insertParamOverrides = [
|
|
545
|
+
...Array.from(ctx.insertParamOverrides.values(), flattenInsertParamOverride),
|
|
546
|
+
...oneShotInsertParamOverrides.map(flattenInsertParamOverride),
|
|
547
|
+
];
|
|
327
548
|
ctx.postSync({
|
|
328
549
|
type: 'syncMixer',
|
|
329
550
|
lanes,
|
|
@@ -335,10 +556,41 @@ export function syncMixer(
|
|
|
335
556
|
busSidechains: Array.from(ctx.busSidechains.values()),
|
|
336
557
|
masterSidechains: Array.from(ctx.masterSidechains.values()),
|
|
337
558
|
...(insertParamOverrides.length > 0 ? { insertParamOverrides } : {}),
|
|
338
|
-
...(
|
|
559
|
+
...(insertBaseResets.length > 0 ? { insertBaseResets } : {}),
|
|
339
560
|
});
|
|
340
561
|
}
|
|
341
562
|
|
|
563
|
+
function syncMixerDraft(
|
|
564
|
+
ctx: EngineMixerContext,
|
|
565
|
+
draft: MixerRoutingDraft,
|
|
566
|
+
busesAlreadyApplied = false,
|
|
567
|
+
insertBaseResets: StripJsonTarget[] = [],
|
|
568
|
+
oneShotInsertParamOverrides: InsertParamOverride[] = [],
|
|
569
|
+
): void {
|
|
570
|
+
const { lanes, buses } = applyMixerRouting(ctx, draft, busesAlreadyApplied);
|
|
571
|
+
commitMixerRouting(ctx, draft);
|
|
572
|
+
postMixerSync(ctx, lanes, buses, insertBaseResets, oneShotInsertParamOverrides);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Mirrors the current mixer routing into the offline engine and posts the full
|
|
577
|
+
* mixer-sync message (lanes, buses, strip JSON, sidechains) to the worklet.
|
|
578
|
+
*/
|
|
579
|
+
export function syncMixer(
|
|
580
|
+
ctx: EngineMixerContext,
|
|
581
|
+
busesAlreadyApplied = false,
|
|
582
|
+
insertBaseResets: StripJsonTarget[] = [],
|
|
583
|
+
oneShotInsertParamOverrides: InsertParamOverride[] = [],
|
|
584
|
+
): void {
|
|
585
|
+
syncMixerDraft(
|
|
586
|
+
ctx,
|
|
587
|
+
cloneMixerRouting(ctx),
|
|
588
|
+
busesAlreadyApplied,
|
|
589
|
+
insertBaseResets,
|
|
590
|
+
oneShotInsertParamOverrides,
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
|
|
342
594
|
/**
|
|
343
595
|
* Reapplies retained by-name values after native topology setters clear their
|
|
344
596
|
* smoother slots. This runs once per mixer sync, never once per knob tick.
|
|
@@ -397,6 +649,7 @@ export function replayInsertParamOverrides(
|
|
|
397
649
|
* with the already-declared lane ids in their current order and may only
|
|
398
650
|
* append new track ids after them. Entries carrying `sends` replace that
|
|
399
651
|
* track's send list; entries without `sends` leave existing sends untouched.
|
|
652
|
+
* The same omission rule preserves an existing source channel layout.
|
|
400
653
|
*
|
|
401
654
|
* @param lanes Track ids or lane descriptors in the desired lane order.
|
|
402
655
|
*/
|
|
@@ -404,24 +657,51 @@ export function setTrackLanes(
|
|
|
404
657
|
ctx: EngineMixerContext,
|
|
405
658
|
lanes: ReadonlyArray<number | EngineTrackLane>,
|
|
406
659
|
): void {
|
|
407
|
-
const
|
|
660
|
+
const draft = cloneMixerRouting(ctx);
|
|
661
|
+
const { entries, ids } = normalizeTrackLanes(draft.trackLaneIds, lanes);
|
|
408
662
|
for (const entry of entries) {
|
|
409
663
|
if (entry.sends) {
|
|
410
|
-
|
|
664
|
+
draft.trackSends.set(
|
|
411
665
|
entry.trackId,
|
|
412
666
|
entry.sends.map((send) => ({ ...send })),
|
|
413
667
|
);
|
|
414
668
|
}
|
|
415
669
|
if (entry.outputBusId !== undefined) {
|
|
416
670
|
if (entry.outputBusId === 0) {
|
|
417
|
-
|
|
671
|
+
draft.trackOutputBus.delete(entry.trackId);
|
|
418
672
|
} else {
|
|
419
|
-
|
|
673
|
+
draft.trackOutputBus.set(entry.trackId, entry.outputBusId);
|
|
420
674
|
}
|
|
421
675
|
}
|
|
676
|
+
if (entry.sourceChannelLayout !== undefined) {
|
|
677
|
+
draft.trackSourceChannelLayout.set(entry.trackId, entry.sourceChannelLayout);
|
|
678
|
+
}
|
|
422
679
|
}
|
|
423
|
-
|
|
424
|
-
ctx
|
|
680
|
+
draft.trackLaneIds = ids;
|
|
681
|
+
syncMixerDraft(ctx, draft, true);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function ensureTrackLaneInDraft(draft: MixerRoutingDraft, target: string | number): number {
|
|
685
|
+
const trackId = resolveTargetId(target);
|
|
686
|
+
if (!Number.isInteger(trackId) || trackId <= 0) {
|
|
687
|
+
throw new RangeError(`Invalid track id for mixer lane: ${String(target)}`);
|
|
688
|
+
}
|
|
689
|
+
const existing = draft.trackLaneIds.indexOf(trackId);
|
|
690
|
+
if (existing >= 0) {
|
|
691
|
+
return existing;
|
|
692
|
+
}
|
|
693
|
+
draft.trackLaneIds.push(trackId);
|
|
694
|
+
return draft.trackLaneIds.length - 1;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/** Ensures a lane exists, committing it only after native routing accepts it. */
|
|
698
|
+
export function ensureTrackLane(ctx: EngineMixerContext, target: string | number): number {
|
|
699
|
+
const draft = cloneMixerRouting(ctx);
|
|
700
|
+
const laneIndex = ensureTrackLaneInDraft(draft, target);
|
|
701
|
+
if (draft.trackLaneIds.length !== ctx.trackLaneIds.length) {
|
|
702
|
+
syncMixerDraft(ctx, draft, true);
|
|
703
|
+
}
|
|
704
|
+
return laneIndex;
|
|
425
705
|
}
|
|
426
706
|
|
|
427
707
|
/**
|
|
@@ -433,20 +713,22 @@ export function setTrackOutputBus(
|
|
|
433
713
|
target: string | number,
|
|
434
714
|
busId: number,
|
|
435
715
|
): void {
|
|
436
|
-
const
|
|
437
|
-
const
|
|
716
|
+
const draft = cloneMixerRouting(ctx);
|
|
717
|
+
const laneIndex = ensureTrackLaneInDraft(draft, target);
|
|
718
|
+
const trackId = draft.trackLaneIds[laneIndex];
|
|
438
719
|
if (busId === 0) {
|
|
439
|
-
|
|
720
|
+
draft.trackOutputBus.delete(trackId);
|
|
440
721
|
} else {
|
|
441
|
-
|
|
722
|
+
draft.trackOutputBus.set(trackId, busId);
|
|
442
723
|
}
|
|
443
|
-
ctx
|
|
724
|
+
syncMixerDraft(ctx, draft, true);
|
|
444
725
|
}
|
|
445
726
|
|
|
446
727
|
/**
|
|
447
728
|
* Keys one insert of a lane strip from another lane's post-strip pre-fader
|
|
448
729
|
* audio (ducking/sidechainRouter inserts). sourceTarget null removes the
|
|
449
|
-
* binding.
|
|
730
|
+
* binding. The binding is cached for resync only once the engine has accepted
|
|
731
|
+
* it, so a refused (self-keyed or cyclic) binding throws and is never replayed.
|
|
450
732
|
*/
|
|
451
733
|
export function setLaneSidechain(
|
|
452
734
|
ctx: EngineMixerContext,
|
|
@@ -552,13 +834,14 @@ export function setSends(
|
|
|
552
834
|
target: string | number,
|
|
553
835
|
sends: EngineTrackSend[],
|
|
554
836
|
): void {
|
|
555
|
-
const
|
|
556
|
-
const
|
|
557
|
-
|
|
837
|
+
const draft = cloneMixerRouting(ctx);
|
|
838
|
+
const laneIndex = ensureTrackLaneInDraft(draft, target);
|
|
839
|
+
const trackId = draft.trackLaneIds[laneIndex];
|
|
840
|
+
draft.trackSends.set(
|
|
558
841
|
trackId,
|
|
559
842
|
sends.map((send) => ({ ...send })),
|
|
560
843
|
);
|
|
561
|
-
ctx
|
|
844
|
+
syncMixerDraft(ctx, draft, true);
|
|
562
845
|
}
|
|
563
846
|
|
|
564
847
|
export function setTrackBuses(ctx: EngineMixerContext, buses: EngineBus[]): void {
|
|
@@ -603,18 +886,70 @@ export function setBusGain(ctx: EngineMixerContext, busId: number, db: number):
|
|
|
603
886
|
return ctx.sendSmoothedParam(engineMixerBusTarget(busIndex, ENGINE_MIXER_PARAM_FADER_DB), db);
|
|
604
887
|
}
|
|
605
888
|
|
|
889
|
+
/** Control-side state a strip-scene replacement reads and updates. */
|
|
890
|
+
export interface StripSceneContext {
|
|
891
|
+
readonly offlineEngine: RealtimeEngine;
|
|
892
|
+
readonly insertParamOverrides: InsertParamOverrideMap;
|
|
893
|
+
clearInsertAutomationLanes(target: StripJsonTarget): void;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Applies a strip scene to the offline mirror and returns what the realtime
|
|
898
|
+
* sync must repeat: the targets whose manual insert bases it clears and the
|
|
899
|
+
* construction values it restores. A changed insert chain forgets manual edits;
|
|
900
|
+
* an equal chain with manual edits restores each edited parameter's
|
|
901
|
+
* construction value. Native state of equal inserts is kept either way.
|
|
902
|
+
*/
|
|
903
|
+
export function replaceStripScene(
|
|
904
|
+
ctx: StripSceneContext,
|
|
905
|
+
target: StripJsonTarget,
|
|
906
|
+
previousJson: string | undefined,
|
|
907
|
+
sceneJson: string,
|
|
908
|
+
): { insertBaseResets: StripJsonTarget[]; constructionOverrides: InsertParamOverride[] } {
|
|
909
|
+
const chainChanged =
|
|
910
|
+
previousJson !== undefined &&
|
|
911
|
+
decodedInsertSignature(previousJson, target) !== decodedInsertSignature(sceneJson, target);
|
|
912
|
+
const hasOverrides = hasInsertParamOverrides(ctx.insertParamOverrides, target);
|
|
913
|
+
applyFullStripJson(ctx.offlineEngine, target, sceneJson);
|
|
914
|
+
if (!chainChanged && !hasOverrides) {
|
|
915
|
+
return { insertBaseResets: [], constructionOverrides: [] };
|
|
916
|
+
}
|
|
917
|
+
const constructionOverrides = chainChanged
|
|
918
|
+
? []
|
|
919
|
+
: constructionInsertOverrides(ctx.offlineEngine, target, ctx.insertParamOverrides);
|
|
920
|
+
ctx.clearInsertAutomationLanes(target);
|
|
921
|
+
clearInsertParameterBases(ctx.offlineEngine, target);
|
|
922
|
+
applyConstructionInsertOverrides(ctx.offlineEngine, constructionOverrides);
|
|
923
|
+
clearInsertParamOverrides(ctx.insertParamOverrides, target);
|
|
924
|
+
return { insertBaseResets: [target], constructionOverrides };
|
|
925
|
+
}
|
|
926
|
+
|
|
606
927
|
export function setBusStripJson(ctx: EngineMixerContext, busId: number, sceneJson: string): void {
|
|
607
|
-
ctx.ensureBus(busId);
|
|
608
928
|
const target: StripJsonTarget = { kind: 'bus', busId };
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
929
|
+
// Parse before declaring a new bus so malformed text cannot declare one.
|
|
930
|
+
decodedInsertSignature(sceneJson, target);
|
|
931
|
+
ctx.ensureBus(busId);
|
|
932
|
+
const { insertBaseResets, constructionOverrides } = replaceStripScene(
|
|
933
|
+
ctx,
|
|
934
|
+
target,
|
|
935
|
+
ctx.busStripJson.get(busId),
|
|
936
|
+
sceneJson,
|
|
937
|
+
);
|
|
616
938
|
pruneStripSidechains(ctx, target, sceneJson);
|
|
617
939
|
ctx.busStripJson.set(busId, sceneJson);
|
|
618
|
-
|
|
619
|
-
|
|
940
|
+
ctx.syncMixer(insertBaseResets, constructionOverrides);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/** Applies a master strip scene while retaining native state for equal inserts. */
|
|
944
|
+
export function setMasterStripJson(ctx: EngineMixerContext, sceneJson: string): void {
|
|
945
|
+
const target: StripJsonTarget = { kind: 'master' };
|
|
946
|
+
const { insertBaseResets, constructionOverrides } = replaceStripScene(
|
|
947
|
+
ctx,
|
|
948
|
+
target,
|
|
949
|
+
ctx.getMasterStripJson(),
|
|
950
|
+
sceneJson,
|
|
951
|
+
);
|
|
952
|
+
pruneStripSidechains(ctx, target, sceneJson);
|
|
953
|
+
ctx.cacheMasterStripJson(sceneJson);
|
|
954
|
+
ctx.syncMixer(insertBaseResets, constructionOverrides);
|
|
620
955
|
}
|