@onda-lang/webaudio 0.7.4 → 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/src/worklet.js CHANGED
@@ -1,7 +1,23 @@
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
 
7
23
  class OndaWasmProcessor extends AudioWorkletProcessor {
@@ -47,6 +63,8 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
47
63
  this.stateBytes = null;
48
64
  this.inputViews = [];
49
65
  this.outputViews = [];
66
+ this.delegateRecordView = null;
67
+ this.printRecordView = null;
50
68
 
51
69
  const heapBase = Number(
52
70
  this.exports.__heap_base?.value ?? this.exports.__heap_base ?? 0,
@@ -64,6 +82,12 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
64
82
  this.eventInfo = Array.isArray(metadata.metadata?.events)
65
83
  ? metadata.metadata.events
66
84
  : [];
85
+ this.delegateInfo = Array.isArray(metadata.metadata?.delegates)
86
+ ? metadata.metadata.delegates
87
+ : [];
88
+ this.logSiteInfo = Array.isArray(metadata.metadata?.log_sites)
89
+ ? metadata.metadata.log_sites
90
+ : [];
67
91
  this.controlOutputInfo = Array.isArray(metadata.metadata?.control_outputs)
68
92
  ? metadata.metadata.control_outputs
69
93
  : [];
@@ -115,7 +139,6 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
115
139
  this.outputPtrs = [];
116
140
  this.outputCapacityFrames = 0;
117
141
  this.blockCursor = 0;
118
- this.executionFailed = false;
119
142
 
120
143
  const paramBytes = Number(metadata.runtime?.param_size_bytes ?? 0);
121
144
  if (!Number.isInteger(paramBytes) || paramBytes < 0) {
@@ -148,13 +171,101 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
148
171
  this.eventPayloadPtr = this.eventPayloadCapacity
149
172
  ? this.alloc(this.eventPayloadCapacity, 8)
150
173
  : 0;
174
+ this.delegateCapacity = this.configuredDelegateCapacity(
175
+ processorOptions.delegateCapacityBytes,
176
+ );
177
+ this.delegateStoragePtr = this.delegateInfo.length && this.delegateCapacity
178
+ ? this.alloc(this.delegateCapacity, 8)
179
+ : 0;
180
+ this.delegateBatchPtr = this.delegateInfo.length
181
+ ? this.alloc(DELEGATE_BATCH_SIZE_BYTES, 4)
182
+ : 0;
183
+ if (this.delegateBatchPtr) {
184
+ const view = new DataView(this.memory.buffer);
185
+ view.setUint32(this.delegateBatchPtr, this.delegateStoragePtr, true);
186
+ view.setUint32(this.delegateBatchPtr + 4, this.delegateCapacity, true);
187
+ view.setUint32(this.delegateBatchPtr + 8, 0, true);
188
+ view.setUint32(this.delegateBatchPtr + 12, 0, true);
189
+ view.setUint32(this.delegateBatchPtr + 16, 0, true);
190
+ }
191
+ this.printCapacity = this.configuredPrintCapacity(
192
+ processorOptions.printCapacityBytes,
193
+ );
194
+ this.printStoragePtr = this.logSiteInfo.length && this.printCapacity
195
+ ? this.alloc(this.printCapacity, 8)
196
+ : 0;
197
+ this.printBatchPtr = this.logSiteInfo.length
198
+ ? this.alloc(PRINT_BATCH_SIZE_BYTES, 4)
199
+ : 0;
200
+ if (this.printBatchPtr) {
201
+ const view = new DataView(this.memory.buffer);
202
+ view.setUint32(this.printBatchPtr, this.printStoragePtr, true);
203
+ view.setUint32(this.printBatchPtr + 4, this.printCapacity, true);
204
+ view.setUint32(this.printBatchPtr + 8, 0, true);
205
+ view.setUint32(this.printBatchPtr + 12, 0, true);
206
+ view.setUint32(this.printBatchPtr + 16, 0, true);
207
+ }
208
+ this.printCollectionEnabled = processorOptions.printCollectionEnabled === true
209
+ && this.printStoragePtr !== 0;
210
+ this.printSubscriptionId = Number.isSafeInteger(processorOptions.printSubscriptionId)
211
+ ? processorOptions.printSubscriptionId
212
+ : 0;
213
+ this.executionOutputPtr = this.delegateBatchPtr || this.printBatchPtr
214
+ ? this.alloc(EXECUTION_OUTPUT_SIZE_BYTES, 4)
215
+ : 0;
216
+ if (this.executionOutputPtr) {
217
+ const view = new DataView(this.memory.buffer);
218
+ view.setUint32(this.executionOutputPtr, 0, true);
219
+ view.setUint32(
220
+ this.executionOutputPtr + 4,
221
+ this.printCollectionEnabled ? this.printBatchPtr : 0,
222
+ true,
223
+ );
224
+ view.setUint32(this.executionOutputPtr + 8, 0, true);
225
+ }
226
+ this.delegateCollectionEnabled = false;
227
+ this.delegateSubscriptionId = 0;
228
+ this.executionOutputRing = processorOptions.executionOutputRing === undefined
229
+ || processorOptions.executionOutputRing === null
230
+ ? null
231
+ : openExecutionOutputRing(processorOptions.executionOutputRing);
232
+ if (
233
+ this.executionOutputRing
234
+ && (
235
+ this.executionOutputRing.delegateCapacity !== this.delegateCapacity
236
+ || this.executionOutputRing.printCapacity !== this.printCapacity
237
+ )
238
+ ) {
239
+ throw new Error("execution-output ring capacities do not match processor storage");
240
+ }
241
+ this.pendingDelegateDrops = 0;
242
+ this.pendingDelegateOverflow = 0;
243
+ this.pendingPrintDrops = 0;
244
+ this.pendingPrintOverflow = 0;
245
+ this.executionOutputEntry = {
246
+ operation: 0,
247
+ operationIndex: 0,
248
+ delegateSubscriptionId: 0,
249
+ delegateUsed: 0,
250
+ delegateRecordCount: 0,
251
+ delegateOverflowCount: 0,
252
+ delegateTransportDropCount: 0,
253
+ printSubscriptionId: 0,
254
+ printUsed: 0,
255
+ printRecordCount: 0,
256
+ printOverflowCount: 0,
257
+ printTransportDropCount: 0,
258
+ };
151
259
  this.writeParamDefaults();
152
260
  this.writeInitialParams(processorOptions.params ?? {});
153
261
  this.ensureInputCapacity(this.blockSize);
154
262
  this.ensureOutputCapacity(this.blockSize);
155
263
  this.viewsReady = true;
156
264
  this.refreshMemoryCache(true);
157
- this.reset();
265
+ this.invalidateState();
266
+ if (processorOptions.initialize === true) {
267
+ this.init(ONDA_INIT_FULL);
268
+ }
158
269
  this.allocationLocked = true;
159
270
  this.port.onmessage = (event) => this.handleMessage(event.data ?? {});
160
271
  }
@@ -175,7 +286,7 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
175
286
 
176
287
  alloc(size, align) {
177
288
  if (this.allocationLocked) {
178
- throw new Error("Onda worklet memory allocation is locked after initialization");
289
+ throw new Error("Onda worklet memory allocation is locked after construction");
179
290
  }
180
291
  const ptr = this.alignUp(this.heap, align);
181
292
  const next = ptr + size;
@@ -212,6 +323,12 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
212
323
  this.outputCapacityFrames,
213
324
  );
214
325
  });
