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