@onda-lang/webaudio 0.8.19 → 0.8.21
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 +12 -0
- package/api.md +12 -1
- package/package.json +2 -2
- package/src/index.d.ts +8 -0
- package/src/index.js +7 -1
- package/src/param-metadata.js +116 -0
- package/src/worklet.js +35 -8
package/README.md
CHANGED
|
@@ -50,6 +50,18 @@ adapter clamps ranged scalar values and snaps stepped domains before posting the
|
|
|
50
50
|
worklet only writes the resulting canonical value in the declared scalar representation. Unknown
|
|
51
51
|
initial parameter names or indices are rejected before the worklet node is constructed.
|
|
52
52
|
|
|
53
|
+
The optional host setting `paramSmoothingSeconds` defaults to `0`. A positive value applies finite
|
|
54
|
+
linear ramps to continuous float parameters and array elements. Values advance once per logical
|
|
55
|
+
block, independently of Web Audio callback sizes; durations at most one block are bypassed.
|
|
56
|
+
Integer, boolean, and stepped controls update directly. Initial parameters and reinitialization
|
|
57
|
+
settle immediately. A new target restarts from the published value, while repeated writes of the
|
|
58
|
+
same target preserve its deadline. Host ramps are excluded from processor snapshots. The browser
|
|
59
|
+
playground uses `0.03` seconds (30 ms); explicit DSP smoothing can be used for interpolation within
|
|
60
|
+
an audio block.
|
|
61
|
+
|
|
62
|
+
`resetParams()` restores all declared defaults immediately and cancels host ramps without resetting
|
|
63
|
+
DSP state.
|
|
64
|
+
|
|
53
65
|
`setParamNormalized()` accepts a host value in `[0, 1]`. The adapter uses the artifact descriptor's
|
|
54
66
|
linear or logarithmic scale and step metadata to convert it to a plain value before posting the
|
|
55
67
|
write to the worklet. Boolean normalized values use the `0.5` threshold. For example:
|
package/api.md
CHANGED
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
const compiler = await createCompiler();
|
|
31
31
|
const { artifact } = await compiler.compileSource(source, {
|
|
32
32
|
sampleRate: audioContext.sampleRate,
|
|
33
|
-
blockSize:
|
|
33
|
+
blockSize: 512,
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
const print = ({ text }) => console.debug(text);
|
|
@@ -78,6 +78,7 @@ The returned instance provides:
|
|
|
78
78
|
- `dispose()`, an idempotent terminal release of compiler and worker resources.
|
|
79
79
|
|
|
80
80
|
Compile options accept `sampleRate`, `blockSize`, typed compile-constant overrides, and `codegen`.
|
|
81
|
+
The sample rate defaults to 48000 Hz and the block size to 512 frames.
|
|
81
82
|
Code generation can select optimization level `0..4`, shrink level `0..2`, strict or fast math,
|
|
82
83
|
SIMD, loop-containing inlining, and optional WAT emission. A successful compilation returns an
|
|
83
84
|
`OndaCompilationResult` containing the artifact, resolved source paths, and the exact source graph
|
|
@@ -206,12 +207,22 @@ the artifact metadata as the adapter constructor's second argument and
|
|
|
206
207
|
`processorOptions.executionOutputRing` as its fourth argument. Event encoding and numeric
|
|
207
208
|
unknown-index classification require that metadata.
|
|
208
209
|
|
|
210
|
+
`paramSmoothingSeconds` is an optional **host** setting on `OndaAudioProcessorOptions`, defaulting
|
|
211
|
+
to `0`. It applies finite linear ramps to continuous floating-point parameters and array elements;
|
|
212
|
+
integer, boolean, and stepped parameters update directly. Ramps publish once per logical compile
|
|
213
|
+
block, independent of Web Audio callback sizes. Durations round up to host samples and are bypassed
|
|
214
|
+
when they fit within one block. A changed target starts a fresh ramp from the published value;
|
|
215
|
+
repeated writes of the same target keep its deadline. Initial values and reinitialization settle
|
|
216
|
+
immediately, and processor snapshots do not contain the host ramps. The browser playground opts
|
|
217
|
+
into `0.03` seconds (30 ms). These block-level updates do not interpolate inside `sample:`.
|
|
218
|
+
|
|
209
219
|
### `OndaAudioProcessor`
|
|
210
220
|
|
|
211
221
|
The adapter exposes its `node` and validated `metadata`, plus these operations:
|
|
212
222
|
|
|
213
223
|
- `setParam(nameOrIndex, plain)` and `setParamNormalized(nameOrIndex, normalized)` for whole
|
|
214
224
|
parameters.
|
|
225
|
+
- `resetParams()` to restore parameter defaults immediately and cancel host ramps, retaining DSP state.
|
|
215
226
|
- `setParamElement(nameOrIndex, element, plain)` and
|
|
216
227
|
`setParamElementNormalized(nameOrIndex, element, normalized)` for one scalar or fixed-array
|
|
217
228
|
element.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onda-lang/webaudio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Optional Web Audio adapter for Onda processor WebAssembly artifacts",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"prepack": "node ../../scripts/stage-web-api-docs.mjs"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@onda-lang/processor-abi": "0.8.
|
|
37
|
+
"@onda-lang/processor-abi": "0.8.21"
|
|
38
38
|
},
|
|
39
39
|
"publishConfig": {
|
|
40
40
|
"access": "public"
|
package/src/index.d.ts
CHANGED
|
@@ -52,6 +52,12 @@ export interface OndaAudioProcessorOptions {
|
|
|
52
52
|
workletUrl?: string | URL;
|
|
53
53
|
/** Initial plain Onda parameter values, keyed or ordered by descriptor parameter. */
|
|
54
54
|
params?: Record<string, unknown> | unknown[];
|
|
55
|
+
/**
|
|
56
|
+
* Host-owned linear ramp duration in seconds for continuous float parameters.
|
|
57
|
+
* Defaults to 0. Ramps advance once per logical block and are bypassed when
|
|
58
|
+
* the duration is at most one block. Initial values and reinitialization settle immediately.
|
|
59
|
+
*/
|
|
60
|
+
paramSmoothingSeconds?: number;
|
|
55
61
|
/**
|
|
56
62
|
* Initial external-buffer bindings keyed by physical name, grouped by logical array name, or
|
|
57
63
|
* ordered by physical descriptor slot. Missing and null slots use neutral one-frame storage.
|
|
@@ -126,6 +132,8 @@ export class OndaAudioProcessor {
|
|
|
126
132
|
request(type: string, fields?: Record<string, unknown>, transfer?: Transferable[]): Promise<any>;
|
|
127
133
|
/** Set a plain Onda value; ranged scalar values are clamped and snapped. */
|
|
128
134
|
setParam(param: string | number, value: unknown): Promise<any>;
|
|
135
|
+
/** Restore all parameter defaults immediately, cancelling host ramps and retaining DSP state. */
|
|
136
|
+
resetParams(): Promise<any>;
|
|
129
137
|
/** Set one scalar or fixed-array parameter element without modifying its siblings. */
|
|
130
138
|
setParamElement(param: string | number, element: number, value: unknown): Promise<any>;
|
|
131
139
|
/** Map a host value in [0, 1] through the descriptor and set the resulting plain value. */
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { paramAddress, paramElementAddress } from "./param-metadata.js";
|
|
1
|
+
import { paramAddress, paramElementAddress, paramSmoothingSamples } from "./param-metadata.js";
|
|
2
2
|
import {
|
|
3
3
|
PayloadPlan,
|
|
4
4
|
createParamControl,
|
|
@@ -86,6 +86,7 @@ function audioWorkletNodeOptionsFromValidated(
|
|
|
86
86
|
if (options.onPrint !== undefined && typeof options.onPrint !== "function") {
|
|
87
87
|
throw new TypeError("initial print listener must be a function");
|
|
88
88
|
}
|
|
89
|
+
paramSmoothingSamples(options.paramSmoothingSeconds ?? 0, metadata.compile.sample_rate, metadata.compile.block_size);
|
|
89
90
|
const inputChannels = flattenedAudioChannelCount(metadata.metadata.inputs);
|
|
90
91
|
const outputChannels = flattenedAudioChannelCount(metadata.metadata.outputs);
|
|
91
92
|
if (inputChannels > 32 || outputChannels > 32) {
|
|
@@ -133,6 +134,7 @@ function audioWorkletNodeOptionsFromValidated(
|
|
|
133
134
|
? { wasmBytes: wasm }
|
|
134
135
|
: { wasmModule: options.compiledModule }),
|
|
135
136
|
metadata,
|
|
137
|
+
paramSmoothingSeconds: options.paramSmoothingSeconds ?? 0,
|
|
136
138
|
params: constrainInitialParamValues(
|
|
137
139
|
metadata.metadata.params,
|
|
138
140
|
options.params ?? {},
|
|
@@ -658,6 +660,10 @@ export class OndaAudioProcessor {
|
|
|
658
660
|
}
|
|
659
661
|
}
|
|
660
662
|
|
|
663
|
+
resetParams() {
|
|
664
|
+
return this.request("reset-params");
|
|
665
|
+
}
|
|
666
|
+
|
|
661
667
|
setParamElement(param, element, value) {
|
|
662
668
|
try {
|
|
663
669
|
if (!Array.isArray(this.paramInfo)) {
|
package/src/param-metadata.js
CHANGED
|
@@ -31,3 +31,119 @@ export function paramElementAddress(params, selector, element) {
|
|
|
31
31
|
}
|
|
32
32
|
return { info: address.info, element };
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
export function paramSmoothingSamples(seconds, sampleRate, blockSize) {
|
|
36
|
+
const samples = Math.ceil(seconds * sampleRate);
|
|
37
|
+
if (!Number.isFinite(seconds) || seconds < 0 || !Number.isSafeInteger(samples)) {
|
|
38
|
+
throw new Error("paramSmoothingSeconds must be finite, non-negative, and fit the host sample counter");
|
|
39
|
+
}
|
|
40
|
+
return samples > blockSize ? samples : 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Private host state: descriptors and processor snapshots contain no ramps.
|
|
44
|
+
export class ParamSmoothing {
|
|
45
|
+
constructor(params, samples, view, paramsPtr) {
|
|
46
|
+
this.samples = samples;
|
|
47
|
+
this.inverseSamples = 1 / Math.max(samples, 1);
|
|
48
|
+
this.started = false;
|
|
49
|
+
this.addresses = new Map();
|
|
50
|
+
this.entries = [];
|
|
51
|
+
if (samples > 0) {
|
|
52
|
+
for (const param of params) {
|
|
53
|
+
if ((param.scalar !== "f32" && param.scalar !== "f64")
|
|
54
|
+
|| param.param_control?.step_count != null) continue;
|
|
55
|
+
const elements = [];
|
|
56
|
+
const f32 = param.scalar === "f32";
|
|
57
|
+
for (let element = 0; element < param.array_len; element += 1) {
|
|
58
|
+
const offset = Number(param.byte_offset) + element * (f32 ? 4 : 8);
|
|
59
|
+
const value = f32 ? view.getFloat32(paramsPtr + offset, true)
|
|
60
|
+
: view.getFloat64(paramsPtr + offset, true);
|
|
61
|
+
elements.push(this.entries.length);
|
|
62
|
+
this.entries.push({ offset, f32, current: value, start: value,
|
|
63
|
+
target: value, elapsed: samples, active: false });
|
|
64
|
+
}
|
|
65
|
+
this.addresses.set(param, elements);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
this.active = new Uint32Array(this.entries.length);
|
|
69
|
+
this.activeCount = 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
isEnabledFor(param) {
|
|
73
|
+
return this.addresses.has(param);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
setTarget(param, element, value, initialized) {
|
|
77
|
+
const id = this.addresses.get(param)?.[element];
|
|
78
|
+
if (id === undefined) return false;
|
|
79
|
+
const entry = this.entries[id];
|
|
80
|
+
const target = entry.f32 ? Math.fround(Number(value)) : Number(value);
|
|
81
|
+
if (!initialized || !this.started || !Number.isFinite(target) || !Number.isFinite(entry.current)) {
|
|
82
|
+
entry.current = target;
|
|
83
|
+
entry.target = target;
|
|
84
|
+
entry.elapsed = this.samples;
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
if (target === entry.target) return true;
|
|
88
|
+
entry.start = entry.current;
|
|
89
|
+
entry.target = target;
|
|
90
|
+
entry.elapsed = target === entry.current ? this.samples : 0;
|
|
91
|
+
if (!entry.active) {
|
|
92
|
+
entry.active = true;
|
|
93
|
+
this.active[this.activeCount++] = id;
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
beginBlock(frames, view, paramsPtr) {
|
|
99
|
+
this.started = true;
|
|
100
|
+
let cursor = 0;
|
|
101
|
+
while (cursor < this.activeCount) {
|
|
102
|
+
const entry = this.entries[this.active[cursor]];
|
|
103
|
+
entry.elapsed += Math.min(frames, this.samples - entry.elapsed);
|
|
104
|
+
let value = entry.target;
|
|
105
|
+
if (entry.elapsed !== this.samples) {
|
|
106
|
+
const weight = entry.elapsed * this.inverseSamples;
|
|
107
|
+
if ((entry.start < 0) !== (entry.target < 0)) {
|
|
108
|
+
value = entry.start * (1 - weight) + entry.target * weight;
|
|
109
|
+
} else if (weight <= 0.5) {
|
|
110
|
+
value = entry.start + (entry.target - entry.start) * weight;
|
|
111
|
+
} else {
|
|
112
|
+
value = entry.target + (entry.start - entry.target) * (1 - weight);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
this.write(entry, value, view, paramsPtr);
|
|
116
|
+
if (entry.elapsed === this.samples) {
|
|
117
|
+
entry.active = false;
|
|
118
|
+
this.active[cursor] = this.active[--this.activeCount];
|
|
119
|
+
} else {
|
|
120
|
+
cursor += 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
settle(view, paramsPtr) {
|
|
126
|
+
this.activeCount = 0;
|
|
127
|
+
this.started = false;
|
|
128
|
+
for (const entry of this.entries) {
|
|
129
|
+
this.write(entry, entry.target, view, paramsPtr);
|
|
130
|
+
entry.start = entry.target;
|
|
131
|
+
entry.elapsed = this.samples;
|
|
132
|
+
entry.active = false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
reset(view, paramsPtr) {
|
|
137
|
+
for (const entry of this.entries) {
|
|
138
|
+
entry.target = entry.f32 ? view.getFloat32(paramsPtr + entry.offset, true)
|
|
139
|
+
: view.getFloat64(paramsPtr + entry.offset, true);
|
|
140
|
+
}
|
|
141
|
+
this.settle(view, paramsPtr);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
write(entry, value, view, paramsPtr) {
|
|
145
|
+
entry.current = entry.f32 ? Math.fround(value) : value;
|
|
146
|
+
if (entry.f32) view.setFloat32(paramsPtr + entry.offset, entry.current, true);
|
|
147
|
+
else view.setFloat64(paramsPtr + entry.offset, entry.current, true);
|
|
148
|
+
}
|
|
149
|
+
}
|
package/src/worklet.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { paramAddress, paramElementAddress } from "./param-metadata.js";
|
|
1
|
+
import { paramAddress, paramElementAddress, paramSmoothingSamples, ParamSmoothing } from "./param-metadata.js";
|
|
2
2
|
import {
|
|
3
3
|
EXECUTION_OPERATION_EVENT,
|
|
4
4
|
EXECUTION_OPERATION_INIT,
|
|
@@ -144,7 +144,7 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
|
|
|
144
144
|
"the Onda Web Audio processor requires at least one audio input or output",
|
|
145
145
|
);
|
|
146
146
|
}
|
|
147
|
-
this.blockSize = Number(metadata.compile?.block_size ??
|
|
147
|
+
this.blockSize = Number(metadata.compile?.block_size ?? 512);
|
|
148
148
|
this.compileSampleRate = Number(metadata.compile?.sample_rate ?? sampleRate);
|
|
149
149
|
if (!Number.isInteger(this.blockSize) || this.blockSize <= 0) {
|
|
150
150
|
throw new Error(`invalid compile-time block size: ${this.blockSize}`);
|
|
@@ -292,6 +292,12 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
|
|
|
292
292
|
};
|
|
293
293
|
this.writeParamDefaults();
|
|
294
294
|
this.writeInitialParams(processorOptions.params ?? {});
|
|
295
|
+
this.paramSmoothing = new ParamSmoothing(
|
|
296
|
+
this.paramInfo,
|
|
297
|
+
paramSmoothingSamples(processorOptions.paramSmoothingSeconds ?? 0, this.compileSampleRate, this.blockSize),
|
|
298
|
+
this.memoryView(),
|
|
299
|
+
this.paramsPtr,
|
|
300
|
+
);
|
|
295
301
|
this.ensureInputCapacity(this.blockSize);
|
|
296
302
|
this.ensureOutputCapacity(this.blockSize);
|
|
297
303
|
this.viewsReady = true;
|
|
@@ -517,14 +523,26 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
|
|
|
517
523
|
throw new Error(`Onda parameter '${param.name}' has invalid storage metadata`);
|
|
518
524
|
}
|
|
519
525
|
if (element !== null) {
|
|
520
|
-
this.
|
|
526
|
+
this.setParamScalar(param, element, value);
|
|
521
527
|
return;
|
|
522
528
|
}
|
|
523
|
-
this.
|
|
524
|
-
this.paramsPtr + offset,
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
);
|
|
529
|
+
if (!this.paramSmoothing?.isEnabledFor(param)) {
|
|
530
|
+
this.writeStorage(this.paramsPtr + offset, param, value);
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
this.validateStorageValue(param, value);
|
|
534
|
+
if (this.isFixedArray(param)) {
|
|
535
|
+
for (let index = 0; index < length; index += 1) {
|
|
536
|
+
this.setParamScalar(param, index, value[index]);
|
|
537
|
+
}
|
|
538
|
+
} else {
|
|
539
|
+
this.setParamScalar(param, 0, value);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
setParamScalar(param, element, value) {
|
|
544
|
+
if (this.paramSmoothing?.setTarget(param, element, value, this.initialized)) return;
|
|
545
|
+
this.writeScalar(this.paramsPtr + Number(param.byte_offset) + element * this.scalarByteSize(param.scalar), param.scalar, value);
|
|
528
546
|
}
|
|
529
547
|
|
|
530
548
|
init(mode) {
|
|
@@ -560,6 +578,7 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
|
|
|
560
578
|
this.invalidateState();
|
|
561
579
|
this.refreshMemoryCache();
|
|
562
580
|
if (collectOutput) this.prepareExecutionOutput();
|
|
581
|
+
this.paramSmoothing.settle(this.memoryView(), this.paramsPtr);
|
|
563
582
|
const status = this.exports.onda_processor_init(
|
|
564
583
|
this.paramsPtr,
|
|
565
584
|
this.statePtr,
|
|
@@ -729,6 +748,10 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
|
|
|
729
748
|
message.element ?? null,
|
|
730
749
|
);
|
|
731
750
|
this.postResponse(message, { type: "onda-ok", operation: message.type });
|
|
751
|
+
} else if (message.type === "reset-params") {
|
|
752
|
+
this.writeParamDefaults();
|
|
753
|
+
this.paramSmoothing.reset(this.memoryView(), this.paramsPtr);
|
|
754
|
+
this.postResponse(message, { type: "onda-ok", operation: message.type });
|
|
732
755
|
} else if (message.type === "init") {
|
|
733
756
|
this.init(message.mode);
|
|
734
757
|
this.postResponse(message, { type: "onda-ok", operation: message.type });
|
|
@@ -1587,6 +1610,10 @@ class OndaWasmProcessor extends AudioWorkletProcessor {
|
|
|
1587
1610
|
const flags = (startFrame === 0 ? PROCESSOR_BEGIN_BLOCK : 0)
|
|
1588
1611
|
| (endsBlock ? PROCESSOR_END_BLOCK : 0);
|
|
1589
1612
|
|
|
1613
|
+
if (startFrame === 0) {
|
|
1614
|
+
this.paramSmoothing.beginBlock(this.blockSize, this.dataViewCache, this.paramsPtr);
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1590
1617
|
this.marshalInputSegment(
|
|
1591
1618
|
inputs,
|
|
1592
1619
|
frames,
|