@libraz/libsonare 1.5.2 → 1.5.4

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