@libraz/libsonare 1.5.2 → 1.5.4
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 +69 -725
- package/dist/index.d.ts +1155 -183
- package/dist/index.js +4924 -3935
- package/dist/index.js.map +1 -1
- package/dist/sonare.js +1 -1
- package/dist/sonare.wasm +0 -0
- package/dist/worklet.d.ts +29 -1
- package/dist/worklet.js +1141 -970
- package/dist/worklet.js.map +1 -1
- package/package.json +15 -3
- package/src/_chain_config.ts +46 -0
- package/src/audio.ts +24 -4
- package/src/codes.ts +27 -4
- package/src/effects_mastering.ts +25 -1
- package/src/effects_transform.ts +292 -46
- package/src/effects_voice_change.ts +46 -53
- package/src/feature_core.ts +316 -12
- package/src/feature_music.ts +371 -4
- package/src/feature_pitch.ts +61 -0
- package/src/feature_resample.ts +17 -2
- package/src/feature_spectral.ts +349 -2
- package/src/feature_spectrogram.ts +336 -0
- package/src/index.ts +130 -1
- package/src/mastering_chain.ts +305 -50
- package/src/mastering_core.ts +232 -16
- package/src/mastering_dynamics.ts +66 -10
- package/src/mastering_repair.ts +119 -7
- package/src/metering.ts +370 -83
- package/src/mixer.ts +9 -2
- package/src/mixing_oneshot.ts +25 -5
- package/src/module_state.ts +1 -1
- package/src/project_class.ts +30 -6
- package/src/project_internal.ts +12 -2
- package/src/project_types.ts +25 -2
- package/src/public_types_mastering.ts +114 -4
- package/src/public_types_spectral.ts +7 -0
- package/src/quick_analysis.ts +325 -117
- package/src/realtime_engine.ts +8 -0
- package/src/realtime_voice_changer.ts +79 -3
- package/src/sonare.js.d.ts +35 -0
- package/src/stream_analyzer.ts +20 -1
- package/src/stream_types.ts +12 -1
- package/src/validation.ts +17 -2
- package/src/worklet/engine-clips.ts +67 -2
- package/src/worklet/engine-processor.ts +33 -5
- package/src/worklet/engine.ts +7 -3
- package/src/worklet/guards.ts +3 -0
- package/src/worklet/messages.ts +27 -0
package/dist/worklet.js
CHANGED
|
@@ -170,6 +170,8 @@ function panLawCode(panLaw) {
|
|
|
170
170
|
return panLaw;
|
|
171
171
|
}
|
|
172
172
|
switch (panLaw) {
|
|
173
|
+
case "const3dB":
|
|
174
|
+
return 0;
|
|
173
175
|
case "const4.5dB":
|
|
174
176
|
return 1;
|
|
175
177
|
case "const6dB":
|
|
@@ -177,7 +179,7 @@ function panLawCode(panLaw) {
|
|
|
177
179
|
case "linear0dB":
|
|
178
180
|
return 3;
|
|
179
181
|
default:
|
|
180
|
-
|
|
182
|
+
throw new Error(`Invalid pan law: ${panLaw}`);
|
|
181
183
|
}
|
|
182
184
|
}
|
|
183
185
|
function panModeCode(panMode) {
|
|
@@ -185,6 +187,8 @@ function panModeCode(panMode) {
|
|
|
185
187
|
return panMode;
|
|
186
188
|
}
|
|
187
189
|
switch (panMode) {
|
|
190
|
+
case "balance":
|
|
191
|
+
return 0;
|
|
188
192
|
case "stereoPan":
|
|
189
193
|
case "stereo-pan":
|
|
190
194
|
return 1;
|
|
@@ -192,1212 +196,1298 @@ function panModeCode(panMode) {
|
|
|
192
196
|
case "dual-pan":
|
|
193
197
|
return 2;
|
|
194
198
|
default:
|
|
195
|
-
|
|
199
|
+
throw new Error(`Invalid pan mode: ${panMode}`);
|
|
196
200
|
}
|
|
197
201
|
}
|
|
198
202
|
function meterTapCode(tap) {
|
|
199
|
-
|
|
203
|
+
if (typeof tap === "number") {
|
|
204
|
+
return tap;
|
|
205
|
+
}
|
|
206
|
+
switch (tap) {
|
|
207
|
+
case "preFader":
|
|
208
|
+
return 0;
|
|
209
|
+
case "postFader":
|
|
210
|
+
return 1;
|
|
211
|
+
default:
|
|
212
|
+
throw new Error(`Invalid meter tap: ${tap}`);
|
|
213
|
+
}
|
|
200
214
|
}
|
|
201
215
|
function sendTimingCode(timing) {
|
|
202
216
|
if (typeof timing === "number") {
|
|
203
217
|
return timing;
|
|
204
218
|
}
|
|
205
|
-
|
|
219
|
+
switch (timing) {
|
|
220
|
+
case "postFader":
|
|
221
|
+
return 0;
|
|
222
|
+
case "preFader":
|
|
223
|
+
return 1;
|
|
224
|
+
default:
|
|
225
|
+
throw new Error(`Invalid send timing: ${timing}`);
|
|
226
|
+
}
|
|
206
227
|
}
|
|
207
228
|
|
|
208
|
-
// src/
|
|
209
|
-
var
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
229
|
+
// src/realtime_engine.ts
|
|
230
|
+
var EXPECTED_ENGINE_ABI_VERSION = 3;
|
|
231
|
+
function engineCapabilities() {
|
|
232
|
+
const abiVersion = getSonareModule().engineAbiVersion();
|
|
233
|
+
const sharedArrayBuffer = typeof globalThis.SharedArrayBuffer === "function";
|
|
234
|
+
const atomics = typeof globalThis.Atomics === "object";
|
|
235
|
+
const audioWorklet = typeof AudioWorkletNode !== "undefined" || typeof globalThis.AudioWorkletProcessor !== "undefined";
|
|
236
|
+
return {
|
|
237
|
+
engineAbiVersion: abiVersion,
|
|
238
|
+
expectedEngineAbiVersion: EXPECTED_ENGINE_ABI_VERSION,
|
|
239
|
+
abiCompatible: abiVersion === EXPECTED_ENGINE_ABI_VERSION,
|
|
240
|
+
sharedArrayBuffer,
|
|
241
|
+
atomics,
|
|
242
|
+
audioWorklet,
|
|
243
|
+
mode: sharedArrayBuffer && atomics ? "sab" : "postMessage"
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
var RealtimeEngine = class {
|
|
247
|
+
constructor(sampleRate = 48e3, maxBlockSize = 128, commandCapacity = 1024, telemetryCapacity = 1024) {
|
|
221
248
|
const module2 = getSonareModule();
|
|
222
|
-
|
|
249
|
+
const capabilities = engineCapabilities();
|
|
250
|
+
if (!capabilities.abiCompatible) {
|
|
251
|
+
throw new Error(
|
|
252
|
+
`Engine ABI mismatch: wasm=${capabilities.engineAbiVersion}, expected=${capabilities.expectedEngineAbiVersion}`
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
this.native = new module2.RealtimeEngine(
|
|
256
|
+
sampleRate,
|
|
257
|
+
maxBlockSize,
|
|
258
|
+
commandCapacity,
|
|
259
|
+
telemetryCapacity
|
|
260
|
+
);
|
|
223
261
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
this.mixer.compile();
|
|
262
|
+
prepare(sampleRate, maxBlockSize, commandCapacity = 1024, telemetryCapacity = 1024) {
|
|
263
|
+
this.native.prepare(sampleRate, maxBlockSize, commandCapacity, telemetryCapacity);
|
|
227
264
|
}
|
|
228
|
-
/**
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
* (a likely typo, or a key meant for a different processor). The scene still
|
|
232
|
-
* loaded; these keys simply took no effect. Empty when every key was consumed.
|
|
233
|
-
* Use {@link masteringInsertParamNames} to discover the keys an insert accepts.
|
|
234
|
-
*/
|
|
235
|
-
sceneWarnings() {
|
|
236
|
-
return this.mixer.sceneWarnings();
|
|
265
|
+
/** Queue a sample-accurate parameter change (engine kSetParam). */
|
|
266
|
+
setParameter(paramId, value, renderFrame = -1) {
|
|
267
|
+
this.native.setParameter(paramId, value, renderFrame);
|
|
237
268
|
}
|
|
238
|
-
/**
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
* @param leftChannels - `leftChannels[i]` is the left channel of strip `i`
|
|
242
|
-
* @param rightChannels - `rightChannels[i]` is the right channel of strip `i`
|
|
243
|
-
* @returns Mixed stereo master (`left`, `right`, `sampleRate`)
|
|
244
|
-
*/
|
|
245
|
-
processStereo(leftChannels, rightChannels) {
|
|
246
|
-
if (leftChannels.length !== rightChannels.length) {
|
|
247
|
-
throw new Error("leftChannels and rightChannels must have the same length.");
|
|
248
|
-
}
|
|
249
|
-
return this.mixer.processStereo(leftChannels, rightChannels);
|
|
269
|
+
/** Queue a smoothed parameter change (engine kSetParamSmoothed). */
|
|
270
|
+
setParameterSmoothed(paramId, value, renderFrame = -1) {
|
|
271
|
+
this.native.setParameterSmoothed(paramId, value, renderFrame);
|
|
250
272
|
}
|
|
251
273
|
/**
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
* intended for realtime bridges such as AudioWorklet; the input channel count
|
|
256
|
-
* must match the scene strip count and all arrays must have the same length.
|
|
274
|
+
* Set the default ramp time (ms) for engine-level smoothed parameters —
|
|
275
|
+
* fader/pan glides, insert-parameter automation, and MIDI-CC mappings. The
|
|
276
|
+
* default is 20 ms; pass `0` for instant (un-ramped) changes.
|
|
257
277
|
*/
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
throw new Error("leftChannels and rightChannels must have the same length.");
|
|
261
|
-
}
|
|
262
|
-
if (outLeft.length !== outRight.length) {
|
|
263
|
-
throw new Error("outLeft and outRight must have the same length.");
|
|
264
|
-
}
|
|
265
|
-
this.mixer.processStereoInto(leftChannels, rightChannels, outLeft, outRight);
|
|
278
|
+
setParamSmoothingMs(smoothingMs) {
|
|
279
|
+
this.native.setParamSmoothingMs(smoothingMs);
|
|
266
280
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
*
|
|
270
|
-
* Fill `leftInputs[i]` / `rightInputs[i]`, call `process()`, then read
|
|
271
|
-
* `outLeft` / `outRight`. The views are owned by this mixer and become invalid
|
|
272
|
-
* after {@link delete}.
|
|
273
|
-
*/
|
|
274
|
-
createRealtimeBuffer() {
|
|
275
|
-
const stripCount = this.stripCount();
|
|
276
|
-
let leftInputs = [];
|
|
277
|
-
let rightInputs = [];
|
|
278
|
-
let outLeft = this.mixer.outputLeftView();
|
|
279
|
-
let outRight = this.mixer.outputRightView();
|
|
280
|
-
const acquire = () => {
|
|
281
|
-
leftInputs = [];
|
|
282
|
-
rightInputs = [];
|
|
283
|
-
for (let index = 0; index < stripCount; index++) {
|
|
284
|
-
leftInputs.push(this.mixer.inputLeftView(index));
|
|
285
|
-
rightInputs.push(this.mixer.inputRightView(index));
|
|
286
|
-
}
|
|
287
|
-
outLeft = this.mixer.outputLeftView();
|
|
288
|
-
outRight = this.mixer.outputRightView();
|
|
289
|
-
};
|
|
290
|
-
acquire();
|
|
291
|
-
const reacquireIfDetached = () => {
|
|
292
|
-
if (outLeft.byteLength === 0 || (leftInputs[0]?.byteLength ?? 1) === 0) {
|
|
293
|
-
acquire();
|
|
294
|
-
}
|
|
295
|
-
};
|
|
296
|
-
return {
|
|
297
|
-
get leftInputs() {
|
|
298
|
-
reacquireIfDetached();
|
|
299
|
-
return leftInputs;
|
|
300
|
-
},
|
|
301
|
-
get rightInputs() {
|
|
302
|
-
reacquireIfDetached();
|
|
303
|
-
return rightInputs;
|
|
304
|
-
},
|
|
305
|
-
get outLeft() {
|
|
306
|
-
reacquireIfDetached();
|
|
307
|
-
return outLeft;
|
|
308
|
-
},
|
|
309
|
-
get outRight() {
|
|
310
|
-
reacquireIfDetached();
|
|
311
|
-
return outRight;
|
|
312
|
-
},
|
|
313
|
-
process: (numSamples = outLeft.length) => {
|
|
314
|
-
reacquireIfDetached();
|
|
315
|
-
this.mixer.processPreparedStereo(numSamples);
|
|
316
|
-
}
|
|
317
|
-
};
|
|
281
|
+
setSoloMute(laneIndex, solo, mute, renderFrame = -1) {
|
|
282
|
+
this.native.setSoloMute(laneIndex, solo, mute, renderFrame);
|
|
318
283
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
284
|
+
setMidiClips(clips) {
|
|
285
|
+
this.native.setMidiClips(clips);
|
|
286
|
+
}
|
|
287
|
+
setBuiltinInstrument(config = {}, destinationId = config.destinationId ?? 0) {
|
|
288
|
+
this.native.setBuiltinInstrument(destinationId, config);
|
|
322
289
|
}
|
|
323
290
|
/**
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
* @
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
* advances an internal position from 0 on the first {@link processStereo}
|
|
332
|
-
* call; recompiling resets it to 0)
|
|
333
|
-
* @param value - Target parameter value
|
|
334
|
-
* @param curve - Interpolation curve (default: `'linear'`)
|
|
335
|
-
* @throws If the strip index is out of range or the schedule call fails
|
|
336
|
-
* (unknown curve, out-of-range insert index, or full event lane)
|
|
291
|
+
* Bind the patch-driven NativeSynth to a realtime MIDI destination. `patch`
|
|
292
|
+
* is a {@link SynthPatch} or a preset-name string (`'saw-lead'` /
|
|
293
|
+
* `'va:saw-lead'`; see {@link synthPresetNames}), resolving exactly like
|
|
294
|
+
* {@link Project.bounceWithSynthInstrument}. Live note/CC commands and
|
|
295
|
+
* scheduled MIDI clips routed to that destination render through the synth.
|
|
296
|
+
* Unknown preset names throw. An object patch's `destinationId` is a JS
|
|
297
|
+
* binding convenience, not part of the NativeSynth patch itself.
|
|
337
298
|
*/
|
|
338
|
-
|
|
339
|
-
this.
|
|
340
|
-
stripIndex,
|
|
341
|
-
insertIndex,
|
|
342
|
-
paramId,
|
|
343
|
-
samplePos,
|
|
344
|
-
value,
|
|
345
|
-
automationCurveCode(curve)
|
|
346
|
-
);
|
|
299
|
+
setSynthInstrument(patch = {}, destinationId = (typeof patch === "object" ? patch.destinationId : void 0) ?? 0) {
|
|
300
|
+
this.native.setSynthInstrument(destinationId, patch);
|
|
347
301
|
}
|
|
348
302
|
/**
|
|
349
|
-
*
|
|
350
|
-
*
|
|
303
|
+
* Load (parse) SoundFont 2 bytes into the engine so SF2 instruments can be
|
|
304
|
+
* bound with {@link setSf2Instrument}. The host fetches the `.sf2` and
|
|
305
|
+
* passes the raw bytes; they are copied into linear memory for the call and
|
|
306
|
+
* not referenced afterwards. Replaces any previously loaded SoundFont.
|
|
351
307
|
*/
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
return index < 0 ? null : index;
|
|
308
|
+
loadSoundFont(data) {
|
|
309
|
+
this.native.loadSoundFont(data);
|
|
355
310
|
}
|
|
356
311
|
/**
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
312
|
+
* Bind a GS-compatible SoundFont player to a realtime MIDI destination, fed
|
|
313
|
+
* by the engine's loaded SoundFont ({@link loadSoundFont}). Live note/CC
|
|
314
|
+
* commands and scheduled MIDI clips routed to that destination render
|
|
315
|
+
* through the player (16 MIDI channels, channel 10 drums, GS NRPN part
|
|
316
|
+
* edits, GS/GM SysEx resets). Without a loaded SoundFont — or for programs
|
|
317
|
+
* the SoundFont does not cover — notes play through the built-in
|
|
318
|
+
* synthesizer GM fallback bank (the data-free floor).
|
|
360
319
|
*/
|
|
361
|
-
|
|
362
|
-
this.
|
|
320
|
+
setSf2Instrument(config = {}, destinationId = config.destinationId ?? 0) {
|
|
321
|
+
this.native.setSf2Instrument(destinationId, config);
|
|
363
322
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
this.mixer.removeBus(id);
|
|
323
|
+
clearMidiInstrument(destinationId = 0) {
|
|
324
|
+
this.native.clearMidiInstrument(destinationId);
|
|
367
325
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
return this.mixer.busCount();
|
|
326
|
+
midiInstrumentCount() {
|
|
327
|
+
return this.native.midiInstrumentCount();
|
|
371
328
|
}
|
|
372
329
|
/**
|
|
373
|
-
*
|
|
374
|
-
*
|
|
330
|
+
* Bind a live MIDI CC to an engine automation parameter. The MIDI event still
|
|
331
|
+
* reaches the destination instrument; when bound, its 7-bit value is also
|
|
332
|
+
* mapped into [minValue, maxValue] for `paramId`.
|
|
375
333
|
*/
|
|
376
|
-
|
|
377
|
-
this.
|
|
334
|
+
bindMidiCc(channel, controller, paramId, options = {}) {
|
|
335
|
+
this.native.bindMidiCc(
|
|
336
|
+
channel,
|
|
337
|
+
controller,
|
|
338
|
+
paramId,
|
|
339
|
+
options.minValue ?? 0,
|
|
340
|
+
options.maxValue ?? 1
|
|
341
|
+
);
|
|
378
342
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
this.mixer.setVcaGroupGainDb(id, gainDb);
|
|
343
|
+
clearMidiCcBindings() {
|
|
344
|
+
this.native.clearMidiCcBindings();
|
|
382
345
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
this.mixer.removeVcaGroup(id);
|
|
386
|
-
}
|
|
387
|
-
/** Number of VCA groups in the mixer topology. */
|
|
388
|
-
vcaGroupCount() {
|
|
389
|
-
return this.mixer.vcaGroupCount();
|
|
346
|
+
midiCcBindingCount() {
|
|
347
|
+
return this.native.midiCcBindingCount();
|
|
390
348
|
}
|
|
391
|
-
/**
|
|
392
|
-
|
|
393
|
-
this.
|
|
349
|
+
/** Install/replace a live non-destructive MIDI-FX insert for one destination. */
|
|
350
|
+
setMidiFx(destinationId, configJson) {
|
|
351
|
+
this.native.setMidiFx(destinationId, configJson);
|
|
394
352
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
this.mixer.setFaderDb(stripIndex, db);
|
|
353
|
+
clearMidiFx(destinationId = 0) {
|
|
354
|
+
this.native.clearMidiFx(destinationId);
|
|
398
355
|
}
|
|
399
|
-
/**
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
403
|
-
* @param pan - Pan position in `[-1, 1]`
|
|
404
|
-
* @param panMode - Optional pan mode. When omitted the strip's current pan
|
|
405
|
-
* mode is kept (passes `SONARE_PAN_MODE_KEEP`), so a plain pan nudge does
|
|
406
|
-
* not reset a scene-defined `'stereoPan'` / `'dualPan'` mode back to
|
|
407
|
-
* balance. Pass `'balance'` (or `0`) explicitly to force balance mode.
|
|
408
|
-
*/
|
|
409
|
-
setPan(stripIndex, pan, panMode) {
|
|
410
|
-
const mode = panMode === void 0 ? -1 : panModeCode(panMode);
|
|
411
|
-
this.mixer.setPan(stripIndex, pan, mode);
|
|
356
|
+
/** Enable the engine-owned live MIDI input source for a destination. */
|
|
357
|
+
setMidiInputSource(destinationId = 0) {
|
|
358
|
+
this.native.setMidiInputSource(destinationId);
|
|
412
359
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
this.mixer.setWidth(stripIndex, width);
|
|
360
|
+
clearMidiInputSource() {
|
|
361
|
+
this.native.clearMidiInputSource();
|
|
416
362
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
this.mixer.setMuted(stripIndex, muted);
|
|
363
|
+
midiInputPendingCount() {
|
|
364
|
+
return this.native.midiInputPendingCount();
|
|
420
365
|
}
|
|
421
366
|
/**
|
|
422
|
-
*
|
|
423
|
-
*
|
|
367
|
+
* Route a destination's (track lane's) MIDI to the external output queue
|
|
368
|
+
* instead of the internal instrument rack, so the track plays an external
|
|
369
|
+
* device. Clearing it restores internal-synth playback.
|
|
424
370
|
*/
|
|
425
|
-
|
|
426
|
-
this.
|
|
371
|
+
setMidiDestinationExternal(destinationId, external) {
|
|
372
|
+
this.native.setMidiDestinationExternal(destinationId, external);
|
|
427
373
|
}
|
|
428
374
|
/**
|
|
429
|
-
*
|
|
430
|
-
*
|
|
375
|
+
* Enable/disable forwarding MIDI clock + transport (start/continue/stop) to
|
|
376
|
+
* the external output queue so external gear tracks the transport tempo.
|
|
431
377
|
*/
|
|
432
|
-
|
|
433
|
-
this.
|
|
434
|
-
}
|
|
435
|
-
/** Invert the polarity of the left and/or right channel of a strip. */
|
|
436
|
-
setPolarityInvert(stripIndex, invertLeft, invertRight) {
|
|
437
|
-
this.mixer.setPolarityInvert(stripIndex, invertLeft, invertRight);
|
|
378
|
+
setExternalMidiClockEnabled(enabled) {
|
|
379
|
+
this.native.setExternalMidiClockEnabled(enabled);
|
|
438
380
|
}
|
|
439
|
-
/**
|
|
440
|
-
|
|
441
|
-
this.
|
|
381
|
+
/** Count of external-MIDI events dropped because the output queue was full. */
|
|
382
|
+
externalMidiDroppedCount() {
|
|
383
|
+
return this.native.externalMidiDroppedCount();
|
|
442
384
|
}
|
|
443
385
|
/**
|
|
444
|
-
*
|
|
445
|
-
*
|
|
386
|
+
* Drain queued external-MIDI events, already lowered to MIDI 1.0 byte
|
|
387
|
+
* messages ready to write to a Web MIDI output port. Call once per audio
|
|
388
|
+
* block / animation frame. `maxRecords` caps the number of output events
|
|
389
|
+
* returned — the shared unit across every surface. Events past the cap stay
|
|
390
|
+
* queued for the next call (lossless); call again to drain the rest.
|
|
446
391
|
*/
|
|
447
|
-
|
|
448
|
-
this.
|
|
392
|
+
drainExternalMidi(maxRecords = 1024) {
|
|
393
|
+
return this.native.drainExternalMidi(maxRecords);
|
|
449
394
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
this.mixer.setVcaOffsetDb(stripIndex, offsetDb);
|
|
395
|
+
pushMidiInputNoteOn(group, channel, note, velocity, portTimeSamples = 0) {
|
|
396
|
+
this.native.pushMidiInputNoteOn(group, channel, note, velocity, portTimeSamples);
|
|
453
397
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
this.mixer.setDualPan(stripIndex, leftPan, rightPan);
|
|
398
|
+
pushMidiInputNoteOff(group, channel, note, velocity = 0, portTimeSamples = 0) {
|
|
399
|
+
this.native.pushMidiInputNoteOff(group, channel, note, velocity, portTimeSamples);
|
|
457
400
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
* Stored on the scene; inert until the surround DSP path applies it.
|
|
461
|
-
*/
|
|
462
|
-
setSurroundPan(stripIndex, pan) {
|
|
463
|
-
this.mixer.setSurroundPan(stripIndex, pan);
|
|
401
|
+
pushMidiInputCc(group, channel, controller, value, portTimeSamples = 0) {
|
|
402
|
+
this.native.pushMidiInputCc(group, channel, controller, value, portTimeSamples);
|
|
464
403
|
}
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
*
|
|
468
|
-
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
469
|
-
* @param id - Send id
|
|
470
|
-
* @param destinationBusId - Destination bus id
|
|
471
|
-
* @param sendDb - Initial send level in dB
|
|
472
|
-
* @param timing - `'preFader'` or `'postFader'` (default: `'postFader'`)
|
|
473
|
-
* @returns The new send's index
|
|
474
|
-
*/
|
|
475
|
-
addSend(stripIndex, id, destinationBusId, sendDb = 0, timing = "postFader") {
|
|
476
|
-
return this.mixer.addSend(stripIndex, id, destinationBusId, sendDb, sendTimingCode(timing));
|
|
404
|
+
pushMidiNoteOn(destinationId, group, channel, note, velocity, renderFrame = -1) {
|
|
405
|
+
this.native.pushMidiNoteOn(destinationId, group, channel, note, velocity, renderFrame);
|
|
477
406
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
this.mixer.setSendDb(stripIndex, sendIndex, sendDb);
|
|
407
|
+
pushMidiNoteOff(destinationId, group, channel, note, velocity = 0, renderFrame = -1) {
|
|
408
|
+
this.native.pushMidiNoteOff(destinationId, group, channel, note, velocity, renderFrame);
|
|
481
409
|
}
|
|
482
410
|
/**
|
|
483
|
-
*
|
|
484
|
-
*
|
|
485
|
-
*
|
|
486
|
-
*
|
|
487
|
-
* results so the routing graph rebuilds.
|
|
488
|
-
*
|
|
489
|
-
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
490
|
-
* @param sendIndex - Send index in add order
|
|
411
|
+
* Queue an immediate (live) MIDI control change to a MIDI destination
|
|
412
|
+
* (engine kMidiCcImmediate). `group`/`channel` are 0..15; `controller`/`value`
|
|
413
|
+
* are 7-bit (0..127). `renderFrame` is the frame to fire at, or -1 for
|
|
414
|
+
* immediate. Mirrors the Node/Python/C-ABI `pushMidiCc`.
|
|
491
415
|
*/
|
|
492
|
-
|
|
493
|
-
this.
|
|
416
|
+
pushMidiCc(destinationId, group, channel, controller, value, renderFrame = -1) {
|
|
417
|
+
this.native.pushMidiCc(destinationId, group, channel, controller, value, renderFrame);
|
|
494
418
|
}
|
|
495
419
|
/**
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
*
|
|
499
|
-
*
|
|
420
|
+
* Queue an immediate (live) MIDI SysEx frame to a MIDI destination. `data` is
|
|
421
|
+
* the full message including the leading 0xF0 and trailing 0xF7 (1..512
|
|
422
|
+
* bytes). `renderFrame` is the frame to fire at, or -1 for immediate. Mirrors
|
|
423
|
+
* the Node/Python/C-ABI `pushMidiSysex`.
|
|
500
424
|
*/
|
|
501
|
-
|
|
502
|
-
|
|
425
|
+
pushMidiSysex(destinationId, data, renderFrame = -1) {
|
|
426
|
+
this.native.pushMidiSysex(destinationId, data, renderFrame);
|
|
503
427
|
}
|
|
504
428
|
/**
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
* With no `tap` argument this reads the strip's own (post-fader) meter,
|
|
508
|
-
* matching the Node/Python tap-less `stripMeter` contract. Pass an optional
|
|
509
|
-
* `tap` (`'preFader'` / `'postFader'`) to read the tap-selectable snapshot
|
|
510
|
-
* instead — the same backing call as {@link meterTap}.
|
|
511
|
-
*
|
|
512
|
-
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
513
|
-
* @param tap - Optional tap point (`'preFader'` / `'postFader'`); when omitted
|
|
514
|
-
* the tap-less post-fader strip meter is read.
|
|
429
|
+
* Queue a MIDI panic (all-notes-off) releasing every sounding note at
|
|
430
|
+
* `renderFrame` (-1 = immediate). Mirrors the C-ABI `pushMidiPanic`.
|
|
515
431
|
*/
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
return this.mixer.stripMeter(stripIndex);
|
|
519
|
-
}
|
|
520
|
-
return this.mixer.meterTap(stripIndex, meterTapCode(tap));
|
|
432
|
+
pushMidiPanic(renderFrame = -1) {
|
|
433
|
+
this.native.pushMidiPanic(renderFrame);
|
|
521
434
|
}
|
|
522
435
|
/**
|
|
523
|
-
*
|
|
524
|
-
*
|
|
525
|
-
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
526
|
-
* @param samplePos - Absolute samples from the start of processing
|
|
527
|
-
* @param faderDb - Target fader level in dB
|
|
528
|
-
* @param curve - Interpolation curve (default: `'linear'`)
|
|
436
|
+
* Remove all registered parameters (and their automation lanes). Control-thread
|
|
437
|
+
* only; not realtime-safe. Mirrors the C-ABI `clearParameters`.
|
|
529
438
|
*/
|
|
530
|
-
|
|
531
|
-
this.
|
|
439
|
+
clearParameters() {
|
|
440
|
+
this.native.clearParameters();
|
|
532
441
|
}
|
|
533
|
-
/**
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
537
|
-
* @param samplePos - Absolute samples from the start of processing
|
|
538
|
-
* @param pan - Target pan position
|
|
539
|
-
* @param curve - Interpolation curve (default: `'linear'`)
|
|
540
|
-
*/
|
|
541
|
-
schedulePanAutomation(stripIndex, samplePos, pan, curve = "linear") {
|
|
542
|
-
this.mixer.schedulePanAutomation(stripIndex, samplePos, pan, automationCurveCode(curve));
|
|
442
|
+
/** Read back the current transport state snapshot. */
|
|
443
|
+
getTransportState() {
|
|
444
|
+
return this.native.getTransportState();
|
|
543
445
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
*
|
|
547
|
-
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
548
|
-
* @param samplePos - Absolute samples from the start of processing
|
|
549
|
-
* @param width - Target stereo width
|
|
550
|
-
* @param curve - Interpolation curve (default: `'linear'`)
|
|
551
|
-
*/
|
|
552
|
-
scheduleWidthAutomation(stripIndex, samplePos, width, curve = "linear") {
|
|
553
|
-
this.mixer.scheduleWidthAutomation(stripIndex, samplePos, width, automationCurveCode(curve));
|
|
446
|
+
play(renderFrame = -1) {
|
|
447
|
+
this.native.play(renderFrame);
|
|
554
448
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
* @param samplePos - Absolute samples from the start of processing
|
|
561
|
-
* @param db - Target send level in dB
|
|
562
|
-
* @param curve - Interpolation curve (default: `'linear'`)
|
|
563
|
-
*/
|
|
564
|
-
scheduleSendAutomation(stripIndex, sendIndex, samplePos, db, curve = "linear") {
|
|
565
|
-
this.mixer.scheduleSendAutomation(
|
|
566
|
-
stripIndex,
|
|
567
|
-
sendIndex,
|
|
568
|
-
samplePos,
|
|
569
|
-
db,
|
|
570
|
-
automationCurveCode(curve)
|
|
571
|
-
);
|
|
449
|
+
stop(renderFrame = -1) {
|
|
450
|
+
this.native.stop(renderFrame);
|
|
451
|
+
}
|
|
452
|
+
seekSample(timelineSample, renderFrame = -1) {
|
|
453
|
+
this.native.seekSample(timelineSample, renderFrame);
|
|
572
454
|
}
|
|
573
455
|
/**
|
|
574
|
-
*
|
|
575
|
-
*
|
|
456
|
+
* Snaps every in-flight parameter ramp (engine-level smoothed params, mixer
|
|
457
|
+
* lane fader/pan/gate, bus gains) to its target value. Offline renders call
|
|
458
|
+
* this after a priming process() block so the first audible block renders at
|
|
459
|
+
* settled values instead of ramping in from defaults.
|
|
576
460
|
*/
|
|
577
|
-
|
|
578
|
-
|
|
461
|
+
settleParameters() {
|
|
462
|
+
this.native.settleParameters();
|
|
579
463
|
}
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
return this.mixer.toSceneJson();
|
|
464
|
+
seekPpq(ppq, renderFrame = -1) {
|
|
465
|
+
this.native.seekPpq(ppq, renderFrame);
|
|
583
466
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
* compiles the routing graph if the topology is dirty.
|
|
587
|
-
*/
|
|
588
|
-
tailSamples() {
|
|
589
|
-
return this.mixer.tailSamples();
|
|
467
|
+
setTempo(bpm) {
|
|
468
|
+
this.native.setTempo(bpm);
|
|
590
469
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
* dry/wet material. Lazily compiles the routing graph if the topology is dirty.
|
|
594
|
-
*/
|
|
595
|
-
latencySamples() {
|
|
596
|
-
return this.mixer.latencySamples();
|
|
470
|
+
setTempoSegments(segments) {
|
|
471
|
+
this.native.setTempoSegments([...segments]);
|
|
597
472
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
* frames after the host stops feeding strip inputs. Returns the mixed stereo
|
|
601
|
-
* master (`left`, `right`, `sampleRate`).
|
|
602
|
-
*/
|
|
603
|
-
drainTailStereo(numSamples) {
|
|
604
|
-
return this.mixer.drainTailStereo(numSamples);
|
|
473
|
+
setTimeSignature(numerator, denominator) {
|
|
474
|
+
this.native.setTimeSignature(numerator, denominator);
|
|
605
475
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
this.mixer.delete();
|
|
476
|
+
setTimeSignatureSegments(segments) {
|
|
477
|
+
this.native.setTimeSignatureSegments([...segments]);
|
|
609
478
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
this.delete();
|
|
479
|
+
sampleAtPpq(ppq) {
|
|
480
|
+
return Number(this.native.sampleAtPpq(ppq));
|
|
613
481
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
// src/realtime_voice_changer.ts
|
|
617
|
-
var RealtimeVoiceChanger = class {
|
|
618
|
-
constructor(config = "neutral-monitor") {
|
|
619
|
-
const module2 = getSonareModule();
|
|
620
|
-
this.changer = module2.createRealtimeVoiceChanger(config);
|
|
482
|
+
setLoop(startPpq, endPpq, enabled = true) {
|
|
483
|
+
this.native.setLoop(startPpq, endPpq, enabled);
|
|
621
484
|
}
|
|
622
|
-
|
|
623
|
-
this.
|
|
485
|
+
addParameter(info) {
|
|
486
|
+
this.native.addParameter(info);
|
|
624
487
|
}
|
|
625
|
-
|
|
626
|
-
this.
|
|
488
|
+
parameterCount() {
|
|
489
|
+
return this.native.parameterCount();
|
|
627
490
|
}
|
|
628
|
-
|
|
629
|
-
this.
|
|
491
|
+
parameterInfoByIndex(index) {
|
|
492
|
+
return this.native.parameterInfoByIndex(index);
|
|
630
493
|
}
|
|
631
|
-
|
|
632
|
-
return this.
|
|
494
|
+
parameterInfo(id) {
|
|
495
|
+
return this.native.parameterInfo(id);
|
|
633
496
|
}
|
|
634
|
-
|
|
635
|
-
|
|
497
|
+
setAutomationLane(paramId, points) {
|
|
498
|
+
this.native.setAutomationLane(paramId, points);
|
|
636
499
|
}
|
|
637
|
-
|
|
638
|
-
return this.
|
|
500
|
+
automationLaneCount() {
|
|
501
|
+
return this.native.automationLaneCount();
|
|
639
502
|
}
|
|
640
|
-
|
|
641
|
-
this.
|
|
503
|
+
setMarkers(markers) {
|
|
504
|
+
this.native.setMarkers(markers);
|
|
642
505
|
}
|
|
643
|
-
|
|
644
|
-
return this.
|
|
506
|
+
markerCount() {
|
|
507
|
+
return this.native.markerCount();
|
|
645
508
|
}
|
|
646
|
-
|
|
647
|
-
this.
|
|
509
|
+
markerByIndex(index) {
|
|
510
|
+
return this.native.markerByIndex(index);
|
|
648
511
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
*
|
|
652
|
-
* Write your input samples into the returned `Float32Array` directly (e.g.
|
|
653
|
-
* via `input.set(source)`); no copy crosses the JS↔C++ bridge until
|
|
654
|
-
* {@link processPreparedMono} is called. The view is owned by this
|
|
655
|
-
* RealtimeVoiceChanger and becomes invalid after {@link delete}; it may
|
|
656
|
-
* also be invalidated if you later call this method with a larger
|
|
657
|
-
* `numSamples` value (the underlying buffer may be reallocated).
|
|
658
|
-
*/
|
|
659
|
-
getMonoInputBuffer(numSamples) {
|
|
660
|
-
return this.changer.getMonoInputBuffer(numSamples);
|
|
512
|
+
marker(id) {
|
|
513
|
+
return this.native.marker(id);
|
|
661
514
|
}
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
return this.changer.getMonoOutputBuffer(numSamples);
|
|
515
|
+
seekMarker(markerId, renderFrame = -1) {
|
|
516
|
+
this.native.seekMarker(markerId, renderFrame);
|
|
665
517
|
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
* appears in the buffer returned by {@link getMonoOutputBuffer}. No JS↔C++
|
|
669
|
-
* sample-level crossings happen on this call — it just hands control to
|
|
670
|
-
* the underlying DSP on already-on-heap data.
|
|
671
|
-
*/
|
|
672
|
-
processPreparedMono(numSamples) {
|
|
673
|
-
this.changer.processPreparedMono(numSamples);
|
|
518
|
+
setLoopFromMarkers(startMarkerId, endMarkerId) {
|
|
519
|
+
this.native.setLoopFromMarkers(startMarkerId, endMarkerId);
|
|
674
520
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
return this.changer.getInterleavedInputBuffer(numFrames, numChannels);
|
|
521
|
+
setMetronome(config) {
|
|
522
|
+
this.native.setMetronome(config);
|
|
678
523
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
return this.changer.getInterleavedOutputBuffer(numFrames, numChannels);
|
|
524
|
+
metronome() {
|
|
525
|
+
return this.native.metronome();
|
|
682
526
|
}
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
* appears in the buffer returned by {@link getInterleavedOutputBuffer}.
|
|
686
|
-
*/
|
|
687
|
-
processPreparedInterleaved(numFrames, numChannels) {
|
|
688
|
-
this.changer.processPreparedInterleaved(numFrames, numChannels);
|
|
527
|
+
countInEndSample(startSample, bars) {
|
|
528
|
+
return Number(this.native.countInEndSample(startSample, bars));
|
|
689
529
|
}
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
* AudioWorklet's native layout; processing happens in place.
|
|
693
|
-
*/
|
|
694
|
-
getPlanarChannelBuffer(channel, numFrames) {
|
|
695
|
-
return this.changer.getPlanarChannelBuffer(channel, numFrames);
|
|
530
|
+
setGraph(spec) {
|
|
531
|
+
this.native.setGraph(spec);
|
|
696
532
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
533
|
+
graphNodeCount() {
|
|
534
|
+
return this.native.graphNodeCount();
|
|
535
|
+
}
|
|
536
|
+
graphConnectionCount() {
|
|
537
|
+
return this.native.graphConnectionCount();
|
|
538
|
+
}
|
|
539
|
+
setClips(clips) {
|
|
540
|
+
this.native.setClips(
|
|
541
|
+
clips.map((clip) => ({
|
|
542
|
+
...clip,
|
|
543
|
+
pageProvider: typeof clip.pageProvider === "object" && clip.pageProvider !== null ? clip.pageProvider.id : clip.pageProvider
|
|
544
|
+
}))
|
|
545
|
+
);
|
|
704
546
|
}
|
|
705
547
|
/**
|
|
706
|
-
*
|
|
707
|
-
*
|
|
708
|
-
* views are reused across calls and become invalid after {@link delete}.
|
|
548
|
+
* Returns the PCM generated for a tempo-sync clip by the control-thread
|
|
549
|
+
* setter, or `null` when the clip did not require a tempo-sync bake.
|
|
709
550
|
*/
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
let output = this.getMonoOutputBuffer(numSamples);
|
|
713
|
-
const reacquireIfDetached = () => {
|
|
714
|
-
if (input.byteLength === 0 || output.byteLength === 0) {
|
|
715
|
-
input = this.getMonoInputBuffer(numSamples);
|
|
716
|
-
output = this.getMonoOutputBuffer(numSamples);
|
|
717
|
-
}
|
|
718
|
-
};
|
|
719
|
-
return {
|
|
720
|
-
get input() {
|
|
721
|
-
reacquireIfDetached();
|
|
722
|
-
return input;
|
|
723
|
-
},
|
|
724
|
-
get output() {
|
|
725
|
-
reacquireIfDetached();
|
|
726
|
-
return output;
|
|
727
|
-
},
|
|
728
|
-
process: () => {
|
|
729
|
-
reacquireIfDetached();
|
|
730
|
-
this.processPreparedMono(numSamples);
|
|
731
|
-
}
|
|
732
|
-
};
|
|
551
|
+
prebakedClipChannels(clipId) {
|
|
552
|
+
return this.native.prebakedClipChannels(clipId);
|
|
733
553
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
this.processPreparedInterleaved(numFrames, numChannels);
|
|
757
|
-
}
|
|
758
|
-
};
|
|
554
|
+
clipCount() {
|
|
555
|
+
return this.native.clipCount();
|
|
556
|
+
}
|
|
557
|
+
setTrackLanes(lanes) {
|
|
558
|
+
this.native.setTrackLanes(
|
|
559
|
+
lanes.map((lane) => {
|
|
560
|
+
if (typeof lane === "number") {
|
|
561
|
+
return { trackId: lane };
|
|
562
|
+
}
|
|
563
|
+
if (!lane.sends) {
|
|
564
|
+
return lane;
|
|
565
|
+
}
|
|
566
|
+
return {
|
|
567
|
+
...lane,
|
|
568
|
+
sends: lane.sends.map((send) => ({
|
|
569
|
+
...send,
|
|
570
|
+
// Post-fader (0) is the default for an omitted sendTiming.
|
|
571
|
+
sendTiming: send.sendTiming === void 0 ? 0 : sendTimingCode(send.sendTiming)
|
|
572
|
+
}))
|
|
573
|
+
};
|
|
574
|
+
})
|
|
575
|
+
);
|
|
759
576
|
}
|
|
760
577
|
/**
|
|
761
|
-
*
|
|
762
|
-
*
|
|
763
|
-
* wired to the same `numFrames`. Buffers are reused across calls and
|
|
764
|
-
* become invalid after {@link delete}.
|
|
578
|
+
* Keys one insert of a lane strip from another lane's post-strip audio
|
|
579
|
+
* (ducking/sidechainRouter inserts). sourceTrackId 0 removes the binding.
|
|
765
580
|
*/
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
const acquire = () => {
|
|
769
|
-
channels = [];
|
|
770
|
-
for (let ch = 0; ch < numChannels; ch++) {
|
|
771
|
-
channels.push(this.getPlanarChannelBuffer(ch, numFrames));
|
|
772
|
-
}
|
|
773
|
-
};
|
|
774
|
-
acquire();
|
|
775
|
-
const reacquireIfDetached = () => {
|
|
776
|
-
if ((channels[0]?.byteLength ?? 0) === 0) {
|
|
777
|
-
acquire();
|
|
778
|
-
}
|
|
779
|
-
};
|
|
780
|
-
return {
|
|
781
|
-
get channels() {
|
|
782
|
-
reacquireIfDetached();
|
|
783
|
-
return channels;
|
|
784
|
-
},
|
|
785
|
-
process: () => {
|
|
786
|
-
reacquireIfDetached();
|
|
787
|
-
this.processPreparedPlanar(numFrames);
|
|
788
|
-
}
|
|
789
|
-
};
|
|
581
|
+
setLaneSidechain(trackId, insertIndex, sourceTrackId) {
|
|
582
|
+
this.native.setLaneSidechain(trackId, insertIndex, sourceTrackId);
|
|
790
583
|
}
|
|
791
|
-
|
|
792
|
-
this.
|
|
584
|
+
setTrackBuses(buses) {
|
|
585
|
+
this.native.setTrackBuses(buses);
|
|
793
586
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
const sharedArrayBuffer = typeof globalThis.SharedArrayBuffer === "function";
|
|
801
|
-
const atomics = typeof globalThis.Atomics === "object";
|
|
802
|
-
const audioWorklet = typeof AudioWorkletNode !== "undefined" || typeof globalThis.AudioWorkletProcessor !== "undefined";
|
|
803
|
-
return {
|
|
804
|
-
engineAbiVersion: abiVersion,
|
|
805
|
-
expectedEngineAbiVersion: EXPECTED_ENGINE_ABI_VERSION,
|
|
806
|
-
abiCompatible: abiVersion === EXPECTED_ENGINE_ABI_VERSION,
|
|
807
|
-
sharedArrayBuffer,
|
|
808
|
-
atomics,
|
|
809
|
-
audioWorklet,
|
|
810
|
-
mode: sharedArrayBuffer && atomics ? "sab" : "postMessage"
|
|
811
|
-
};
|
|
812
|
-
}
|
|
813
|
-
var RealtimeEngine = class {
|
|
814
|
-
constructor(sampleRate = 48e3, maxBlockSize = 128, commandCapacity = 1024, telemetryCapacity = 1024) {
|
|
815
|
-
const module2 = getSonareModule();
|
|
816
|
-
const capabilities = engineCapabilities();
|
|
817
|
-
if (!capabilities.abiCompatible) {
|
|
818
|
-
throw new Error(
|
|
819
|
-
`Engine ABI mismatch: wasm=${capabilities.engineAbiVersion}, expected=${capabilities.expectedEngineAbiVersion}`
|
|
820
|
-
);
|
|
587
|
+
setBusStripJson(busId, sceneJson) {
|
|
588
|
+
try {
|
|
589
|
+
JSON.parse(sceneJson);
|
|
590
|
+
} catch (error) {
|
|
591
|
+
const message = error instanceof Error ? error.message : "invalid bus strip JSON";
|
|
592
|
+
throw new SonareError(2 /* InvalidFormat */, "InvalidFormat", message);
|
|
821
593
|
}
|
|
822
|
-
this.native
|
|
823
|
-
sampleRate,
|
|
824
|
-
maxBlockSize,
|
|
825
|
-
commandCapacity,
|
|
826
|
-
telemetryCapacity
|
|
827
|
-
);
|
|
594
|
+
this.native.setBusStripJson(busId, sceneJson);
|
|
828
595
|
}
|
|
829
|
-
|
|
830
|
-
|
|
596
|
+
setTrackStripJson(trackId, sceneJson) {
|
|
597
|
+
try {
|
|
598
|
+
JSON.parse(sceneJson);
|
|
599
|
+
} catch (error) {
|
|
600
|
+
const message = error instanceof Error ? error.message : "invalid track strip JSON";
|
|
601
|
+
throw new SonareError(2 /* InvalidFormat */, "InvalidFormat", message);
|
|
602
|
+
}
|
|
603
|
+
this.native.setTrackStripJson(trackId, sceneJson);
|
|
831
604
|
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
605
|
+
setTrackStripEqBand(trackId, bandIndex, band) {
|
|
606
|
+
this.native.setTrackStripEqBandJson(
|
|
607
|
+
trackId,
|
|
608
|
+
bandIndex,
|
|
609
|
+
typeof band === "string" ? band : JSON.stringify(band)
|
|
610
|
+
);
|
|
835
611
|
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
this.native.setParameterSmoothed(paramId, value, renderFrame);
|
|
612
|
+
setTrackStripEqBandJson(trackId, bandIndex, bandJson) {
|
|
613
|
+
this.native.setTrackStripEqBandJson(trackId, bandIndex, bandJson);
|
|
839
614
|
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
* fader/pan glides, insert-parameter automation, and MIDI-CC mappings. The
|
|
843
|
-
* default is 20 ms; pass `0` for instant (un-ramped) changes.
|
|
844
|
-
*/
|
|
845
|
-
setParamSmoothingMs(smoothingMs) {
|
|
846
|
-
this.native.setParamSmoothingMs(smoothingMs);
|
|
615
|
+
setTrackStripInsertBypassed(trackId, insertIndex, bypassed, resetOnBypass = false) {
|
|
616
|
+
this.native.setTrackStripInsertBypassed(trackId, insertIndex, bypassed, resetOnBypass);
|
|
847
617
|
}
|
|
848
|
-
|
|
849
|
-
|
|
618
|
+
setMasterStripJson(sceneJson) {
|
|
619
|
+
try {
|
|
620
|
+
JSON.parse(sceneJson);
|
|
621
|
+
} catch (error) {
|
|
622
|
+
const message = error instanceof Error ? error.message : "invalid master strip JSON";
|
|
623
|
+
throw new SonareError(2 /* InvalidFormat */, "InvalidFormat", message);
|
|
624
|
+
}
|
|
625
|
+
this.native.setMasterStripJson(sceneJson);
|
|
850
626
|
}
|
|
851
|
-
|
|
852
|
-
this.native.
|
|
627
|
+
setMasterStripEqBand(bandIndex, band) {
|
|
628
|
+
this.native.setMasterStripEqBandJson(
|
|
629
|
+
bandIndex,
|
|
630
|
+
typeof band === "string" ? band : JSON.stringify(band)
|
|
631
|
+
);
|
|
853
632
|
}
|
|
854
|
-
|
|
855
|
-
this.native.
|
|
633
|
+
setMasterStripEqBandJson(bandIndex, bandJson) {
|
|
634
|
+
this.native.setMasterStripEqBandJson(bandIndex, bandJson);
|
|
856
635
|
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
* is a {@link SynthPatch} or a preset-name string (`'saw-lead'` /
|
|
860
|
-
* `'va:saw-lead'`; see {@link synthPresetNames}), resolving exactly like
|
|
861
|
-
* {@link Project.bounceWithSynthInstrument}. Live note/CC commands and
|
|
862
|
-
* scheduled MIDI clips routed to that destination render through the synth.
|
|
863
|
-
* Unknown preset names throw. An object patch's `destinationId` is a JS
|
|
864
|
-
* binding convenience, not part of the NativeSynth patch itself.
|
|
865
|
-
*/
|
|
866
|
-
setSynthInstrument(patch = {}, destinationId = (typeof patch === "object" ? patch.destinationId : void 0) ?? 0) {
|
|
867
|
-
this.native.setSynthInstrument(destinationId, patch);
|
|
636
|
+
setMasterStripInsertBypassed(insertIndex, bypassed, resetOnBypass = false) {
|
|
637
|
+
this.native.setMasterStripInsertBypassed(insertIndex, bypassed, resetOnBypass);
|
|
868
638
|
}
|
|
869
639
|
/**
|
|
870
|
-
*
|
|
871
|
-
*
|
|
872
|
-
*
|
|
873
|
-
*
|
|
640
|
+
* Changes one track-strip insert parameter in realtime, addressed by the
|
|
641
|
+
* processor's JSON-key parameter name (see {@link masteringInsertParamInfo}).
|
|
642
|
+
* Applied at the next block head via the engine command queue; safe during
|
|
643
|
+
* playback. Throws if the track, insert, or name is unknown, the param is not
|
|
644
|
+
* realtime-safe, or the command queue is full.
|
|
874
645
|
*/
|
|
875
|
-
|
|
876
|
-
this.native.
|
|
646
|
+
setTrackStripInsertParamByName(trackId, insertIndex, paramName, value) {
|
|
647
|
+
this.native.setTrackStripInsertParamByName(trackId, insertIndex, paramName, value);
|
|
877
648
|
}
|
|
878
|
-
/**
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
* commands and scheduled MIDI clips routed to that destination render
|
|
882
|
-
* through the player (16 MIDI channels, channel 10 drums, GS NRPN part
|
|
883
|
-
* edits, GS/GM SysEx resets). Without a loaded SoundFont — or for programs
|
|
884
|
-
* the SoundFont does not cover — notes play through the built-in
|
|
885
|
-
* synthesizer GM fallback bank (the data-free floor).
|
|
886
|
-
*/
|
|
887
|
-
setSf2Instrument(config = {}, destinationId = config.destinationId ?? 0) {
|
|
888
|
-
this.native.setSf2Instrument(destinationId, config);
|
|
649
|
+
/** Master-strip counterpart of {@link setTrackStripInsertParamByName}. */
|
|
650
|
+
setMasterStripInsertParamByName(insertIndex, paramName, value) {
|
|
651
|
+
this.native.setMasterStripInsertParamByName(insertIndex, paramName, value);
|
|
889
652
|
}
|
|
890
|
-
|
|
891
|
-
|
|
653
|
+
/** Bus-strip counterpart of {@link setTrackStripInsertParamByName}. */
|
|
654
|
+
setBusStripInsertParamByName(busId, insertIndex, paramName, value) {
|
|
655
|
+
this.native.setBusStripInsertParamByName(busId, insertIndex, paramName, value);
|
|
892
656
|
}
|
|
893
|
-
|
|
894
|
-
|
|
657
|
+
/** Bus-strip counterpart of {@link setTrackStripInsertBypassed}. */
|
|
658
|
+
setBusStripInsertBypassed(busId, insertIndex, bypassed, resetOnBypass = false) {
|
|
659
|
+
this.native.setBusStripInsertBypassed(busId, insertIndex, bypassed, resetOnBypass);
|
|
895
660
|
}
|
|
896
661
|
/**
|
|
897
|
-
*
|
|
898
|
-
*
|
|
899
|
-
*
|
|
662
|
+
* Resolves a track-lane insert parameter (by its JSON-key name) to the
|
|
663
|
+
* reserved automation id usable with `setAutomationLane` / `setParameter`.
|
|
664
|
+
* Returns `-1` when the track, insert, or name is unknown. (The Python binding
|
|
665
|
+
* raises a `SonareError` for an unknown id where Node/WASM return the `-1`
|
|
666
|
+
* sentinel.)
|
|
900
667
|
*/
|
|
901
|
-
|
|
902
|
-
this.native.
|
|
903
|
-
channel,
|
|
904
|
-
controller,
|
|
905
|
-
paramId,
|
|
906
|
-
options.minValue ?? 0,
|
|
907
|
-
options.maxValue ?? 1
|
|
908
|
-
);
|
|
909
|
-
}
|
|
910
|
-
clearMidiCcBindings() {
|
|
911
|
-
this.native.clearMidiCcBindings();
|
|
668
|
+
resolveTrackInsertAutomationId(trackId, insertIndex, paramName) {
|
|
669
|
+
return this.native.resolveTrackInsertAutomationId(trackId, insertIndex, paramName);
|
|
912
670
|
}
|
|
913
|
-
|
|
914
|
-
return this.native.
|
|
671
|
+
resolveMasterInsertAutomationId(insertIndex, paramName) {
|
|
672
|
+
return this.native.resolveMasterInsertAutomationId(insertIndex, paramName);
|
|
915
673
|
}
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
this.native.setMidiFx(destinationId, configJson);
|
|
674
|
+
resolveBusInsertAutomationId(busId, insertIndex, paramName) {
|
|
675
|
+
return this.native.resolveBusInsertAutomationId(busId, insertIndex, paramName);
|
|
919
676
|
}
|
|
920
|
-
|
|
921
|
-
|
|
677
|
+
/** Sets a track lane strip's pan position in realtime (glitch-free). */
|
|
678
|
+
setTrackStripPan(trackId, pan) {
|
|
679
|
+
this.native.setTrackStripPan(trackId, pan);
|
|
922
680
|
}
|
|
923
|
-
/**
|
|
924
|
-
|
|
925
|
-
this.native.
|
|
681
|
+
/** Sets a track lane strip's pan law in realtime. */
|
|
682
|
+
setTrackStripPanLaw(trackId, panLaw) {
|
|
683
|
+
this.native.setTrackStripPanLaw(trackId, panLawCode(panLaw));
|
|
926
684
|
}
|
|
927
|
-
|
|
928
|
-
|
|
685
|
+
/** Sets a track lane strip's pan mode in realtime. */
|
|
686
|
+
setTrackStripPanMode(trackId, panMode) {
|
|
687
|
+
this.native.setTrackStripPanMode(trackId, panModeCode(panMode));
|
|
929
688
|
}
|
|
930
|
-
|
|
931
|
-
|
|
689
|
+
/** Sets a track lane strip's dual-pan left/right positions in realtime. */
|
|
690
|
+
setTrackStripDualPan(trackId, leftPan, rightPan) {
|
|
691
|
+
this.native.setTrackStripDualPan(trackId, leftPan, rightPan);
|
|
932
692
|
}
|
|
933
693
|
/**
|
|
934
|
-
*
|
|
935
|
-
*
|
|
936
|
-
* device. Clearing it restores internal-synth playback.
|
|
694
|
+
* Sets a track lane strip's inter-channel alignment delay (whole samples).
|
|
695
|
+
* Adjusts strip latency, so PDC and reported graph latency are refreshed.
|
|
937
696
|
*/
|
|
938
|
-
|
|
939
|
-
this.native.
|
|
697
|
+
setTrackStripChannelDelaySamples(trackId, delaySamples) {
|
|
698
|
+
this.native.setTrackStripChannelDelaySamples(trackId, delaySamples);
|
|
940
699
|
}
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
*/
|
|
945
|
-
setExternalMidiClockEnabled(enabled) {
|
|
946
|
-
this.native.setExternalMidiClockEnabled(enabled);
|
|
700
|
+
createClipPageProvider(numChannels, numSamples, pageFrames) {
|
|
701
|
+
const id = this.native.createClipPageProvider(numChannels, numSamples, pageFrames);
|
|
702
|
+
return new ClipPageProvider(this, id);
|
|
947
703
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
return this.native.externalMidiDroppedCount();
|
|
704
|
+
supplyClipPage(providerId, pageIndex, channels) {
|
|
705
|
+
this.native.supplyClipPage(providerId, pageIndex, channels);
|
|
951
706
|
}
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
* messages ready to write to a Web MIDI output port. Call once per audio
|
|
955
|
-
* block / animation frame. `maxRecords` caps the number of output events
|
|
956
|
-
* returned — the shared unit across every surface. Events past the cap stay
|
|
957
|
-
* queued for the next call (lossless); call again to drain the rest.
|
|
958
|
-
*/
|
|
959
|
-
drainExternalMidi(maxRecords = 1024) {
|
|
960
|
-
return this.native.drainExternalMidi(maxRecords);
|
|
707
|
+
clearClipPage(providerId, pageIndex) {
|
|
708
|
+
this.native.clearClipPage(providerId, pageIndex);
|
|
961
709
|
}
|
|
962
|
-
|
|
963
|
-
this.native.
|
|
710
|
+
destroyClipPageProvider(providerId) {
|
|
711
|
+
this.native.destroyClipPageProvider(providerId);
|
|
964
712
|
}
|
|
965
|
-
|
|
966
|
-
this.native.
|
|
713
|
+
popClipPageRequest() {
|
|
714
|
+
return this.native.popClipPageRequest();
|
|
967
715
|
}
|
|
968
|
-
|
|
969
|
-
this.native.
|
|
716
|
+
setCaptureBuffer(numChannels, capacityFrames) {
|
|
717
|
+
this.native.setCaptureBuffer(numChannels, capacityFrames);
|
|
970
718
|
}
|
|
971
|
-
|
|
972
|
-
this.native.
|
|
719
|
+
armCapture(armed = true) {
|
|
720
|
+
this.native.armCapture(armed);
|
|
973
721
|
}
|
|
974
|
-
|
|
975
|
-
this.native.
|
|
722
|
+
setCapturePunch(startSample, endSample, enabled = true) {
|
|
723
|
+
this.native.setCapturePunch(startSample, endSample, enabled);
|
|
976
724
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
* (engine kMidiCcImmediate). `group`/`channel` are 0..15; `controller`/`value`
|
|
980
|
-
* are 7-bit (0..127). `renderFrame` is the frame to fire at, or -1 for
|
|
981
|
-
* immediate. Mirrors the Node/Python/C-ABI `pushMidiCc`.
|
|
982
|
-
*/
|
|
983
|
-
pushMidiCc(destinationId, group, channel, controller, value, renderFrame = -1) {
|
|
984
|
-
this.native.pushMidiCc(destinationId, group, channel, controller, value, renderFrame);
|
|
725
|
+
setCaptureSource(source) {
|
|
726
|
+
this.native.setCaptureSource(source);
|
|
985
727
|
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
this.native.
|
|
728
|
+
setRecordOffsetSamples(offsetSamples) {
|
|
729
|
+
this.native.setRecordOffsetSamples(offsetSamples);
|
|
730
|
+
}
|
|
731
|
+
setInputMonitor(enabled, gain = 1) {
|
|
732
|
+
this.native.setInputMonitor(enabled, gain);
|
|
733
|
+
}
|
|
734
|
+
resetCapture() {
|
|
735
|
+
this.native.resetCapture();
|
|
736
|
+
}
|
|
737
|
+
captureStatus() {
|
|
738
|
+
return this.native.captureStatus();
|
|
739
|
+
}
|
|
740
|
+
capturedAudio() {
|
|
741
|
+
return this.native.capturedAudio();
|
|
742
|
+
}
|
|
743
|
+
process(channels) {
|
|
744
|
+
return this.native.process(channels);
|
|
994
745
|
}
|
|
995
746
|
/**
|
|
996
|
-
*
|
|
997
|
-
* `
|
|
747
|
+
* Allocates persistent per-channel WASM-heap scratch for the zero-copy
|
|
748
|
+
* `getChannelBuffer` / `processPrepared` realtime path. Call once (off the
|
|
749
|
+
* audio thread) before driving `processPrepared` from an AudioWorklet so the
|
|
750
|
+
* render callback never allocates on the C++/JS heap.
|
|
998
751
|
*/
|
|
999
|
-
|
|
1000
|
-
this.native.
|
|
752
|
+
prepareChannels(numChannels, maxFrames) {
|
|
753
|
+
this.native.prepareChannels(numChannels, maxFrames);
|
|
1001
754
|
}
|
|
1002
755
|
/**
|
|
1003
|
-
*
|
|
1004
|
-
*
|
|
756
|
+
* Returns a Float32Array view onto the persistent WASM-heap scratch for one
|
|
757
|
+
* channel (valid for up to `numFrames`). Fill it, call `processPrepared`, then
|
|
758
|
+
* read the same view back. Re-acquire after WASM memory growth.
|
|
1005
759
|
*/
|
|
1006
|
-
|
|
1007
|
-
this.native.
|
|
760
|
+
getChannelBuffer(channel, numFrames) {
|
|
761
|
+
return this.native.getChannelBuffer(channel, numFrames);
|
|
1008
762
|
}
|
|
1009
|
-
/**
|
|
1010
|
-
|
|
1011
|
-
|
|
763
|
+
/**
|
|
764
|
+
* Runs the engine in place over the prepared per-channel scratch buffers.
|
|
765
|
+
* Allocation-free: safe to call on the AudioWorklet render thread after
|
|
766
|
+
* `prepareChannels`.
|
|
767
|
+
*/
|
|
768
|
+
processPrepared(numFrames) {
|
|
769
|
+
this.native.processPrepared(numFrames);
|
|
1012
770
|
}
|
|
1013
|
-
|
|
1014
|
-
this.native.
|
|
771
|
+
processWithMonitor(channels) {
|
|
772
|
+
return this.native.processWithMonitor(channels);
|
|
1015
773
|
}
|
|
1016
|
-
|
|
1017
|
-
this.native.
|
|
774
|
+
renderOffline(channels, blockSize = 128) {
|
|
775
|
+
return this.native.renderOffline(channels, blockSize);
|
|
1018
776
|
}
|
|
1019
|
-
|
|
1020
|
-
this.native.
|
|
777
|
+
bounceOffline(options) {
|
|
778
|
+
return this.native.bounceOffline(options);
|
|
1021
779
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
* lane fader/pan/gate, bus gains) to its target value. Offline renders call
|
|
1025
|
-
* this after a priming process() block so the first audible block renders at
|
|
1026
|
-
* settled values instead of ramping in from defaults.
|
|
1027
|
-
*/
|
|
1028
|
-
settleParameters() {
|
|
1029
|
-
this.native.settleParameters();
|
|
780
|
+
freezeOffline(options) {
|
|
781
|
+
return this.native.freezeOffline(options);
|
|
1030
782
|
}
|
|
1031
|
-
|
|
1032
|
-
this.native.
|
|
783
|
+
drainTelemetry(maxRecords = 1024) {
|
|
784
|
+
return this.native.drainTelemetry(maxRecords);
|
|
1033
785
|
}
|
|
1034
|
-
|
|
1035
|
-
this.native.
|
|
786
|
+
drainMeterTelemetry(maxRecords = 1024) {
|
|
787
|
+
return this.native.drainMeterTelemetry(maxRecords);
|
|
1036
788
|
}
|
|
1037
|
-
|
|
1038
|
-
|
|
789
|
+
/**
|
|
790
|
+
* Drains pending meter telemetry as per-plane (wide) records for a surround
|
|
791
|
+
* target. Use this for a surround mix target; {@link drainMeterTelemetry}
|
|
792
|
+
* stays the stereo fast path. The two share one queue — call only one per
|
|
793
|
+
* target. The live AudioWorklet path owns the queue via the stereo drain, so
|
|
794
|
+
* this wide drain is for an offline (non-worklet) engine instance; per-plane
|
|
795
|
+
* surround meters are not delivered over the live worklet meter ring.
|
|
796
|
+
*/
|
|
797
|
+
drainMeterTelemetryWide(maxRecords = 1024) {
|
|
798
|
+
return this.native.drainMeterTelemetryWide(maxRecords);
|
|
1039
799
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
800
|
+
/**
|
|
801
|
+
* Enables per-target spectrum + vectorscope capture. @param intervalFrames is
|
|
802
|
+
* the minimum render-frame gap between snapshots (0 disables). @param bandCount
|
|
803
|
+
* is the FFT band resolution (1..64); changing it re-prepares the tap. Returns
|
|
804
|
+
* the band count actually applied.
|
|
805
|
+
*/
|
|
806
|
+
configureScopeTelemetry(intervalFrames, bandCount) {
|
|
807
|
+
return this.native.configureScopeTelemetry(intervalFrames, bandCount);
|
|
1042
808
|
}
|
|
1043
|
-
|
|
1044
|
-
|
|
809
|
+
/** Drains pending spectrum + vectorscope snapshots (per mix target). */
|
|
810
|
+
drainScopeTelemetry(maxRecords = 1024) {
|
|
811
|
+
return this.native.drainScopeTelemetry(maxRecords);
|
|
1045
812
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
813
|
+
destroy() {
|
|
814
|
+
this.native.delete();
|
|
1048
815
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
816
|
+
};
|
|
817
|
+
var ClipPageProvider = class {
|
|
818
|
+
constructor(engine, id) {
|
|
819
|
+
this.engine = engine;
|
|
820
|
+
this.id = id;
|
|
821
|
+
this.disposed = false;
|
|
1051
822
|
}
|
|
1052
|
-
|
|
1053
|
-
this.
|
|
823
|
+
supply(pageIndex, channels) {
|
|
824
|
+
if (this.disposed) {
|
|
825
|
+
throw new Error("ClipPageProvider is destroyed");
|
|
826
|
+
}
|
|
827
|
+
this.engine.supplyClipPage(this.id, pageIndex, channels);
|
|
1054
828
|
}
|
|
1055
|
-
|
|
1056
|
-
|
|
829
|
+
clear(pageIndex) {
|
|
830
|
+
if (this.disposed) {
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
this.engine.clearClipPage(this.id, pageIndex);
|
|
1057
834
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
835
|
+
destroy() {
|
|
836
|
+
if (this.disposed) {
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
this.disposed = true;
|
|
840
|
+
this.engine.destroyClipPageProvider(this.id);
|
|
1060
841
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
// src/mixer.ts
|
|
845
|
+
var Mixer = class _Mixer {
|
|
846
|
+
constructor(mixer, blockSize) {
|
|
847
|
+
this.mixer = mixer;
|
|
848
|
+
this.blockSize = blockSize;
|
|
1063
849
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
850
|
+
/**
|
|
851
|
+
* Build a mixer from a scene JSON string.
|
|
852
|
+
*
|
|
853
|
+
* @param json - Scene JSON (strips, buses, sends, connections, inserts)
|
|
854
|
+
* @param sampleRate - Sample rate in Hz (default: 48000)
|
|
855
|
+
* @param blockSize - Maximum block size per {@link processStereo} call (default: 512)
|
|
856
|
+
*/
|
|
857
|
+
static fromSceneJson(json, sampleRate = 48e3, blockSize = 512) {
|
|
858
|
+
const module2 = getSonareModule();
|
|
859
|
+
return new _Mixer(module2.createMixerFromSceneJson(json, sampleRate, blockSize), blockSize);
|
|
1066
860
|
}
|
|
1067
|
-
|
|
1068
|
-
|
|
861
|
+
/** Rebuild and compile the routing graph from the current scene topology. */
|
|
862
|
+
compile() {
|
|
863
|
+
this.mixer.compile();
|
|
1069
864
|
}
|
|
1070
|
-
|
|
1071
|
-
|
|
865
|
+
/**
|
|
866
|
+
* Non-fatal warnings captured when this mixer was built from scene JSON: one
|
|
867
|
+
* entry per channel-strip insert that was handed param keys it does not read
|
|
868
|
+
* (a likely typo, or a key meant for a different processor). The scene still
|
|
869
|
+
* loaded; these keys simply took no effect. Empty when every key was consumed.
|
|
870
|
+
* Use {@link masteringInsertParamNames} to discover the keys an insert accepts.
|
|
871
|
+
*/
|
|
872
|
+
sceneWarnings() {
|
|
873
|
+
return this.mixer.sceneWarnings();
|
|
1072
874
|
}
|
|
1073
|
-
|
|
1074
|
-
|
|
875
|
+
/**
|
|
876
|
+
* Mix one block of per-strip stereo audio into the stereo master.
|
|
877
|
+
*
|
|
878
|
+
* @param leftChannels - `leftChannels[i]` is the left channel of strip `i`
|
|
879
|
+
* @param rightChannels - `rightChannels[i]` is the right channel of strip `i`
|
|
880
|
+
* @returns Mixed stereo master (`left`, `right`, `sampleRate`)
|
|
881
|
+
*/
|
|
882
|
+
processStereo(leftChannels, rightChannels) {
|
|
883
|
+
if (leftChannels.length !== rightChannels.length) {
|
|
884
|
+
throw new Error("leftChannels and rightChannels must have the same length.");
|
|
885
|
+
}
|
|
886
|
+
return this.mixer.processStereo(leftChannels, rightChannels);
|
|
1075
887
|
}
|
|
1076
|
-
|
|
1077
|
-
|
|
888
|
+
/**
|
|
889
|
+
* Mix one block into caller-owned output arrays.
|
|
890
|
+
*
|
|
891
|
+
* This avoids allocating the result object and result `Float32Array`s. It is
|
|
892
|
+
* intended for realtime bridges such as AudioWorklet; the input channel count
|
|
893
|
+
* must match the scene strip count and all arrays must have the same length.
|
|
894
|
+
*/
|
|
895
|
+
processStereoInto(leftChannels, rightChannels, outLeft, outRight) {
|
|
896
|
+
if (leftChannels.length !== rightChannels.length) {
|
|
897
|
+
throw new Error("leftChannels and rightChannels must have the same length.");
|
|
898
|
+
}
|
|
899
|
+
if (outLeft.length !== outRight.length) {
|
|
900
|
+
throw new Error("outLeft and outRight must have the same length.");
|
|
901
|
+
}
|
|
902
|
+
this.mixer.processStereoInto(leftChannels, rightChannels, outLeft, outRight);
|
|
1078
903
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
904
|
+
/**
|
|
905
|
+
* Create reusable WASM-heap input/output views for realtime-style processing.
|
|
906
|
+
*
|
|
907
|
+
* Fill `leftInputs[i]` / `rightInputs[i]`, call `process()`, then read
|
|
908
|
+
* `outLeft` / `outRight`. The views are owned by this mixer and become invalid
|
|
909
|
+
* after {@link delete}.
|
|
910
|
+
*/
|
|
911
|
+
createRealtimeBuffer() {
|
|
912
|
+
const stripCount = this.stripCount();
|
|
913
|
+
let leftInputs = [];
|
|
914
|
+
let rightInputs = [];
|
|
915
|
+
let outLeft = this.mixer.outputLeftView();
|
|
916
|
+
let outRight = this.mixer.outputRightView();
|
|
917
|
+
const acquire = () => {
|
|
918
|
+
leftInputs = [];
|
|
919
|
+
rightInputs = [];
|
|
920
|
+
for (let index = 0; index < stripCount; index++) {
|
|
921
|
+
leftInputs.push(this.mixer.inputLeftView(index));
|
|
922
|
+
rightInputs.push(this.mixer.inputRightView(index));
|
|
923
|
+
}
|
|
924
|
+
outLeft = this.mixer.outputLeftView();
|
|
925
|
+
outRight = this.mixer.outputRightView();
|
|
926
|
+
};
|
|
927
|
+
acquire();
|
|
928
|
+
const reacquireIfDetached = () => {
|
|
929
|
+
if (outLeft.byteLength === 0 || (leftInputs[0]?.byteLength ?? 1) === 0) {
|
|
930
|
+
acquire();
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
return {
|
|
934
|
+
get leftInputs() {
|
|
935
|
+
reacquireIfDetached();
|
|
936
|
+
return leftInputs;
|
|
937
|
+
},
|
|
938
|
+
get rightInputs() {
|
|
939
|
+
reacquireIfDetached();
|
|
940
|
+
return rightInputs;
|
|
941
|
+
},
|
|
942
|
+
get outLeft() {
|
|
943
|
+
reacquireIfDetached();
|
|
944
|
+
return outLeft;
|
|
945
|
+
},
|
|
946
|
+
get outRight() {
|
|
947
|
+
reacquireIfDetached();
|
|
948
|
+
return outRight;
|
|
949
|
+
},
|
|
950
|
+
process: (numSamples = outLeft.length) => {
|
|
951
|
+
reacquireIfDetached();
|
|
952
|
+
this.mixer.processPreparedStereo(numSamples);
|
|
953
|
+
}
|
|
954
|
+
};
|
|
1081
955
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
956
|
+
/** Number of strips in the mixer (e.g. strips loaded from the scene). */
|
|
957
|
+
stripCount() {
|
|
958
|
+
return this.mixer.stripCount();
|
|
1084
959
|
}
|
|
1085
|
-
|
|
1086
|
-
|
|
960
|
+
/**
|
|
961
|
+
* Schedule sample-accurate insert-parameter automation on a strip's insert.
|
|
962
|
+
*
|
|
963
|
+
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
964
|
+
* @param insertIndex - Index into the strip's combined insert sequence
|
|
965
|
+
* (`[pre-inserts... post-inserts...]`)
|
|
966
|
+
* @param paramId - Processor-specific parameter id
|
|
967
|
+
* @param samplePos - Absolute samples from the start of processing (the mixer
|
|
968
|
+
* advances an internal position from 0 on the first {@link processStereo}
|
|
969
|
+
* call; recompiling resets it to 0)
|
|
970
|
+
* @param value - Target parameter value
|
|
971
|
+
* @param curve - Interpolation curve (default: `'linear'`)
|
|
972
|
+
* @throws If the strip index is out of range or the schedule call fails
|
|
973
|
+
* (unknown curve, out-of-range insert index, or full event lane)
|
|
974
|
+
*/
|
|
975
|
+
scheduleInsertAutomation(stripIndex, insertIndex, paramId, samplePos, value, curve = "linear") {
|
|
976
|
+
this.mixer.scheduleInsertAutomation(
|
|
977
|
+
stripIndex,
|
|
978
|
+
insertIndex,
|
|
979
|
+
paramId,
|
|
980
|
+
samplePos,
|
|
981
|
+
value,
|
|
982
|
+
automationCurveCode(curve)
|
|
983
|
+
);
|
|
1087
984
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
985
|
+
/**
|
|
986
|
+
* Resolve a strip's index in `[0, stripCount())` from its scene id, or `null`
|
|
987
|
+
* when no strip with that id exists (matches the Node binding's `number | null`).
|
|
988
|
+
*/
|
|
989
|
+
stripById(id) {
|
|
990
|
+
const index = this.mixer.stripById(id);
|
|
991
|
+
return index < 0 ? null : index;
|
|
1090
992
|
}
|
|
1091
|
-
|
|
1092
|
-
|
|
993
|
+
/**
|
|
994
|
+
* Add a bus to the mixer topology. `role` is one of `'master'`, `'aux'`, or
|
|
995
|
+
* `'submix'` (defaults to `'aux'`). Marks the routing graph dirty; call
|
|
996
|
+
* {@link compile} (or {@link processStereo}) to rebuild.
|
|
997
|
+
*/
|
|
998
|
+
addBus(id, role = "aux") {
|
|
999
|
+
this.mixer.addBus(id, role);
|
|
1093
1000
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1001
|
+
/** Remove a bus by id. Marks the routing graph dirty. */
|
|
1002
|
+
removeBus(id) {
|
|
1003
|
+
this.mixer.removeBus(id);
|
|
1096
1004
|
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1005
|
+
/** Number of buses in the mixer topology. */
|
|
1006
|
+
busCount() {
|
|
1007
|
+
return this.mixer.busCount();
|
|
1099
1008
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1009
|
+
/**
|
|
1010
|
+
* Add a VCA group with the given gain offset (dB). `members` is a list of
|
|
1011
|
+
* strip ids governed by the group (may be empty).
|
|
1012
|
+
*/
|
|
1013
|
+
addVcaGroup(id, gainDb = 0, members = []) {
|
|
1014
|
+
this.mixer.addVcaGroup(id, gainDb, members);
|
|
1102
1015
|
}
|
|
1103
|
-
|
|
1104
|
-
|
|
1016
|
+
/** Set an existing VCA group's gain in dB. */
|
|
1017
|
+
setVcaGroupGainDb(id, gainDb) {
|
|
1018
|
+
this.mixer.setVcaGroupGainDb(id, gainDb);
|
|
1105
1019
|
}
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
...clip,
|
|
1110
|
-
pageProvider: typeof clip.pageProvider === "object" && clip.pageProvider !== null ? clip.pageProvider.id : clip.pageProvider
|
|
1111
|
-
}))
|
|
1112
|
-
);
|
|
1020
|
+
/** Remove a VCA group by id. */
|
|
1021
|
+
removeVcaGroup(id) {
|
|
1022
|
+
this.mixer.removeVcaGroup(id);
|
|
1113
1023
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1024
|
+
/** Number of VCA groups in the mixer topology. */
|
|
1025
|
+
vcaGroupCount() {
|
|
1026
|
+
return this.mixer.vcaGroupCount();
|
|
1116
1027
|
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
return lane;
|
|
1125
|
-
}
|
|
1126
|
-
return {
|
|
1127
|
-
...lane,
|
|
1128
|
-
sends: lane.sends.map((send) => ({
|
|
1129
|
-
...send,
|
|
1130
|
-
// Post-fader (0) is the default for an omitted sendTiming.
|
|
1131
|
-
sendTiming: send.sendTiming === void 0 ? 0 : sendTimingCode(send.sendTiming)
|
|
1132
|
-
}))
|
|
1133
|
-
};
|
|
1134
|
-
})
|
|
1135
|
-
);
|
|
1028
|
+
/** Set the strip's input trim in dB. */
|
|
1029
|
+
setInputTrimDb(stripIndex, db) {
|
|
1030
|
+
this.mixer.setInputTrimDb(stripIndex, db);
|
|
1031
|
+
}
|
|
1032
|
+
/** Set the strip's fader level in dB. */
|
|
1033
|
+
setFaderDb(stripIndex, db) {
|
|
1034
|
+
this.mixer.setFaderDb(stripIndex, db);
|
|
1136
1035
|
}
|
|
1137
1036
|
/**
|
|
1138
|
-
*
|
|
1139
|
-
*
|
|
1037
|
+
* Set the strip's pan position.
|
|
1038
|
+
*
|
|
1039
|
+
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
1040
|
+
* @param pan - Pan position in `[-1, 1]`
|
|
1041
|
+
* @param panMode - Optional pan mode. When omitted the strip's current pan
|
|
1042
|
+
* mode is kept (passes `SONARE_PAN_MODE_KEEP`), so a plain pan nudge does
|
|
1043
|
+
* not reset a scene-defined `'stereoPan'` / `'dualPan'` mode back to
|
|
1044
|
+
* balance. Pass `'balance'` (or `0`) explicitly to force balance mode.
|
|
1140
1045
|
*/
|
|
1141
|
-
|
|
1142
|
-
|
|
1046
|
+
setPan(stripIndex, pan, panMode) {
|
|
1047
|
+
const mode = panMode === void 0 ? -1 : panModeCode(panMode);
|
|
1048
|
+
this.mixer.setPan(stripIndex, pan, mode);
|
|
1143
1049
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1050
|
+
/** Set the strip's stereo width. */
|
|
1051
|
+
setWidth(stripIndex, width) {
|
|
1052
|
+
this.mixer.setWidth(stripIndex, width);
|
|
1146
1053
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
} catch (error) {
|
|
1151
|
-
const message = error instanceof Error ? error.message : "invalid bus strip JSON";
|
|
1152
|
-
throw new SonareError(2 /* InvalidFormat */, "InvalidFormat", message);
|
|
1153
|
-
}
|
|
1154
|
-
this.native.setBusStripJson(busId, sceneJson);
|
|
1054
|
+
/** Set the strip's mute state. */
|
|
1055
|
+
setMuted(stripIndex, muted) {
|
|
1056
|
+
this.mixer.setMuted(stripIndex, muted);
|
|
1155
1057
|
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
}
|
|
1163
|
-
this.native.setTrackStripJson(trackId, sceneJson);
|
|
1058
|
+
/**
|
|
1059
|
+
* Set a strip's solo state. Takes effect on the next process without a
|
|
1060
|
+
* graph recompile.
|
|
1061
|
+
*/
|
|
1062
|
+
setSoloed(stripIndex, soloed) {
|
|
1063
|
+
this.mixer.setSoloed(stripIndex, soloed);
|
|
1164
1064
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
);
|
|
1065
|
+
/**
|
|
1066
|
+
* Mark a strip solo-safe so it is never implied-muted by another strip's
|
|
1067
|
+
* solo. Takes effect on the next process without a graph recompile.
|
|
1068
|
+
*/
|
|
1069
|
+
setSoloSafe(stripIndex, soloSafe) {
|
|
1070
|
+
this.mixer.setSoloSafe(stripIndex, soloSafe);
|
|
1171
1071
|
}
|
|
1172
|
-
|
|
1173
|
-
|
|
1072
|
+
/** Invert the polarity of the left and/or right channel of a strip. */
|
|
1073
|
+
setPolarityInvert(stripIndex, invertLeft, invertRight) {
|
|
1074
|
+
this.mixer.setPolarityInvert(stripIndex, invertLeft, invertRight);
|
|
1174
1075
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1076
|
+
/** Set the strip's pan law. */
|
|
1077
|
+
setPanLaw(stripIndex, panLaw) {
|
|
1078
|
+
this.mixer.setPanLaw(stripIndex, panLawCode(panLaw));
|
|
1177
1079
|
}
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
}
|
|
1185
|
-
this.native.setMasterStripJson(sceneJson);
|
|
1080
|
+
/**
|
|
1081
|
+
* Set a per-strip channel delay in samples. This changes the strip's reported
|
|
1082
|
+
* latency; recompile to re-run latency compensation.
|
|
1083
|
+
*/
|
|
1084
|
+
setChannelDelaySamples(stripIndex, delaySamples) {
|
|
1085
|
+
this.mixer.setChannelDelaySamples(stripIndex, delaySamples);
|
|
1186
1086
|
}
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
typeof band === "string" ? band : JSON.stringify(band)
|
|
1191
|
-
);
|
|
1087
|
+
/** Set the strip's live VCA gain offset in dB (not persisted to the scene). */
|
|
1088
|
+
setVcaOffsetDb(stripIndex, offsetDb) {
|
|
1089
|
+
this.mixer.setVcaOffsetDb(stripIndex, offsetDb);
|
|
1192
1090
|
}
|
|
1193
|
-
|
|
1194
|
-
|
|
1091
|
+
/** Set independent left/right pan positions (dual-pan mode). */
|
|
1092
|
+
setDualPan(stripIndex, leftPan, rightPan) {
|
|
1093
|
+
this.mixer.setDualPan(stripIndex, leftPan, rightPan);
|
|
1195
1094
|
}
|
|
1196
|
-
|
|
1197
|
-
|
|
1095
|
+
/**
|
|
1096
|
+
* Set the strip's surround pan position, used when it feeds a >2-channel bus.
|
|
1097
|
+
* Stored on the scene; inert until the surround DSP path applies it.
|
|
1098
|
+
*/
|
|
1099
|
+
setSurroundPan(stripIndex, pan) {
|
|
1100
|
+
this.mixer.setSurroundPan(stripIndex, pan);
|
|
1198
1101
|
}
|
|
1199
1102
|
/**
|
|
1200
|
-
*
|
|
1201
|
-
*
|
|
1202
|
-
*
|
|
1203
|
-
*
|
|
1204
|
-
*
|
|
1103
|
+
* Add a send to a strip after construction.
|
|
1104
|
+
*
|
|
1105
|
+
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
1106
|
+
* @param id - Send id
|
|
1107
|
+
* @param destinationBusId - Destination bus id
|
|
1108
|
+
* @param sendDb - Initial send level in dB
|
|
1109
|
+
* @param timing - `'preFader'` or `'postFader'` (default: `'postFader'`)
|
|
1110
|
+
* @returns The new send's index
|
|
1205
1111
|
*/
|
|
1206
|
-
|
|
1207
|
-
this.
|
|
1112
|
+
addSend(stripIndex, id, destinationBusId, sendDb = 0, timing = "postFader") {
|
|
1113
|
+
return this.mixer.addSend(stripIndex, id, destinationBusId, sendDb, sendTimingCode(timing));
|
|
1208
1114
|
}
|
|
1209
|
-
/**
|
|
1210
|
-
|
|
1211
|
-
this.
|
|
1115
|
+
/** Set the send level (in dB) for an existing send by index. */
|
|
1116
|
+
setSendDb(stripIndex, sendIndex, sendDb) {
|
|
1117
|
+
this.mixer.setSendDb(stripIndex, sendIndex, sendDb);
|
|
1212
1118
|
}
|
|
1213
|
-
/**
|
|
1214
|
-
|
|
1215
|
-
|
|
1119
|
+
/**
|
|
1120
|
+
* Remove an existing send from a strip by index.
|
|
1121
|
+
*
|
|
1122
|
+
* Sends are addressed in add order. After removal, sends with a higher index
|
|
1123
|
+
* than `sendIndex` shift down by one. Recompile (or process) before reading
|
|
1124
|
+
* results so the routing graph rebuilds.
|
|
1125
|
+
*
|
|
1126
|
+
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
1127
|
+
* @param sendIndex - Send index in add order
|
|
1128
|
+
*/
|
|
1129
|
+
removeSend(stripIndex, sendIndex) {
|
|
1130
|
+
this.mixer.removeSend(stripIndex, sendIndex);
|
|
1216
1131
|
}
|
|
1217
|
-
/**
|
|
1218
|
-
|
|
1219
|
-
|
|
1132
|
+
/**
|
|
1133
|
+
* Read a strip's meter snapshot at the given tap point.
|
|
1134
|
+
*
|
|
1135
|
+
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
1136
|
+
* @param tap - `'preFader'` or `'postFader'` (default: `'postFader'`)
|
|
1137
|
+
*/
|
|
1138
|
+
meterTap(stripIndex, tap = "postFader") {
|
|
1139
|
+
return this.mixer.meterTap(stripIndex, meterTapCode(tap));
|
|
1220
1140
|
}
|
|
1221
1141
|
/**
|
|
1222
|
-
*
|
|
1223
|
-
*
|
|
1224
|
-
*
|
|
1225
|
-
*
|
|
1226
|
-
*
|
|
1142
|
+
* Read a strip's meter snapshot.
|
|
1143
|
+
*
|
|
1144
|
+
* With no `tap` argument this reads the strip's own (post-fader) meter,
|
|
1145
|
+
* matching the Node/Python tap-less `stripMeter` contract. Pass an optional
|
|
1146
|
+
* `tap` (`'preFader'` / `'postFader'`) to read the tap-selectable snapshot
|
|
1147
|
+
* instead — the same backing call as {@link meterTap}.
|
|
1148
|
+
*
|
|
1149
|
+
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
1150
|
+
* @param tap - Optional tap point (`'preFader'` / `'postFader'`); when omitted
|
|
1151
|
+
* the tap-less post-fader strip meter is read.
|
|
1227
1152
|
*/
|
|
1228
|
-
|
|
1229
|
-
|
|
1153
|
+
stripMeter(stripIndex, tap) {
|
|
1154
|
+
if (tap === void 0) {
|
|
1155
|
+
return this.mixer.stripMeter(stripIndex);
|
|
1156
|
+
}
|
|
1157
|
+
return this.mixer.meterTap(stripIndex, meterTapCode(tap));
|
|
1230
1158
|
}
|
|
1231
|
-
|
|
1232
|
-
|
|
1159
|
+
/**
|
|
1160
|
+
* Schedule sample-accurate fader automation on a strip.
|
|
1161
|
+
*
|
|
1162
|
+
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
1163
|
+
* @param samplePos - Absolute samples from the start of processing
|
|
1164
|
+
* @param faderDb - Target fader level in dB
|
|
1165
|
+
* @param curve - Interpolation curve (default: `'linear'`)
|
|
1166
|
+
*/
|
|
1167
|
+
scheduleFaderAutomation(stripIndex, samplePos, faderDb, curve = "linear") {
|
|
1168
|
+
this.mixer.scheduleFaderAutomation(stripIndex, samplePos, faderDb, automationCurveCode(curve));
|
|
1233
1169
|
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1170
|
+
/**
|
|
1171
|
+
* Schedule sample-accurate pan automation on a strip.
|
|
1172
|
+
*
|
|
1173
|
+
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
1174
|
+
* @param samplePos - Absolute samples from the start of processing
|
|
1175
|
+
* @param pan - Target pan position
|
|
1176
|
+
* @param curve - Interpolation curve (default: `'linear'`)
|
|
1177
|
+
*/
|
|
1178
|
+
schedulePanAutomation(stripIndex, samplePos, pan, curve = "linear") {
|
|
1179
|
+
this.mixer.schedulePanAutomation(stripIndex, samplePos, pan, automationCurveCode(curve));
|
|
1236
1180
|
}
|
|
1237
|
-
/**
|
|
1238
|
-
|
|
1239
|
-
|
|
1181
|
+
/**
|
|
1182
|
+
* Schedule sample-accurate width automation on a strip.
|
|
1183
|
+
*
|
|
1184
|
+
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
1185
|
+
* @param samplePos - Absolute samples from the start of processing
|
|
1186
|
+
* @param width - Target stereo width
|
|
1187
|
+
* @param curve - Interpolation curve (default: `'linear'`)
|
|
1188
|
+
*/
|
|
1189
|
+
scheduleWidthAutomation(stripIndex, samplePos, width, curve = "linear") {
|
|
1190
|
+
this.mixer.scheduleWidthAutomation(stripIndex, samplePos, width, automationCurveCode(curve));
|
|
1240
1191
|
}
|
|
1241
|
-
/**
|
|
1242
|
-
|
|
1243
|
-
|
|
1192
|
+
/**
|
|
1193
|
+
* Schedule sample-accurate send-level automation on a strip's send.
|
|
1194
|
+
*
|
|
1195
|
+
* @param stripIndex - Strip index in `[0, stripCount())`
|
|
1196
|
+
* @param sendIndex - Send index in the strip's add order
|
|
1197
|
+
* @param samplePos - Absolute samples from the start of processing
|
|
1198
|
+
* @param db - Target send level in dB
|
|
1199
|
+
* @param curve - Interpolation curve (default: `'linear'`)
|
|
1200
|
+
*/
|
|
1201
|
+
scheduleSendAutomation(stripIndex, sendIndex, samplePos, db, curve = "linear") {
|
|
1202
|
+
this.mixer.scheduleSendAutomation(
|
|
1203
|
+
stripIndex,
|
|
1204
|
+
sendIndex,
|
|
1205
|
+
samplePos,
|
|
1206
|
+
db,
|
|
1207
|
+
automationCurveCode(curve)
|
|
1208
|
+
);
|
|
1244
1209
|
}
|
|
1245
|
-
/**
|
|
1246
|
-
|
|
1247
|
-
|
|
1210
|
+
/**
|
|
1211
|
+
* Read up to `maxPoints` of a strip's most recent goniometer samples
|
|
1212
|
+
* (oldest to newest).
|
|
1213
|
+
*/
|
|
1214
|
+
readGoniometerLatest(stripIndex, maxPoints) {
|
|
1215
|
+
return this.mixer.readGoniometerLatest(stripIndex, maxPoints);
|
|
1248
1216
|
}
|
|
1249
|
-
/**
|
|
1250
|
-
|
|
1251
|
-
this.
|
|
1217
|
+
/** Serialize the current scene (strips, buses, sends, connections) to JSON. */
|
|
1218
|
+
toSceneJson() {
|
|
1219
|
+
return this.mixer.toSceneJson();
|
|
1252
1220
|
}
|
|
1253
1221
|
/**
|
|
1254
|
-
*
|
|
1255
|
-
*
|
|
1222
|
+
* Longest audible serial processor-tail path to the master, in samples. Lazily
|
|
1223
|
+
* compiles the routing graph if the topology is dirty.
|
|
1256
1224
|
*/
|
|
1257
|
-
|
|
1258
|
-
this.
|
|
1259
|
-
}
|
|
1260
|
-
createClipPageProvider(numChannels, numSamples, pageFrames) {
|
|
1261
|
-
const id = this.native.createClipPageProvider(numChannels, numSamples, pageFrames);
|
|
1262
|
-
return new ClipPageProvider(this, id);
|
|
1225
|
+
tailSamples() {
|
|
1226
|
+
return this.mixer.tailSamples();
|
|
1263
1227
|
}
|
|
1264
|
-
|
|
1265
|
-
|
|
1228
|
+
/**
|
|
1229
|
+
* Reported latency (samples) of the compiled mixer graph, for aligning
|
|
1230
|
+
* dry/wet material. Lazily compiles the routing graph if the topology is dirty.
|
|
1231
|
+
*/
|
|
1232
|
+
latencySamples() {
|
|
1233
|
+
return this.mixer.latencySamples();
|
|
1266
1234
|
}
|
|
1267
|
-
|
|
1268
|
-
|
|
1235
|
+
/**
|
|
1236
|
+
* Drain delayed / tail audio by processing a zero-input block of `numSamples`
|
|
1237
|
+
* frames after the host stops feeding strip inputs. Returns the mixed stereo
|
|
1238
|
+
* master (`left`, `right`, `sampleRate`).
|
|
1239
|
+
*/
|
|
1240
|
+
drainTailStereo(numSamples) {
|
|
1241
|
+
if (!Number.isSafeInteger(numSamples) || numSamples <= 0 || numSamples > this.blockSize) {
|
|
1242
|
+
throw new RangeError(
|
|
1243
|
+
`Mixer.drainTailStereo: numSamples must be an integer in [1, ${this.blockSize}]`
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
return this.mixer.drainTailStereo(numSamples);
|
|
1269
1247
|
}
|
|
1270
|
-
|
|
1271
|
-
|
|
1248
|
+
/** Release the underlying WASM object. Safe to call only once. */
|
|
1249
|
+
delete() {
|
|
1250
|
+
this.mixer.delete();
|
|
1272
1251
|
}
|
|
1273
|
-
|
|
1274
|
-
|
|
1252
|
+
/** Alias for {@link delete}, provided for cross-binding (Node) compatibility. */
|
|
1253
|
+
destroy() {
|
|
1254
|
+
this.delete();
|
|
1275
1255
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1256
|
+
};
|
|
1257
|
+
|
|
1258
|
+
// src/realtime_voice_changer.ts
|
|
1259
|
+
function isFlatVoiceChangerPod(config) {
|
|
1260
|
+
return typeof config === "object" && config !== null && "retuneSemitones" in config;
|
|
1261
|
+
}
|
|
1262
|
+
function flatVoiceChangerPodToNested(pod) {
|
|
1263
|
+
return {
|
|
1264
|
+
inputGainDb: pod.inputGainDb,
|
|
1265
|
+
outputGainDb: pod.outputGainDb,
|
|
1266
|
+
wetMix: pod.wetMix,
|
|
1267
|
+
retune: { semitones: pod.retuneSemitones, mix: pod.retuneMix, grainSize: pod.retuneGrainSize },
|
|
1268
|
+
formant: {
|
|
1269
|
+
factor: pod.formantFactor,
|
|
1270
|
+
amount: pod.formantAmount,
|
|
1271
|
+
body: pod.formantBody,
|
|
1272
|
+
brightness: pod.formantBrightness,
|
|
1273
|
+
nasal: pod.formantNasal
|
|
1274
|
+
},
|
|
1275
|
+
eq: {
|
|
1276
|
+
highpassHz: pod.eqHighpassHz,
|
|
1277
|
+
bodyDb: pod.eqBodyDb,
|
|
1278
|
+
presenceDb: pod.eqPresenceDb,
|
|
1279
|
+
airDb: pod.eqAirDb
|
|
1280
|
+
},
|
|
1281
|
+
gate: {
|
|
1282
|
+
thresholdDb: pod.gateThresholdDb,
|
|
1283
|
+
attackMs: pod.gateAttackMs,
|
|
1284
|
+
releaseMs: pod.gateReleaseMs,
|
|
1285
|
+
rangeDb: pod.gateRangeDb
|
|
1286
|
+
},
|
|
1287
|
+
compressor: {
|
|
1288
|
+
thresholdDb: pod.compressorThresholdDb,
|
|
1289
|
+
ratio: pod.compressorRatio,
|
|
1290
|
+
attackMs: pod.compressorAttackMs,
|
|
1291
|
+
releaseMs: pod.compressorReleaseMs,
|
|
1292
|
+
makeupGainDb: pod.compressorMakeupGainDb
|
|
1293
|
+
},
|
|
1294
|
+
deesser: {
|
|
1295
|
+
frequencyHz: pod.deesserFrequencyHz,
|
|
1296
|
+
thresholdDb: pod.deesserThresholdDb,
|
|
1297
|
+
ratio: pod.deesserRatio,
|
|
1298
|
+
rangeDb: pod.deesserRangeDb
|
|
1299
|
+
},
|
|
1300
|
+
reverb: {
|
|
1301
|
+
mix: pod.reverbMix,
|
|
1302
|
+
timeMs: pod.reverbTimeMs,
|
|
1303
|
+
damping: pod.reverbDamping,
|
|
1304
|
+
seed: pod.reverbSeed
|
|
1305
|
+
},
|
|
1306
|
+
limiter: {
|
|
1307
|
+
ceilingDb: pod.limiterCeilingDb,
|
|
1308
|
+
releaseMs: pod.limiterReleaseMs,
|
|
1309
|
+
enableIspLimiter: pod.limiterEnableIspLimiter,
|
|
1310
|
+
ispCeilingDbtp: pod.limiterIspCeilingDbtp
|
|
1311
|
+
}
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
var RealtimeVoiceChanger = class {
|
|
1315
|
+
constructor(config = "neutral-monitor") {
|
|
1316
|
+
const module2 = getSonareModule();
|
|
1317
|
+
this.changer = module2.createRealtimeVoiceChanger(config);
|
|
1278
1318
|
}
|
|
1279
|
-
|
|
1280
|
-
this.
|
|
1319
|
+
prepare(sampleRate, maxBlockSize = 128, channels = 1) {
|
|
1320
|
+
this.changer.prepare(sampleRate, maxBlockSize, channels);
|
|
1281
1321
|
}
|
|
1282
|
-
|
|
1283
|
-
this.
|
|
1322
|
+
reset() {
|
|
1323
|
+
this.changer.reset();
|
|
1284
1324
|
}
|
|
1285
|
-
|
|
1286
|
-
|
|
1325
|
+
setConfig(config) {
|
|
1326
|
+
const resolved = isFlatVoiceChangerPod(config) ? flatVoiceChangerPodToNested(config) : config;
|
|
1327
|
+
this.changer.setConfig(resolved);
|
|
1287
1328
|
}
|
|
1288
|
-
|
|
1289
|
-
this.
|
|
1329
|
+
configJson() {
|
|
1330
|
+
return this.changer.configJson();
|
|
1290
1331
|
}
|
|
1291
|
-
|
|
1292
|
-
this.
|
|
1332
|
+
latencySamples() {
|
|
1333
|
+
return this.changer.latencySamples();
|
|
1293
1334
|
}
|
|
1294
|
-
|
|
1295
|
-
this.
|
|
1335
|
+
processMono(samples) {
|
|
1336
|
+
return this.changer.processMono(samples);
|
|
1296
1337
|
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1338
|
+
processMonoInto(samples, output) {
|
|
1339
|
+
this.changer.processMonoInto(samples, output);
|
|
1299
1340
|
}
|
|
1300
|
-
|
|
1301
|
-
return this.
|
|
1341
|
+
processInterleaved(samples, channels) {
|
|
1342
|
+
return this.changer.processInterleaved(samples, channels);
|
|
1302
1343
|
}
|
|
1303
|
-
|
|
1304
|
-
|
|
1344
|
+
processInterleavedInto(samples, channels, output) {
|
|
1345
|
+
this.changer.processInterleavedInto(samples, channels, output);
|
|
1305
1346
|
}
|
|
1306
1347
|
/**
|
|
1307
|
-
*
|
|
1308
|
-
*
|
|
1309
|
-
*
|
|
1310
|
-
*
|
|
1348
|
+
* Acquire a typed-memory view onto the WASM heap for mono input.
|
|
1349
|
+
*
|
|
1350
|
+
* Write your input samples into the returned `Float32Array` directly (e.g.
|
|
1351
|
+
* via `input.set(source)`); no copy crosses the JS↔C++ bridge until
|
|
1352
|
+
* {@link processPreparedMono} is called. The view is owned by this
|
|
1353
|
+
* RealtimeVoiceChanger and becomes invalid after {@link delete}; it may
|
|
1354
|
+
* also be invalidated if you later call this method with a larger
|
|
1355
|
+
* `numSamples` value (the underlying buffer may be reallocated).
|
|
1311
1356
|
*/
|
|
1312
|
-
|
|
1313
|
-
this.
|
|
1357
|
+
getMonoInputBuffer(numSamples) {
|
|
1358
|
+
return this.changer.getMonoInputBuffer(numSamples);
|
|
1314
1359
|
}
|
|
1315
|
-
/**
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
* read the same view back. Re-acquire after WASM memory growth.
|
|
1319
|
-
*/
|
|
1320
|
-
getChannelBuffer(channel, numFrames) {
|
|
1321
|
-
return this.native.getChannelBuffer(channel, numFrames);
|
|
1360
|
+
/** Mono output view counterpart to {@link getMonoInputBuffer}. */
|
|
1361
|
+
getMonoOutputBuffer(numSamples) {
|
|
1362
|
+
return this.changer.getMonoOutputBuffer(numSamples);
|
|
1322
1363
|
}
|
|
1323
1364
|
/**
|
|
1324
|
-
*
|
|
1325
|
-
*
|
|
1326
|
-
*
|
|
1365
|
+
* Process the previously-acquired mono input buffer in place. The output
|
|
1366
|
+
* appears in the buffer returned by {@link getMonoOutputBuffer}. No JS↔C++
|
|
1367
|
+
* sample-level crossings happen on this call — it just hands control to
|
|
1368
|
+
* the underlying DSP on already-on-heap data.
|
|
1327
1369
|
*/
|
|
1328
|
-
|
|
1329
|
-
this.
|
|
1330
|
-
}
|
|
1331
|
-
processWithMonitor(channels) {
|
|
1332
|
-
return this.native.processWithMonitor(channels);
|
|
1333
|
-
}
|
|
1334
|
-
renderOffline(channels, blockSize = 128) {
|
|
1335
|
-
return this.native.renderOffline(channels, blockSize);
|
|
1336
|
-
}
|
|
1337
|
-
bounceOffline(options) {
|
|
1338
|
-
return this.native.bounceOffline(options);
|
|
1339
|
-
}
|
|
1340
|
-
freezeOffline(options) {
|
|
1341
|
-
return this.native.freezeOffline(options);
|
|
1370
|
+
processPreparedMono(numSamples) {
|
|
1371
|
+
this.changer.processPreparedMono(numSamples);
|
|
1342
1372
|
}
|
|
1343
|
-
|
|
1344
|
-
|
|
1373
|
+
/** Interleaved input view (layout L0,R0,L1,R1,...). */
|
|
1374
|
+
getInterleavedInputBuffer(numFrames, numChannels) {
|
|
1375
|
+
return this.changer.getInterleavedInputBuffer(numFrames, numChannels);
|
|
1345
1376
|
}
|
|
1346
|
-
|
|
1347
|
-
|
|
1377
|
+
/** Interleaved output view counterpart. */
|
|
1378
|
+
getInterleavedOutputBuffer(numFrames, numChannels) {
|
|
1379
|
+
return this.changer.getInterleavedOutputBuffer(numFrames, numChannels);
|
|
1348
1380
|
}
|
|
1349
1381
|
/**
|
|
1350
|
-
*
|
|
1351
|
-
*
|
|
1352
|
-
* stays the stereo fast path. The two share one queue — call only one per
|
|
1353
|
-
* target. The live AudioWorklet path owns the queue via the stereo drain, so
|
|
1354
|
-
* this wide drain is for an offline (non-worklet) engine instance; per-plane
|
|
1355
|
-
* surround meters are not delivered over the live worklet meter ring.
|
|
1382
|
+
* Process the previously-acquired interleaved buffer in place. Output
|
|
1383
|
+
* appears in the buffer returned by {@link getInterleavedOutputBuffer}.
|
|
1356
1384
|
*/
|
|
1357
|
-
|
|
1358
|
-
|
|
1385
|
+
processPreparedInterleaved(numFrames, numChannels) {
|
|
1386
|
+
this.changer.processPreparedInterleaved(numFrames, numChannels);
|
|
1359
1387
|
}
|
|
1360
1388
|
/**
|
|
1361
|
-
*
|
|
1362
|
-
*
|
|
1363
|
-
* is the FFT band resolution (1..64); changing it re-prepares the tap. Returns
|
|
1364
|
-
* the band count actually applied.
|
|
1389
|
+
* Planar-channel input/output view (one Float32Array per channel). Matches
|
|
1390
|
+
* AudioWorklet's native layout; processing happens in place.
|
|
1365
1391
|
*/
|
|
1366
|
-
|
|
1367
|
-
return this.
|
|
1368
|
-
}
|
|
1369
|
-
/** Drains pending spectrum + vectorscope snapshots (per mix target). */
|
|
1370
|
-
drainScopeTelemetry(maxRecords = 1024) {
|
|
1371
|
-
return this.native.drainScopeTelemetry(maxRecords);
|
|
1392
|
+
getPlanarChannelBuffer(channel, numFrames) {
|
|
1393
|
+
return this.changer.getPlanarChannelBuffer(channel, numFrames);
|
|
1372
1394
|
}
|
|
1373
|
-
|
|
1374
|
-
|
|
1395
|
+
/**
|
|
1396
|
+
* Process the previously-acquired planar channel buffers in place. Each
|
|
1397
|
+
* channel must have been obtained from {@link getPlanarChannelBuffer}
|
|
1398
|
+
* with the same `numFrames`. Output replaces input in the same buffers.
|
|
1399
|
+
*/
|
|
1400
|
+
processPreparedPlanar(numFrames) {
|
|
1401
|
+
this.changer.processPreparedPlanar(numFrames);
|
|
1375
1402
|
}
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1403
|
+
/**
|
|
1404
|
+
* Convenience factory for the mono zero-copy path: returns the input/output
|
|
1405
|
+
* heap views plus a `process()` thunk wired to the same `numSamples`. The
|
|
1406
|
+
* views are reused across calls and become invalid after {@link delete}.
|
|
1407
|
+
*/
|
|
1408
|
+
createRealtimeMonoBuffer(numSamples) {
|
|
1409
|
+
let input = this.getMonoInputBuffer(numSamples);
|
|
1410
|
+
let output = this.getMonoOutputBuffer(numSamples);
|
|
1411
|
+
const reacquireIfDetached = () => {
|
|
1412
|
+
if (input.byteLength === 0 || output.byteLength === 0) {
|
|
1413
|
+
input = this.getMonoInputBuffer(numSamples);
|
|
1414
|
+
output = this.getMonoOutputBuffer(numSamples);
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1417
|
+
return {
|
|
1418
|
+
get input() {
|
|
1419
|
+
reacquireIfDetached();
|
|
1420
|
+
return input;
|
|
1421
|
+
},
|
|
1422
|
+
get output() {
|
|
1423
|
+
reacquireIfDetached();
|
|
1424
|
+
return output;
|
|
1425
|
+
},
|
|
1426
|
+
process: () => {
|
|
1427
|
+
reacquireIfDetached();
|
|
1428
|
+
this.processPreparedMono(numSamples);
|
|
1429
|
+
}
|
|
1430
|
+
};
|
|
1382
1431
|
}
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1432
|
+
/** Same as {@link createRealtimeMonoBuffer} but for interleaved I/O. */
|
|
1433
|
+
createRealtimeInterleavedBuffer(numFrames, numChannels) {
|
|
1434
|
+
let input = this.getInterleavedInputBuffer(numFrames, numChannels);
|
|
1435
|
+
let output = this.getInterleavedOutputBuffer(numFrames, numChannels);
|
|
1436
|
+
const reacquireIfDetached = () => {
|
|
1437
|
+
if (input.byteLength === 0 || output.byteLength === 0) {
|
|
1438
|
+
input = this.getInterleavedInputBuffer(numFrames, numChannels);
|
|
1439
|
+
output = this.getInterleavedOutputBuffer(numFrames, numChannels);
|
|
1440
|
+
}
|
|
1441
|
+
};
|
|
1442
|
+
return {
|
|
1443
|
+
get input() {
|
|
1444
|
+
reacquireIfDetached();
|
|
1445
|
+
return input;
|
|
1446
|
+
},
|
|
1447
|
+
get output() {
|
|
1448
|
+
reacquireIfDetached();
|
|
1449
|
+
return output;
|
|
1450
|
+
},
|
|
1451
|
+
channels: numChannels,
|
|
1452
|
+
process: () => {
|
|
1453
|
+
reacquireIfDetached();
|
|
1454
|
+
this.processPreparedInterleaved(numFrames, numChannels);
|
|
1455
|
+
}
|
|
1456
|
+
};
|
|
1388
1457
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1458
|
+
/**
|
|
1459
|
+
* Convenience factory for the planar zero-copy path. Acquires one
|
|
1460
|
+
* heap-backed Float32Array per channel and returns a `process()` thunk
|
|
1461
|
+
* wired to the same `numFrames`. Buffers are reused across calls and
|
|
1462
|
+
* become invalid after {@link delete}.
|
|
1463
|
+
*/
|
|
1464
|
+
createRealtimePlanarBuffer(numFrames, numChannels) {
|
|
1465
|
+
let channels = [];
|
|
1466
|
+
const acquire = () => {
|
|
1467
|
+
channels = [];
|
|
1468
|
+
for (let ch = 0; ch < numChannels; ch++) {
|
|
1469
|
+
channels.push(this.getPlanarChannelBuffer(ch, numFrames));
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1472
|
+
acquire();
|
|
1473
|
+
const reacquireIfDetached = () => {
|
|
1474
|
+
if ((channels[0]?.byteLength ?? 0) === 0) {
|
|
1475
|
+
acquire();
|
|
1476
|
+
}
|
|
1477
|
+
};
|
|
1478
|
+
return {
|
|
1479
|
+
get channels() {
|
|
1480
|
+
reacquireIfDetached();
|
|
1481
|
+
return channels;
|
|
1482
|
+
},
|
|
1483
|
+
process: () => {
|
|
1484
|
+
reacquireIfDetached();
|
|
1485
|
+
this.processPreparedPlanar(numFrames);
|
|
1486
|
+
}
|
|
1487
|
+
};
|
|
1394
1488
|
}
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
return;
|
|
1398
|
-
}
|
|
1399
|
-
this.disposed = true;
|
|
1400
|
-
this.engine.destroyClipPageProvider(this.id);
|
|
1489
|
+
delete() {
|
|
1490
|
+
this.changer.delete();
|
|
1401
1491
|
}
|
|
1402
1492
|
};
|
|
1403
1493
|
|
|
@@ -2128,6 +2218,8 @@ async function resetCapture(ctx) {
|
|
|
2128
2218
|
}
|
|
2129
2219
|
|
|
2130
2220
|
// src/worklet/engine-clips.ts
|
|
2221
|
+
var PREBAKED_CLIP_PAGE_THRESHOLD = 16384;
|
|
2222
|
+
var PREBAKED_CLIP_PAGE_FRAMES = 4096;
|
|
2131
2223
|
function addClip(ctx, trackId, buffer, startPpq, opts = {}) {
|
|
2132
2224
|
const id = opts.id ?? ctx.allocateClipId();
|
|
2133
2225
|
const clip = {
|
|
@@ -2157,9 +2249,56 @@ function setMidiClips(ctx, clips) {
|
|
|
2157
2249
|
function syncClipsDelta(ctx, upserts, removeIds) {
|
|
2158
2250
|
const clips = Array.from(ctx.clips.values());
|
|
2159
2251
|
ctx.offlineEngine.setClips(clips);
|
|
2252
|
+
const preparedById = /* @__PURE__ */ new Map();
|
|
2253
|
+
for (const clip of clips) {
|
|
2254
|
+
if (clip.id === void 0) {
|
|
2255
|
+
continue;
|
|
2256
|
+
}
|
|
2257
|
+
const bakedChannels = ctx.offlineEngine.prebakedClipChannels(clip.id);
|
|
2258
|
+
preparedById.set(
|
|
2259
|
+
clip.id,
|
|
2260
|
+
bakedChannels === null ? clip : {
|
|
2261
|
+
...clip,
|
|
2262
|
+
channels: bakedChannels,
|
|
2263
|
+
clipOffsetSamples: 0,
|
|
2264
|
+
lengthSamples: bakedChannels[0]?.length ?? 0,
|
|
2265
|
+
loop: false,
|
|
2266
|
+
warpMode: "off",
|
|
2267
|
+
warpAnchors: void 0
|
|
2268
|
+
}
|
|
2269
|
+
);
|
|
2270
|
+
}
|
|
2271
|
+
const inlineUpserts = [];
|
|
2272
|
+
for (const clip of upserts) {
|
|
2273
|
+
const prepared = clip.id === void 0 ? clip : preparedById.get(clip.id) ?? clip;
|
|
2274
|
+
const channels = prepared.channels;
|
|
2275
|
+
if (prepared.id === void 0 || prepared.warpMode !== "off" || !channels || channels.length === 0 || channels[0].length <= PREBAKED_CLIP_PAGE_THRESHOLD) {
|
|
2276
|
+
inlineUpserts.push(prepared);
|
|
2277
|
+
continue;
|
|
2278
|
+
}
|
|
2279
|
+
const numSamples = channels[0].length;
|
|
2280
|
+
ctx.postSync({
|
|
2281
|
+
type: "syncClipPageProvider",
|
|
2282
|
+
clipId: prepared.id,
|
|
2283
|
+
clip: { ...prepared, channels: void 0, pageProvider: void 0 },
|
|
2284
|
+
numChannels: channels.length,
|
|
2285
|
+
numSamples,
|
|
2286
|
+
pageFrames: PREBAKED_CLIP_PAGE_FRAMES
|
|
2287
|
+
});
|
|
2288
|
+
for (let start = 0, pageIndex = 0; start < numSamples; start += PREBAKED_CLIP_PAGE_FRAMES, pageIndex++) {
|
|
2289
|
+
const page = channels.map(
|
|
2290
|
+
(channel) => channel.slice(start, start + PREBAKED_CLIP_PAGE_FRAMES)
|
|
2291
|
+
);
|
|
2292
|
+
ctx.postSync(
|
|
2293
|
+
{ type: "syncClipPage", clipId: prepared.id, pageIndex, channels: page },
|
|
2294
|
+
page.map((channel) => channel.buffer)
|
|
2295
|
+
);
|
|
2296
|
+
}
|
|
2297
|
+
ctx.postSync({ type: "syncClipPageCommit", clipId: prepared.id });
|
|
2298
|
+
}
|
|
2160
2299
|
ctx.postSync({
|
|
2161
2300
|
type: "syncClipsDelta",
|
|
2162
|
-
upserts,
|
|
2301
|
+
upserts: inlineUpserts,
|
|
2163
2302
|
removeIds
|
|
2164
2303
|
});
|
|
2165
2304
|
}
|
|
@@ -2336,7 +2475,7 @@ function isEngineSyncMessage(value) {
|
|
|
2336
2475
|
if (!isRecord(value) || typeof value.type !== "string") {
|
|
2337
2476
|
return false;
|
|
2338
2477
|
}
|
|
2339
|
-
return value.type === "syncClips" || value.type === "syncClipsDelta" || value.type === "syncMidiClips" || value.type === "syncMarkers" || value.type === "syncMetronome" || value.type === "syncAutomation" || value.type === "syncTempo" || value.type === "syncMixer" || value.type === "syncCapture" || value.type === "syncTrackStripEqBand" || value.type === "syncMasterStripEqBand" || value.type === "syncTrackStripInsertBypassed" || value.type === "syncMasterStripInsertBypassed" || value.type === "syncTrackStripInsertParamByName" || value.type === "syncMasterStripInsertParamByName" || value.type === "syncBusStripInsertParamByName" || value.type === "syncTrackStripPan" || value.type === "syncTrackStripPanLaw" || value.type === "syncTrackStripPanMode" || value.type === "syncTrackStripDualPan" || value.type === "syncTrackStripChannelDelaySamples" || value.type === "syncBuiltinInstrument" || value.type === "syncSynthInstrument" || value.type === "syncSf2Instrument" || value.type === "syncLoadSoundFont" || value.type === "syncMidiFx" || value.type === "syncClearMidiFx" || value.type === "syncMidiNoteOn" || value.type === "syncMidiNoteOff" || value.type === "syncMidiCc" || value.type === "syncMidiSysex" || value.type === "syncMidiPanic" || value.type === "syncMidiDestinationExternal" || value.type === "syncExternalMidiClock";
|
|
2478
|
+
return value.type === "syncClips" || value.type === "syncClipsDelta" || value.type === "syncClipPageProvider" || value.type === "syncClipPage" || value.type === "syncClipPageCommit" || value.type === "syncMidiClips" || value.type === "syncMarkers" || value.type === "syncMetronome" || value.type === "syncAutomation" || value.type === "syncTempo" || value.type === "syncMixer" || value.type === "syncCapture" || value.type === "syncTrackStripEqBand" || value.type === "syncMasterStripEqBand" || value.type === "syncTrackStripInsertBypassed" || value.type === "syncMasterStripInsertBypassed" || value.type === "syncTrackStripInsertParamByName" || value.type === "syncMasterStripInsertParamByName" || value.type === "syncBusStripInsertParamByName" || value.type === "syncTrackStripPan" || value.type === "syncTrackStripPanLaw" || value.type === "syncTrackStripPanMode" || value.type === "syncTrackStripDualPan" || value.type === "syncTrackStripChannelDelaySamples" || value.type === "syncBuiltinInstrument" || value.type === "syncSynthInstrument" || value.type === "syncSf2Instrument" || value.type === "syncLoadSoundFont" || value.type === "syncMidiFx" || value.type === "syncClearMidiFx" || value.type === "syncMidiNoteOn" || value.type === "syncMidiNoteOff" || value.type === "syncMidiCc" || value.type === "syncMidiSysex" || value.type === "syncMidiPanic" || value.type === "syncMidiDestinationExternal" || value.type === "syncExternalMidiClock";
|
|
2340
2479
|
}
|
|
2341
2480
|
function isEngineCaptureRequestMessage(value) {
|
|
2342
2481
|
return isRecord(value) && value.type === "captureRequest" && typeof value.requestId === "number" && (value.op === "status" || value.op === "read" || value.op === "reset");
|
|
@@ -3502,11 +3641,15 @@ var SonareEngine = class _SonareEngine {
|
|
|
3502
3641
|
// Posts an out-of-band control-sync message to the worklet engine processor.
|
|
3503
3642
|
// Sync messages use a string `type` so the worklet's message handler routes
|
|
3504
3643
|
// them to receiveSync() (numeric `type` is reserved for SonareEngineCommandRecord).
|
|
3505
|
-
postSync(message) {
|
|
3644
|
+
postSync(message, transfer) {
|
|
3506
3645
|
if (this.destroyed) {
|
|
3507
3646
|
return;
|
|
3508
3647
|
}
|
|
3509
|
-
|
|
3648
|
+
if (transfer && transfer.length > 0) {
|
|
3649
|
+
this.realtimeNode.node.port.postMessage(message, transfer);
|
|
3650
|
+
} else {
|
|
3651
|
+
this.realtimeNode.node.port.postMessage(message);
|
|
3652
|
+
}
|
|
3510
3653
|
}
|
|
3511
3654
|
// Collaborator surface handed to the mixer/routing free functions so they can
|
|
3512
3655
|
// mutate the routing stores (held by reference), mirror into the offline
|
|
@@ -3624,7 +3767,7 @@ var SonareEngine = class _SonareEngine {
|
|
|
3624
3767
|
clips: this.clips,
|
|
3625
3768
|
midiClips: this.midiClips,
|
|
3626
3769
|
allocateClipId: () => this.nextClipId++,
|
|
3627
|
-
postSync: (message) => this.postSync(message),
|
|
3770
|
+
postSync: (message, transfer) => this.postSync(message, transfer),
|
|
3628
3771
|
ensureTrackLane: (target) => this.ensureTrackLane(target),
|
|
3629
3772
|
resolveTargetId: (target) => this.resolveTargetId(target)
|
|
3630
3773
|
};
|
|
@@ -3721,6 +3864,8 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
|
|
|
3721
3864
|
// SetMetronome command only toggles enabled state; the config arrives here.
|
|
3722
3865
|
this.metronomeConfig = { ...DEFAULT_METRONOME_CONFIG };
|
|
3723
3866
|
this.liveClips = /* @__PURE__ */ new Map();
|
|
3867
|
+
this.pagedClipProviders = /* @__PURE__ */ new Map();
|
|
3868
|
+
this.pendingPagedClips = /* @__PURE__ */ new Map();
|
|
3724
3869
|
this.sampleRate = options.sampleRate ?? 48e3;
|
|
3725
3870
|
this.blockSize = options.blockSize ?? 128;
|
|
3726
3871
|
this.channelCount = Math.max(1, Math.floor(options.channelCount ?? 2));
|
|
@@ -3818,11 +3963,10 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
|
|
|
3818
3963
|
this.applyCommand(command);
|
|
3819
3964
|
}
|
|
3820
3965
|
}
|
|
3821
|
-
// Applies an out-of-band control-plane sync message
|
|
3822
|
-
//
|
|
3823
|
-
//
|
|
3824
|
-
//
|
|
3825
|
-
// of the engine's control-thread RtPublisher setters.
|
|
3966
|
+
// Applies an out-of-band control-plane sync message on the AudioWorklet
|
|
3967
|
+
// thread. These handlers must remain bounded: expensive clip transforms are
|
|
3968
|
+
// performed on the main-thread mirror, and long pre-baked PCM arrives in
|
|
3969
|
+
// small pages before the final lightweight clip schedule is committed.
|
|
3826
3970
|
receiveSync(message) {
|
|
3827
3971
|
if (this.closed) {
|
|
3828
3972
|
return;
|
|
@@ -3848,6 +3992,33 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
|
|
|
3848
3992
|
}
|
|
3849
3993
|
this.engine.setClips(Array.from(this.liveClips.values()));
|
|
3850
3994
|
break;
|
|
3995
|
+
case "syncClipPageProvider": {
|
|
3996
|
+
const provider = this.engine.createClipPageProvider(
|
|
3997
|
+
message.numChannels,
|
|
3998
|
+
message.numSamples,
|
|
3999
|
+
message.pageFrames
|
|
4000
|
+
);
|
|
4001
|
+
this.pagedClipProviders.set(message.clipId, provider.id);
|
|
4002
|
+
this.pendingPagedClips.set(message.clipId, message.clip);
|
|
4003
|
+
break;
|
|
4004
|
+
}
|
|
4005
|
+
case "syncClipPage": {
|
|
4006
|
+
const providerId = this.pagedClipProviders.get(message.clipId);
|
|
4007
|
+
if (providerId !== void 0) {
|
|
4008
|
+
this.engine.supplyClipPage(providerId, message.pageIndex, message.channels);
|
|
4009
|
+
}
|
|
4010
|
+
break;
|
|
4011
|
+
}
|
|
4012
|
+
case "syncClipPageCommit": {
|
|
4013
|
+
const providerId = this.pagedClipProviders.get(message.clipId);
|
|
4014
|
+
const clip = this.pendingPagedClips.get(message.clipId);
|
|
4015
|
+
if (providerId !== void 0 && clip) {
|
|
4016
|
+
this.liveClips.set(message.clipId, { ...clip, pageProvider: providerId });
|
|
4017
|
+
this.pendingPagedClips.delete(message.clipId);
|
|
4018
|
+
this.engine.setClips(Array.from(this.liveClips.values()));
|
|
4019
|
+
}
|
|
4020
|
+
break;
|
|
4021
|
+
}
|
|
3851
4022
|
case "syncMidiClips":
|
|
3852
4023
|
this.engine.setMidiClips(message.clips);
|
|
3853
4024
|
break;
|