@onda-lang/webaudio 0.8.3 → 0.8.5

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 CHANGED
@@ -84,8 +84,6 @@ for an array.
84
84
 
85
85
  Controlled `i64` domains use the descriptor's exact binary64 integer range. Full-width unranged
86
86
  `i64` values continue to use `bigint` when written directly.
87
- Event `i64` values accept bigint, safe integers, or decimal integer strings and reject values
88
- outside the signed 64-bit range.
89
87
 
90
88
  The artifact must be compiled for exactly `audioContext.sampleRate`; the adapter rejects a mismatch
91
89
  before registering the node so sample-rate-derived language semantics cannot silently drift. A Web
@@ -97,6 +95,41 @@ Call `processor.close()` when the adapter is no longer used. Closing is idempote
97
95
  rejects pending requests, removes listeners, and makes subsequent operations fail immediately. The
98
96
  wrapped `AudioWorkletNode` and `AudioContext` remain caller-owned.
99
97
 
98
+ ## Events
99
+
100
+ `trigger(nameOrIndex, values?)` accepts event parameters as an ordered array or an object keyed by
101
+ parameter name. Nominal structs are objects containing exactly their declared fields; tuples, fixed
102
+ arrays, and runtime slices are sequences. The adapter validates and encodes the complete logical
103
+ value on the main thread before transferring its packed bytes to the worklet.
104
+
105
+ For this Onda declaration:
106
+
107
+ ```onda
108
+ struct Note:
109
+ frequency = 440.0
110
+ velocity = 1.0
111
+
112
+ init:
113
+ current_note: Note
114
+
115
+ event note_on(note: Note):
116
+ current_note = note
117
+ ```
118
+
119
+ the corresponding host call is:
120
+
121
+ ```js
122
+ await processor.trigger("note_on", {
123
+ note: { frequency: 523.25, velocity: 0.7 },
124
+ });
125
+ ```
126
+
127
+ Fixed arrays must have their declared length. Slice lengths may vary up to the configured event
128
+ payload capacity. `i64` leaves accept `bigint`, exact safe integers, or decimal integer strings and
129
+ reject values outside the signed 64-bit range. Missing top-level arguments use declared defaults;
130
+ structs and struct arrays have no event defaults and must be supplied in full. Input rejected during
131
+ worklet preflight leaves processor state usable and rejects the returned promise.
132
+
100
133
  ## Prints
101
134
 
102
135
  Authored `print(...)` occurrences leave generated execution as bounded typed records. The worklet
package/api.md CHANGED
@@ -139,7 +139,8 @@ real-time execution.
139
139
 
140
140
  `writeDelegateBatch` and `writePrintBatch` initialize reusable batch descriptors.
141
141
  `writeExecutionOutput` connects their addresses. Call `resetExecutionOutput` immediately before
142
- every generated init, process, or event entry. After a successful generated call,
142
+ every generated init or process entry. Generated event entries reset after successful input
143
+ preflight instead, so rejected input preserves existing records. After a successful generated call,
143
144
  `readDelegateBatch` or `readPrintBatch` validates the result counters.
144
145
  `decodeDelegateRecords` converts delegate payloads using `metadata.delegates`.
145
146
  `decodePrintRecords` preserves primitive types and source sites; `formatPrintRecords` and
@@ -296,6 +297,17 @@ validateProcessorModule
296
297
  writeDelegateBatch
297
298
  writeExecutionOutput
298
299
  writePrintBatch
300
+ OndaPayloadType
301
+ OndaPayloadField
302
+ OndaPayloadSchema
303
+ OndaPayloadDefault
304
+ OndaPayloadValue
305
+ OndaPayloadSizes
306
+ PayloadPlan
307
+ canonicalF32Number
308
+ EVENT_INPUT_SIZE_BYTES
309
+ PROCESSOR_EXECUTION_INPUT_REJECTED
310
+ writeEventInput
299
311
  <!-- END WEB API onda_processor_abi -->
