@libraz/libsonare 1.5.4 → 1.6.0

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 (79) hide show
  1. package/README.md +109 -20
  2. package/dist/analysis.d.ts +5730 -0
  3. package/dist/analysis.js +2464 -0
  4. package/dist/analysis.js.map +1 -0
  5. package/dist/index.d.ts +873 -63
  6. package/dist/index.js +1037 -265
  7. package/dist/index.js.map +1 -1
  8. package/dist/schemas/realtime-voice-changer-preset-pack.schema.json +27 -0
  9. package/dist/schemas/realtime-voice-changer-preset.schema.json +140 -0
  10. package/dist/sonare-analysis.js +2 -0
  11. package/dist/sonare-analysis.wasm +0 -0
  12. package/dist/sonare.js +1 -1
  13. package/dist/sonare.wasm +0 -0
  14. package/dist/worker.d.ts +65 -0
  15. package/dist/worker.js +707 -0
  16. package/dist/worker.js.map +1 -0
  17. package/dist/worklet.d.ts +596 -86
  18. package/dist/worklet.js +1658 -235
  19. package/dist/worklet.js.map +1 -1
  20. package/package.json +23 -11
  21. package/src/_chain_config.ts +5 -23
  22. package/src/analysis.ts +137 -0
  23. package/src/analysis_helpers.ts +29 -0
  24. package/src/audio.ts +5 -3
  25. package/src/clip_page_streamer.ts +62 -8
  26. package/src/codes.ts +51 -64
  27. package/src/effects_mastering.ts +1 -0
  28. package/src/effects_transform.ts +12 -6
  29. package/src/errors.ts +1 -0
  30. package/src/feature_core.ts +159 -3
  31. package/src/feature_music.ts +35 -16
  32. package/src/feature_pitch.ts +75 -6
  33. package/src/feature_spectral.ts +234 -8
  34. package/src/feature_spectrogram.ts +186 -5
  35. package/src/features.ts +16 -1
  36. package/src/index.ts +104 -7
  37. package/src/mastering_chain.ts +20 -8
  38. package/src/mastering_core.ts +7 -0
  39. package/src/mastering_dynamics.ts +7 -1
  40. package/src/metering.ts +51 -5
  41. package/src/mixer.ts +14 -1
  42. package/src/opfs_clip_pages.ts +26 -1
  43. package/src/project.ts +7 -0
  44. package/src/project_class.ts +74 -13
  45. package/src/project_internal.ts +24 -29
  46. package/src/project_types.ts +69 -10
  47. package/src/public_types.ts +68 -0
  48. package/src/public_types_mastering.ts +28 -0
  49. package/src/public_types_mixing.ts +11 -2
  50. package/src/public_types_music.ts +13 -0
  51. package/src/public_types_spectral.ts +43 -3
  52. package/src/quick_analysis.ts +48 -12
  53. package/src/realtime_engine.ts +162 -4
  54. package/src/realtime_voice_changer.ts +51 -77
  55. package/src/sonare-analysis.js.d.ts +8 -0
  56. package/src/sonare.js.d.ts +305 -9
  57. package/src/streaming_processors.ts +22 -1
  58. package/src/validation.ts +6 -0
  59. package/src/web_midi.ts +36 -4
  60. package/src/worker.ts +196 -0
  61. package/src/worker_client.ts +352 -0
  62. package/src/worker_protocol.ts +59 -0
  63. package/src/worklet/engine-automation.ts +2 -1
  64. package/src/worklet/engine-capture-facade.ts +13 -4
  65. package/src/worklet/engine-clips.ts +12 -3
  66. package/src/worklet/engine-node.ts +212 -4
  67. package/src/worklet/engine-offline.ts +7 -2
  68. package/src/worklet/engine-parameter-facade.ts +26 -5
  69. package/src/worklet/engine-processor.ts +383 -42
  70. package/src/worklet/engine-register.ts +8 -0
  71. package/src/worklet/engine-strips.ts +17 -1
  72. package/src/worklet/engine-sync.ts +2 -6
  73. package/src/worklet/engine-tempo-facade.ts +8 -6
  74. package/src/worklet/engine.ts +380 -13
  75. package/src/worklet/guards.ts +22 -1
  76. package/src/worklet/messages.ts +141 -9
  77. package/src/worklet/protocol.ts +303 -17
  78. package/src/worklet/voice-changer-processor.ts +12 -15
  79. package/src/worklet.ts +20 -1
package/dist/worklet.js CHANGED
@@ -1,3 +1,397 @@
1
+ // src/opfs_clip_pages.ts
2
+ var opfsClipPageWorkerSource = `
3
+ const sonareClipPageReadQueues = new Map();
4
+
5
+ function sonareEnqueueClipPageRead(key, task) {
6
+ const previous = sonareClipPageReadQueues.get(key) || Promise.resolve();
7
+ const next = previous.catch(() => undefined).then(task);
8
+ const queued = next.finally(() => {
9
+ if (sonareClipPageReadQueues.get(key) === queued) {
10
+ sonareClipPageReadQueues.delete(key);
11
+ }
12
+ });
13
+ sonareClipPageReadQueues.set(key, queued);
14
+ return next;
15
+ }
16
+
17
+ self.onmessage = async (event) => {
18
+ const message = event.data;
19
+ if (!message || message.type !== 'sonare:read-clip-page') return;
20
+ const { requestId, path, pageIndex, numChannels, numSamples, pageFrames, dataOffsetBytes = 0 } = message;
21
+ await sonareEnqueueClipPageRead(String(path), async () => {
22
+ try {
23
+ if (pageIndex < 0) {
24
+ self.postMessage({ type: 'sonare:clip-page', requestId, pageIndex, ok: false });
25
+ return;
26
+ }
27
+ const startFrame = pageIndex * pageFrames;
28
+ if (startFrame >= numSamples) {
29
+ self.postMessage({ type: 'sonare:clip-page', requestId, pageIndex, ok: false });
30
+ return;
31
+ }
32
+ const root = await self.navigator.storage.getDirectory();
33
+ let dir = root;
34
+ const parts = String(path).split('/').filter(Boolean);
35
+ for (let i = 0; i < parts.length - 1; ++i) {
36
+ dir = await dir.getDirectoryHandle(parts[i]);
37
+ }
38
+ const fileHandle = await dir.getFileHandle(parts[parts.length - 1]);
39
+ const access = await fileHandle.createSyncAccessHandle();
40
+ try {
41
+ const frames = Math.min(pageFrames, numSamples - startFrame);
42
+ const frameBytes = numChannels * 4;
43
+ const bytes = new Uint8Array(frames * frameBytes);
44
+ let bytesReadTotal = 0;
45
+ const readOffset = dataOffsetBytes + startFrame * frameBytes;
46
+ while (bytesReadTotal < bytes.byteLength) {
47
+ const bytesRead = access.read(bytes.subarray(bytesReadTotal), {
48
+ at: readOffset + bytesReadTotal,
49
+ });
50
+ if (bytesRead <= 0) {
51
+ break;
52
+ }
53
+ bytesReadTotal += bytesRead;
54
+ }
55
+ if (bytesReadTotal !== bytes.byteLength || bytesReadTotal % frameBytes !== 0) {
56
+ self.postMessage({ type: 'sonare:clip-page', requestId, pageIndex, ok: false });
57
+ return;
58
+ }
59
+ const framesRead = bytesReadTotal / frameBytes;
60
+ const view = new DataView(bytes.buffer, 0, framesRead * frameBytes);
61
+ const channelBuffers = Array.from({ length: numChannels }, () => new ArrayBuffer(framesRead * 4));
62
+ for (let ch = 0; ch < numChannels; ++ch) {
63
+ const channel = new Float32Array(channelBuffers[ch]);
64
+ for (let frame = 0; frame < framesRead; ++frame) {
65
+ channel[frame] = view.getFloat32((frame * numChannels + ch) * 4, true);
66
+ }
67
+ }
68
+ self.postMessage(
69
+ { type: 'sonare:clip-page', requestId, pageIndex, ok: true, frames: framesRead, channelBuffers },
70
+ channelBuffers,
71
+ );
72
+ } finally {
73
+ access.close();
74
+ }
75
+ } catch (error) {
76
+ self.postMessage({
77
+ type: 'sonare:clip-page',
78
+ requestId,
79
+ pageIndex,
80
+ ok: false,
81
+ error: error instanceof Error ? error.message : String(error),
82
+ });
83
+ }
84
+ });
85
+ };
86
+ `;
87
+ function createOpfsClipPageWorker() {
88
+ const blob = new Blob([opfsClipPageWorkerSource], { type: "text/javascript" });
89
+ const url = URL.createObjectURL(blob);
90
+ try {
91
+ return new Worker(url);
92
+ } finally {
93
+ URL.revokeObjectURL(url);
94
+ }
95
+ }
96
+ function createOpfsClipPageProvider(engine, options) {
97
+ if (options.numChannels <= 0 || options.numSamples <= 0 || options.pageFrames <= 0) {
98
+ throw new Error("numChannels, numSamples, and pageFrames must be positive");
99
+ }
100
+ const provider = engine.createClipPageProvider(
101
+ options.numChannels,
102
+ options.numSamples,
103
+ options.pageFrames
104
+ );
105
+ const worker = options.worker ?? createOpfsClipPageWorker();
106
+ const ownsWorker = options.worker === void 0 || options.terminateWorkerOnClose === true;
107
+ let nextRequestId = 1;
108
+ let closed = false;
109
+ let readQueue = Promise.resolve();
110
+ const pending = /* @__PURE__ */ new Map();
111
+ const onMessage = (event) => {
112
+ const response = event.data;
113
+ if (response?.type !== "sonare:clip-page") {
114
+ return;
115
+ }
116
+ const entry = pending.get(response.requestId);
117
+ if (!entry) {
118
+ return;
119
+ }
120
+ pending.delete(response.requestId);
121
+ if (!response.ok) {
122
+ entry.resolve(false);
123
+ return;
124
+ }
125
+ const channels = response.channels ?? response.channelBuffers?.map(
126
+ (buffer) => new Float32Array(buffer, 0, response.frames ?? buffer.byteLength / 4)
127
+ );
128
+ if (!channels || channels.length === 0) {
129
+ entry.resolve(false);
130
+ return;
131
+ }
132
+ try {
133
+ provider.supply(response.pageIndex, channels);
134
+ options.onPageSupplied?.(response.pageIndex, channels);
135
+ } catch {
136
+ entry.resolve(false);
137
+ return;
138
+ }
139
+ entry.resolve(true);
140
+ };
141
+ worker.addEventListener("message", onMessage);
142
+ const supplyPage = (pageIndex) => {
143
+ if (closed) {
144
+ return Promise.reject(new Error("OpfsClipPageProvider is closed"));
145
+ }
146
+ const requestId = nextRequestId++;
147
+ const promise = new Promise((resolve, reject) => {
148
+ pending.set(requestId, { resolve, reject });
149
+ });
150
+ readQueue = readQueue.catch(() => void 0).then(() => {
151
+ if (closed) {
152
+ const entry = pending.get(requestId);
153
+ pending.delete(requestId);
154
+ entry?.reject(new Error("OpfsClipPageProvider is closed"));
155
+ return;
156
+ }
157
+ worker.postMessage({
158
+ type: "sonare:read-clip-page",
159
+ requestId,
160
+ path: options.path,
161
+ pageIndex,
162
+ numChannels: options.numChannels,
163
+ numSamples: options.numSamples,
164
+ pageFrames: options.pageFrames,
165
+ dataOffsetBytes: options.dataOffsetBytes ?? 0
166
+ });
167
+ return promise.then(
168
+ () => void 0,
169
+ () => void 0
170
+ );
171
+ });
172
+ readQueue.catch(() => {
173
+ });
174
+ return promise;
175
+ };
176
+ return {
177
+ provider,
178
+ supplyPage,
179
+ supplyRequest(request) {
180
+ return supplyPage(Math.floor(request.sample / options.pageFrames));
181
+ },
182
+ clearPage(pageIndex) {
183
+ if (closed) {
184
+ return;
185
+ }
186
+ provider.clear(pageIndex);
187
+ options.onPageCleared?.(pageIndex);
188
+ },
189
+ close() {
190
+ if (closed) {
191
+ return;
192
+ }
193
+ closed = true;
194
+ worker.removeEventListener("message", onMessage);
195
+ for (const entry of pending.values()) {
196
+ entry.reject(new Error("OpfsClipPageProvider is closed"));
197
+ }
198
+ pending.clear();
199
+ provider.destroy();
200
+ options.onClose?.();
201
+ if (ownsWorker) {
202
+ worker.terminate();
203
+ }
204
+ }
205
+ };
206
+ }
207
+
208
+ // src/clip_page_streamer.ts
209
+ var ClipPageStreamer = class {
210
+ constructor(engine, options = {}) {
211
+ this.sources = /* @__PURE__ */ new Map();
212
+ this.closed = false;
213
+ this.engine = engine;
214
+ this.readAheadPages = Math.max(0, Math.floor(options.readAheadPages ?? 2));
215
+ this.retainBehindPages = Math.max(0, Math.floor(options.retainBehindPages ?? 1));
216
+ this.maxRequestsPerPump = Math.max(1, Math.floor(options.maxRequestsPerPump ?? 256));
217
+ }
218
+ /**
219
+ * Register a paged clip. Pages already supplied to the provider before
220
+ * registration (for example a primed first page) should be passed in
221
+ * `initialResidentPages` so they participate in eviction.
222
+ */
223
+ addSource(source, initialResidentPages = []) {
224
+ if (source.pageFrames <= 0 || source.numSamples <= 0) {
225
+ throw new Error("pageFrames and numSamples must be positive");
226
+ }
227
+ const lastPage = Math.ceil(source.numSamples / source.pageFrames) - 1;
228
+ const previous = this.sources.get(source.clipId);
229
+ if (previous) {
230
+ this.resetState(previous);
231
+ }
232
+ this.sources.set(source.clipId, {
233
+ source,
234
+ lastPage,
235
+ generation: 0,
236
+ lastFrontier: null,
237
+ resident: new Map(Array.from(initialResidentPages, (page) => [page, 0]))
238
+ });
239
+ }
240
+ /** Stop tracking a clip. Does not close its binding (the caller owns that). */
241
+ removeSource(clipId) {
242
+ const state = this.sources.get(clipId);
243
+ if (state) {
244
+ this.resetState(state);
245
+ }
246
+ this.sources.delete(clipId);
247
+ }
248
+ /**
249
+ * Explicitly start a new playback generation after a host seek/loop. Resident
250
+ * pages are evicted and any older in-flight fetch is cleared when it settles.
251
+ * The next miss establishes the new bounded window.
252
+ */
253
+ resetSource(clipId) {
254
+ const state = this.sources.get(clipId);
255
+ if (state) {
256
+ this.resetState(state);
257
+ }
258
+ }
259
+ /**
260
+ * Drain pending page-miss requests, fetch the missing pages plus their
261
+ * read-ahead window, and evict out-of-window pages. Resolves once this round's
262
+ * fetches settle. Concurrent fetches are serialized inside each binding.
263
+ */
264
+ async pump() {
265
+ if (this.closed) {
266
+ return;
267
+ }
268
+ const frontiers = /* @__PURE__ */ new Map();
269
+ for (let drained = 0; drained < this.maxRequestsPerPump; ++drained) {
270
+ const request = this.engine.popClipPageRequest();
271
+ if (!request) {
272
+ break;
273
+ }
274
+ const state = this.sources.get(request.clipId);
275
+ if (!state) {
276
+ continue;
277
+ }
278
+ const page = request.pageIndex !== void 0 ? request.pageIndex : Math.floor((request.sample ?? Number.NaN) / state.source.pageFrames);
279
+ if (!Number.isInteger(page) || page < 0 || page > state.lastPage) {
280
+ continue;
281
+ }
282
+ frontiers.set(request.clipId, page);
283
+ }
284
+ const fetches = [];
285
+ for (const [clipId, frontier] of frontiers) {
286
+ const state = this.sources.get(clipId);
287
+ if (!state) {
288
+ continue;
289
+ }
290
+ fetches.push(...this.serviceFrontier(state, frontier));
291
+ }
292
+ await Promise.all(fetches);
293
+ }
294
+ /** Close every registered clip's binding and stop tracking. */
295
+ close() {
296
+ if (this.closed) {
297
+ return;
298
+ }
299
+ this.closed = true;
300
+ for (const state of this.sources.values()) {
301
+ this.resetState(state);
302
+ state.source.binding.close();
303
+ }
304
+ this.sources.clear();
305
+ }
306
+ serviceFrontier(state, frontier) {
307
+ if (state.lastFrontier !== null && frontier < state.lastFrontier) {
308
+ this.resetState(state);
309
+ }
310
+ state.lastFrontier = frontier;
311
+ const generation = state.generation;
312
+ const low = Math.max(0, frontier - this.retainBehindPages);
313
+ const high = Math.min(state.lastPage, frontier + this.readAheadPages);
314
+ for (const page of state.resident.keys()) {
315
+ if (page < low || page > high) {
316
+ this.clearPage(state, page);
317
+ state.resident.delete(page);
318
+ }
319
+ }
320
+ const fetches = [];
321
+ for (let page = low; page <= high; ++page) {
322
+ if (state.resident.get(page) === generation) {
323
+ continue;
324
+ }
325
+ state.resident.set(page, generation);
326
+ const pageIndex = page;
327
+ fetches.push(
328
+ state.source.binding.supplyPage(pageIndex).then(
329
+ (ok) => {
330
+ if (state.generation !== generation) {
331
+ this.clearPage(state, pageIndex);
332
+ } else if (!ok && state.resident.get(pageIndex) === generation) {
333
+ state.resident.delete(pageIndex);
334
+ }
335
+ return ok;
336
+ },
337
+ (error) => {
338
+ if (state.resident.get(pageIndex) === generation) {
339
+ state.resident.delete(pageIndex);
340
+ }
341
+ throw error;
342
+ }
343
+ )
344
+ );
345
+ }
346
+ return fetches;
347
+ }
348
+ resetState(state) {
349
+ state.generation += 1;
350
+ state.lastFrontier = null;
351
+ for (const page of state.resident.keys()) {
352
+ this.clearPage(state, page);
353
+ }
354
+ state.resident.clear();
355
+ }
356
+ clearPage(state, pageIndex) {
357
+ if (state.source.binding.clearPage) {
358
+ state.source.binding.clearPage(pageIndex);
359
+ } else {
360
+ state.source.binding.provider.clear(pageIndex);
361
+ }
362
+ }
363
+ };
364
+ async function attachOpfsClipStream(streamerOrEngine, engineOrOptions, maybeOptions) {
365
+ if (!(streamerOrEngine instanceof ClipPageStreamer)) {
366
+ return streamerOrEngine.attachOpfsClipStream(engineOrOptions);
367
+ }
368
+ const streamer = streamerOrEngine;
369
+ const engine = engineOrOptions;
370
+ const options = maybeOptions;
371
+ if (!options) {
372
+ throw new Error("attachOpfsClipStream requires options.");
373
+ }
374
+ const { clipId, primePages = 1, ...providerOptions } = options;
375
+ const binding = createOpfsClipPageProvider(engine, providerOptions);
376
+ const lastPage = Math.ceil(providerOptions.numSamples / providerOptions.pageFrames) - 1;
377
+ const primed = [];
378
+ for (let page = 0; page < primePages && page <= lastPage; ++page) {
379
+ if (await binding.supplyPage(page)) {
380
+ primed.push(page);
381
+ }
382
+ }
383
+ streamer.addSource(
384
+ {
385
+ clipId,
386
+ binding,
387
+ pageFrames: providerOptions.pageFrames,
388
+ numSamples: providerOptions.numSamples
389
+ },
390
+ primed
391
+ );
392
+ return { binding, provider: binding.provider };
393
+ }
394
+
1
395
  // src/errors.ts
