@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 +35 -2
- package/api.md +31 -1
- package/package.json +2 -2
- package/src/index.js +12 -1
- package/src/worklet.js +20 -77
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
|
|
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
|
+
"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.
|
|
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
|
-
|
|
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.
|
|
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,
|
|
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
|
-
|
|
765
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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(
|