@onda-lang/webaudio 0.7.5 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/worklet.js CHANGED
@@ -1,9 +1,42 @@
1
+ import {
2
+ EXECUTION_OPERATION_EVENT,
3
+ EXECUTION_OPERATION_INIT,
4
+ EXECUTION_OPERATION_PROCESS,
5
+ EXECUTION_OPERATION_TRANSPORT,
6
+ openExecutionOutputRing,
7
+ writeExecutionOutputRing,
8
+ } from "./execution-output-ring.js";
9
+
1
10
  const ONDA_PROCESS_BEGIN_BLOCK = 1 << 0;
2
11
  const ONDA_PROCESS_END_BLOCK = 1 << 1;
12
+ const ONDA_INIT_PRESERVE_PINNED = 0;
13
+ const ONDA_INIT_FULL = 1;
3
14
  const ONDA_AUDIO_WORKLET_PROCESSOR_NAME = "onda-wasm-processor";
4
15
  const DEFAULT_EVENT_PAYLOAD_CAPACITY_BYTES = 64 * 1024;
16
+ const DEFAULT_DELEGATE_CAPACITY_BYTES = 64 * 1024;
17
+ const DEFAULT_PRINT_CAPACITY_BYTES = 64 * 1024;
18
+ const DELEGATE_BATCH_SIZE_BYTES = 20;
19
+ const PRINT_BATCH_SIZE_BYTES = 20;
20
+ const EXECUTION_OUTPUT_SIZE_BYTES = 12;
5
21
  const HOST_LITTLE_ENDIAN = new Uint8Array(new Uint16Array([1]).buffer)[0] === 1;
6
22
 
