@onda-lang/webaudio 0.8.5 → 0.8.7

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
 
@@ -135,14 +137,17 @@ endpoints, apply linear, logarithmic, or curved scale, and clamp and snap to the
135
137
 
136
138
  Complete wasm32 processor exports receive an optional execution-output descriptor containing
137
139
  independently nullable delegate and print batches. Allocate all descriptors and storage before
138
- real-time execution.
140
+ real-time execution. Their linear-memory regions must be mutually disjoint and must not overlap any
141
+ other region accessed by the processor call, matching the processor ABI's execution-output
142
+ contract.
139
143
 
140
144
  `writeDelegateBatch` and `writePrintBatch` initialize reusable batch descriptors.
141
145
  `writeExecutionOutput` connects their addresses. Call `resetExecutionOutput` immediately before
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,
146
+ every generated init or process entry. Generated event entries reset before input preflight, so
147
+ rejected input returns empty batches. After a successful generated call,
144
148
  `readDelegateBatch` or `readPrintBatch` validates the result counters.
145
- `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`.
146
151
  `decodePrintRecords` preserves primitive types and source sites; `formatPrintRecords` and
147
152
  `formatPrintBatch` add canonical text formatting. Both decoded record types expose a `sequence` from
148
153
  the host-reset call-local counter; merge that call's two arrays by this field when presenting one
@@ -184,27 +189,36 @@ The package root re-exports the common artifact validation and file helpers. The
184
189
  - `compileOndaProcessorModule(artifact)` validates and compiles a reusable module off the render thread.
185
190
  - `ondaAudioWorkletNodeOptions(artifact, options?)` builds low-level node options.
186
191
  - `createOndaAudioProcessor(context, artifact, options?)` allocates an uninitialized adapter.
187
- - `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.
188
194
  - `flattenedAudioChannelCount(ports?)` totals declared physical channels with validation.
189
195
 
190
196
  `OndaAudioProcessorOptions` accepts initial plain parameter values, external buffers, event,
191
197
  delegate, and print capacities, an optional construction-time `onPrint` listener, a precompiled
192
198
  module, custom node options, and an `AudioWorkletNode` constructor. Pass `onPrint` when using the
193
- 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
194
200
  construction cannot replay output from an execution that has already completed. The artifact sample
195
201
  rate must equal the context sample rate and it must expose at least one audio input or output.
196
202
  Print and delegate delivery uses a bounded `SharedArrayBuffer` ring and requires cross-origin
197
203
  isolation in browsers; ordinary audio processing remains available when shared memory is absent.
198
204
  Low-level callers that manually pair the returned node options with `OndaAudioProcessor` must pass
199
- `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.
200
208
 
201
209
  ### `OndaAudioProcessor`
202
210
 
203
211
  The adapter exposes its `node` and validated `metadata`, plus these operations:
204
212
 
