@libraz/libsonare 1.5.5 → 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.
- package/README.md +109 -20
- package/dist/analysis.d.ts +5730 -0
- package/dist/analysis.js +2464 -0
- package/dist/analysis.js.map +1 -0
- package/dist/index.d.ts +759 -35
- package/dist/index.js +904 -209
- package/dist/index.js.map +1 -1
- package/dist/schemas/realtime-voice-changer-preset-pack.schema.json +27 -0
- package/dist/schemas/realtime-voice-changer-preset.schema.json +140 -0
- package/dist/sonare-analysis.js +2 -0
- package/dist/sonare-analysis.wasm +0 -0
- package/dist/sonare.js +1 -1
- package/dist/sonare.wasm +0 -0
- package/dist/worker.d.ts +65 -0
- package/dist/worker.js +707 -0
- package/dist/worker.js.map +1 -0
- package/dist/worklet.d.ts +574 -86
- package/dist/worklet.js +1634 -235
- package/dist/worklet.js.map +1 -1
- package/package.json +23 -11
- package/src/_chain_config.ts +5 -3
- package/src/analysis.ts +137 -0
- package/src/analysis_helpers.ts +6 -0
- package/src/clip_page_streamer.ts +62 -8
- package/src/codes.ts +51 -64
- package/src/effects_mastering.ts +1 -0
- package/src/errors.ts +1 -0
- package/src/feature_core.ts +140 -2
- package/src/feature_music.ts +17 -2
- package/src/feature_pitch.ts +70 -1
- package/src/feature_spectral.ts +231 -5
- package/src/feature_spectrogram.ts +159 -0
- package/src/features.ts +16 -1
- package/src/index.ts +101 -7
- package/src/mastering_chain.ts +20 -8
- package/src/mastering_core.ts +5 -0
- package/src/mastering_dynamics.ts +7 -1
- package/src/metering.ts +6 -1
- package/src/opfs_clip_pages.ts +26 -1
- package/src/project.ts +7 -0
- package/src/project_class.ts +62 -8
- package/src/project_internal.ts +24 -29
- package/src/project_types.ts +69 -10
- package/src/public_types.ts +68 -0
- package/src/public_types_mastering.ts +22 -0
- package/src/public_types_mixing.ts +11 -2
- package/src/public_types_music.ts +9 -0
- package/src/public_types_spectral.ts +33 -0
- package/src/quick_analysis.ts +25 -7
- package/src/realtime_engine.ts +153 -3
- package/src/realtime_voice_changer.ts +51 -77
- package/src/sonare-analysis.js.d.ts +8 -0
- package/src/sonare.js.d.ts +276 -6
- package/src/streaming_processors.ts +15 -0
- package/src/web_midi.ts +36 -4
- package/src/worker.ts +196 -0
- package/src/worker_client.ts +352 -0
- package/src/worker_protocol.ts +59 -0
- package/src/worklet/engine-automation.ts +2 -1
- package/src/worklet/engine-capture-facade.ts +13 -4
- package/src/worklet/engine-clips.ts +12 -3
- package/src/worklet/engine-node.ts +212 -4
- package/src/worklet/engine-offline.ts +7 -2
- package/src/worklet/engine-parameter-facade.ts +26 -5
- package/src/worklet/engine-processor.ts +383 -42
- package/src/worklet/engine-register.ts +8 -0
- package/src/worklet/engine-strips.ts +17 -1
- package/src/worklet/engine-sync.ts +2 -6
- package/src/worklet/engine-tempo-facade.ts +8 -6
- package/src/worklet/engine.ts +380 -13
- package/src/worklet/guards.ts +22 -1
- package/src/worklet/messages.ts +125 -7
- package/src/worklet/protocol.ts +301 -17
- package/src/worklet/voice-changer-processor.ts +12 -15
- 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
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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;
|
|
166
554
|
}
|
|
555
|
+
if (typeof value === "string") {
|
|
556
|
+
const ordinal = values[value];
|
|
557
|
+
if (ordinal !== void 0) {
|
|
558
|
+
return ordinal;
|
|
559
|
+
}
|
|
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
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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) {
|
|
@@ -386,6 +766,9 @@ var RealtimeEngine = class {
|
|
|
386
766
|
externalMidiDroppedCount() {
|
|
387
767
|
return this.native.externalMidiDroppedCount();
|
|
388
768
|
}
|
|
769
|
+
externalMidiPendingCount() {
|
|
770
|
+
return this.native.externalMidiPendingCount();
|
|
771
|
+
}
|
|
389
772
|
/**
|
|
390
773
|
* Drain queued external-MIDI events, already lowered to MIDI 1.0 byte
|
|
391
774
|
* messages ready to write to a Web MIDI output port. Call once per audio
|
|
@@ -396,6 +779,25 @@ var RealtimeEngine = class {
|
|
|
396
779
|
drainExternalMidi(maxRecords = 1024) {
|
|
397
780
|
return this.native.drainExternalMidi(maxRecords);
|
|
398
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
|
+
}
|
|
399
801
|
pushMidiInputNoteOn(group, channel, note, velocity, portTimeSamples = 0) {
|
|
400
802
|
this.native.pushMidiInputNoteOn(group, channel, note, velocity, portTimeSamples);
|
|
401
803
|
}
|
|
@@ -420,6 +822,10 @@ var RealtimeEngine = class {
|
|
|
420
822
|
pushMidiCc(destinationId, group, channel, controller, value, renderFrame = -1) {
|
|
421
823
|
this.native.pushMidiCc(destinationId, group, channel, controller, value, renderFrame);
|
|
422
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
|
+
}
|
|
423
829
|
/**
|
|
424
830
|
* Queue an immediate (live) MIDI SysEx frame to a MIDI destination. `data` is
|
|
425
831
|
* the full message including the leading 0xF0 and trailing 0xF7 (1..512
|
|
@@ -465,6 +871,10 @@ var RealtimeEngine = class {
|
|
|
465
871
|
settleParameters() {
|
|
466
872
|
this.native.settleParameters();
|
|
467
873
|
}
|
|
874
|
+
/** Drains queued commands on an offline/control-only engine immediately. */
|
|
875
|
+
flushControlCommands() {
|
|
876
|
+
this.native.flushControlCommands();
|
|
877
|
+
}
|
|
468
878
|
seekPpq(ppq, renderFrame = -1) {
|
|
469
879
|
this.native.seekPpq(ppq, renderFrame);
|
|
470
880
|
}
|
|
@@ -719,6 +1129,23 @@ var RealtimeEngine = class {
|
|
|
719
1129
|
popClipPageRequest() {
|
|
720
1130
|
return this.native.popClipPageRequest();
|
|
721
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
|
+
}
|
|
722
1149
|
setCaptureBuffer(numChannels, capacityFrames) {
|
|
723
1150
|
this.native.setCaptureBuffer(numChannels, capacityFrames);
|
|
724
1151
|
}
|
|
@@ -747,6 +1174,10 @@ var RealtimeEngine = class {
|
|
|
747
1174
|
capturedAudio() {
|
|
748
1175
|
return this.native.capturedAudio();
|
|
749
1176
|
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Renders in place, adding engine output to `channels`. Zero each plane first
|
|
1179
|
+
* when it contains no upstream input.
|
|
1180
|
+
*/
|
|
750
1181
|
process(channels) {
|
|
751
1182
|
return this.native.process(channels);
|
|
752
1183
|
}
|
|
@@ -769,6 +1200,7 @@ var RealtimeEngine = class {
|
|
|
769
1200
|
}
|
|
770
1201
|
/**
|
|
771
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.
|
|
772
1204
|
* Allocation-free: safe to call on the AudioWorklet render thread after
|
|
773
1205
|
* `prepareChannels`.
|
|
774
1206
|
*/
|
|
@@ -790,6 +1222,42 @@ var RealtimeEngine = class {
|
|
|
790
1222
|
drainTelemetry(maxRecords = 1024) {
|
|
791
1223
|
return this.native.drainTelemetry(maxRecords);
|
|
792
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
|
+
}
|
|
793
1261
|
drainMeterTelemetry(maxRecords = 1024) {
|
|
794
1262
|
return this.native.drainMeterTelemetry(maxRecords);
|
|
795
1263
|
}
|
|
@@ -817,6 +1285,30 @@ var RealtimeEngine = class {
|
|
|
817
1285
|
drainScopeTelemetry(maxRecords = 1024) {
|
|
818
1286
|
return this.native.drainScopeTelemetry(maxRecords);
|
|
819
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
|
+
}
|
|
820
1312
|
destroy() {
|
|
821
1313
|
this.native.delete();
|
|
822
1314
|
}
|
|
@@ -1274,65 +1766,18 @@ var Mixer = class _Mixer {
|
|
|
1274
1766
|
};
|
|
1275
1767
|
|
|
1276
1768
|
// src/realtime_voice_changer.ts
|
|
1277
|
-
function isFlatVoiceChangerPod(config) {
|
|
1278
|
-
return typeof config === "object" && config !== null && "retuneSemitones" in config;
|
|
1279
|
-
}
|
|
1280
|
-
function flatVoiceChangerPodToNested(pod) {
|
|
1281
|
-
return {
|
|
1282
|
-
inputGainDb: pod.inputGainDb,
|
|
1283
|
-
outputGainDb: pod.outputGainDb,
|
|
1284
|
-
wetMix: pod.wetMix,
|
|
1285
|
-
retune: { semitones: pod.retuneSemitones, mix: pod.retuneMix, grainSize: pod.retuneGrainSize },
|
|
1286
|
-
formant: {
|
|
1287
|
-
factor: pod.formantFactor,
|
|
1288
|
-
amount: pod.formantAmount,
|
|
1289
|
-
body: pod.formantBody,
|
|
1290
|
-
brightness: pod.formantBrightness,
|
|
1291
|
-
nasal: pod.formantNasal
|
|
1292
|
-
},
|
|
1293
|
-
eq: {
|
|
1294
|
-
highpassHz: pod.eqHighpassHz,
|
|
1295
|
-
bodyDb: pod.eqBodyDb,
|
|
1296
|
-
presenceDb: pod.eqPresenceDb,
|
|
1297
|
-
airDb: pod.eqAirDb
|
|
1298
|
-
},
|
|
1299
|
-
gate: {
|
|
1300
|
-
thresholdDb: pod.gateThresholdDb,
|
|
1301
|
-
attackMs: pod.gateAttackMs,
|
|
1302
|
-
releaseMs: pod.gateReleaseMs,
|
|
1303
|
-
rangeDb: pod.gateRangeDb
|
|
1304
|
-
},
|
|
1305
|
-
compressor: {
|
|
1306
|
-
thresholdDb: pod.compressorThresholdDb,
|
|
1307
|
-
ratio: pod.compressorRatio,
|
|
1308
|
-
attackMs: pod.compressorAttackMs,
|
|
1309
|
-
releaseMs: pod.compressorReleaseMs,
|
|
1310
|
-
makeupGainDb: pod.compressorMakeupGainDb
|
|
1311
|
-
},
|
|
1312
|
-
deesser: {
|
|
1313
|
-
frequencyHz: pod.deesserFrequencyHz,
|
|
1314
|
-
thresholdDb: pod.deesserThresholdDb,
|
|
1315
|
-
ratio: pod.deesserRatio,
|
|
1316
|
-
rangeDb: pod.deesserRangeDb
|
|
1317
|
-
},
|
|
1318
|
-
reverb: {
|
|
1319
|
-
mix: pod.reverbMix,
|
|
1320
|
-
timeMs: pod.reverbTimeMs,
|
|
1321
|
-
damping: pod.reverbDamping,
|
|
1322
|
-
seed: pod.reverbSeed
|
|
1323
|
-
},
|
|
1324
|
-
limiter: {
|
|
1325
|
-
ceilingDb: pod.limiterCeilingDb,
|
|
1326
|
-
releaseMs: pod.limiterReleaseMs,
|
|
1327
|
-
enableIspLimiter: pod.limiterEnableIspLimiter,
|
|
1328
|
-
ispCeilingDbtp: pod.limiterIspCeilingDbtp
|
|
1329
|
-
}
|
|
1330
|
-
};
|
|
1331
|
-
}
|
|
1332
1769
|
var RealtimeVoiceChanger = class {
|
|
1333
|
-
|
|
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) {
|
|
1334
1776
|
const module2 = getSonareModule();
|
|
1335
1777
|
this.changer = module2.createRealtimeVoiceChanger(config);
|
|
1778
|
+
if (sampleRate !== void 0) {
|
|
1779
|
+
this.changer.prepare(sampleRate, maxBlockSize, channels);
|
|
1780
|
+
}
|
|
1336
1781
|
}
|
|
1337
1782
|
prepare(sampleRate, maxBlockSize = 128, channels = 1) {
|
|
1338
1783
|
this.changer.prepare(sampleRate, maxBlockSize, channels);
|
|
@@ -1341,8 +1786,15 @@ var RealtimeVoiceChanger = class {
|
|
|
1341
1786
|
this.changer.reset();
|
|
1342
1787
|
}
|
|
1343
1788
|
setConfig(config) {
|
|
1344
|
-
|
|
1345
|
-
|
|
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);
|
|
1346
1798
|
}
|
|
1347
1799
|
configJson() {
|
|
1348
1800
|
return this.changer.configJson();
|
|
@@ -1350,6 +1802,13 @@ var RealtimeVoiceChanger = class {
|
|
|
1350
1802
|
latencySamples() {
|
|
1351
1803
|
return this.changer.latencySamples();
|
|
1352
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
|
+
}
|
|
1353
1812
|
processMono(samples) {
|
|
1354
1813
|
return this.changer.processMono(samples);
|
|
1355
1814
|
}
|
|
@@ -1426,10 +1885,12 @@ var RealtimeVoiceChanger = class {
|
|
|
1426
1885
|
createRealtimeMonoBuffer(numSamples) {
|
|
1427
1886
|
let input = this.getMonoInputBuffer(numSamples);
|
|
1428
1887
|
let output = this.getMonoOutputBuffer(numSamples);
|
|
1888
|
+
let generation = this.bufferGeneration();
|
|
1429
1889
|
const reacquireIfDetached = () => {
|
|
1430
|
-
if (input.byteLength === 0 || output.byteLength === 0) {
|
|
1890
|
+
if (generation !== this.bufferGeneration() || input.byteLength === 0 || output.byteLength === 0) {
|
|
1431
1891
|
input = this.getMonoInputBuffer(numSamples);
|
|
1432
1892
|
output = this.getMonoOutputBuffer(numSamples);
|
|
1893
|
+
generation = this.bufferGeneration();
|
|
1433
1894
|
}
|
|
1434
1895
|
};
|
|
1435
1896
|
return {
|
|
@@ -1451,10 +1912,12 @@ var RealtimeVoiceChanger = class {
|
|
|
1451
1912
|
createRealtimeInterleavedBuffer(numFrames, numChannels) {
|
|
1452
1913
|
let input = this.getInterleavedInputBuffer(numFrames, numChannels);
|
|
1453
1914
|
let output = this.getInterleavedOutputBuffer(numFrames, numChannels);
|
|
1915
|
+
let generation = this.bufferGeneration();
|
|
1454
1916
|
const reacquireIfDetached = () => {
|
|
1455
|
-
if (input.byteLength === 0 || output.byteLength === 0) {
|
|
1917
|
+
if (generation !== this.bufferGeneration() || input.byteLength === 0 || output.byteLength === 0) {
|
|
1456
1918
|
input = this.getInterleavedInputBuffer(numFrames, numChannels);
|
|
1457
1919
|
output = this.getInterleavedOutputBuffer(numFrames, numChannels);
|
|
1920
|
+
generation = this.bufferGeneration();
|
|
1458
1921
|
}
|
|
1459
1922
|
};
|
|
1460
1923
|
return {
|
|
@@ -1481,15 +1944,17 @@ var RealtimeVoiceChanger = class {
|
|
|
1481
1944
|
*/
|
|
1482
1945
|
createRealtimePlanarBuffer(numFrames, numChannels) {
|
|
1483
1946
|
let channels = [];
|
|
1947
|
+
let generation = this.bufferGeneration();
|
|
1484
1948
|
const acquire = () => {
|
|
1485
1949
|
channels = [];
|
|
1486
1950
|
for (let ch = 0; ch < numChannels; ch++) {
|
|
1487
1951
|
channels.push(this.getPlanarChannelBuffer(ch, numFrames));
|
|
1488
1952
|
}
|
|
1953
|
+
generation = this.bufferGeneration();
|
|
1489
1954
|
};
|
|
1490
1955
|
acquire();
|
|
1491
1956
|
const reacquireIfDetached = () => {
|
|
1492
|
-
if ((channels[0]?.byteLength ?? 0) === 0) {
|
|
1957
|
+
if (generation !== this.bufferGeneration() || (channels[0]?.byteLength ?? 0) === 0) {
|
|
1493
1958
|
acquire();
|
|
1494
1959
|
}
|
|
1495
1960
|
};
|
|
@@ -1571,7 +2036,7 @@ function resolveParamId(parameters, nodeId, param) {
|
|
|
1571
2036
|
if (byName) {
|
|
1572
2037
|
return byName.id;
|
|
1573
2038
|
}
|
|
1574
|
-
|
|
2039
|
+
throw new RangeError(`Unknown engine parameter ${JSON.stringify(param)} for node ${nodeId}`);
|
|
1575
2040
|
}
|
|
1576
2041
|
function curveCode(curve) {
|
|
1577
2042
|
if (typeof curve === "number") {
|
|
@@ -1636,12 +2101,16 @@ function buildTransportFacade(ctx) {
|
|
|
1636
2101
|
},
|
|
1637
2102
|
seekPpq: (ppq, sampleTime = -1) => {
|
|
1638
2103
|
ctx.offlineEngine.seekPpq(ppq, sampleTime);
|
|
1639
|
-
|
|
2104
|
+
const ok = ctx.realtimeNode.seekPpq(ppq, sampleTime);
|
|
2105
|
+
ctx.flushOfflineMirror();
|
|
2106
|
+
return ok;
|
|
1640
2107
|
},
|
|
1641
2108
|
seekSeconds: (seconds, sampleTime = -1) => {
|
|
1642
2109
|
const timelineSample = Math.max(0, Math.round(seconds * ctx.sampleRate));
|
|
1643
2110
|
ctx.offlineEngine.seekSample(timelineSample, sampleTime);
|
|
1644
|
-
|
|
2111
|
+
const ok = ctx.realtimeNode.seekSample(timelineSample, sampleTime);
|
|
2112
|
+
ctx.flushOfflineMirror();
|
|
2113
|
+
return ok;
|
|
1645
2114
|
},
|
|
1646
2115
|
setTempo: (bpm) => ctx.setTempo(bpm),
|
|
1647
2116
|
setTempoSegments: (segments) => ctx.setTempoSegments(segments),
|
|
@@ -1728,6 +2197,10 @@ function decodeFrame(lo, hi) {
|
|
|
1728
2197
|
var SONARE_ENGINE_RING_HEADER_INTS = 5;
|
|
1729
2198
|
var SONARE_ENGINE_COMMAND_RECORD_BYTES = 32;
|
|
1730
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;
|
|
1731
2204
|
var SonareEngineCommandType = /* @__PURE__ */ ((SonareEngineCommandType2) => {
|
|
1732
2205
|
SonareEngineCommandType2[SonareEngineCommandType2["SetParam"] = 0] = "SetParam";
|
|
1733
2206
|
SonareEngineCommandType2[SonareEngineCommandType2["SetParamSmoothed"] = 1] = "SetParamSmoothed";
|
|
@@ -1774,6 +2247,7 @@ var SonareEngineTelemetryError = /* @__PURE__ */ ((SonareEngineTelemetryError2)
|
|
|
1774
2247
|
SonareEngineTelemetryError2[SonareEngineTelemetryError2["InsertAutomationOverflow"] = 16] = "InsertAutomationOverflow";
|
|
1775
2248
|
SonareEngineTelemetryError2[SonareEngineTelemetryError2["MidiClockOverflow"] = 17] = "MidiClockOverflow";
|
|
1776
2249
|
SonareEngineTelemetryError2[SonareEngineTelemetryError2["MetronomeOverflow"] = 18] = "MetronomeOverflow";
|
|
2250
|
+
SonareEngineTelemetryError2[SonareEngineTelemetryError2["InvalidCommand"] = 19] = "InvalidCommand";
|
|
1777
2251
|
return SonareEngineTelemetryError2;
|
|
1778
2252
|
})(SonareEngineTelemetryError || {});
|
|
1779
2253
|
function toDb(value) {
|
|
@@ -1971,6 +2445,128 @@ function sonareEngineTelemetryRingBufferByteLength(capacity) {
|
|
|
1971
2445
|
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
1972
2446
|
return SONARE_ENGINE_RING_HEADER_INTS * Int32Array.BYTES_PER_ELEMENT + clampedCapacity * SONARE_ENGINE_TELEMETRY_RECORD_BYTES;
|
|
1973
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
|
+
}
|
|
1974
2570
|
function createSonareEngineCommandRingBuffer(capacity = 128) {
|
|
1975
2571
|
const clampedCapacity = Math.max(1, Math.floor(capacity));
|
|
1976
2572
|
const sharedBuffer = new SharedArrayBuffer(
|
|
@@ -2111,51 +2707,82 @@ function engineRingFromSharedBuffer(sharedBuffer, recordBytes, fallbackCapacity)
|
|
|
2111
2707
|
capacity
|
|
2112
2708
|
};
|
|
2113
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
|
+
}
|
|
2114
2731
|
function recordOffset(index, capacity, recordBytes) {
|
|
2115
2732
|
return index % capacity * recordBytes;
|
|
2116
2733
|
}
|
|
2117
|
-
function
|
|
2118
|
-
|
|
2119
|
-
|
|
2734
|
+
function toSafeInteger(value, fallback) {
|
|
2735
|
+
const resolved = typeof value === "bigint" ? Number(value) : value;
|
|
2736
|
+
if (resolved === void 0) {
|
|
2737
|
+
return fallback;
|
|
2120
2738
|
}
|
|
2121
|
-
if (
|
|
2122
|
-
|
|
2739
|
+
if (!Number.isSafeInteger(resolved)) {
|
|
2740
|
+
throw new RangeError("64-bit ring values must be safe integers");
|
|
2123
2741
|
}
|
|
2124
|
-
return
|
|
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);
|
|
2125
2751
|
}
|
|
2126
2752
|
function writeEngineCommandRecord(view, offset, command) {
|
|
2127
2753
|
view.setUint32(offset, command.type, true);
|
|
2128
2754
|
view.setUint32(offset + 4, command.targetId ?? 0, true);
|
|
2129
|
-
view
|
|
2755
|
+
writeInt64Words(view, offset + 8, toSafeInteger(command.sampleTime, -1));
|
|
2130
2756
|
view.setFloat64(offset + 16, command.argFloat ?? 0, true);
|
|
2131
|
-
view
|
|
2757
|
+
writeInt64Words(view, offset + 24, toSafeInteger(command.argInt, 0));
|
|
2132
2758
|
}
|
|
2133
2759
|
function readEngineCommandRecord(view, offset) {
|
|
2134
2760
|
return {
|
|
2135
2761
|
type: view.getUint32(offset, true),
|
|
2136
2762
|
targetId: view.getUint32(offset + 4, true),
|
|
2137
|
-
sampleTime:
|
|
2763
|
+
sampleTime: readInt64Words(view, offset + 8),
|
|
2138
2764
|
argFloat: view.getFloat64(offset + 16, true),
|
|
2139
|
-
argInt:
|
|
2765
|
+
argInt: readInt64Words(view, offset + 24)
|
|
2140
2766
|
};
|
|
2141
2767
|
}
|
|
2142
2768
|
function writeEngineTelemetryRecord(view, offset, telemetry) {
|
|
2143
2769
|
view.setUint32(offset, telemetry.type, true);
|
|
2144
2770
|
view.setUint32(offset + 4, telemetry.error, true);
|
|
2145
|
-
view
|
|
2146
|
-
view
|
|
2147
|
-
view
|
|
2771
|
+
writeInt64Words(view, offset + 8, telemetry.renderFrame);
|
|
2772
|
+
writeInt64Words(view, offset + 16, telemetry.timelineSample);
|
|
2773
|
+
writeInt64Words(view, offset + 24, telemetry.audibleTimelineSample);
|
|
2148
2774
|
view.setInt32(offset + 32, telemetry.graphLatencySamplesQ8, true);
|
|
2149
2775
|
view.setUint32(offset + 36, telemetry.value, true);
|
|
2150
|
-
view.
|
|
2776
|
+
view.setUint32(offset + 40, 0, true);
|
|
2777
|
+
view.setUint32(offset + 44, 0, true);
|
|
2151
2778
|
}
|
|
2152
2779
|
function readEngineTelemetryRecord(view, offset) {
|
|
2153
2780
|
return {
|
|
2154
2781
|
type: view.getUint32(offset, true),
|
|
2155
2782
|
error: view.getUint32(offset + 4, true),
|
|
2156
|
-
renderFrame:
|
|
2157
|
-
timelineSample:
|
|
2158
|
-
audibleTimelineSample:
|
|
2783
|
+
renderFrame: readInt64Words(view, offset + 8),
|
|
2784
|
+
timelineSample: readInt64Words(view, offset + 16),
|
|
2785
|
+
audibleTimelineSample: readInt64Words(view, offset + 24),
|
|
2159
2786
|
graphLatencySamplesQ8: view.getInt32(offset + 32, true),
|
|
2160
2787
|
value: view.getUint32(offset + 36, true)
|
|
2161
2788
|
};
|
|
@@ -2204,13 +2831,16 @@ function configureCapture(ctx, options) {
|
|
|
2204
2831
|
ctx.postSync({ type: "syncCapture", ...config });
|
|
2205
2832
|
}
|
|
2206
2833
|
function armRecord(ctx, trackId, enabled) {
|
|
2834
|
+
if (trackId !== 0) {
|
|
2835
|
+
throw new RangeError("Capture is global; armRecord only accepts trackId 0");
|
|
2836
|
+
}
|
|
2207
2837
|
if (enabled && !ctx.getCaptureConfig()) {
|
|
2208
2838
|
throw new Error("Capture buffer is not configured");
|
|
2209
2839
|
}
|
|
2210
2840
|
ctx.offlineEngine.armCapture(enabled);
|
|
2211
|
-
return ctx.
|
|
2841
|
+
return ctx.sendCommand({
|
|
2212
2842
|
type: 13 /* ArmRecord */,
|
|
2213
|
-
targetId:
|
|
2843
|
+
targetId: 0,
|
|
2214
2844
|
sampleTime: -1,
|
|
2215
2845
|
argInt: enabled ? 1 : 0
|
|
2216
2846
|
});
|
|
@@ -2219,7 +2849,7 @@ function punch(ctx, inPpq, outPpq) {
|
|
|
2219
2849
|
const inSample = ctx.offlineEngine.sampleAtPpq(inPpq);
|
|
2220
2850
|
const outSample = ctx.offlineEngine.sampleAtPpq(outPpq);
|
|
2221
2851
|
ctx.offlineEngine.setCapturePunch(inSample, outSample, true);
|
|
2222
|
-
return ctx.
|
|
2852
|
+
return ctx.sendCommand({
|
|
2223
2853
|
type: 14 /* Punch */,
|
|
2224
2854
|
sampleTime: -1,
|
|
2225
2855
|
argInt: inSample,
|
|
@@ -2245,7 +2875,7 @@ function addClip(ctx, trackId, buffer, startPpq, opts = {}) {
|
|
|
2245
2875
|
const clip = {
|
|
2246
2876
|
...opts,
|
|
2247
2877
|
id,
|
|
2248
|
-
channels: buffer,
|
|
2878
|
+
...Array.isArray(buffer) ? { channels: buffer } : { pageProvider: buffer },
|
|
2249
2879
|
startPpq,
|
|
2250
2880
|
trackId: ctx.resolveTargetId(trackId)
|
|
2251
2881
|
};
|
|
@@ -2292,6 +2922,12 @@ function syncClipsDelta(ctx, upserts, removeIds) {
|
|
|
2292
2922
|
for (const clip of upserts) {
|
|
2293
2923
|
const prepared = clip.id === void 0 ? clip : preparedById.get(clip.id) ?? clip;
|
|
2294
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
|
+
}
|
|
2295
2931
|
if (prepared.id === void 0 || prepared.warpMode !== "off" || !channels || channels.length === 0 || channels[0].length <= PREBAKED_CLIP_PAGE_THRESHOLD) {
|
|
2296
2932
|
inlineUpserts.push(prepared);
|
|
2297
2933
|
continue;
|
|
@@ -2495,7 +3131,7 @@ function isEngineSyncMessage(value) {
|
|
|
2495
3131
|
if (!isRecord(value) || typeof value.type !== "string") {
|
|
2496
3132
|
return false;
|
|
2497
3133
|
}
|
|
2498
|
-
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";
|
|
2499
3135
|
}
|
|
2500
3136
|
function isEngineCaptureRequestMessage(value) {
|
|
2501
3137
|
return isRecord(value) && value.type === "captureRequest" && typeof value.requestId === "number" && (value.op === "status" || value.op === "read" || value.op === "reset");
|
|
@@ -2521,20 +3157,51 @@ function isEngineTelemetryRecord(value) {
|
|
|
2521
3157
|
function isExternalMidiBatchMessage(value) {
|
|
2522
3158
|
return isRecord(value) && value.type === "externalMidi" && Array.isArray(value.events);
|
|
2523
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
|
+
}
|
|
2524
3165
|
function isMeterSnapshot(value) {
|
|
2525
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);
|
|
2526
3167
|
}
|
|
2527
3168
|
|
|
2528
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
|
+
}
|
|
2529
3193
|
var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
|
|
2530
|
-
constructor(node, capabilities, commandRing, telemetryRing, meterRing, scopeRing) {
|
|
3194
|
+
constructor(node, capabilities, commandRing, telemetryRing, meterRing, scopeRing, clipPageRequestRing, externalMidiRing) {
|
|
2531
3195
|
this.telemetryReadIndex = 0;
|
|
2532
3196
|
this.meterReadIndex = 0;
|
|
2533
3197
|
this.scopeReadIndex = 0;
|
|
3198
|
+
this.clipPageRequestDroppedRead = 0;
|
|
2534
3199
|
this.telemetryListeners = /* @__PURE__ */ new Set();
|
|
2535
3200
|
this.meterListeners = /* @__PURE__ */ new Set();
|
|
2536
3201
|
this.scopeListeners = /* @__PURE__ */ new Set();
|
|
2537
3202
|
this.midiOutListeners = /* @__PURE__ */ new Set();
|
|
3203
|
+
this.clipPageRequestListeners = /* @__PURE__ */ new Set();
|
|
3204
|
+
this.syncErrorListeners = /* @__PURE__ */ new Set();
|
|
2538
3205
|
this.captureRequestId = 1;
|
|
2539
3206
|
this.captureRequests = /* @__PURE__ */ new Map();
|
|
2540
3207
|
this.transportRequestId = 1;
|
|
@@ -2546,6 +3213,8 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
|
|
|
2546
3213
|
this.telemetryRing = telemetryRing;
|
|
2547
3214
|
this.meterRing = meterRing;
|
|
2548
3215
|
this.scopeRing = scopeRing;
|
|
3216
|
+
this.clipPageRequestRing = clipPageRequestRing;
|
|
3217
|
+
this.externalMidiRing = externalMidiRing;
|
|
2549
3218
|
this.ready = new Promise((resolve, reject) => {
|
|
2550
3219
|
this.resolveReady = resolve;
|
|
2551
3220
|
this.rejectReady = reject;
|
|
@@ -2580,6 +3249,17 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
|
|
|
2580
3249
|
this.emitMeter(event.data);
|
|
2581
3250
|
} else if (isExternalMidiBatchMessage(event.data)) {
|
|
2582
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
|
+
}
|
|
2583
3263
|
} else if (isRecord(event.data) && event.data.type === "ready") {
|
|
2584
3264
|
this.resolveReady();
|
|
2585
3265
|
} else if (isRecord(event.data) && event.data.type === "error") {
|
|
@@ -2587,7 +3267,13 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
|
|
|
2587
3267
|
}
|
|
2588
3268
|
};
|
|
2589
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
|
+
}
|
|
2590
3275
|
static async create(context, options = {}) {
|
|
3276
|
+
const blockSize = workletBlockSize(options.blockSize);
|
|
2591
3277
|
const processorName = options.processorName ?? "sonare-realtime-engine-processor";
|
|
2592
3278
|
const moduleUrl = options.moduleUrl;
|
|
2593
3279
|
if (moduleUrl && context.audioWorklet?.addModule) {
|
|
@@ -2618,10 +3304,12 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
|
|
|
2618
3304
|
const meterRing = mode === "sab" ? createSonareMeterRingBuffer(options.meterRingCapacity ?? 128) : void 0;
|
|
2619
3305
|
const scopeIntervalFrames = Math.max(0, Math.floor(options.scopeIntervalFrames ?? 0));
|
|
2620
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;
|
|
2621
3309
|
const channelCount = Math.max(1, Math.floor(options.channelCount ?? 2));
|
|
2622
3310
|
const processorOptions = {
|
|
2623
3311
|
sampleRate: options.sampleRate ?? context.sampleRate,
|
|
2624
|
-
blockSize
|
|
3312
|
+
blockSize,
|
|
2625
3313
|
channelCount,
|
|
2626
3314
|
commandSharedBuffer: commandRing?.sharedBuffer,
|
|
2627
3315
|
commandRingCapacity: commandRing?.capacity,
|
|
@@ -2633,6 +3321,10 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
|
|
|
2633
3321
|
scopeRingCapacity: scopeRing?.capacity,
|
|
2634
3322
|
scopeBands: scopeRing?.bands,
|
|
2635
3323
|
scopeIntervalFrames: scopeRing ? scopeIntervalFrames : void 0,
|
|
3324
|
+
clipPageRequestSharedBuffer: clipPageRequestRing?.sharedBuffer,
|
|
3325
|
+
clipPageRequestRingCapacity: clipPageRequestRing?.capacity,
|
|
3326
|
+
externalMidiSharedBuffer: externalMidiRing?.sharedBuffer,
|
|
3327
|
+
externalMidiRingCapacity: externalMidiRing?.capacity,
|
|
2636
3328
|
wasmBinary: options.wasmBinary,
|
|
2637
3329
|
initialSyncMessages: options.initialSyncMessages,
|
|
2638
3330
|
initialCommands: options.initialCommands
|
|
@@ -2652,16 +3344,23 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
|
|
|
2652
3344
|
sharedArrayBuffer,
|
|
2653
3345
|
atomics,
|
|
2654
3346
|
audioWorklet,
|
|
3347
|
+
clipPageRequestsRealtimeSafe: mode === "sab",
|
|
3348
|
+
externalMidiRealtimeSafe: mode === "sab",
|
|
2655
3349
|
engineAbiVersion: detectedCapabilities?.engineAbiVersion,
|
|
2656
3350
|
expectedEngineAbiVersion: detectedCapabilities?.expectedEngineAbiVersion,
|
|
2657
3351
|
abiCompatible: detectedCapabilities?.abiCompatible,
|
|
2658
3352
|
degradedReason,
|
|
2659
|
-
|
|
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
|
|
2660
3357
|
},
|
|
2661
3358
|
commandRing,
|
|
2662
3359
|
telemetryRing,
|
|
2663
3360
|
meterRing,
|
|
2664
|
-
scopeRing
|
|
3361
|
+
scopeRing,
|
|
3362
|
+
clipPageRequestRing,
|
|
3363
|
+
externalMidiRing
|
|
2665
3364
|
);
|
|
2666
3365
|
}
|
|
2667
3366
|
play(sampleTime = -1) {
|
|
@@ -2678,6 +3377,9 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
|
|
|
2678
3377
|
});
|
|
2679
3378
|
}
|
|
2680
3379
|
seekPpq(ppq, sampleTime = -1) {
|
|
3380
|
+
if (!Number.isFinite(ppq) || !Number.isSafeInteger(sampleTime)) {
|
|
3381
|
+
return false;
|
|
3382
|
+
}
|
|
2681
3383
|
return this.sendCommand({
|
|
2682
3384
|
type: 5 /* TransportSeekPpq */,
|
|
2683
3385
|
sampleTime,
|
|
@@ -2685,7 +3387,7 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
|
|
|
2685
3387
|
});
|
|
2686
3388
|
}
|
|
2687
3389
|
sendCommand(command) {
|
|
2688
|
-
if (this.destroyed) {
|
|
3390
|
+
if (this.destroyed || !isValidCommandRecord(command)) {
|
|
2689
3391
|
return false;
|
|
2690
3392
|
}
|
|
2691
3393
|
if (this.commandRing) {
|
|
@@ -2759,22 +3461,73 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
|
|
|
2759
3461
|
}
|
|
2760
3462
|
return read.scopes;
|
|
2761
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
|
+
}
|
|
2762
3509
|
onTelemetry(callback) {
|
|
2763
3510
|
this.telemetryListeners.add(callback);
|
|
3511
|
+
this.startRingPolling();
|
|
2764
3512
|
return () => {
|
|
2765
3513
|
this.telemetryListeners.delete(callback);
|
|
3514
|
+
this.stopRingPollingIfUnused();
|
|
2766
3515
|
};
|
|
2767
3516
|
}
|
|
2768
3517
|
onMeter(callback) {
|
|
2769
3518
|
this.meterListeners.add(callback);
|
|
3519
|
+
this.startRingPolling();
|
|
2770
3520
|
return () => {
|
|
2771
3521
|
this.meterListeners.delete(callback);
|
|
3522
|
+
this.stopRingPollingIfUnused();
|
|
2772
3523
|
};
|
|
2773
3524
|
}
|
|
2774
3525
|
onScope(callback) {
|
|
2775
3526
|
this.scopeListeners.add(callback);
|
|
3527
|
+
this.startRingPolling();
|
|
2776
3528
|
return () => {
|
|
2777
3529
|
this.scopeListeners.delete(callback);
|
|
3530
|
+
this.stopRingPollingIfUnused();
|
|
2778
3531
|
};
|
|
2779
3532
|
}
|
|
2780
3533
|
/**
|
|
@@ -2784,8 +3537,19 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
|
|
|
2784
3537
|
*/
|
|
2785
3538
|
onMidiOut(callback) {
|
|
2786
3539
|
this.midiOutListeners.add(callback);
|
|
3540
|
+
this.startRingPolling();
|
|
3541
|
+
return () => {
|
|
3542
|
+
this.midiOutListeners.delete(callback);
|
|
3543
|
+
};
|
|
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);
|
|
2787
3551
|
return () => {
|
|
2788
|
-
this.
|
|
3552
|
+
this.clipPageRequestListeners.delete(callback);
|
|
2789
3553
|
};
|
|
2790
3554
|
}
|
|
2791
3555
|
destroy() {
|
|
@@ -2793,7 +3557,11 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
|
|
|
2793
3557
|
return;
|
|
2794
3558
|
}
|
|
2795
3559
|
this.destroyed = true;
|
|
2796
|
-
this.
|
|
3560
|
+
if (this.ringPollTimer !== void 0) {
|
|
3561
|
+
clearInterval(this.ringPollTimer);
|
|
3562
|
+
this.ringPollTimer = void 0;
|
|
3563
|
+
}
|
|
3564
|
+
this.node.port.postMessage({ type: "destroy" });
|
|
2797
3565
|
this.node.disconnect();
|
|
2798
3566
|
for (const pending of this.captureRequests.values()) {
|
|
2799
3567
|
pending.reject(new Error("Realtime engine node is destroyed."));
|
|
@@ -2807,12 +3575,34 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
|
|
|
2807
3575
|
this.meterListeners.clear();
|
|
2808
3576
|
this.scopeListeners.clear();
|
|
2809
3577
|
this.midiOutListeners.clear();
|
|
3578
|
+
this.clipPageRequestListeners.clear();
|
|
2810
3579
|
}
|
|
2811
3580
|
emitTelemetry(telemetry) {
|
|
2812
3581
|
for (const listener of this.telemetryListeners) {
|
|
2813
3582
|
listener(telemetry);
|
|
2814
3583
|
}
|
|
2815
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
|
+
}
|
|
2816
3606
|
emitMeter(meter) {
|
|
2817
3607
|
for (const listener of this.meterListeners) {
|
|
2818
3608
|
listener(meter);
|
|
@@ -2823,6 +3613,11 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
|
|
|
2823
3613
|
listener(events);
|
|
2824
3614
|
}
|
|
2825
3615
|
}
|
|
3616
|
+
emitClipPageRequests(message) {
|
|
3617
|
+
for (const listener of this.clipPageRequestListeners) {
|
|
3618
|
+
listener(message);
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
2826
3621
|
emitScope(scope) {
|
|
2827
3622
|
for (const listener of this.scopeListeners) {
|
|
2828
3623
|
listener(scope);
|
|
@@ -2856,7 +3651,7 @@ var SonareRealtimeEngineNode = class _SonareRealtimeEngineNode {
|
|
|
2856
3651
|
function setParam(ctx, nodeId, param, value) {
|
|
2857
3652
|
const paramId = ctx.resolveParamId(nodeId, param);
|
|
2858
3653
|
ctx.offlineEngine.setParameter(paramId, value);
|
|
2859
|
-
return ctx.
|
|
3654
|
+
return ctx.sendCommand({
|
|
2860
3655
|
type: 0 /* SetParam */,
|
|
2861
3656
|
targetId: paramId,
|
|
2862
3657
|
sampleTime: -1,
|
|
@@ -2866,7 +3661,7 @@ function setParam(ctx, nodeId, param, value) {
|
|
|
2866
3661
|
function setSoloMute(ctx, target, solo, mute) {
|
|
2867
3662
|
const laneIndex = ctx.ensureTrackLane(target);
|
|
2868
3663
|
ctx.offlineEngine.setSoloMute(laneIndex, solo, mute);
|
|
2869
|
-
return ctx.
|
|
3664
|
+
return ctx.sendCommand({
|
|
2870
3665
|
type: 10 /* SetSoloMute */,
|
|
2871
3666
|
targetId: laneIndex,
|
|
2872
3667
|
sampleTime: -1,
|
|
@@ -2908,6 +3703,15 @@ function listParameters(ctx) {
|
|
|
2908
3703
|
}
|
|
2909
3704
|
return parameters;
|
|
2910
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
|
+
}
|
|
2911
3715
|
|
|
2912
3716
|
// src/worklet/engine-strips.ts
|
|
2913
3717
|
function trackIdFor(ctx, target) {
|
|
@@ -3022,6 +3826,11 @@ function pushMidiCc(ctx, trackId, group, channel, controller, value, renderFrame
|
|
|
3022
3826
|
renderFrame
|
|
3023
3827
|
});
|
|
3024
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
|
+
}
|
|
3025
3834
|
function setBuiltinInstrument(ctx, trackId, config) {
|
|
3026
3835
|
const destinationId = ctx.resolveTargetId(trackId);
|
|
3027
3836
|
ctx.offlineEngine.setBuiltinInstrument(config, destinationId);
|
|
@@ -3082,11 +3891,6 @@ function setTempo(ctx, bpm) {
|
|
|
3082
3891
|
ctx.setTempoSegments([{ startPpq: 0, bpm }]);
|
|
3083
3892
|
ctx.offlineEngine.setTempo(bpm);
|
|
3084
3893
|
postTempoSync(ctx);
|
|
3085
|
-
ctx.realtimeNode.sendCommand({
|
|
3086
|
-
type: 6 /* SetTempoMap */,
|
|
3087
|
-
sampleTime: -1,
|
|
3088
|
-
argFloat: bpm
|
|
3089
|
-
});
|
|
3090
3894
|
}
|
|
3091
3895
|
function setTempoSegments(ctx, segments) {
|
|
3092
3896
|
const copied = segments.map((segment) => ({ ...segment }));
|
|
@@ -3113,7 +3917,7 @@ function setTimeSignatureSegments(ctx, segments) {
|
|
|
3113
3917
|
}
|
|
3114
3918
|
function setLoop(ctx, startPpq, endPpq, enabled = true) {
|
|
3115
3919
|
ctx.offlineEngine.setLoop(startPpq, endPpq, enabled);
|
|
3116
|
-
return ctx.
|
|
3920
|
+
return ctx.sendCommand({
|
|
3117
3921
|
type: 7 /* SetLoop */,
|
|
3118
3922
|
targetId: enabled ? 1 : 0,
|
|
3119
3923
|
sampleTime: -1,
|
|
@@ -3134,6 +3938,19 @@ function cachedTransportState(ctx) {
|
|
|
3134
3938
|
}
|
|
3135
3939
|
|
|
3136
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
|
+
}
|
|
3137
3954
|
var SonareEngine = class _SonareEngine {
|
|
3138
3955
|
constructor(context, realtimeNode, offlineEngine, sampleRate, offlineBlockSize, offlineChannelCount) {
|
|
3139
3956
|
this.automationLanes = /* @__PURE__ */ new Map();
|
|
@@ -3157,6 +3974,11 @@ var SonareEngine = class _SonareEngine {
|
|
|
3157
3974
|
this.nextMarkerId = 1;
|
|
3158
3975
|
this.transportPlaying = false;
|
|
3159
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();
|
|
3160
3982
|
this.destroyed = false;
|
|
3161
3983
|
this.context = context;
|
|
3162
3984
|
this.realtimeNode = realtimeNode;
|
|
@@ -3166,10 +3988,17 @@ var SonareEngine = class _SonareEngine {
|
|
|
3166
3988
|
this.sampleRate = sampleRate;
|
|
3167
3989
|
this.offlineBlockSize = offlineBlockSize;
|
|
3168
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
|
+
});
|
|
3169
3997
|
this.transport = buildTransportFacade({
|
|
3170
3998
|
sampleRate: this.sampleRate,
|
|
3171
3999
|
realtimeNode: this.realtimeNode,
|
|
3172
4000
|
offlineEngine: this.offlineEngine,
|
|
4001
|
+
flushOfflineMirror: () => this.flushOfflineMirror(),
|
|
3173
4002
|
setTransportPlaying: (playing) => {
|
|
3174
4003
|
this.transportPlaying = playing;
|
|
3175
4004
|
},
|
|
@@ -3187,8 +4016,14 @@ var SonareEngine = class _SonareEngine {
|
|
|
3187
4016
|
Math.floor(options.offlineChannelCount ?? options.channelCount ?? 2)
|
|
3188
4017
|
);
|
|
3189
4018
|
const realtimeNode = await SonareRealtimeEngineNode.create(context, options);
|
|
4019
|
+
try {
|
|
4020
|
+
await realtimeNode.ready;
|
|
4021
|
+
} catch (error) {
|
|
4022
|
+
realtimeNode.destroy();
|
|
4023
|
+
throw error;
|
|
4024
|
+
}
|
|
3190
4025
|
const offlineEngine = options.offlineEngine ?? new RealtimeEngine(sampleRate, blockSize);
|
|
3191
|
-
|
|
4026
|
+
const engine = new _SonareEngine(
|
|
3192
4027
|
context,
|
|
3193
4028
|
realtimeNode,
|
|
3194
4029
|
offlineEngine,
|
|
@@ -3196,6 +4031,8 @@ var SonareEngine = class _SonareEngine {
|
|
|
3196
4031
|
blockSize,
|
|
3197
4032
|
channelCount
|
|
3198
4033
|
);
|
|
4034
|
+
engine.syncParameters();
|
|
4035
|
+
return engine;
|
|
3199
4036
|
}
|
|
3200
4037
|
async suspend() {
|
|
3201
4038
|
if (this.destroyed) {
|
|
@@ -3333,6 +4170,14 @@ var SonareEngine = class _SonareEngine {
|
|
|
3333
4170
|
listParameters() {
|
|
3334
4171
|
return listParameters(this.parameterContext);
|
|
3335
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
|
+
}
|
|
3336
4181
|
setSoloMute(target, solo, mute) {
|
|
3337
4182
|
return setSoloMute(this.parameterContext, target, solo, mute);
|
|
3338
4183
|
}
|
|
@@ -3468,6 +4313,76 @@ var SonareEngine = class _SonareEngine {
|
|
|
3468
4313
|
setMasterChain(sceneJson) {
|
|
3469
4314
|
this.setMasterStripJson(sceneJson);
|
|
3470
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
|
+
}
|
|
3471
4386
|
addClip(trackId, buffer, startPpq, opts = {}) {
|
|
3472
4387
|
return addClip(this.clipContext, trackId, buffer, startPpq, opts);
|
|
3473
4388
|
}
|
|
@@ -3529,9 +4444,59 @@ var SonareEngine = class _SonareEngine {
|
|
|
3529
4444
|
pushMidiCc(trackId, group, channel, controller, value, renderFrame = -1) {
|
|
3530
4445
|
pushMidiCc(this.stripContext, trackId, group, channel, controller, value, renderFrame);
|
|
3531
4446
|
}
|
|
4447
|
+
pushMidiUmp(trackId, word0, renderFrame = -1) {
|
|
4448
|
+
pushMidiUmp(this.stripContext, trackId, word0, renderFrame);
|
|
4449
|
+
}
|
|
3532
4450
|
pushMidiSysex(trackId, data, renderFrame = -1) {
|
|
3533
4451
|
pushMidiSysex(this.stripContext, trackId, data, renderFrame);
|
|
3534
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
|
+
}
|
|
3535
4500
|
pushMidiPanic(renderFrame = -1) {
|
|
3536
4501
|
this.offlineEngine.pushMidiPanic(renderFrame);
|
|
3537
4502
|
this.postSync({ type: "syncMidiPanic", renderFrame });
|
|
@@ -3539,6 +4504,10 @@ var SonareEngine = class _SonareEngine {
|
|
|
3539
4504
|
configureCapture(options) {
|
|
3540
4505
|
configureCapture(this.captureContext, options);
|
|
3541
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
|
+
*/
|
|
3542
4511
|
armRecord(trackId, enabled) {
|
|
3543
4512
|
return armRecord(this.captureContext, trackId, enabled);
|
|
3544
4513
|
}
|
|
@@ -3557,7 +4526,7 @@ var SonareEngine = class _SonareEngine {
|
|
|
3557
4526
|
setMetronome(opts) {
|
|
3558
4527
|
this.offlineEngine.setMetronome(opts);
|
|
3559
4528
|
this.postSync({ type: "syncMetronome", config: opts });
|
|
3560
|
-
this.
|
|
4529
|
+
this.sendMirroredCommand({
|
|
3561
4530
|
type: 15 /* SetMetronome */,
|
|
3562
4531
|
sampleTime: -1,
|
|
3563
4532
|
argInt: opts.enabled ? 1 : 0
|
|
@@ -3627,12 +4596,101 @@ var SonareEngine = class _SonareEngine {
|
|
|
3627
4596
|
if (this.destroyed) {
|
|
3628
4597
|
return;
|
|
3629
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();
|
|
3630
4608
|
this.destroyed = true;
|
|
3631
4609
|
this.transport.stop();
|
|
3632
4610
|
this.realtimeNode.pollTelemetry();
|
|
3633
4611
|
this.realtimeNode.destroy();
|
|
3634
4612
|
this.offlineEngine.destroy();
|
|
3635
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
|
+
}
|
|
3636
4694
|
mixerLanes() {
|
|
3637
4695
|
return mixerLanes(this.mixerContext);
|
|
3638
4696
|
}
|
|
@@ -3665,12 +4723,27 @@ var SonareEngine = class _SonareEngine {
|
|
|
3665
4723
|
if (this.destroyed) {
|
|
3666
4724
|
return;
|
|
3667
4725
|
}
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
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();
|
|
3672
4734
|
}
|
|
3673
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
|
+
}
|
|
3674
4747
|
// Collaborator surface handed to the mixer/routing free functions so they can
|
|
3675
4748
|
// mutate the routing stores (held by reference), mirror into the offline
|
|
3676
4749
|
// engine, post mixer-sync messages, and declare lanes/buses without a
|
|
@@ -3725,13 +4798,13 @@ var SonareEngine = class _SonareEngine {
|
|
|
3725
4798
|
return {
|
|
3726
4799
|
offlineEngine: this.offlineEngine,
|
|
3727
4800
|
realtimeNode: this.realtimeNode,
|
|
4801
|
+
sendCommand: (command) => this.sendMirroredCommand(command),
|
|
3728
4802
|
offlineChannelCount: this.offlineChannelCount,
|
|
3729
4803
|
postSync: (message) => this.postSync(message),
|
|
3730
4804
|
getCaptureConfig: () => this.captureConfig,
|
|
3731
4805
|
setCaptureConfig: (config) => {
|
|
3732
4806
|
this.captureConfig = config;
|
|
3733
|
-
}
|
|
3734
|
-
resolveTargetId: (target) => this.resolveTargetId(target)
|
|
4807
|
+
}
|
|
3735
4808
|
};
|
|
3736
4809
|
}
|
|
3737
4810
|
// Collaborator surface handed to the parameter / automation-id resolution
|
|
@@ -3741,7 +4814,9 @@ var SonareEngine = class _SonareEngine {
|
|
|
3741
4814
|
get parameterContext() {
|
|
3742
4815
|
return {
|
|
3743
4816
|
offlineEngine: this.offlineEngine,
|
|
3744
|
-
|
|
4817
|
+
sendCommand: (command) => this.sendMirroredCommand(command),
|
|
4818
|
+
postSync: (message) => this.postSync(message),
|
|
4819
|
+
automationLanes: this.automationLanes,
|
|
3745
4820
|
trackLaneIds: this.trackLaneIds,
|
|
3746
4821
|
resolveParamId: (nodeId, param) => this.resolveParamId(nodeId, param),
|
|
3747
4822
|
ensureTrackLane: (target) => this.ensureTrackLane(target),
|
|
@@ -3755,6 +4830,7 @@ var SonareEngine = class _SonareEngine {
|
|
|
3755
4830
|
return {
|
|
3756
4831
|
offlineEngine: this.offlineEngine,
|
|
3757
4832
|
realtimeNode: this.realtimeNode,
|
|
4833
|
+
sendCommand: (command) => this.sendMirroredCommand(command),
|
|
3758
4834
|
postSync: (message) => this.postSync(message),
|
|
3759
4835
|
getTempoBpm: () => this.tempoBpm,
|
|
3760
4836
|
setTempoBpm: (bpm) => {
|
|
@@ -3789,7 +4865,8 @@ var SonareEngine = class _SonareEngine {
|
|
|
3789
4865
|
allocateClipId: () => this.nextClipId++,
|
|
3790
4866
|
postSync: (message, transfer) => this.postSync(message, transfer),
|
|
3791
4867
|
ensureTrackLane: (target) => this.ensureTrackLane(target),
|
|
3792
|
-
resolveTargetId: (target) => this.resolveTargetId(target)
|
|
4868
|
+
resolveTargetId: (target) => this.resolveTargetId(target),
|
|
4869
|
+
commitWorkletClipPageProvider: (clip) => this.commitWorkletClipPageProvider(clip)
|
|
3793
4870
|
};
|
|
3794
4871
|
}
|
|
3795
4872
|
// Collaborator surface handed to the marker free functions so they can mutate
|
|
@@ -3804,7 +4881,7 @@ var SonareEngine = class _SonareEngine {
|
|
|
3804
4881
|
this.nextMarkerId = value;
|
|
3805
4882
|
},
|
|
3806
4883
|
postSync: (message) => this.postSync(message),
|
|
3807
|
-
sendCommand: (command) => this.
|
|
4884
|
+
sendCommand: (command) => this.sendMirroredCommand(command),
|
|
3808
4885
|
setLoop: (startPpq, endPpq, enabled) => this.setLoop(startPpq, endPpq, enabled)
|
|
3809
4886
|
};
|
|
3810
4887
|
}
|
|
@@ -3820,7 +4897,7 @@ var SonareEngine = class _SonareEngine {
|
|
|
3820
4897
|
// sample-accurate smoothed-param command to the realtime runtime.
|
|
3821
4898
|
sendSmoothedParam(paramId, value) {
|
|
3822
4899
|
this.offlineEngine.setParameter(paramId, value);
|
|
3823
|
-
return this.
|
|
4900
|
+
return this.sendMirroredCommand({
|
|
3824
4901
|
type: 1 /* SetParamSmoothed */,
|
|
3825
4902
|
targetId: paramId,
|
|
3826
4903
|
sampleTime: -1,
|
|
@@ -3830,6 +4907,12 @@ var SonareEngine = class _SonareEngine {
|
|
|
3830
4907
|
resolveParamId(nodeId, param) {
|
|
3831
4908
|
return resolveParamId(this.listParameters(), nodeId, param);
|
|
3832
4909
|
}
|
|
4910
|
+
syncParameters() {
|
|
4911
|
+
const parameters = this.listParameters();
|
|
4912
|
+
if (parameters.length > 0) {
|
|
4913
|
+
this.postSync({ type: "syncParameters", parameters });
|
|
4914
|
+
}
|
|
4915
|
+
}
|
|
3833
4916
|
resolveTargetId(target) {
|
|
3834
4917
|
return resolveTargetId(target);
|
|
3835
4918
|
}
|
|
@@ -3865,15 +4948,17 @@ var SonareEngine = class _SonareEngine {
|
|
|
3865
4948
|
var DEFAULT_METRONOME_CONFIG = {
|
|
3866
4949
|
beatGain: 0.35,
|
|
3867
4950
|
accentGain: 0.7,
|
|
3868
|
-
clickSamples: 0
|
|
4951
|
+
clickSamples: 0,
|
|
4952
|
+
clickSeconds: 0
|
|
3869
4953
|
};
|
|
3870
4954
|
function resolveMetronomeConfig(config) {
|
|
3871
4955
|
const resolved = {
|
|
3872
4956
|
beatGain: config.beatGain ?? DEFAULT_METRONOME_CONFIG.beatGain,
|
|
3873
4957
|
accentGain: config.accentGain ?? DEFAULT_METRONOME_CONFIG.accentGain,
|
|
3874
|
-
clickSamples: config.clickSamples ?? DEFAULT_METRONOME_CONFIG.clickSamples
|
|
4958
|
+
clickSamples: config.clickSamples ?? DEFAULT_METRONOME_CONFIG.clickSamples,
|
|
4959
|
+
clickSeconds: config.clickSeconds ?? DEFAULT_METRONOME_CONFIG.clickSeconds
|
|
3875
4960
|
};
|
|
3876
|
-
if (!Number.isFinite(resolved.beatGain) || resolved.beatGain < 0 || !Number.isFinite(resolved.accentGain) || resolved.accentGain < 0 || !Number.isInteger(resolved.clickSamples) || resolved.clickSamples < 0 || resolved.clickSamples > 384e3 || config.clickSeconds !== void 0 && (!Number.isFinite(config.clickSeconds) || config.clickSeconds < 0 || config.clickSeconds > 1)) {
|
|
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)) {
|
|
3877
4962
|
throw new RangeError("invalid metronome gains or click length");
|
|
3878
4963
|
}
|
|
3879
4964
|
return resolved;
|
|
@@ -3889,7 +4974,15 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
|
|
|
3889
4974
|
this.metronomeConfig = { ...DEFAULT_METRONOME_CONFIG };
|
|
3890
4975
|
this.liveClips = /* @__PURE__ */ new Map();
|
|
3891
4976
|
this.pagedClipProviders = /* @__PURE__ */ new Map();
|
|
4977
|
+
this.pagedClipPageFrames = /* @__PURE__ */ new Map();
|
|
3892
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;
|
|
3893
4986
|
this.sampleRate = options.sampleRate ?? 48e3;
|
|
3894
4987
|
this.blockSize = options.blockSize ?? 128;
|
|
3895
4988
|
this.channelCount = Math.max(1, Math.floor(options.channelCount ?? 2));
|
|
@@ -3906,7 +4999,21 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
|
|
|
3906
4999
|
options.scopeRingCapacity,
|
|
3907
5000
|
options.scopeBands
|
|
3908
5001
|
) : void 0;
|
|
3909
|
-
this.
|
|
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
|
+
);
|
|
3910
5017
|
this.engine.prepareChannels(this.channelCount, this.blockSize);
|
|
3911
5018
|
this.channelBuffers = new Array(this.channelCount);
|
|
3912
5019
|
for (let ch = 0; ch < this.channelCount; ch++) {
|
|
@@ -3971,6 +5078,7 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
|
|
|
3971
5078
|
target.fill(0);
|
|
3972
5079
|
}
|
|
3973
5080
|
}
|
|
5081
|
+
this.publishClipPageRequests();
|
|
3974
5082
|
this.publishTelemetry();
|
|
3975
5083
|
this.publishMeters();
|
|
3976
5084
|
this.publishScope();
|
|
@@ -3984,7 +5092,7 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
|
|
|
3984
5092
|
}
|
|
3985
5093
|
receiveCommand(command) {
|
|
3986
5094
|
if (!this.closed) {
|
|
3987
|
-
this.
|
|
5095
|
+
this.safeApplyCommand(command);
|
|
3988
5096
|
}
|
|
3989
5097
|
}
|
|
3990
5098
|
// Applies an out-of-band control-plane sync message on the AudioWorklet
|
|
@@ -3995,35 +5103,64 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
|
|
|
3995
5103
|
if (this.closed) {
|
|
3996
5104
|
return;
|
|
3997
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) {
|
|
3998
5117
|
switch (message.type) {
|
|
3999
|
-
case "
|
|
5118
|
+
case "destroy":
|
|
5119
|
+
this.destroy();
|
|
5120
|
+
return;
|
|
5121
|
+
case "syncClips": {
|
|
5122
|
+
this.engine.setClips(message.clips);
|
|
5123
|
+
this.clearPagedClipProviders();
|
|
4000
5124
|
this.liveClips.clear();
|
|
4001
5125
|
for (const clip of message.clips) {
|
|
4002
5126
|
if (clip.id !== void 0) {
|
|
4003
5127
|
this.liveClips.set(clip.id, clip);
|
|
4004
5128
|
}
|
|
4005
5129
|
}
|
|
4006
|
-
this.engine.setClips(message.clips);
|
|
4007
5130
|
break;
|
|
4008
|
-
|
|
5131
|
+
}
|
|
5132
|
+
case "syncClipsDelta": {
|
|
5133
|
+
const nextClips = new Map(this.liveClips);
|
|
4009
5134
|
for (const clipId of message.removeIds) {
|
|
4010
|
-
|
|
5135
|
+
nextClips.delete(clipId);
|
|
4011
5136
|
}
|
|
4012
5137
|
for (const clip of message.upserts) {
|
|
4013
5138
|
if (clip.id !== void 0) {
|
|
4014
|
-
|
|
5139
|
+
nextClips.set(clip.id, clip);
|
|
4015
5140
|
}
|
|
4016
5141
|
}
|
|
4017
|
-
this.engine.setClips(Array.from(
|
|
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
|
+
}
|
|
4018
5150
|
break;
|
|
5151
|
+
}
|
|
4019
5152
|
case "syncClipPageProvider": {
|
|
4020
5153
|
const provider = this.engine.createClipPageProvider(
|
|
4021
5154
|
message.numChannels,
|
|
4022
5155
|
message.numSamples,
|
|
4023
5156
|
message.pageFrames
|
|
4024
5157
|
);
|
|
5158
|
+
this.removePagedClipProvider(message.clipId);
|
|
4025
5159
|
this.pagedClipProviders.set(message.clipId, provider.id);
|
|
4026
|
-
this.
|
|
5160
|
+
this.pagedClipPageFrames.set(message.clipId, message.pageFrames);
|
|
5161
|
+
if (message.clip) {
|
|
5162
|
+
this.pendingPagedClips.set(message.clipId, message.clip);
|
|
5163
|
+
}
|
|
4027
5164
|
break;
|
|
4028
5165
|
}
|
|
4029
5166
|
case "syncClipPage": {
|
|
@@ -4033,16 +5170,34 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
|
|
|
4033
5170
|
}
|
|
4034
5171
|
break;
|
|
4035
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
|
+
}
|
|
4036
5180
|
case "syncClipPageCommit": {
|
|
4037
5181
|
const providerId = this.pagedClipProviders.get(message.clipId);
|
|
4038
|
-
const clip = this.pendingPagedClips.get(message.clipId);
|
|
5182
|
+
const clip = message.clip ?? this.pendingPagedClips.get(message.clipId);
|
|
4039
5183
|
if (providerId !== void 0 && clip) {
|
|
4040
|
-
|
|
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);
|
|
4041
5189
|
this.pendingPagedClips.delete(message.clipId);
|
|
4042
|
-
this.engine.setClips(Array.from(this.liveClips.values()));
|
|
4043
5190
|
}
|
|
4044
5191
|
break;
|
|
4045
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
|
+
}
|
|
4046
5201
|
case "syncMidiClips":
|
|
4047
5202
|
this.engine.setMidiClips(message.clips);
|
|
4048
5203
|
break;
|
|
@@ -4050,12 +5205,21 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
|
|
|
4050
5205
|
this.engine.setMarkers(message.markers);
|
|
4051
5206
|
break;
|
|
4052
5207
|
case "syncMetronome":
|
|
4053
|
-
|
|
4054
|
-
|
|
5208
|
+
{
|
|
5209
|
+
const config = resolveMetronomeConfig(message.config);
|
|
5210
|
+
this.engine.setMetronome(message.config);
|
|
5211
|
+
this.metronomeConfig = config;
|
|
5212
|
+
}
|
|
4055
5213
|
break;
|
|
4056
5214
|
case "syncAutomation":
|
|
4057
5215
|
this.engine.setAutomationLane(message.paramId, message.points);
|
|
4058
5216
|
break;
|
|
5217
|
+
case "syncParameters":
|
|
5218
|
+
this.engine.clearParameters();
|
|
5219
|
+
for (const info of message.parameters) {
|
|
5220
|
+
this.engine.addParameter(info);
|
|
5221
|
+
}
|
|
5222
|
+
break;
|
|
4059
5223
|
case "syncTempo":
|
|
4060
5224
|
if (message.tempoSegments) {
|
|
4061
5225
|
this.engine.setTempoSegments(message.tempoSegments);
|
|
@@ -4202,6 +5366,9 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
|
|
|
4202
5366
|
message.renderFrame
|
|
4203
5367
|
);
|
|
4204
5368
|
break;
|
|
5369
|
+
case "syncMidiUmp":
|
|
5370
|
+
this.engine.pushMidiUmp(message.destinationId, message.word0, message.renderFrame);
|
|
5371
|
+
break;
|
|
4205
5372
|
case "syncMidiSysex":
|
|
4206
5373
|
this.engine.pushMidiSysex(message.destinationId, message.data, message.renderFrame);
|
|
4207
5374
|
break;
|
|
@@ -4214,6 +5381,45 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
|
|
|
4214
5381
|
case "syncExternalMidiClock":
|
|
4215
5382
|
this.engine.setExternalMidiClockEnabled(message.enabled);
|
|
4216
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;
|
|
4217
5423
|
}
|
|
4218
5424
|
}
|
|
4219
5425
|
receiveCaptureRequest(message) {
|
|
@@ -4294,6 +5500,7 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
|
|
|
4294
5500
|
}
|
|
4295
5501
|
destroy() {
|
|
4296
5502
|
if (!this.closed) {
|
|
5503
|
+
this.clearPagedClipProviders();
|
|
4297
5504
|
this.engine.destroy();
|
|
4298
5505
|
this.closed = true;
|
|
4299
5506
|
}
|
|
@@ -4307,7 +5514,22 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
|
|
|
4307
5514
|
if (!command) {
|
|
4308
5515
|
return;
|
|
4309
5516
|
}
|
|
5517
|
+
this.safeApplyCommand(command);
|
|
5518
|
+
}
|
|
5519
|
+
}
|
|
5520
|
+
safeApplyCommand(command) {
|
|
5521
|
+
try {
|
|
4310
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
|
+
});
|
|
4311
5533
|
}
|
|
4312
5534
|
}
|
|
4313
5535
|
applyCommand(command) {
|
|
@@ -4339,9 +5561,6 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
|
|
|
4339
5561
|
case 5 /* TransportSeekPpq */:
|
|
4340
5562
|
this.engine.seekPpq(Number(command.argFloat ?? 0), sampleTime);
|
|
4341
5563
|
break;
|
|
4342
|
-
case 6 /* SetTempoMap */:
|
|
4343
|
-
this.engine.setTempo(Number(command.argFloat ?? 120));
|
|
4344
|
-
break;
|
|
4345
5564
|
case 7 /* SetLoop */:
|
|
4346
5565
|
this.engine.setLoop(
|
|
4347
5566
|
Number(command.argFloat ?? 0),
|
|
@@ -4364,7 +5583,8 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
|
|
|
4364
5583
|
enabled: Boolean(command.argInt),
|
|
4365
5584
|
beatGain: this.metronomeConfig.beatGain,
|
|
4366
5585
|
accentGain: this.metronomeConfig.accentGain,
|
|
4367
|
-
clickSamples: this.metronomeConfig.clickSamples
|
|
5586
|
+
clickSamples: this.metronomeConfig.clickSamples,
|
|
5587
|
+
clickSeconds: this.metronomeConfig.clickSeconds
|
|
4368
5588
|
});
|
|
4369
5589
|
break;
|
|
4370
5590
|
case 17 /* SeekMarker */:
|
|
@@ -4392,10 +5612,34 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
|
|
|
4392
5612
|
}
|
|
4393
5613
|
}
|
|
4394
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
|
+
}
|
|
4395
5622
|
for (const item of this.engine.drainTelemetry(64)) {
|
|
4396
5623
|
this.publishTelemetryRecord(telemetryFromEngine(item));
|
|
4397
5624
|
}
|
|
4398
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
|
+
}
|
|
4399
5643
|
publishTelemetryRecord(record) {
|
|
4400
5644
|
if (this.telemetryRing) {
|
|
4401
5645
|
writeSonareEngineTelemetryRingBuffer(this.telemetryRing, record);
|
|
@@ -4418,6 +5662,19 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
|
|
|
4418
5662
|
if (this.meterIntervalFrames <= 0 || !this.transport && !this.meterRing) {
|
|
4419
5663
|
return;
|
|
4420
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
|
+
}
|
|
4421
5678
|
for (const item of this.engine.drainMeterTelemetry(64)) {
|
|
4422
5679
|
const meter = meterFromEngine(item);
|
|
4423
5680
|
if (meter.frame !== this.lastMeterFrame && meter.frame - this.lastMeterFrame < this.meterIntervalFrames) {
|
|
@@ -4434,6 +5691,18 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
|
|
|
4434
5691
|
}
|
|
4435
5692
|
}
|
|
4436
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
|
+
}
|
|
4437
5706
|
writeMeterRing(meter) {
|
|
4438
5707
|
const ring = this.meterRing;
|
|
4439
5708
|
if (!ring) {
|
|
@@ -4465,45 +5734,160 @@ var _SonareRealtimeEngineWorkletProcessor = class _SonareRealtimeEngineWorkletPr
|
|
|
4465
5734
|
if (!ring) {
|
|
4466
5735
|
return;
|
|
4467
5736
|
}
|
|
4468
|
-
for (
|
|
4469
|
-
this.
|
|
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);
|
|
4470
5760
|
}
|
|
5761
|
+
Atomics.store(ring.header, 0, writeIndex + 1);
|
|
4471
5762
|
}
|
|
4472
5763
|
// Drains queued external-MIDI events (already lowered to MIDI 1.0 bytes) and
|
|
4473
5764
|
// forwards them to the main thread for delivery to Web MIDI output ports.
|
|
4474
5765
|
// One batch per render block; skipped entirely when nothing is queued, so an
|
|
4475
5766
|
// all-internal project never allocates or posts here.
|
|
4476
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
|
+
}
|
|
4477
5782
|
if (!this.transport?.postMessage) {
|
|
4478
5783
|
return;
|
|
4479
5784
|
}
|
|
5785
|
+
if (this.engine.externalMidiPendingCount() === 0) {
|
|
5786
|
+
return;
|
|
5787
|
+
}
|
|
4480
5788
|
const events = this.engine.drainExternalMidi(256);
|
|
4481
5789
|
if (events.length === 0) {
|
|
4482
5790
|
return;
|
|
4483
5791
|
}
|
|
4484
5792
|
this.transport.postMessage({ type: "externalMidi", events });
|
|
4485
5793
|
}
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
const
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
const bandsBase = base + SONARE_SCOPE_RING_RECORD_PREFIX_FLOATS;
|
|
4497
|
-
for (let i = 0; i < bandCount; i++) {
|
|
4498
|
-
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;
|
|
4499
5804
|
}
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
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);
|
|
4505
5890
|
}
|
|
4506
|
-
Atomics.store(ring.header, 0, writeIndex + 1);
|
|
4507
5891
|
}
|
|
4508
5892
|
commandRingFromSharedBuffer(sharedBuffer, fallbackCapacity) {
|
|
4509
5893
|
const ring = engineRingFromSharedBuffer(
|
|
@@ -4536,6 +5920,7 @@ function registerSonareRealtimeEngineWorkletProcessor(name = "sonare-realtime-en
|
|
|
4536
5920
|
constructor(options) {
|
|
4537
5921
|
super();
|
|
4538
5922
|
this.pendingMessages = [];
|
|
5923
|
+
this.pendingMessagesOverflowed = false;
|
|
4539
5924
|
const port = this.port;
|
|
4540
5925
|
const processorOptions = options?.processorOptions ?? {};
|
|
4541
5926
|
void this.initializeEmbind(processorOptions, port);
|
|
@@ -4543,6 +5928,8 @@ function registerSonareRealtimeEngineWorkletProcessor(name = "sonare-realtime-en
|
|
|
4543
5928
|
if (!this.bridge) {
|
|
4544
5929
|
if (this.pendingMessages.length < 1024) {
|
|
4545
5930
|
this.pendingMessages.push(event.data);
|
|
5931
|
+
} else {
|
|
5932
|
+
this.pendingMessagesOverflowed = true;
|
|
4546
5933
|
}
|
|
4547
5934
|
return;
|
|
4548
5935
|
}
|
|
@@ -4604,6 +5991,9 @@ function registerSonareRealtimeEngineWorkletProcessor(name = "sonare-realtime-en
|
|
|
4604
5991
|
moduleFactory
|
|
4605
5992
|
});
|
|
4606
5993
|
}
|
|
5994
|
+
if (this.pendingMessagesOverflowed) {
|
|
5995
|
+
throw new Error("AudioWorklet initialization message buffer overflowed.");
|
|
5996
|
+
}
|
|
4607
5997
|
this.bridge = new SonareRealtimeEngineWorkletProcessor(options, {
|
|
4608
5998
|
postMessage: (message) => port?.postMessage?.(message),
|
|
4609
5999
|
onMeter: (meter) => port?.postMessage?.(meter)
|
|
@@ -4919,6 +6309,7 @@ var _SonareRealtimeVoiceChangerWorkletProcessor = class _SonareRealtimeVoiceChan
|
|
|
4919
6309
|
this.changer.prepare(this.sampleRate, this.blockSize, this.channelCount);
|
|
4920
6310
|
this.monoInput = this.changer.getMonoInputBuffer(this.blockSize);
|
|
4921
6311
|
this.monoOutput = this.changer.getMonoOutputBuffer(this.blockSize);
|
|
6312
|
+
this.bufferGeneration = this.changer.bufferGeneration();
|
|
4922
6313
|
this.planarChannels = [];
|
|
4923
6314
|
if (this.channelCount > 1) {
|
|
4924
6315
|
for (let ch = 0; ch < this.channelCount; ch++) {
|
|
@@ -4927,19 +6318,16 @@ var _SonareRealtimeVoiceChangerWorkletProcessor = class _SonareRealtimeVoiceChan
|
|
|
4927
6318
|
}
|
|
4928
6319
|
}
|
|
4929
6320
|
/**
|
|
4930
|
-
* Handles a control-plane message from the main thread.
|
|
4931
|
-
*
|
|
4932
|
-
*
|
|
4933
|
-
* DSP coefficient recomputation here. `setConfig` MUST NOT be deferred
|
|
4934
|
-
* into `process()` because that would block the audio thread for longer
|
|
4935
|
-
* 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.
|
|
4936
6324
|
*/
|
|
4937
6325
|
receiveMessage(message) {
|
|
4938
6326
|
if (this.destroyed) {
|
|
4939
6327
|
return;
|
|
4940
6328
|
}
|
|
4941
6329
|
if (message.type === "setConfig") {
|
|
4942
|
-
this.changer.
|
|
6330
|
+
this.changer.setPodConfig(message.config);
|
|
4943
6331
|
} else if (message.type === "reset") {
|
|
4944
6332
|
this.changer.reset();
|
|
4945
6333
|
} else if (message.type === "destroy") {
|
|
@@ -4951,7 +6339,7 @@ var _SonareRealtimeVoiceChangerWorkletProcessor = class _SonareRealtimeVoiceChan
|
|
|
4951
6339
|
if (this.destroyed || !output || output.length === 0) {
|
|
4952
6340
|
return !this.destroyed;
|
|
4953
6341
|
}
|
|
4954
|
-
if (this.monoInput.byteLength === 0) {
|
|
6342
|
+
if (this.bufferGeneration !== this.changer.bufferGeneration() || this.monoInput.byteLength === 0) {
|
|
4955
6343
|
this.reacquireBuffers();
|
|
4956
6344
|
}
|
|
4957
6345
|
const input = inputs[0];
|
|
@@ -4968,10 +6356,7 @@ var _SonareRealtimeVoiceChangerWorkletProcessor = class _SonareRealtimeVoiceChan
|
|
|
4968
6356
|
} else {
|
|
4969
6357
|
this.monoInput.fill(0, 0, frames2);
|
|
4970
6358
|
}
|
|
4971
|
-
this.changer.
|
|
4972
|
-
this.monoInput.subarray(0, frames2),
|
|
4973
|
-
this.monoOutput.subarray(0, frames2)
|
|
4974
|
-
);
|
|
6359
|
+
this.changer.processPreparedMono(frames2);
|
|
4975
6360
|
output[0].set(this.monoOutput.subarray(0, frames2));
|
|
4976
6361
|
return true;
|
|
4977
6362
|
}
|
|
@@ -5017,6 +6402,7 @@ var _SonareRealtimeVoiceChangerWorkletProcessor = class _SonareRealtimeVoiceChan
|
|
|
5017
6402
|
this.planarChannels[ch] = this.changer.getPlanarChannelBuffer(ch, this.blockSize);
|
|
5018
6403
|
}
|
|
5019
6404
|
}
|
|
6405
|
+
this.bufferGeneration = this.changer.bufferGeneration();
|
|
5020
6406
|
}
|
|
5021
6407
|
/**
|
|
5022
6408
|
* Returns the number of frames we can actually process given the
|
|
@@ -5089,9 +6475,13 @@ function registerSonareRealtimeVoiceChangerWorkletProcessor(name = "sonare-realt
|
|
|
5089
6475
|
scope.registerProcessor(name, RegisteredSonareRealtimeVoiceChangerWorkletProcessor);
|
|
5090
6476
|
}
|
|
5091
6477
|
export {
|
|
6478
|
+
SONARE_CLIP_PAGE_REQUEST_RING_HEADER_INTS,
|
|
6479
|
+
SONARE_CLIP_PAGE_REQUEST_RING_RECORD_UINT32S,
|
|
5092
6480
|
SONARE_ENGINE_COMMAND_RECORD_BYTES,
|
|
5093
6481
|
SONARE_ENGINE_RING_HEADER_INTS,
|
|
5094
6482
|
SONARE_ENGINE_TELEMETRY_RECORD_BYTES,
|
|
6483
|
+
SONARE_EXTERNAL_MIDI_RING_HEADER_INTS,
|
|
6484
|
+
SONARE_EXTERNAL_MIDI_RING_RECORD_UINT32S,
|
|
5095
6485
|
SONARE_METER_RING_HEADER_INTS,
|
|
5096
6486
|
SONARE_METER_RING_RECORD_FLOATS,
|
|
5097
6487
|
SONARE_SCOPE_RING_HEADER_INTS,
|
|
@@ -5104,8 +6494,11 @@ export {
|
|
|
5104
6494
|
SonareRealtimeEngineWorkletProcessor,
|
|
5105
6495
|
SonareRealtimeVoiceChangerWorkletProcessor,
|
|
5106
6496
|
SonareWorkletProcessor,
|
|
6497
|
+
attachOpfsClipStream,
|
|
6498
|
+
createSonareClipPageRequestRingBuffer,
|
|
5107
6499
|
createSonareEngineCommandRingBuffer,
|
|
5108
6500
|
createSonareEngineTelemetryRingBuffer,
|
|
6501
|
+
createSonareExternalMidiRingBuffer,
|
|
5109
6502
|
createSonareMeterRingBuffer,
|
|
5110
6503
|
createSonareScopeRingBuffer,
|
|
5111
6504
|
createSonareSpectrumRingBuffer,
|
|
@@ -5115,16 +6508,22 @@ export {
|
|
|
5115
6508
|
init,
|
|
5116
6509
|
isInitialized,
|
|
5117
6510
|
popSonareEngineCommandRingBuffer,
|
|
6511
|
+
pushSonareClipPageRequestRingBuffer,
|
|
5118
6512
|
pushSonareEngineCommandRingBuffer,
|
|
6513
|
+
pushSonareExternalMidiRingBuffer,
|
|
6514
|
+
readSonareClipPageRequestRingBuffer,
|
|
5119
6515
|
readSonareEngineTelemetryRingBuffer,
|
|
6516
|
+
readSonareExternalMidiRingBuffer,
|
|
5120
6517
|
readSonareMeterRingBuffer,
|
|
5121
6518
|
readSonareScopeRingBuffer,
|
|
5122
6519
|
readSonareSpectrumRingBuffer,
|
|
5123
6520
|
registerSonareRealtimeEngineWorkletProcessor,
|
|
5124
6521
|
registerSonareRealtimeVoiceChangerWorkletProcessor,
|
|
5125
6522
|
registerSonareWorkletProcessor,
|
|
6523
|
+
sonareClipPageRequestRingBufferByteLength,
|
|
5126
6524
|
sonareEngineCommandRingBufferByteLength,
|
|
5127
6525
|
sonareEngineTelemetryRingBufferByteLength,
|
|
6526
|
+
sonareExternalMidiRingBufferByteLength,
|
|
5128
6527
|
sonareMeterRingBufferByteLength,
|
|
5129
6528
|
sonareScopeRingBufferByteLength,
|
|
5130
6529
|
sonareSpectrumRingBufferByteLength,
|