23
+ function exactI64(value) {
24
+ let integer;
25
+ if (typeof value === "bigint") {
26
+ integer = value;
27
+ } else if (typeof value === "number" && Number.isSafeInteger(value)) {
28
+ integer = BigInt(value);
29
+ } else if (typeof value === "string" && /^-?[0-9]+$/.test(value)) {
30
+ integer = BigInt(value);
31
+ } else {
32
+ throw new TypeError("i64 value must be a bigint, safe integer, or decimal integer string");
33
+ }
34
+ if (BigInt.asIntN(64, integer) !== integer) {
35
+ throw new RangeError(`i64 value '${integer}' is outside the signed 64-bit range`);
36
+ }
37
+ return integer;
38
+ }
39
+
7
40
  class OndaWasmProcessor extends AudioWorkletProcessor {
8
41
  constructor(options) {
9
42
  super();
@@ -47,6 +80,8 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
47
80
  this.stateBytes = null;
48
81
  this.inputViews = [];
49
82
  this.outputViews = [];
83
+ this.delegateRecordView = null;
84
+ this.printRecordView = null;
50
85
 
51
86
  const heapBase = Number(
52
87
  this.exports.__heap_base?.value ?? this.exports.__heap_base ?? 0,
@@ -64,6 +99,12 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
64
99
  this.eventInfo = Array.isArray(metadata.metadata?.events)
65
100
  ? metadata.metadata.events
66
101
  : [];
102
+ this.delegateInfo = Array.isArray(metadata.metadata?.delegates)
103
+ ? metadata.metadata.delegates
104
+ : [];
105
+ this.logSiteInfo = Array.isArray(metadata.metadata?.log_sites)
106
+ ? metadata.metadata.log_sites
107
+ : [];
67
108
  this.controlOutputInfo = Array.isArray(metadata.metadata?.control_outputs)
68
109
  ? metadata.metadata.control_outputs
69
110
  : [];
@@ -115,7 +156,6 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
115
156
  this.outputPtrs = [];
116
157
  this.outputCapacityFrames = 0;
117
158
  this.blockCursor = 0;
118
- this.executionFailed = false;
119
159
 
120
160
  const paramBytes = Number(metadata.runtime?.param_size_bytes ?? 0);
121
161
  if (!Number.isInteger(paramBytes) || paramBytes < 0) {
@@ -148,13 +188,101 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
148
188
  this.eventPayloadPtr = this.eventPayloadCapacity
149
189
  ? this.alloc(this.eventPayloadCapacity, 8)
150
190
  : 0;
191
+ this.delegateCapacity = this.configuredDelegateCapacity(
192
+ processorOptions.delegateCapacityBytes,
193
+ );
194
+ this.delegateStoragePtr = this.delegateInfo.length && this.delegateCapacity
195
+ ? this.alloc(this.delegateCapacity, 8)
196
+ : 0;
197
+ this.delegateBatchPtr = this.delegateInfo.length
198
+ ? this.alloc(DELEGATE_BATCH_SIZE_BYTES, 4)
199
+ : 0;
200
+ if (this.delegateBatchPtr) {
201
+ const view = new DataView(this.memory.buffer);
202
+ view.setUint32(this.delegateBatchPtr, this.delegateStoragePtr, true);
203
+ view.setUint32(this.delegateBatchPtr + 4, this.delegateCapacity, true);
204
+ view.setUint32(this.delegateBatchPtr + 8, 0, true);
205
+ view.setUint32(this.delegateBatchPtr + 12, 0, true);
206
+ view.setUint32(this.delegateBatchPtr + 16, 0, true);
207
+ }
208
+ this.printCapacity = this.configuredPrintCapacity(
209
+ processorOptions.printCapacityBytes,
210
+ );
211
+ this.printStoragePtr = this.logSiteInfo.length && this.printCapacity
212
+ ? this.alloc(this.printCapacity, 8)
213
+ : 0;
214
+ this.printBatchPtr = this.logSiteInfo.length
215
+ ? this.alloc(PRINT_BATCH_SIZE_BYTES, 4)
216
+ : 0;
217
+ if (this.printBatchPtr) {
218
+ const view = new DataView(this.memory.buffer);
219
+ view.setUint32(this.printBatchPtr, this.printStoragePtr, true);
220
+ view.setUint32(this.printBatchPtr + 4, this.printCapacity, true);
221
+ view.setUint32(this.printBatchPtr + 8, 0, true);
222
+ view.setUint32(this.printBatchPtr + 12, 0, true);
223
+ view.setUint32(this.printBatchPtr + 16, 0, true);
224
+ }
225
+ this.printCollectionEnabled = processorOptions.printCollectionEnabled === true
226
+ && this.printStoragePtr !== 0;
227
+ this.printSubscriptionId = Number.isSafeInteger(processorOptions.printSubscriptionId)
228
+ ? processorOptions.printSubscriptionId
229
+ : 0;
230
+ this.executionOutputPtr = this.delegateBatchPtr || this.printBatchPtr
231
+ ? this.alloc(EXECUTION_OUTPUT_SIZE_BYTES, 4)
232
+ : 0;
233
+ if (this.executionOutputPtr) {
234
+ const view = new DataView(this.memory.buffer);
235
+ view.setUint32(this.executionOutputPtr, 0, true);
236
+ view.setUint32(
237
+ this.executionOutputPtr + 4,
238
+ this.printCollectionEnabled ? this.printBatchPtr : 0,
239
+ true,
240
+ );
241
+ view.setUint32(this.executionOutputPtr + 8, 0, true);
242
+ }
243
+ this.delegateCollectionEnabled = false;
244
+ this.delegateSubscriptionId = 0;
245
+ this.executionOutputRing = processorOptions.executionOutputRing === undefined
246
+ || processorOptions.executionOutputRing === null
247
+ ? null
248
+ : openExecutionOutputRing(processorOptions.executionOutputRing);
249
+ if (
250
+ this.executionOutputRing
251
+ && (
252
+ this.executionOutputRing.delegateCapacity !== this.delegateCapacity
253
+ || this.executionOutputRing.printCapacity !== this.printCapacity
254
+ )
255
+ ) {
256
+ throw new Error("execution-output ring capacities do not match processor storage");
257
+ }
258
+ this.pendingDelegateDrops = 0;
259
+ this.pendingDelegateOverflow = 0;
260
+ this.pendingPrintDrops = 0;
261
+ this.pendingPrintOverflow = 0;
262
+ this.executionOutputEntry = {
263
+ operation: 0,
264
+ operationIndex: 0,
265
+ delegateSubscriptionId: 0,
266
+ delegateUsed: 0,
267
+ delegateRecordCount: 0,
268
+ delegateOverflowCount: 0,
269
+ delegateTransportDropCount: 0,
270
+ printSubscriptionId: 0,
271
+ printUsed: 0,
272
+ printRecordCount: 0,
273
+ printOverflowCount: 0,
274
+ printTransportDropCount: 0,
275
+ };
151
276
  this.writeParamDefaults();
152
277
  this.writeInitialParams(processorOptions.params ?? {});
153
278
  this.ensureInputCapacity(this.blockSize);
154
279
  this.ensureOutputCapacity(this.blockSize);
155
280
  this.viewsReady = true;
156
281
  this.refreshMemoryCache(true);
157
- this.reset();
282
+ this.invalidateState();
283
+ if (processorOptions.initialize === true) {
284
+ this.init(ONDA_INIT_FULL);
285
+ }
158
286
  this.allocationLocked = true;
159
287
  this.port.onmessage = (event) => this.handleMessage(event.data ?? {});
160
288
  }
@@ -175,7 +303,7 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
175
303
 
176
304
  alloc(size, align) {
177
305
  if (this.allocationLocked) {
178
- throw new Error("Onda worklet memory allocation is locked after initialization");
306
+ throw new Error("Onda worklet memory allocation is locked after construction");
179
307
  }
180
308
  const ptr = this.alignUp(this.heap, align);
181
309
  const next = ptr + size;
@@ -212,6 +340,12 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
212
340
  this.outputCapacityFrames,
213
341
  );
214
342
  });
343
+ this.delegateRecordView = this.delegateStoragePtr
344
+ ? new Uint8Array(buffer, this.delegateStoragePtr, this.delegateCapacity)
345
+ : null;
346
+ this.printRecordView = this.printStoragePtr
347
+ ? new Uint8Array(buffer, this.printStoragePtr, this.printCapacity)
348
+ : null;
215
349
  }
