@onda-lang/webaudio 0.7.5 → 0.8.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 +124 -14
- package/api.md +407 -0
- package/package.json +5 -3
- package/src/execution-output-ring.js +234 -0
- package/src/index.d.ts +75 -3
- package/src/index.js +472 -10
- package/src/worklet.js +414 -45
package/src/index.js
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createParamControl,
|
|
3
|
+
decodeDelegateRecords,
|
|
4
|
+
formatPrintRecords,
|
|
3
5
|
validateProcessorArtifact,
|
|
4
6
|
validateProcessorModule,
|
|
5
7
|
} from "@onda-lang/processor-abi";
|
|
8
|
+
import {
|
|
9
|
+
EXECUTION_OPERATION_EVENT,
|
|
10
|
+
EXECUTION_OPERATION_INIT,
|
|
11
|
+
EXECUTION_OPERATION_PROCESS,
|
|
12
|
+
EXECUTION_OPERATION_TRANSPORT,
|
|
13
|
+
EXECUTION_OUTPUT_RING_WAKE_INDEX,
|
|
14
|
+
createExecutionOutputRing,
|
|
15
|
+
drainExecutionOutputRing,
|
|
16
|
+
openExecutionOutputRing,
|
|
17
|
+
} from "./execution-output-ring.js";
|
|
6
18
|
|
|
7
19
|
export {
|
|
8
20
|
createParamDomain,
|
|
@@ -13,8 +25,12 @@ export {
|
|
|
13
25
|
} from "@onda-lang/processor-abi";
|
|
14
26
|
|
|
15
27
|
export const ONDA_AUDIO_WORKLET_PROCESSOR_NAME = "onda-wasm-processor";
|
|
28
|
+
export const ONDA_INIT_PRESERVE_PINNED = 0;
|
|
29
|
+
export const ONDA_INIT_FULL = 1;
|
|
16
30
|
|
|
17
31
|
const registrationByContext = new WeakMap();
|
|
32
|
+
const DEFAULT_DELEGATE_CAPACITY_BYTES = 64 * 1024;
|
|
33
|
+
const DEFAULT_PRINT_CAPACITY_BYTES = 64 * 1024;
|
|
18
34
|
|
|
19
35
|
export function flattenedAudioChannelCount(ports = []) {
|
|
20
36
|
if (!Array.isArray(ports)) {
|
|
@@ -48,6 +64,9 @@ function audioWorkletNodeOptionsFromValidated(
|
|
|
48
64
|
options,
|
|
49
65
|
validateCompiledModule,
|
|
50
66
|
) {
|
|
67
|
+
if (options.onPrint !== undefined && typeof options.onPrint !== "function") {
|
|
68
|
+
throw new TypeError("initial print listener must be a function");
|
|
69
|
+
}
|
|
51
70
|
const inputChannels = flattenedAudioChannelCount(metadata.metadata.inputs);
|
|
52
71
|
const outputChannels = flattenedAudioChannelCount(metadata.metadata.outputs);
|
|
53
72
|
if (inputChannels > 32 || outputChannels > 32) {
|
|
@@ -61,6 +80,27 @@ function audioWorkletNodeOptionsFromValidated(
|
|
|
61
80
|
if (validateCompiledModule) {
|
|
62
81
|
validateProcessorModule(options.compiledModule, metadata);
|
|
63
82
|
}
|
|
83
|
+
const delegateCapacityBytes = configuredExecutionOutputCapacity(
|
|
84
|
+
metadata.metadata?.delegates,
|
|
85
|
+
options.delegateCapacityBytes,
|
|
86
|
+
DEFAULT_DELEGATE_CAPACITY_BYTES,
|
|
87
|
+
"delegate",
|
|
88
|
+
);
|
|
89
|
+
const printCapacityBytes = configuredExecutionOutputCapacity(
|
|
90
|
+
metadata.metadata?.log_sites,
|
|
91
|
+
options.printCapacityBytes,
|
|
92
|
+
DEFAULT_PRINT_CAPACITY_BYTES,
|
|
93
|
+
"print",
|
|
94
|
+
);
|
|
95
|
+
const executionOutputRing = createExecutionOutputRing(
|
|
96
|
+
delegateCapacityBytes,
|
|
97
|
+
printCapacityBytes,
|
|
98
|
+
);
|
|
99
|
+
if (options.onPrint !== undefined && executionOutputRing === null) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
"print delivery requires SharedArrayBuffer; enable cross-origin isolation",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
64
104
|
const nodeOptions = {
|
|
65
105
|
...options.nodeOptions,
|
|
66
106
|
numberOfInputs: inputChannels ? 1 : 0,
|
|
@@ -80,6 +120,12 @@ function audioWorkletNodeOptionsFromValidated(
|
|
|
80
120
|
),
|
|
81
121
|
buffers: options.buffers ?? {},
|
|
82
122
|
eventPayloadCapacityBytes: options.eventPayloadCapacityBytes,
|
|
123
|
+
delegateCapacityBytes,
|
|
124
|
+
printCapacityBytes,
|
|
125
|
+
executionOutputRing,
|
|
126
|
+
printCollectionEnabled: options.onPrint !== undefined,
|
|
127
|
+
printSubscriptionId: options.onPrint === undefined ? 0 : 1,
|
|
128
|
+
initialize: options.initialize === true,
|
|
83
129
|
},
|
|
84
130
|
};
|
|
85
131
|
if (outputChannels) {
|
|
@@ -90,6 +136,22 @@ function audioWorkletNodeOptionsFromValidated(
|
|
|
90
136
|
return nodeOptions;
|
|
91
137
|
}
|
|
92
138
|
|
|
139
|
+
function configuredExecutionOutputCapacity(
|
|
140
|
+
descriptors,
|
|
141
|
+
configured,
|
|
142
|
+
defaultCapacity,
|
|
143
|
+
name,
|
|
144
|
+
) {
|
|
145
|
+
const capacity = configured
|
|
146
|
+
?? (Array.isArray(descriptors) && descriptors.length ? defaultCapacity : 0);
|
|
147
|
+
if (!Number.isSafeInteger(capacity) || capacity < 0 || capacity > 0x7fff_ffff) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`${name} capacity must be an integer from 0 through 2147483647 bytes`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return capacity;
|
|
153
|
+
}
|
|
154
|
+
|
|
93
155
|
function paramInfoFor(paramInfo, selector) {
|
|
94
156
|
const info = Number.isInteger(selector)
|
|
95
157
|
? paramInfo[selector]
|
|
@@ -155,6 +217,18 @@ export async function registerOndaAudioWorklet(
|
|
|
155
217
|
}
|
|
156
218
|
|
|
157
219
|
export async function createOndaAudioProcessor(context, artifact, options = {}) {
|
|
220
|
+
return createOndaAudioProcessorImpl(context, artifact, options, false);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function createOndaAudioProcessorInitialized(
|
|
224
|
+
context,
|
|
225
|
+
artifact,
|
|
226
|
+
options = {},
|
|
227
|
+
) {
|
|
228
|
+
return createOndaAudioProcessorImpl(context, artifact, options, true);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function createOndaAudioProcessorImpl(context, artifact, options, initialize) {
|
|
158
232
|
const validated = validateExecutableArtifact(artifact, false);
|
|
159
233
|
validateContextSampleRate(context, validated.metadata);
|
|
160
234
|
if (options.compiledModule !== undefined) {
|
|
@@ -170,16 +244,22 @@ export async function createOndaAudioProcessor(context, artifact, options = {})
|
|
|
170
244
|
if (typeof NodeConstructor !== "function") {
|
|
171
245
|
throw new Error("AudioWorkletNode is not available in this environment");
|
|
172
246
|
}
|
|
247
|
+
const nodeOptions = audioWorkletNodeOptionsFromValidated(
|
|
248
|
+
validated,
|
|
249
|
+
{ ...options, compiledModule, initialize },
|
|
250
|
+
false,
|
|
251
|
+
);
|
|
173
252
|
const node = new NodeConstructor(
|
|
174
253
|
context,
|
|
175
254
|
ONDA_AUDIO_WORKLET_PROCESSOR_NAME,
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
255
|
+
nodeOptions,
|
|
256
|
+
);
|
|
257
|
+
return new OndaAudioProcessor(
|
|
258
|
+
node,
|
|
259
|
+
validated.metadata,
|
|
260
|
+
options.onPrint,
|
|
261
|
+
nodeOptions.processorOptions.executionOutputRing,
|
|
181
262
|
);
|
|
182
|
-
return new OndaAudioProcessor(node, validated.metadata);
|
|
183
263
|
}
|
|
184
264
|
|
|
185
265
|
export async function compileOndaProcessorModule(artifact) {
|
|
@@ -195,13 +275,43 @@ async function compileValidatedProcessorModule({ wasm, metadata }) {
|
|
|
195
275
|
}
|
|
196
276
|
|
|
197
277
|
export class OndaAudioProcessor {
|
|
198
|
-
constructor(
|
|
278
|
+
constructor(
|
|
279
|
+
node,
|
|
280
|
+
metadata = null,
|
|
281
|
+
initialPrintListener = undefined,
|
|
282
|
+
executionOutputRing = null,
|
|
283
|
+
) {
|
|
284
|
+
if (
|
|
285
|
+
initialPrintListener !== undefined
|
|
286
|
+
&& typeof initialPrintListener !== "function"
|
|
287
|
+
) {
|
|
288
|
+
throw new TypeError("initial print listener must be a function");
|
|
289
|
+
}
|
|
199
290
|
this.node = node;
|
|
200
291
|
this.metadata = metadata;
|
|
201
292
|
this.paramInfo = metadata?.metadata?.params ?? null;
|
|
202
293
|
this.paramControls = new WeakMap();
|
|
203
294
|
this.nextRequestId = 1;
|
|
204
295
|
this.pending = new Map();
|
|
296
|
+
this.delegateListeners = new Set();
|
|
297
|
+
this.delegateSubscriptionId = 0;
|
|
298
|
+
this.printListeners = new Set(
|
|
299
|
+
initialPrintListener === undefined ? [] : [initialPrintListener],
|
|
300
|
+
);
|
|
301
|
+
this.printSubscriptionId = initialPrintListener === undefined ? 0 : 1;
|
|
302
|
+
this.executionOutputRing = executionOutputRing === null
|
|
303
|
+
? null
|
|
304
|
+
: openExecutionOutputRing(executionOutputRing);
|
|
305
|
+
if (initialPrintListener !== undefined && !this.executionOutputRing) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
"print delivery requires SharedArrayBuffer; enable cross-origin isolation",
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
this.executionOutputWaitGeneration = 0;
|
|
311
|
+
this.executionOutputPoll = null;
|
|
312
|
+
this.executionOutputDrainActive = false;
|
|
313
|
+
this.closed = false;
|
|
314
|
+
this.closeReason = null;
|
|
205
315
|
this.handleMessage = (event) => {
|
|
206
316
|
const message = event.data ?? {};
|
|
207
317
|
if (message.requestId === undefined) return;
|
|
@@ -216,9 +326,243 @@ export class OndaAudioProcessor {
|
|
|
216
326
|
};
|
|
217
327
|
node.port.addEventListener("message", this.handleMessage);
|
|
218
328
|
node.port.start?.();
|
|
329
|
+
if (this.printListeners.size !== 0) this.startExecutionOutputDrain();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
startExecutionOutputDrain() {
|
|
333
|
+
if (!this.executionOutputRing || this.executionOutputDrainActive) return;
|
|
334
|
+
this.executionOutputDrainActive = true;
|
|
335
|
+
this.drainExecutionOutput();
|
|
336
|
+
if (typeof Atomics.waitAsync !== "function") {
|
|
337
|
+
this.executionOutputPoll = setInterval(() => {
|
|
338
|
+
if (!this.closed) this.drainExecutionOutput();
|
|
339
|
+
}, 16);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const generation = ++this.executionOutputWaitGeneration;
|
|
343
|
+
const wait = async () => {
|
|
344
|
+
while (!this.closed && generation === this.executionOutputWaitGeneration) {
|
|
345
|
+
const expected = Atomics.load(
|
|
346
|
+
this.executionOutputRing.control,
|
|
347
|
+
EXECUTION_OUTPUT_RING_WAKE_INDEX,
|
|
348
|
+
);
|
|
349
|
+
this.drainExecutionOutput();
|
|
350
|
+
if (this.closed || generation !== this.executionOutputWaitGeneration) return;
|
|
351
|
+
const result = Atomics.waitAsync(
|
|
352
|
+
this.executionOutputRing.control,
|
|
353
|
+
EXECUTION_OUTPUT_RING_WAKE_INDEX,
|
|
354
|
+
expected,
|
|
355
|
+
);
|
|
356
|
+
if (result.async) await result.value;
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
void wait().catch((error) => this.reportExecutionOutputError(error));
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
stopExecutionOutputDrain() {
|
|
363
|
+
if (!this.executionOutputDrainActive) return;
|
|
364
|
+
this.executionOutputDrainActive = false;
|
|
365
|
+
this.executionOutputWaitGeneration += 1;
|
|
366
|
+
if (this.executionOutputPoll !== null) {
|
|
367
|
+
clearInterval(this.executionOutputPoll);
|
|
368
|
+
this.executionOutputPoll = null;
|
|
369
|
+
}
|
|
370
|
+
if (!this.executionOutputRing) return;
|
|
371
|
+
Atomics.add(
|
|
372
|
+
this.executionOutputRing.control,
|
|
373
|
+
EXECUTION_OUTPUT_RING_WAKE_INDEX,
|
|
374
|
+
1,
|
|
375
|
+
);
|
|
376
|
+
Atomics.notify(
|
|
377
|
+
this.executionOutputRing.control,
|
|
378
|
+
EXECUTION_OUTPUT_RING_WAKE_INDEX,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
reportExecutionOutputError(error) {
|
|
383
|
+
queueMicrotask(() => {
|
|
384
|
+
throw error;
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
notifyExecutionOutputListeners(listeners, batch) {
|
|
389
|
+
for (const listener of listeners) {
|
|
390
|
+
try {
|
|
391
|
+
listener(batch);
|
|
392
|
+
} catch (error) {
|
|
393
|
+
this.reportExecutionOutputError(error);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
drainExecutionOutput() {
|
|
399
|
+
if (!this.executionOutputRing) return 0;
|
|
400
|
+
return drainExecutionOutputRing(
|
|
401
|
+
this.executionOutputRing,
|
|
402
|
+
(entry) => {
|
|
403
|
+
try {
|
|
404
|
+
this.consumeExecutionOutput(entry);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
this.reportExecutionOutputError(error);
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
consumeExecutionOutput(entry) {
|
|
413
|
+
const operation = this.executionOperationName(
|
|
414
|
+
entry.operation,
|
|
415
|
+
entry.operationIndex,
|
|
416
|
+
);
|
|
417
|
+
const records = [];
|
|
418
|
+
let print = null;
|
|
419
|
+
let delegate = null;
|
|
420
|
+
if (
|
|
421
|
+
entry.printSubscriptionId === this.printSubscriptionId
|
|
422
|
+
&& this.printListeners.size !== 0
|
|
423
|
+
&& (
|
|
424
|
+
entry.printUsed
|
|
425
|
+
|| entry.printRecordCount
|
|
426
|
+
|| entry.printOverflowCount
|
|
427
|
+
|| entry.printTransportDropCount
|
|
428
|
+
)
|
|
429
|
+
) {
|
|
430
|
+
const formatted = formatPrintRecords(
|
|
431
|
+
entry.printStorage,
|
|
432
|
+
entry.printUsed,
|
|
433
|
+
this.metadata,
|
|
434
|
+
entry.printOverflowCount,
|
|
435
|
+
);
|
|
436
|
+
if (formatted.entries.length !== entry.printRecordCount) {
|
|
437
|
+
throw new Error("processor print count does not match packed storage");
|
|
438
|
+
}
|
|
439
|
+
const lines = formatted.text.match(/.*\n/g) ?? [];
|
|
440
|
+
if (lines.length !== formatted.entries.length) {
|
|
441
|
+
throw new Error("formatted print output does not match its decoded entries");
|
|
442
|
+
}
|
|
443
|
+
print = {
|
|
444
|
+
overflowCount: formatted.overflowCount,
|
|
445
|
+
transportDropCount: entry.printTransportDropCount,
|
|
446
|
+
};
|
|
447
|
+
formatted.entries.forEach((decoded, index) => records.push({
|
|
448
|
+
kind: "print",
|
|
449
|
+
sequence: decoded.sequence,
|
|
450
|
+
entry: decoded,
|
|
451
|
+
line: lines[index],
|
|
452
|
+
}));
|
|
453
|
+
}
|
|
454
|
+
if (
|
|
455
|
+
entry.delegateSubscriptionId === this.delegateSubscriptionId
|
|
456
|
+
&& this.delegateListeners.size !== 0
|
|
457
|
+
&& (
|
|
458
|
+
entry.delegateUsed
|
|
459
|
+
|| entry.delegateRecordCount
|
|
460
|
+
|| entry.delegateOverflowCount
|
|
461
|
+
|| entry.delegateTransportDropCount
|
|
462
|
+
)
|
|
463
|
+
) {
|
|
464
|
+
const decodedRecords = decodeDelegateRecords(
|
|
465
|
+
entry.delegateStorage,
|
|
466
|
+
entry.delegateUsed,
|
|
467
|
+
this.metadata?.metadata?.delegates,
|
|
468
|
+
this.metadata?.target?.byte_order,
|
|
469
|
+
);
|
|
470
|
+
if (decodedRecords.length !== entry.delegateRecordCount) {
|
|
471
|
+
throw new Error("processor delegate count does not match packed storage");
|
|
472
|
+
}
|
|
473
|
+
delegate = {
|
|
474
|
+
overflowCount: entry.delegateOverflowCount,
|
|
475
|
+
transportDropCount: entry.delegateTransportDropCount,
|
|
476
|
+
};
|
|
477
|
+
decodedRecords.forEach((record) => {
|
|
478
|
+
const occurrence = {
|
|
479
|
+
sequence: record.sequence,
|
|
480
|
+
index: record.delegateIndex,
|
|
481
|
+
name: record.name,
|
|
482
|
+
values: record.values,
|
|
483
|
+
};
|
|
484
|
+
records.push({
|
|
485
|
+
kind: "delegate",
|
|
486
|
+
sequence: occurrence.sequence,
|
|
487
|
+
occurrence,
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
this.deliverExecutionOutput(records, print, delegate, operation);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
executionOperationName(operation, operationIndex) {
|
|
495
|
+
if (operation === EXECUTION_OPERATION_INIT) return "processor init";
|
|
496
|
+
if (operation === EXECUTION_OPERATION_PROCESS) return "process";
|
|
497
|
+
if (operation === EXECUTION_OPERATION_TRANSPORT) return "transport";
|
|
498
|
+
if (operation === EXECUTION_OPERATION_EVENT) {
|
|
499
|
+
const event = this.metadata?.metadata?.events?.[operationIndex];
|
|
500
|
+
if (!event) throw new Error("execution-output ring references an unknown event");
|
|
501
|
+
return `event '${event.name}'`;
|
|
502
|
+
}
|
|
503
|
+
throw new Error("execution-output ring contains an unknown operation");
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
deliverExecutionOutput(records, print, delegate, operation) {
|
|
507
|
+
records.sort((lhs, rhs) => lhs.sequence - rhs.sequence);
|
|
508
|
+
let cursor = 0;
|
|
509
|
+
while (cursor < records.length) {
|
|
510
|
+
const kind = records[cursor].kind;
|
|
511
|
+
let end = cursor + 1;
|
|
512
|
+
while (end < records.length && records[end].kind === kind) end += 1;
|
|
513
|
+
const batchRecords = records.slice(cursor, end);
|
|
514
|
+
if (kind === "print") {
|
|
515
|
+
const meta = print ?? { overflowCount: 0, transportDropCount: 0 };
|
|
516
|
+
print = null;
|
|
517
|
+
const batch = {
|
|
518
|
+
type: "onda-print",
|
|
519
|
+
operation,
|
|
520
|
+
text: batchRecords.map((record) => record.line).join(""),
|
|
521
|
+
entries: batchRecords.map((record) => record.entry),
|
|
522
|
+
...meta,
|
|
523
|
+
};
|
|
524
|
+
this.notifyExecutionOutputListeners(this.printListeners, batch);
|
|
525
|
+
} else {
|
|
526
|
+
const meta = delegate ?? { overflowCount: 0, transportDropCount: 0 };
|
|
527
|
+
delegate = null;
|
|
528
|
+
const batch = {
|
|
529
|
+
type: "onda-delegates",
|
|
530
|
+
operation,
|
|
531
|
+
occurrences: batchRecords.map((record) => record.occurrence),
|
|
532
|
+
...meta,
|
|
533
|
+
};
|
|
534
|
+
this.notifyExecutionOutputListeners(this.delegateListeners, batch);
|
|
535
|
+
}
|
|
536
|
+
cursor = end;
|
|
537
|
+
}
|
|
538
|
+
if (print && (print.overflowCount || print.transportDropCount)) {
|
|
539
|
+
const batch = {
|
|
540
|
+
type: "onda-print",
|
|
541
|
+
operation,
|
|
542
|
+
text: "",
|
|
543
|
+
entries: [],
|
|
544
|
+
...print,
|
|
545
|
+
};
|
|
546
|
+
this.notifyExecutionOutputListeners(this.printListeners, batch);
|
|
547
|
+
}
|
|
548
|
+
if (
|
|
549
|
+
delegate
|
|
550
|
+
&& (delegate.overflowCount || delegate.transportDropCount)
|
|
551
|
+
) {
|
|
552
|
+
const batch = {
|
|
553
|
+
type: "onda-delegates",
|
|
554
|
+
operation,
|
|
555
|
+
occurrences: [],
|
|
556
|
+
...delegate,
|
|
557
|
+
};
|
|
558
|
+
this.notifyExecutionOutputListeners(this.delegateListeners, batch);
|
|
559
|
+
}
|
|
219
560
|
}
|
|
220
561
|
|
|
221
562
|
request(type, fields = {}, transfer = []) {
|
|
563
|
+
if (this.closed) {
|
|
564
|
+
return Promise.reject(this.closeReason);
|
|
565
|
+
}
|
|
222
566
|
const requestId = this.nextRequestId++;
|
|
223
567
|
return new Promise((resolve, reject) => {
|
|
224
568
|
this.pending.set(requestId, { resolve, reject });
|
|
@@ -271,8 +615,87 @@ export class OndaAudioProcessor {
|
|
|
271
615
|
return this.request("event", { event, values });
|
|
272
616
|
}
|
|
273
617
|
|
|
274
|
-
|
|
275
|
-
|
|
618
|
+
onDelegates(listener) {
|
|
619
|
+
this.assertOpen();
|
|
620
|
+
if (typeof listener !== "function") {
|
|
621
|
+
throw new TypeError("delegate listener must be a function");
|
|
622
|
+
}
|
|
623
|
+
this.requireExecutionOutputRing("delegate");
|
|
624
|
+
const wasEmpty = this.delegateListeners.size === 0;
|
|
625
|
+
this.delegateListeners.add(listener);
|
|
626
|
+
if (wasEmpty) {
|
|
627
|
+
this.delegateSubscriptionId = (this.delegateSubscriptionId + 1) >>> 0;
|
|
628
|
+
if (this.delegateSubscriptionId === 0) this.delegateSubscriptionId = 1;
|
|
629
|
+
try {
|
|
630
|
+
this.startExecutionOutputDrain();
|
|
631
|
+
this.node.port.postMessage({
|
|
632
|
+
type: "delegate-subscription",
|
|
633
|
+
enabled: true,
|
|
634
|
+
subscriptionId: this.delegateSubscriptionId,
|
|
635
|
+
});
|
|
636
|
+
} catch (error) {
|
|
637
|
+
this.delegateListeners.delete(listener);
|
|
638
|
+
if (this.printListeners.size === 0) this.stopExecutionOutputDrain();
|
|
639
|
+
throw error;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return () => {
|
|
643
|
+
const removed = this.delegateListeners.delete(listener);
|
|
644
|
+
if (removed && this.delegateListeners.size === 0) {
|
|
645
|
+
this.node.port.postMessage({
|
|
646
|
+
type: "delegate-subscription",
|
|
647
|
+
enabled: false,
|
|
648
|
+
subscriptionId: this.delegateSubscriptionId,
|
|
649
|
+
});
|
|
650
|
+
if (this.printListeners.size === 0) this.stopExecutionOutputDrain();
|
|
651
|
+
}
|
|
652
|
+
return removed;
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
onPrint(listener) {
|
|
657
|
+
this.assertOpen();
|
|
658
|
+
if (typeof listener !== "function") {
|
|
659
|
+
throw new TypeError("print listener must be a function");
|
|
660
|
+
}
|
|
661
|
+
this.requireExecutionOutputRing("print");
|
|
662
|
+
const wasEmpty = this.printListeners.size === 0;
|
|
663
|
+
this.printListeners.add(listener);
|
|
664
|
+
if (wasEmpty) {
|
|
665
|
+
this.printSubscriptionId = (this.printSubscriptionId + 1) >>> 0;
|
|
666
|
+
if (this.printSubscriptionId === 0) this.printSubscriptionId = 1;
|
|
667
|
+
try {
|
|
668
|
+
this.startExecutionOutputDrain();
|
|
669
|
+
this.node.port.postMessage({
|
|
670
|
+
type: "print-subscription",
|
|
671
|
+
enabled: true,
|
|
672
|
+
subscriptionId: this.printSubscriptionId,
|
|
673
|
+
});
|
|
674
|
+
} catch (error) {
|
|
675
|
+
this.printListeners.delete(listener);
|
|
676
|
+
if (this.delegateListeners.size === 0) this.stopExecutionOutputDrain();
|
|
677
|
+
throw error;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return () => {
|
|
681
|
+
const removed = this.printListeners.delete(listener);
|
|
682
|
+
if (removed && this.printListeners.size === 0) {
|
|
683
|
+
this.node.port.postMessage({
|
|
684
|
+
type: "print-subscription",
|
|
685
|
+
enabled: false,
|
|
686
|
+
subscriptionId: this.printSubscriptionId,
|
|
687
|
+
});
|
|
688
|
+
if (this.delegateListeners.size === 0) this.stopExecutionOutputDrain();
|
|
689
|
+
}
|
|
690
|
+
return removed;
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
init(mode) {
|
|
695
|
+
if (mode !== ONDA_INIT_PRESERVE_PINNED && mode !== ONDA_INIT_FULL) {
|
|
696
|
+
return Promise.reject(new Error(`invalid Onda init mode '${String(mode)}'`));
|
|
697
|
+
}
|
|
698
|
+
return this.request("init", { mode });
|
|
276
699
|
}
|
|
277
700
|
|
|
278
701
|
async snapshot() {
|
|
@@ -295,9 +718,48 @@ export class OndaAudioProcessor {
|
|
|
295
718
|
}
|
|
296
719
|
|
|
297
720
|
close(reason = new Error("Onda AudioWorklet processor closed")) {
|
|
721
|
+
if (this.closed) return;
|
|
722
|
+
this.closed = true;
|
|
723
|
+
this.closeReason = reason instanceof Error ? reason : new Error(String(reason));
|
|
724
|
+
if (this.delegateListeners.size !== 0) {
|
|
725
|
+
try {
|
|
726
|
+
this.node.port.postMessage({
|
|
727
|
+
type: "delegate-subscription",
|
|
728
|
+
enabled: false,
|
|
729
|
+
subscriptionId: this.delegateSubscriptionId,
|
|
730
|
+
});
|
|
731
|
+
} catch {
|
|
732
|
+
// The underlying node may already be gone; local closure still proceeds.
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
if (this.printListeners.size !== 0) {
|
|
736
|
+
try {
|
|
737
|
+
this.node.port.postMessage({
|
|
738
|
+
type: "print-subscription",
|
|
739
|
+
enabled: false,
|
|
740
|
+
subscriptionId: this.printSubscriptionId,
|
|
741
|
+
});
|
|
742
|
+
} catch {
|
|
743
|
+
// The underlying node may already be gone; local closure still proceeds.
|
|
744
|
+
}
|
|
745
|
+
}
|
|
298
746
|
this.node.port.removeEventListener("message", this.handleMessage);
|
|
299
|
-
|
|
747
|
+
this.stopExecutionOutputDrain();
|
|
748
|
+
for (const pending of this.pending.values()) pending.reject(this.closeReason);
|
|
300
749
|
this.pending.clear();
|
|
750
|
+
this.delegateListeners.clear();
|
|
751
|
+
this.printListeners.clear();
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
assertOpen() {
|
|
755
|
+
if (this.closed) throw this.closeReason;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
requireExecutionOutputRing(kind) {
|
|
759
|
+
if (this.executionOutputRing) return;
|
|
760
|
+
throw new Error(
|
|
761
|
+
`${kind} delivery requires SharedArrayBuffer; enable cross-origin isolation`,
|
|
762
|
+
);
|
|
301
763
|
}
|
|
302
764
|
}
|
|
303
765
|
|