326
+ this.delegateRecordView = this.delegateStoragePtr
327
+ ? new Uint8Array(buffer, this.delegateStoragePtr, this.delegateCapacity)
328
+ : null;
329
+ this.printRecordView = this.printStoragePtr
330
+ ? new Uint8Array(buffer, this.printStoragePtr, this.printCapacity)
331
+ : null;
215
332
  }
216
333
 
217
334
  memoryView() {
@@ -255,6 +372,32 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
255
372
  return capacity;
256
373
  }
257
374
 
375
+ configuredDelegateCapacity(configuredCapacity) {
376
+ const capacity = configuredCapacity
377
+ ?? (this.delegateInfo.length ? DEFAULT_DELEGATE_CAPACITY_BYTES : 0);
378
+ if (
379
+ !Number.isSafeInteger(capacity)
380
+ || capacity < 0
381
+ || capacity > 0x7fff_ffff
382
+ ) {
383
+ throw new Error(
384
+ "delegate capacity must be an integer from 0 through 2147483647 bytes",
385
+ );
386
+ }
387
+ return capacity;
388
+ }
389
+
390
+ configuredPrintCapacity(configuredCapacity) {
391
+ const capacity = configuredCapacity
392
+ ?? (this.logSiteInfo.length ? DEFAULT_PRINT_CAPACITY_BYTES : 0);
393
+ if (!Number.isSafeInteger(capacity) || capacity < 0 || capacity > 0x7fff_ffff) {
394
+ throw new Error(
395
+ "print capacity must be an integer from 0 through 2147483647 bytes",
396
+ );
397
+ }
398
+ return capacity;
399
+ }
400
+
258
401
  flattenAudioChannels(ports, kind) {
259
402
  const channels = [];
260
403
  for (const port of ports) {
@@ -349,24 +492,73 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
349
492
  );
350
493
  }