216
350
 
217
351
  memoryView() {
@@ -255,6 +389,32 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
255
389
  return capacity;
256
390
  }
257
391
 
392
+ configuredDelegateCapacity(configuredCapacity) {
393
+ const capacity = configuredCapacity
394
+ ?? (this.delegateInfo.length ? DEFAULT_DELEGATE_CAPACITY_BYTES : 0);
395
+ if (
396
+ !Number.isSafeInteger(capacity)
397
+ || capacity < 0
398
+ || capacity > 0x7fff_ffff
399
+ ) {
400
+ throw new Error(
401
+ "delegate capacity must be an integer from 0 through 2147483647 bytes",
402
+ );
403
+ }
404
+ return capacity;
405
+ }
406
+
407
+ configuredPrintCapacity(configuredCapacity) {
408
+ const capacity = configuredCapacity
409
+ ?? (this.logSiteInfo.length ? DEFAULT_PRINT_CAPACITY_BYTES : 0);
410
+ if (!Number.isSafeInteger(capacity) || capacity < 0 || capacity > 0x7fff_ffff) {
411
+ throw new Error(
412
+ "print capacity must be an integer from 0 through 2147483647 bytes",
413
+ );
414
+ }
415
+ return capacity;
416
+ }
417
+
258
418
  flattenAudioChannels(ports, kind) {
259
419
  const channels = [];
260
420
  for (const port of ports) {
@@ -349,24 +509,73 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
349
509
  );
350
510
  }