2
396
  var SonareError = class extends Error {
3
397
  constructor(code, codeName, message) {
@@ -151,79 +545,58 @@ function getSonareModule() {
151
545
  }
152
546
 
153
547
  // src/codes.ts
154
- function automationCurveCode(curve) {
155
- switch (curve) {
156
- case "linear":
157
- return 0;
158
- case "exponential":
159
- return 1;
160
- case "hold":
161
- return 2;
162
- case "s-curve":
163
- return 3;
164
- default:
165
- throw new Error(`Invalid automation curve: ${curve}`);
548
+ function resolveEnumOrdinal(value, values, enumName) {
549
+ if (typeof value === "number") {
550
+ if (!Number.isSafeInteger(value) || !Object.values(values).includes(value)) {
551
+ throw new RangeError(`Invalid ${enumName}: ${String(value)}`);
552
+ }
553
+ return value;
554
+ }
555
+ if (typeof value === "string") {
556
+ const ordinal = values[value];
557
+ if (ordinal !== void 0) {
558
+ return ordinal;
559
+ }
166
560
  }
561
+ throw new RangeError(`Invalid ${enumName}: ${String(value)}`);
562
+ }
563
+ var AUTOMATION_CURVE_VALUES = {
564
+ linear: 0,
565
+ exponential: 1,
566
+ hold: 2,
567
+ "s-curve": 3
568
+ };
569
+ var PAN_LAW_VALUES = {
570
+ const3dB: 0,
571
+ "const4.5dB": 1,
572
+ const6dB: 2,
573
+ linear0dB: 3
574
+ };
575
+ var PAN_MODE_VALUES = {
576
+ balance: 0,
577
+ pan: 0,
578
+ stereopan: 1,
579
+ "stereo-pan": 1,
580
+ dualpan: 2,
581
+ "dual-pan": 2
582
+ };
583
+ var METER_TAP_VALUES = { preFader: 0, postFader: 1 };
584
+ var SEND_TIMING_VALUES = { postFader: 0, preFader: 1 };
585
+ function automationCurveCode(curve) {
586
+ return resolveEnumOrdinal(curve, AUTOMATION_CURVE_VALUES, "automation curve");
167
587
  }
168
588
  function panLawCode(panLaw) {
169
- if (typeof panLaw === "number") {
170
- return panLaw;
171
- }
172
- switch (panLaw) {
173
- case "const3dB":
174
- return 0;
175
- case "const4.5dB":
176
- return 1;
177
- case "const6dB":
178
- return 2;
179
- case "linear0dB":
180
- return 3;
181
- default:
182
- throw new Error(`Invalid pan law: ${panLaw}`);
183
- }
589
+ return resolveEnumOrdinal(panLaw, PAN_LAW_VALUES, "pan law");
184
590
  }
185
591
  function panModeCode(panMode) {
186
- if (typeof panMode === "number") {
187
- return panMode;
188
- }
189
- switch (panMode) {
190
- case "balance":
191
- return 0;
192
- case "stereoPan":
193
- case "stereo-pan":
194
- return 1;
195
- case "dualPan":
196
- case "dual-pan":
197
- return 2;
198
- default:
199
- throw new Error(`Invalid pan mode: ${panMode}`);
200
- }
592
+ const normalized = typeof panMode === "string" ? panMode.replace(/_/g, "-").toLowerCase() : panMode;
593
+ return resolveEnumOrdinal(normalized, PAN_MODE_VALUES, "pan mode");
201
594
  }
202
595
  function meterTapCode(tap) {
203
- if (typeof tap === "number") {
204
- return tap;
205
- }
206
- switch (tap) {
207
- case "preFader":
208
- return 0;
209
- case "postFader":
210
- return 1;
211
- default:
212
- throw new Error(`Invalid meter tap: ${tap}`);
213
- }
596
+ return resolveEnumOrdinal(tap, METER_TAP_VALUES, "meter tap");
214
597
  }
215
598
  function sendTimingCode(timing) {
216
- if (typeof timing === "number") {
217
- return timing;
218
- }
219
- switch (timing) {
220
- case "postFader":
221
- return 0;
222
- case "preFader":
223
- return 1;
224
- default:
225
- throw new Error(`Invalid send timing: ${timing}`);
226
- }
599
+ return resolveEnumOrdinal(timing, SEND_TIMING_VALUES, "send timing");
227
600
  }
228
601
 
229
602
  // src/realtime_engine.ts
@@ -244,7 +617,7 @@ function engineCapabilities() {
244
617
  };
245
618
  }
246
619
  var RealtimeEngine = class {
247
- constructor(sampleRate = 48e3, maxBlockSize = 128, commandCapacity = 1024, telemetryCapacity = 1024) {
620
+ constructor(sampleRate = 48e3, maxBlockSize = 128, commandCapacity = 1024, telemetryCapacity = 1024, maxChannels = 64) {
248
621
  const module2 = getSonareModule();
249
622
  const capabilities = engineCapabilities();
250
623
  if (!capabilities.abiCompatible) {
@@ -256,11 +629,18 @@ var RealtimeEngine = class {
256
629
  sampleRate,
257
630
  maxBlockSize,
258
631
  commandCapacity,
259
- telemetryCapacity
632
+ telemetryCapacity,
633
+ maxChannels
260
634
  );
261
635
  }
262
- prepare(sampleRate, maxBlockSize, commandCapacity = 1024, telemetryCapacity = 1024) {
263
- this.native.prepare(sampleRate, maxBlockSize, commandCapacity, telemetryCapacity);
636
+ prepare(sampleRate, maxBlockSize, commandCapacity = 1024, telemetryCapacity = 1024, maxChannels = 64) {
637
+ this.native.prepareWithChannels(
638
+ sampleRate,
639
+ maxBlockSize,
640
+ commandCapacity,
641
+ telemetryCapacity,
642
+ maxChannels
643
+ );
264
644
  }
265
645
  /** Queue a sample-accurate parameter change (engine kSetParam). */
266
646
  setParameter(paramId, value, renderFrame = -1) {
@@ -340,6 +720,10 @@ var RealtimeEngine = class {
340
720
  options.maxValue ?? 1
341
721
  );
342
722
  }
723
+ /** Bind a 7/14-bit CC, RPN, or NRPN descriptor to a live parameter. */
724
+ bindMidiCcBinding(binding) {
725
+ this.native.bindMidiCcBinding(binding);
726
+ }
343
727
  clearMidiCcBindings() {
344
728
  this.native.clearMidiCcBindings();
345
729
  }
@@ -382,6 +766,9 @@ var RealtimeEngine = class {
382
766
  externalMidiDroppedCount() {
383
767
  return this.native.externalMidiDroppedCount();
384
768
  }
769
+ externalMidiPendingCount() {
770
+ return this.native.externalMidiPendingCount();
771
+ }
385
772
  /**
386
773
  * Drain queued external-MIDI events, already lowered to MIDI 1.0 byte
387
774
  * messages ready to write to a Web MIDI output port. Call once per audio
@@ -392,6 +779,25 @@ var RealtimeEngine = class {
392
779
  drainExternalMidi(maxRecords = 1024) {
393
780
  return this.native.drainExternalMidi(maxRecords);
394
781
  }
782
+ /** Scalar, allocation-free external-MIDI drain for AudioWorklet SAB output. */
783
+ popExternalMidiToScratch() {
784
+ return this.native.popExternalMidiToScratch();
785
+ }
786
+ externalMidiScratchDestinationId() {
787
+ return this.native.externalMidiScratchDestinationId();
788
+ }
789
+ externalMidiScratchRenderFrame() {
790
+ return this.native.externalMidiScratchRenderFrame();
791
+ }
792
+ externalMidiScratchByteWord() {
793
+ return this.native.externalMidiScratchByteWord();
794
+ }
795
+ externalMidiScratchByteCount() {
796
+ return this.native.externalMidiScratchByteCount();
797
+ }
798
+ consumeExternalMidiScratch() {
799
+ this.native.consumeExternalMidiScratch();
800
+ }
395
801
  pushMidiInputNoteOn(group, channel, note, velocity, portTimeSamples = 0) {
396
802
  this.native.pushMidiInputNoteOn(group, channel, note, velocity, portTimeSamples);
397
803
  }
@@ -416,6 +822,10 @@ var RealtimeEngine = class {
416
822
  pushMidiCc(destinationId, group, channel, controller, value, renderFrame = -1) {
417
823
  this.native.pushMidiCc(destinationId, group, channel, controller, value, renderFrame);
418
824
  }
825
+ /** Queue one immediate MIDI 1.0 channel-voice UMP word for a destination. */
826
+ pushMidiUmp(destinationId, word0, renderFrame = -1) {
827
+ this.native.pushMidiUmp(destinationId, word0, renderFrame);
828
+ }
419
829
  /**
420
830
  * Queue an immediate (live) MIDI SysEx frame to a MIDI destination. `data` is
421
831
  * the full message including the leading 0xF0 and trailing 0xF7 (1..512
@@ -461,9 +871,14 @@ var RealtimeEngine = class {
461
871
  settleParameters() {
462
872
  this.native.settleParameters();
463
873
  }
874
+ /** Drains queued commands on an offline/control-only engine immediately. */
875
+ flushControlCommands() {
876
+ this.native.flushControlCommands();
877
+ }
464
878
  seekPpq(ppq, renderFrame = -1) {
465
879
  this.native.seekPpq(ppq, renderFrame);
466
880
  }
881
+ /** Set a finite tempo in the range (0, 100000] BPM. */
467
882
  setTempo(bpm) {
468
883
  this.native.setTempo(bpm);
469
884
  }
@@ -518,6 +933,7 @@ var RealtimeEngine = class {
518
933
  setLoopFromMarkers(startMarkerId, endMarkerId) {
519
934
  this.native.setLoopFromMarkers(startMarkerId, endMarkerId);
520
935
  }
936
+ /** Set a metronome config; click lengths are limited to one second. */
521
937
  setMetronome(config) {
522
938
  this.native.setMetronome(config);
523
939
  }
@@ -713,6 +1129,23 @@ var RealtimeEngine = class {
713
1129
  popClipPageRequest() {
714
1130
  return this.native.popClipPageRequest();
715
1131
  }
1132
+ /**
1133
+ * Moves one native request into the binding's persistent scalar scratch.
1134
+ * This avoids creating an embind JS object in AudioWorklet process().
1135
+ */
1136
+ popClipPageRequestToScratch() {
1137
+ return this.native.popClipPageRequestToScratch();
1138
+ }
1139
+ clipPageRequestScratchClipId() {
1140
+ return this.native.clipPageRequestScratchClipId();
1141
+ }
1142
+ clipPageRequestScratchSample() {
1143
+ return this.native.clipPageRequestScratchSample();
1144
+ }
1145
+ /** Cumulative page misses dropped because the native bounded request queue was full. */
1146
+ clipPageRequestOverflowCount() {
1147
+ return this.native.clipPageRequestOverflowCount();
1148
+ }
716
1149
  setCaptureBuffer(numChannels, capacityFrames) {
717
1150
  this.native.setCaptureBuffer(numChannels, capacityFrames);
718
1151
  }
@@ -725,6 +1158,7 @@ var RealtimeEngine = class {
725
1158
  setCaptureSource(source) {
726
1159
  this.native.setCaptureSource(source);
727
1160
  }
1161
+ /** Positive values delay capture relative to the punch window. */
728
1162
  setRecordOffsetSamples(offsetSamples) {
729
1163
  this.native.setRecordOffsetSamples(offsetSamples);
730
1164
  }
@@ -740,6 +1174,10 @@ var RealtimeEngine = class {
740
1174
  capturedAudio() {
741
1175
  return this.native.capturedAudio();
742
1176
  }
1177
+ /**
1178
+ * Renders in place, adding engine output to `channels`. Zero each plane first
1179
+ * when it contains no upstream input.
1180
+ */
743
1181
  process(channels) {
744
1182
  return this.native.process(channels);
745
1183
  }
@@ -762,6 +1200,7 @@ var RealtimeEngine = class {
762
1200
  }
763
1201
  /**
764
1202
  * Runs the engine in place over the prepared per-channel scratch buffers.
1203
+ * Zero each active span first when it contains no upstream input.
765
1204
  * Allocation-free: safe to call on the AudioWorklet render thread after
766
1205
  * `prepareChannels`.
767
1206
  */
@@ -783,6 +1222,42 @@ var RealtimeEngine = class {
783
1222
  drainTelemetry(maxRecords = 1024) {
784
1223
  return this.native.drainTelemetry(maxRecords);
785
1224
  }
1225
+ popTelemetryToScratch() {
1226
+ return this.native.popTelemetryToScratch();
1227
+ }
1228
+ telemetryScratchType() {
1229
+ return this.native.telemetryScratchType();
1230
+ }
1231
+ telemetryScratchError() {
1232
+ return this.native.telemetryScratchError();
1233
+ }
1234
+ telemetryScratchRenderFrame() {
1235
+ return Number(this.native.telemetryScratchRenderFrame());
1236
+ }
1237
+ telemetryScratchTimelineSample() {
1238
+ return Number(this.native.telemetryScratchTimelineSample());
1239
+ }
1240
+ telemetryScratchAudibleTimelineSample() {
1241
+ return Number(this.native.telemetryScratchAudibleTimelineSample());
1242
+ }
1243
+ telemetryScratchGraphLatencySamplesQ8() {
1244
+ return this.native.telemetryScratchGraphLatencySamplesQ8();
1245
+ }
1246
+ telemetryScratchValue() {
1247
+ return this.native.telemetryScratchValue();
1248
+ }
1249
+ popMeterTelemetryToScratch() {
1250
+ return this.native.popMeterTelemetryToScratch();
1251
+ }
1252
+ meterScratchTargetId() {
1253
+ return this.native.meterScratchTargetId();
1254
+ }
1255
+ meterScratchRenderFrame() {
1256
+ return Number(this.native.meterScratchRenderFrame());
1257
+ }
1258
+ meterScratchValue(field) {
1259
+ return this.native.meterScratchValue(field);
1260
+ }
786
1261
  drainMeterTelemetry(maxRecords = 1024) {
787
1262
  return this.native.drainMeterTelemetry(maxRecords);
788
1263
  }
@@ -810,6 +1285,30 @@ var RealtimeEngine = class {
810
1285
  drainScopeTelemetry(maxRecords = 1024) {
811
1286
  return this.native.drainScopeTelemetry(maxRecords);
812
1287
  }
1288
+ popScopeTelemetryToScratch() {
1289
+ return this.native.popScopeTelemetryToScratch();
1290
+ }
1291
+ scopeScratchTargetId() {
1292
+ return this.native.scopeScratchTargetId();
1293
+ }
1294
+ scopeScratchRenderFrame() {
1295
+ return Number(this.native.scopeScratchRenderFrame());
1296
+ }
1297
+ scopeScratchBandCount() {
1298
+ return this.native.scopeScratchBandCount();
1299
+ }
1300
+ scopeScratchBand(index) {
1301
+ return this.native.scopeScratchBand(index);
1302
+ }
1303
+ scopeScratchPointCount() {
1304
+ return this.native.scopeScratchPointCount();
1305
+ }
1306
+ scopeScratchPointLeft(index) {
1307
+ return this.native.scopeScratchPointLeft(index);
1308
+ }
1309
+ scopeScratchPointRight(index) {
1310
+ return this.native.scopeScratchPointRight(index);
1311
+ }
813
1312
  destroy() {
814
1313
  this.native.delete();
815
1314
  }
@@ -858,7 +1357,10 @@ var Mixer = class _Mixer {
858
1357
  const module2 = getSonareModule();
859
1358
  return new _Mixer(module2.createMixerFromSceneJson(json, sampleRate, blockSize), blockSize);
860
1359
  }
861
- /** Rebuild and compile the routing graph from the current scene topology. */
1360
+ /**
1361
+ * Rebuild and compile the routing graph without resetting its absolute
1362
+ * automation sample position or queued strip automation.
1363
+ */
862
1364
  compile() {
863
1365
  this.mixer.compile();
864
1366
  }
@@ -1017,6 +1519,10 @@ var Mixer = class _Mixer {
1017
1519
  setVcaGroupGainDb(id, gainDb) {
1018
1520
  this.mixer.setVcaGroupGainDb(id, gainDb);
1019
1521
  }
1522
+ /** Replace an existing VCA group's strip membership. */
1523
+ setVcaGroupMembers(id, members) {
1524
+ this.mixer.setVcaGroupMembers(id, members);
1525
+ }
1020
1526
  /** Remove a VCA group by id. */
1021
1527
  removeVcaGroup(id) {
1022
1528
  this.mixer.removeVcaGroup(id);
@@ -1156,6 +1662,10 @@ var Mixer = class _Mixer {
1156
1662
  }
1157
1663
  return this.mixer.meterTap(stripIndex, meterTapCode(tap));
1158
1664
  }
1665
+ /** Read the post-insert meter for a compiled bus, including master. */
1666
+ busMeter(busId) {
1667
+ return this.mixer.busMeter(busId);
1668
+ }
1159
1669
  /**
1160
1670
  * Schedule sample-accurate fader automation on a strip.
1161
1671
  *
@@ -1256,65 +1766,18 @@ var Mixer = class _Mixer {
1256
1766
  };
1257
1767
 
1258
1768
  // src/realtime_voice_changer.ts
1259
- function isFlatVoiceChangerPod(config) {
1260
- return typeof config === "object" && config !== null && "retuneSemitones" in config;
1261
- }
1262
- function flatVoiceChangerPodToNested(pod) {
1263
- return {
1264
- inputGainDb: pod.inputGainDb,
1265
- outputGainDb: pod.outputGainDb,
1266
- wetMix: pod.wetMix,
1267
- retune: { semitones: pod.retuneSemitones, mix: pod.retuneMix, grainSize: pod.retuneGrainSize },
1268
- formant: {
1269
- factor: pod.formantFactor,
1270
- amount: pod.formantAmount,
1271
- body: pod.formantBody,
1272
- brightness: pod.formantBrightness,
1273
- nasal: pod.formantNasal
1274
- },
1275
- eq: {
1276
- highpassHz: pod.eqHighpassHz,
1277
- bodyDb: pod.eqBodyDb,
1278
- presenceDb: pod.eqPresenceDb,
1279
- airDb: pod.eqAirDb
1280
- },
1281
- gate: {
1282
- thresholdDb: pod.gateThresholdDb,
1283
- attackMs: pod.gateAttackMs,
1284
- releaseMs: pod.gateReleaseMs,
1285
- rangeDb: pod.gateRangeDb
1286
- },
1287
- compressor: {
1288
- thresholdDb: pod.compressorThresholdDb,
1289
- ratio: pod.compressorRatio,
1290
- attackMs: pod.compressorAttackMs,
1291
- releaseMs: pod.compressorReleaseMs,
1292
- makeupGainDb: pod.compressorMakeupGainDb
1293
- },
1294
- deesser: {
1295
- frequencyHz: pod.deesserFrequencyHz,
1296
- thresholdDb: pod.deesserThresholdDb,
1297
- ratio: pod.deesserRatio,
1298
- rangeDb: pod.deesserRangeDb
1299
- },
1300
- reverb: {
1301
- mix: pod.reverbMix,
1302
- timeMs: pod.reverbTimeMs,
1303
- damping: pod.reverbDamping,
1304
- seed: pod.reverbSeed
1305
- },
1306
- limiter: {
1307
- ceilingDb: pod.limiterCeilingDb,
1308
- releaseMs: pod.limiterReleaseMs,
1309
- enableIspLimiter: pod.limiterEnableIspLimiter,
1310
- ispCeilingDbtp: pod.limiterIspCeilingDbtp
1311
- }
1312
- };
1313
- }
1314
1769
  var RealtimeVoiceChanger = class {
1315
- constructor(config = "neutral-monitor") {
1770
+ /**
1771
+ * Creates a voice changer. Supplying `sampleRate` prepares it immediately,
1772
+ * matching the Node and Python constructors; omitting it preserves the
1773
+ * explicit {@link prepare} lifecycle for callers that configure later.
1774
+ */
1775
+ constructor(config = "neutral-monitor", sampleRate, maxBlockSize = 128, channels = 1) {
1316
1776
  const module2 = getSonareModule();
1317
1777
  this.changer = module2.createRealtimeVoiceChanger(config);
1778
+ if (sampleRate !== void 0) {
1779
+ this.changer.prepare(sampleRate, maxBlockSize, channels);
1780
+ }
1318
1781
  }
1319
1782
  prepare(sampleRate, maxBlockSize = 128, channels = 1) {
1320
1783
  this.changer.prepare(sampleRate, maxBlockSize, channels);
@@ -1323,8 +1786,15 @@ var RealtimeVoiceChanger = class {
1323
1786
  this.changer.reset();
1324
1787
  }
1325
1788
  setConfig(config) {
1326
- const resolved = isFlatVoiceChangerPod(config) ? flatVoiceChangerPodToNested(config) : config;
1327
- this.changer.setConfig(resolved);
1789
+ this.changer.setConfig(config);
1790
+ }
1791
+ /**
1792
+ * Apply a flat, pre-normalized config without JSON serialization. Intended
1793
+ * for AudioWorklet control messages whose sender prepared the POD on the
1794
+ * main thread.
1795
+ */
1796
+ setPodConfig(config) {
1797
+ this.changer.setPodConfig(config);
1328
1798
  }
1329
1799
  configJson() {
1330
1800
  return this.changer.configJson();
@@ -1332,6 +1802,13 @@ var RealtimeVoiceChanger = class {
1332
1802
  latencySamples() {
1333
1803
  return this.changer.latencySamples();
1334
1804
  }
1805
+ /**
1806
+ * Monotonically increases whenever {@link prepare} can replace the native
1807
+ * scratch buffers. Cached WASM heap views must be reacquired after it changes.
1808
+ */
1809
+ bufferGeneration() {
1810
+ return this.changer.bufferGeneration();
1811
+ }
1335
1812
  processMono(samples) {
1336
1813
  return this.changer.processMono(samples);
1337
1814
  }
@@ -1408,10 +1885,12 @@ var RealtimeVoiceChanger = class {
1408
1885
  createRealtimeMonoBuffer(numSamples) {
1409
1886
  let input = this.getMonoInputBuffer(numSamples);
1410
1887
  let output = this.getMonoOutputBuffer(numSamples);
1888
+ let generation = this.bufferGeneration();
1411
1889
  const reacquireIfDetached = () => {
1412
- if (input.byteLength === 0 || output.byteLength === 0) {
1890
+ if (generation !== this.bufferGeneration() || input.byteLength === 0 || output.byteLength === 0) {
1413
1891
  input = this.getMonoInputBuffer(numSamples);
1414
1892
  output = this.getMonoOutputBuffer(numSamples);
1893
+ generation = this.bufferGeneration();
1415
1894
  }
1416
1895
  };
1417
1896
  return {
@@ -1433,10 +1912,12 @@ var RealtimeVoiceChanger = class {
1433
1912
  createRealtimeInterleavedBuffer(numFrames, numChannels) {
1434
1913
  let input = this.getInterleavedInputBuffer(numFrames, numChannels);
1435
1914
  let output = this.getInterleavedOutputBuffer(numFrames, numChannels);
1915
+ let generation = this.bufferGeneration();
1436
1916
  const reacquireIfDetached = () => {
1437
- if (input.byteLength === 0 || output.byteLength === 0) {
1917
+ if (generation !== this.bufferGeneration() || input.byteLength === 0 || output.byteLength === 0) {
1438
1918
  input = this.getInterleavedInputBuffer(numFrames, numChannels);
1439
1919
  output = this.getInterleavedOutputBuffer(numFrames, numChannels);
1920
+ generation = this.bufferGeneration();
1440
1921
  }
1441
1922
  };
1442
1923
  return {
@@ -1463,15 +1944,17 @@ var RealtimeVoiceChanger = class {
1463
1944
  */
1464
1945
  createRealtimePlanarBuffer(numFrames, numChannels) {
1465
1946
  let channels = [];
1947
+ let generation = this.bufferGeneration();
1466
1948
  const acquire = () => {
1467
1949
  channels = [];
1468
1950
  for (let ch = 0; ch < numChannels; ch++) {
1469
1951
  channels.push(this.getPlanarChannelBuffer(ch, numFrames));
1470
1952
  }
1953
+ generation = this.bufferGeneration();
1471
1954
  };
1472
1955
  acquire();
1473
1956
  const reacquireIfDetached = () => {
1474
- if ((channels[0]?.byteLength ?? 0) === 0) {
1957
+ if (generation !== this.bufferGeneration() || (channels[0]?.byteLength ?? 0) === 0) {
1475
1958
  acquire();
1476
1959
  }
1477
1960
  };
@@ -1553,7 +2036,7 @@ function resolveParamId(parameters, nodeId, param) {
1553
2036
  if (byName) {
1554
2037
  return byName.id;
1555
2038
  }
1556
- return resolveTargetId(param || nodeId);
2039
+ throw new RangeError(`Unknown engine parameter ${JSON.stringify(param)} for node ${nodeId}`);
1557
2040
  }
1558
2041
  function curveCode(curve) {
1559
2042
  if (typeof curve === "number") {
@@ -1618,12 +2101,16 @@ function buildTransportFacade(ctx) {
1618
2101
  },
1619
2102
  seekPpq: (ppq, sampleTime = -1) => {
1620
2103
  ctx.offlineEngine.seekPpq(ppq, sampleTime);
1621
- return ctx.realtimeNode.seekPpq(ppq, sampleTime);
2104
+ const ok = ctx.realtimeNode.seekPpq(ppq, sampleTime);
2105
+ ctx.flushOfflineMirror();
2106
+ return ok;
1622
2107
  },
1623
2108
  seekSeconds: (seconds, sampleTime = -1) => {
1624
2109
  const timelineSample = Math.max(0, Math.round(seconds * ctx.sampleRate));
1625
2110
  ctx.offlineEngine.seekSample(timelineSample, sampleTime);
1626
- return ctx.realtimeNode.seekSample(timelineSample, sampleTime);
2111
+ const ok = ctx.realtimeNode.seekSample(timelineSample, sampleTime);
2112
+ ctx.flushOfflineMirror();
2113
+ return ok;
1627
2114
  },
1628
2115
  setTempo: (bpm) => ctx.setTempo(bpm),
1629
2116
  setTempoSegments: (segments) => ctx.setTempoSegments(segments),
@@ -1710,6 +2197,10 @@ function decodeFrame(lo, hi) {
1710
2197
  var SONARE_ENGINE_RING_HEADER_INTS = 5;
1711
2198
  var SONARE_ENGINE_COMMAND_RECORD_BYTES = 32;
1712
2199
  var SONARE_ENGINE_TELEMETRY_RECORD_BYTES = 48;
2200
+ var SONARE_CLIP_PAGE_REQUEST_RING_HEADER_INTS = 5;
2201
+ var SONARE_CLIP_PAGE_REQUEST_RING_RECORD_UINT32S = 2;
2202
+ var SONARE_EXTERNAL_MIDI_RING_HEADER_INTS = 5;
2203
+ var SONARE_EXTERNAL_MIDI_RING_RECORD_UINT32S = 4;
1713
2204
  var SonareEngineCommandType = /* @__PURE__ */ ((SonareEngineCommandType2) => {
1714
2205
  SonareEngineCommandType2[SonareEngineCommandType2["SetParam"] = 0] = "SetParam";
1715
2206
  SonareEngineCommandType2[SonareEngineCommandType2["SetParamSmoothed"] = 1] = "SetParamSmoothed";
@@ -1754,6 +2245,9 @@ var SonareEngineTelemetryError = /* @__PURE__ */ ((SonareEngineTelemetryError2)
1754
2245
  SonareEngineTelemetryError2[SonareEngineTelemetryError2["CommandBacklogDeferred"] = 14] = "CommandBacklogDeferred";
1755
2246
  SonareEngineTelemetryError2[SonareEngineTelemetryError2["ClipPageUnderrun"] = 15] = "ClipPageUnderrun";
1756
2247
  SonareEngineTelemetryError2[SonareEngineTelemetryError2["InsertAutomationOverflow"] = 16] = "InsertAutomationOverflow";
2248
+ SonareEngineTelemetryError2[SonareEngineTelemetryError2["MidiClockOverflow"] = 17] = "MidiClockOverflow";
2249
+ SonareEngineTelemetryError2[SonareEngineTelemetryError2["MetronomeOverflow"] = 18] = "MetronomeOverflow";
2250
+ SonareEngineTelemetryError2[SonareEngineTelemetryError2["InvalidCommand"] = 19] = "InvalidCommand";
1757
2251
  return SonareEngineTelemetryError2;
1758
2252
  })(SonareEngineTelemetryError || {});
1759
2253
  function toDb(value) {
@@ -1951,6 +2445,128 @@ function sonareEngineTelemetryRingBufferByteLength(capacity) {
1951
2445
  const clampedCapacity = Math.max(1, Math.floor(capacity));
1952
2446
  return SONARE_ENGINE_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT + clampedCapacity * SONARE_ENGINE_TELEMETRY_RECORD_BYTES;
1953
2447
  }
2448
+ function sonareClipPageRequestRingBufferByteLength(capacity) {
2449
+ const clampedCapacity = Math.max(1, Math.floor(capacity));
2450
+ return SONARE_CLIP_PAGE_REQUEST_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT + clampedCapacity * SONARE_CLIP_PAGE_REQUEST_RING_RECORD_UINT32S * Uint32Array.BYTES_PER_ELEMENT;
2451
+ }
2452
+ function sonareExternalMidiRingBufferByteLength(capacity) {
2453
+ const clampedCapacity = Math.max(1, Math.floor(capacity));
2454
+ return SONARE_EXTERNAL_MIDI_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT + clampedCapacity * SONARE_EXTERNAL_MIDI_RING_RECORD_UINT32S * Uint32Array.BYTES_PER_ELEMENT;
2455
+ }
2456
+ function externalMidiRingFromSharedBuffer(sharedBuffer, fallbackCapacity) {
2457
+ const headerBytes = SONARE_EXTERNAL_MIDI_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT;
2458
+ const header = new Int32Array(sharedBuffer, 0, SONARE_EXTERNAL_MIDI_RING_HEADER_INTS);
2459
+ const existingCapacity = Atomics.load(header, 2);
2460
+ const capacity = Math.max(1, Math.floor(existingCapacity || fallbackCapacity || 1));
2461
+ if (sharedBuffer.byteLength < sonareExternalMidiRingBufferByteLength(capacity)) {
2462
+ throw new Error("externalMidiSharedBuffer is too small for the requested ring capacity.");
2463
+ }
2464
+ Atomics.store(header, 2, capacity);
2465
+ Atomics.store(header, 3, SONARE_EXTERNAL_MIDI_RING_RECORD_UINT32S);
2466
+ return {
2467
+ header,
2468
+ records: new Uint32Array(
2469
+ sharedBuffer,
2470
+ headerBytes,
2471
+ capacity * SONARE_EXTERNAL_MIDI_RING_RECORD_UINT32S
2472
+ ),
2473
+ capacity
2474
+ };
2475
+ }
2476
+ function createSonareExternalMidiRingBuffer(capacity = 256) {
2477
+ const clampedCapacity = Math.max(1, Math.floor(capacity));
2478
+ const sharedBuffer = new SharedArrayBuffer(
2479
+ sonareExternalMidiRingBufferByteLength(clampedCapacity)
2480
+ );
2481
+ const ring = externalMidiRingFromSharedBuffer(sharedBuffer, clampedCapacity);
2482
+ Atomics.store(ring.header, 0, 0);
2483
+ Atomics.store(ring.header, 1, 0);
2484
+ Atomics.store(ring.header, 4, 0);
2485
+ return { sharedBuffer, header: ring.header, records: ring.records, capacity: ring.capacity };
2486
+ }
2487
+ function pushSonareExternalMidiRingBuffer(ring, destinationId, renderFrame, byteWord, byteCount) {
2488
+ const destination = Number(destinationId);
2489
+ const frame = Number(renderFrame);
2490
+ const packedBytes = Number(byteWord);
2491
+ const count = Number(byteCount);
2492
+ if (!Number.isSafeInteger(destination) || !Number.isSafeInteger(frame) || frame < 0 || !Number.isSafeInteger(packedBytes) || packedBytes < 0 || packedBytes > 16777215 || !Number.isSafeInteger(count) || count < 1 || count > 3) {
2493
+ Atomics.add(ring.header, 4, 1);
2494
+ return false;
2495
+ }
2496
+ const writeIndex = Atomics.load(ring.header, 0);
2497
+ const readIndex = Atomics.load(ring.header, 1);
2498
+ if (writeIndex - readIndex >= ring.capacity) {
2499
+ Atomics.add(ring.header, 4, 1);
2500
+ return false;
2501
+ }
2502
+ const offset = writeIndex % ring.capacity * SONARE_EXTERNAL_MIDI_RING_RECORD_UINT32S;
2503
+ Atomics.store(ring.records, offset, destination);
2504
+ Atomics.store(ring.records, offset + 1, frame);
2505
+ Atomics.store(ring.records, offset + 2, packedBytes);
2506
+ Atomics.store(ring.records, offset + 3, count);
2507
+ Atomics.store(ring.header, 0, writeIndex + 1);
2508
+ return true;
2509
+ }
2510
+ function readSonareExternalMidiRingBuffer(ring) {
2511
+ const readIndex = Atomics.load(ring.header, 1);
2512
+ const writeIndex = Atomics.load(ring.header, 0);
2513
+ const events = [];
2514
+ for (let index = readIndex; index < writeIndex; index++) {
2515
+ const offset = index % ring.capacity * SONARE_EXTERNAL_MIDI_RING_RECORD_UINT32S;
2516
+ events.push({
2517
+ destinationId: Atomics.load(ring.records, offset) >>> 0,
2518
+ renderFrame: Atomics.load(ring.records, offset + 1) >>> 0,
2519
+ byteWord: Atomics.load(ring.records, offset + 2) >>> 0,
2520
+ byteCount: Atomics.load(ring.records, offset + 3) >>> 0
2521
+ });
2522
+ }
2523
+ Atomics.store(ring.header, 1, writeIndex);
2524
+ return { events, dropped: Atomics.load(ring.header, 4) >>> 0 };
2525
+ }
2526
+ function createSonareClipPageRequestRingBuffer(capacity = 128) {
2527
+ const clampedCapacity = Math.max(1, Math.floor(capacity));
2528
+ const sharedBuffer = new SharedArrayBuffer(
2529
+ sonareClipPageRequestRingBufferByteLength(clampedCapacity)
2530
+ );
2531
+ const ring = clipPageRequestRingFromSharedBuffer(sharedBuffer, clampedCapacity);
2532
+ Atomics.store(ring.header, 0, 0);
2533
+ Atomics.store(ring.header, 1, 0);
2534
+ Atomics.store(ring.header, 2, ring.capacity);
2535
+ Atomics.store(ring.header, 3, SONARE_CLIP_PAGE_REQUEST_RING_RECORD_UINT32S);
2536
+ Atomics.store(ring.header, 4, 0);
2537
+ return { sharedBuffer, header: ring.header, records: ring.records, capacity: ring.capacity };
2538
+ }
2539
+ function pushSonareClipPageRequestRingBuffer(ring, clipId, pageIndex) {
2540
+ if (!Number.isSafeInteger(clipId) || clipId < 0 || clipId > 4294967295 || !Number.isSafeInteger(pageIndex) || pageIndex < 0 || pageIndex > 4294967295) {
2541
+ Atomics.add(ring.header, 4, 1);
2542
+ return false;
2543
+ }
2544
+ const writeIndex = Atomics.load(ring.header, 0);
2545
+ const readIndex = Atomics.load(ring.header, 1);
2546
+ if (writeIndex - readIndex >= ring.capacity) {
2547
+ Atomics.add(ring.header, 4, 1);
2548
+ return false;
2549
+ }
2550
+ const offset = writeIndex % ring.capacity * SONARE_CLIP_PAGE_REQUEST_RING_RECORD_UINT32S;
2551
+ Atomics.store(ring.records, offset, clipId);
2552
+ Atomics.store(ring.records, offset + 1, pageIndex);
2553
+ Atomics.store(ring.header, 0, writeIndex + 1);
2554
+ return true;
2555
+ }
2556
+ function readSonareClipPageRequestRingBuffer(ring) {
2557
+ const readIndex = Atomics.load(ring.header, 1);
2558
+ const writeIndex = Atomics.load(ring.header, 0);
2559
+ const requests = [];
2560
+ for (let index = readIndex; index < writeIndex; index++) {
2561
+ const offset = index % ring.capacity * SONARE_CLIP_PAGE_REQUEST_RING_RECORD_UINT32S;
2562
+ requests.push({
2563
+ clipId: Atomics.load(ring.records, offset),
2564
+ pageIndex: Atomics.load(ring.records, offset + 1)
2565
+ });
2566
+ }
2567
+ Atomics.store(ring.header, 1, writeIndex);
2568
+ return { requests, dropped: Atomics.load(ring.header, 4) >>> 0 };
2569
+ }
1954
2570
  function createSonareEngineCommandRingBuffer(capacity = 128) {
1955
2571
  const clampedCapacity = Math.max(1, Math.floor(capacity));
1956
2572
  const sharedBuffer = new SharedArrayBuffer(
@@ -2091,51 +2707,82 @@ function engineRingFromSharedBuffer(sharedBuffer, recordBytes, fallbackCapacity)
2091
2707
  capacity
2092
2708
  };
2093
2709
  }
2710
+ function clipPageRequestRingFromSharedBuffer(sharedBuffer, fallbackCapacity) {
2711
+ const headerBytes = SONARE_CLIP_PAGE_REQUEST_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT;
2712
+ const header = new Int32Array(sharedBuffer, 0, SONARE_CLIP_PAGE_REQUEST_RING_HEADER_INTS);
2713
+ const existingCapacity = Atomics.load(header, 2);
2714
+ const capacity = Math.max(1, Math.floor(existingCapacity || fallbackCapacity || 1));
2715
+ const minBytes = sonareClipPageRequestRingBufferByteLength(capacity);
2716
+ if (sharedBuffer.byteLength < minBytes) {
2717
+ throw new Error("clipPageRequestSharedBuffer is too small for the requested ring capacity.");
2718
+ }
2719
+ Atomics.store(header, 2, capacity);
2720
+ Atomics.store(header, 3, SONARE_CLIP_PAGE_REQUEST_RING_RECORD_UINT32S);
2721
+ return {
2722
+ header,
2723
+ records: new Uint32Array(
2724
+ sharedBuffer,
2725
+ headerBytes,
2726
+ capacity * SONARE_CLIP_PAGE_REQUEST_RING_RECORD_UINT32S
2727
+ ),
2728
+ capacity
2729
+ };
2730
+ }
2094
2731
  function recordOffset(index, capacity, recordBytes) {
2095
2732
  return index % capacity * recordBytes;
2096
2733
  }
2097
- function toBigInt64(value, fallback) {
2098
- if (typeof value === "bigint") {
2099
- return value;
2734
+ function toSafeInteger(value, fallback) {
2735
+ const resolved = typeof value === "bigint" ? Number(value) : value;
2736
+ if (resolved === void 0) {
2737
+ return fallback;
2100
2738
  }
2101
- if (typeof value === "number") {
2102
- return BigInt(Math.trunc(value));
2739
+ if (!Number.isSafeInteger(resolved)) {
2740
+ throw new RangeError("64-bit ring values must be safe integers");
2103
2741
  }
2104
- return fallback;
2742
+ return resolved;
2743
+ }
2744
+ function writeInt64Words(view, offset, value) {
2745
+ const integer = toSafeInteger(value, 0);
2746
+ view.setUint32(offset, integer >>> 0, true);
2747
+ view.setInt32(offset + 4, Math.floor(integer / 4294967296), true);
2748
+ }
2749
+ function readInt64Words(view, offset) {
2750
+ return view.getInt32(offset + 4, true) * 4294967296 + view.getUint32(offset, true);
2105
2751
  }
2106
2752
  function writeEngineCommandRecord(view, offset, command) {
2107
2753
  view.setUint32(offset, command.type, true);
2108
2754
  view.setUint32(offset + 4, command.targetId ?? 0, true);
2109
- view.setBigInt64(offset + 8, toBigInt64(command.sampleTime, -1n), true);
2755
+ writeInt64Words(view, offset + 8, toSafeInteger(command.sampleTime, -1));
2110
2756
  view.setFloat64(offset + 16, command.argFloat ?? 0, true);
2111
- view.setBigInt64(offset + 24, toBigInt64(command.argInt, 0n), true);
2757
+ writeInt64Words(view, offset + 24, toSafeInteger(command.argInt, 0));
2112
2758
  }
2113
2759
  function readEngineCommandRecord(view, offset) {
2114
2760
  return {
2115
2761
  type: view.getUint32(offset, true),
2116
2762
  targetId: view.getUint32(offset + 4, true),
2117
- sampleTime: Number(view.getBigInt64(offset + 8, true)),
2763
+ sampleTime: readInt64Words(view, offset + 8),
2118
2764
  argFloat: view.getFloat64(offset + 16, true),
2119
- argInt: Number(view.getBigInt64(offset + 24, true))
2765
+ argInt: readInt64Words(view, offset + 24)
2120
2766
  };
2121
2767
  }
2122
2768
  function writeEngineTelemetryRecord(view, offset, telemetry) {
2123
2769
  view.setUint32(offset, telemetry.type, true);
2124
2770
  view.setUint32(offset + 4, telemetry.error, true);
2125
- view.setBigInt64(offset + 8, BigInt(Math.trunc(telemetry.renderFrame)), true);
2126
- view.setBigInt64(offset + 16, BigInt(Math.trunc(telemetry.timelineSample)), true);
2127
- view.setBigInt64(offset + 24, BigInt(Math.trunc(telemetry.audibleTimelineSample)), true);
2771
+ writeInt64Words(view, offset + 8, telemetry.renderFrame);
2772
+ writeInt64Words(view, offset + 16, telemetry.timelineSample);
2773
+ writeInt64Words(view, offset + 24, telemetry.audibleTimelineSample);
2128
2774
  view.setInt32(offset + 32, telemetry.graphLatencySamplesQ8, true);
2129
2775
  view.setUint32(offset + 36, telemetry.value, true);
2130
- view.setBigInt64(offset + 40, 0n, true);
2776
+ view.setUint32(offset + 40, 0, true);
2777
+ view.setUint32(offset + 44, 0, true);
2131
2778
  }
2132
2779
  function readEngineTelemetryRecord(view, offset) {
2133
2780
  return {
2134
2781
  type: view.getUint32(offset, true),
2135
2782
  error: view.getUint32(offset + 4, true),
2136
- renderFrame: Number(view.getBigInt64(offset + 8, true)),
2137
- timelineSample: Number(view.getBigInt64(offset + 16, true)),
2138
- audibleTimelineSample: Number(view.getBigInt64(offset + 24, true)),
2783
+ renderFrame: readInt64Words(view, offset + 8),
2784
+ timelineSample: readInt64Words(view, offset + 16),
2785
+ audibleTimelineSample: readInt64Words(view, offset + 24),
2139
2786
  graphLatencySamplesQ8: view.getInt32(offset + 32, true),
2140
2787
  value: view.getUint32(offset + 36, true)
2141
2788
  };
@@ -2184,13 +2831,16 @@ function configureCapture(ctx, options) {
2184
2831
  ctx.postSync({ type: "syncCapture", ...config });
2185
2832
  }
2186
2833
  function armRecord(ctx, trackId, enabled) {
2834
+ if (trackId !== 0) {
2835
+ throw new RangeError("Capture is global; armRecord only accepts trackId 0");
2836
+ }
2187
2837
  if (enabled && !ctx.getCaptureConfig()) {
2188
2838
  throw new Error("Capture buffer is not configured");
2189
2839
  }
2190
2840
  ctx.offlineEngine.armCapture(enabled);
2191
- return ctx.realtimeNode.sendCommand({
2841
+ return ctx.sendCommand({
2192
2842
  type: 13 /* ArmRecord */,
2193
- targetId: ctx.resolveTargetId(trackId),
2843
+ targetId: 0,
2194
2844
  sampleTime: -1,
2195
2845
  argInt: enabled ? 1 : 0
2196
2846
  });
@@ -2199,7 +2849,7 @@ function punch(ctx, inPpq, outPpq) {
2199
2849
  const inSample = ctx.offlineEngine.sampleAtPpq(inPpq);
2200
2850
  const outSample = ctx.offlineEngine.sampleAtPpq(outPpq);
2201
2851
  ctx.offlineEngine.setCapturePunch(inSample, outSample, true);
2202
- return ctx.realtimeNode.sendCommand({
2852
+ return ctx.sendCommand({
2203
2853
  type: 14 /* Punch */,
2204
2854
  sampleTime: -1,
2205
2855
  argInt: inSample,
@@ -2225,7 +2875,7 @@ function addClip(ctx, trackId, buffer, startPpq, opts = {}) {
2225
2875
  const clip = {
2226
2876
  ...opts,
2227
2877
  id,
2228
- channels: buffer,
2878
+ ...Array.isArray(buffer) ? { channels: buffer } : { pageProvider: buffer },
2229
2879
  startPpq,
2230
2880
  trackId: ctx.resolveTargetId(trackId)
2231
2881
  };
@@ -2272,6 +2922,12 @@ function syncClipsDelta(ctx, upserts, removeIds) {
2272
2922
  for (const clip of upserts) {
2273
2923
  const prepared = clip.id === void 0 ? clip : preparedById.get(clip.id) ?? clip;
2274
2924
  const channels = prepared.channels;
2925
+ if (!channels && prepared.pageProvider !== void 0) {
2926
+ if (ctx.commitWorkletClipPageProvider(prepared)) {
2927
+ continue;
2928
+ }
2929
+ throw new Error("A pageProvider on SonareEngine must be created by attachOpfsClipStream().");
2930
+ }
2275
2931
  if (prepared.id === void 0 || prepared.warpMode !== "off" || !channels || channels.length === 0 || channels[0].length <= PREBAKED_CLIP_PAGE_THRESHOLD) {
2276
2932
  inlineUpserts.push(prepared);
2277
2933
  continue;
@@ -2475,7 +3131,7 @@ function isEngineSyncMessage(value) {
2475
3131
  if (!isRecord(value) || typeof value.type !== "string") {
2476
3132
  return false;
2477
3133
  }
2478
- return value.type === "syncClips" || value.type === "syncClipsDelta" || value.type === "syncClipPageProvider" || value.type === "syncClipPage" || value.type === "syncClipPageCommit" || value.type === "syncMidiClips" || value.type === "syncMarkers" || value.type === "syncMetronome" || value.type === "syncAutomation" || value.type === "syncTempo" || value.type === "syncMixer" || value.type === "syncCapture" || value.type === "syncTrackStripEqBand" || value.type === "syncMasterStripEqBand" || value.type === "syncTrackStripInsertBypassed" || value.type === "syncMasterStripInsertBypassed" || value.type === "syncTrackStripInsertParamByName" || value.type === "syncMasterStripInsertParamByName" || value.type === "syncBusStripInsertParamByName" || value.type === "syncTrackStripPan" || value.type === "syncTrackStripPanLaw" || value.type === "syncTrackStripPanMode" || value.type === "syncTrackStripDualPan" || value.type === "syncTrackStripChannelDelaySamples" || value.type === "syncBuiltinInstrument" || value.type === "syncSynthInstrument" || value.type === "syncSf2Instrument" || value.type === "syncLoadSoundFont" || value.type === "syncMidiFx" || value.type === "syncClearMidiFx" || value.type === "syncMidiNoteOn" || value.type === "syncMidiNoteOff" || value.type === "syncMidiCc" || value.type === "syncMidiSysex" || value.type === "syncMidiPanic" || value.type === "syncMidiDestinationExternal" || value.type === "syncExternalMidiClock";
3134
+ return value.type === "syncClips" || value.type === "syncClipsDelta" || value.type === "syncClipPageProvider" || value.type === "syncClipPage" || value.type === "syncClipPageClear" || value.type === "syncClipPageCommit" || value.type === "syncClipPageDestroy" || 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 === "syncMidiUmp" || value.type === "syncMidiSysex" || value.type === "syncMidiPanic" || value.type === "syncMidiDestinationExternal" || value.type === "syncExternalMidiClock" || value.type === "destroy";
2479
3135
  }
2480
3136
  function isEngineCaptureRequestMessage(value) {
2481
3137
  return isRecord(value) && value.type === "captureRequest" && typeof value.requestId === "number" && (value.op === "status" || value.op === "read" || value.op === "reset");
@@ -2501,20 +3157,51 @@ function isEngineTelemetryRecord(value) {
2501
3157
  function isExternalMidiBatchMessage(value) {
2502
3158
  return isRecord(value) && value.type === "externalMidi" && Array.isArray(value.events);
2503
3159
  }
3160
+ function isClipPageRequestMessage(value) {
3161
+ return isRecord(value) && value.type === "clipPageRequest" && Array.isArray(value.requests) && value.requests.every(
3162
+ (request) => isRecord(request) && typeof request.clipId === "number" && typeof request.pageIndex === "number"
3163
+ );
3164
+ }
2504
3165
  function isMeterSnapshot(value) {
2505
3166
  return isRecord(value) && value.type === "meter" && typeof value.frame === "number" && typeof value.peakDbL === "number" && typeof value.peakDbR === "number" && typeof value.rmsDbL === "number" && typeof value.rmsDbR === "number" && typeof value.correlation === "number" && (typeof value.targetId === "number" || value.targetId === void 0);
2506
3167
  }
2507
3168
 
2508
3169
  // src/worklet/engine-node.ts
3170
+ function isFiniteInteger(value) {
3171
+ if (value === void 0) {
3172
+ return true;
3173
+ }
3174
+ return typeof value === "bigint" || Number.isFinite(value) && Number.isSafeInteger(value);
3175
+ }
3176
+ function isValidCommandRecord(command) {
3177
+ const type = Number(command.type);
3178
+ if (!Number.isSafeInteger(type) || type < 0 /* SetParam */ || type > 17 /* SeekMarker */) {
3179
+ return false;
3180
+ }
3181
+ return (command.targetId === void 0 || Number.isSafeInteger(command.targetId)) && (command.argFloat === void 0 || Number.isFinite(command.argFloat)) && isFiniteInteger(command.argInt) && isFiniteInteger(command.sampleTime);
3182
+ }
3183
+ var AUDIO_WORKLET_RENDER_QUANTUM = 128;
3184
+ function workletBlockSize(blockSize) {
3185
+ const resolved = blockSize ?? AUDIO_WORKLET_RENDER_QUANTUM;
3186
+ if (!Number.isSafeInteger(resolved) || resolved < AUDIO_WORKLET_RENDER_QUANTUM) {
3187
+ throw new RangeError(
3188
+ `blockSize must be an integer of at least ${AUDIO_WORKLET_RENDER_QUANTUM} frames`
3189
+ );
3190
+ }
3191
+ return resolved;
3192
+ }
2509
3193
  var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2510
- constructor(node, capabilities, commandRing, telemetryRing, meterRing, scopeRing) {
3194
+ constructor(node, capabilities, commandRing, telemetryRing, meterRing, scopeRing, clipPageRequestRing, externalMidiRing) {
2511
3195
  this.telemetryReadIndex = 0;
2512
3196
  this.meterReadIndex = 0;
2513
3197
  this.scopeReadIndex = 0;
3198
+ this.clipPageRequestDroppedRead = 0;
2514
3199
  this.telemetryListeners = /* @__PURE__ */ new Set();
2515
3200
  this.meterListeners = /* @__PURE__ */ new Set();
2516
3201
  this.scopeListeners = /* @__PURE__ */ new Set();
2517
3202
  this.midiOutListeners = /* @__PURE__ */ new Set();
3203
+ this.clipPageRequestListeners = /* @__PURE__ */ new Set();
3204
+ this.syncErrorListeners = /* @__PURE__ */ new Set();
2518
3205
  this.captureRequestId = 1;
2519
3206
  this.captureRequests = /* @__PURE__ */ new Map();
2520
3207
  this.transportRequestId = 1;
@@ -2526,6 +3213,8 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2526
3213
  this.telemetryRing = telemetryRing;
2527
3214
  this.meterRing = meterRing;
2528
3215
  this.scopeRing = scopeRing;
3216
+ this.clipPageRequestRing = clipPageRequestRing;
3217
+ this.externalMidiRing = externalMidiRing;
2529
3218
  this.ready = new Promise((resolve, reject) => {
2530
3219
  this.resolveReady = resolve;
2531
3220
  this.rejectReady = reject;
@@ -2560,6 +3249,17 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2560
3249
  this.emitMeter(event.data);
2561
3250
  } else if (isExternalMidiBatchMessage(event.data)) {
2562
3251
  this.emitMidiOut(event.data.events);
3252
+ } else if (isClipPageRequestMessage(event.data)) {
3253
+ this.emitClipPageRequests(event.data);
3254
+ } else if (isRecord(event.data) && event.data.type === "syncError") {
3255
+ const syncError = {
3256
+ type: "syncError",
3257
+ syncType: String(event.data.syncType),
3258
+ message: String(event.data.message ?? "AudioWorklet sync failed")
3259
+ };
3260
+ for (const listener of this.syncErrorListeners) {
3261
+ listener(syncError);
3262
+ }
2563
3263
  } else if (isRecord(event.data) && event.data.type === "ready") {
2564
3264
  this.resolveReady();
2565
3265
  } else if (isRecord(event.data) && event.data.type === "error") {
@@ -2567,7 +3267,13 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2567
3267
  }
2568
3268
  };
2569
3269
  }
3270
+ /** Subscribes to control-plane sync rejections reported by the worklet. */
3271
+ onSyncError(listener) {
3272
+ this.syncErrorListeners.add(listener);
3273
+ return () => this.syncErrorListeners.delete(listener);
3274
+ }
2570
3275
  static async create(context, options = {}) {
3276
+ const blockSize = workletBlockSize(options.blockSize);
2571
3277
  const processorName = options.processorName ?? "sonare-realtime-engine-processor";
2572
3278
  const moduleUrl = options.moduleUrl;
2573
3279
  if (moduleUrl && context.audioWorklet?.addModule) {
@@ -2598,10 +3304,12 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2598
3304
  const meterRing = mode === "sab" ? createSonareMeterRingBuffer(options.meterRingCapacity ?? 128) : void 0;
2599
3305
  const scopeIntervalFrames = Math.max(0, Math.floor(options.scopeIntervalFrames ?? 0));
2600
3306
  const scopeRing = mode === "sab" && scopeIntervalFrames > 0 ? createSonareScopeRingBuffer(options.scopeRingCapacity ?? 64, options.scopeBands ?? 48) : void 0;
3307
+ const clipPageRequestRing = mode === "sab" ? createSonareClipPageRequestRingBuffer(options.clipPageRequestRingCapacity ?? 128) : void 0;
3308
+ const externalMidiRing = mode === "sab" ? createSonareExternalMidiRingBuffer(options.externalMidiRingCapacity ?? 256) : void 0;
2601
3309
  const channelCount = Math.max(1, Math.floor(options.channelCount ?? 2));
2602
3310
  const processorOptions = {
2603
3311
  sampleRate: options.sampleRate ?? context.sampleRate,
2604
- blockSize: options.blockSize,
3312
+ blockSize,
2605
3313
  channelCount,
2606
3314
  commandSharedBuffer: commandRing?.sharedBuffer,
2607
3315
  commandRingCapacity: commandRing?.capacity,
@@ -2613,6 +3321,10 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2613
3321
  scopeRingCapacity: scopeRing?.capacity,
2614
3322
  scopeBands: scopeRing?.bands,
2615
3323
  scopeIntervalFrames: scopeRing ? scopeIntervalFrames : void 0,
3324
+ clipPageRequestSharedBuffer: clipPageRequestRing?.sharedBuffer,
3325
+ clipPageRequestRingCapacity: clipPageRequestRing?.capacity,
3326
+ externalMidiSharedBuffer: externalMidiRing?.sharedBuffer,
3327
+ externalMidiRingCapacity: externalMidiRing?.capacity,
2616
3328
  wasmBinary: options.wasmBinary,
2617
3329
  initialSyncMessages: options.initialSyncMessages,
2618
3330
  initialCommands: options.initialCommands
@@ -2632,16 +3344,23 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2632
3344
  sharedArrayBuffer,
2633
3345
  atomics,
2634
3346
  audioWorklet,
3347
+ clipPageRequestsRealtimeSafe: mode === "sab",
3348
+ externalMidiRealtimeSafe: mode === "sab",
2635
3349
  engineAbiVersion: detectedCapabilities?.engineAbiVersion,
2636
3350
  expectedEngineAbiVersion: detectedCapabilities?.expectedEngineAbiVersion,
2637
3351
  abiCompatible: detectedCapabilities?.abiCompatible,
2638
3352
  degradedReason,
2639
- readyMessage: moduleUrl !== void 0 && !options.nodeFactory
3353
+ // A processor posts ready/error irrespective of whether its module was
3354
+ // loaded here or registered by the host beforehand. Waiting in both
3355
+ // cases is the only way to surface a failed worklet-side WASM init.
3356
+ readyMessage: true
2640
3357
  },
2641
3358
  commandRing,
2642
3359
  telemetryRing,
2643
3360
  meterRing,
2644
- scopeRing
3361
+ scopeRing,
3362
+ clipPageRequestRing,
3363
+ externalMidiRing
2645
3364
  );
2646
3365
  }
2647
3366
  play(sampleTime = -1) {
@@ -2658,6 +3377,9 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2658
3377
  });
2659
3378
  }
2660
3379
  seekPpq(ppq, sampleTime = -1) {
3380
+ if (!Number.isFinite(ppq) || !Number.isSafeInteger(sampleTime)) {
3381
+ return false;
3382
+ }
2661
3383
  return this.sendCommand({
2662
3384
  type: 5 /* TransportSeekPpq */,
2663
3385
  sampleTime,
@@ -2665,7 +3387,7 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2665
3387
  });
2666
3388
  }
2667
3389
  sendCommand(command) {
2668
- if (this.destroyed) {
3390
+ if (this.destroyed || !isValidCommandRecord(command)) {
2669
3391
  return false;
2670
3392
  }
2671
3393
  if (this.commandRing) {
@@ -2739,22 +3461,73 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2739
3461
  }
2740
3462
  return read.scopes;
2741
3463
  }
3464
+ /** Drain lowered MIDI-1 records from the SAB ring on the main thread. */
3465
+ pollMidiOut() {
3466
+ if (!this.externalMidiRing) {
3467
+ return [];
3468
+ }
3469
+ const read = readSonareExternalMidiRingBuffer(this.externalMidiRing);
3470
+ const events = read.events.map((event) => {
3471
+ const bytes = [];
3472
+ for (let index = 0; index < event.byteCount; index++) {
3473
+ bytes.push(event.byteWord >>> 8 * index & 255);
3474
+ }
3475
+ return {
3476
+ destinationId: event.destinationId,
3477
+ renderFrame: event.renderFrame,
3478
+ bytes
3479
+ };
3480
+ });
3481
+ if (events.length > 0) {
3482
+ this.emitMidiOut(events);
3483
+ }
3484
+ return events;
3485
+ }
3486
+ /**
3487
+ * Drains bounded paged-clip misses from the SAB ring and forwards one batch
3488
+ * to subscribers. In postMessage mode the legacy handler remains available,
3489
+ * but that degraded path is not realtime-safe for OPFS streaming.
3490
+ */
3491
+ pollClipPageRequests() {
3492
+ if (!this.clipPageRequestRing) {
3493
+ return void 0;
3494
+ }
3495
+ const read = readSonareClipPageRequestRingBuffer(this.clipPageRequestRing);
3496
+ const dropped = read.dropped - this.clipPageRequestDroppedRead >>> 0;
3497
+ this.clipPageRequestDroppedRead = read.dropped;
3498
+ if (read.requests.length === 0 && dropped === 0) {
3499
+ return void 0;
3500
+ }
3501
+ const message = {
3502
+ type: "clipPageRequest",
3503
+ requests: read.requests,
3504
+ ...dropped > 0 ? { dropped } : {}
3505
+ };
3506
+ this.emitClipPageRequests(message);
3507
+ return message;
3508
+ }
2742
3509
  onTelemetry(callback) {
2743
3510
  this.telemetryListeners.add(callback);
3511
+ this.startRingPolling();
2744
3512
  return () => {
2745
3513
  this.telemetryListeners.delete(callback);
3514
+ this.stopRingPollingIfUnused();
2746
3515
  };
2747
3516
  }
2748
3517
  onMeter(callback) {
2749
3518
  this.meterListeners.add(callback);
3519
+ this.startRingPolling();
2750
3520
  return () => {
2751
3521
  this.meterListeners.delete(callback);
3522
+ this.stopRingPollingIfUnused();
2752
3523
  };
2753
3524
  }
2754
3525
  onScope(callback) {
2755
3526
  this.scopeListeners.add(callback);
3527
+ this.startRingPolling();
2756
3528
  return () => {
2757
3529
  this.scopeListeners.delete(callback);
3530
+ this.stopRingPollingIfUnused();
2758
3531
  };
2759
3532
  }
2760
3533
  /**
@@ -2764,16 +3537,31 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2764
3537
  */
2765
3538
  onMidiOut(callback) {
2766
3539
  this.midiOutListeners.add(callback);
3540
+ this.startRingPolling();
2767
3541
  return () => {
2768
3542
  this.midiOutListeners.delete(callback);
2769
3543
  };
2770
3544
  }
3545
+ /**
3546
+ * Subscribe to bounded batches of paged-clip misses from the worklet. The
3547
+ * callback runs on the main thread and may initiate asynchronous OPFS I/O.
3548
+ */
3549
+ onClipPageRequests(callback) {
3550
+ this.clipPageRequestListeners.add(callback);
3551
+ return () => {
3552
+ this.clipPageRequestListeners.delete(callback);
3553
+ };
3554
+ }
2771
3555
  destroy() {
2772
3556
  if (this.destroyed) {
2773
3557
  return;
2774
3558
  }
2775
3559
  this.destroyed = true;
2776
- this.node.port.postMessage({ type: 3 /* TransportStop */, sampleTime: -1 });
3560
+ if (this.ringPollTimer !== void 0) {
3561
+ clearInterval(this.ringPollTimer);
3562
+ this.ringPollTimer = void 0;
3563
+ }
3564
+ this.node.port.postMessage({ type: "destroy" });
2777
3565
  this.node.disconnect();
2778
3566
  for (const pending of this.captureRequests.values()) {
2779
3567
  pending.reject(new Error("Realtime engine node is destroyed."));
@@ -2787,12 +3575,34 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2787
3575
  this.meterListeners.clear();
2788
3576
  this.scopeListeners.clear();
2789
3577
  this.midiOutListeners.clear();
3578
+ this.clipPageRequestListeners.clear();
2790
3579
  }
2791
3580
  emitTelemetry(telemetry) {
2792
3581
  for (const listener of this.telemetryListeners) {
2793
3582
  listener(telemetry);
2794
3583
  }
2795
3584
  }
3585
+ startRingPolling() {
3586
+ if (this.ringPollTimer !== void 0 || !this.telemetryRing && !this.meterRing && !this.scopeRing && !this.externalMidiRing) {
3587
+ return;
3588
+ }
3589
+ const poll = () => {
3590
+ if (!this.destroyed) {
3591
+ this.pollTelemetry();
3592
+ this.pollMeters();
3593
+ this.pollScope();
3594
+ this.pollMidiOut();
3595
+ }
3596
+ };
3597
+ poll();
3598
+ this.ringPollTimer = setInterval(poll, 16);
3599
+ }
3600
+ stopRingPollingIfUnused() {
3601
+ if (this.ringPollTimer !== void 0 && this.telemetryListeners.size === 0 && this.meterListeners.size === 0 && this.scopeListeners.size === 0 && this.midiOutListeners.size === 0) {
3602
+ clearInterval(this.ringPollTimer);
3603
+ this.ringPollTimer = void 0;
3604
+ }
3605
+ }
2796
3606
  emitMeter(meter) {
2797
3607
  for (const listener of this.meterListeners) {
2798
3608
  listener(meter);
@@ -2803,6 +3613,11 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2803
3613
  listener(events);
2804
3614
  }
2805
3615
  }
3616
+ emitClipPageRequests(message) {
3617
+ for (const listener of this.clipPageRequestListeners) {
3618
+ listener(message);
3619
+ }
3620
+ }
2806
3621
  emitScope(scope) {
2807
3622
  for (const listener of this.scopeListeners) {
2808
3623
  listener(scope);
@@ -2836,7 +3651,7 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
2836
3651
  function setParam(ctx, nodeId, param, value) {
2837
3652
  const paramId = ctx.resolveParamId(nodeId, param);
2838
3653
  ctx.offlineEngine.setParameter(paramId, value);
2839
- return ctx.realtimeNode.sendCommand({
3654
+ return ctx.sendCommand({
2840
3655
  type: 0 /* SetParam */,
2841
3656
  targetId: paramId,
2842
3657
  sampleTime: -1,
@@ -2846,7 +3661,7 @@ function setParam(ctx, nodeId, param, value) {
2846
3661
  function setSoloMute(ctx, target, solo, mute) {
2847
3662
  const laneIndex = ctx.ensureTrackLane(target);
2848
3663
  ctx.offlineEngine.setSoloMute(laneIndex, solo, mute);
2849
- return ctx.realtimeNode.sendCommand({
3664
+ return ctx.sendCommand({
2850
3665
  type: 10 /* SetSoloMute */,
2851
3666
  targetId: laneIndex,
2852
3667
  sampleTime: -1,
@@ -2888,6 +3703,15 @@ function listParameters(ctx) {
2888
3703
  }
2889
3704
  return parameters;
2890
3705
  }
3706
+ function addParameter(ctx, info) {
3707
+ ctx.offlineEngine.addParameter(info);
3708
+ ctx.postSync({ type: "syncParameters", parameters: listParameters(ctx) });
3709
+ }
3710
+ function clearParameters(ctx) {
3711
+ ctx.offlineEngine.clearParameters();
3712
+ ctx.automationLanes.clear();
3713
+ ctx.postSync({ type: "syncParameters", parameters: [] });
3714
+ }
2891
3715
 
2892
3716
  // src/worklet/engine-strips.ts
2893
3717
  function trackIdFor(ctx, target) {
@@ -3002,6 +3826,11 @@ function pushMidiCc(ctx, trackId, group, channel, controller, value, renderFrame
3002
3826
  renderFrame
3003
3827
  });
3004
3828
  }
3829
+ function pushMidiUmp(ctx, trackId, word0, renderFrame) {
3830
+ const destinationId = ctx.resolveTargetId(trackId);
3831
+ ctx.offlineEngine.pushMidiUmp(destinationId, word0, renderFrame);
3832
+ ctx.postSync({ type: "syncMidiUmp", destinationId, word0, renderFrame });
3833
+ }
3005
3834
  function setBuiltinInstrument(ctx, trackId, config) {
3006
3835
  const destinationId = ctx.resolveTargetId(trackId);
3007
3836
  ctx.offlineEngine.setBuiltinInstrument(config, destinationId);
@@ -3062,11 +3891,6 @@ function setTempo(ctx, bpm) {
3062
3891
  ctx.setTempoSegments([{ startPpq: 0, bpm }]);
3063
3892
  ctx.offlineEngine.setTempo(bpm);
3064
3893
  postTempoSync(ctx);
3065
- ctx.realtimeNode.sendCommand({
3066
- type: 6 /* SetTempoMap */,
3067
- sampleTime: -1,
3068
- argFloat: bpm
3069
- });
3070
3894
  }
3071
3895
  function setTempoSegments(ctx, segments) {
3072
3896
  const copied = segments.map((segment) => ({ ...segment }));
@@ -3093,7 +3917,7 @@ function setTimeSignatureSegments(ctx, segments) {
3093
3917
  }
3094
3918
  function setLoop(ctx, startPpq, endPpq, enabled = true) {
3095
3919
  ctx.offlineEngine.setLoop(startPpq, endPpq, enabled);
3096
- return ctx.realtimeNode.sendCommand({
3920
+ return ctx.sendCommand({
3097
3921
  type: 7 /* SetLoop */,
3098
3922
  targetId: enabled ? 1 : 0,
3099
3923
  sampleTime: -1,
@@ -3114,6 +3938,19 @@ function cachedTransportState(ctx) {
3114
3938
  }
3115
3939
 
3116
3940
  // src/worklet/engine.ts
3941
+ var MAX_PENDING_WORKLET_CLIP_PAGE_REQUESTS = 256;
3942
+ function transferableAudioBuffers(channels) {
3943
+ const transfers = [];
3944
+ const seen = /* @__PURE__ */ new Set();
3945
+ for (const channel of channels) {
3946
+ const buffer = channel.buffer;
3947
+ if (buffer instanceof ArrayBuffer && !seen.has(buffer)) {
3948
+ seen.add(buffer);
3949
+ transfers.push(buffer);
3950
+ }
3951
+ }
3952
+ return transfers;
3953
+ }
3117
3954
  var SonareEngine = class _SonareEngine {
3118
3955
  constructor(context, realtimeNode, offlineEngine, sampleRate, offlineBlockSize, offlineChannelCount) {
3119
3956
  this.automationLanes = /* @__PURE__ */ new Map();
@@ -3137,6 +3974,11 @@ var SonareEngine = class _SonareEngine {
3137
3974
  this.nextMarkerId = 1;
3138
3975
  this.transportPlaying = false;
3139
3976
  this.pendingInstrumentSync = [];
3977
+ // One latest frontier per clip is sufficient: ClipPageStreamer expands it to
3978
+ // the bounded read window. A Map both coalesces repeated page misses and
3979
+ // places a hard cap on work queued while OPFS I/O is stalled.
3980
+ this.workletClipPageRequests = /* @__PURE__ */ new Map();
3981
+ this.workletPageProviderClipIds = /* @__PURE__ */ new Map();
3140
3982
  this.destroyed = false;
3141
3983
  this.context = context;
3142
3984
  this.realtimeNode = realtimeNode;
@@ -3146,10 +3988,17 @@ var SonareEngine = class _SonareEngine {
3146
3988
  this.sampleRate = sampleRate;
3147
3989
  this.offlineBlockSize = offlineBlockSize;
3148
3990
  this.offlineChannelCount = offlineChannelCount;
3991
+ this.unsubscribeWorkletClipRequests = this.realtimeNode.onClipPageRequests((message) => {
3992
+ for (const request of message.requests) {
3993
+ this.enqueueWorkletClipPageRequest(request);
3994
+ }
3995
+ this.pumpWorkletClipPages();
3996
+ });
3149
3997
  this.transport = buildTransportFacade({
3150
3998
  sampleRate: this.sampleRate,
3151
3999
  realtimeNode: this.realtimeNode,
3152
4000
  offlineEngine: this.offlineEngine,
4001
+ flushOfflineMirror: () => this.flushOfflineMirror(),
3153
4002
  setTransportPlaying: (playing) => {
3154
4003
  this.transportPlaying = playing;
3155
4004
  },
@@ -3167,8 +4016,14 @@ var SonareEngine = class _SonareEngine {
3167
4016
  Math.floor(options.offlineChannelCount ?? options.channelCount ?? 2)
3168
4017
  );
3169
4018
  const realtimeNode = await SonareRealtimeEngineNode.create(context, options);
4019
+ try {
4020
+ await realtimeNode.ready;
4021
+ } catch (error) {
4022
+ realtimeNode.destroy();
4023
+ throw error;
4024
+ }
3170
4025
  const offlineEngine = options.offlineEngine ?? new RealtimeEngine(sampleRate, blockSize);
3171
- return new _SonareEngine(
4026
+ const engine = new _SonareEngine(
3172
4027
  context,
3173
4028
  realtimeNode,
3174
4029
  offlineEngine,
@@ -3176,6 +4031,8 @@ var SonareEngine = class _SonareEngine {
3176
4031
  blockSize,
3177
4032
  channelCount
3178
4033
  );
4034
+ engine.syncParameters();
4035
+ return engine;
3179
4036
  }
3180
4037
  async suspend() {
3181
4038
  if (this.destroyed) {
@@ -3313,6 +4170,14 @@ var SonareEngine = class _SonareEngine {
3313
4170
  listParameters() {
3314
4171
  return listParameters(this.parameterContext);
3315
4172
  }
4173
+ /** Registers a custom parameter on the offline mirror and worklet engine. */
4174
+ addParameter(info) {
4175
+ addParameter(this.parameterContext, info);
4176
+ }
4177
+ /** Clears custom parameters and their automation lanes on both engines. */
4178
+ clearParameters() {
4179
+ clearParameters(this.parameterContext);
4180
+ }
3316
4181
  setSoloMute(target, solo, mute) {
3317
4182
  return setSoloMute(this.parameterContext, target, solo, mute);
3318
4183
  }
@@ -3448,6 +4313,76 @@ var SonareEngine = class _SonareEngine {
3448
4313
  setMasterChain(sceneJson) {
3449
4314
  this.setMasterStripJson(sceneJson);
3450
4315
  }
4316
+ /**
4317
+ * Creates and primes an OPFS-backed page provider for this live worklet
4318
+ * engine. Pass the returned `provider` to {@link addClip} in place of a
4319
+ * `Float32Array[]`; the `clipId` in `options` must equal that clip's explicit
4320
+ * `opts.id`. Subsequent cache misses are fetched on the main thread and
4321
+ * supplied back to the worklet through its bounded pull protocol.
4322
+ */
4323
+ async attachOpfsClipStream(options) {
4324
+ if (this.destroyed) {
4325
+ throw new Error("SonareEngine is destroyed.");
4326
+ }
4327
+ if (!this.capabilities.clipPageRequestsRealtimeSafe) {
4328
+ throw new Error(
4329
+ "OPFS clip streaming requires SharedArrayBuffer clip-page requests; the postMessage fallback is not realtime-safe."
4330
+ );
4331
+ }
4332
+ const { clipId, primePages = 1, ...providerOptions } = options;
4333
+ if ([...this.workletPageProviderClipIds.values()].includes(clipId)) {
4334
+ throw new Error(`An OPFS stream is already attached for clip ${clipId}.`);
4335
+ }
4336
+ const streamer = this.ensureWorkletClipStreamer();
4337
+ this.postSync({
4338
+ type: "syncClipPageProvider",
4339
+ clipId,
4340
+ numChannels: providerOptions.numChannels,
4341
+ numSamples: providerOptions.numSamples,
4342
+ pageFrames: providerOptions.pageFrames
4343
+ });
4344
+ let binding;
4345
+ binding = createOpfsClipPageProvider(this.offlineEngine, {
4346
+ ...providerOptions,
4347
+ onPageSupplied: (pageIndex, channels) => {
4348
+ this.postSync(
4349
+ { type: "syncClipPage", clipId, pageIndex, channels },
4350
+ transferableAudioBuffers(channels)
4351
+ );
4352
+ },
4353
+ onPageCleared: (pageIndex) => {
4354
+ this.postSync({ type: "syncClipPageClear", clipId, pageIndex });
4355
+ },
4356
+ onClose: () => {
4357
+ this.workletPageProviderClipIds.delete(binding.provider.id);
4358
+ this.postSync({ type: "syncClipPageDestroy", clipId });
4359
+ }
4360
+ });
4361
+ this.workletPageProviderClipIds.set(binding.provider.id, clipId);
4362
+ const lastPage = Math.ceil(providerOptions.numSamples / providerOptions.pageFrames) - 1;
4363
+ const primed = [];
4364
+ try {
4365
+ for (let page = 0; page < primePages && page <= lastPage; ++page) {
4366
+ if (await binding.supplyPage(page)) {
4367
+ primed.push(page);
4368
+ }
4369
+ }
4370
+ streamer.addSource(
4371
+ {
4372
+ clipId,
4373
+ binding,
4374
+ pageFrames: providerOptions.pageFrames,
4375
+ numSamples: providerOptions.numSamples
4376
+ },
4377
+ primed
4378
+ );
4379
+ this.startWorkletClipPagePolling();
4380
+ } catch (error) {
4381
+ binding.close();
4382
+ throw error;
4383
+ }
4384
+ return { binding, provider: binding.provider };
4385
+ }
3451
4386
  addClip(trackId, buffer, startPpq, opts = {}) {
3452
4387
  return addClip(this.clipContext, trackId, buffer, startPpq, opts);
3453
4388
  }
@@ -3509,9 +4444,59 @@ var SonareEngine = class _SonareEngine {
3509
4444
  pushMidiCc(trackId, group, channel, controller, value, renderFrame = -1) {
3510
4445
  pushMidiCc(this.stripContext, trackId, group, channel, controller, value, renderFrame);
3511
4446
  }
4447
+ pushMidiUmp(trackId, word0, renderFrame = -1) {
4448
+ pushMidiUmp(this.stripContext, trackId, word0, renderFrame);
4449
+ }
3512
4450
  pushMidiSysex(trackId, data, renderFrame = -1) {
3513
4451
  pushMidiSysex(this.stripContext, trackId, data, renderFrame);
3514
4452
  }
4453
+ bindMidiCc(channel, controller, paramId, options = {}) {
4454
+ const minValue = options.minValue ?? 0;
4455
+ const maxValue = options.maxValue ?? 1;
4456
+ this.offlineEngine.bindMidiCc(channel, controller, paramId, { minValue, maxValue });
4457
+ this.postSync({ type: "syncMidiCcBinding", channel, controller, paramId, minValue, maxValue });
4458
+ }
4459
+ setMidiInputSource(destinationId = 0) {
4460
+ this.offlineEngine.setMidiInputSource(destinationId);
4461
+ this.postSync({ type: "syncMidiInputSource", destinationId });
4462
+ }
4463
+ clearMidiInputSource() {
4464
+ this.offlineEngine.clearMidiInputSource();
4465
+ this.postSync({ type: "syncClearMidiInputSource" });
4466
+ }
4467
+ pushMidiInputNoteOn(group, channel, note, velocity, portTimeSamples = 0) {
4468
+ this.offlineEngine.pushMidiInputNoteOn(group, channel, note, velocity, portTimeSamples);
4469
+ this.postSync({
4470
+ type: "syncMidiInputNoteOn",
4471
+ group,
4472
+ channel,
4473
+ data0: note,
4474
+ data1: velocity,
4475
+ portTimeSamples
4476
+ });
4477
+ }
4478
+ pushMidiInputNoteOff(group, channel, note, velocity = 0, portTimeSamples = 0) {
4479
+ this.offlineEngine.pushMidiInputNoteOff(group, channel, note, velocity, portTimeSamples);
4480
+ this.postSync({
4481
+ type: "syncMidiInputNoteOff",
4482
+ group,
4483
+ channel,
4484
+ data0: note,
4485
+ data1: velocity,
4486
+ portTimeSamples
4487
+ });
4488
+ }
4489
+ pushMidiInputCc(group, channel, controller, value, portTimeSamples = 0) {
4490
+ this.offlineEngine.pushMidiInputCc(group, channel, controller, value, portTimeSamples);
4491
+ this.postSync({
4492
+ type: "syncMidiInputCc",
4493
+ group,
4494
+ channel,
4495
+ data0: controller,
4496
+ data1: value,
4497
+ portTimeSamples
4498
+ });
4499
+ }
3515
4500
  pushMidiPanic(renderFrame = -1) {
3516
4501
  this.offlineEngine.pushMidiPanic(renderFrame);
3517
4502
  this.postSync({ type: "syncMidiPanic", renderFrame });
@@ -3519,6 +4504,10 @@ var SonareEngine = class _SonareEngine {
3519
4504
  configureCapture(options) {
3520
4505
  configureCapture(this.captureContext, options);
3521
4506
  }
4507
+ /**
4508
+ * Arms the engine-global capture path. `trackId` is retained for source
4509
+ * compatibility and must be `0`; per-track capture is not implemented.
4510
+ */
3522
4511
  armRecord(trackId, enabled) {
3523
4512
  return armRecord(this.captureContext, trackId, enabled);
3524
4513
  }
@@ -3537,7 +4526,7 @@ var SonareEngine = class _SonareEngine {
3537
4526
  setMetronome(opts) {
3538
4527
  this.offlineEngine.setMetronome(opts);
3539
4528
  this.postSync({ type: "syncMetronome", config: opts });
3540
- this.realtimeNode.sendCommand({
4529
+ this.sendMirroredCommand({
3541
4530
  type: 15 /* SetMetronome */,
3542
4531
  sampleTime: -1,
3543
4532
  argInt: opts.enabled ? 1 : 0
@@ -3607,12 +4596,101 @@ var SonareEngine = class _SonareEngine {
3607
4596
  if (this.destroyed) {
3608
4597
  return;
3609
4598
  }
4599
+ this.unsubscribeWorkletClipRequests?.();
4600
+ this.unsubscribeWorkletClipRequests = void 0;
4601
+ this.workletClipStreamer?.close();
4602
+ this.workletClipStreamer = void 0;
4603
+ if (this.workletClipPagePollTimer !== void 0) {
4604
+ clearInterval(this.workletClipPagePollTimer);
4605
+ this.workletClipPagePollTimer = void 0;
4606
+ }
4607
+ this.workletClipPageRequests.clear();
3610
4608
  this.destroyed = true;
3611
4609
  this.transport.stop();
3612
4610
  this.realtimeNode.pollTelemetry();
3613
4611
  this.realtimeNode.destroy();
3614
4612
  this.offlineEngine.destroy();
3615
4613
  }
4614
+ ensureWorkletClipStreamer() {
4615
+ if (!this.workletClipStreamer) {
4616
+ this.workletClipStreamer = new ClipPageStreamer({
4617
+ popClipPageRequest: () => this.popWorkletClipPageRequest()
4618
+ });
4619
+ }
4620
+ return this.workletClipStreamer;
4621
+ }
4622
+ /**
4623
+ * SAB requests have no postMessage wake-up by design. Polling on the main
4624
+ * thread is therefore intentionally outside the audio callback; 8 ms keeps
4625
+ * the bounded OPFS prefetch frontier responsive without adding worklet GC.
4626
+ */
4627
+ startWorkletClipPagePolling() {
4628
+ if (this.workletClipPagePollTimer !== void 0) {
4629
+ return;
4630
+ }
4631
+ const poll = () => {
4632
+ if (!this.destroyed) {
4633
+ this.realtimeNode.pollClipPageRequests();
4634
+ }
4635
+ };
4636
+ poll();
4637
+ this.workletClipPagePollTimer = setInterval(poll, 8);
4638
+ }
4639
+ pumpWorkletClipPages() {
4640
+ const streamer = this.workletClipStreamer;
4641
+ if (!streamer || this.workletClipPump) {
4642
+ return;
4643
+ }
4644
+ this.workletClipPump = streamer.pump().catch((error) => {
4645
+ console.error("Sonare OPFS clip-page supply failed:", error);
4646
+ }).finally(() => {
4647
+ this.workletClipPump = void 0;
4648
+ if (this.workletClipPageRequests.size > 0) {
4649
+ this.pumpWorkletClipPages();
4650
+ }
4651
+ });
4652
+ }
4653
+ enqueueWorkletClipPageRequest(request) {
4654
+ if (!Number.isInteger(request.clipId) || request.clipId < 0 || !Number.isInteger(request.pageIndex) || (request.pageIndex ?? -1) < 0) {
4655
+ return;
4656
+ }
4657
+ this.workletClipPageRequests.delete(request.clipId);
4658
+ if (this.workletClipPageRequests.size >= MAX_PENDING_WORKLET_CLIP_PAGE_REQUESTS) {
4659
+ const oldest = this.workletClipPageRequests.keys().next().value;
4660
+ if (oldest !== void 0) {
4661
+ this.workletClipPageRequests.delete(oldest);
4662
+ }
4663
+ }
4664
+ this.workletClipPageRequests.set(request.clipId, request);
4665
+ }
4666
+ popWorkletClipPageRequest() {
4667
+ const entry = this.workletClipPageRequests.entries().next().value;
4668
+ if (!entry) {
4669
+ return null;
4670
+ }
4671
+ const [clipId, request] = entry;
4672
+ this.workletClipPageRequests.delete(clipId);
4673
+ return request;
4674
+ }
4675
+ commitWorkletClipPageProvider(clip) {
4676
+ const providerId = typeof clip.pageProvider === "object" && clip.pageProvider !== null ? clip.pageProvider.id : clip.pageProvider;
4677
+ if (providerId === void 0) {
4678
+ return false;
4679
+ }
4680
+ const clipId = this.workletPageProviderClipIds.get(providerId);
4681
+ if (clipId === void 0) {
4682
+ return false;
4683
+ }
4684
+ if (clip.id !== clipId) {
4685
+ throw new Error(`OPFS stream clipId ${clipId} must match addClip(..., { id: ${clipId} }).`);
4686
+ }
4687
+ this.postSync({
4688
+ type: "syncClipPageCommit",
4689
+ clipId,
4690
+ clip: { ...clip, channels: void 0, pageProvider: void 0 }
4691
+ });
4692
+ return true;
4693
+ }
3616
4694
  mixerLanes() {
3617
4695
  return mixerLanes(this.mixerContext);
3618
4696
  }
@@ -3645,12 +4723,27 @@ var SonareEngine = class _SonareEngine {
3645
4723
  if (this.destroyed) {
3646
4724
  return;
3647
4725
  }
3648
- if (transfer && transfer.length > 0) {
3649
- this.realtimeNode.node.port.postMessage(message, transfer);
3650
- } else {
3651
- this.realtimeNode.node.port.postMessage(message);
4726
+ try {
4727
+ if (transfer && transfer.length > 0) {
4728
+ this.realtimeNode.node.port.postMessage(message, transfer);
4729
+ } else {
4730
+ this.realtimeNode.node.port.postMessage(message);
4731
+ }
4732
+ } finally {
4733
+ this.flushOfflineMirror();
3652
4734
  }
3653
4735
  }
4736
+ // The offline engine is a control-thread mirror. It has no render loop to
4737
+ // drain its command ring, so drain it after each control operation. Never
4738
+ // call this from the worklet/audio path: it is intentionally control-only.
4739
+ flushOfflineMirror() {
4740
+ this.offlineEngine.flushControlCommands();
4741
+ }
4742
+ sendMirroredCommand(command) {
4743
+ const accepted = this.realtimeNode.sendCommand(command);
4744
+ this.flushOfflineMirror();
4745
+ return accepted;
4746
+ }
3654
4747
  // Collaborator surface handed to the mixer/routing free functions so they can
3655
4748
  // mutate the routing stores (held by reference), mirror into the offline
3656
4749
  // engine, post mixer-sync messages, and declare lanes/buses without a
@@ -3705,13 +4798,13 @@ var SonareEngine = class _SonareEngine {
3705
4798
  return {
3706
4799
  offlineEngine: this.offlineEngine,
3707
4800
  realtimeNode: this.realtimeNode,
4801
+ sendCommand: (command) => this.sendMirroredCommand(command),
3708
4802
  offlineChannelCount: this.offlineChannelCount,
3709
4803
  postSync: (message) => this.postSync(message),
3710
4804
  getCaptureConfig: () => this.captureConfig,
3711
4805
  setCaptureConfig: (config) => {
3712
4806
  this.captureConfig = config;
3713
- },
3714
- resolveTargetId: (target) => this.resolveTargetId(target)
4807
+ }
3715
4808
  };
3716
4809
  }
3717
4810
  // Collaborator surface handed to the parameter / automation-id resolution
@@ -3721,7 +4814,9 @@ var SonareEngine = class _SonareEngine {
3721
4814
  get parameterContext() {
3722
4815
  return {
3723
4816
  offlineEngine: this.offlineEngine,
3724
- realtimeNode: this.realtimeNode,
4817
+ sendCommand: (command) => this.sendMirroredCommand(command),
4818
+ postSync: (message) => this.postSync(message),
4819
+ automationLanes: this.automationLanes,
3725
4820
  trackLaneIds: this.trackLaneIds,
3726
4821
  resolveParamId: (nodeId, param) => this.resolveParamId(nodeId, param),
3727
4822
  ensureTrackLane: (target) => this.ensureTrackLane(target),
@@ -3735,6 +4830,7 @@ var SonareEngine = class _SonareEngine {
3735
4830
  return {
3736
4831
  offlineEngine: this.offlineEngine,
3737
4832
  realtimeNode: this.realtimeNode,
4833
+ sendCommand: (command) => this.sendMirroredCommand(command),
3738
4834
  postSync: (message) => this.postSync(message),
3739
4835
  getTempoBpm: () => this.tempoBpm,
3740
4836
  setTempoBpm: (bpm) => {
@@ -3769,7 +4865,8 @@ var SonareEngine = class _SonareEngine {
3769
4865
  allocateClipId: () => this.nextClipId++,
3770
4866
  postSync: (message, transfer) => this.postSync(message, transfer),
3771
4867
  ensureTrackLane: (target) => this.ensureTrackLane(target),
3772
- resolveTargetId: (target) => this.resolveTargetId(target)
4868
+ resolveTargetId: (target) => this.resolveTargetId(target),
4869
+ commitWorkletClipPageProvider: (clip) => this.commitWorkletClipPageProvider(clip)
3773
4870
  };
3774
4871
  }
3775
4872
  // Collaborator surface handed to the marker free functions so they can mutate
@@ -3784,7 +4881,7 @@ var SonareEngine = class _SonareEngine {
3784
4881
  this.nextMarkerId = value;
3785
4882
  },
3786
4883
  postSync: (message) => this.postSync(message),
3787
- sendCommand: (command) => this.realtimeNode.sendCommand(command),
4884
+ sendCommand: (command) => this.sendMirroredCommand(command),
3788
4885
  setLoop: (startPpq, endPpq, enabled) => this.setLoop(startPpq, endPpq, enabled)
3789
4886
  };
3790
4887
  }
@@ -3800,7 +4897,7 @@ var SonareEngine = class _SonareEngine {
3800
4897
  // sample-accurate smoothed-param command to the realtime runtime.
3801
4898
  sendSmoothedParam(paramId, value) {
3802
4899
  this.offlineEngine.setParameter(paramId, value);
3803
- return this.realtimeNode.sendCommand({
4900
+ return this.sendMirroredCommand({
3804
4901
  type: 1 /* SetParamSmoothed */,
3805
4902
  targetId: paramId,
3806
4903
  sampleTime: -1,
@@ -3810,6 +4907,12 @@ var SonareEngine = class _SonareEngine {
3810
4907
  resolveParamId(nodeId, param) {
3811
4908
  return resolveParamId(this.listParameters(), nodeId, param);
3812
4909
  }
4910
+ syncParameters() {
4911
+ const parameters = this.listParameters();
4912
+ if (parameters.length > 0) {
4913
+ this.postSync({ type: "syncParameters", parameters });
4914
+ }
4915
+ }
3813
4916
  resolveTargetId(target) {
3814
4917
  return resolveTargetId(target);
3815
4918
  }
@@ -3845,14 +4948,20 @@ var SonareEngine = class _SonareEngine {
3845
4948
  var DEFAULT_METRONOME_CONFIG = {
3846
4949
  beatGain: 0.35,
3847
4950
  accentGain: 0.7,
3848
- clickSamples: 96
4951
+ clickSamples: 0,
4952
+ clickSeconds: 0
3849
4953
  };
3850
4954
  function resolveMetronomeConfig(config) {
3851
- return {
4955
+ const resolved = {
3852
4956
  beatGain: config.beatGain ?? DEFAULT_METRONOME_CONFIG.beatGain,
3853
4957
  accentGain: config.accentGain ?? DEFAULT_METRONOME_CONFIG.accentGain,
3854
- clickSamples: config.clickSamples ?? DEFAULT_METRONOME_CONFIG.clickSamples
4958
+ clickSamples: config.clickSamples ?? DEFAULT_METRONOME_CONFIG.clickSamples,
4959
+ clickSeconds: config.clickSeconds ?? DEFAULT_METRONOME_CONFIG.clickSeconds
3855
4960
  };
4961
+ if (!Number.isFinite(resolved.beatGain) || resolved.beatGain < 0 || !Number.isFinite(resolved.accentGain) || resolved.accentGain < 0 || !Number.isInteger(resolved.clickSamples) || resolved.clickSamples < 0 || !Number.isFinite(resolved.clickSeconds) || resolved.clickSeconds < 0 || resolved.clickSamples > 384e3 || config.clickSeconds !== void 0 && (!Number.isFinite(config.clickSeconds) || config.clickSeconds < 0 || config.clickSeconds > 1)) {
4962
+ throw new RangeError("invalid metronome gains or click length");
4963
+ }
4964
+ return resolved;
3856
4965
  }
3857
4966
 
3858
4967
  // src/worklet/engine-processor.ts
@@ -3865,7 +4974,15 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
3865
4974
  this.metronomeConfig = { ...DEFAULT_METRONOME_CONFIG };
3866
4975
  this.liveClips = /* @__PURE__ */ new Map();
3867
4976
  this.pagedClipProviders = /* @__PURE__ */ new Map();
4977
+ this.pagedClipPageFrames = /* @__PURE__ */ new Map();
3868
4978
  this.pendingPagedClips = /* @__PURE__ */ new Map();
4979
+ // The worklet drains at most this many distinct page misses per render
4980
+ // quantum. The fixed scalar buffers avoid retaining an unbounded queue on
4981
+ // the audio thread; any remaining native requests stay queued for the next
4982
+ // quantum and render silence meanwhile.
4983
+ this.clipPageRequestClipIds = new Float64Array(64);
4984
+ this.clipPageRequestPageIndices = new Float64Array(64);
4985
+ this.clipPageRequestOverflowReported = 0;
3869
4986
  this.sampleRate = options.sampleRate ?? 48e3;
3870
4987
  this.blockSize = options.blockSize ?? 128;
3871
4988
  this.channelCount = Math.max(1, Math.floor(options.channelCount ?? 2));
@@ -3882,7 +4999,21 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
3882
4999
  options.scopeRingCapacity,
3883
5000
  options.scopeBands
3884
5001
  ) : void 0;
3885
- this.engine = new RealtimeEngine(this.sampleRate, this.blockSize);
5002
+ this.clipPageRequestRing = options.clipPageRequestSharedBuffer ? clipPageRequestRingFromSharedBuffer(
5003
+ options.clipPageRequestSharedBuffer,
5004
+ options.clipPageRequestRingCapacity
5005
+ ) : void 0;
5006
+ this.externalMidiRing = options.externalMidiSharedBuffer ? externalMidiRingFromSharedBuffer(
5007
+ options.externalMidiSharedBuffer,
5008
+ options.externalMidiRingCapacity
5009
+ ) : void 0;
5010
+ this.engine = new RealtimeEngine(
5011
+ this.sampleRate,
5012
+ this.blockSize,
5013
+ 1024,
5014
+ 1024,
5015
+ this.channelCount
5016
+ );
3886
5017
  this.engine.prepareChannels(this.channelCount, this.blockSize);
3887
5018
  this.channelBuffers = new Array(this.channelCount);
3888
5019
  for (let ch = 0; ch < this.channelCount; ch++) {
@@ -3947,6 +5078,7 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
3947
5078
  target.fill(0);
3948
5079
  }
3949
5080
  }
5081
+ this.publishClipPageRequests();
3950
5082
  this.publishTelemetry();
3951
5083
  this.publishMeters();
3952
5084
  this.publishScope();
@@ -3960,7 +5092,7 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
3960
5092
  }
3961
5093
  receiveCommand(command) {
3962
5094
  if (!this.closed) {
3963
- this.applyCommand(command);
5095
+ this.safeApplyCommand(command);
3964
5096
  }
3965
5097
  }
3966
5098
  // Applies an out-of-band control-plane sync message on the AudioWorklet
@@ -3971,35 +5103,64 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
3971
5103
  if (this.closed) {
3972
5104
  return;
3973
5105
  }
5106
+ try {
5107
+ this.applySync(message);
5108
+ } catch (error) {
5109
+ this.transport?.postMessage?.({
5110
+ type: "syncError",
5111
+ syncType: message.type,
5112
+ message: error instanceof Error ? error.message : String(error)
5113
+ });
5114
+ }
5115
+ }
5116
+ applySync(message) {
3974
5117
  switch (message.type) {
3975
- case "syncClips":
5118
+ case "destroy":
5119
+ this.destroy();
5120
+ return;
5121
+ case "syncClips": {
5122
+ this.engine.setClips(message.clips);
5123
+ this.clearPagedClipProviders();
3976
5124
  this.liveClips.clear();
3977
5125
  for (const clip of message.clips) {
3978
5126
  if (clip.id !== void 0) {
3979
5127
  this.liveClips.set(clip.id, clip);
3980
5128
  }
3981
5129
  }
3982
- this.engine.setClips(message.clips);
3983
5130
  break;
3984
- case "syncClipsDelta":
5131
+ }
5132
+ case "syncClipsDelta": {
5133
+ const nextClips = new Map(this.liveClips);
3985
5134
  for (const clipId of message.removeIds) {
3986
- this.liveClips.delete(clipId);
5135
+ nextClips.delete(clipId);
3987
5136
  }
3988
5137
  for (const clip of message.upserts) {
3989
5138
  if (clip.id !== void 0) {
3990
- this.liveClips.set(clip.id, clip);
5139
+ nextClips.set(clip.id, clip);
3991
5140
  }
3992
5141
  }
3993
- this.engine.setClips(Array.from(this.liveClips.values()));
5142
+ this.engine.setClips(Array.from(nextClips.values()));
5143
+ for (const clipId of message.removeIds) {
5144
+ this.removePagedClipProvider(clipId);
5145
+ }
5146
+ this.liveClips.clear();
5147
+ for (const [clipId, clip] of nextClips) {
5148
+ this.liveClips.set(clipId, clip);
5149
+ }
3994
5150
  break;
5151
+ }
3995
5152
  case "syncClipPageProvider": {
3996
5153
  const provider = this.engine.createClipPageProvider(
3997
5154
  message.numChannels,
3998
5155
  message.numSamples,
3999
5156
  message.pageFrames
4000
5157
  );
5158
+ this.removePagedClipProvider(message.clipId);
4001
5159
  this.pagedClipProviders.set(message.clipId, provider.id);
4002
- this.pendingPagedClips.set(message.clipId, message.clip);
5160
+ this.pagedClipPageFrames.set(message.clipId, message.pageFrames);
5161
+ if (message.clip) {
5162
+ this.pendingPagedClips.set(message.clipId, message.clip);
5163
+ }
4003
5164
  break;
4004
5165
  }
4005
5166
  case "syncClipPage": {
@@ -4009,16 +5170,34 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
4009
5170
  }
4010
5171
  break;
4011
5172
  }
5173
+ case "syncClipPageClear": {
5174
+ const providerId = this.pagedClipProviders.get(message.clipId);
5175
+ if (providerId !== void 0) {
5176
+ this.engine.clearClipPage(providerId, message.pageIndex);
5177
+ }
5178
+ break;
5179
+ }
4012
5180
  case "syncClipPageCommit": {
4013
5181
  const providerId = this.pagedClipProviders.get(message.clipId);
4014
- const clip = this.pendingPagedClips.get(message.clipId);
5182
+ const clip = message.clip ?? this.pendingPagedClips.get(message.clipId);
4015
5183
  if (providerId !== void 0 && clip) {
4016
- this.liveClips.set(message.clipId, { ...clip, pageProvider: providerId });
5184
+ const nextClip = { ...clip, pageProvider: providerId };
5185
+ const nextClips = new Map(this.liveClips);
5186
+ nextClips.set(message.clipId, nextClip);
5187
+ this.engine.setClips(Array.from(nextClips.values()));
5188
+ this.liveClips.set(message.clipId, nextClip);
4017
5189
  this.pendingPagedClips.delete(message.clipId);
4018
- this.engine.setClips(Array.from(this.liveClips.values()));
4019
5190
  }
4020
5191
  break;
4021
5192
  }
5193
+ case "syncClipPageDestroy": {
5194
+ const nextClips = new Map(this.liveClips);
5195
+ nextClips.delete(message.clipId);
5196
+ this.engine.setClips(Array.from(nextClips.values()));
5197
+ this.liveClips.delete(message.clipId);
5198
+ this.removePagedClipProvider(message.clipId);
5199
+ break;
5200
+ }
4022
5201
  case "syncMidiClips":
4023
5202
  this.engine.setMidiClips(message.clips);
4024
5203
  break;
@@ -4026,12 +5205,21 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
4026
5205
  this.engine.setMarkers(message.markers);
4027
5206
  break;
4028
5207
  case "syncMetronome":
4029
- this.metronomeConfig = resolveMetronomeConfig(message.config);
4030
- this.engine.setMetronome(message.config);
5208
+ {
5209
+ const config = resolveMetronomeConfig(message.config);
5210
+ this.engine.setMetronome(message.config);
5211
+ this.metronomeConfig = config;
5212
+ }
4031
5213
  break;
4032
5214
  case "syncAutomation":
4033
5215
  this.engine.setAutomationLane(message.paramId, message.points);
4034
5216
  break;
5217
+ case "syncParameters":
5218
+ this.engine.clearParameters();
5219
+ for (const info of message.parameters) {
5220
+ this.engine.addParameter(info);
5221
+ }
5222
+ break;
4035
5223
  case "syncTempo":
4036
5224
  if (message.tempoSegments) {
4037
5225
  this.engine.setTempoSegments(message.tempoSegments);
@@ -4178,6 +5366,9 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
4178
5366
  message.renderFrame
4179
5367
  );
4180
5368
  break;
5369
+ case "syncMidiUmp":
5370
+ this.engine.pushMidiUmp(message.destinationId, message.word0, message.renderFrame);
5371
+ break;
4181
5372
  case "syncMidiSysex":
4182
5373
  this.engine.pushMidiSysex(message.destinationId, message.data, message.renderFrame);
4183
5374
  break;
@@ -4190,6 +5381,45 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
4190
5381
  case "syncExternalMidiClock":
4191
5382
  this.engine.setExternalMidiClockEnabled(message.enabled);
4192
5383
  break;
5384
+ case "syncMidiInputSource":
5385
+ this.engine.setMidiInputSource(message.destinationId ?? 0);
5386
+ break;
5387
+ case "syncClearMidiInputSource":
5388
+ this.engine.clearMidiInputSource();
5389
+ break;
5390
+ case "syncMidiCcBinding":
5391
+ this.engine.bindMidiCc(message.channel, message.controller, message.paramId, {
5392
+ minValue: message.minValue,
5393
+ maxValue: message.maxValue
5394
+ });
5395
+ break;
5396
+ case "syncMidiInputNoteOn":
5397
+ this.engine.pushMidiInputNoteOn(
5398
+ message.group,
5399
+ message.channel,
5400
+ message.data0,
5401
+ message.data1,
5402
+ message.portTimeSamples
5403
+ );
5404
+ break;
5405
+ case "syncMidiInputNoteOff":
5406
+ this.engine.pushMidiInputNoteOff(
5407
+ message.group,
5408
+ message.channel,
5409
+ message.data0,
5410
+ message.data1,
5411
+ message.portTimeSamples
5412
+ );
5413
+ break;
5414
+ case "syncMidiInputCc":
5415
+ this.engine.pushMidiInputCc(
5416
+ message.group,
5417
+ message.channel,
5418
+ message.data0,
5419
+ message.data1,
5420
+ message.portTimeSamples
5421
+ );
5422
+ break;
4193
5423
  }
4194
5424
  }
4195
5425
  receiveCaptureRequest(message) {
@@ -4270,6 +5500,7 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
4270
5500
  }
4271
5501
  destroy() {
4272
5502
  if (!this.closed) {
5503
+ this.clearPagedClipProviders();
4273
5504
  this.engine.destroy();
4274
5505
  this.closed = true;
4275
5506
  }
@@ -4283,7 +5514,22 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
4283
5514
  if (!command) {
4284
5515
  return;
4285
5516
  }
5517
+ this.safeApplyCommand(command);
5518
+ }
5519
+ }
5520
+ safeApplyCommand(command) {
5521
+ try {
4286
5522
  this.applyCommand(command);
5523
+ } catch {
5524
+ this.publishTelemetryRecord({
5525
+ type: 1 /* Error */,
5526
+ error: 19 /* InvalidCommand */,
5527
+ renderFrame: 0,
5528
+ timelineSample: 0,
5529
+ audibleTimelineSample: 0,
5530
+ graphLatencySamplesQ8: 0,
5531
+ value: Number(command.type)
5532
+ });
4287
5533
  }
4288
5534
  }
4289
5535
  applyCommand(command) {
@@ -4315,9 +5561,6 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
4315
5561
  case 5 /* TransportSeekPpq */:
4316
5562
  this.engine.seekPpq(Number(command.argFloat ?? 0), sampleTime);
4317
5563
  break;
4318
- case 6 /* SetTempoMap */:
4319
- this.engine.setTempo(Number(command.argFloat ?? 120));
4320
- break;
4321
5564
  case 7 /* SetLoop */:
4322
5565
  this.engine.setLoop(
4323
5566
  Number(command.argFloat ?? 0),
@@ -4340,7 +5583,8 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
4340
5583
  enabled: Boolean(command.argInt),
4341
5584
  beatGain: this.metronomeConfig.beatGain,
4342
5585
  accentGain: this.metronomeConfig.accentGain,
4343
- clickSamples: this.metronomeConfig.clickSamples
5586
+ clickSamples: this.metronomeConfig.clickSamples,
5587
+ clickSeconds: this.metronomeConfig.clickSeconds
4344
5588
  });
4345
5589
  break;
4346
5590
  case 17 /* SeekMarker */:
@@ -4368,10 +5612,34 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
4368
5612
  }
4369
5613
  }
4370
5614
  publishTelemetry() {
5615
+ const ring = this.telemetryRing;
5616
+ if (ring) {
5617
+ for (let count = 0; count < 64 && this.engine.popTelemetryToScratch(); count++) {
5618
+ this.writeTelemetryScratch(ring);
5619
+ }
5620
+ return;
5621
+ }
4371
5622
  for (const item of this.engine.drainTelemetry(64)) {
4372
5623
  this.publishTelemetryRecord(telemetryFromEngine(item));
4373
5624
  }
4374
5625
  }
5626
+ writeTelemetryScratch(ring) {
5627
+ const writeIndex = Atomics.load(ring.header, 0);
5628
+ const offset = writeIndex % ring.capacity * SONARE_ENGINE_TELEMETRY_RECORD_BYTES;
5629
+ ring.view.setUint32(offset, this.engine.telemetryScratchType(), true);
5630
+ ring.view.setUint32(offset + 4, this.engine.telemetryScratchError(), true);
5631
+ writeInt64Words(ring.view, offset + 8, this.engine.telemetryScratchRenderFrame());
5632
+ writeInt64Words(ring.view, offset + 16, this.engine.telemetryScratchTimelineSample());
5633
+ writeInt64Words(ring.view, offset + 24, this.engine.telemetryScratchAudibleTimelineSample());
5634
+ ring.view.setInt32(offset + 32, this.engine.telemetryScratchGraphLatencySamplesQ8(), true);
5635
+ ring.view.setUint32(offset + 36, this.engine.telemetryScratchValue(), true);
5636
+ ring.view.setUint32(offset + 40, 0, true);
5637
+ ring.view.setUint32(offset + 44, 0, true);
5638
+ Atomics.store(ring.header, 0, writeIndex + 1);
5639
+ if (writeIndex + 1 > ring.capacity) {
5640
+ Atomics.store(ring.header, 4, writeIndex + 1 - ring.capacity);
5641
+ }
5642
+ }
4375
5643
  publishTelemetryRecord(record) {
4376
5644
  if (this.telemetryRing) {
4377
5645
  writeSonareEngineTelemetryRingBuffer(this.telemetryRing, record);
@@ -4394,6 +5662,19 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
4394
5662
  if (this.meterIntervalFrames <= 0 || !this.transport && !this.meterRing) {
4395
5663
  return;
4396
5664
  }
5665
+ if (this.meterRing) {
5666
+ for (let count = 0; count < 64 && this.engine.popMeterTelemetryToScratch(); count++) {
5667
+ const frame = this.engine.meterScratchRenderFrame();
5668
+ if (frame !== this.lastMeterFrame && frame - this.lastMeterFrame < this.meterIntervalFrames) {
5669
+ continue;
5670
+ }
5671
+ if (frame !== this.lastMeterFrame) {
5672
+ this.lastMeterFrame = frame;
5673
+ }
5674
+ this.writeMeterScratch(this.meterRing);
5675
+ }
5676
+ return;
5677
+ }
4397
5678
  for (const item of this.engine.drainMeterTelemetry(64)) {
4398
5679
  const meter = meterFromEngine(item);
4399
5680
  if (meter.frame !== this.lastMeterFrame && meter.frame - this.lastMeterFrame < this.meterIntervalFrames) {
@@ -4410,6 +5691,18 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
4410
5691
  }
4411
5692
  }
4412
5693
  }
5694
+ writeMeterScratch(ring) {
5695
+ const writeIndex = Atomics.load(ring.header, 0);
5696
+ const offset = writeIndex % ring.capacity * SONARE_METER_RING_RECORD_FLOATS;
5697
+ const frame = Number(this.engine.meterScratchRenderFrame());
5698
+ ring.records[offset] = encodeFrameLo(frame);
5699
+ ring.records[offset + 1] = encodeFrameHi(frame);
5700
+ ring.records[offset + 2] = this.engine.meterScratchTargetId();
5701
+ for (let field = 0; field < 11; field++) {
5702
+ ring.records[offset + 3 + field] = this.engine.meterScratchValue(field);
5703
+ }
5704
+ Atomics.store(ring.header, 0, writeIndex + 1);
5705
+ }
4413
5706
  writeMeterRing(meter) {
4414
5707
  const ring = this.meterRing;
4415
5708
  if (!ring) {
@@ -4441,45 +5734,160 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
4441
5734
  if (!ring) {
4442
5735
  return;
4443
5736
  }
4444
- for (const item of this.engine.drainScopeTelemetry(64)) {
4445
- this.writeScopeRing(ring, item);
5737
+ for (let count = 0; count < 64 && this.engine.popScopeTelemetryToScratch(); count++) {
5738
+ this.writeScopeScratch(ring);
5739
+ }
5740
+ }
5741
+ writeScopeScratch(ring) {
5742
+ const writeIndex = Atomics.load(ring.header, 0);
5743
+ const base = writeIndex % ring.capacity * ring.recordFloats;
5744
+ const frame = Number(this.engine.scopeScratchRenderFrame());
5745
+ ring.records[base] = encodeFrameLo(frame);
5746
+ ring.records[base + 1] = encodeFrameHi(frame);
5747
+ ring.records[base + 2] = this.engine.scopeScratchTargetId();
5748
+ const bandCount = Math.min(ring.bands, this.engine.scopeScratchBandCount());
5749
+ ring.records[base + 3] = bandCount;
5750
+ const pointCount = Math.min(ring.maxPoints, this.engine.scopeScratchPointCount());
5751
+ ring.records[base + 4] = pointCount;
5752
+ const bandsBase = base + SONARE_SCOPE_RING_RECORD_PREFIX_FLOATS;
5753
+ for (let i = 0; i < bandCount; i++) {
5754
+ ring.records[bandsBase + i] = this.engine.scopeScratchBand(i);
5755
+ }
5756
+ const pointsBase = bandsBase + ring.bands;
5757
+ for (let i = 0; i < pointCount; i++) {
5758
+ ring.records[pointsBase + 2 * i] = this.engine.scopeScratchPointLeft(i);
5759
+ ring.records[pointsBase + 2 * i + 1] = this.engine.scopeScratchPointRight(i);
4446
5760
  }
5761
+ Atomics.store(ring.header, 0, writeIndex + 1);
4447
5762
  }
4448
5763
  // Drains queued external-MIDI events (already lowered to MIDI 1.0 bytes) and
4449
5764
  // forwards them to the main thread for delivery to Web MIDI output ports.
4450
5765
  // One batch per render block; skipped entirely when nothing is queued, so an
4451
5766
  // all-internal project never allocates or posts here.
4452
5767
  publishExternalMidi() {
5768
+ const ring = this.externalMidiRing;
5769
+ if (ring) {
5770
+ for (let count = 0; count < 256 && this.engine.popExternalMidiToScratch(); count++) {
5771
+ pushSonareExternalMidiRingBuffer(
5772
+ ring,
5773
+ this.engine.externalMidiScratchDestinationId(),
5774
+ this.engine.externalMidiScratchRenderFrame(),
5775
+ this.engine.externalMidiScratchByteWord(),
5776
+ this.engine.externalMidiScratchByteCount()
5777
+ );
5778
+ this.engine.consumeExternalMidiScratch();
5779
+ }
5780
+ return;
5781
+ }
4453
5782
  if (!this.transport?.postMessage) {
4454
5783
  return;
4455
5784
  }
5785
+ if (this.engine.externalMidiPendingCount() === 0) {
5786
+ return;
5787
+ }
4456
5788
  const events = this.engine.drainExternalMidi(256);
4457
5789
  if (events.length === 0) {
4458
5790
  return;
4459
5791
  }
4460
5792
  this.transport.postMessage({ type: "externalMidi", events });
4461
5793
  }
4462
- writeScopeRing(ring, record) {
4463
- const writeIndex = Atomics.load(ring.header, 0);
4464
- const base = writeIndex % ring.capacity * ring.recordFloats;
4465
- ring.records[base] = encodeFrameLo(record.renderFrame);
4466
- ring.records[base + 1] = encodeFrameHi(record.renderFrame);
4467
- ring.records[base + 2] = record.targetId;
4468
- const bandCount = Math.min(ring.bands, record.bands.length);
4469
- ring.records[base + 3] = bandCount;
4470
- const pointCount = Math.min(ring.maxPoints, record.points.length);
4471
- ring.records[base + 4] = pointCount;
4472
- const bandsBase = base + SONARE_SCOPE_RING_RECORD_PREFIX_FLOATS;
4473
- for (let i = 0; i < bandCount; i++) {
4474
- ring.records[bandsBase + i] = record.bands[i];
5794
+ /**
5795
+ * Forward a bounded batch of native page misses to the main thread. This
5796
+ * deliberately runs after audio rendering: a cache miss is silence for this
5797
+ * block, and OPFS I/O must never delay `process()`.
5798
+ */
5799
+ publishClipPageRequests() {
5800
+ const ring = this.clipPageRequestRing;
5801
+ const transport = this.transport;
5802
+ if (!ring && !transport?.postMessage) {
5803
+ return;
4475
5804
  }
4476
- const pointsBase = bandsBase + ring.bands;
4477
- for (let i = 0; i < pointCount; i++) {
4478
- const point = record.points[i];
4479
- ring.records[pointsBase + 2 * i] = point.left;
4480
- ring.records[pointsBase + 2 * i + 1] = point.right;
5805
+ let count = 0;
5806
+ let drained = 0;
5807
+ while (drained < this.clipPageRequestClipIds.length) {
5808
+ drained += 1;
5809
+ let clipId;
5810
+ let sample;
5811
+ if (ring) {
5812
+ if (!this.engine.popClipPageRequestToScratch()) {
5813
+ break;
5814
+ }
5815
+ clipId = this.engine.clipPageRequestScratchClipId();
5816
+ sample = this.engine.clipPageRequestScratchSample();
5817
+ } else {
5818
+ const request = this.engine.popClipPageRequest();
5819
+ if (!request) {
5820
+ break;
5821
+ }
5822
+ clipId = request.clipId;
5823
+ sample = request.sample;
5824
+ }
5825
+ const pageFrames = this.pagedClipPageFrames.get(clipId);
5826
+ if (!pageFrames) {
5827
+ continue;
5828
+ }
5829
+ const pageIndex = Math.floor(sample / pageFrames);
5830
+ let duplicate = false;
5831
+ for (let i = 0; i < count; ++i) {
5832
+ if (this.clipPageRequestClipIds[i] === clipId && this.clipPageRequestPageIndices[i] === pageIndex) {
5833
+ duplicate = true;
5834
+ break;
5835
+ }
5836
+ }
5837
+ if (!duplicate) {
5838
+ this.clipPageRequestClipIds[count] = clipId;
5839
+ this.clipPageRequestPageIndices[count] = pageIndex;
5840
+ count += 1;
5841
+ }
5842
+ }
5843
+ const overflowCount = this.engine.clipPageRequestOverflowCount();
5844
+ const dropped = overflowCount - this.clipPageRequestOverflowReported >>> 0;
5845
+ this.clipPageRequestOverflowReported = overflowCount;
5846
+ if (ring) {
5847
+ if (dropped > 0) {
5848
+ Atomics.add(ring.header, 4, dropped);
5849
+ }
5850
+ for (let i = 0; i < count; ++i) {
5851
+ pushSonareClipPageRequestRingBuffer(
5852
+ ring,
5853
+ this.clipPageRequestClipIds[i],
5854
+ this.clipPageRequestPageIndices[i]
5855
+ );
5856
+ }
5857
+ return;
5858
+ }
5859
+ if (count === 0 && dropped === 0) {
5860
+ return;
5861
+ }
5862
+ if (!transport?.postMessage) {
5863
+ return;
5864
+ }
5865
+ const requests = new Array(count);
5866
+ for (let i = 0; i < count; ++i) {
5867
+ requests[i] = {
5868
+ clipId: this.clipPageRequestClipIds[i],
5869
+ pageIndex: this.clipPageRequestPageIndices[i]
5870
+ };
5871
+ }
5872
+ transport.postMessage({
5873
+ type: "clipPageRequest",
5874
+ requests,
5875
+ ...dropped > 0 ? { dropped } : {}
5876
+ });
5877
+ }
5878
+ removePagedClipProvider(clipId) {
5879
+ const providerId = this.pagedClipProviders.get(clipId);
5880
+ if (providerId !== void 0) {
5881
+ this.engine.destroyClipPageProvider(providerId);
5882
+ }
5883
+ this.pagedClipProviders.delete(clipId);
5884
+ this.pagedClipPageFrames.delete(clipId);
5885
+ this.pendingPagedClips.delete(clipId);
5886
+ }
5887
+ clearPagedClipProviders() {
5888
+ for (const clipId of this.pagedClipProviders.keys()) {
5889
+ this.removePagedClipProvider(clipId);
4481
5890
  }
4482
- Atomics.store(ring.header, 0, writeIndex + 1);
4483
5891
  }
4484
5892
  commandRingFromSharedBuffer(sharedBuffer, fallbackCapacity) {
4485
5893
  const ring = engineRingFromSharedBuffer(
@@ -4512,6 +5920,7 @@ function registerSonareRealtimeEngineWorkletProcessor(name = "sonare-realtime-en
4512
5920
  constructor(options) {
4513
5921
  super();
4514
5922
  this.pendingMessages = [];
5923
+ this.pendingMessagesOverflowed = false;
4515
5924
  const port = this.port;
4516
5925
  const processorOptions = options?.processorOptions ?? {};
4517
5926
  void this.initializeEmbind(processorOptions, port);
@@ -4519,6 +5928,8 @@ function registerSonareRealtimeEngineWorkletProcessor(name = "sonare-realtime-en
4519
5928
  if (!this.bridge) {
4520
5929
  if (this.pendingMessages.length < 1024) {
4521
5930
  this.pendingMessages.push(event.data);
5931
+ } else {
5932
+ this.pendingMessagesOverflowed = true;
4522
5933
  }
4523
5934
  return;
4524
5935
  }
@@ -4580,6 +5991,9 @@ function registerSonareRealtimeEngineWorkletProcessor(name = "sonare-realtime-en
4580
5991
  moduleFactory
4581
5992
  });
4582
5993
  }
5994
+ if (this.pendingMessagesOverflowed) {
5995
+ throw new Error("AudioWorklet initialization message buffer overflowed.");
5996
+ }
4583
5997
  this.bridge = new SonareRealtimeEngineWorkletProcessor(options, {
4584
5998
  postMessage: (message) => port?.postMessage?.(message),
4585
5999
  onMeter: (meter) => port?.postMessage?.(meter)
@@ -4895,6 +6309,7 @@ var _SonareRealtimeVoiceChangerWorkletProcessor = class _SonareRealtimeVoiceChan
4895
6309
  this.changer.prepare(this.sampleRate, this.blockSize, this.channelCount);
4896
6310
  this.monoInput = this.changer.getMonoInputBuffer(this.blockSize);
4897
6311
  this.monoOutput = this.changer.getMonoOutputBuffer(this.blockSize);
6312
+ this.bufferGeneration = this.changer.bufferGeneration();
4898
6313
  this.planarChannels = [];
4899
6314
  if (this.channelCount > 1) {
4900
6315
  for (let ch = 0; ch < this.channelCount; ch++) {
@@ -4903,19 +6318,16 @@ var _SonareRealtimeVoiceChangerWorkletProcessor = class _SonareRealtimeVoiceChan
4903
6318
  }
4904
6319
  }
4905
6320
  /**
4906
- * Handles a control-plane message from the main thread. Runs on the
4907
- * AudioWorklet global scope but OUTSIDE of `process()` (i.e. outside the
4908
- * realtime audio callback), so it is safe to perform JSON parsing and
4909
- * DSP coefficient recomputation here. `setConfig` MUST NOT be deferred
4910
- * into `process()` because that would block the audio thread for longer
4911
- * than one render quantum (e.g. 128 samples / 44.1 kHz = ~2.9 ms).
6321
+ * Handles a control-plane message from the main thread. AudioWorklet port
6322
+ * handlers run on the same audio rendering thread as `process()`: this path
6323
+ * therefore accepts only a pre-normalized POD and never parses JSON.
4912
6324
  */
4913
6325
  receiveMessage(message) {
4914
6326
  if (this.destroyed) {
4915
6327
  return;
4916
6328
  }
4917
6329
  if (message.type === "setConfig") {
4918
- this.changer.setConfig(message.preset);
6330
+ this.changer.setPodConfig(message.config);
4919
6331
  } else if (message.type === "reset") {
4920
6332
  this.changer.reset();
4921
6333
  } else if (message.type === "destroy") {
@@ -4927,7 +6339,7 @@ var _SonareRealtimeVoiceChangerWorkletProcessor = class _SonareRealtimeVoiceChan
4927
6339
  if (this.destroyed || !output || output.length === 0) {
4928
6340
  return !this.destroyed;
4929
6341
  }
4930
- if (this.monoInput.byteLength === 0) {
6342
+ if (this.bufferGeneration !== this.changer.bufferGeneration() || this.monoInput.byteLength === 0) {
4931
6343
  this.reacquireBuffers();
4932
6344
  }
4933
6345
  const input = inputs[0];
@@ -4944,10 +6356,7 @@ var _SonareRealtimeVoiceChangerWorkletProcessor = class _SonareRealtimeVoiceChan
4944
6356
  } else {
4945
6357
  this.monoInput.fill(0, 0, frames2);
4946
6358
  }
4947
- this.changer.processMonoInto(
4948
- this.monoInput.subarray(0, frames2),
4949
- this.monoOutput.subarray(0, frames2)
4950
- );
6359
+ this.changer.processPreparedMono(frames2);
4951
6360
  output[0].set(this.monoOutput.subarray(0, frames2));
4952
6361
  return true;
4953
6362
  }
@@ -4993,6 +6402,7 @@ var _SonareRealtimeVoiceChangerWorkletProcessor = class _SonareRealtimeVoiceChan
4993
6402
  this.planarChannels[ch] = this.changer.getPlanarChannelBuffer(ch, this.blockSize);
4994
6403
  }
4995
6404
  }
6405
+ this.bufferGeneration = this.changer.bufferGeneration();
4996
6406
  }
4997
6407
  /**
4998
6408
  * Returns the number of frames we can actually process given the
@@ -5065,9 +6475,13 @@ function registerSonareRealtimeVoiceChangerWorkletProcessor(name = "sonare-realt
5065
6475
  scope.registerProcessor(name, RegisteredSonareRealtimeVoiceChangerWorkletProcessor);
5066
6476
  }
5067
6477
  export {
6478
+ SONARE_CLIP_PAGE_REQUEST_RING_HEADER_INTS,
6479
+ SONARE_CLIP_PAGE_REQUEST_RING_RECORD_UINT32S,
5068
6480
  SONARE_ENGINE_COMMAND_RECORD_BYTES,
5069
6481
  SONARE_ENGINE_RING_HEADER_INTS,
5070
6482
  SONARE_ENGINE_TELEMETRY_RECORD_BYTES,
6483
+ SONARE_EXTERNAL_MIDI_RING_HEADER_INTS,
6484
+ SONARE_EXTERNAL_MIDI_RING_RECORD_UINT32S,
5071
6485
  SONARE_METER_RING_HEADER_INTS,
5072
6486
  SONARE_METER_RING_RECORD_FLOATS,
5073
6487
  SONARE_SCOPE_RING_HEADER_INTS,
@@ -5080,8 +6494,11 @@ export {
5080
6494
  SonareRealtimeEngineWorkletProcessor,
5081
6495
  SonareRealtimeVoiceChangerWorkletProcessor,
5082
6496
  SonareWorkletProcessor,
6497
+ attachOpfsClipStream,
6498
+ createSonareClipPageRequestRingBuffer,
5083
6499
  createSonareEngineCommandRingBuffer,
5084
6500
  createSonareEngineTelemetryRingBuffer,
6501
+ createSonareExternalMidiRingBuffer,
5085
6502
  createSonareMeterRingBuffer,
5086
6503
  createSonareScopeRingBuffer,
5087
6504
  createSonareSpectrumRingBuffer,
@@ -5091,16 +6508,22 @@ export {
5091
6508
  init,
5092
6509
  isInitialized,
5093
6510
  popSonareEngineCommandRingBuffer,
6511
+ pushSonareClipPageRequestRingBuffer,
5094
6512
  pushSonareEngineCommandRingBuffer,
6513
+ pushSonareExternalMidiRingBuffer,
6514
+ readSonareClipPageRequestRingBuffer,
5095
6515
  readSonareEngineTelemetryRingBuffer,
6516
+ readSonareExternalMidiRingBuffer,
5096
6517
  readSonareMeterRingBuffer,
5097
6518
  readSonareScopeRingBuffer,
5098
6519
  readSonareSpectrumRingBuffer,
5099
6520
  registerSonareRealtimeEngineWorkletProcessor,
5100
6521
  registerSonareRealtimeVoiceChangerWorkletProcessor,
5101
6522
  registerSonareWorkletProcessor,
6523
+ sonareClipPageRequestRingBufferByteLength,
5102
6524
  sonareEngineCommandRingBufferByteLength,
5103
6525
  sonareEngineTelemetryRingBufferByteLength,
6526
+ sonareExternalMidiRingBufferByteLength,
5104
6527
  sonareMeterRingBufferByteLength,
5105
6528
  sonareScopeRingBufferByteLength,
5106
6529
  sonareSpectrumRingBufferByteLength,