351
494
 
352
- reset() {
353
- this.refreshMemoryCache();
354
- this.stateBytes.fill(0);
495
+ init(mode) {
496
+ if (mode !== ONDA_INIT_PRESERVE_PINNED && mode !== ONDA_INIT_FULL) {
497
+ throw new Error(`invalid Onda init mode '${String(mode)}'`);
498
+ }
499
+ if (mode === ONDA_INIT_PRESERVE_PINNED && !this.initialized) {
500
+ throw new Error("full initialization is required before preserving pinned state");
501
+ }
502
+ this.runInitialization(mode);
503
+ }
504
+
505
+ invalidateState() {
506
+ this.initialized = false;
507
+ this.process = this.processPending;
355
508
  this.blockCursor = 0;
356
- this.executionFailed = false;
357
- this.checkExecutionStatus(
358
- this.exports.onda_init(this.paramsPtr, this.statePtr),
359
- "processor init",
509
+ }
510
+
511
+ commitInitializedState(blockCursor) {
512
+ this.initialized = true;
513
+ this.process = this.processInitialized;
514
+ this.blockCursor = blockCursor;
515
+ }
516
+
517
+ runInitialization(mode, afterInitialize) {
518
+ // Reinitializing state does not create a compile-block boundary. Retain
519
+ // the host-side position so the next process segment cannot synthesize an
520
+ // extra BEGIN_BLOCK or postpone the matching END_BLOCK.
521
+ const blockCursor = this.initialized ? this.blockCursor : 0;
522
+ // Generated initialization mutates the live image in place. Stop exposing
523
+ // it before entering Wasm so a failure, including one in snapshot overlay,
524
+ // leaves the processor on the silent pending path.
525
+ this.invalidateState();
526
+ this.refreshMemoryCache();
527
+ this.prepareExecutionOutput();
528
+ const status = this.exports.onda_processor_init(
529
+ this.paramsPtr,
530
+ this.statePtr,
531
+ mode,
532
+ this.bufferPointersPtr,
533
+ this.bufferFramesPtr,
534
+ this.bufferChannelsPtr,
535
+ this.bufferSampleRatesPtr,
536
+ this.executionOutputPtr,
360
537
  );
538
+ // Generated failures retain diagnostic prints but clear delegates.
539
+ this.publishExecutionOutput(
540
+ EXECUTION_OPERATION_INIT,
541
+ 0,
542
+ );
543
+ this.checkExecutionStatus(status, "processor init");
544
+ afterInitialize?.();
545
+ this.commitInitializedState(blockCursor);
546
+ }
547
+
548
+ requireInitialized(operation) {
549
+ if (!this.initialized) {
550
+ throw new Error(`full initialization is required before ${operation}`);
551
+ }
361
552
  }
362
553
 
363
554
  checkExecutionStatus(status, operation) {
364
555
  if (status === 0) return;
365
- this.executionFailed = true;
556
+ this.invalidateState();
366
557
  throw new Error(`${operation} failed with Onda execution status ${String(status)}`);
367
558
  }
368
559
 
369
560
  createSnapshot() {
561
+ this.requireInitialized("snapshot");
370
562
  const snapshot = new Uint8Array(this.snapshotSizeBytes);
371
563
  this.refreshMemoryCache();
372
564
  const state = this.stateBytes;
@@ -391,19 +583,65 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
391
583
  );
392
584
  }
393
585
  // 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
- );
586
+ // control-mirror state never leak across a restore. Initialization and
587
+ // overlay form one lifecycle transition: neither partial result is ready.
588
+ this.runInitialization(ONDA_INIT_FULL, () => {
589
+ const state = this.stateBytes;
590
+ for (const entry of this.snapshotInfo) {
591
+ const packedOffset = Number(entry.packed_snapshot_byte_offset);
592
+ const physicalOffset = Number(entry.physical_state_byte_offset);
593
+ const byteSize = Number(entry.byte_size);
594
+ this.validateSnapshotEntry(entry, packedOffset, physicalOffset, byteSize);
595
+ state.set(
596
+ snapshot.subarray(packedOffset, packedOffset + byteSize),
597
+ physicalOffset,
598
+ );
599
+ this.normalizeSnapshotIntegerRange(entry, state, physicalOffset, byteSize);
600
+ }
601
+ });
602
+ }
603
+
604
+ normalizeSnapshotIntegerRange(entry, state, offset, byteSize) {
605
+ const range = entry.integer_range;
606
+ if (range === null || range === undefined) return;
607
+ if (entry.array_len !== 1 || (entry.scalar !== "i32" && entry.scalar !== "i64")) {
608
+ throw new Error(`state '${String(entry.name)}' has an invalid integer range`);
609
+ }
610
+ const mode = range.mode;
611
+ if (mode !== "clamp" && mode !== "wrap") {
612
+ throw new Error(`state '${String(entry.name)}' has an invalid integer range mode`);
613
+ }
614
+ const view = new DataView(state.buffer, state.byteOffset + offset, byteSize);
615
+ if (entry.scalar === "i32") {
616
+ const min = Number(range.min?.value);
617
+ const max = Number(range.max?.value);
618
+ if (!Number.isInteger(min) || !Number.isInteger(max) || min > max || byteSize !== 4) {
619
+ throw new Error(`state '${String(entry.name)}' has invalid i32 range bounds`);
620
+ }
621
+ const value = view.getInt32(0, true);
622
+ const normalized = mode === "clamp"
623
+ ? Math.min(Math.max(value, min), max)
624
+ : min + (((value - min) % (max - min + 1)) + (max - min + 1)) % (max - min + 1);
625
+ view.setInt32(0, normalized, true);
626
+ return;
406
627
  }
628
+ let min;
629
+ let max;
630
+ try {
631
+ min = BigInt(range.min?.value);
632
+ max = BigInt(range.max?.value);
633
+ } catch {
634
+ throw new Error(`state '${String(entry.name)}' has invalid i64 range bounds`);
635
+ }
636
+ if (min > max || byteSize !== 8) {
637
+ throw new Error(`state '${String(entry.name)}' has invalid i64 range bounds`);
638
+ }
639
+ const value = view.getBigInt64(0, true);
640
+ const width = max - min + 1n;
641
+ const normalized = mode === "clamp"
642
+ ? (value < min ? min : (value > max ? max : value))
643
+ : min + ((value - min) % width + width) % width;
644
+ view.setBigInt64(0, normalized, true);
407
645
  }