351
511
 
352
- reset() {
353
- this.refreshMemoryCache();
354
- this.stateBytes.fill(0);
512
+ init(mode) {
513
+ if (mode !== ONDA_INIT_PRESERVE_PINNED && mode !== ONDA_INIT_FULL) {
514
+ throw new Error(`invalid Onda init mode '${String(mode)}'`);
515
+ }
516
+ if (mode === ONDA_INIT_PRESERVE_PINNED && !this.initialized) {
517
+ throw new Error("full initialization is required before preserving pinned state");
518
+ }
519
+ this.runInitialization(mode);
520
+ }
521
+
522
+ invalidateState() {
523
+ this.initialized = false;
524
+ this.process = this.processPending;
355
525
  this.blockCursor = 0;
356
- this.executionFailed = false;
357
- this.checkExecutionStatus(
358
- this.exports.onda_init(this.paramsPtr, this.statePtr),
359
- "processor init",
526
+ }
527
+
528
+ commitInitializedState(blockCursor) {
529
+ this.initialized = true;
530
+ this.process = this.processInitialized;
531
+ this.blockCursor = blockCursor;
532
+ }
533
+
534
+ runInitialization(mode, afterInitialize) {
535
+ // Reinitializing state does not create a compile-block boundary. Retain
536
+ // the host-side position so the next process segment cannot synthesize an
537
+ // extra BEGIN_BLOCK or postpone the matching END_BLOCK.
538
+ const blockCursor = this.initialized ? this.blockCursor : 0;
539
+ // Generated initialization mutates the live image in place. Stop exposing
540
+ // it before entering Wasm so a failure, including one in snapshot overlay,
541
+ // leaves the processor on the silent pending path.
542
+ this.invalidateState();
543
+ this.refreshMemoryCache();
544
+ this.prepareExecutionOutput();
545
+ const status = this.exports.onda_processor_init(
546
+ this.paramsPtr,
547
+ this.statePtr,
548
+ mode,
549
+ this.bufferPointersPtr,
550
+ this.bufferFramesPtr,
551
+ this.bufferChannelsPtr,
552
+ this.bufferSampleRatesPtr,
553
+ this.executionOutputPtr,
360
554
  );
555
+ // Generated failures retain diagnostic prints but clear delegates.
556
+ this.publishExecutionOutput(
557
+ EXECUTION_OPERATION_INIT,
558
+ 0,
559
+ );
560
+ this.checkExecutionStatus(status, "processor init");
561
+ afterInitialize?.();
562
+ this.commitInitializedState(blockCursor);
563
+ }
564
+
565
+ requireInitialized(operation) {
566
+ if (!this.initialized) {
567
+ throw new Error(`full initialization is required before ${operation}`);
568
+ }
361
569
  }
362
570
 
363
571
  checkExecutionStatus(status, operation) {
364
572
  if (status === 0) return;
365
- this.executionFailed = true;
573
+ this.invalidateState();
366
574
  throw new Error(`${operation} failed with Onda execution status ${String(status)}`);
367
575
  }
368
576
 
369
577
  createSnapshot() {
578
+ this.requireInitialized("snapshot");
370
579
  const snapshot = new Uint8Array(this.snapshotSizeBytes);
371
580
  this.refreshMemoryCache();
372
581
  const state = this.stateBytes;
@@ -391,20 +600,22 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
391
600
  );
392
601
  }
393
602
  // The ABI restore base is a fresh post-init image, so scratch and
394
- // control-mirror state never leak across a restore.
395
- this.reset();
396
- const state = this.stateBytes;
397
- for (const entry of this.snapshotInfo) {
398
- const packedOffset = Number(entry.packed_snapshot_byte_offset);
399
- const physicalOffset = Number(entry.physical_state_byte_offset);
400
- const byteSize = Number(entry.byte_size);
401
- this.validateSnapshotEntry(entry, packedOffset, physicalOffset, byteSize);
402
- state.set(
403
- snapshot.subarray(packedOffset, packedOffset + byteSize),
404
- physicalOffset,
405
- );
406
- this.normalizeSnapshotIntegerRange(entry, state, physicalOffset, byteSize);
407
- }
603
+ // control-mirror state never leak across a restore. Initialization and
604
+ // overlay form one lifecycle transition: neither partial result is ready.
605
+ this.runInitialization(ONDA_INIT_FULL, () => {
606
+ const state = this.stateBytes;
607
+ for (const entry of this.snapshotInfo) {
608
+ const packedOffset = Number(entry.packed_snapshot_byte_offset);
609
+ const physicalOffset = Number(entry.physical_state_byte_offset);
610
+ const byteSize = Number(entry.byte_size);
611
+ this.validateSnapshotEntry(entry, packedOffset, physicalOffset, byteSize);
612
+ state.set(
613
+ snapshot.subarray(packedOffset, packedOffset + byteSize),
614
+ physicalOffset,
615
+ );
616
+ this.normalizeSnapshotIntegerRange(entry, state, physicalOffset, byteSize);
617
+ }
618
+ });
408
619
  }
