@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 +44 -0
- package/package.json +2 -2
- package/src/index.d.ts +23 -3
- package/src/index.js +95 -4
- package/src/worklet.js +49 -11
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.
|
|
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.
|
|
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 {
|
|
4
|
-
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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,
|