@libraz/libsonare 1.5.2 → 1.5.3

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