409
620
 
410
621
  normalizeSnapshotIntegerRange(entry, state, offset, byteSize) {
@@ -494,8 +705,8 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
494
705
  message.value,
495
706
  );
496
707
  this.postResponse(message, { type: "onda-ok", operation: message.type });
497
- } else if (message.type === "reset") {
498
- this.reset();
708
+ } else if (message.type === "init") {
709
+ this.init(message.mode);
499
710
  this.postResponse(message, { type: "onda-ok", operation: message.type });
500
711
  } else if (message.type === "event") {
501
712
  this.dispatchEvent(
@@ -525,6 +736,10 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
525
736
  } else if (message.type === "restore-snapshot") {
526
737
  this.restoreSnapshot(message.snapshot ?? message.bytes);
527
738
  this.postResponse(message, { type: "onda-ok", operation: message.type });
739
+ } else if (message.type === "delegate-subscription") {
740
+ this.setDelegateSubscription(message.enabled, message.subscriptionId);
741
+ } else if (message.type === "print-subscription") {
742
+ this.setPrintSubscription(message.enabled, message.subscriptionId);
528
743
  } else {
529
744
  throw new Error(`unknown Onda worklet operation '${String(message.type)}'`);
530
745
  }
@@ -539,6 +754,7 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
539
754
  }
540
755
 