300
312
 
301
313
  ### `@onda-lang/binaryen-web`
@@ -410,3 +422,21 @@ paramNormalizedToPlain
410
422
  paramPlainToNormalized
411
423
  registerOndaAudioWorklet
412
424
  <!-- END WEB API onda_webaudio -->
425
+
426
+ ### Structured event and delegate values
427
+
428
+ `PayloadPlan` prepares a recursive message schema once. `encode(values)` accepts ordered parameter
429
+ arrays or objects keyed by parameter name; structs are objects containing exactly their declared
430
+ own fields, and arrays/tuples are sequences. Unknown named parameters and excess positional values
431
+ are rejected. Missing event arguments use declared constant defaults.
432
+ Supply `i64` as `bigint`, an exact safe integer, or a decimal string. `decode(bytes)` returns named
433
+ nested values with `bigint` for `i64`. Plans own immutable schema snapshots.
434
+
435
+ `requiredWorkspace(bytes)` preflights packed little-endian input. `sizes(lengths)` reports wire
436
+ and aligned workspace sizes without allocating payload storage. Low-level wasm hosts provision
437
+ both regions and a 16-byte descriptor, then call `writeEventInput` before invoking the event.
438
+ Rejected input returns `PROCESSOR_EXECUTION_INPUT_REJECTED` (2) and leaves processor state usable.
439
+ `OndaAudioProcessor.trigger` performs logical encoding before transferring bytes to the worklet.
440
+ Decoded `f32` leaves use `canonicalF32Number(value)`: the returned JavaScript number has the
441
+ shortest decimal representation that round-trips to the same `f32`, so JSON and controls do not
442
+ expose an irrelevant widened binary tail.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onda-lang/webaudio",
3
- "version": "0.8.3",
3
+ "version": "0.8.5",
4
4
  "type": "module",
5
5
  "description": "Optional Web Audio adapter for Onda processor WebAssembly artifacts",
6
6
  "license": "MIT",
@@ -34,7 +34,7 @@
34
34
  "prepack": "node ../../scripts/stage-web-api-docs.mjs"
35
35
  },
36
36
  "dependencies": {
37
- "@onda-lang/processor-abi": "0.8.3"
37
+ "@onda-lang/processor-abi": "0.8.5"
38
38
  },
