@onda-lang/webaudio 0.5.3 → 0.6.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 CHANGED
@@ -13,6 +13,7 @@ const processor = await createOndaAudioProcessor(audioContext, artifact, {
13
13
  });
14
14
  processor.node.connect(audioContext.destination);
15
15
  await processor.setParam("gain", 0.75);
16
+ await processor.setParamNormalized("cutoff", 0.5);
16
17
  const snapshot = await processor.snapshot();
17
18
  ```
18
19
 
@@ -21,6 +22,46 @@ metadata, marshals declared scalar widths, schedules arbitrary render quanta acr
21
22
  blocks, and provides request/response helpers for parameters, events, buffers, control outputs,
22
23
  reset, and portable snapshots.
23
24
 
25
+ ## Parameters
26
+
27
+ `params` passed during construction and values passed to `setParam()` are plain Onda values. The
28
+ adapter clamps ranged scalar values and snaps stepped domains before posting them; the real-time
29
+ worklet only writes the resulting canonical value in the declared scalar representation. Unknown
30
+ initial parameter names or indices are rejected before the worklet node is constructed.
31
+
32
+ `setParamNormalized()` accepts a host value in `[0, 1]`. The adapter uses the artifact descriptor's
33
+ linear or logarithmic scale and step metadata to convert it to a plain value before posting the
34
+ write to the worklet. Boolean normalized values use the `0.5` threshold. For example:
35
+
36
+ ```js
37
+ await processor.setParam("cutoff", 440); // exactly 440 Hz
38
+ await processor.setParamNormalized("cutoff", 0.5); // midpoint in its declared control scale
39
+ ```
40
+
41
+ The same synchronous conversion helpers are re-exported for UI display and typed entry:
42
+
43
+ ```js
44
+ import {
45
+ paramNormalizedToPlain,
46
+ paramPlainToNormalized,
47
+ } from "@onda-lang/webaudio";
48
+
49
+ const cutoff = processor.metadata.metadata.params.find(
50
+ (param) => param.name === "cutoff",
51
+ );
52
+ const displayValue = paramNormalizedToPlain(cutoff, sliderValue);
53
+ const sliderValueFor440Hz = paramPlainToNormalized(cutoff, 440);
54
+ ```
55
+
56
+ The helpers preserve exact endpoints, clamp and snap consistently with adapter writes, and reject
57
+ arrays or numeric parameters without a host-control domain. `scale`, `curve`, `unit`, `step_repr`,
58
+ and `step_count` remain available on each parameter's descriptor metadata for control construction
59
+ and formatting. For repeated UI conversion, `createParamControl(param)` prepares and validates the
60
+ descriptor once and returns bound conversion methods.
61
+
62
+ Controlled `i64` domains use the descriptor's exact binary64 integer range. Full-width unranged
63
+ `i64` values continue to use `bigint` when written directly.
64
+
24
65
  The artifact must be compiled for exactly `audioContext.sampleRate`; the adapter rejects a mismatch
25
66
  before registering the node so sample-rate-derived language semantics cannot silently drift. A Web
26
67
  Audio processor must expose at least one audio input or output, because an empty callback surface
@@ -55,6 +96,9 @@ rendering node.
55
96
 
56
97
  Artifact descriptors and module exports are validated by the shared, compiler-free
57
98
  `@onda-lang/processor-abi` package before anything reaches the rendering thread.
99
+ If generated init, event, or process code returns a nonzero execution status, the adapter reports
100
+ an `onda-error`, latches the failed state, and emits silence without re-entering processing. A
101
+ successful reset clears the latch after reinitializing state.
58
102
 
59
103
  Dynamic event storage is also allocated before rendering. Its default capacity is 64 KiB per
60
104
  processor with dynamic events and can be changed explicitly:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onda-lang/webaudio",
3
- "version": "0.5.3",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "description": "Optional Web Audio adapter for Onda processor WebAssembly artifacts",
6
6
  "license": "MIT",
@@ -32,7 +32,7 @@
32
32
  "test": "node --test"
33
33
  },
34
34
  "dependencies": {
35
- "@onda-lang/processor-abi": "0.5.3"
35
+ "@onda-lang/processor-abi": "0.6.0"
36
36
  },
37
37
  "publishConfig": {
38
38
  "access": "public"
package/src/index.d.ts CHANGED
@@ -1,10 +1,26 @@
1
1
  export const ONDA_AUDIO_WORKLET_PROCESSOR_NAME: "onda-wasm-processor";
2
2
 
3
- export type { OndaProcessorArtifact, OndaProcessorMetadata } from "@onda-lang/processor-abi";
4
- import type { OndaProcessorArtifact } from "@onda-lang/processor-abi";
3
+ export type {
4
+ OndaParamDomain,
5
+ OndaPreparedParamControl,
6
+ OndaProcessorArtifact,
7
+ OndaProcessorMetadata,
8
+ } from "@onda-lang/processor-abi";
9
+ export {
10
+ createParamDomain,
11
+ createParamControl,
12
+ constrainParamPlain,
13
+ paramNormalizedToPlain,
14
+ paramPlainToNormalized,
15
+ } from "@onda-lang/processor-abi";
16
+ import type {
17
+ OndaProcessorArtifact,
18
+ OndaProcessorMetadata,
19
+ } from "@onda-lang/processor-abi";
5
20
 
6
21
  export interface OndaAudioProcessorOptions {
7
22
  workletUrl?: string | URL;
23
+ /** Initial plain Onda parameter values, keyed or ordered by descriptor parameter. */
8
24
  params?: Record<string, unknown> | unknown[];
9
25
  buffers?: Record<string, unknown> | unknown[];
10
26
  /** Preallocated capacity for dynamic event payloads. Defaults to 64 KiB. */
@@ -34,10 +50,14 @@ export function compileOndaProcessorModule(
34
50
  ): Promise<WebAssembly.Module>;
35
51
 
36
52
  export class OndaAudioProcessor {
37
- constructor(node: AudioWorkletNode);
53
+ constructor(node: AudioWorkletNode, metadata?: OndaProcessorMetadata | null);
38
54
  readonly node: AudioWorkletNode;
55
+ readonly metadata: OndaProcessorMetadata | null;
39
56
  request(type: string, fields?: Record<string, unknown>, transfer?: Transferable[]): Promise<any>;
57
+ /** Set a plain Onda value; ranged scalar values are clamped and snapped. */
40
58
  setParam(param: string | number, value: unknown): Promise<any>;
59
+ /** Map a host value in [0, 1] through the descriptor and set the resulting plain value. */
60
+ setParamNormalized(param: string | number, value: number): Promise<any>;
41
61
  trigger(event: string | number, values?: Record<string, unknown> | unknown[]): Promise<any>;
42
62
  reset(): Promise<any>;
43
63
  snapshot(): Promise<Uint8Array>;
package/src/index.js CHANGED
@@ -1,8 +1,17 @@
1
1
  import {
2
+ createParamControl,
2
3
  validateProcessorArtifact,
3
4
  validateProcessorModule,
4
5
  } from "@onda-lang/processor-abi";
5
6
 
7
+ export {
8
+ createParamDomain,
9
+ createParamControl,
10
+ constrainParamPlain,
11
+ paramNormalizedToPlain,
12
+ paramPlainToNormalized,
13
+ } from "@onda-lang/processor-abi";
14
+
6
15
  export const ONDA_AUDIO_WORKLET_PROCESSOR_NAME = "onda-wasm-processor";
7
16
 
8
17
  const registrationByContext = new WeakMap();
@@ -65,7 +74,10 @@ function audioWorkletNodeOptionsFromValidated(
65
74
  ? { wasmBytes: wasm }
66
75
  : { wasmModule: options.compiledModule }),
67
76
  metadata,
68
- params: options.params ?? {},
77
+ params: constrainInitialParamValues(
78
+ metadata.metadata.params,
79
+ options.params ?? {},
80
+ ),
69
81
  buffers: options.buffers ?? {},
70
82
  eventPayloadCapacityBytes: options.eventPayloadCapacityBytes,
71
83
  },
@@ -78,6 +90,50 @@ function audioWorkletNodeOptionsFromValidated(
78
90
  return nodeOptions;
79
91
  }
80
92
 
93
+ function paramInfoFor(paramInfo, selector) {
94
+ const info = Number.isInteger(selector)
95
+ ? paramInfo[selector]
96
+ : paramInfo.find((candidate) => candidate.name === selector);
97
+ if (!info) {
98
+ throw new Error(`unknown Onda parameter '${String(selector)}'`);
99
+ }
100
+ return info;
101
+ }
102
+
103
+ function preparedParamControl(info, cache = null) {
104
+ if (Number(info.array_len) !== 1) return null;
105
+ if (info.scalar !== "bool" && info.param_control === null) return null;
106
+ let control = cache?.get(info);
107
+ if (!control) {
108
+ control = createParamControl(info);
109
+ cache?.set(info, control);
110
+ }
111
+ return control;
112
+ }
113
+
114
+ function constrainParamValue(info, value, cache = null) {
115
+ return preparedParamControl(info, cache)?.constrainPlain(value) ?? value;
116
+ }
117
+
118
+ function constrainInitialParamValues(paramInfo, values) {
119
+ if (Array.isArray(values)) {
120
+ return values.map((value, index) => (
121
+ value === undefined
122
+ ? value
123
+ : constrainParamValue(paramInfoFor(paramInfo, index), value)
124
+ ));
125
+ }
126
+ if (values && typeof values === "object") {
127
+ return Object.fromEntries(
128
+ Object.entries(values).map(([name, value]) => [
129
+ name,
130
+ constrainParamValue(paramInfoFor(paramInfo, name), value),
131
+ ]),
132
+ );
133
+ }
134
+ throw new Error("params must be an array or object");
135
+ }
136
+
81
137
  export async function registerOndaAudioWorklet(
82
138
  context,
83
139
  workletUrl = new URL("./worklet.js", import.meta.url),
@@ -123,7 +179,7 @@ export async function createOndaAudioProcessor(context, artifact, options = {})
123
179
  false,
124
180
  ),
125
181
  );
126
- return new OndaAudioProcessor(node);
182
+ return new OndaAudioProcessor(node, validated.metadata);
127
183
  }
128
184
 
129
185
  export async function compileOndaProcessorModule(artifact) {
@@ -139,8 +195,11 @@ async function compileValidatedProcessorModule({ wasm, metadata }) {
139
195
  }
140
196
 
141
197
  export class OndaAudioProcessor {
142
- constructor(node) {
198
+ constructor(node, metadata = null) {
143
199
  this.node = node;
200
+ this.metadata = metadata;
201
+ this.paramInfo = metadata?.metadata?.params ?? null;
202
+ this.paramControls = new WeakMap();
144
203
  this.nextRequestId = 1;
145
204
  this.pending = new Map();
146
205
  this.handleMessage = (event) => {
@@ -173,7 +232,39 @@ export class OndaAudioProcessor {
173
232
  }
174
233
 
175
234
  setParam(param, value) {
176
- return this.request("set-param", { param, value });
235
+ try {
236
+ if (!Array.isArray(this.paramInfo)) {
237
+ return this.request("set-param", { param, value });
238
+ }
239
+ const info = paramInfoFor(this.paramInfo ?? [], param);
240
+ return this.request("set-param", {
241
+ param,
242
+ value: constrainParamValue(info, value, this.paramControls),
243
+ });
244
+ } catch (error) {
245
+ return Promise.reject(error);
246
+ }
247
+ }
248
+
249
+ setParamNormalized(param, value) {
250
+ try {
251
+ if (!Array.isArray(this.paramInfo)) {
252
+ throw new Error(
253
+ "setParamNormalized requires processor metadata; construct the adapter with createOndaAudioProcessor()",
254
+ );
255
+ }
256
+ const info = paramInfoFor(this.paramInfo, param);
257
+ const control = preparedParamControl(info, this.paramControls);
258
+ if (!control) {
259
+ throw new Error(`Onda parameter '${info.name}' has no scalar host-control domain`);
260
+ }
261
+ return this.request("set-param", {
262
+ param,
263
+ value: control.normalizedToPlain(value),
264
+ });
265
+ } catch (error) {
266
+ return Promise.reject(error);
267
+ }
177
268
  }
178
269
 
179
270
  trigger(event, values = {}) {
package/src/worklet.js CHANGED
@@ -112,6 +112,7 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
112
112
  this.outputPtrs = [];
113
113
  this.outputCapacityFrames = 0;
114
114
  this.blockCursor = 0;
115
+ this.executionFailed = false;
115
116
 
116
117
  const paramBytes = Number(metadata.runtime?.param_size_bytes ?? 0);
117
118
  if (!Number.isInteger(paramBytes) || paramBytes < 0) {
@@ -338,14 +339,28 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
338
339
  ) {
339
340
  throw new Error(`Onda parameter '${param.name}' has invalid storage metadata`);
340
341
  }
341
- this.writeStorage(this.paramsPtr + offset, param, value);
342
+ this.writeStorage(
343
+ this.paramsPtr + offset,
344
+ param,
345
+ value,
346
+ );
342
347
  }
343
348
 
344
349
  reset() {
345
350
  this.refreshMemoryCache();
346
351
  this.stateBytes.fill(0);
347
352
  this.blockCursor = 0;
348
- this.exports.onda_init(this.paramsPtr, this.statePtr);
353
+ this.executionFailed = false;
354
+ this.checkExecutionStatus(
355
+ this.exports.onda_init(this.paramsPtr, this.statePtr),
356
+ "processor init",
357
+ );
358
+ }
359
+
360
+ checkExecutionStatus(status, operation) {
361
+ if (status === 0) return;
362
+ this.executionFailed = true;
363
+ throw new Error(`${operation} failed with Onda execution status ${String(status)}`);
349
364
  }
350
365
 
351
366
  createSnapshot() {
@@ -536,14 +551,17 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
536
551
  if (typeof handler !== "function") {
537
552
  throw new Error(`missing WebAssembly export '${event.export}'`);
538
553
  }
539
- handler(
540
- this.eventPayloadPtr,
541
- this.paramsPtr,
542
- this.statePtr,
543
- this.bufferPointersPtr,
544
- this.bufferFramesPtr,
545
- this.bufferChannelsPtr,
546
- this.bufferSampleRatesPtr,
554
+ this.checkExecutionStatus(
555
+ handler(
556
+ this.eventPayloadPtr,
557
+ this.paramsPtr,
558
+ this.statePtr,
559
+ this.bufferPointersPtr,
560
+ this.bufferFramesPtr,
561
+ this.bufferChannelsPtr,
562
+ this.bufferSampleRatesPtr,
563
+ ),
564
+ `event '${event.name}'`,
547
565
  );
548
566
  }
549
567
 
@@ -1110,8 +1128,18 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
1110
1128
  );
1111
1129
  }
1112
1130
 
1131
+ clearOutputs(outputs) {
1132
+ for (const bus of outputs) {
1133
+ for (const channel of bus) channel.fill(0);
1134
+ }
1135
+ }
1136
+
1113
1137
  process(inputs, outputs) {
1114
1138
  this.refreshMemoryCache();
1139
+ if (this.executionFailed) {
1140
+ this.clearOutputs(outputs);
1141
+ return true;
1142
+ }
1115
1143
  const frames = this.audioFrameCount(inputs, outputs);
1116
1144
 
1117
1145
  let callbackOffset = 0;
@@ -1132,11 +1160,21 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
1132
1160
  startFrame,
1133
1161
  segmentFrames,
1134
1162
  );
1135
- this.invokeProcessSegment(
1163
+ const status = this.invokeProcessSegment(
1136
1164
  startFrame,
1137
1165
  segmentFrames,
1138
1166
  flags,
1139
1167
  );
1168
+ if (status !== 0) {
1169
+ this.executionFailed = true;
1170
+ this.clearOutputs(outputs);
1171
+ this.port.postMessage({
1172
+ type: "onda-error",
1173
+ operation: "process",
1174
+ error: `processor process failed with Onda execution status ${String(status)}`,
1175
+ });
1176
+ return true;
1177
+ }
1140
1178
  this.marshalOutputSegment(
1141
1179
  outputs,
1142
1180
  frames,