@onda-lang/webaudio 0.8.6 → 0.8.8

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
@@ -57,8 +57,13 @@ write to the worklet. Boolean normalized values use the `0.5` threshold. For exa
57
57
  ```js
58
58
  await processor.setParam("cutoff", 440); // exactly 440 Hz
59
59
  await processor.setParamNormalized("cutoff", 0.5); // midpoint in its declared control scale
60
+ await processor.setParamElement("offsets", 1, 12); // one fixed-array element
61
+ await processor.setParamElementNormalized("offsets", 1, 0.5);
60
62
  ```
61
63
 
64
+ The element methods also accept scalar parameters with element index zero. Indexed names such as
65
+ `offsets[1]` remain available through `setParam()` and `setParamNormalized()`.
66
+
62
67
  The same synchronous conversion helpers are re-exported for UI display and typed entry:
63
68
 
64
69
  ```js
@@ -128,7 +133,9 @@ Fixed arrays must have their declared length. Slice lengths may vary up to the c
128
133
  payload capacity. `i64` leaves accept `bigint`, exact safe integers, or decimal integer strings and
129
134
  reject values outside the signed 64-bit range. Missing top-level arguments use declared defaults;
130
135
  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.
136
+ worklet preflight leaves processor state usable and rejects the returned promise. Unknown
137
+ nonnegative numeric event indices are successful no-ops, matching native dispatch; unknown names
138
+ and negative indices reject the promise.
132
139
 
133
140
  ## Prints
134
141
 
@@ -249,23 +256,28 @@ const processor = await createOndaAudioProcessorInitialized(context, artifact, {
249
256
  Hosts may instead pass a flat array in physical descriptor order or key individual physical names
250
257
  such as `"bank[1]"`. Logical group metadata determines the contiguous slot range; no sample data is
251
258
  copied when Onda selects a slot while processing. Initial descriptors are installed before the
252
- initialized constructor runs full initialization, so authored top-level and proc init code can
259
+ initialized factory awaits full initialization, so authored top-level and proc init code can
253
260
  preprocess the supplied samples once before rendering begins.
254
261
 
255
262
  Artifact descriptors and module exports are validated by the shared, compiler-free
256
263
  `@onda-lang/processor-abi` package before anything reaches the rendering thread.
257
- If generated init or event code returns a nonzero execution status, the adapter reports the error
258
- to the caller. A failing process call reports an `onda-error` and emits silence. Any generated-code
259
- failure invalidates the live state, so later callbacks remain silent and stateful operations are
260
- rejected until full initialization or snapshot restoration succeeds.
264
+ If generated init or event code returns a nonzero execution status, the adapter rejects with
265
+ `OndaExecutionError`; its `operation` and `status` fields preserve the raw processor result. Event
266
+ input rejection preserves live state. The package re-exports the `PROCESSOR_EXECUTION_*` constants
267
+ for direct status comparisons. Runtime-safety failure invalidates the processor, so later callbacks
268
+ remain silent and stateful operations are rejected until full initialization or snapshot
269
+ restoration succeeds. Subscribe with `onExecutionError(listener)` to receive the same typed error
270
+ when a render-time process call fails; the failed callback emits silence.
261
271
  `init(ONDA_INIT_PRESERVE_PINNED)` reruns generated initialization
262
272
  while preserving pinned roots and task continuations; `init(ONDA_INIT_FULL)` initializes the
263
273
  complete physical state and is required before processing an instance returned by
264
- `createOndaAudioProcessor`. The initialized convenience constructor performs full initialization
265
- during worklet construction. Neither mode allocates on the successful path, and a
274
+ `createOndaAudioProcessor`. The initialized convenience factory installs its response and output
275
+ handling before requesting full initialization, then resolves only after initialization succeeds.
276
+ Neither mode allocates on the successful path, and a
266
277
  failure returns the processor to its silent pending state until full initialization or snapshot
267
278
  restore succeeds. Suspend or disconnect playback before initialization that performs substantial
268
- work.
279
+ work. Snapshot restoration performs its internal full initialization silently and does not deliver
280
+ init prints or delegates.
269
281
 
270
282
  Dynamic event storage is also allocated before rendering. Its default capacity is 64 KiB per
271
283
  processor with dynamic events and can be changed explicitly:
package/api.md CHANGED
@@ -121,8 +121,10 @@ versions rather than guessing layout compatibility.
121
121
  `createParamControl(metadata)` validates one scalar parameter descriptor and returns a reusable
122
122
  `OndaPreparedParamControl`. For an array, `createParamControl(metadata, index)` prepares one element;
123
123
  `paramElementMetadata(metadata, index)` projects its scalar descriptor, default, and storage offset.
124
- The Web Audio adapter accepts indexed names such as `offsets[1]` in `setParam` and
125
- `setParamNormalized`, and exact-length arrays in `setParam("offsets", values)`.
124
+ The Web Audio adapter exposes `setParamElement(nameOrIndex, element, value)` and
125
+ `setParamElementNormalized(nameOrIndex, element, normalized)` for individual scalar or fixed-array
126
+ elements. Indexed names such as `offsets[1]` remain accepted by `setParam` and
127
+ `setParamNormalized`; exact-length arrays use `setParam("offsets", values)`.
126
128
  `createParamDomain(domain)` does the same from already-decoded values.
127
129
  Prepared controls expose `constrainPlain`, `normalizedToPlain`, and `plainToNormalized` methods.
128
130
 
@@ -144,7 +146,8 @@ contract.
144
146
  every generated init or process entry. Generated event entries reset before input preflight, so
145
147
  rejected input returns empty batches. After a successful generated call,
146
148
  `readDelegateBatch` or `readPrintBatch` validates the result counters.
147
- `decodeDelegateRecords` converts delegate payloads using `metadata.delegates`.
149
+ `decodeDelegateBatch` validates and decodes a complete delegate batch in one call;
150
+ `decodeDelegateRecords` decodes an already-isolated storage region using `metadata.delegates`.
148
151
  `decodePrintRecords` preserves primitive types and source sites; `formatPrintRecords` and
149
152
  `formatPrintBatch` add canonical text formatting. Both decoded record types expose a `sequence` from
150
153
  the host-reset call-local counter; merge that call's two arrays by this field when presenting one
@@ -186,27 +189,36 @@ The package root re-exports the common artifact validation and file helpers. The
186
189
  - `compileOndaProcessorModule(artifact)` validates and compiles a reusable module off the render thread.
187
190
  - `ondaAudioWorkletNodeOptions(artifact, options?)` builds low-level node options.
188
191
  - `createOndaAudioProcessor(context, artifact, options?)` allocates an uninitialized adapter.
189
- - `createOndaAudioProcessorInitialized(context, artifact, options?)` allocates and fully initializes it.
192
+ - `createOndaAudioProcessorInitialized(context, artifact, options?)` allocates it, requests full
193
+ initialization through the status-bearing control protocol, and resolves only on success.
190
194
  - `flattenedAudioChannelCount(ports?)` totals declared physical channels with validation.
191
195
 
192
196
  `OndaAudioProcessorOptions` accepts initial plain parameter values, external buffers, event,
193
197
  delegate, and print capacities, an optional construction-time `onPrint` listener, a precompiled
194
198
  module, custom node options, and an `AudioWorkletNode` constructor. Pass `onPrint` when using the
195
- initialized constructor if initialization output must be observed; registering a listener after
199
+ initialized factory if initialization output must be observed; registering a listener after
196
200
  construction cannot replay output from an execution that has already completed. The artifact sample
197
201
  rate must equal the context sample rate and it must expose at least one audio input or output.
198
202
  Print and delegate delivery uses a bounded `SharedArrayBuffer` ring and requires cross-origin
199
203
  isolation in browsers; ordinary audio processing remains available when shared memory is absent.
200
204
  Low-level callers that manually pair the returned node options with `OndaAudioProcessor` must pass
201
- `processorOptions.executionOutputRing` as the adapter constructor's fourth argument.
205
+ the artifact metadata as the adapter constructor's second argument and
206
+ `processorOptions.executionOutputRing` as its fourth argument. Event encoding and numeric
207
+ unknown-index classification require that metadata.
202
208
 
203
209
  ### `OndaAudioProcessor`
204
210
 
205
211
  The adapter exposes its `node` and validated `metadata`, plus these operations:
206
212
 
207
- - `setParam(nameOrIndex, plain)` and `setParamNormalized(nameOrIndex, normalized)`.
208
- - `trigger(nameOrIndex, values?)` for input events.
209
- - `onDelegates(listener)` and `onPrint(listener)`, each returning an unsubscribe function.
213
+ - `setParam(nameOrIndex, plain)` and `setParamNormalized(nameOrIndex, normalized)` for whole
214
+ parameters.
215
+ - `setParamElement(nameOrIndex, element, plain)` and
216
+ `setParamElementNormalized(nameOrIndex, element, normalized)` for one scalar or fixed-array
217
+ element.
218
+ - `trigger(nameOrIndex, values?)` for input events. Unknown nonnegative numeric indices are
219
+ successful no-ops, matching native dispatch; unknown names and negative indices are rejected.
220
+ - `onExecutionError(listener)`, `onDelegates(listener)`, and `onPrint(listener)`, each returning an
221
+ unsubscribe function.
210
222
  - `init(mode)`, `snapshot()`, and `restoreSnapshot(bytes)`.
211
223
  - `readControlOutputs()` and `readBuffer(nameOrIndex)`.
212
224
  - `request(type, fields?, transfer?)` for adapter protocol extensions.
@@ -224,7 +236,24 @@ each init, event, or process segment.
224
236
 
225
237
  `ONDA_INIT_FULL` clears and initializes all physical state. `ONDA_INIT_PRESERVE_PINNED` retains
226
238
  pinned state according to the processor ABI. The package also re-exports the prepared and one-shot
227
- parameter conversion helpers.
239
+ parameter conversion helpers. Snapshot restoration performs its internal full initialization
240
+ silently, without delivering init prints or delegates.
241
+
242
+ Raw WebAssembly exports use the same generated status values as native processor objects:
243
+ `PROCESSOR_EXECUTION_OK` (`0`), `PROCESSOR_EXECUTION_RUNTIME_SAFETY_FAILURE` (`1`), and event-only
244
+ `PROCESSOR_EXECUTION_INPUT_REJECTED` (`2`). The Web Audio adapter translates host-side validation
245
+ problems into rejected promises before generated execution. It also rejects init and event promises
246
+ when generated code returns a nonzero status: status `1` invalidates the live processor, while event
247
+ status `2` preserves it. These generated failures use `OndaExecutionError`, whose `operation` and
248
+ `status` fields retain the machine-readable failure. The Web Audio root re-exports all three
249
+ `PROCESSOR_EXECUTION_*` constants for direct comparison. Render-time process failure invalidates the
250
+ processor and silences subsequent callbacks. `onExecutionError(listener)` receives its typed
251
+ `OndaExecutionError`; requested init and event failures remain on their promise rejection path and
252
+ are not reported twice.
253
+ The raw `@onda-lang/processor-abi` package exposes `PROCESSOR_BEGIN_BLOCK`,
254
+ `PROCESSOR_END_BLOCK`, and `PROCESSOR_FULL_BLOCK` for native and direct WebAssembly hosts that use
255
+ segmented processing. Web Audio schedules render quanta internally and has no segmented-process
256
+ method.
228
257
 
229
258
  `@onda-lang/webaudio/worklet` is the side-effect-only AudioWorklet registration module and has no
230
259
  named exports.
@@ -271,8 +300,11 @@ PRINT_RECORD_HEADER_SIZE_BYTES
271
300
  PROCESSOR_ABI_VERSION
272
301
  PROCESSOR_ARTIFACT_FORMAT
273
302
  PROCESSOR_ARTIFACT_FORMAT_VERSION
303
+ PROCESSOR_BEGIN_BLOCK
304
+ PROCESSOR_END_BLOCK
274
305
  PROCESSOR_EXECUTION_OK
275
306
  PROCESSOR_EXECUTION_RUNTIME_SAFETY_FAILURE
307
+ PROCESSOR_FULL_BLOCK
276
308
  PROCESSOR_INIT_FULL
277
309
  PROCESSOR_INIT_PRESERVE_PINNED
278
310
  PROCESSOR_SNAPSHOT_FORMAT_VERSION
@@ -281,6 +313,7 @@ createParamControl
281
313
  paramElementMetadata
282
314
  createParamDomain
283
315
  createProcessorArtifactFiles
316
+ decodeDelegateBatch
284
317
  decodeDelegateRecords
285
318
  decodePrintRecords
286
319
  formatPrintBatch
@@ -320,12 +353,19 @@ OndaArtifactError
320
353
  OndaBinaryenError
321
354
  OndaBinaryenOptions
322
355
  OndaProcessorArtifact
356
+ OndaProcessorInitMode
323
357
  OndaProcessorMetadata
324
358
  PROCESSOR_ABI_VERSION
325
359
  PROCESSOR_ARTIFACT_FORMAT
326
360
  PROCESSOR_ARTIFACT_FORMAT_VERSION
361
+ PROCESSOR_BEGIN_BLOCK
362
+ PROCESSOR_END_BLOCK
363
+ PROCESSOR_EXECUTION_INPUT_REJECTED
327
364
  PROCESSOR_EXECUTION_OK
328
365
  PROCESSOR_EXECUTION_RUNTIME_SAFETY_FAILURE
366
+ PROCESSOR_FULL_BLOCK
367
+ PROCESSOR_INIT_FULL
368
+ PROCESSOR_INIT_PRESERVE_PINNED
329
369
  PROCESSOR_SNAPSHOT_FORMAT_VERSION
330
370
  SUPPORTED_MIR_SCHEMA_VERSION
331
371
  compileTrustedMir
@@ -366,6 +406,7 @@ OndaLspAnalysisOptions
366
406
  OndaLspMessage
367
407
  OndaMaterializedProjectFile
368
408
  OndaProcessorArtifact
409
+ OndaProcessorInitMode
369
410
  OndaProcessorMetadata
370
411
  OndaProjectBufferInfo
371
412
  OndaProjectCapabilities
@@ -383,8 +424,14 @@ OndaWorkerLike
383
424
  PROCESSOR_ABI_VERSION
384
425
  PROCESSOR_ARTIFACT_FORMAT
385
426
  PROCESSOR_ARTIFACT_FORMAT_VERSION
427
+ PROCESSOR_BEGIN_BLOCK
428
+ PROCESSOR_END_BLOCK
429
+ PROCESSOR_EXECUTION_INPUT_REJECTED
386
430
  PROCESSOR_EXECUTION_OK
387
431
  PROCESSOR_EXECUTION_RUNTIME_SAFETY_FAILURE
432
+ PROCESSOR_FULL_BLOCK
433
+ PROCESSOR_INIT_FULL
434
+ PROCESSOR_INIT_PRESERVE_PINNED
388
435
  PROCESSOR_SNAPSHOT_FORMAT_VERSION
389
436
  WorkerCompilerOptions
390
437
  createCompiler
@@ -408,11 +455,16 @@ OndaAudioProcessor
408
455
  OndaAudioProcessorOptions
409
456
  OndaAudioPrintBatch
410
457
  OndaAudioPrintListener
458
+ OndaExecutionError
459
+ OndaExecutionErrorListener
411
460
  OndaInitMode
412
461
  OndaParamDomain
413
462
  OndaPreparedParamControl
414
463
  OndaProcessorArtifact
415
464
  OndaProcessorMetadata
465
+ PROCESSOR_EXECUTION_INPUT_REJECTED
466
+ PROCESSOR_EXECUTION_OK
467
+ PROCESSOR_EXECUTION_RUNTIME_SAFETY_FAILURE
416
468
  compileOndaProcessorModule
417
469
  constrainParamPlain
418
470
  createOndaAudioProcessor
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onda-lang/webaudio",
3
- "version": "0.8.6",
3
+ "version": "0.8.8",
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.6"
37
+ "@onda-lang/processor-abi": "0.8.8"
38
38
  },
39
39
  "publishConfig": {
40
40
  "access": "public"
package/src/index.d.ts CHANGED
@@ -3,6 +3,21 @@ export const ONDA_INIT_PRESERVE_PINNED: 0;
3
3
  export const ONDA_INIT_FULL: 1;
4
4
  export type OndaInitMode = 0 | 1;
5
5
 
6
+ export {
7
+ PROCESSOR_EXECUTION_INPUT_REJECTED,
8
+ PROCESSOR_EXECUTION_OK,
9
+ PROCESSOR_EXECUTION_RUNTIME_SAFETY_FAILURE,
10
+ } from "@onda-lang/processor-abi";
11
+
12
+ /** A nonzero status returned by generated processor execution. */
13
+ export class OndaExecutionError extends Error {
14
+ constructor(message: string, operation: string, status: number);
15
+ readonly operation: string;
16
+ readonly status: number;
17
+ }
18
+
19
+ export type OndaExecutionErrorListener = (error: OndaExecutionError) => void;
20
+
6
21
  export type {
7
22
  OndaParamDomain,
8
23
  OndaPreparedParamControl,
@@ -88,6 +103,7 @@ export function createOndaAudioProcessor(
88
103
  artifact: OndaProcessorArtifact,
89
104
  options?: OndaAudioProcessorOptions,
90
105
  ): Promise<OndaAudioProcessor>;
106
+ /** Resolves after full initialization succeeds; generated failures reject with OndaExecutionError. */
91
107
  export function createOndaAudioProcessorInitialized(
92
108
  context: BaseAudioContext,
93
109
  artifact: OndaProcessorArtifact,
@@ -110,9 +126,20 @@ export class OndaAudioProcessor {
110
126
  request(type: string, fields?: Record<string, unknown>, transfer?: Transferable[]): Promise<any>;
111
127
  /** Set a plain Onda value; ranged scalar values are clamped and snapped. */
112
128
  setParam(param: string | number, value: unknown): Promise<any>;
129
+ /** Set one scalar or fixed-array parameter element without modifying its siblings. */
130
+ setParamElement(param: string | number, element: number, value: unknown): Promise<any>;
113
131
  /** Map a host value in [0, 1] through the descriptor and set the resulting plain value. */
114
132
  setParamNormalized(param: string | number, value: number): Promise<any>;
133
+ /** Normalized form of setParamElement(). */
134
+ setParamElementNormalized(
135
+ param: string | number,
136
+ element: number,
137
+ value: number,
138
+ ): Promise<any>;
139
+ /** Unknown nonnegative numeric event indices are successful no-ops; unknown names are rejected. */
115
140
  trigger(event: string | number, values?: Record<string, unknown> | unknown[]): Promise<any>;
141
+ /** Subscribe to asynchronous render-time generated failures. */
142
+ onExecutionError(listener: OndaExecutionErrorListener): () => boolean;
116
143
  /**
117
144
  * Subscribe to batches decoded after generated execution. Collection is active only while at
118
145
  * least one listener is registered. overflowCount reports configured-capacity loss and
package/src/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { paramAddress } from "./param-metadata.js";
1
+ import { paramAddress, paramElementAddress } from "./param-metadata.js";
2
2
  import {
3
3
  PayloadPlan,
4
4
  createParamControl,
@@ -18,6 +18,10 @@ import {
18
18
  drainExecutionOutputRing,
19
19
  openExecutionOutputRing,
20
20
  } from "./execution-output-ring.js";
21
+ import {
22
+ PROCESSOR_INIT_FULL,
23
+ PROCESSOR_INIT_PRESERVE_PINNED,
24
+ } from "./processor-constants.js";
21
25
 
22
26
  export {
23
27
  createParamDomain,
@@ -25,11 +29,23 @@ export {
25
29
  constrainParamPlain,
26
30
  paramNormalizedToPlain,
27
31
  paramPlainToNormalized,
32
+ PROCESSOR_EXECUTION_INPUT_REJECTED,
33
+ PROCESSOR_EXECUTION_OK,
34
+ PROCESSOR_EXECUTION_RUNTIME_SAFETY_FAILURE,
28
35
  } from "@onda-lang/processor-abi";
29
36
 
30
37
  export const ONDA_AUDIO_WORKLET_PROCESSOR_NAME = "onda-wasm-processor";
31
- export const ONDA_INIT_PRESERVE_PINNED = 0;
32
- export const ONDA_INIT_FULL = 1;
38
+ export const ONDA_INIT_PRESERVE_PINNED = PROCESSOR_INIT_PRESERVE_PINNED;
39
+ export const ONDA_INIT_FULL = PROCESSOR_INIT_FULL;
40
+
41
+ export class OndaExecutionError extends Error {
42
+ constructor(message, operation, status) {
43
+ super(message);
44
+ this.name = "OndaExecutionError";
45
+ this.operation = operation;
46
+ this.status = status;
47
+ }
48
+ }
33
49
 
34
50
  const registrationByContext = new WeakMap();
35
51
  const DEFAULT_DELEGATE_CAPACITY_BYTES = 64 * 1024;
@@ -171,6 +187,11 @@ function paramInfoFor(params, selector, cache = null) {
171
187
  return element === null ? info : paramElementInfo(info, element, cache);
172
188
  }
173
189
 
190
+ function paramElementInfoFor(params, selector, element, cache = null) {
191
+ const address = paramElementAddress(params, selector, element);
192
+ return paramElementInfo(address.info, address.element, cache);
193
+ }
194
+
174
195
  function preparedParamControl(info, cache = null) {
175
196
  if (info.type_repr !== info.scalar) return null;
176
197
  if (info.scalar !== "bool" && info.param_control === null) return null;
@@ -263,7 +284,7 @@ async function createOndaAudioProcessorImpl(context, artifact, options, initiali
263
284
  }
264
285
  const nodeOptions = audioWorkletNodeOptionsFromValidated(
265
286
  validated,
266
- { ...options, compiledModule, initialize },
287
+ { ...options, compiledModule, initialize: false },
267
288
  false,
268
289
  );
269
290
  const node = new NodeConstructor(
@@ -271,12 +292,32 @@ async function createOndaAudioProcessorImpl(context, artifact, options, initiali
271
292
  ONDA_AUDIO_WORKLET_PROCESSOR_NAME,
272
293
  nodeOptions,
273
294
  );
274
- return new OndaAudioProcessor(
295
+ const processor = new OndaAudioProcessor(
275
296
  node,
276
297
  validated.metadata,
277
298
  options.onPrint,
278
299
  nodeOptions.processorOptions.executionOutputRing,
279
300
  );
301
+ if (!initialize) return processor;
302
+ try {
303
+ await processor.init(ONDA_INIT_FULL);
304
+ return processor;
305
+ } catch (error) {
306
+ // Initialization publishes diagnostic prints before reporting its status. Consume any
307
+ // already-published records while the construction-time listener is still installed.
308
+ try {
309
+ processor.drainExecutionOutput();
310
+ } catch {
311
+ // Preserve the initialization failure if consuming its diagnostics also fails.
312
+ }
313
+ processor.close(error);
314
+ try {
315
+ node.disconnect?.();
316
+ } catch {
317
+ // Preserve the initialization failure if node teardown also fails.
318
+ }
319
+ throw error;
320
+ }
280
321
  }
281
322
 
282
323
  export async function compileOndaProcessorModule(artifact) {
@@ -312,6 +353,7 @@ export class OndaAudioProcessor {
312
353
  this.paramElements = new WeakMap();
313
354
  this.nextRequestId = 1;
314
355
  this.pending = new Map();
356
+ this.executionErrorListeners = new Set();
315
357
  this.delegateListeners = new Set();
316
358
  this.delegateSubscriptionId = 0;
317
359
  this.printListeners = new Set(
@@ -333,15 +375,22 @@ export class OndaAudioProcessor {
333
375
  this.closeReason = null;
334
376
  this.handleMessage = (event) => {
335
377
  const message = event.data ?? {};
336
- if (message.requestId === undefined) return;
378
+ const error = message.type === "onda-error"
379
+ ? Number.isInteger(message.status)
380
+ ? new OndaExecutionError(message.error, message.operation, message.status)
381
+ : new Error(message.error)
382
+ : null;
383
+ if (message.requestId === undefined) {
384
+ if (error instanceof OndaExecutionError) {
385
+ this.notifyListeners(this.executionErrorListeners, error);
386
+ }
387
+ return;
388
+ }
337
389
  const pending = this.pending.get(message.requestId);
338
390
  if (!pending) return;
339
391
  this.pending.delete(message.requestId);
340
- if (message.type === "onda-error") {
341
- pending.reject(new Error(message.error));
342
- } else {
343
- pending.resolve(message);
344
- }
392
+ if (error) pending.reject(error);
393
+ else pending.resolve(message);
345
394
  };
346
395
  node.port.addEventListener("message", this.handleMessage);
347
396
  node.port.start?.();
@@ -375,7 +424,7 @@ export class OndaAudioProcessor {
375
424
  if (result.async) await result.value;
376
425
  }
377
426
  };
378
- void wait().catch((error) => this.reportExecutionOutputError(error));
427
+ void wait().catch((error) => this.reportListenerError(error));
379
428
  }
380
429
 
381
430
  stopExecutionOutputDrain() {
@@ -398,18 +447,18 @@ export class OndaAudioProcessor {
398
447
  );
399
448
  }
400
449
 
401
- reportExecutionOutputError(error) {
450
+ reportListenerError(error) {
402
451
  queueMicrotask(() => {
403
452
  throw error;
404
453
  });
405
454
  }
406
455
 
407
- notifyExecutionOutputListeners(listeners, batch) {
456
+ notifyListeners(listeners, value) {
408
457
  for (const listener of listeners) {
409
458
  try {
410
- listener(batch);
459
+ listener(value);
411
460
  } catch (error) {
412
- this.reportExecutionOutputError(error);
461
+ this.reportListenerError(error);
413
462
  }
414
463
  }
415
464
  }
@@ -422,7 +471,7 @@ export class OndaAudioProcessor {
422
471
  try {
423
472
  this.consumeExecutionOutput(entry);
424
473
  } catch (error) {
425
- this.reportExecutionOutputError(error);
474
+ this.reportListenerError(error);
426
475
  }
427
476
  },
428
477
  );
@@ -540,7 +589,7 @@ export class OndaAudioProcessor {
540
589
  entries: batchRecords.map((record) => record.entry),
541
590
  ...meta,
542
591
  };
543
- this.notifyExecutionOutputListeners(this.printListeners, batch);
592
+ this.notifyListeners(this.printListeners, batch);
544
593
  } else {
545
594
  const meta = delegate ?? { overflowCount: 0, transportDropCount: 0 };
546
595
  delegate = null;
@@ -550,7 +599,7 @@ export class OndaAudioProcessor {
550
599
  occurrences: batchRecords.map((record) => record.occurrence),
551
600
  ...meta,
552
601
  };
553
- this.notifyExecutionOutputListeners(this.delegateListeners, batch);
602
+ this.notifyListeners(this.delegateListeners, batch);
554
603
  }
555
604
  cursor = end;
556
605
  }
@@ -562,7 +611,7 @@ export class OndaAudioProcessor {
562
611
  entries: [],
563
612
  ...print,
564
613
  };
565
- this.notifyExecutionOutputListeners(this.printListeners, batch);
614
+ this.notifyListeners(this.printListeners, batch);
566
615
  }
567
616
  if (
568
617
  delegate
@@ -574,7 +623,7 @@ export class OndaAudioProcessor {
574
623
  occurrences: [],
575
624
  ...delegate,
576
625
  };
577
- this.notifyExecutionOutputListeners(this.delegateListeners, batch);
626
+ this.notifyListeners(this.delegateListeners, batch);
578
627
  }
579
628
  }
580
629
 
@@ -609,6 +658,29 @@ export class OndaAudioProcessor {
609
658
  }
610
659
  }
611
660
 
661
+ setParamElement(param, element, value) {
662
+ try {
663
+ if (!Array.isArray(this.paramInfo)) {
664
+ throw new Error(
665
+ "setParamElement requires processor metadata; construct the adapter with createOndaAudioProcessor()",
666
+ );
667
+ }
668
+ const info = paramElementInfoFor(
669
+ this.paramInfo,
670
+ param,
671
+ element,
672
+ this.paramElements,
673
+ );
674
+ return this.request("set-param", {
675
+ param,
676
+ element,
677
+ value: constrainParamValue(info, value, this.paramControls, this.paramElements),
678
+ });
679
+ } catch (error) {
680
+ return Promise.reject(error);
681
+ }
682
+ }
683
+
612
684
  setParamNormalized(param, value) {
613
685
  try {
614
686
  if (!Array.isArray(this.paramInfo)) {
@@ -630,11 +702,51 @@ export class OndaAudioProcessor {
630
702
  }
631
703
  }
632
704
 
705
+ setParamElementNormalized(param, element, value) {
706
+ try {
707
+ if (!Array.isArray(this.paramInfo)) {
708
+ throw new Error(
709
+ "setParamElementNormalized requires processor metadata; construct the adapter with createOndaAudioProcessor()",
710
+ );
711
+ }
712
+ const info = paramElementInfoFor(
713
+ this.paramInfo,
714
+ param,
715
+ element,
716
+ this.paramElements,
717
+ );
718
+ const control = preparedParamControl(info, this.paramControls);
719
+ if (!control) {
720
+ throw new Error(`Onda parameter '${info.name}' has no scalar host-control domain`);
721
+ }
722
+ return this.request("set-param", {
723
+ param,
724
+ element,
725
+ value: control.normalizedToPlain(value),
726
+ });
727
+ } catch (error) {
728
+ return Promise.reject(error);
729
+ }
730
+ }
731
+
633
732
  trigger(event, values = {}) {
634
733
  try {
635
734
  this.assertOpen();
636
735
  const events = this.metadata?.metadata?.events;
637
- const info = Number.isInteger(event) ? events?.[event] : events?.find((entry) => entry.name === event);
736
+ if (Number.isInteger(event)) {
737
+ if (!Array.isArray(events)) {
738
+ throw new Error(
739
+ "event triggers require processor metadata; construct the adapter with createOndaAudioProcessor()",
740
+ );
741
+ }
742
+ if (event >= events.length) {
743
+ const payload = new Uint8Array();
744
+ return this.request("event", { event, payload }, [payload.buffer]);
745
+ }
746
+ }
747
+ const info = Number.isInteger(event)
748
+ ? events?.[event]
749
+ : events?.find((entry) => entry.name === event);
638
750
  if (!info) throw new Error(`unknown Onda event '${String(event)}'`);
639
751
  let plan = this.eventPlans.get(info);
640
752
  if (!plan) { plan = new PayloadPlan(info.schema); this.eventPlans.set(info, plan); }
@@ -643,6 +755,15 @@ export class OndaAudioProcessor {
643
755
  } catch (error) { return Promise.reject(error); }
644
756
  }
645
757
 
758
+ onExecutionError(listener) {
759
+ this.assertOpen();
760
+ if (typeof listener !== "function") {
761
+ throw new TypeError("execution-error listener must be a function");
762
+ }
763
+ this.executionErrorListeners.add(listener);
764
+ return () => this.executionErrorListeners.delete(listener);
765
+ }
766
+
646
767
  onDelegates(listener) {
647
768
  this.assertOpen();
648
769
  if (typeof listener !== "function") {
@@ -775,6 +896,7 @@ export class OndaAudioProcessor {
775
896
  this.stopExecutionOutputDrain();
776
897
  for (const pending of this.pending.values()) pending.reject(this.closeReason);
777
898
  this.pending.clear();
899
+ this.executionErrorListeners.clear();
778
900
  this.delegateListeners.clear();
779
901
  this.printListeners.clear();
780
902
  }
@@ -16,3 +16,18 @@ export function paramAddress(params, selector) {
16
16
  }
17
17
  throw new Error(`unknown Onda parameter '${String(selector)}'`);
18
18
  }
19
+
20
+ export function paramElementAddress(params, selector, element) {
21
+ const address = paramAddress(params, selector);
22
+ if (address.element !== null) {
23
+ throw new Error("an explicit element write requires a parameter name or index");
24
+ }
25
+ if (
26
+ !Number.isSafeInteger(element)
27
+ || element < 0
28
+ || element >= address.info.array_len
29
+ ) {
30
+ throw new Error(`Onda parameter '${address.info.name}' element index is out of bounds`);
31
+ }
32
+ return { info: address.info, element };
33
+ }
@@ -0,0 +1,10 @@
1
+ // Keep the AudioWorklet dependency graph relative-only so the published module can be loaded
2
+ // directly. The adapter tests verify these processor ABI values against @onda-lang/processor-abi.
3
+ export const PROCESSOR_EXECUTION_OK = 0;
4
+ export const PROCESSOR_EXECUTION_RUNTIME_SAFETY_FAILURE = 1;
5
+ export const PROCESSOR_EXECUTION_INPUT_REJECTED = 2;
6
+ export const PROCESSOR_BEGIN_BLOCK = 1 << 0;
7
+ export const PROCESSOR_END_BLOCK = 1 << 1;
8
+ export const PROCESSOR_FULL_BLOCK = PROCESSOR_BEGIN_BLOCK | PROCESSOR_END_BLOCK;
9
+ export const PROCESSOR_INIT_PRESERVE_PINNED = 0;
10
+ export const PROCESSOR_INIT_FULL = 1;
package/src/worklet.js CHANGED
@@ -1,4 +1,4 @@
1
- import { paramAddress } from "./param-metadata.js";
1
+ import { paramAddress, paramElementAddress } from "./param-metadata.js";
2
2
  import {
3
3
  EXECUTION_OPERATION_EVENT,
4
4
  EXECUTION_OPERATION_INIT,
@@ -7,16 +7,25 @@ import {
7
7
  openExecutionOutputRing,
8
8
  writeExecutionOutputRing,
9
9
  } from "./execution-output-ring.js";
10
-
11
- const ONDA_PROCESS_BEGIN_BLOCK = 1 << 0;
12
- const ONDA_PROCESS_END_BLOCK = 1 << 1;
13
- const ONDA_INIT_PRESERVE_PINNED = 0;
14
- const ONDA_INIT_FULL = 1;
15
- const PROCESSOR_EXECUTION_INPUT_REJECTED = 2;
10
+ import {
11
+ PROCESSOR_BEGIN_BLOCK,
12
+ PROCESSOR_END_BLOCK,
13
+ PROCESSOR_EXECUTION_OK,
14
+ PROCESSOR_EXECUTION_INPUT_REJECTED,
15
+ PROCESSOR_INIT_FULL,
16
+ PROCESSOR_INIT_PRESERVE_PINNED,
17
+ } from "./processor-constants.js";
16
18
  const ONDA_AUDIO_WORKLET_PROCESSOR_NAME = "onda-wasm-processor";
17
19
  const DEFAULT_EVENT_PAYLOAD_CAPACITY_BYTES = 64 * 1024;
18
20
  const DEFAULT_DELEGATE_CAPACITY_BYTES = 64 * 1024;
19
21
  const DEFAULT_PRINT_CAPACITY_BYTES = 64 * 1024;
22
+
23
+ class ProcessorExecutionError extends Error {
24
+ constructor(message, status) {
25
+ super(message);
26
+ this.status = status;
27
+ }
28
+ }
20
29
  const DELEGATE_BATCH_SIZE_BYTES = 20;
21
30
  const PRINT_BATCH_SIZE_BYTES = 20;
22
31
  const EXECUTION_OUTPUT_SIZE_BYTES = 12;
@@ -289,7 +298,7 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
289
298
  this.refreshMemoryCache(true);
290
299
  this.invalidateState();
291
300
  if (processorOptions.initialize === true) {
292
- this.init(ONDA_INIT_FULL);
301
+ this.init(PROCESSOR_INIT_FULL);
293
302
  }
294
303
  this.allocationLocked = true;
295
304
  this.port.onmessage = (event) => this.handleMessage(event.data ?? {});
@@ -485,8 +494,10 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
485
494
  }
486
495
  }
487
496
 
488
- setParam(selector, value) {
489
- const { info: param, element } = paramAddress(this.paramInfo, selector);
497
+ setParam(selector, value, explicitElement = null) {
498
+ const { info: param, element } = explicitElement === null
499
+ ? paramAddress(this.paramInfo, selector)
500
+ : paramElementAddress(this.paramInfo, selector, explicitElement);
490
501
  if (value === undefined) {
491
502
  throw new Error(`Onda parameter '${param.name}' requires a value`);
492
503
  }
@@ -517,10 +528,10 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
517
528
  }
518
529
 
519
530
  init(mode) {
520
- if (mode !== ONDA_INIT_PRESERVE_PINNED && mode !== ONDA_INIT_FULL) {
531
+ if (mode !== PROCESSOR_INIT_PRESERVE_PINNED && mode !== PROCESSOR_INIT_FULL) {
521
532
  throw new Error(`invalid Onda init mode '${String(mode)}'`);
522
533
  }
523
- if (mode === ONDA_INIT_PRESERVE_PINNED && !this.initialized) {
534
+ if (mode === PROCESSOR_INIT_PRESERVE_PINNED && !this.initialized) {
524
535
  throw new Error("full initialization is required before preserving pinned state");
525
536
  }
526
537
  this.runInitialization(mode);
@@ -538,7 +549,7 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
538
549
  this.blockCursor = blockCursor;
539
550
  }
540
551
 
541
- runInitialization(mode, afterInitialize) {
552
+ runInitialization(mode, afterInitialize, collectOutput = true) {
542
553
  // Reinitializing state does not create a compile-block boundary. Retain
543
554
  // the host-side position so the next process segment cannot synthesize an
544
555
  // extra BEGIN_BLOCK or postpone the matching END_BLOCK.
@@ -548,7 +559,7 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
548
559
  // leaves the processor on the silent pending path.
549
560
  this.invalidateState();
550
561
  this.refreshMemoryCache();
551
- this.prepareExecutionOutput();
562
+ if (collectOutput) this.prepareExecutionOutput();
552
563
  const status = this.exports.onda_processor_init(
553
564
  this.paramsPtr,
554
565
  this.statePtr,
@@ -557,13 +568,15 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
557
568
  this.bufferFramesPtr,
558
569
  this.bufferChannelsPtr,
559
570
  this.bufferSampleRatesPtr,
560
- this.executionOutputPtr,
571
+ collectOutput ? this.executionOutputPtr : 0,
561
572
  );
562
573
  // Generated failures retain diagnostic prints but clear delegates.
563
- this.publishExecutionOutput(
564
- EXECUTION_OPERATION_INIT,
565
- 0,
566
- );
574
+ if (collectOutput) {
575
+ this.publishExecutionOutput(
576
+ EXECUTION_OPERATION_INIT,
577
+ 0,
578
+ );
579
+ }
567
580
  this.checkExecutionStatus(status, "processor init");
568
581
  afterInitialize?.();
569
582
  this.commitInitializedState(blockCursor);
@@ -576,9 +589,12 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
576
589
  }
577
590
 
578
591
  checkExecutionStatus(status, operation) {
579
- if (status === 0) return;
592
+ if (status === PROCESSOR_EXECUTION_OK) return;
580
593
  this.invalidateState();
581
- throw new Error(`${operation} failed with Onda execution status ${String(status)}`);
594
+ throw new ProcessorExecutionError(
595
+ `${operation} failed with Onda execution status ${String(status)}`,
596
+ status,
597
+ );
582
598
  }
583
599
 
584
600
  createSnapshot() {
@@ -609,7 +625,7 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
609
625
  // The ABI restore base is a fresh post-init image, so scratch and
610
626
  // control-mirror state never leak across a restore. Initialization and
611
627
  // overlay form one lifecycle transition: neither partial result is ready.
612
- this.runInitialization(ONDA_INIT_FULL, () => {
628
+ this.runInitialization(PROCESSOR_INIT_FULL, () => {
613
629
  const state = this.stateBytes;
614
630
  for (const entry of this.snapshotInfo) {
615
631
  const packedOffset = Number(entry.packed_snapshot_byte_offset);
@@ -622,7 +638,7 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
622
638
  );
623
639
  this.normalizeSnapshotIntegerRange(entry, state, physicalOffset, byteSize);
624
640
  }
625
- });
641
+ }, false);
626
642
  }
627
643
 
628
644
  normalizeSnapshotIntegerRange(entry, state, offset, byteSize) {
@@ -710,6 +726,7 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
710
726
  this.setParam(
711
727
  message.param ?? message.name ?? message.index,
712
728
  message.value,
729
+ message.element ?? null,
713
730
  );
714
731
  this.postResponse(message, { type: "onda-ok", operation: message.type });
715
732
  } else if (message.type === "init") {
@@ -751,17 +768,20 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
751
768
  throw new Error(`unknown Onda worklet operation '${String(message.type)}'`);
752
769
  }
753
770
  } catch (error) {
754
- this.port.postMessage({
771
+ const response = {
755
772
  type: "onda-error",
756
773
  operation: message.type ?? "unknown",
757
774
  requestId: message.requestId,
758
775
  error: String(error && error.message ? error.message : error),
759
- });
776
+ };
777
+ if (Number.isInteger(error?.status)) response.status = error.status;
778
+ this.port.postMessage(response);
760
779
  }
761
780
  }
762
781
 
763
782
  dispatchEvent(selector, payload) {
764
783
  this.requireInitialized("event dispatch");
784
+ if (Number.isInteger(selector) && selector >= this.eventInfo.length) return;
765
785
  const eventId = Number.isInteger(selector)
766
786
  ? selector
767
787
  : this.eventInfo.findIndex((event) => event.name === selector);
@@ -794,7 +814,12 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
794
814
  this.bufferSampleRatesPtr,
795
815
  this.executionOutputPtr,
796
816
  );
797
- if (status === PROCESSOR_EXECUTION_INPUT_REJECTED) throw new Error(`event '${event.name}' input rejected: invalid payload or insufficient workspace`);
817
+ if (status === PROCESSOR_EXECUTION_INPUT_REJECTED) {
818
+ throw new ProcessorExecutionError(
819
+ `event '${event.name}' input rejected: invalid payload or insufficient workspace`,
820
+ status,
821
+ );
822
+ }
798
823
  this.publishExecutionOutput(
799
824
  EXECUTION_OPERATION_EVENT,
800
825
  eventId,
@@ -1559,8 +1584,8 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
1559
1584
  this.blockSize - startFrame,
1560
1585
  );
1561
1586
  const endsBlock = startFrame + segmentFrames === this.blockSize;
1562
- const flags = (startFrame === 0 ? ONDA_PROCESS_BEGIN_BLOCK : 0)
1563
- | (endsBlock ? ONDA_PROCESS_END_BLOCK : 0);
1587
+ const flags = (startFrame === 0 ? PROCESSOR_BEGIN_BLOCK : 0)
1588
+ | (endsBlock ? PROCESSOR_END_BLOCK : 0);
1564
1589
 
1565
1590
  this.marshalInputSegment(
1566
1591
  inputs,
@@ -1578,12 +1603,13 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
1578
1603
  EXECUTION_OPERATION_PROCESS,
1579
1604
  0,
1580
1605
  );
1581
- if (status !== 0) {
1606
+ if (status !== PROCESSOR_EXECUTION_OK) {
1582
1607
  this.invalidateState();
1583
1608
  this.clearOutputs(outputs);
1584
1609
  this.port.postMessage({
1585
1610
  type: "onda-error",
1586
1611
  operation: "process",
1612
+ status,
1587
1613
  error: `processor process failed with Onda execution status ${String(status)}`,
1588
1614
  });
1589
1615
  return true;