39
39
  "publishConfig": {
40
40
  "access": "public"
package/src/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { paramAddress } from "./param-metadata.js";
2
2
  import {
3
+ PayloadPlan,
3
4
  createParamControl,
4
5
  paramElementMetadata,
5
6
  decodeDelegateRecords,
@@ -306,6 +307,7 @@ export class OndaAudioProcessor {
306
307
  this.node = node;
307
308
  this.metadata = metadata;
308
309
  this.paramInfo = metadata?.metadata?.params ?? null;
310
+ this.eventPlans = new WeakMap();
309
311
  this.paramControls = new WeakMap();
310
312
  this.paramElements = new WeakMap();
311
313
  this.nextRequestId = 1;
@@ -629,7 +631,16 @@ export class OndaAudioProcessor {
629
631
  }
630
632
 
631
633
  trigger(event, values = {}) {
632
- return this.request("event", { event, values });
634
+ try {
635
+ this.assertOpen();
636
+ const events = this.metadata?.metadata?.events;
637
+ const info = Number.isInteger(event) ? events?.[event] : events?.find((entry) => entry.name === event);
638
+ if (!info) throw new Error(`unknown Onda event '${String(event)}'`);
639
+ let plan = this.eventPlans.get(info);
640
+ if (!plan) { plan = new PayloadPlan(info.schema); this.eventPlans.set(info, plan); }
641
+ const payload = plan.encode(values);
642
+ return this.request("event", { event, payload }, [payload.buffer]);
643
+ } catch (error) { return Promise.reject(error); }
633
644
  }
634
645
 
635
646
  onDelegates(listener) {
package/src/worklet.js CHANGED
@@ -12,6 +12,7 @@ const ONDA_PROCESS_BEGIN_BLOCK = 1 << 0;
12
12
  const ONDA_PROCESS_END_BLOCK = 1 << 1;
13
13
  const ONDA_INIT_PRESERVE_PINNED = 0;
14
14
  const ONDA_INIT_FULL = 1;
15
+ const PROCESSOR_EXECUTION_INPUT_REJECTED = 2;
15
16
  const ONDA_AUDIO_WORKLET_PROCESSOR_NAME = "onda-wasm-processor";
16
17
  const DEFAULT_EVENT_PAYLOAD_CAPACITY_BYTES = 64 * 1024;
17
18
  const DEFAULT_DELEGATE_CAPACITY_BYTES = 64 * 1024;
@@ -189,6 +190,12 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
189
190
  this.eventPayloadPtr = this.eventPayloadCapacity
190
191
  ? this.alloc(this.eventPayloadCapacity, 8)
191
192
  : 0;
193
+ // Each tensor and dynamic prefix adds at most seven alignment bytes.
194
+ // Reserve before rendering; dispatch never grows memory for preparation.
195
+ this.eventWorkspaceCapacity = this.eventPayloadCapacity + 7 * Math.max(0,
196
+ ...this.eventInfo.map((event) => event.params.length * 2));
197
+ this.eventWorkspacePtr = this.eventWorkspaceCapacity ? this.alloc(this.eventWorkspaceCapacity, 8) : 0;
198
+ this.eventInputPtr = this.eventInfo.length ? this.alloc(16, 4) : 0;
192
199
  this.delegateCapacity = this.configuredDelegateCapacity(
193
200
  processorOptions.delegateCapacityBytes,
194
201
  );
@@ -341,6 +348,7 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
341
348
  this.outputCapacityFrames,
342
349
  );
343
350
  });
351
+ this.eventPayloadView = new Uint8Array(buffer, this.eventPayloadPtr, this.eventPayloadCapacity);
344
352
  this.delegateRecordView = this.delegateStoragePtr
345
353
  ? new Uint8Array(buffer, this.delegateStoragePtr, this.delegateCapacity)
346
354
  : null;
@@ -710,7 +718,7 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
710
718
  } else if (message.type === "event") {
711
719
  this.dispatchEvent(
712
720
  message.event ?? message.name ?? message.index,
713
- message.values ?? message.args ?? {},
721
+ message.payload,
714
722
  );
715
723
  this.postResponse(message, { type: "onda-ok", operation: message.type });
716
724
  } else if (message.type === "read-control-outputs") {
@@ -752,7 +760,7 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
752
760
  }
753
761
  }
754
762
 