408
646
 
409
647
  snapshotBytes(value) {
@@ -450,8 +688,8 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
450
688
  message.value,
451
689
  );
452
690
  this.postResponse(message, { type: "onda-ok", operation: message.type });
453
- } else if (message.type === "reset") {
454
- this.reset();
691
+ } else if (message.type === "init") {
692
+ this.init(message.mode);
455
693
  this.postResponse(message, { type: "onda-ok", operation: message.type });
456
694
  } else if (message.type === "event") {
457
695
  this.dispatchEvent(
@@ -481,6 +719,10 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
481
719
  } else if (message.type === "restore-snapshot") {
482
720
  this.restoreSnapshot(message.snapshot ?? message.bytes);
483
721
  this.postResponse(message, { type: "onda-ok", operation: message.type });
722
+ } else if (message.type === "delegate-subscription") {
723
+ this.setDelegateSubscription(message.enabled, message.subscriptionId);
724
+ } else if (message.type === "print-subscription") {
725
+ this.setPrintSubscription(message.enabled, message.subscriptionId);
484
726
  } else {
485
727
  throw new Error(`unknown Onda worklet operation '${String(message.type)}'`);
486
728
  }
@@ -495,6 +737,7 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
495
737
  }
496
738
 
497
739
  dispatchEvent(selector, values) {
740
+ this.requireInitialized("event dispatch");
498
741
  const eventId = Number.isInteger(selector)
499
742
  ? selector
500
743
  : this.eventInfo.findIndex((event) => event.name === selector);
@@ -554,18 +797,22 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
554
797
  if (typeof handler !== "function") {
555
798
  throw new Error(`missing WebAssembly export '${event.export}'`);
556
799
  }
557
- this.checkExecutionStatus(
558
- handler(
559
- this.eventPayloadPtr,
560
- this.paramsPtr,
561
- this.statePtr,
562
- this.bufferPointersPtr,
563
- this.bufferFramesPtr,
564
- this.bufferChannelsPtr,
565
- this.bufferSampleRatesPtr,
566
- ),
567
- `event '${event.name}'`,
800
+ this.prepareExecutionOutput();
801
+ const status = handler(
802
+ this.eventPayloadPtr,
803
+ this.paramsPtr,
804
+ this.statePtr,
805
+ this.bufferPointersPtr,
806
+ this.bufferFramesPtr,
807
+ this.bufferChannelsPtr,
808
+ this.bufferSampleRatesPtr,
809
+ this.executionOutputPtr,
568
810
  );
811
+ this.publishExecutionOutput(
812
+ EXECUTION_OPERATION_EVENT,
813
+ eventId,
814
+ );
815
+ this.checkExecutionStatus(status, `event '${event.name}'`);
569
816
  }
570
817
 
571
818
  eventValue(event, param, paramId, values) {
@@ -597,6 +844,7 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
597
844
  }
598
845
 
599
846
  readControlOutputs() {
847
+ this.requireInitialized("reading control outputs");
600
848
  return Object.fromEntries(
601
849
  this.controlOutputInfo.map((output) => [
602
850
  output.name,
@@ -608,6 +856,142 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
608
856
  );
609
857
  }
610
858
 
859
+ saturatedAdd(lhs, rhs) {
860
+ return Math.min(0xffff_ffff, lhs + rhs);
861
+ }
862
+
863
+ publishExecutionOutput(operation, operationIndex) {
864
+ if (!this.executionOutputRing) return;
865
+ if (
866
+ !this.delegateCollectionEnabled
867
+ && !this.printCollectionEnabled
868
+ && !this.pendingDelegateDrops
869
+ && !this.pendingDelegateOverflow
870
+ && !this.pendingPrintDrops
871
+ && !this.pendingPrintOverflow
872
+ ) return;
873
+ const view = this.memoryView();
874
+ const delegateUsed = this.delegateCollectionEnabled && this.delegateBatchPtr
875
+ ? view.getUint32(this.delegateBatchPtr + 8, true)
876
+ : 0;
877
+ const delegateRecordCount = this.delegateCollectionEnabled && this.delegateBatchPtr
878
+ ? view.getUint32(this.delegateBatchPtr + 12, true)
879
+ : 0;
880
+ const delegateOverflowCount = this.delegateCollectionEnabled && this.delegateBatchPtr
881
+ ? view.getUint32(this.delegateBatchPtr + 16, true)
882
+ : 0;
883
+ const printUsed = this.printCollectionEnabled && this.printBatchPtr
884
+ ? view.getUint32(this.printBatchPtr + 8, true)
885
+ : 0;
886
+ const printRecordCount = this.printCollectionEnabled && this.printBatchPtr
887
+ ? view.getUint32(this.printBatchPtr + 12, true)
888
+ : 0;
889
+ const printOverflowCount = this.printCollectionEnabled && this.printBatchPtr
890
+ ? view.getUint32(this.printBatchPtr + 16, true)
891
+ : 0;
892
+ if (delegateUsed > this.delegateCapacity) {
893
+ throw new Error("processor returned an invalid delegate byte count");
894
+ }
895
+ if (printUsed > this.printCapacity) {
896
+ throw new Error("processor returned an invalid print byte count");
897
+ }
898
+ const hasCurrent = delegateUsed
899
+ || delegateRecordCount
900
+ || delegateOverflowCount
901
+ || printUsed
902
+ || printRecordCount
903
+ || printOverflowCount;
904
+ const hasPending = this.pendingDelegateDrops
905
+ || this.pendingDelegateOverflow
906
+ || this.pendingPrintDrops
907
+ || this.pendingPrintOverflow;
908
+ if (!hasCurrent && !hasPending) return;
909
+
910
+ const entry = this.executionOutputEntry;
911
+ entry.operation = hasCurrent ? operation : EXECUTION_OPERATION_TRANSPORT;
912
+ entry.operationIndex = hasCurrent ? operationIndex : 0;
913
+ entry.delegateSubscriptionId = this.delegateSubscriptionId;
914
+ entry.delegateUsed = delegateUsed;
915
+ entry.delegateRecordCount = delegateRecordCount;
916
+ entry.delegateOverflowCount = this.saturatedAdd(
917
+ this.pendingDelegateOverflow,
918
+ delegateOverflowCount,
919
+ );
920
+ entry.delegateTransportDropCount = this.pendingDelegateDrops;
921
+ entry.printSubscriptionId = this.printSubscriptionId;
922
+ entry.printUsed = printUsed;
923
+ entry.printRecordCount = printRecordCount;
924
+ entry.printOverflowCount = this.saturatedAdd(
925
+ this.pendingPrintOverflow,
926
+ printOverflowCount,
927
+ );
928
+ entry.printTransportDropCount = this.pendingPrintDrops;
929
+ if (writeExecutionOutputRing(
930
+ this.executionOutputRing,
931
+ entry,
932
+ this.delegateRecordView,
933
+ this.printRecordView,
934
+ )) {
935
+ this.pendingDelegateDrops = 0;
936
+ this.pendingDelegateOverflow = 0;
937
+ this.pendingPrintDrops = 0;
938
+ this.pendingPrintOverflow = 0;
939
+ return;
940
+ }
941
+ this.pendingDelegateDrops = this.saturatedAdd(
942
+ this.pendingDelegateDrops,
943
+ delegateRecordCount,
944
+ );
945
+ this.pendingDelegateOverflow = this.saturatedAdd(
946
+ this.pendingDelegateOverflow,
947
+ delegateOverflowCount,
948
+ );
949
+ this.pendingPrintDrops = this.saturatedAdd(
950
+ this.pendingPrintDrops,
951
+ printRecordCount,
952
+ );
953
+ this.pendingPrintOverflow = this.saturatedAdd(
954
+ this.pendingPrintOverflow,
955
+ printOverflowCount,
956
+ );
957
+ }
958
+
959
+ setDelegateSubscription(enabled, subscriptionId) {
960
+ this.delegateCollectionEnabled = enabled === true && this.delegateStoragePtr !== 0;
961
+ this.delegateSubscriptionId = Number.isSafeInteger(subscriptionId)
962
+ ? subscriptionId
963
+ : 0;
964
+ this.pendingDelegateDrops = 0;
965
+ this.pendingDelegateOverflow = 0;
966
+ if (this.delegateBatchPtr) {
967
+ const view = this.memoryView();
968
+ this.resetRecordBatch(view, this.delegateBatchPtr);
969
+ view.setUint32(
970
+ this.executionOutputPtr,
971
+ this.delegateCollectionEnabled ? this.delegateBatchPtr : 0,
972
+ true,
973
+ );
974
+ }
975
+ }
976
+
977
+ setPrintSubscription(enabled, subscriptionId) {
978
+ this.printCollectionEnabled = enabled === true && this.printStoragePtr !== 0;
979
+ this.printSubscriptionId = Number.isSafeInteger(subscriptionId)
980
+ ? subscriptionId
981
+ : 0;
982
+ this.pendingPrintDrops = 0;
983
+ this.pendingPrintOverflow = 0;
984
+ if (this.printBatchPtr) {
985
+ const view = this.memoryView();
986
+ this.resetRecordBatch(view, this.printBatchPtr);
987
+ view.setUint32(
988
+ this.executionOutputPtr + 4,
989
+ this.printCollectionEnabled ? this.printBatchPtr : 0,
990
+ true,
991
+ );
992
+ }
993
+ }
994
+
611
995
  bindInitialBuffers(options) {
612
996
  this.bufferInfo.forEach((buffer, bufferId) => {
613
997
  const supplied = this.initialBufferOption(options, buffer, bufferId);
@@ -760,7 +1144,8 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
760
1144
  }
761
1145
 
762
1146
  isFixedArray(info) {
763
- return info?.is_slice !== true && /\[[0-9]+\]$/.test(info?.type_repr ?? "");
1147
+ return info?.is_array
1148
+ ?? (info?.is_slice !== true && /\[[0-9]+\]$/.test(info?.type_repr ?? ""));
764
1149
  }
765
1150
 
766
1151
  decodeConstantScalar(scalar) {
@@ -1154,6 +1539,7 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
1154
1539
  }
1155
1540
 
1156
1541
  invokeProcessSegment(startFrame, frames, flags) {
1542
+ this.prepareExecutionOutput();
1157
1543
  return this.exports.onda_process(
1158
1544
  this.statePtr,
1159
1545
  this.paramsPtr,
@@ -1166,21 +1552,44 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
1166
1552
  this.bufferFramesPtr,
1167
1553
  this.bufferChannelsPtr,
1168
1554
  this.bufferSampleRatesPtr,
1555
+ this.executionOutputPtr,
1169
1556
  );
1170
1557
  }
1171
1558
 
1559
+ resetRecordBatch(view, batchPtr) {
1560
+ if (!batchPtr) return;
1561
+ view.setUint32(batchPtr + 8, 0, true);
1562
+ view.setUint32(batchPtr + 12, 0, true);
1563
+ view.setUint32(batchPtr + 16, 0, true);
1564
+ }
1565
+
1566
+ prepareExecutionOutput() {
1567
+ if (!this.executionOutputPtr) return;
1568
+ const view = this.memoryView();
1569
+ this.resetRecordBatch(
1570
+ view,
1571
+ view.getUint32(this.executionOutputPtr, true),
1572
+ );
1573
+ this.resetRecordBatch(
1574
+ view,
1575
+ view.getUint32(this.executionOutputPtr + 4, true),
1576
+ );
1577
+ view.setUint32(this.executionOutputPtr + 8, 0, true);
1578
+ }
1579
+
1172
1580
  clearOutputs(outputs) {
1173
1581
  for (const bus of outputs) {
1174
1582
  for (const channel of bus) channel.fill(0);
1175
1583
  }
1176
1584
  }
1177
1585
 
1178
- process(inputs, outputs) {
1586
+ processPending(_inputs, outputs) {
1587
+ this.clearOutputs(outputs);
1588
+ return true;
1589
+ }
1590
+
1591
+ processInitialized(inputs, outputs) {
1179
1592
  this.refreshMemoryCache();
1180
- if (this.executionFailed) {
1181
- this.clearOutputs(outputs);
1182
- return true;
1183
- }
1184
1593
  const frames = this.audioFrameCount(inputs, outputs);
1185
1594
 
1186
1595
  let callbackOffset = 0;
@@ -1206,8 +1615,12 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
1206
1615
  segmentFrames,
1207
1616
  flags,
1208
1617
  );
1618
+ this.publishExecutionOutput(
1619
+ EXECUTION_OPERATION_PROCESS,
1620
+ 0,
1621
+ );
1209
1622
  if (status !== 0) {
1210
- this.executionFailed = true;
1623
+ this.invalidateState();
1211
1624
  this.clearOutputs(outputs);
1212
1625
  this.port.postMessage({
1213
1626
  type: "onda-error",