541
756
  dispatchEvent(selector, values) {
757
+ this.requireInitialized("event dispatch");
542
758
  const eventId = Number.isInteger(selector)
543
759
  ? selector
544
760
  : this.eventInfo.findIndex((event) => event.name === selector);
@@ -598,18 +814,22 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
598
814
  if (typeof handler !== "function") {
599
815
  throw new Error(`missing WebAssembly export '${event.export}'`);
600
816
  }
601
- this.checkExecutionStatus(
602
- handler(
603
- this.eventPayloadPtr,
604
- this.paramsPtr,
605
- this.statePtr,
606
- this.bufferPointersPtr,
607
- this.bufferFramesPtr,
608
- this.bufferChannelsPtr,
609
- this.bufferSampleRatesPtr,
610
- ),
611
- `event '${event.name}'`,
817
+ this.prepareExecutionOutput();
818
+ const status = handler(
819
+ this.eventPayloadPtr,
820
+ this.paramsPtr,
821
+ this.statePtr,
822
+ this.bufferPointersPtr,
823
+ this.bufferFramesPtr,
824
+ this.bufferChannelsPtr,
825
+ this.bufferSampleRatesPtr,
826
+ this.executionOutputPtr,
612
827
  );
828
+ this.publishExecutionOutput(
829
+ EXECUTION_OPERATION_EVENT,
830
+ eventId,
831
+ );
832
+ this.checkExecutionStatus(status, `event '${event.name}'`);
613
833
  }
614
834
 
615
835
  eventValue(event, param, paramId, values) {
@@ -641,6 +861,7 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
641
861
  }
642
862
 
643
863
  readControlOutputs() {
864
+ this.requireInitialized("reading control outputs");
644
865
  return Object.fromEntries(
645
866
  this.controlOutputInfo.map((output) => [
646
867
  output.name,
@@ -652,6 +873,142 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
652
873
  );
653
874
  }
654
875
 
876
+ saturatedAdd(lhs, rhs) {
877
+ return Math.min(0xffff_ffff, lhs + rhs);
878
+ }
879
+
880
+ publishExecutionOutput(operation, operationIndex) {
881
+ if (!this.executionOutputRing) return;
882
+ if (
883
+ !this.delegateCollectionEnabled
884
+ && !this.printCollectionEnabled
885
+ && !this.pendingDelegateDrops
886
+ && !this.pendingDelegateOverflow
887
+ && !this.pendingPrintDrops
888
+ && !this.pendingPrintOverflow
889
+ ) return;
890
+ const view = this.memoryView();
891
+ const delegateUsed = this.delegateCollectionEnabled && this.delegateBatchPtr
892
+ ? view.getUint32(this.delegateBatchPtr + 8, true)
893
+ : 0;
894
+ const delegateRecordCount = this.delegateCollectionEnabled && this.delegateBatchPtr
895
+ ? view.getUint32(this.delegateBatchPtr + 12, true)
896
+ : 0;
897
+ const delegateOverflowCount = this.delegateCollectionEnabled && this.delegateBatchPtr
898
+ ? view.getUint32(this.delegateBatchPtr + 16, true)
899
+ : 0;
900
+ const printUsed = this.printCollectionEnabled && this.printBatchPtr
901
+ ? view.getUint32(this.printBatchPtr + 8, true)
902
+ : 0;
903
+ const printRecordCount = this.printCollectionEnabled && this.printBatchPtr
904
+ ? view.getUint32(this.printBatchPtr + 12, true)
905
+ : 0;
906
+ const printOverflowCount = this.printCollectionEnabled && this.printBatchPtr
907
+ ? view.getUint32(this.printBatchPtr + 16, true)
908
+ : 0;
909
+ if (delegateUsed > this.delegateCapacity) {
910
+ throw new Error("processor returned an invalid delegate byte count");
911
+ }
912
+ if (printUsed > this.printCapacity) {
913
+ throw new Error("processor returned an invalid print byte count");
914
+ }
915
+ const hasCurrent = delegateUsed
916
+ || delegateRecordCount
917
+ || delegateOverflowCount
918
+ || printUsed
919
+ || printRecordCount
920
+ || printOverflowCount;
921
+ const hasPending = this.pendingDelegateDrops
922
+ || this.pendingDelegateOverflow
923
+ || this.pendingPrintDrops
924
+ || this.pendingPrintOverflow;
925
+ if (!hasCurrent && !hasPending) return;
926
+
927
+ const entry = this.executionOutputEntry;
928
+ entry.operation = hasCurrent ? operation : EXECUTION_OPERATION_TRANSPORT;
929
+ entry.operationIndex = hasCurrent ? operationIndex : 0;
930
+ entry.delegateSubscriptionId = this.delegateSubscriptionId;
931
+ entry.delegateUsed = delegateUsed;
932
+ entry.delegateRecordCount = delegateRecordCount;
933
+ entry.delegateOverflowCount = this.saturatedAdd(
934
+ this.pendingDelegateOverflow,
935
+ delegateOverflowCount,
936
+ );
937
+ entry.delegateTransportDropCount = this.pendingDelegateDrops;
938
+ entry.printSubscriptionId = this.printSubscriptionId;
939
+ entry.printUsed = printUsed;
940
+ entry.printRecordCount = printRecordCount;
941
+ entry.printOverflowCount = this.saturatedAdd(
942
+ this.pendingPrintOverflow,
943
+ printOverflowCount,
944
+ );
945
+ entry.printTransportDropCount = this.pendingPrintDrops;
946
+ if (writeExecutionOutputRing(
947
+ this.executionOutputRing,
948
+ entry,
949
+ this.delegateRecordView,
950
+ this.printRecordView,
951
+ )) {
952
+ this.pendingDelegateDrops = 0;
953
+ this.pendingDelegateOverflow = 0;
954
+ this.pendingPrintDrops = 0;
955
+ this.pendingPrintOverflow = 0;
956
+ return;
957
+ }
958
+ this.pendingDelegateDrops = this.saturatedAdd(
959
+ this.pendingDelegateDrops,
960
+ delegateRecordCount,
961
+ );
962
+ this.pendingDelegateOverflow = this.saturatedAdd(
963
+ this.pendingDelegateOverflow,
964
+ delegateOverflowCount,
965
+ );
966
+ this.pendingPrintDrops = this.saturatedAdd(
967
+ this.pendingPrintDrops,
968
+ printRecordCount,
969
+ );
970
+ this.pendingPrintOverflow = this.saturatedAdd(
971
+ this.pendingPrintOverflow,
972
+ printOverflowCount,
973
+ );
974
+ }
975
+
976
+ setDelegateSubscription(enabled, subscriptionId) {
977
+ this.delegateCollectionEnabled = enabled === true && this.delegateStoragePtr !== 0;
978
+ this.delegateSubscriptionId = Number.isSafeInteger(subscriptionId)
979
+ ? subscriptionId
980
+ : 0;
981
+ this.pendingDelegateDrops = 0;
982
+ this.pendingDelegateOverflow = 0;
983
+ if (this.delegateBatchPtr) {
984
+ const view = this.memoryView();
985
+ this.resetRecordBatch(view, this.delegateBatchPtr);
986
+ view.setUint32(
987
+ this.executionOutputPtr,
988
+ this.delegateCollectionEnabled ? this.delegateBatchPtr : 0,
989
+ true,
990
+ );
991
+ }
992
+ }
993
+
994
+ setPrintSubscription(enabled, subscriptionId) {
995
+ this.printCollectionEnabled = enabled === true && this.printStoragePtr !== 0;
996
+ this.printSubscriptionId = Number.isSafeInteger(subscriptionId)
997
+ ? subscriptionId
998
+ : 0;
999
+ this.pendingPrintDrops = 0;
1000
+ this.pendingPrintOverflow = 0;
1001
+ if (this.printBatchPtr) {
1002
+ const view = this.memoryView();
1003
+ this.resetRecordBatch(view, this.printBatchPtr);
1004
+ view.setUint32(
1005
+ this.executionOutputPtr + 4,
1006
+ this.printCollectionEnabled ? this.printBatchPtr : 0,
1007
+ true,
1008
+ );
1009
+ }
1010
+ }
1011
+
655
1012
  bindInitialBuffers(options) {
656
1013
  this.bufferInfo.forEach((buffer, bufferId) => {
657
1014
  const supplied = this.initialBufferOption(options, buffer, bufferId);
@@ -804,7 +1161,8 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
804
1161
  }
805
1162
 
806
1163
  isFixedArray(info) {
807
- return info?.is_slice !== true && /\[[0-9]+\]$/.test(info?.type_repr ?? "");
1164
+ return info?.is_array
1165
+ ?? (info?.is_slice !== true && /\[[0-9]+\]$/.test(info?.type_repr ?? ""));
808
1166
  }
809
1167
 
810
1168
  decodeConstantScalar(scalar) {
@@ -889,7 +1247,7 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
889
1247
  }
890
1248
  } else if (scalar === "i64") {
891
1249
  for (let index = 0; index < length; index += 1) {
892
- target[index] = BigInt(values[index]);
1250
+ target[index] = exactI64(values[index]);
893
1251
  }
894
1252
  } else {
895
1253
  target.set(values);
@@ -921,7 +1279,7 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
921
1279
  writeScalar(address, scalar, value, view = this.memoryView()) {
922
1280
  if (scalar === "bool") view.setUint8(address, value ? 1 : 0);
923
1281
  else if (scalar === "i32") view.setInt32(address, Number(value), true);
924
- else if (scalar === "i64") view.setBigInt64(address, BigInt(value), true);
1282
+ else if (scalar === "i64") view.setBigInt64(address, exactI64(value), true);
925
1283
  else if (scalar === "f32") view.setFloat32(address, Number(value), true);
926
1284
  else if (scalar === "f64") view.setFloat64(address, Number(value), true);
927
1285
  else throw new Error(`unsupported ABI scalar '${String(scalar)}'`);
@@ -1198,6 +1556,7 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
1198
1556
  }
1199
1557
 
1200
1558
  invokeProcessSegment(startFrame, frames, flags) {
1559
+ this.prepareExecutionOutput();
1201
1560
  return this.exports.onda_process(
1202
1561
  this.statePtr,
1203
1562
  this.paramsPtr,
@@ -1210,21 +1569,44 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
1210
1569
  this.bufferFramesPtr,
1211
1570
  this.bufferChannelsPtr,
1212
1571
  this.bufferSampleRatesPtr,
1572
+ this.executionOutputPtr,
1213
1573
  );
1214
1574
  }
1215
1575
 
1576
+ resetRecordBatch(view, batchPtr) {
1577
+ if (!batchPtr) return;
1578
+ view.setUint32(batchPtr + 8, 0, true);
1579
+ view.setUint32(batchPtr + 12, 0, true);
1580
+ view.setUint32(batchPtr + 16, 0, true);
1581
+ }
1582
+
1583
+ prepareExecutionOutput() {
1584
+ if (!this.executionOutputPtr) return;
1585
+ const view = this.memoryView();
1586
+ this.resetRecordBatch(
1587
+ view,
1588
+ view.getUint32(this.executionOutputPtr, true),
1589
+ );
1590
+ this.resetRecordBatch(
1591
+ view,
1592
+ view.getUint32(this.executionOutputPtr + 4, true),
1593
+ );
1594
+ view.setUint32(this.executionOutputPtr + 8, 0, true);
1595
+ }
1596
+
1216
1597
  clearOutputs(outputs) {
1217
1598
  for (const bus of outputs) {
1218
1599
  for (const channel of bus) channel.fill(0);
1219
1600
  }
1220
1601
  }
1221
1602
 
1222
- process(inputs, outputs) {
1603
+ processPending(_inputs, outputs) {
1604
+ this.clearOutputs(outputs);
1605
+ return true;
1606
+ }
1607
+
1608
+ processInitialized(inputs, outputs) {
1223
1609
  this.refreshMemoryCache();
1224
- if (this.executionFailed) {
1225
- this.clearOutputs(outputs);
1226
- return true;
1227
- }
1228
1610
  const frames = this.audioFrameCount(inputs, outputs);
1229
1611
 
1230
1612
  let callbackOffset = 0;
@@ -1250,8 +1632,12 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
1250
1632
  segmentFrames,
1251
1633
  flags,
1252
1634
  );
1635
+ this.publishExecutionOutput(
1636
+ EXECUTION_OPERATION_PROCESS,
1637
+ 0,
1638
+ );
1253
1639
  if (status !== 0) {
1254
- this.executionFailed = true;
1640
+ this.invalidateState();
1255
1641
  this.clearOutputs(outputs);
1256
1642
  this.port.postMessage({
1257
1643
  type: "onda-error",