@libraz/libsonare 1.2.3 → 1.3.1

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 (43) hide show
  1. package/README.md +38 -1
  2. package/dist/index.d.ts +1 -2842
  3. package/dist/index.js +3667 -1984
  4. package/dist/index.js.map +1 -1
  5. package/dist/sonare-rt-module.js +1 -1
  6. package/dist/sonare-rt.js +1 -1
  7. package/dist/sonare-rt.wasm +0 -0
  8. package/dist/sonare.js +1 -1
  9. package/dist/sonare.wasm +0 -0
  10. package/dist/worklet.d.ts +4816 -483
  11. package/dist/worklet.js +747 -440
  12. package/dist/worklet.js.map +1 -1
  13. package/package.json +2 -1
  14. package/src/analysis_helpers.ts +152 -0
  15. package/src/audio.ts +493 -0
  16. package/src/codes.ts +56 -0
  17. package/src/effects_mastering.ts +964 -0
  18. package/src/feature_core.ts +248 -0
  19. package/src/feature_music.ts +419 -0
  20. package/src/feature_pitch.ts +80 -0
  21. package/src/feature_resample.ts +21 -0
  22. package/src/feature_spectral.ts +330 -0
  23. package/src/feature_spectrogram.ts +454 -0
  24. package/src/features.ts +84 -0
  25. package/src/index.ts +341 -4963
  26. package/src/live_audio.ts +47 -0
  27. package/src/metering.ts +380 -0
  28. package/src/mixer.ts +523 -0
  29. package/src/module_state.ts +14 -0
  30. package/src/opfs_clip_pages.ts +203 -0
  31. package/src/project.ts +1614 -0
  32. package/src/public_types.ts +177 -2
  33. package/src/quick_analysis.ts +508 -0
  34. package/src/realtime_engine.ts +667 -0
  35. package/src/realtime_voice_changer.ts +275 -0
  36. package/src/scale.ts +42 -0
  37. package/src/sonare.js.d.ts +302 -4
  38. package/src/stream_analyzer.ts +275 -0
  39. package/src/stream_types.ts +26 -1
  40. package/src/streaming_mixing.ts +18 -0
  41. package/src/streaming_processors.ts +335 -0
  42. package/src/validation.ts +82 -0
  43. package/src/web_midi.ts +366 -0
