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