@onda-lang/webaudio 0.5.0-rc.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Francesco Cameli
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # Onda Web Audio adapter
2
+
3
+ This optional package hosts a complete wasm32 Onda processor artifact in an `AudioWorklet`. It is a
4
+ reference adapter for the generic Onda processor ABI; Web Audio is not required by the compiler or
5
+ by native and relocatable-WebAssembly object consumers.
6
+
7
+ ```js
8
+ import { createOndaAudioProcessor } from "@onda-lang/webaudio";
9
+
10
+ const processor = await createOndaAudioProcessor(audioContext, artifact, {
11
+ params: { gain: 0.5 },
12
+ buffers: {},
13
+ });
14
+ processor.node.connect(audioContext.destination);
15
+ await processor.setParam("gain", 0.75);
16
+ const snapshot = await processor.snapshot();
17
+ ```
18
+
19
+ The adapter registers `onda-wasm-processor`, derives Web Audio channel options from artifact
20
+ metadata, marshals declared scalar widths, schedules arbitrary render quanta across Onda compile
21
+ blocks, and provides request/response helpers for parameters, events, buffers, control outputs,
22
+ reset, and portable snapshots.
23
+
24
+ The artifact must be compiled for exactly `audioContext.sampleRate`; the adapter rejects a mismatch
25
+ before registering the node so sample-rate-derived language semantics cannot silently drift. A Web
26
+ Audio processor must expose at least one audio input or output, because an empty callback surface
27
+ does not carry a render-quantum frame count. Control-only artifacts remain usable through the generic
28
+ processor ABI in a non-Web-Audio host.
29
+
30
+ ## Real-time behavior
31
+
32
+ `createOndaAudioProcessor` compiles the processor's `WebAssembly.Module` concurrently with worklet
33
+ registration, before constructing the `AudioWorkletNode`. The module is structured-cloned into the
34
+ worklet, so processor construction only instantiates it. Applications creating several instances can
35
+ compile once and reuse the module:
36
+
37
+ ```js
38
+ import {
39
+ compileOndaProcessorModule,
40
+ createOndaAudioProcessor,
41
+ } from "@onda-lang/webaudio";
42
+
43
+ const compiledModule = await compileOndaProcessorModule(artifact);
44
+ const left = await createOndaAudioProcessor(context, artifact, { compiledModule });
45
+ const right = await createOndaAudioProcessor(context, artifact, { compiledModule });
46
+ ```
47
+
48
+ After construction, the normal f32 render callback reuses cached Wasm-memory views and performs no
49
+ host-side allocation or memory growth. Full-block f32 inputs and outputs use typed-array bulk copies;
50
+ segmented callbacks and other ABI scalar widths use preallocated typed views with conversion loops
51
+ (i64 input conversion necessarily creates JavaScript `BigInt` values). External buffers are copied
52
+ into Wasm with typed-array bulk operations during construction.
53
+
54
+ Artifact descriptors and module exports are validated by the shared, compiler-free
55
+ `@onda-lang/processor-abi` package before anything reaches the rendering thread.
56
+
57
+ Dynamic event storage is also allocated before rendering. Its default capacity is 64 KiB per
58
+ processor with dynamic events and can be changed explicitly:
59
+
60
+ ```js
61
+ const processor = await createOndaAudioProcessor(context, artifact, {
62
+ eventPayloadCapacityBytes: 256 * 1024,
63
+ });
64
+ ```
65
+
66
+ An event exceeding the configured capacity is rejected instead of growing memory on the rendering
67
+ thread. Parameters and ordinary events are lightweight control operations. Snapshot creation,
68
+ snapshot restore, control-output reads, and especially complete external-buffer reads necessarily
69
+ copy data; suspend or disconnect real-time playback before requesting large transfers.
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@onda-lang/webaudio",
3
+ "version": "0.5.0-rc.0",
4
+ "type": "module",
5
+ "description": "Optional Web Audio adapter for Onda processor WebAssembly artifacts",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/onda-lang/onda.git",
10
+ "directory": "packages/onda_webaudio"
11
+ },
12
+ "homepage": "https://onda-lang.github.io/onda/",
13
+ "bugs": {
14
+ "url": "https://github.com/onda-lang/onda/issues"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "types": "./src/index.d.ts",
19
+ "default": "./src/index.js"
20
+ },
21
+ "./worklet": {
22
+ "types": "./src/worklet.d.ts",
23
+ "default": "./src/worklet.js"
24
+ }
25
+ },
26
+ "files": [
27
+ "src",
28
+ "LICENSE",
29
+ "README.md"
30
+ ],
31
+ "scripts": {
32
+ "test": "node --test"
33
+ },
34
+ "dependencies": {
35
+ "@onda-lang/processor-abi": "0.5.0-rc.0"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ }
40
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ export const ONDA_AUDIO_WORKLET_PROCESSOR_NAME: "onda-wasm-processor";
2
+
3
+ export type { OndaProcessorArtifact, OndaProcessorMetadata } from "@onda-lang/processor-abi";
4
+ import type { OndaProcessorArtifact } from "@onda-lang/processor-abi";
5
+
6
+ export interface OndaAudioProcessorOptions {
7
+ workletUrl?: string | URL;
8
+ params?: Record<string, unknown> | unknown[];
9
+ buffers?: Record<string, unknown> | unknown[];
10
+ /** Preallocated capacity for dynamic event payloads. Defaults to 64 KiB. */
11
+ eventPayloadCapacityBytes?: number;
12
+ /** Reusable module compiled outside the audio rendering thread. */
13
+ compiledModule?: WebAssembly.Module;
14
+ nodeOptions?: AudioWorkletNodeOptions;
15
+ AudioWorkletNode?: typeof AudioWorkletNode;
16
+ }
17
+
18
+ export function flattenedAudioChannelCount(ports?: unknown[]): number;
19
+ export function ondaAudioWorkletNodeOptions(
20
+ artifact: OndaProcessorArtifact,
21
+ options?: OndaAudioProcessorOptions,
22
+ ): AudioWorkletNodeOptions;
23
+ export function registerOndaAudioWorklet(
24
+ context: BaseAudioContext,
25
+ workletUrl?: string | URL,
26
+ ): Promise<void>;
27
+ export function createOndaAudioProcessor(
28
+ context: BaseAudioContext,
29
+ artifact: OndaProcessorArtifact,
30
+ options?: OndaAudioProcessorOptions,
31
+ ): Promise<OndaAudioProcessor>;
32
+ export function compileOndaProcessorModule(
33
+ artifact: OndaProcessorArtifact,
34
+ ): Promise<WebAssembly.Module>;
35
+
36
+ export class OndaAudioProcessor {
37
+ constructor(node: AudioWorkletNode);
38
+ readonly node: AudioWorkletNode;
39
+ request(type: string, fields?: Record<string, unknown>, transfer?: Transferable[]): Promise<any>;
40
+ setParam(param: string | number, value: unknown): Promise<any>;
41
+ trigger(event: string | number, values?: Record<string, unknown> | unknown[]): Promise<any>;
42
+ reset(): Promise<any>;
43
+ snapshot(): Promise<Uint8Array>;
44
+ restoreSnapshot(snapshot: Uint8Array | ArrayBuffer): Promise<any>;
45
+ readControlOutputs(): Promise<Record<string, unknown>>;
46
+ readBuffer(buffer: string | number): Promise<any>;
47
+ close(reason?: Error): void;
48
+ }
package/src/index.js ADDED
@@ -0,0 +1,241 @@
1
+ import {
2
+ validateProcessorArtifact,
3
+ validateProcessorModule,
4
+ } from "@onda-lang/processor-abi";
5
+
6
+ export const ONDA_AUDIO_WORKLET_PROCESSOR_NAME = "onda-wasm-processor";
7
+
8
+ const registrationByContext = new WeakMap();
9
+
10
+ export function flattenedAudioChannelCount(ports = []) {
11
+ if (!Array.isArray(ports)) {
12
+ throw new Error("processor audio ports must be an array");
13
+ }
14
+ return ports.reduce((count, port, index) => {
15
+ const channels = Number(port?.array_len);
16
+ if (!Number.isSafeInteger(channels) || channels <= 0) {
17
+ throw new Error(
18
+ `processor audio port ${index} has invalid array_len '${String(port?.array_len)}'`,
19
+ );
20
+ }
21
+ return count + channels;
22
+ }, 0);
23
+ }
24
+
25
+ export function ondaAudioWorkletNodeOptions(artifact, options = {}) {
26
+ const validated = validateExecutableArtifact(
27
+ artifact,
28
+ options.compiledModule === undefined,
29
+ );
30
+ return audioWorkletNodeOptionsFromValidated(
31
+ validated,
32
+ options,
33
+ options.compiledModule !== undefined,
34
+ );
35
+ }
36
+
37
+ function audioWorkletNodeOptionsFromValidated(
38
+ { wasm, metadata },
39
+ options,
40
+ validateCompiledModule,
41
+ ) {
42
+ const inputChannels = flattenedAudioChannelCount(metadata.metadata.inputs);
43
+ const outputChannels = flattenedAudioChannelCount(metadata.metadata.outputs);
44
+ if (inputChannels > 32 || outputChannels > 32) {
45
+ throw new Error("Web Audio supports at most 32 flattened channels per Onda node");
46
+ }
47
+ if (inputChannels === 0 && outputChannels === 0) {
48
+ throw new Error(
49
+ "the Web Audio adapter requires at least one audio input or output to establish the render quantum",
50
+ );
51
+ }
52
+ if (validateCompiledModule) {
53
+ validateProcessorModule(options.compiledModule, metadata);
54
+ }
55
+ const nodeOptions = {
56
+ ...options.nodeOptions,
57
+ numberOfInputs: inputChannels ? 1 : 0,
58
+ numberOfOutputs: outputChannels ? 1 : 0,
59
+ channelCount: Math.max(inputChannels, 1),
60
+ channelCountMode: "explicit",
61
+ channelInterpretation: "discrete",
62
+ processorOptions: {
63
+ ...options.nodeOptions?.processorOptions,
64
+ ...(options.compiledModule === undefined
65
+ ? { wasmBytes: wasm }
66
+ : { wasmModule: options.compiledModule }),
67
+ metadata,
68
+ params: options.params ?? {},
69
+ buffers: options.buffers ?? {},
70
+ eventPayloadCapacityBytes: options.eventPayloadCapacityBytes,
71
+ },
72
+ };
73
+ if (outputChannels) {
74
+ nodeOptions.outputChannelCount = [outputChannels];
75
+ } else {
76
+ delete nodeOptions.outputChannelCount;
77
+ }
78
+ return nodeOptions;
79
+ }
80
+
81
+ export async function registerOndaAudioWorklet(
82
+ context,
83
+ workletUrl = new URL("./worklet.js", import.meta.url),
84
+ ) {
85
+ if (!context?.audioWorklet?.addModule) {
86
+ throw new Error("an AudioContext with AudioWorklet support is required");
87
+ }
88
+ let registration = registrationByContext.get(context);
89
+ if (!registration) {
90
+ registration = context.audioWorklet.addModule(workletUrl);
91
+ registrationByContext.set(context, registration);
92
+ }
93
+ try {
94
+ await registration;
95
+ } catch (error) {
96
+ registrationByContext.delete(context);
97
+ throw error;
98
+ }
99
+ }
100
+
101
+ export async function createOndaAudioProcessor(context, artifact, options = {}) {
102
+ const validated = validateExecutableArtifact(artifact, false);
103
+ validateContextSampleRate(context, validated.metadata);
104
+ if (options.compiledModule !== undefined) {
105
+ validateProcessorModule(options.compiledModule, validated.metadata);
106
+ }
107
+ const [, compiledModule] = await Promise.all([
108
+ registerOndaAudioWorklet(context, options.workletUrl),
109
+ options.compiledModule === undefined
110
+ ? compileValidatedProcessorModule(validated)
111
+ : Promise.resolve(options.compiledModule),
112
+ ]);
113
+ const NodeConstructor = options.AudioWorkletNode ?? globalThis.AudioWorkletNode;
114
+ if (typeof NodeConstructor !== "function") {
115
+ throw new Error("AudioWorkletNode is not available in this environment");
116
+ }
117
+ const node = new NodeConstructor(
118
+ context,
119
+ ONDA_AUDIO_WORKLET_PROCESSOR_NAME,
120
+ audioWorkletNodeOptionsFromValidated(
121
+ validated,
122
+ { ...options, compiledModule },
123
+ false,
124
+ ),
125
+ );
126
+ return new OndaAudioProcessor(node);
127
+ }
128
+
129
+ export async function compileOndaProcessorModule(artifact) {
130
+ return compileValidatedProcessorModule(
131
+ validateExecutableArtifact(artifact, false),
132
+ );
133
+ }
134
+
135
+ async function compileValidatedProcessorModule({ wasm, metadata }) {
136
+ const module = await WebAssembly.compile(wasm);
137
+ validateProcessorModule(module, metadata);
138
+ return module;
139
+ }
140
+
141
+ export class OndaAudioProcessor {
142
+ constructor(node) {
143
+ this.node = node;
144
+ this.nextRequestId = 1;
145
+ this.pending = new Map();
146
+ this.handleMessage = (event) => {
147
+ const message = event.data ?? {};
148
+ if (message.requestId === undefined) return;
149
+ const pending = this.pending.get(message.requestId);
150
+ if (!pending) return;
151
+ this.pending.delete(message.requestId);
152
+ if (message.type === "onda-error") {
153
+ pending.reject(new Error(message.error));
154
+ } else {
155
+ pending.resolve(message);
156
+ }
157
+ };
158
+ node.port.addEventListener("message", this.handleMessage);
159
+ node.port.start?.();
160
+ }
161
+
162
+ request(type, fields = {}, transfer = []) {
163
+ const requestId = this.nextRequestId++;
164
+ return new Promise((resolve, reject) => {
165
+ this.pending.set(requestId, { resolve, reject });
166
+ try {
167
+ this.node.port.postMessage({ type, ...fields, requestId }, transfer);
168
+ } catch (error) {
169
+ this.pending.delete(requestId);
170
+ reject(error);
171
+ }
172
+ });
173
+ }
174
+
175
+ setParam(param, value) {
176
+ return this.request("set-param", { param, value });
177
+ }
178
+
179
+ trigger(event, values = {}) {
180
+ return this.request("event", { event, values });
181
+ }
182
+
183
+ reset() {
184
+ return this.request("reset");
185
+ }
186
+
187
+ async snapshot() {
188
+ return (await this.request("snapshot")).bytes;
189
+ }
190
+
191
+ restoreSnapshot(snapshot) {
192
+ const bytes = snapshot instanceof Uint8Array
193
+ ? snapshot.slice()
194
+ : new Uint8Array(snapshot.slice(0));
195
+ return this.request("restore-snapshot", { snapshot: bytes }, [bytes.buffer]);
196
+ }
197
+
198
+ async readControlOutputs() {
199
+ return (await this.request("read-control-outputs")).values;
200
+ }
201
+
202
+ async readBuffer(buffer) {
203
+ return this.request("read-buffer", { buffer });
204
+ }
205
+
206
+ close(reason = new Error("Onda AudioWorklet processor closed")) {
207
+ this.node.port.removeEventListener("message", this.handleMessage);
208
+ for (const pending of this.pending.values()) pending.reject(reason);
209
+ this.pending.clear();
210
+ }
211
+ }
212
+
213
+ function validateExecutableArtifact(artifact, inspectModule = true) {
214
+ const { wasm, metadata } = validateProcessorArtifact(artifact, { inspectModule });
215
+ if (
216
+ metadata.integration?.profile?.kind !== "core_webassembly_module"
217
+ || metadata?.target?.pointer_model !== "linear_memory_offset"
218
+ || metadata?.target?.pointer_width_bits !== 32
219
+ ) {
220
+ throw new Error("the Web Audio adapter requires an Onda wasm32 module artifact");
221
+ }
222
+ for (const field of ["inputs", "outputs"]) {
223
+ if (!Array.isArray(metadata.metadata?.[field])) {
224
+ throw new Error(`processor metadata is missing '${field}'`);
225
+ }
226
+ }
227
+ return { wasm, metadata };
228
+ }
229
+
230
+ function validateContextSampleRate(context, metadata) {
231
+ const actual = Number(context?.sampleRate);
232
+ const compiled = Number(metadata.compile.sample_rate);
233
+ if (!Number.isFinite(actual) || actual <= 0) {
234
+ throw new Error("an AudioContext with a valid sampleRate is required");
235
+ }
236
+ if (actual !== compiled) {
237
+ throw new Error(
238
+ `processor was compiled for ${compiled} Hz but the AudioContext runs at ${actual} Hz; recompile for the actual context sample rate`,
239
+ );
240
+ }
241
+ }
@@ -0,0 +1 @@
1
+ export {};