205
- - `setParam(nameOrIndex, plain)` and `setParamNormalized(nameOrIndex, normalized)`.
206
- - `trigger(nameOrIndex, values?)` for input events.
207
- - `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.
208
222
  - `init(mode)`, `snapshot()`, and `restoreSnapshot(bytes)`.
209
223
  - `readControlOutputs()` and `readBuffer(nameOrIndex)`.
210
224
  - `request(type, fields?, transfer?)` for adapter protocol extensions.
@@ -222,7 +236,24 @@ each init, event, or process segment.
222
236
 
223
237
  `ONDA_INIT_FULL` clears and initializes all physical state. `ONDA_INIT_PRESERVE_PINNED` retains
224
238
  pinned state according to the processor ABI. The package also re-exports the prepared and one-shot
225
- 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.
226
257
 
227
258
  `@onda-lang/webaudio/worklet` is the side-effect-only AudioWorklet registration module and has no
228
259
  named exports.
@@ -269,8 +300,11 @@ PRINT_RECORD_HEADER_SIZE_BYTES
269
300
  PROCESSOR_ABI_VERSION
270
301
  PROCESSOR_ARTIFACT_FORMAT
271
302
  PROCESSOR_ARTIFACT_FORMAT_VERSION
303
+ PROCESSOR_BEGIN_BLOCK
304
+ PROCESSOR_END_BLOCK
272
305
  PROCESSOR_EXECUTION_OK
273
306
  PROCESSOR_EXECUTION_RUNTIME_SAFETY_FAILURE
307
+ PROCESSOR_FULL_BLOCK
274
308
  PROCESSOR_INIT_FULL
275
309
  PROCESSOR_INIT_PRESERVE_PINNED
276
310
  PROCESSOR_SNAPSHOT_FORMAT_VERSION
@@ -279,6 +313,7 @@ createParamControl
279
313
  paramElementMetadata
280
314
  createParamDomain
281
315
  createProcessorArtifactFiles
316
+ decodeDelegateBatch
282
317
  decodeDelegateRecords
283
318
  decodePrintRecords
284
319
  formatPrintBatch
@@ -303,6 +338,7 @@ OndaPayloadSchema
303
338
  OndaPayloadDefault
304
339
  OndaPayloadValue
305
340
  OndaPayloadSizes
341
+ OndaPayloadTensorMetadata
306
342
  PayloadPlan
307
343
  canonicalF32Number
308
344
  EVENT_INPUT_SIZE_BYTES
@@ -317,12 +353,19 @@ OndaArtifactError
317
353
  OndaBinaryenError
318
354
  OndaBinaryenOptions
319
355
  OndaProcessorArtifact
356
+ OndaProcessorInitMode
320
357
  OndaProcessorMetadata
321
358
  PROCESSOR_ABI_VERSION
322
359
  PROCESSOR_ARTIFACT_FORMAT
323
360
  PROCESSOR_ARTIFACT_FORMAT_VERSION
361
+ PROCESSOR_BEGIN_BLOCK
362
+ PROCESSOR_END_BLOCK
363
+ PROCESSOR_EXECUTION_INPUT_REJECTED
324
364
  PROCESSOR_EXECUTION_OK
325
365
  PROCESSOR_EXECUTION_RUNTIME_SAFETY_FAILURE
366
+ PROCESSOR_FULL_BLOCK
367
+ PROCESSOR_INIT_FULL
368
+ PROCESSOR_INIT_PRESERVE_PINNED
326
369
  PROCESSOR_SNAPSHOT_FORMAT_VERSION
327
370
  SUPPORTED_MIR_SCHEMA_VERSION
328
371
  compileTrustedMir
@@ -363,6 +406,7 @@ OndaLspAnalysisOptions
363
406
  OndaLspMessage
364
407
  OndaMaterializedProjectFile
365
408
  OndaProcessorArtifact
409
+ OndaProcessorInitMode
366
410
  OndaProcessorMetadata
367
411
  OndaProjectBufferInfo
368
412
  OndaProjectCapabilities
@@ -380,8 +424,14 @@ OndaWorkerLike
380
424
  PROCESSOR_ABI_VERSION
381
425
  PROCESSOR_ARTIFACT_FORMAT
382
426
  PROCESSOR_ARTIFACT_FORMAT_VERSION
427
+ PROCESSOR_BEGIN_BLOCK
428
+ PROCESSOR_END_BLOCK
429
+ PROCESSOR_EXECUTION_INPUT_REJECTED
383
430
  PROCESSOR_EXECUTION_OK
384
431
  PROCESSOR_EXECUTION_RUNTIME_SAFETY_FAILURE
432
+ PROCESSOR_FULL_BLOCK
433
+ PROCESSOR_INIT_FULL
434
+ PROCESSOR_INIT_PRESERVE_PINNED
385
435
  PROCESSOR_SNAPSHOT_FORMAT_VERSION
386
436
  WorkerCompilerOptions
387
437
  createCompiler
@@ -405,11 +455,16 @@ OndaAudioProcessor
405
455
  OndaAudioProcessorOptions
406
456
  OndaAudioPrintBatch
407
457
  OndaAudioPrintListener
458
+ OndaExecutionError
459
+ OndaExecutionErrorListener
408
460
  OndaInitMode
409
461
  OndaParamDomain
410
462
  OndaPreparedParamControl
411
463
  OndaProcessorArtifact
412
464
  OndaProcessorMetadata
465
+ PROCESSOR_EXECUTION_INPUT_REJECTED
466
+ PROCESSOR_EXECUTION_OK
467
+ PROCESSOR_EXECUTION_RUNTIME_SAFETY_FAILURE
413
468
  compileOndaProcessorModule
414
469
  constrainParamPlain
415
470
  createOndaAudioProcessor
@@ -432,11 +487,21 @@ are rejected. Missing event arguments use declared constant defaults.
432
487
  Supply `i64` as `bigint`, an exact safe integer, or a decimal string. `decode(bytes)` returns named
433
488
  nested values with `bigint` for `i64`. Plans own immutable schema snapshots.
434
489
 
490
+ `tensorMetadata` exposes the schema's flattened SoA contract without reparsing JSON. Each immutable
491
+ record contains `path`, `encoding`, fixed `shape`, `parameterIndex`, `isSlice`, and
492
+ `fixedElementCount`. Tuple path components use `__0`, `__1`, and so on. A slice's runtime tensor
493
+ length is its logical length times `fixedElementCount`; the dynamic outer axis is therefore not
494
+ repeated in `shape`.
495
+
435
496
  `requiredWorkspace(bytes)` preflights packed little-endian input. `sizes(lengths)` reports wire
436
497
  and aligned workspace sizes without allocating payload storage. Low-level wasm hosts provision
437
498
  both regions and a 16-byte descriptor, then call `writeEventInput` before invoking the event.
438
499
  Rejected input returns `PROCESSOR_EXECUTION_INPUT_REJECTED` (2) and leaves processor state usable.
439
500
  `OndaAudioProcessor.trigger` performs logical encoding before transferring bytes to the worklet.
501
+ Web hosts intentionally have no unchecked tensor-view event entry: JavaScript values must cross the
502
+ main-thread/worklet and Wasm linear-memory boundaries, so native borrowed addresses cannot be
503
+ preserved end to end. Web event dispatch remains the packed, checked `onda_event_N` path and uses
504
+ its preallocated workspace.
440
505
  Decoded `f32` leaves use `canonicalF32Number(value)`: the returned JavaScript number has the
441
506
  shortest decimal representation that round-trips to the same `f32`, so JSON and controls do not
442
507
  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.5",
3
+ "version": "0.8.7",
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.5"
37
+ "@onda-lang/processor-abi": "0.8.7"
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;