package/dist/worklet.js CHANGED
@@ -1,5 +1,16 @@
1
- // src/index.ts
2
- var EXPECTED_ENGINE_ABI_VERSION = 2;
1
+ // src/module_state.ts
2
+ var wasmModule = null;
3
+ function setSonareModule(module2) {
4
+ wasmModule = module2;
5
+ }
6
+ function getSonareModule() {
7
+ if (!wasmModule) {
8
+ throw new Error("Module not initialized. Call init() first.");
9
+ }
10
+ return wasmModule;
11
+ }
12
+
13
+ // src/codes.ts
3
14
  function automationCurveCode(curve) {
4
15
  switch (curve) {
5
16
  case "linear":
@@ -50,238 +61,396 @@ function meterTapCode(tap) {
50
61
  function sendTimingCode(timing) {
51
62
  return timing === "preFader" || timing === 0 ? 0 : 1;
52
63
  }
53
- var module = null;
54
- var initPromise = null;
55
- async function init(options) {
56
- if (module) {
57
- return;
64
+
65
+ // src/mixer.ts
66
+ var Mixer = class _Mixer {
67
+ constructor(mixer) {
68
+ this.mixer = mixer;
58
69
  }
59
- if (initPromise) {
60
- return initPromise;
70
+ /**
71
+ * Build a mixer from a scene JSON string.
72
+ *
73
+ * @param json - Scene JSON (strips, buses, sends, connections, inserts)
74
+ * @param sampleRate - Sample rate in Hz (default: 48000)
75
+ * @param blockSize - Maximum block size per {@link processStereo} call (default: 512)
76
+ */
77
+ static fromSceneJson(json, sampleRate = 48e3, blockSize = 512) {
78
+ const module2 = getSonareModule();
79
+ return new _Mixer(module2.createMixerFromSceneJson(json, sampleRate, blockSize));
61
80
  }
62
- initPromise = (async () => {
63
- try {
64
- const createModule = (await import("./sonare.js")).default;
65
- module = await createModule(options);
66
- } catch (error) {
67
- initPromise = null;
68
- throw error;
81
+ /** Rebuild and compile the routing graph from the current scene topology. */
82
+ compile() {
83
+ this.mixer.compile();
84
+ }
85
+ /**
86
+ * Mix one block of per-strip stereo audio into the stereo master.
87
+ *
88
+ * @param leftChannels - `leftChannels[i]` is the left channel of strip `i`
89
+ * @param rightChannels - `rightChannels[i]` is the right channel of strip `i`
90
+ * @returns Mixed stereo master (`left`, `right`, `sampleRate`)
91
+ */
92
+ processStereo(leftChannels, rightChannels) {
93
+ if (leftChannels.length !== rightChannels.length) {
94
+ throw new Error("leftChannels and rightChannels must have the same length.");
69
95
  }
70
- })();
71
- return initPromise;
72
- }
73
- function isInitialized() {
74
- return module !== null;
75
- }
76
- function engineAbiVersion() {
77
- if (!module) {
78
- throw new Error("Module not initialized. Call init() first.");
96
+ return this.mixer.processStereo(leftChannels, rightChannels);
79
97
  }
80
- return module.engineAbiVersion();
81
- }
82
- function engineCapabilities() {
83
- const abiVersion = engineAbiVersion();
84
- const sharedArrayBuffer = typeof globalThis.SharedArrayBuffer === "function";
85
- const atomics = typeof globalThis.Atomics === "object";
86
- const audioWorklet = typeof AudioWorkletNode !== "undefined" || typeof globalThis.AudioWorkletProcessor !== "undefined";
87
- return {
88
- engineAbiVersion: abiVersion,
89
- expectedEngineAbiVersion: EXPECTED_ENGINE_ABI_VERSION,
90
- abiCompatible: abiVersion === EXPECTED_ENGINE_ABI_VERSION,
91
- sharedArrayBuffer,
92
- atomics,
93
- audioWorklet,
94
- mode: sharedArrayBuffer && atomics ? "sab" : "postMessage"
95
- };
96
- }
97
- var RealtimeEngine = class {
98
- constructor(sampleRate = 48e3, maxBlockSize = 128, commandCapacity = 1024, telemetryCapacity = 1024) {
99
- if (!module) {
100
- throw new Error("Module not initialized. Call init() first.");
98
+ /**
99
+ * Mix one block into caller-owned output arrays.
100
+ *
101
+ * This avoids allocating the result object and result `Float32Array`s. It is
102
+ * intended for realtime bridges such as AudioWorklet; the input channel count
103
+ * must match the scene strip count and all arrays must have the same length.
104
+ */
105
+ processStereoInto(leftChannels, rightChannels, outLeft, outRight) {
106
+ if (leftChannels.length !== rightChannels.length) {
107
+ throw new Error("leftChannels and rightChannels must have the same length.");
101
108
  }
102
- const capabilities = engineCapabilities();
103
- if (!capabilities.abiCompatible) {
104
- throw new Error(
105
- `Engine ABI mismatch: wasm=${capabilities.engineAbiVersion}, expected=${capabilities.expectedEngineAbiVersion}`
106
- );
109
+ if (outLeft.length !== outRight.length) {
110
+ throw new Error("outLeft and outRight must have the same length.");
107
111
  }
108
- this.native = new module.RealtimeEngine(
109
- sampleRate,
110
- maxBlockSize,
111
- commandCapacity,
112
- telemetryCapacity
113
- );
114
- }
115
- prepare(sampleRate, maxBlockSize, commandCapacity = 1024, telemetryCapacity = 1024) {
116
- this.native.prepare(sampleRate, maxBlockSize, commandCapacity, telemetryCapacity);
117
- }
118
- /** Queue a sample-accurate parameter change (engine kSetParam). */
119
- setParameter(paramId, value, renderFrame = -1) {
120
- this.native.setParameter(paramId, value, renderFrame);
112
+ this.mixer.processStereoInto(leftChannels, rightChannels, outLeft, outRight);
121
113
  }
122
- /** Queue a smoothed parameter change (engine kSetParamSmoothed). */
123
- setParameterSmoothed(paramId, value, renderFrame = -1) {
124
- this.native.setParameterSmoothed(paramId, value, renderFrame);
114
+ /**
115
+ * Create reusable WASM-heap input/output views for realtime-style processing.
116
+ *
117
+ * Fill `leftInputs[i]` / `rightInputs[i]`, call `process()`, then read
118
+ * `outLeft` / `outRight`. The views are owned by this mixer and become invalid
119
+ * after {@link delete}.
120
+ */
121
+ createRealtimeBuffer() {
122
+ const stripCount = this.stripCount();
123
+ let leftInputs = [];
124
+ let rightInputs = [];
125
+ let outLeft = this.mixer.outputLeftView();
126
+ let outRight = this.mixer.outputRightView();
127
+ const acquire = () => {
128
+ leftInputs = [];
129
+ rightInputs = [];
130
+ for (let index = 0; index < stripCount; index++) {
131
+ leftInputs.push(this.mixer.inputLeftView(index));
132
+ rightInputs.push(this.mixer.inputRightView(index));
133
+ }
134
+ outLeft = this.mixer.outputLeftView();
135
+ outRight = this.mixer.outputRightView();
136
+ };
137
+ acquire();
138
+ const reacquireIfDetached = () => {
139
+ if (outLeft.byteLength === 0 || (leftInputs[0]?.byteLength ?? 1) === 0) {
140
+ acquire();
141
+ }
142
+ };
143
+ return {
144
+ get leftInputs() {
145
+ reacquireIfDetached();
146
+ return leftInputs;
147
+ },
148
+ get rightInputs() {
149
+ reacquireIfDetached();
150
+ return rightInputs;
151
+ },
152
+ get outLeft() {
153
+ reacquireIfDetached();
154
+ return outLeft;
155
+ },
156
+ get outRight() {
157
+ reacquireIfDetached();
158
+ return outRight;
159
+ },
160
+ process: (numSamples = outLeft.length) => {
161
+ reacquireIfDetached();
162
+ this.mixer.processPreparedStereo(numSamples);
163
+ }
164
+ };
125
165
  }
126
- /** Read back the current transport state snapshot. */
127
- getTransportState() {
128
- return this.native.getTransportState();
166
+ /** Number of strips in the mixer (e.g. strips loaded from the scene). */
167
+ stripCount() {
168
+ return this.mixer.stripCount();
129
169
  }
130
- play(renderFrame = -1) {
131
- this.native.play(renderFrame);
170
+ /**
171
+ * Schedule sample-accurate insert-parameter automation on a strip's insert.
172
+ *
173
+ * @param stripIndex - Strip index in `[0, stripCount())`
174
+ * @param insertIndex - Index into the strip's combined insert sequence
175
+ * (`[pre-inserts... post-inserts...]`)
176
+ * @param paramId - Processor-specific parameter id
177
+ * @param samplePos - Absolute samples from the start of processing (the mixer
178
+ * advances an internal position from 0 on the first {@link processStereo}
179
+ * call; recompiling resets it to 0)
180
+ * @param value - Target parameter value
181
+ * @param curve - Interpolation curve (default: `'linear'`)
182
+ * @throws If the strip index is out of range or the schedule call fails
183
+ * (unknown curve, out-of-range insert index, or full event lane)
184
+ */
185
+ scheduleInsertAutomation(stripIndex, insertIndex, paramId, samplePos, value, curve = "linear") {
186
+ this.mixer.scheduleInsertAutomation(
187
+ stripIndex,
188
+ insertIndex,
189
+ paramId,
190
+ samplePos,
191
+ value,
192
+ automationCurveCode(curve)
193
+ );
132
194
  }
133
- stop(renderFrame = -1) {
134
- this.native.stop(renderFrame);
195
+ /**
196
+ * Resolve a strip's index in `[0, stripCount())` from its scene id, or `null`
197
+ * when no strip with that id exists (matches the Node binding's `number | null`).
198
+ */
199
+ stripById(id) {
200
+ const index = this.mixer.stripById(id);
201
+ return index < 0 ? null : index;
135
202
  }
136
- seekSample(timelineSample, renderFrame = -1) {
137
- this.native.seekSample(timelineSample, renderFrame);
203
+ /**
204
+ * Add a bus to the mixer topology. `role` is one of `'master'`, `'aux'`, or
205
+ * `'submix'` (defaults to `'aux'`). Marks the routing graph dirty; call
206
+ * {@link compile} (or {@link processStereo}) to rebuild.
207
+ */
208
+ addBus(id, role = "aux") {
209
+ this.mixer.addBus(id, role);
138
210
  }
139
- seekPpq(ppq, renderFrame = -1) {
140
- this.native.seekPpq(ppq, renderFrame);
211
+ /** Remove a bus by id. Marks the routing graph dirty. */
212
+ removeBus(id) {
213
+ this.mixer.removeBus(id);
141
214
  }
142
- setTempo(bpm) {
143
- this.native.setTempo(bpm);
215
+ /** Number of buses in the mixer topology. */
216
+ busCount() {
217
+ return this.mixer.busCount();
144
218
  }
145
- setTimeSignature(numerator, denominator) {
146
- this.native.setTimeSignature(numerator, denominator);
219
+ /**
220
+ * Add a VCA group with the given gain offset (dB). `members` is a list of
221
+ * strip ids governed by the group (may be empty).
222
+ */
223
+ addVcaGroup(id, gainDb = 0, members = []) {
224
+ this.mixer.addVcaGroup(id, gainDb, members);
147
225
  }
148
- setLoop(startPpq, endPpq, enabled = true) {
149
- this.native.setLoop(startPpq, endPpq, enabled);
226
+ /** Set an existing VCA group's gain in dB. */
227
+ setVcaGroupGainDb(id, gainDb) {
228
+ this.mixer.setVcaGroupGainDb(id, gainDb);
150
229
  }
151
- addParameter(info) {
152
- this.native.addParameter(info);
230
+ /** Remove a VCA group by id. */
231
+ removeVcaGroup(id) {
232
+ this.mixer.removeVcaGroup(id);
153
233
  }
154
- parameterCount() {
155
- return this.native.parameterCount();
234
+ /** Number of VCA groups in the mixer topology. */
235
+ vcaGroupCount() {
236
+ return this.mixer.vcaGroupCount();
156
237
  }
157
- parameterInfoByIndex(index) {
158
- return this.native.parameterInfoByIndex(index);
238
+ /** Set the strip's input trim in dB. */
239
+ setInputTrimDb(stripIndex, db) {
240
+ this.mixer.setInputTrimDb(stripIndex, db);
159
241
  }
160
- parameterInfo(id) {
161
- return this.native.parameterInfo(id);
242
+ /** Set the strip's fader level in dB. */
243
+ setFaderDb(stripIndex, db) {
244
+ this.mixer.setFaderDb(stripIndex, db);
162
245
  }
163
- setAutomationLane(paramId, points) {
164
- this.native.setAutomationLane(paramId, points);
246
+ /**
247
+ * Set the strip's pan position.
248
+ *
249
+ * @param stripIndex - Strip index in `[0, stripCount())`
250
+ * @param pan - Pan position in `[-1, 1]`
251
+ * @param panMode - Optional pan mode. When omitted the strip's current pan
252
+ * mode is kept (passes `SONARE_PAN_MODE_KEEP`), so a plain pan nudge does
253
+ * not reset a scene-defined `'stereoPan'` / `'dualPan'` mode back to
254
+ * balance. Pass `'balance'` (or `0`) explicitly to force balance mode.
255
+ */
256
+ setPan(stripIndex, pan, panMode) {
257
+ const mode = panMode === void 0 ? -1 : panModeCode(panMode);
258
+ this.mixer.setPan(stripIndex, pan, mode);
165
259
  }
166
- automationLaneCount() {
167
- return this.native.automationLaneCount();
260
+ /** Set the strip's stereo width. */
261
+ setWidth(stripIndex, width) {
262
+ this.mixer.setWidth(stripIndex, width);
168
263
  }
169
- setMarkers(markers) {
170
- this.native.setMarkers(markers);
264
+ /** Set the strip's mute state. */
265
+ setMuted(stripIndex, muted) {
266
+ this.mixer.setMuted(stripIndex, muted);
171
267
  }
172
- markerCount() {
173
- return this.native.markerCount();
268
+ /**
269
+ * Set a strip's solo state. Takes effect on the next process without a
270
+ * graph recompile.
271
+ */
272
+ setSoloed(stripIndex, soloed) {
273
+ this.mixer.setSoloed(stripIndex, soloed);
174
274
  }
175
- markerByIndex(index) {
176
- return this.native.markerByIndex(index);
275
+ /**
276
+ * Mark a strip solo-safe so it is never implied-muted by another strip's
277
+ * solo. Takes effect on the next process without a graph recompile.
278
+ */
279
+ setSoloSafe(stripIndex, soloSafe) {
280
+ this.mixer.setSoloSafe(stripIndex, soloSafe);
177
281
  }
178
- marker(id) {
179
- return this.native.marker(id);
282
+ /** Invert the polarity of the left and/or right channel of a strip. */
283
+ setPolarityInvert(stripIndex, invertLeft, invertRight) {
284
+ this.mixer.setPolarityInvert(stripIndex, invertLeft, invertRight);
180
285
  }
181
- seekMarker(markerId, renderFrame = -1) {
182
- this.native.seekMarker(markerId, renderFrame);
286
+ /** Set the strip's pan law. */
287
+ setPanLaw(stripIndex, panLaw) {
288
+ this.mixer.setPanLaw(stripIndex, panLawCode(panLaw));
183
289
  }
184
- setLoopFromMarkers(startMarkerId, endMarkerId) {
185
- this.native.setLoopFromMarkers(startMarkerId, endMarkerId);
186
- }
187
- setMetronome(config) {
188
- this.native.setMetronome(config);
189
- }
190
- metronome() {
191
- return this.native.metronome();
192
- }
193
- countInEndSample(startSample, bars) {
194
- return Number(this.native.countInEndSample(startSample, bars));
195
- }
196
- setGraph(spec) {
197
- this.native.setGraph(spec);
198
- }
199
- graphNodeCount() {
200
- return this.native.graphNodeCount();
201
- }
202
- graphConnectionCount() {
203
- return this.native.graphConnectionCount();
204
- }
205
- setClips(clips) {
206
- this.native.setClips(clips);
207
- }
208
- clipCount() {
209
- return this.native.clipCount();
290
+ /**
291
+ * Set a per-strip channel delay in samples. This changes the strip's reported
292
+ * latency; recompile to re-run latency compensation.
293
+ */
294
+ setChannelDelaySamples(stripIndex, delaySamples) {
295
+ this.mixer.setChannelDelaySamples(stripIndex, delaySamples);
210
296
  }
211
- setCaptureBuffer(numChannels, capacityFrames) {
212
- this.native.setCaptureBuffer(numChannels, capacityFrames);
297
+ /** Set the strip's live VCA gain offset in dB (not persisted to the scene). */
298
+ setVcaOffsetDb(stripIndex, offsetDb) {
299
+ this.mixer.setVcaOffsetDb(stripIndex, offsetDb);
213
300
  }
214
- armCapture(armed = true) {
215
- this.native.armCapture(armed);
301
+ /** Set independent left/right pan positions (dual-pan mode). */
302
+ setDualPan(stripIndex, leftPan, rightPan) {
303
+ this.mixer.setDualPan(stripIndex, leftPan, rightPan);
216
304
  }
217
- setCapturePunch(startSample, endSample, enabled = true) {
218
- this.native.setCapturePunch(startSample, endSample, enabled);
305
+ /**
306
+ * Add a send to a strip after construction.
307
+ *
308
+ * @param stripIndex - Strip index in `[0, stripCount())`
309
+ * @param id - Send id
310
+ * @param destinationBusId - Destination bus id
311
+ * @param sendDb - Initial send level in dB
312
+ * @param timing - `'preFader'` or `'postFader'` (default: `'postFader'`)
313
+ * @returns The new send's index
314
+ */
315
+ addSend(stripIndex, id, destinationBusId, sendDb = 0, timing = "postFader") {
316
+ return this.mixer.addSend(stripIndex, id, destinationBusId, sendDb, sendTimingCode(timing));
219
317
  }
220
- resetCapture() {
221
- this.native.resetCapture();
318
+ /** Set the send level (in dB) for an existing send by index. */
319
+ setSendDb(stripIndex, sendIndex, sendDb) {
320
+ this.mixer.setSendDb(stripIndex, sendIndex, sendDb);
222
321
  }
223
- captureStatus() {
224
- return this.native.captureStatus();
322
+ /**
323
+ * Remove an existing send from a strip by index.
324
+ *
325
+ * Sends are addressed in add order. After removal, sends with a higher index
326
+ * than `sendIndex` shift down by one. Recompile (or process) before reading
327
+ * results so the routing graph rebuilds.
328
+ *
329
+ * @param stripIndex - Strip index in `[0, stripCount())`
330
+ * @param sendIndex - Send index in add order
331
+ */
332
+ removeSend(stripIndex, sendIndex) {
333
+ this.mixer.removeSend(stripIndex, sendIndex);
225
334
  }
226
- capturedAudio() {
227
- return this.native.capturedAudio();
335
+ /**
336
+ * Read a strip's meter snapshot at the given tap point.
337
+ *
338
+ * @param stripIndex - Strip index in `[0, stripCount())`
339
+ * @param tap - `'preFader'` or `'postFader'` (default: `'postFader'`)
340
+ */
341
+ meterTap(stripIndex, tap = "postFader") {
342
+ return this.mixer.meterTap(stripIndex, meterTapCode(tap));
228
343
  }
229
- process(channels) {
230
- return this.native.process(channels);
344
+ /**
345
+ * Read a strip's meter snapshot.
346
+ *
347
+ * With no `tap` argument this reads the strip's own (post-fader) meter,
348
+ * matching the Node/Python tap-less `stripMeter` contract. Pass an optional
349
+ * `tap` (`'preFader'` / `'postFader'`) to read the tap-selectable snapshot
350
+ * instead — the same backing call as {@link meterTap}.
351
+ *
352
+ * @param stripIndex - Strip index in `[0, stripCount())`
353
+ * @param tap - Optional tap point (`'preFader'` / `'postFader'`); when omitted
354
+ * the tap-less post-fader strip meter is read.
355
+ */
356
+ stripMeter(stripIndex, tap) {
357
+ if (tap === void 0) {
358
+ return this.mixer.stripMeter(stripIndex);
359
+ }
360
+ return this.mixer.meterTap(stripIndex, meterTapCode(tap));
231
361
  }
232
362
  /**
233
- * Allocates persistent per-channel WASM-heap scratch for the zero-copy
234
- * `getChannelBuffer` / `processPrepared` realtime path. Call once (off the
235
- * audio thread) before driving `processPrepared` from an AudioWorklet so the
236
- * render callback never allocates on the C++/JS heap.
363
+ * Schedule sample-accurate fader automation on a strip.
364
+ *
365
+ * @param stripIndex - Strip index in `[0, stripCount())`
366
+ * @param samplePos - Absolute samples from the start of processing
367
+ * @param faderDb - Target fader level in dB
368
+ * @param curve - Interpolation curve (default: `'linear'`)
237
369
  */
238
- prepareChannels(numChannels, maxFrames) {
239
- this.native.prepareChannels(numChannels, maxFrames);
370
+ scheduleFaderAutomation(stripIndex, samplePos, faderDb, curve = "linear") {
371
+ this.mixer.scheduleFaderAutomation(stripIndex, samplePos, faderDb, automationCurveCode(curve));
240
372
  }
241
373
  /**
242
- * Returns a Float32Array view onto the persistent WASM-heap scratch for one
243
- * channel (valid for up to `numFrames`). Fill it, call `processPrepared`, then
244
- * read the same view back. Re-acquire after WASM memory growth.
374
+ * Schedule sample-accurate pan automation on a strip.
375
+ *
376
+ * @param stripIndex - Strip index in `[0, stripCount())`
377
+ * @param samplePos - Absolute samples from the start of processing
378
+ * @param pan - Target pan position
379
+ * @param curve - Interpolation curve (default: `'linear'`)
245
380
  */
246
- getChannelBuffer(channel, numFrames) {
247
- return this.native.getChannelBuffer(channel, numFrames);
381
+ schedulePanAutomation(stripIndex, samplePos, pan, curve = "linear") {
382
+ this.mixer.schedulePanAutomation(stripIndex, samplePos, pan, automationCurveCode(curve));
248
383
  }
249
384
  /**
250
- * Runs the engine in place over the prepared per-channel scratch buffers.
251
- * Allocation-free: safe to call on the AudioWorklet render thread after
252
- * `prepareChannels`.
385
+ * Schedule sample-accurate width automation on a strip.
386
+ *
387
+ * @param stripIndex - Strip index in `[0, stripCount())`
388
+ * @param samplePos - Absolute samples from the start of processing
389
+ * @param width - Target stereo width
390
+ * @param curve - Interpolation curve (default: `'linear'`)
253
391
  */
254
- processPrepared(numFrames) {
255
- this.native.processPrepared(numFrames);
392
+ scheduleWidthAutomation(stripIndex, samplePos, width, curve = "linear") {
393
+ this.mixer.scheduleWidthAutomation(stripIndex, samplePos, width, automationCurveCode(curve));
256
394
  }
257
- processWithMonitor(channels) {
258
- return this.native.processWithMonitor(channels);
395
+ /**
396
+ * Schedule sample-accurate send-level automation on a strip's send.
397
+ *
398
+ * @param stripIndex - Strip index in `[0, stripCount())`
399
+ * @param sendIndex - Send index in the strip's add order
400
+ * @param samplePos - Absolute samples from the start of processing
401
+ * @param db - Target send level in dB
402
+ * @param curve - Interpolation curve (default: `'linear'`)
403
+ */
404
+ scheduleSendAutomation(stripIndex, sendIndex, samplePos, db, curve = "linear") {
405
+ this.mixer.scheduleSendAutomation(
406
+ stripIndex,
407
+ sendIndex,
408
+ samplePos,
409
+ db,
410
+ automationCurveCode(curve)
411
+ );
259
412
  }
260
- renderOffline(channels, blockSize = 128) {
261
- return this.native.renderOffline(channels, blockSize);
413
+ /**
414
+ * Read up to `maxPoints` of a strip's most recent goniometer samples
415
+ * (oldest to newest).
416
+ */
417
+ readGoniometerLatest(stripIndex, maxPoints) {
418
+ return this.mixer.readGoniometerLatest(stripIndex, maxPoints);
262
419
  }
263
- bounceOffline(options) {
264
- return this.native.bounceOffline(options);
420
+ /** Serialize the current scene (strips, buses, sends, connections) to JSON. */
421
+ toSceneJson() {
422
+ return this.mixer.toSceneJson();
265
423
  }
266
- freezeOffline(options) {
267
- return this.native.freezeOffline(options);
424
+ /**
425
+ * Maximum processor tail length (samples) in the compiled mixer graph. Lazily
426
+ * compiles the routing graph if the topology is dirty.
427
+ */
428
+ tailSamples() {
429
+ return this.mixer.tailSamples();
268
430
  }
269
- drainTelemetry(maxRecords = 1024) {
270
- return this.native.drainTelemetry(maxRecords);
431
+ /**
432
+ * Drain delayed / tail audio by processing a zero-input block of `numSamples`
433
+ * frames after the host stops feeding strip inputs. Returns the mixed stereo
434
+ * master (`left`, `right`, `sampleRate`).
435
+ */
436
+ drainTailStereo(numSamples) {
437
+ return this.mixer.drainTailStereo(numSamples);
271
438
  }
272
- drainMeterTelemetry(maxRecords = 1024) {
273
- return this.native.drainMeterTelemetry(maxRecords);
439
+ /** Release the underlying WASM object. Safe to call only once. */
440
+ delete() {
441
+ this.mixer.delete();
274
442
  }
443
+ /** Alias for {@link delete}, provided for cross-binding (Node) compatibility. */
275
444
  destroy() {
276
- this.native.delete();
445
+ this.delete();
277
446
  }
278
447
  };
448
+
449
+ // src/realtime_voice_changer.ts
279
450
  var RealtimeVoiceChanger = class {
280
451
  constructor(config = "neutral-monitor") {
281
- if (!module) {
282
- throw new Error("Module not initialized. Call init() first.");
283
- }
284
- this.changer = module.createRealtimeVoiceChanger(config);
452
+ const module2 = getSonareModule();
453
+ this.changer = module2.createRealtimeVoiceChanger(config);
285
454
  }
286
455
  prepare(sampleRate, maxBlockSize = 128, channels = 1) {
287
456
  this.changer.prepare(sampleRate, maxBlockSize, channels);
@@ -372,23 +541,53 @@ var RealtimeVoiceChanger = class {
372
541
  * views are reused across calls and become invalid after {@link delete}.
373
542
  */
374
543
  createRealtimeMonoBuffer(numSamples) {
375
- const input = this.getMonoInputBuffer(numSamples);
376
- const output = this.getMonoOutputBuffer(numSamples);
544
+ let input = this.getMonoInputBuffer(numSamples);
545
+ let output = this.getMonoOutputBuffer(numSamples);
546
+ const reacquireIfDetached = () => {
547
+ if (input.byteLength === 0 || output.byteLength === 0) {
548
+ input = this.getMonoInputBuffer(numSamples);
549
+ output = this.getMonoOutputBuffer(numSamples);
550
+ }
551
+ };
377
552
  return {
378
- input,
379
- output,
380
- process: () => this.processPreparedMono(numSamples)
553
+ get input() {
554
+ reacquireIfDetached();
555
+ return input;
556
+ },
557
+ get output() {
558
+ reacquireIfDetached();
559
+ return output;
560
+ },
561
+ process: () => {
562
+ reacquireIfDetached();
563
+ this.processPreparedMono(numSamples);
564
+ }
381
565
  };
382
566
  }
383
567
  /** Same as {@link createRealtimeMonoBuffer} but for interleaved I/O. */
384
568
  createRealtimeInterleavedBuffer(numFrames, numChannels) {
385
- const input = this.getInterleavedInputBuffer(numFrames, numChannels);
386
- const output = this.getInterleavedOutputBuffer(numFrames, numChannels);
569
+ let input = this.getInterleavedInputBuffer(numFrames, numChannels);
570
+ let output = this.getInterleavedOutputBuffer(numFrames, numChannels);
571
+ const reacquireIfDetached = () => {
572
+ if (input.byteLength === 0 || output.byteLength === 0) {
573
+ input = this.getInterleavedInputBuffer(numFrames, numChannels);
574
+ output = this.getInterleavedOutputBuffer(numFrames, numChannels);
575
+ }
576
+ };
387
577
  return {
388
- input,
389
- output,
578
+ get input() {
579
+ reacquireIfDetached();
580
+ return input;
581
+ },
582
+ get output() {
583
+ reacquireIfDetached();
584
+ return output;
585
+ },
390
586
  channels: numChannels,
391
- process: () => this.processPreparedInterleaved(numFrames, numChannels)
587
+ process: () => {
588
+ reacquireIfDetached();
589
+ this.processPreparedInterleaved(numFrames, numChannels);
590
+ }
392
591
  };
393
592
  }
394
593
  /**
@@ -398,326 +597,434 @@ var RealtimeVoiceChanger = class {
398
597
  * become invalid after {@link delete}.
399
598
  */
400
599
  createRealtimePlanarBuffer(numFrames, numChannels) {
401
- const channels = [];
402
- for (let ch = 0; ch < numChannels; ch++) {
403
- channels.push(this.getPlanarChannelBuffer(ch, numFrames));
404
- }
600
+ let channels = [];
601
+ const acquire = () => {
602
+ channels = [];
603
+ for (let ch = 0; ch < numChannels; ch++) {
604
+ channels.push(this.getPlanarChannelBuffer(ch, numFrames));
605
+ }
606
+ };
607
+ acquire();
608
+ const reacquireIfDetached = () => {
609
+ if ((channels[0]?.byteLength ?? 0) === 0) {
610
+ acquire();
611
+ }
612
+ };
405
613
  return {
406
- channels,
407
- process: () => this.processPreparedPlanar(numFrames)
614
+ get channels() {
615
+ reacquireIfDetached();
616
+ return channels;
617
+ },
618
+ process: () => {
619
+ reacquireIfDetached();
620
+ this.processPreparedPlanar(numFrames);
621
+ }
408
622
  };
409
623
  }
410
624
  delete() {
411
625
  this.changer.delete();
412
626
  }
413
627
  };
414
- var Mixer = class _Mixer {
415
- constructor(mixer) {
416
- this.mixer = mixer;
628
+
629
+ // src/realtime_engine.ts
630
+ var EXPECTED_ENGINE_ABI_VERSION = 3;
631
+ function engineCapabilities() {
632
+ const abiVersion = getSonareModule().engineAbiVersion();
633
+ const sharedArrayBuffer = typeof globalThis.SharedArrayBuffer === "function";
634
+ const atomics = typeof globalThis.Atomics === "object";
635
+ const audioWorklet = typeof AudioWorkletNode !== "undefined" || typeof globalThis.AudioWorkletProcessor !== "undefined";
636
+ return {
637
+ engineAbiVersion: abiVersion,
638
+ expectedEngineAbiVersion: EXPECTED_ENGINE_ABI_VERSION,
639
+ abiCompatible: abiVersion === EXPECTED_ENGINE_ABI_VERSION,
640
+ sharedArrayBuffer,
641
+ atomics,
642
+ audioWorklet,
643
+ mode: sharedArrayBuffer && atomics ? "sab" : "postMessage"
644
+ };
645
+ }
646
+ var RealtimeEngine = class {
647
+ nativeExt() {
648
+ return this.native;
417
649
  }
418
- /**
419
- * Build a mixer from a scene JSON string.
420
- *
421
- * @param json - Scene JSON (strips, buses, sends, connections, inserts)
422
- * @param sampleRate - Sample rate in Hz (default: 48000)
423
- * @param blockSize - Maximum block size per {@link processStereo} call (default: 512)
424
- */
425
- static fromSceneJson(json, sampleRate = 48e3, blockSize = 512) {
426
- if (!module) {
427
- throw new Error("Module not initialized. Call init() first.");
650
+ constructor(sampleRate = 48e3, maxBlockSize = 128, commandCapacity = 1024, telemetryCapacity = 1024) {
651
+ const module2 = getSonareModule();
652
+ const capabilities = engineCapabilities();
653
+ if (!capabilities.abiCompatible) {
654
+ throw new Error(
655
+ `Engine ABI mismatch: wasm=${capabilities.engineAbiVersion}, expected=${capabilities.expectedEngineAbiVersion}`
656
+ );
428
657
  }
429
- return new _Mixer(module.createMixerFromSceneJson(json, sampleRate, blockSize));
658
+ this.native = new module2.RealtimeEngine(
659
+ sampleRate,
660
+ maxBlockSize,
661
+ commandCapacity,
662
+ telemetryCapacity
663
+ );
430
664
  }
431
- /** Rebuild and compile the routing graph from the current scene topology. */
432
- compile() {
433
- this.mixer.compile();
665
+ prepare(sampleRate, maxBlockSize, commandCapacity = 1024, telemetryCapacity = 1024) {
666
+ this.native.prepare(sampleRate, maxBlockSize, commandCapacity, telemetryCapacity);
667
+ }
668
+ /** Queue a sample-accurate parameter change (engine kSetParam). */
669
+ setParameter(paramId, value, renderFrame = -1) {
670
+ this.native.setParameter(paramId, value, renderFrame);
671
+ }
672
+ /** Queue a smoothed parameter change (engine kSetParamSmoothed). */
673
+ setParameterSmoothed(paramId, value, renderFrame = -1) {
674
+ this.native.setParameterSmoothed(paramId, value, renderFrame);
675
+ }
676
+ setBuiltinInstrument(config = {}, destinationId = config.destinationId ?? 0) {
677
+ this.nativeExt().setBuiltinInstrument(destinationId, config);
434
678
  }
435
679
  /**
436
- * Mix one block of per-strip stereo audio into the stereo master.
437
- *
438
- * @param leftChannels - `leftChannels[i]` is the left channel of strip `i`
439
- * @param rightChannels - `rightChannels[i]` is the right channel of strip `i`
440
- * @returns Mixed stereo master (`left`, `right`, `sampleRate`)
680
+ * Bind the patch-driven NativeSynth to a realtime MIDI destination. `patch`
681
+ * is a {@link SynthPatch} or a preset-name string (`'saw-lead'` /
682
+ * `'va:saw-lead'`; see {@link synthPresetNames}), resolving exactly like
683
+ * {@link Project.bounceWithSynthInstrument}. Live note/CC commands and
684
+ * scheduled MIDI clips routed to that destination render through the synth.
685
+ * Unknown preset names throw. An object patch's `destinationId` is a JS
686
+ * binding convenience, not part of the NativeSynth patch itself.
441
687
  */
442
- processStereo(leftChannels, rightChannels) {
443
- if (leftChannels.length !== rightChannels.length) {
444
- throw new Error("leftChannels and rightChannels must have the same length.");
445
- }
446
- return this.mixer.processStereo(leftChannels, rightChannels);
688
+ setSynthInstrument(patch = {}, destinationId = (typeof patch === "object" ? patch.destinationId : void 0) ?? 0) {
689
+ this.nativeExt().setSynthInstrument(destinationId, patch);
447
690
  }
448
691
  /**
449
- * Mix one block into caller-owned output arrays.
450
- *
451
- * This avoids allocating the result object and result `Float32Array`s. It is
452
- * intended for realtime bridges such as AudioWorklet; the input channel count
453
- * must match the scene strip count and all arrays must have the same length.
692
+ * Load (parse) SoundFont 2 bytes into the engine so SF2 instruments can be
693
+ * bound with {@link setSf2Instrument}. The host fetches the `.sf2` and
694
+ * passes the raw bytes; they are copied into linear memory for the call and
695
+ * not referenced afterwards. Replaces any previously loaded SoundFont.
454
696
  */
455
- processStereoInto(leftChannels, rightChannels, outLeft, outRight) {
456
- if (leftChannels.length !== rightChannels.length) {
457
- throw new Error("leftChannels and rightChannels must have the same length.");
458
- }
459
- if (outLeft.length !== outRight.length) {
460
- throw new Error("outLeft and outRight must have the same length.");
461
- }
462
- this.mixer.processStereoInto(leftChannels, rightChannels, outLeft, outRight);
697
+ loadSoundFont(data) {
698
+ this.nativeExt().loadSoundFont(data);
463
699
  }
464
700
  /**
465
- * Create reusable WASM-heap input/output views for realtime-style processing.
466
- *
467
- * Fill `leftInputs[i]` / `rightInputs[i]`, call `process()`, then read
468
- * `outLeft` / `outRight`. The views are owned by this mixer and become invalid
469
- * after {@link delete}.
701
+ * Bind a GS-compatible SoundFont player to a realtime MIDI destination, fed
702
+ * by the engine's loaded SoundFont ({@link loadSoundFont}). Live note/CC
703
+ * commands and scheduled MIDI clips routed to that destination render
704
+ * through the player (16 MIDI channels, channel 10 drums, GS NRPN part
705
+ * edits, GS/GM SysEx resets). Without a loaded SoundFont — or for programs
706
+ * the SoundFont does not cover — notes play through the built-in
707
+ * synthesizer GM fallback bank (the data-free floor).
470
708
  */
471
- createRealtimeBuffer() {
472
- const stripCount = this.stripCount();
473
- const leftInputs = [];
474
- const rightInputs = [];
475
- for (let index = 0; index < stripCount; index++) {
476
- leftInputs.push(this.mixer.inputLeftView(index));
477
- rightInputs.push(this.mixer.inputRightView(index));
478
- }
479
- const outLeft = this.mixer.outputLeftView();
480
- const outRight = this.mixer.outputRightView();
481
- return {
482
- leftInputs,
483
- rightInputs,
484
- outLeft,
485
- outRight,
486
- process: (numSamples = outLeft.length) => this.mixer.processPreparedStereo(numSamples)
487
- };
709
+ setSf2Instrument(config = {}, destinationId = config.destinationId ?? 0) {
710
+ this.nativeExt().setSf2Instrument(destinationId, config);
488
711
  }
489
- /** Number of strips in the mixer (e.g. strips loaded from the scene). */
490
- stripCount() {
491
- return this.mixer.stripCount();
712
+ clearMidiInstrument(destinationId = 0) {
713
+ this.nativeExt().clearMidiInstrument(destinationId);
714
+ }
715
+ midiInstrumentCount() {
716
+ return this.nativeExt().midiInstrumentCount();
492
717
  }
493
718
  /**
494
- * Schedule sample-accurate insert-parameter automation on a strip's insert.
495
- *
496
- * @param stripIndex - Strip index in `[0, stripCount())`
497
- * @param insertIndex - Index into the strip's combined insert sequence
498
- * (`[pre-inserts... post-inserts...]`)
499
- * @param paramId - Processor-specific parameter id
500
- * @param samplePos - Absolute samples from the start of processing (the mixer
501
- * advances an internal position from 0 on the first {@link processStereo}
502
- * call; recompiling resets it to 0)
503
- * @param value - Target parameter value
504
- * @param curve - Interpolation curve (default: `'linear'`)
505
- * @throws If the strip index is out of range or the schedule call fails
506
- * (unknown curve, out-of-range insert index, or full event lane)
719
+ * Bind a live MIDI CC to an engine automation parameter. The MIDI event still
720
+ * reaches the destination instrument; when bound, its 7-bit value is also
721
+ * mapped into [minValue, maxValue] for `paramId`.
507
722
  */
508
- scheduleInsertAutomation(stripIndex, insertIndex, paramId, samplePos, value, curve = "linear") {
509
- this.mixer.scheduleInsertAutomation(
510
- stripIndex,
511
- insertIndex,
723
+ bindMidiCc(channel, controller, paramId, options = {}) {
724
+ this.nativeExt().bindMidiCc(
725
+ channel,
726
+ controller,
512
727
  paramId,
513
- samplePos,
514
- value,
515
- automationCurveCode(curve)
728
+ options.minValue ?? 0,
729
+ options.maxValue ?? 1
516
730
  );
517
731
  }
518
- /**
519
- * Resolve a strip's index in `[0, stripCount())` from its scene id, or `null`
520
- * when no strip with that id exists (matches the Node binding's `number | null`).
521
- */
522
- stripById(id) {
523
- const index = this.mixer.stripById(id);
524
- return index < 0 ? null : index;
732
+ clearMidiCcBindings() {
733
+ this.nativeExt().clearMidiCcBindings();
525
734
  }
526
- /**
527
- * Add a bus to the mixer topology. `role` is one of `'master'`, `'aux'`, or
528
- * `'submix'` (defaults to `'aux'`). Marks the routing graph dirty; call
529
- * {@link compile} (or {@link processStereo}) to rebuild.
530
- */
531
- addBus(id, role = "aux") {
532
- this.mixer.addBus(id, role);
735
+ midiCcBindingCount() {
736
+ return this.nativeExt().midiCcBindingCount();
533
737
  }
534
- /** Remove a bus by id. Marks the routing graph dirty. */
535
- removeBus(id) {
536
- this.mixer.removeBus(id);
738
+ /** Install/replace a live non-destructive MIDI-FX insert for one destination. */
739
+ setMidiFx(destinationId, configJson) {
740
+ this.nativeExt().setMidiFx(destinationId, configJson);
537
741
  }
538
- /** Number of buses in the mixer topology. */
539
- busCount() {
540
- return this.mixer.busCount();
742
+ clearMidiFx(destinationId = 0) {
743
+ this.nativeExt().clearMidiFx(destinationId);
541
744
  }
542
- /**
543
- * Add a VCA group with the given gain offset (dB). `members` is a list of
544
- * strip ids governed by the group (may be empty).
545
- */
546
- addVcaGroup(id, gainDb = 0, members = []) {
547
- this.mixer.addVcaGroup(id, gainDb, members);
745
+ /** Enable the engine-owned live MIDI input source for a destination. */
746
+ setMidiInputSource(destinationId = 0) {
747
+ this.nativeExt().setMidiInputSource(destinationId);
548
748
  }
549
- /** Remove a VCA group by id. */
550
- removeVcaGroup(id) {
551
- this.mixer.removeVcaGroup(id);
749
+ clearMidiInputSource() {
750
+ this.nativeExt().clearMidiInputSource();
552
751
  }
553
- /** Number of VCA groups in the mixer topology. */
554
- vcaGroupCount() {
555
- return this.mixer.vcaGroupCount();
752
+ midiInputPendingCount() {
753
+ return this.nativeExt().midiInputPendingCount();
556
754
  }
557
- /** Set the strip's input trim in dB. */
558
- setInputTrimDb(stripIndex, db) {
559
- this.mixer.setInputTrimDb(stripIndex, db);
755
+ pushMidiInputNoteOn(group, channel, note, velocity, portTimeSamples = 0) {
756
+ this.nativeExt().pushMidiInputNoteOn(group, channel, note, velocity, portTimeSamples);
560
757
  }
561
- /** Set the strip's fader level in dB. */
562
- setFaderDb(stripIndex, db) {
563
- this.mixer.setFaderDb(stripIndex, db);
758
+ pushMidiInputNoteOff(group, channel, note, velocity = 0, portTimeSamples = 0) {
759
+ this.nativeExt().pushMidiInputNoteOff(group, channel, note, velocity, portTimeSamples);
564
760
  }
565
- /** Set the strip's pan position. */
566
- setPan(stripIndex, pan, panMode = 0) {
567
- this.mixer.setPan(stripIndex, pan, panModeCode(panMode));
761
+ pushMidiInputCc(group, channel, controller, value, portTimeSamples = 0) {
762
+ this.nativeExt().pushMidiInputCc(group, channel, controller, value, portTimeSamples);
568
763
  }
569
- /** Set the strip's stereo width. */
570
- setWidth(stripIndex, width) {
571
- this.mixer.setWidth(stripIndex, width);
764
+ pushMidiNoteOn(destinationId, group, channel, note, velocity, renderFrame = -1) {
765
+ this.nativeExt().pushMidiNoteOn(destinationId, group, channel, note, velocity, renderFrame);
572
766
  }
573
- /** Set the strip's mute state. */
574
- setMuted(stripIndex, muted) {
575
- this.mixer.setMuted(stripIndex, muted);
767
+ pushMidiNoteOff(destinationId, group, channel, note, velocity = 0, renderFrame = -1) {
768
+ this.nativeExt().pushMidiNoteOff(destinationId, group, channel, note, velocity, renderFrame);
576
769
  }
577
770
  /**
578
- * Set a strip's solo state. Takes effect on the next process without a
579
- * graph recompile.
771
+ * Queue an immediate (live) MIDI control change to a MIDI destination
772
+ * (engine kMidiCcImmediate). `group`/`channel` are 0..15; `controller`/`value`
773
+ * are 7-bit (0..127). `renderFrame` is the frame to fire at, or -1 for
774
+ * immediate. Mirrors the Node/Python/C-ABI `pushMidiCc`.
580
775
  */
581
- setSoloed(stripIndex, soloed) {
582
- this.mixer.setSoloed(stripIndex, soloed);
776
+ pushMidiCc(destinationId, group, channel, controller, value, renderFrame = -1) {
777
+ this.nativeExt().pushMidiCc(destinationId, group, channel, controller, value, renderFrame);
583
778
  }
584
779
  /**
585
- * Mark a strip solo-safe so it is never implied-muted by another strip's
586
- * solo. Takes effect on the next process without a graph recompile.
780
+ * Queue a MIDI panic (all-notes-off) releasing every sounding note at
781
+ * `renderFrame` (-1 = immediate). Mirrors the C-ABI `pushMidiPanic`.
587
782
  */
588
- setSoloSafe(stripIndex, soloSafe) {
589
- this.mixer.setSoloSafe(stripIndex, soloSafe);
590
- }
591
- /** Invert the polarity of the left and/or right channel of a strip. */
592
- setPolarityInvert(stripIndex, invertLeft, invertRight) {
593
- this.mixer.setPolarityInvert(stripIndex, invertLeft, invertRight);
594
- }
595
- /** Set the strip's pan law. */
596
- setPanLaw(stripIndex, panLaw) {
597
- this.mixer.setPanLaw(stripIndex, panLawCode(panLaw));
783
+ pushMidiPanic(renderFrame = -1) {
784
+ this.nativeExt().pushMidiPanic(renderFrame);
598
785
  }
599
786
  /**
600
- * Set a per-strip channel delay in samples. This changes the strip's reported
601
- * latency; recompile to re-run latency compensation.
787
+ * Remove all registered parameters (and their automation lanes). Control-thread
788
+ * only; not realtime-safe. Mirrors the C-ABI `clearParameters`.
602
789
  */
603
- setChannelDelaySamples(stripIndex, delaySamples) {
604
- this.mixer.setChannelDelaySamples(stripIndex, delaySamples);
790
+ clearParameters() {
791
+ this.nativeExt().clearParameters();
605
792
  }
606
- /** Set the strip's live VCA gain offset in dB (not persisted to the scene). */
607
- setVcaOffsetDb(stripIndex, offsetDb) {
608
- this.mixer.setVcaOffsetDb(stripIndex, offsetDb);
793
+ /** Read back the current transport state snapshot. */
794
+ getTransportState() {
795
+ return this.native.getTransportState();
609
796
  }
610
- /** Set independent left/right pan positions (dual-pan mode). */
611
- setDualPan(stripIndex, leftPan, rightPan) {
612
- this.mixer.setDualPan(stripIndex, leftPan, rightPan);
797
+ play(renderFrame = -1) {
798
+ this.native.play(renderFrame);
613
799
  }
614
- /**
615
- * Add a send to a strip after construction.
616
- *
617
- * @param stripIndex - Strip index in `[0, stripCount())`
618
- * @param id - Send id
619
- * @param destinationBusId - Destination bus id
620
- * @param sendDb - Initial send level in dB
621
- * @param timing - `'preFader'` or `'postFader'` (default: `'postFader'`)
622
- * @returns The new send's index
623
- */
624
- addSend(stripIndex, id, destinationBusId, sendDb, timing = "postFader") {
625
- return this.mixer.addSend(stripIndex, id, destinationBusId, sendDb, sendTimingCode(timing));
800
+ stop(renderFrame = -1) {
801
+ this.native.stop(renderFrame);
626
802
  }
627
- /** Set the send level (in dB) for an existing send by index. */
628
- setSendDb(stripIndex, sendIndex, sendDb) {
629
- this.mixer.setSendDb(stripIndex, sendIndex, sendDb);
803
+ seekSample(timelineSample, renderFrame = -1) {
804
+ this.native.seekSample(timelineSample, renderFrame);
630
805
  }
631
- /**
632
- * Read a strip's meter snapshot at the given tap point.
633
- *
634
- * @param stripIndex - Strip index in `[0, stripCount())`
635
- * @param tap - `'preFader'` or `'postFader'` (default: `'postFader'`)
636
- */
637
- meterTap(stripIndex, tap = "postFader") {
638
- return this.mixer.meterTap(stripIndex, meterTapCode(tap));
806
+ seekPpq(ppq, renderFrame = -1) {
807
+ this.native.seekPpq(ppq, renderFrame);
639
808
  }
640
- /**
641
- * Read a strip's meter snapshot. Alias of {@link meterTap}, provided for
642
- * cross-binding (Node/Python) parity.
643
- *
644
- * @param stripIndex - Strip index in `[0, stripCount())`
645
- * @param tap - `'preFader'` or `'postFader'` (default: `'postFader'`)
646
- */
647
- stripMeter(stripIndex, tap = "postFader") {
648
- return this.mixer.stripMeter(stripIndex, meterTapCode(tap));
809
+ setTempo(bpm) {
810
+ this.native.setTempo(bpm);
649
811
  }
650
- /**
651
- * Schedule sample-accurate fader automation on a strip.
652
- *
653
- * @param stripIndex - Strip index in `[0, stripCount())`
654
- * @param samplePos - Absolute samples from the start of processing
655
- * @param faderDb - Target fader level in dB
656
- * @param curve - Interpolation curve (default: `'linear'`)
657
- */
658
- scheduleFaderAutomation(stripIndex, samplePos, faderDb, curve = "linear") {
659
- this.mixer.scheduleFaderAutomation(stripIndex, samplePos, faderDb, automationCurveCode(curve));
812
+ setTimeSignature(numerator, denominator) {
813
+ this.native.setTimeSignature(numerator, denominator);
660
814
  }
661
- /**
662
- * Schedule sample-accurate pan automation on a strip.
663
- *
664
- * @param stripIndex - Strip index in `[0, stripCount())`
665
- * @param samplePos - Absolute samples from the start of processing
666
- * @param pan - Target pan position
667
- * @param curve - Interpolation curve (default: `'linear'`)
668
- */
669
- schedulePanAutomation(stripIndex, samplePos, pan, curve = "linear") {
670
- this.mixer.schedulePanAutomation(stripIndex, samplePos, pan, automationCurveCode(curve));
815
+ setLoop(startPpq, endPpq, enabled = true) {
816
+ this.native.setLoop(startPpq, endPpq, enabled);
817
+ }
818
+ addParameter(info) {
819
+ this.native.addParameter(info);
820
+ }
821
+ parameterCount() {
822
+ return this.native.parameterCount();
823
+ }
824
+ parameterInfoByIndex(index) {
825
+ return this.native.parameterInfoByIndex(index);
826
+ }
827
+ parameterInfo(id) {
828
+ return this.native.parameterInfo(id);
829
+ }
830
+ setAutomationLane(paramId, points) {
831
+ this.native.setAutomationLane(paramId, points);
832
+ }
833
+ automationLaneCount() {
834
+ return this.native.automationLaneCount();
835
+ }
836
+ setMarkers(markers) {
837
+ this.native.setMarkers(markers);
838
+ }
839
+ markerCount() {
840
+ return this.native.markerCount();
841
+ }
842
+ markerByIndex(index) {
843
+ return this.native.markerByIndex(index);
844
+ }
845
+ marker(id) {
846
+ return this.native.marker(id);
847
+ }
848
+ seekMarker(markerId, renderFrame = -1) {
849
+ this.native.seekMarker(markerId, renderFrame);
850
+ }
851
+ setLoopFromMarkers(startMarkerId, endMarkerId) {
852
+ this.native.setLoopFromMarkers(startMarkerId, endMarkerId);
853
+ }
854
+ setMetronome(config) {
855
+ this.native.setMetronome(config);
856
+ }
857
+ metronome() {
858
+ return this.native.metronome();
859
+ }
860
+ countInEndSample(startSample, bars) {
861
+ return Number(this.native.countInEndSample(startSample, bars));
862
+ }
863
+ setGraph(spec) {
864
+ this.native.setGraph(spec);
865
+ }
866
+ graphNodeCount() {
867
+ return this.native.graphNodeCount();
868
+ }
869
+ graphConnectionCount() {
870
+ return this.native.graphConnectionCount();
871
+ }
872
+ setClips(clips) {
873
+ this.native.setClips(
874
+ clips.map((clip) => ({
875
+ ...clip,
876
+ pageProvider: typeof clip.pageProvider === "object" && clip.pageProvider !== null ? clip.pageProvider.id : clip.pageProvider
877
+ }))
878
+ );
879
+ }
880
+ clipCount() {
881
+ return this.native.clipCount();
882
+ }
883
+ createClipPageProvider(numChannels, numSamples, pageFrames) {
884
+ const id = this.nativeExt().createClipPageProvider(numChannels, numSamples, pageFrames);
885
+ return new ClipPageProvider(this, id);
886
+ }
887
+ supplyClipPage(providerId, pageIndex, channels) {
888
+ this.nativeExt().supplyClipPage(providerId, pageIndex, channels);
889
+ }
890
+ clearClipPage(providerId, pageIndex) {
891
+ this.nativeExt().clearClipPage(providerId, pageIndex);
892
+ }
893
+ destroyClipPageProvider(providerId) {
894
+ this.nativeExt().destroyClipPageProvider(providerId);
895
+ }
896
+ popClipPageRequest() {
897
+ return this.nativeExt().popClipPageRequest();
898
+ }
899
+ setCaptureBuffer(numChannels, capacityFrames) {
900
+ this.native.setCaptureBuffer(numChannels, capacityFrames);
901
+ }
902
+ armCapture(armed = true) {
903
+ this.native.armCapture(armed);
904
+ }
905
+ setCapturePunch(startSample, endSample, enabled = true) {
906
+ this.native.setCapturePunch(startSample, endSample, enabled);
907
+ }
908
+ setCaptureSource(source) {
909
+ this.native.setCaptureSource(source);
910
+ }
911
+ setRecordOffsetSamples(offsetSamples) {
912
+ this.native.setRecordOffsetSamples(offsetSamples);
913
+ }
914
+ setInputMonitor(enabled, gain = 1) {
915
+ this.native.setInputMonitor(enabled, gain);
916
+ }
917
+ resetCapture() {
918
+ this.native.resetCapture();
919
+ }
920
+ captureStatus() {
921
+ return this.native.captureStatus();
922
+ }
923
+ capturedAudio() {
924
+ return this.native.capturedAudio();
925
+ }
926
+ process(channels) {
927
+ return this.native.process(channels);
671
928
  }
672
929
  /**
673
- * Schedule sample-accurate width automation on a strip.
674
- *
675
- * @param stripIndex - Strip index in `[0, stripCount())`
676
- * @param samplePos - Absolute samples from the start of processing
677
- * @param width - Target stereo width
678
- * @param curve - Interpolation curve (default: `'linear'`)
930
+ * Allocates persistent per-channel WASM-heap scratch for the zero-copy
931
+ * `getChannelBuffer` / `processPrepared` realtime path. Call once (off the
932
+ * audio thread) before driving `processPrepared` from an AudioWorklet so the
933
+ * render callback never allocates on the C++/JS heap.
679
934
  */
680
- scheduleWidthAutomation(stripIndex, samplePos, width, curve = "linear") {
681
- this.mixer.scheduleWidthAutomation(stripIndex, samplePos, width, automationCurveCode(curve));
935
+ prepareChannels(numChannels, maxFrames) {
936
+ this.native.prepareChannels(numChannels, maxFrames);
682
937
  }
683
938
  /**
684
- * Schedule sample-accurate send-level automation on a strip's send.
685
- *
686
- * @param stripIndex - Strip index in `[0, stripCount())`
687
- * @param sendIndex - Send index in the strip's add order
688
- * @param samplePos - Absolute samples from the start of processing
689
- * @param db - Target send level in dB
690
- * @param curve - Interpolation curve (default: `'linear'`)
939
+ * Returns a Float32Array view onto the persistent WASM-heap scratch for one
940
+ * channel (valid for up to `numFrames`). Fill it, call `processPrepared`, then
941
+ * read the same view back. Re-acquire after WASM memory growth.
691
942
  */
692
- scheduleSendAutomation(stripIndex, sendIndex, samplePos, db, curve = "linear") {
693
- this.mixer.scheduleSendAutomation(
694
- stripIndex,
695
- sendIndex,
696
- samplePos,
697
- db,
698
- automationCurveCode(curve)
699
- );
943
+ getChannelBuffer(channel, numFrames) {
944
+ return this.native.getChannelBuffer(channel, numFrames);
700
945
  }
701
946
  /**
702
- * Read up to `maxPoints` of a strip's most recent goniometer samples
703
- * (oldest to newest).
947
+ * Runs the engine in place over the prepared per-channel scratch buffers.
948
+ * Allocation-free: safe to call on the AudioWorklet render thread after
949
+ * `prepareChannels`.
704
950
  */
705
- readGoniometerLatest(stripIndex, maxPoints) {
706
- return this.mixer.readGoniometerLatest(stripIndex, maxPoints);
951
+ processPrepared(numFrames) {
952
+ this.native.processPrepared(numFrames);
707
953
  }
708
- /** Serialize the current scene (strips, buses, sends, connections) to JSON. */
709
- toSceneJson() {
710
- return this.mixer.toSceneJson();
954
+ processWithMonitor(channels) {
955
+ return this.native.processWithMonitor(channels);
711
956
  }
712
- /** Release the underlying WASM object. Safe to call only once. */
713
- delete() {
714
- this.mixer.delete();
957
+ renderOffline(channels, blockSize = 128) {
958
+ return this.native.renderOffline(channels, blockSize);
959
+ }
960
+ bounceOffline(options) {
961
+ return this.native.bounceOffline(options);
962
+ }
963
+ freezeOffline(options) {
964
+ return this.native.freezeOffline(options);
965
+ }
966
+ drainTelemetry(maxRecords = 1024) {
967
+ return this.native.drainTelemetry(maxRecords);
968
+ }
969
+ drainMeterTelemetry(maxRecords = 1024) {
970
+ return this.native.drainMeterTelemetry(maxRecords);
715
971
  }
716
- /** Alias for {@link delete}, provided for cross-binding (Node) compatibility. */
717
972
  destroy() {
718
- this.delete();
973
+ this.native.delete();
719
974
  }
720
975
  };
976
+ var ClipPageProvider = class {
977
+ constructor(engine, id) {
978
+ this.engine = engine;
979
+ this.id = id;
980
+ this.disposed = false;
981
+ }
982
+ supply(pageIndex, channels) {
983
+ if (this.disposed) {
984
+ throw new Error("ClipPageProvider is destroyed");
985
+ }
986
+ this.engine.supplyClipPage(this.id, pageIndex, channels);
987
+ }
988
+ clear(pageIndex) {
989
+ if (this.disposed) {
990
+ return;
991
+ }
992
+ this.engine.clearClipPage(this.id, pageIndex);
993
+ }
994
+ destroy() {
995
+ if (this.disposed) {
996
+ return;
997
+ }
998
+ this.disposed = true;
999
+ this.engine.destroyClipPageProvider(this.id);
1000
+ }
1001
+ };
1002
+
1003
+ // src/index.ts
1004
+ var module = null;
1005
+ var initPromise = null;
1006
+ async function init(options) {
1007
+ if (module) {
1008
+ return;
1009
+ }
1010
+ if (initPromise) {
1011
+ return initPromise;
1012
+ }
1013
+ initPromise = (async () => {
1014
+ try {
1015
+ const createModule = (await import("./sonare.js")).default;
1016
+ module = await createModule(options);
1017
+ setSonareModule(module);
1018
+ } catch (error) {
1019
+ initPromise = null;
1020
+ throw error;
1021
+ }
1022
+ })();
1023
+ return initPromise;
1024
+ }
1025
+ function isInitialized() {
1026
+ return module !== null;
1027
+ }
721
1028
 
722
1029
  // src/worklet.ts
723
1030
  var SONARE_METER_RING_HEADER_INTS = 4;