@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/src/worklet.js ADDED
@@ -0,0 +1,1168 @@
1
+ const ONDA_PROCESS_BEGIN_BLOCK = 1 << 0;
2
+ const ONDA_PROCESS_END_BLOCK = 1 << 1;
3
+ const ONDA_AUDIO_WORKLET_PROCESSOR_NAME = "onda-wasm-processor";
4
+ const DEFAULT_EVENT_PAYLOAD_CAPACITY_BYTES = 64 * 1024;
5
+ const HOST_LITTLE_ENDIAN = new Uint8Array(new Uint16Array([1]).buffer)[0] === 1;
6
+
7
+ class OndaWasmProcessor extends AudioWorkletProcessor {
8
+ constructor(options) {
9
+ super();
10
+
11
+ const processorOptions = options.processorOptions ?? {};
12
+ const wasmBytes = processorOptions.wasmBytes;
13
+ const wasmModule = processorOptions.wasmModule;
14
+ const metadata = processorOptions.metadata ?? {};
15
+
16
+ if (
17
+ metadata.artifact_kind !== "webassembly_module"
18
+ || metadata.target?.pointer_model !== "linear_memory_offset"
19
+ || metadata.target?.pointer_width_bits !== 32
20
+ ) {
21
+ throw new Error("expected an Onda wasm32 executable-module artifact");
22
+ }
23
+
24
+ let module;
25
+ if (wasmModule !== undefined) {
26
+ if (!(wasmModule instanceof WebAssembly.Module)) {
27
+ throw new Error("processorOptions.wasmModule must be a WebAssembly.Module");
28
+ }
29
+ module = wasmModule;
30
+ } else {
31
+ const bytes =
32
+ wasmBytes instanceof Uint8Array ? wasmBytes : new Uint8Array(wasmBytes);
33
+ module = new WebAssembly.Module(bytes);
34
+ }
35
+ const instance = new WebAssembly.Instance(module);
36
+ this.exports = instance.exports;
37
+ this.memory = this.exports.memory;
38
+
39
+ if (!(this.memory instanceof WebAssembly.Memory)) {
40
+ throw new Error("expected exported wasm memory");
41
+ }
42
+
43
+ this.memoryBuffer = null;
44
+ this.dataViewCache = null;
45
+ this.viewsReady = false;
46
+ this.allocationLocked = false;
47
+ this.stateBytes = null;
48
+ this.inputViews = [];
49
+ this.outputViews = [];
50
+
51
+ const heapBase = Number(
52
+ this.exports.__heap_base?.value ?? this.exports.__heap_base ?? 0,
53
+ );
54
+ if (
55
+ !Number.isInteger(heapBase)
56
+ || heapBase < -0x8000_0000
57
+ || heapBase > 0xffff_ffff
58
+ ) {
59
+ throw new Error(`invalid wasm32 heap base: ${heapBase}`);
60
+ }
61
+ this.heap = heapBase >>> 0;
62
+ this.stateSizeBytes = Number(metadata.runtime?.state_size_bytes ?? 0);
63
+ this.paramInfo = Array.isArray(metadata.metadata?.params) ? metadata.metadata.params : [];
64
+ this.eventInfo = Array.isArray(metadata.metadata?.events)
65
+ ? metadata.metadata.events
66
+ : [];
67
+ this.controlOutputInfo = Array.isArray(metadata.metadata?.control_outputs)
68
+ ? metadata.metadata.control_outputs
69
+ : [];
70
+ this.bufferInfo = Array.isArray(metadata.metadata?.buffers)
71
+ ? metadata.metadata.buffers
72
+ : [];
73
+ this.inputInfo = Array.isArray(metadata.metadata?.inputs)
74
+ ? metadata.metadata.inputs
75
+ : [];
76
+ this.outputInfo = Array.isArray(metadata.metadata?.outputs)
77
+ ? metadata.metadata.outputs
78
+ : [];
79
+ this.snapshotInfo = Array.isArray(metadata.metadata?.states)
80
+ ? metadata.metadata.states
81
+ : [];
82
+ this.snapshotSizeBytes = Number(metadata.runtime?.snapshot_size_bytes ?? 0);
83
+ this.inputChannels = this.flattenAudioChannels(this.inputInfo, "input");
84
+ this.outputChannels = this.flattenAudioChannels(this.outputInfo, "output");
85
+ this.inputCount = this.inputChannels.length;
86
+ this.outputCount = this.outputChannels.length;
87
+ if (this.inputCount === 0 && this.outputCount === 0) {
88
+ throw new Error(
89
+ "the Onda Web Audio processor requires at least one audio input or output",
90
+ );
91
+ }
92
+ this.blockSize = Number(metadata.compile?.block_size ?? 128);
93
+ this.compileSampleRate = Number(metadata.compile?.sample_rate ?? sampleRate);
94
+ if (!Number.isInteger(this.blockSize) || this.blockSize <= 0) {
95
+ throw new Error(`invalid compile-time block size: ${this.blockSize}`);
96
+ }
97
+ if (!Number.isFinite(this.compileSampleRate) || this.compileSampleRate <= 0) {
98
+ throw new Error(`invalid compile-time sample rate: ${this.compileSampleRate}`);
99
+ }
100
+ const renderSampleRate = Number(globalThis.sampleRate);
101
+ if (
102
+ Number.isFinite(renderSampleRate)
103
+ && renderSampleRate > 0
104
+ && renderSampleRate !== this.compileSampleRate
105
+ ) {
106
+ throw new Error(
107
+ `processor was compiled for ${this.compileSampleRate} Hz but the AudioWorklet runs at ${renderSampleRate} Hz`,
108
+ );
109
+ }
110
+ this.inputPtrs = [];
111
+ this.inputCapacityFrames = 0;
112
+ this.outputPtrs = [];
113
+ this.outputCapacityFrames = 0;
114
+ this.blockCursor = 0;
115
+
116
+ const paramBytes = Number(metadata.runtime?.param_size_bytes ?? 0);
117
+ if (!Number.isInteger(paramBytes) || paramBytes < 0) {
118
+ throw new Error(`invalid parameter storage size: ${paramBytes}`);
119
+ }
120
+ this.paramSizeBytes = paramBytes;
121
+
122
+ this.paramsPtr = paramBytes ? this.alloc(paramBytes, 4) : 0;
123
+ this.statePtr = this.stateSizeBytes ? this.alloc(this.stateSizeBytes, 16) : 0;
124
+ this.inPtrsPtr = this.inputCount ? this.alloc(this.inputCount * 4, 4) : 0;
125
+ this.outPtrsPtr = this.outputCount ? this.alloc(this.outputCount * 4, 4) : 0;
126
+ const bufferTableBytes = this.bufferInfo.length * 4;
127
+ this.bufferPointersPtr = this.bufferInfo.length
128
+ ? this.alloc(bufferTableBytes, 4)
129
+ : 0;
130
+ this.bufferFramesPtr = this.bufferInfo.length
131
+ ? this.alloc(bufferTableBytes, 4)
132
+ : 0;
133
+ this.bufferChannelsPtr = this.bufferInfo.length
134
+ ? this.alloc(bufferTableBytes, 4)
135
+ : 0;
136
+ this.bufferSampleRatesPtr = this.bufferInfo.length
137
+ ? this.alloc(bufferTableBytes, 4)
138
+ : 0;
139
+ this.bufferBindings = [];
140
+ this.bindInitialBuffers(processorOptions.buffers ?? {});
141
+ this.eventPayloadCapacity = this.configuredEventPayloadCapacity(
142
+ processorOptions.eventPayloadCapacityBytes,
143
+ );
144
+ this.eventPayloadPtr = this.eventPayloadCapacity
145
+ ? this.alloc(this.eventPayloadCapacity, 8)
146
+ : 0;
147
+ this.writeParamDefaults();
148
+ this.writeInitialParams(processorOptions.params ?? {});
149
+ this.ensureInputCapacity(this.blockSize);
150
+ this.ensureOutputCapacity(this.blockSize);
151
+ this.viewsReady = true;
152
+ this.refreshMemoryCache(true);
153
+ this.reset();
154
+ this.allocationLocked = true;
155
+ this.port.onmessage = (event) => this.handleMessage(event.data ?? {});
156
+ }
157
+
158
+ ensureMemoryCapacity(requiredBytes) {
159
+ const pageBytes = 64 * 1024;
160
+ const current = this.memory.buffer.byteLength;
161
+ if (requiredBytes <= current) {
162
+ return;
163
+ }
164
+ const extraPages = Math.ceil((requiredBytes - current) / pageBytes);
165
+ this.memory.grow(extraPages);
166
+ }
167
+
168
+ alignUp(value, align) {
169
+ return Math.ceil(value / align) * align;
170
+ }
171
+
172
+ alloc(size, align) {
173
+ if (this.allocationLocked) {
174
+ throw new Error("Onda worklet memory allocation is locked after initialization");
175
+ }
176
+ const ptr = this.alignUp(this.heap, align);
177
+ const next = ptr + size;
178
+ this.ensureMemoryCapacity(next);
179
+ this.heap = next;
180
+ return ptr;
181
+ }
182
+
183
+ refreshMemoryCache(force = false) {
184
+ const buffer = this.memory.buffer;
185
+ if (!force && buffer === this.memoryBuffer) return;
186
+ this.memoryBuffer = buffer;
187
+ this.dataViewCache = new DataView(buffer);
188
+ if (!this.viewsReady) return;
189
+
190
+ this.stateBytes = new Uint8Array(
191
+ buffer,
192
+ this.statePtr,
193
+ this.stateSizeBytes,
194
+ );
195
+ this.inputViews = this.inputChannels.map((channel, index) => {
196
+ channel.pointer = this.inputPtrs[index];
197
+ return this.scalarView(
198
+ channel.pointer,
199
+ channel.scalar,
200
+ this.inputCapacityFrames,
201
+ );
202
+ });
203
+ this.outputViews = this.outputChannels.map((channel, index) => {
204
+ channel.pointer = this.outputPtrs[index];
205
+ return this.scalarView(
206
+ channel.pointer,
207
+ channel.scalar,
208
+ this.outputCapacityFrames,
209
+ );
210
+ });
211
+ }
212
+
213
+ memoryView() {
214
+ this.refreshMemoryCache();
215
+ return this.dataViewCache;
216
+ }
217
+
218
+ scalarView(address, scalar, length) {
219
+ if (!HOST_LITTLE_ENDIAN) return null;
220
+ if (address % this.scalarByteSize(scalar) !== 0) return null;
221
+ if (scalar === "bool") return new Uint8Array(this.memoryBuffer, address, length);
222
+ if (scalar === "i32") return new Int32Array(this.memoryBuffer, address, length);
223
+ if (scalar === "i64") return new BigInt64Array(this.memoryBuffer, address, length);
224
+ if (scalar === "f32") return new Float32Array(this.memoryBuffer, address, length);
225
+ if (scalar === "f64") return new Float64Array(this.memoryBuffer, address, length);
226
+ throw new Error(`unsupported ABI scalar '${String(scalar)}'`);
227
+ }
228
+
229
+ configuredEventPayloadCapacity(configuredCapacity) {
230
+ let minimum = 0;
231
+ let dynamic = false;
232
+ for (const event of this.eventInfo) {
233
+ minimum = Math.max(
234
+ minimum,
235
+ Number(event.payload_size_bytes ?? event.payload_min_size_bytes ?? 0),
236
+ );
237
+ dynamic ||= event.has_dynamic_payload === true;
238
+ }
239
+ const capacity = configuredCapacity
240
+ ?? (dynamic ? Math.max(minimum, DEFAULT_EVENT_PAYLOAD_CAPACITY_BYTES) : minimum);
241
+ if (
242
+ !Number.isSafeInteger(capacity)
243
+ || capacity < minimum
244
+ || capacity < 0
245
+ || capacity > 0x7fff_ffff
246
+ ) {
247
+ throw new Error(
248
+ `event payload capacity must be an integer from ${minimum} through 2147483647 bytes`,
249
+ );
250
+ }
251
+ return capacity;
252
+ }
253
+
254
+ flattenAudioChannels(ports, kind) {
255
+ const channels = [];
256
+ for (const port of ports) {
257
+ const channelOffset = Number(port.slot_offset);
258
+ const channelCount = Number(port.array_len);
259
+ const scalar = port.scalar ?? "f32";
260
+ const elementSize = this.scalarByteSize(scalar);
261
+ if (
262
+ !Number.isInteger(channelOffset) ||
263
+ channelOffset !== channels.length ||
264
+ !Number.isInteger(channelCount) ||
265
+ channelCount <= 0
266
+ ) {
267
+ throw new Error(`invalid flattened Onda ${kind} channel metadata`);
268
+ }
269
+ if (
270
+ port.element_size_bytes !== undefined &&
271
+ Number(port.element_size_bytes) !== elementSize
272
+ ) {
273
+ throw new Error(
274
+ `Onda ${kind} '${port.name}' has inconsistent scalar byte width`,
275
+ );
276
+ }
277
+ for (let channel = 0; channel < channelCount; channel += 1) {
278
+ channels.push({
279
+ name: port.name,
280
+ scalar,
281
+ elementSize,
282
+ });
283
+ }
284
+ }
285
+ return channels;
286
+ }
287
+
288
+ writeInitialParams(values) {
289
+ if (Array.isArray(values)) {
290
+ values.forEach((value, index) => this.setParam(index, value));
291
+ return;
292
+ }
293
+ if (values && typeof values === "object") {
294
+ for (const [name, value] of Object.entries(values)) {
295
+ this.setParam(name, value);
296
+ }
297
+ return;
298
+ }
299
+ throw new Error("processorOptions.params must be an array or object");
300
+ }
301
+
302
+ writeParamDefaults() {
303
+ for (const param of this.paramInfo) {
304
+ const value = this.metadataDefaultValue(param);
305
+ if (value !== undefined) {
306
+ this.writeStorage(
307
+ this.paramsPtr + Number(param.byte_offset),
308
+ param,
309
+ value,
310
+ );
311
+ }
312
+ }
313
+ }
314
+
315
+ setParam(selector, value) {
316
+ const paramId = Number.isInteger(selector)
317
+ ? selector
318
+ : this.paramInfo.findIndex((param) => param.name === selector);
319
+ const param = this.paramInfo[paramId];
320
+ if (!param) {
321
+ throw new Error(`unknown Onda parameter '${String(selector)}'`);
322
+ }
323
+ if (value === undefined) {
324
+ throw new Error(`Onda parameter '${param.name}' requires a value`);
325
+ }
326
+ const offset = Number(param.byte_offset);
327
+ const byteSize = Number(param.byte_size);
328
+ const length = Number(param.array_len);
329
+ const expectedByteSize = length * this.scalarByteSize(param.scalar);
330
+ if (
331
+ !Number.isInteger(offset) ||
332
+ offset < 0 ||
333
+ !Number.isInteger(byteSize) ||
334
+ !Number.isInteger(length) ||
335
+ length <= 0 ||
336
+ byteSize !== expectedByteSize ||
337
+ offset + byteSize > this.paramSizeBytes
338
+ ) {
339
+ throw new Error(`Onda parameter '${param.name}' has invalid storage metadata`);
340
+ }
341
+ this.writeStorage(this.paramsPtr + offset, param, value);
342
+ }
343
+
344
+ reset() {
345
+ this.refreshMemoryCache();
346
+ this.stateBytes.fill(0);
347
+ this.blockCursor = 0;
348
+ this.exports.onda_init(this.paramsPtr, this.statePtr);
349
+ }
350
+
351
+ createSnapshot() {
352
+ const snapshot = new Uint8Array(this.snapshotSizeBytes);
353
+ this.refreshMemoryCache();
354
+ const state = this.stateBytes;
355
+ for (const entry of this.snapshotInfo) {
356
+ const packedOffset = Number(entry.packed_snapshot_byte_offset);
357
+ const physicalOffset = Number(entry.physical_state_byte_offset);
358
+ const byteSize = Number(entry.byte_size);
359
+ this.validateSnapshotEntry(entry, packedOffset, physicalOffset, byteSize);
360
+ snapshot.set(
361
+ state.subarray(physicalOffset, physicalOffset + byteSize),
362
+ packedOffset,
363
+ );
364
+ }
365
+ return snapshot;
366
+ }
367
+
368
+ restoreSnapshot(value) {
369
+ const snapshot = this.snapshotBytes(value);
370
+ if (snapshot.byteLength !== this.snapshotSizeBytes) {
371
+ throw new Error(
372
+ `snapshot has ${snapshot.byteLength} bytes; expected ${this.snapshotSizeBytes}`,
373
+ );
374
+ }
375
+ // The ABI restore base is a fresh post-init image, so scratch and
376
+ // control-mirror state never leak across a restore.
377
+ this.reset();
378
+ const state = this.stateBytes;
379
+ for (const entry of this.snapshotInfo) {
380
+ const packedOffset = Number(entry.packed_snapshot_byte_offset);
381
+ const physicalOffset = Number(entry.physical_state_byte_offset);
382
+ const byteSize = Number(entry.byte_size);
383
+ this.validateSnapshotEntry(entry, packedOffset, physicalOffset, byteSize);
384
+ state.set(
385
+ snapshot.subarray(packedOffset, packedOffset + byteSize),
386
+ physicalOffset,
387
+ );
388
+ }
389
+ }
390
+
391
+ snapshotBytes(value) {
392
+ if (value instanceof Uint8Array) return value;
393
+ if (value instanceof ArrayBuffer) return new Uint8Array(value);
394
+ if (ArrayBuffer.isView(value)) {
395
+ return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
396
+ }
397
+ throw new Error("snapshot restore requires byte storage");
398
+ }
399
+
400
+ validateSnapshotEntry(entry, packedOffset, physicalOffset, byteSize) {
401
+ if (
402
+ !Number.isSafeInteger(packedOffset)
403
+ || packedOffset < 0
404
+ || !Number.isSafeInteger(physicalOffset)
405
+ || physicalOffset < 0
406
+ || !Number.isSafeInteger(byteSize)
407
+ || byteSize < 0
408
+ || packedOffset + byteSize > this.snapshotSizeBytes
409
+ || physicalOffset + byteSize > this.stateSizeBytes
410
+ ) {
411
+ throw new Error(
412
+ `state '${String(entry.name)}' has invalid snapshot metadata`,
413
+ );
414
+ }
415
+ }
416
+
417
+ postResponse(message, response, always = false, transfer = []) {
418
+ if (!always && message.requestId === undefined) return;
419
+ this.port.postMessage(
420
+ message.requestId === undefined
421
+ ? response
422
+ : { ...response, requestId: message.requestId },
423
+ transfer,
424
+ );
425
+ }
426
+
427
+ handleMessage(message) {
428
+ try {
429
+ if (message.type === "set-param") {
430
+ this.setParam(
431
+ message.param ?? message.name ?? message.index,
432
+ message.value,
433
+ );
434
+ this.postResponse(message, { type: "onda-ok", operation: message.type });
435
+ } else if (message.type === "reset") {
436
+ this.reset();
437
+ this.postResponse(message, { type: "onda-ok", operation: message.type });
438
+ } else if (message.type === "event") {
439
+ this.dispatchEvent(
440
+ message.event ?? message.name ?? message.index,
441
+ message.values ?? message.args ?? {},
442
+ );
443
+ this.postResponse(message, { type: "onda-ok", operation: message.type });
444
+ } else if (message.type === "read-control-outputs") {
445
+ this.postResponse(message, {
446
+ type: "control-outputs",
447
+ values: this.readControlOutputs(),
448
+ }, true);
449
+ } else if (message.type === "read-buffer") {
450
+ this.postResponse(
451
+ message,
452
+ { type: "buffer", ...this.readBuffer(message.buffer) },
453
+ true,
454
+ );
455
+ } else if (message.type === "snapshot") {
456
+ const snapshot = this.createSnapshot();
457
+ this.postResponse(
458
+ message,
459
+ { type: "snapshot", bytes: snapshot },
460
+ true,
461
+ [snapshot.buffer],
462
+ );
463
+ } else if (message.type === "restore-snapshot") {
464
+ this.restoreSnapshot(message.snapshot ?? message.bytes);
465
+ this.postResponse(message, { type: "onda-ok", operation: message.type });
466
+ } else {
467
+ throw new Error(`unknown Onda worklet operation '${String(message.type)}'`);
468
+ }
469
+ } catch (error) {
470
+ this.port.postMessage({
471
+ type: "onda-error",
472
+ operation: message.type ?? "unknown",
473
+ requestId: message.requestId,
474
+ error: String(error && error.message ? error.message : error),
475
+ });
476
+ }
477
+ }
478
+
479
+ dispatchEvent(selector, values) {
480
+ const eventId = Number.isInteger(selector)
481
+ ? selector
482
+ : this.eventInfo.findIndex((event) => event.name === selector);
483
+ const event = this.eventInfo[eventId];
484
+ if (!event) {
485
+ throw new Error(`unknown Onda event '${String(selector)}'`);
486
+ }
487
+ let payloadSize = 0;
488
+ for (let paramId = 0; paramId < event.params.length; paramId += 1) {
489
+ const param = event.params[paramId];
490
+ const value = this.eventValue(event, param, paramId, values);
491
+ if (param.is_slice) {
492
+ const length = this.sequenceLength(
493
+ value,
494
+ `event '${event.name}' slice '${param.name}'`,
495
+ );
496
+ payloadSize = this.checkedEventPayloadSize(
497
+ payloadSize,
498
+ 4 + length * this.scalarByteSize(param.scalar),
499
+ event.name,
500
+ );
501
+ } else {
502
+ this.validateStorageValue(param, value);
503
+ payloadSize = this.checkedEventPayloadSize(
504
+ payloadSize,
505
+ Number(param.byte_size ?? 0),
506
+ event.name,
507
+ );
508
+ }
509
+ }
510
+ if (payloadSize > this.eventPayloadCapacity) {
511
+ throw new Error(
512
+ `event '${event.name}' requires ${payloadSize} payload bytes; configured capacity is ${this.eventPayloadCapacity}`,
513
+ );
514
+ }
515
+
516
+ let offset = 0;
517
+ const view = this.memoryView();
518
+ for (let paramId = 0; paramId < event.params.length; paramId += 1) {
519
+ const param = event.params[paramId];
520
+ const value = this.eventValue(event, param, paramId, values);
521
+ const address = this.eventPayloadPtr + offset;
522
+ if (param.is_slice) {
523
+ const length = this.sequenceLength(
524
+ value,
525
+ `event '${event.name}' slice '${param.name}'`,
526
+ );
527
+ view.setInt32(address, length, true);
528
+ this.writeScalarValues(address + 4, param.scalar, value, length, view);
529
+ offset += 4 + length * this.scalarByteSize(param.scalar);
530
+ } else {
531
+ this.writeStorage(address, param, value, view);
532
+ offset += Number(param.byte_size ?? 0);
533
+ }
534
+ }
535
+ const handler = this.exports[event.export];
536
+ if (typeof handler !== "function") {
537
+ throw new Error(`missing WebAssembly export '${event.export}'`);
538
+ }
539
+ handler(
540
+ this.eventPayloadPtr,
541
+ this.paramsPtr,
542
+ this.statePtr,
543
+ this.bufferPointersPtr,
544
+ this.bufferFramesPtr,
545
+ this.bufferChannelsPtr,
546
+ this.bufferSampleRatesPtr,
547
+ );
548
+ }
549
+
550
+ eventValue(event, param, paramId, values) {
551
+ const supplied = Array.isArray(values)
552
+ ? values[paramId]
553
+ : values?.[param.name];
554
+ const value = supplied === undefined
555
+ ? this.metadataDefaultValue(param)
556
+ : supplied;
557
+ if (value === undefined) {
558
+ throw new Error(
559
+ `event '${event.name}' requires parameter '${param.name}'`,
560
+ );
561
+ }
562
+ return value;
563
+ }
564
+
565
+ checkedEventPayloadSize(current, additional, eventName) {
566
+ const next = current + additional;
567
+ if (
568
+ !Number.isSafeInteger(additional)
569
+ || additional < 0
570
+ || !Number.isSafeInteger(next)
571
+ || next > 0x7fff_ffff
572
+ ) {
573
+ throw new Error(`event '${eventName}' payload exceeds the 32-bit ABI limit`);
574
+ }
575
+ return next;
576
+ }
577
+
578
+ readControlOutputs() {
579
+ return Object.fromEntries(
580
+ this.controlOutputInfo.map((output) => [
581
+ output.name,
582
+ this.readStorage(
583
+ this.statePtr + Number(output.state_byte_offset),
584
+ output,
585
+ ),
586
+ ]),
587
+ );
588
+ }
589
+
590
+ bindInitialBuffers(options) {
591
+ this.bufferInfo.forEach((buffer, bufferId) => {
592
+ const supplied = Array.isArray(options)
593
+ ? options[bufferId]
594
+ : options[buffer.name];
595
+ if (supplied === undefined) {
596
+ throw new Error(`Onda buffer '${buffer.name}' is not bound`);
597
+ }
598
+ const descriptor =
599
+ Array.isArray(supplied) || ArrayBuffer.isView(supplied)
600
+ ? { data: supplied }
601
+ : supplied;
602
+ const data = descriptor?.data;
603
+ const length = this.sequenceLength(data, `Onda buffer '${buffer.name}'`);
604
+ const sampleRate = Math.fround(
605
+ Number(
606
+ descriptor.sampleRate ?? descriptor.sample_rate ?? this.compileSampleRate,
607
+ ),
608
+ );
609
+ if (!Number.isFinite(sampleRate) || sampleRate <= 0) {
610
+ throw new Error(`Onda buffer '${buffer.name}' has an invalid sample rate`);
611
+ }
612
+ let frames;
613
+ let channels;
614
+ let pointer;
615
+ if (length === 0) {
616
+ if (
617
+ (descriptor.frames !== undefined && descriptor.frames !== 0)
618
+ || (descriptor.channels !== undefined && descriptor.channels !== 0)
619
+ ) {
620
+ throw new Error(
621
+ `Onda buffer '${buffer.name}' empty data requires zero frames and channels`,
622
+ );
623
+ }
624
+ frames = 0;
625
+ channels = 0;
626
+ pointer = 0;
627
+ } else {
628
+ const declaredChannels = Number(buffer.static_channels ?? 0);
629
+ channels = Number(descriptor.channels ?? declaredChannels);
630
+ if (!Number.isInteger(channels) || channels <= 0) {
631
+ throw new Error(`Onda buffer '${buffer.name}' requires channels > 0`);
632
+ }
633
+ if (declaredChannels && channels !== declaredChannels) {
634
+ throw new Error(
635
+ `Onda buffer '${buffer.name}' requires ${declaredChannels} channel(s)`,
636
+ );
637
+ }
638
+ frames = Number(descriptor.frames ?? length / channels);
639
+ if (
640
+ !Number.isInteger(frames)
641
+ || frames <= 0
642
+ || frames * channels !== length
643
+ ) {
644
+ throw new Error(
645
+ `Onda buffer '${buffer.name}' data does not match its frame/channel shape`,
646
+ );
647
+ }
648
+ const elementSize = this.scalarByteSize(buffer.scalar);
649
+ pointer = this.alloc(length * elementSize, elementSize);
650
+ this.writeScalarValues(pointer, buffer.scalar, data, length);
651
+ }
652
+ const view = this.memoryView();
653
+ view.setUint32(this.bufferPointersPtr + bufferId * 4, pointer, true);
654
+ view.setInt32(this.bufferFramesPtr + bufferId * 4, frames, true);
655
+ view.setInt32(this.bufferChannelsPtr + bufferId * 4, channels, true);
656
+ view.setFloat32(
657
+ this.bufferSampleRatesPtr + bufferId * 4,
658
+ sampleRate,
659
+ true,
660
+ );
661
+ this.bufferBindings[bufferId] = {
662
+ ...buffer,
663
+ pointer,
664
+ frames,
665
+ channels,
666
+ sampleRate,
667
+ };
668
+ });
669
+ }
670
+
671
+ readBuffer(selector) {
672
+ const bufferId = Number.isInteger(selector)
673
+ ? selector
674
+ : this.bufferInfo.findIndex((buffer) => buffer.name === selector);
675
+ const binding = this.bufferBindings[bufferId];
676
+ if (!binding) {
677
+ throw new Error(`unknown Onda buffer '${String(selector)}'`);
678
+ }
679
+ const length = binding.frames * binding.channels;
680
+ const elementSize = this.scalarByteSize(binding.scalar);
681
+ const view = this.memoryView();
682
+ return {
683
+ name: binding.name,
684
+ frames: binding.frames,
685
+ channels: binding.channels,
686
+ sampleRate: binding.sampleRate,
687
+ data: Array.from({ length }, (_, index) =>
688
+ this.readScalar(
689
+ binding.pointer + index * elementSize,
690
+ binding.scalar,
691
+ view,
692
+ ),
693
+ ),
694
+ };
695
+ }
696
+
697
+ constantValue(constant) {
698
+ if (constant?.kind === "scalar") {
699
+ return this.decodeConstantScalar(constant.data);
700
+ }
701
+ if (constant?.kind === "aggregate" && Array.isArray(constant.data)) {
702
+ return constant.data.map((value) => this.constantValue(value));
703
+ }
704
+ return undefined;
705
+ }
706
+
707
+ metadataDefaultValue(info) {
708
+ if (!Array.isArray(info?.default_reprs)) return undefined;
709
+ const values = info.default_reprs.map((value) =>
710
+ this.decodeConstantScalar({ type: info.scalar, value })
711
+ );
712
+ return this.isFixedArray(info) ? values : values[0];
713
+ }
714
+
715
+ isFixedArray(info) {
716
+ return info?.is_slice !== true && /\[[0-9]+\]$/.test(info?.type_repr ?? "");
717
+ }
718
+
719
+ decodeConstantScalar(scalar) {
720
+ const type = scalar?.type;
721
+ const value = scalar?.value;
722
+ if (typeof value !== "string") return value;
723
+ if (type === "bool") {
724
+ if (value === "true") return true;
725
+ if (value === "false") return false;
726
+ throw new Error(`invalid bool constant '${value}'`);
727
+ }
728
+ if (type === "i32") return Number(value);
729
+ if (type === "i64") return BigInt(value);
730
+ if (type !== "f32" && type !== "f64") return value;
731
+ if (!value.startsWith("0x")) {
732
+ const decoded = Number(value);
733
+ if (Number.isNaN(decoded) && value !== "NaN") {
734
+ throw new Error(`invalid ${type} constant '${value}'`);
735
+ }
736
+ return decoded;
737
+ }
738
+ const width = type === "f32" ? 32 : 64;
739
+ const digits = value.startsWith("0x") ? value.slice(2) : "";
740
+ if (digits.length !== width / 4 || !/^[0-9a-f]+$/.test(digits)) {
741
+ throw new Error(`invalid MIR ${type} bit-pattern constant '${String(value)}'`);
742
+ }
743
+ const bytes = new ArrayBuffer(width / 8);
744
+ const view = new DataView(bytes);
745
+ if (width === 32) {
746
+ view.setUint32(0, Number.parseInt(digits, 16), false);
747
+ return view.getFloat32(0, false);
748
+ }
749
+ view.setBigUint64(0, BigInt(value), false);
750
+ return view.getFloat64(0, false);
751
+ }
752
+
753
+ sequenceLength(value, description) {
754
+ const valid = Array.isArray(value)
755
+ || (ArrayBuffer.isView(value) && typeof value.length === "number");
756
+ if (!valid || !Number.isSafeInteger(value.length) || value.length < 0) {
757
+ throw new Error(`${description} requires array data`);
758
+ }
759
+ return value.length;
760
+ }
761
+
762
+ validateStorageValue(info, value) {
763
+ const length = Number(info.array_len);
764
+ const isFixedArray = this.isFixedArray(info);
765
+ const isArrayValue = Array.isArray(value)
766
+ || (ArrayBuffer.isView(value) && typeof value.length === "number");
767
+ if (isFixedArray !== isArrayValue) {
768
+ throw new Error(
769
+ isFixedArray
770
+ ? `'${info.name}' requires exactly ${length} ${info.scalar} value(s)`
771
+ : `'${info.name}' requires one ${info.scalar} value`,
772
+ );
773
+ }
774
+ if (isFixedArray && value.length !== length) {
775
+ throw new Error(
776
+ `'${info.name}' requires exactly ${length} ${info.scalar} value(s)`,
777
+ );
778
+ }
779
+ return length;
780
+ }
781
+
782
+ writeStorage(address, info, value, view = this.memoryView()) {
783
+ const length = this.validateStorageValue(info, value);
784
+ if (!this.isFixedArray(info)) {
785
+ this.writeScalar(address, info.scalar, value, view);
786
+ return;
787
+ }
788
+ this.writeScalarValues(address, info.scalar, value, length, view);
789
+ }
790
+
791
+ writeScalarValues(address, scalar, values, length, view = this.memoryView()) {
792
+ const size = this.scalarByteSize(scalar);
793
+ const target = this.scalarView(address, scalar, length);
794
+ if (target !== null) {
795
+ if (scalar === "bool") {
796
+ for (let index = 0; index < length; index += 1) {
797
+ target[index] = values[index] ? 1 : 0;
798
+ }
799
+ } else if (scalar === "i64") {
800
+ for (let index = 0; index < length; index += 1) {
801
+ target[index] = BigInt(values[index]);
802
+ }
803
+ } else {
804
+ target.set(values);
805
+ }
806
+ return;
807
+ }
808
+ for (let index = 0; index < length; index += 1) {
809
+ this.writeScalar(address + index * size, scalar, values[index], view);
810
+ }
811
+ }
812
+
813
+ readStorage(address, info) {
814
+ const length = Number(info.array_len);
815
+ const size = this.scalarByteSize(info.scalar);
816
+ const view = this.memoryView();
817
+ const values = Array.from({ length }, (_, index) =>
818
+ this.readScalar(address + index * size, info.scalar, view),
819
+ );
820
+ return this.isFixedArray(info) ? values : values[0];
821
+ }
822
+
823
+ scalarByteSize(scalar) {
824
+ if (scalar === "bool") return 1;
825
+ if (scalar === "i32" || scalar === "f32") return 4;
826
+ if (scalar === "i64" || scalar === "f64") return 8;
827
+ throw new Error(`unsupported ABI scalar '${String(scalar)}'`);
828
+ }
829
+
830
+ writeScalar(address, scalar, value, view = this.memoryView()) {
831
+ if (scalar === "bool") view.setUint8(address, value ? 1 : 0);
832
+ else if (scalar === "i32") view.setInt32(address, Number(value), true);
833
+ else if (scalar === "i64") view.setBigInt64(address, BigInt(value), true);
834
+ else if (scalar === "f32") view.setFloat32(address, Number(value), true);
835
+ else if (scalar === "f64") view.setFloat64(address, Number(value), true);
836
+ else throw new Error(`unsupported ABI scalar '${String(scalar)}'`);
837
+ }
838
+
839
+ readScalar(address, scalar, view = this.memoryView()) {
840
+ if (scalar === "bool") return view.getUint8(address) !== 0;
841
+ if (scalar === "i32") return view.getInt32(address, true);
842
+ if (scalar === "i64") return view.getBigInt64(address, true);
843
+ if (scalar === "f32") return view.getFloat32(address, true);
844
+ if (scalar === "f64") return view.getFloat64(address, true);
845
+ throw new Error(`unsupported ABI scalar '${String(scalar)}'`);
846
+ }
847
+
848
+ ensureInputCapacity(frames) {
849
+ if (frames <= this.inputCapacityFrames) {
850
+ return;
851
+ }
852
+
853
+ this.inputPtrs = [];
854
+ for (const channel of this.inputChannels) {
855
+ const ptr = this.alloc(frames * channel.elementSize, 16);
856
+ this.inputPtrs.push(ptr);
857
+ }
858
+
859
+ const view = this.memoryView();
860
+ for (let channel = 0; channel < this.inputCount; channel += 1) {
861
+ view.setUint32(this.inPtrsPtr + channel * 4, this.inputPtrs[channel], true);
862
+ }
863
+ this.inputCapacityFrames = frames;
864
+ }
865
+
866
+ ensureOutputCapacity(frames) {
867
+ if (frames <= this.outputCapacityFrames) {
868
+ return;
869
+ }
870
+
871
+ this.outputPtrs = [];
872
+ for (const channel of this.outputChannels) {
873
+ const ptr = this.alloc(frames * channel.elementSize, 16);
874
+ this.outputPtrs.push(ptr);
875
+ }
876
+
877
+ const view = this.memoryView();
878
+ for (let channel = 0; channel < this.outputCount; channel += 1) {
879
+ const ptr = this.outputPtrs[channel];
880
+ view.setUint32(this.outPtrsPtr + channel * 4, ptr, true);
881
+ }
882
+ this.outputCapacityFrames = frames;
883
+ }
884
+
885
+ audioFrameCount(inputs, outputs) {
886
+ for (let busId = 0; busId < outputs.length; busId += 1) {
887
+ const bus = outputs[busId];
888
+ if (bus.length > 0) return bus[0].length;
889
+ }
890
+ for (let busId = 0; busId < inputs.length; busId += 1) {
891
+ const bus = inputs[busId];
892
+ if (bus.length > 0) return bus[0].length;
893
+ }
894
+ throw new Error("AudioWorklet callback has no audio channel from which to derive its frame count");
895
+ }
896
+
897
+ copyInputSamples(
898
+ source,
899
+ info,
900
+ target,
901
+ callbackOffset,
902
+ startFrame,
903
+ segmentFrames,
904
+ view,
905
+ ) {
906
+ if (target !== null) {
907
+ if (info.scalar === "f32") {
908
+ if (callbackOffset === 0 && segmentFrames === source.length) {
909
+ target.set(source, startFrame);
910
+ } else {
911
+ for (let frame = 0; frame < segmentFrames; frame += 1) {
912
+ target[startFrame + frame] = source[callbackOffset + frame];
913
+ }
914
+ }
915
+ return;
916
+ }
917
+ if (info.scalar === "f64") {
918
+ for (let frame = 0; frame < segmentFrames; frame += 1) {
919
+ target[startFrame + frame] = source[callbackOffset + frame];
920
+ }
921
+ return;
922
+ }
923
+ if (info.scalar === "i32") {
924
+ for (let frame = 0; frame < segmentFrames; frame += 1) {
925
+ target[startFrame + frame] = Math.trunc(source[callbackOffset + frame]);
926
+ }
927
+ return;
928
+ }
929
+ if (info.scalar === "i64") {
930
+ for (let frame = 0; frame < segmentFrames; frame += 1) {
931
+ target[startFrame + frame] = BigInt(
932
+ Math.trunc(source[callbackOffset + frame]),
933
+ );
934
+ }
935
+ return;
936
+ }
937
+ if (info.scalar === "bool") {
938
+ for (let frame = 0; frame < segmentFrames; frame += 1) {
939
+ target[startFrame + frame] = source[callbackOffset + frame] !== 0 ? 1 : 0;
940
+ }
941
+ return;
942
+ }
943
+ throw new Error(`unsupported audio input scalar '${String(info.scalar)}'`);
944
+ }
945
+
946
+ for (let frame = 0; frame < segmentFrames; frame += 1) {
947
+ const value = source[callbackOffset + frame];
948
+ this.writeScalar(
949
+ info.pointer + (startFrame + frame) * info.elementSize,
950
+ info.scalar,
951
+ info.scalar === "bool"
952
+ ? value !== 0
953
+ : info.scalar === "i64"
954
+ ? BigInt(Math.trunc(value))
955
+ : info.scalar === "i32"
956
+ ? Math.trunc(value)
957
+ : value,
958
+ view,
959
+ );
960
+ }
961
+ }
962
+
963
+ zeroInputSamples(info, target, startFrame, segmentFrames, view) {
964
+ if (target !== null) {
965
+ target.fill(
966
+ info.scalar === "i64" ? 0n : 0,
967
+ startFrame,
968
+ startFrame + segmentFrames,
969
+ );
970
+ return;
971
+ }
972
+ for (let frame = 0; frame < segmentFrames; frame += 1) {
973
+ this.writeScalar(
974
+ info.pointer + (startFrame + frame) * info.elementSize,
975
+ info.scalar,
976
+ info.scalar === "i64" ? 0n : 0,
977
+ view,
978
+ );
979
+ }
980
+ }
981
+
982
+ marshalInputSegment(
983
+ inputs,
984
+ callbackFrames,
985
+ callbackOffset,
986
+ startFrame,
987
+ segmentFrames,
988
+ ) {
989
+ const view = this.dataViewCache;
990
+ let inputChannel = 0;
991
+ for (let busId = 0; busId < inputs.length; busId += 1) {
992
+ const bus = inputs[busId];
993
+ for (let busChannel = 0; busChannel < bus.length; busChannel += 1) {
994
+ const source = bus[busChannel];
995
+ if (source.length !== callbackFrames) {
996
+ throw new Error("AudioWorklet input channels have inconsistent block sizes");
997
+ }
998
+ if (inputChannel < this.inputCount) {
999
+ const info = this.inputChannels[inputChannel];
1000
+ this.copyInputSamples(
1001
+ source,
1002
+ info,
1003
+ this.inputViews[inputChannel],
1004
+ callbackOffset,
1005
+ startFrame,
1006
+ segmentFrames,
1007
+ view,
1008
+ );
1009
+ }
1010
+ inputChannel += 1;
1011
+ }
1012
+ }
1013
+
1014
+ for (; inputChannel < this.inputCount; inputChannel += 1) {
1015
+ const info = this.inputChannels[inputChannel];
1016
+ this.zeroInputSamples(
1017
+ info,
1018
+ this.inputViews[inputChannel],
1019
+ startFrame,
1020
+ segmentFrames,
1021
+ view,
1022
+ );
1023
+ }
1024
+ }
1025
+
1026
+ copyOutputSamples(
1027
+ destination,
1028
+ info,
1029
+ source,
1030
+ callbackOffset,
1031
+ startFrame,
1032
+ segmentFrames,
1033
+ view,
1034
+ ) {
1035
+ if (source !== null) {
1036
+ if (
1037
+ info.scalar === "f32"
1038
+ && callbackOffset === 0
1039
+ && startFrame === 0
1040
+ && segmentFrames === destination.length
1041
+ && source.length === destination.length
1042
+ ) {
1043
+ destination.set(source);
1044
+ return;
1045
+ }
1046
+ if (info.scalar === "bool") {
1047
+ for (let frame = 0; frame < segmentFrames; frame += 1) {
1048
+ destination[callbackOffset + frame] = source[startFrame + frame] ? 1 : 0;
1049
+ }
1050
+ return;
1051
+ }
1052
+ for (let frame = 0; frame < segmentFrames; frame += 1) {
1053
+ destination[callbackOffset + frame] = Number(source[startFrame + frame]);
1054
+ }
1055
+ return;
1056
+ }
1057
+
1058
+ for (let frame = 0; frame < segmentFrames; frame += 1) {
1059
+ const value = this.readScalar(
1060
+ info.pointer + (startFrame + frame) * info.elementSize,
1061
+ info.scalar,
1062
+ view,
1063
+ );
1064
+ destination[callbackOffset + frame] = info.scalar === "bool"
1065
+ ? (value ? 1 : 0)
1066
+ : Number(value);
1067
+ }
1068
+ }
1069
+
1070
+ marshalOutputSegment(
1071
+ outputs,
1072
+ callbackFrames,
1073
+ callbackOffset,
1074
+ startFrame,
1075
+ segmentFrames,
1076
+ ) {
1077
+ const view = this.dataViewCache;
1078
+ let outputChannel = 0;
1079
+ for (let busId = 0; busId < outputs.length; busId += 1) {
1080
+ const bus = outputs[busId];
1081
+ for (let busChannel = 0; busChannel < bus.length; busChannel += 1) {
1082
+ const destination = bus[busChannel];
1083
+ if (destination.length !== callbackFrames) {
1084
+ throw new Error("AudioWorklet output channels have inconsistent block sizes");
1085
+ }
1086
+ if (outputChannel < this.outputCount) {
1087
+ const info = this.outputChannels[outputChannel];
1088
+ this.copyOutputSamples(
1089
+ destination,
1090
+ info,
1091
+ this.outputViews[outputChannel],
1092
+ callbackOffset,
1093
+ startFrame,
1094
+ segmentFrames,
1095
+ view,
1096
+ );
1097
+ } else {
1098
+ destination.fill(
1099
+ 0,
1100
+ callbackOffset,
1101
+ callbackOffset + segmentFrames,
1102
+ );
1103
+ }
1104
+ outputChannel += 1;
1105
+ }
1106
+ }
1107
+ }
1108
+
1109
+ invokeProcessSegment(startFrame, frames, flags) {
1110
+ return this.exports.onda_process(
1111
+ this.statePtr,
1112
+ this.paramsPtr,
1113
+ this.inPtrsPtr,
1114
+ this.outPtrsPtr,
1115
+ startFrame,
1116
+ frames,
1117
+ flags,
1118
+ this.bufferPointersPtr,
1119
+ this.bufferFramesPtr,
1120
+ this.bufferChannelsPtr,
1121
+ this.bufferSampleRatesPtr,
1122
+ );
1123
+ }
1124
+
1125
+ process(inputs, outputs) {
1126
+ this.refreshMemoryCache();
1127
+ const frames = this.audioFrameCount(inputs, outputs);
1128
+
1129
+ let callbackOffset = 0;
1130
+ while (callbackOffset < frames) {
1131
+ const startFrame = this.blockCursor;
1132
+ const segmentFrames = Math.min(
1133
+ frames - callbackOffset,
1134
+ this.blockSize - startFrame,
1135
+ );
1136
+ const endsBlock = startFrame + segmentFrames === this.blockSize;
1137
+ const flags = (startFrame === 0 ? ONDA_PROCESS_BEGIN_BLOCK : 0)
1138
+ | (endsBlock ? ONDA_PROCESS_END_BLOCK : 0);
1139
+
1140
+ this.marshalInputSegment(
1141
+ inputs,
1142
+ frames,
1143
+ callbackOffset,
1144
+ startFrame,
1145
+ segmentFrames,
1146
+ );
1147
+ this.invokeProcessSegment(
1148
+ startFrame,
1149
+ segmentFrames,
1150
+ flags,
1151
+ );
1152
+ this.marshalOutputSegment(
1153
+ outputs,
1154
+ frames,
1155
+ callbackOffset,
1156
+ startFrame,
1157
+ segmentFrames,
1158
+ );
1159
+
1160
+ callbackOffset += segmentFrames;
1161
+ this.blockCursor = endsBlock ? 0 : startFrame + segmentFrames;
1162
+ }
1163
+
1164
+ return true;
1165
+ }
1166
+ }
1167
+
1168
+ registerProcessor(ONDA_AUDIO_WORKLET_PROCESSOR_NAME, OndaWasmProcessor);