755
- dispatchEvent(selector, values) {
763
+ dispatchEvent(selector, payload) {
756
764
  this.requireInitialized("event dispatch");
757
765
  const eventId = Number.isInteger(selector)
758
766
  ? selector
@@ -761,61 +769,23 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
761
769
  if (!event) {
762
770
  throw new Error(`unknown Onda event '${String(selector)}'`);
763
771
  }
764
- let payloadSize = 0;
765
- for (let paramId = 0; paramId < event.params.length; paramId += 1) {
766
- const param = event.params[paramId];
767
- const value = this.eventValue(event, param, paramId, values);
768
- if (param.is_slice) {
769
- const length = this.sequenceLength(
770
- value,
771
- `event '${event.name}' slice '${param.name}'`,
772
- );
773
- payloadSize = this.checkedEventPayloadSize(
774
- payloadSize,
775
- 4 + length * this.scalarByteSize(param.scalar),
776
- event.name,
777
- );
778
- } else {
779
- this.validateStorageValue(param, value);
780
- payloadSize = this.checkedEventPayloadSize(
781
- payloadSize,
782
- Number(param.byte_size ?? 0),
783
- event.name,
784
- );
785
- }
786
- }
772
+ if (!(payload instanceof Uint8Array)) throw new TypeError("event payload must be encoded bytes");
773
+ const payloadSize = payload.byteLength;
787
774
  if (payloadSize > this.eventPayloadCapacity) {
788
- throw new Error(
789
- `event '${event.name}' requires ${payloadSize} payload bytes; configured capacity is ${this.eventPayloadCapacity}`,
790
- );
775
+ throw new Error(`event '${event.name}' requires ${payloadSize} payload bytes; configured capacity is ${this.eventPayloadCapacity}`);
791
776
  }
792
-
793
- let offset = 0;
794
777
  const view = this.memoryView();
795
- for (let paramId = 0; paramId < event.params.length; paramId += 1) {
796
- const param = event.params[paramId];
797
- const value = this.eventValue(event, param, paramId, values);
798
- const address = this.eventPayloadPtr + offset;
799
- if (param.is_slice) {
800
- const length = this.sequenceLength(
801
- value,
802
- `event '${event.name}' slice '${param.name}'`,
803
- );
804
- view.setInt32(address, length, true);
805
- this.writeScalarValues(address + 4, param.scalar, value, length, view);
806
- offset += 4 + length * this.scalarByteSize(param.scalar);
807
- } else {
808
- this.writeStorage(address, param, value, view);
809
- offset += Number(param.byte_size ?? 0);
810
- }
811
- }
778
+ this.eventPayloadView.set(payload);
812
779
  const handler = this.exports[event.export];
813
780
  if (typeof handler !== "function") {
814
781
  throw new Error(`missing WebAssembly export '${event.export}'`);
815
782
  }
816
- this.prepareExecutionOutput();
783
+ view.setUint32(this.eventInputPtr, this.eventPayloadPtr, true);
784
+ view.setUint32(this.eventInputPtr + 4, payloadSize, true);
785
+ view.setUint32(this.eventInputPtr + 8, this.eventWorkspacePtr, true);
786
+ view.setUint32(this.eventInputPtr + 12, this.eventWorkspaceCapacity, true);
817
787
  const status = handler(
818
- this.eventPayloadPtr,
788
+ this.eventInputPtr,
819
789
  this.paramsPtr,
820
790
  this.statePtr,
821
791
  this.bufferPointersPtr,
@@ -824,6 +794,7 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
824
794
  this.bufferSampleRatesPtr,
825
795
  this.executionOutputPtr,
826
796
  );
797
+ if (status === PROCESSOR_EXECUTION_INPUT_REJECTED) throw new Error(`event '${event.name}' input rejected: invalid payload or insufficient workspace`);
827
798
  this.publishExecutionOutput(
828
799
  EXECUTION_OPERATION_EVENT,
829
800
  eventId,
@@ -831,34 +802,6 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
831
802
  this.checkExecutionStatus(status, `event '${event.name}'`);
832
803
  }
833
804
 
834
- eventValue(event, param, paramId, values) {
835
- const supplied = Array.isArray(values)
836
- ? values[paramId]
837
- : values?.[param.name];
838
- const value = supplied === undefined
839
- ? this.metadataDefaultValue(param)
840
- : supplied;
841
- if (value === undefined) {
842
- throw new Error(
843
- `event '${event.name}' requires parameter '${param.name}'`,
844
- );
845
- }
846
- return value;
847
- }
848
-
849
- checkedEventPayloadSize(current, additional, eventName) {
850
- const next = current + additional;
851
- if (
852
- !Number.isSafeInteger(additional)
853
- || additional < 0
854
- || !Number.isSafeInteger(next)
855
- || next > 0x7fff_ffff
856
- ) {
857
- throw new Error(`event '${eventName}' payload exceeds the 32-bit ABI limit`);
858
- }
859
- return next;
860
- }
861
-
862
805
  readControlOutputs() {
863
806
  this.requireInitialized("reading control outputs");
864
807
  return Object.fromEntries(