@onda-lang/webaudio 0.7.5 → 0.8.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.
@@ -0,0 +1,234 @@
1
+ const RING_MAGIC = 0x4f4e4441;
2
+ const RING_VERSION = 1;
3
+
4
+ const CONTROL_MAGIC = 0;
5
+ const CONTROL_VERSION = 1;
6
+ const CONTROL_SLOT_COUNT = 2;
7
+ const CONTROL_DELEGATE_CAPACITY = 3;
8
+ const CONTROL_PRINT_CAPACITY = 4;
9
+ export const EXECUTION_OUTPUT_RING_READ_INDEX = 5;
10
+ export const EXECUTION_OUTPUT_RING_WRITE_INDEX = 6;
11
+ export const EXECUTION_OUTPUT_RING_WAKE_INDEX = 7;
12
+ const CONTROL_WORDS = 8;
13
+
14
+ const SLOT_OPERATION = 0;
15
+ const SLOT_OPERATION_INDEX = 1;
16
+ const SLOT_DELEGATE_SUBSCRIPTION = 2;
17
+ const SLOT_DELEGATE_USED = 3;
18
+ const SLOT_DELEGATE_RECORDS = 4;
19
+ const SLOT_DELEGATE_OVERFLOW = 5;
20
+ const SLOT_DELEGATE_TRANSPORT_DROPS = 6;
21
+ const SLOT_PRINT_SUBSCRIPTION = 7;
22
+ const SLOT_PRINT_USED = 8;
23
+ const SLOT_PRINT_RECORDS = 9;
24
+ const SLOT_PRINT_OVERFLOW = 10;
25
+ const SLOT_PRINT_TRANSPORT_DROPS = 11;
26
+ const SLOT_WORDS = 12;
27
+
28
+ const CONTROL_BYTES = CONTROL_WORDS * 4;
29
+ const SLOT_HEADER_BYTES = SLOT_WORDS * 4;
30
+ const DEFAULT_RING_BUDGET_BYTES = 4 * 1024 * 1024;
31
+ const MAX_RING_SLOTS = 32;
32
+
33
+ export const EXECUTION_OPERATION_INIT = 1;
34
+ export const EXECUTION_OPERATION_PROCESS = 2;
35
+ export const EXECUTION_OPERATION_EVENT = 3;
36
+ export const EXECUTION_OPERATION_TRANSPORT = 4;
37
+
38
+ function align4(value) {
39
+ return Math.ceil(value / 4) * 4;
40
+ }
41
+
42
+ function checkedCapacity(value, name) {
43
+ if (!Number.isSafeInteger(value) || value < 0 || value > 0x7fff_ffff) {
44
+ throw new Error(`${name} must be an integer from 0 through 2147483647 bytes`);
45
+ }
46
+ return value;
47
+ }
48
+
49
+ export function createExecutionOutputRing(
50
+ delegateCapacity,
51
+ printCapacity,
52
+ budgetBytes = DEFAULT_RING_BUDGET_BYTES,
53
+ ) {
54
+ const delegateBytes = checkedCapacity(delegateCapacity, "delegate capacity");
55
+ const printBytes = checkedCapacity(printCapacity, "print capacity");
56
+ if (typeof SharedArrayBuffer !== "function") return null;
57
+ if (!Number.isSafeInteger(budgetBytes) || budgetBytes <= 0) {
58
+ throw new Error("execution-output ring budget must be a positive integer");
59
+ }
60
+
61
+ const payloadBytes = delegateBytes + printBytes;
62
+ if (!Number.isSafeInteger(payloadBytes)) {
63
+ throw new Error("combined execution-output capacity exceeds the JavaScript size limit");
64
+ }
65
+ const slotBytes = align4(SLOT_HEADER_BYTES + payloadBytes);
66
+ const slotCount = Math.max(
67
+ 1,
68
+ Math.min(MAX_RING_SLOTS, Math.floor(budgetBytes / slotBytes)),
69
+ );
70
+ const totalBytes = CONTROL_BYTES + slotCount * slotBytes;
71
+ let buffer;
72
+ try {
73
+ buffer = new SharedArrayBuffer(totalBytes);
74
+ } catch (error) {
75
+ throw new Error(
76
+ `cannot allocate ${totalBytes} bytes for the shared execution-output ring`,
77
+ { cause: error },
78
+ );
79
+ }
80
+ const control = new Int32Array(buffer, 0, CONTROL_WORDS);
81
+ control[CONTROL_MAGIC] = RING_MAGIC;
82
+ control[CONTROL_VERSION] = RING_VERSION;
83
+ control[CONTROL_SLOT_COUNT] = slotCount;
84
+ control[CONTROL_DELEGATE_CAPACITY] = delegateBytes;
85
+ control[CONTROL_PRINT_CAPACITY] = printBytes;
86
+ return buffer;
87
+ }
88
+
89
+ export function openExecutionOutputRing(buffer) {
90
+ if (
91
+ typeof SharedArrayBuffer !== "function"
92
+ || !(buffer instanceof SharedArrayBuffer)
93
+ ) {
94
+ throw new Error("execution-output ring must be a SharedArrayBuffer");
95
+ }
96
+ if (buffer.byteLength < CONTROL_BYTES) {
97
+ throw new Error("execution-output ring is shorter than its control header");
98
+ }
99
+ const control = new Int32Array(buffer, 0, CONTROL_WORDS);
100
+ const slotCount = control[CONTROL_SLOT_COUNT] >>> 0;
101
+ const delegateCapacity = control[CONTROL_DELEGATE_CAPACITY] >>> 0;
102
+ const printCapacity = control[CONTROL_PRINT_CAPACITY] >>> 0;
103
+ const slotBytes = align4(SLOT_HEADER_BYTES + delegateCapacity + printCapacity);
104
+ if (
105
+ (control[CONTROL_MAGIC] >>> 0) !== RING_MAGIC
106
+ || (control[CONTROL_VERSION] >>> 0) !== RING_VERSION
107
+ || slotCount === 0
108
+ || CONTROL_BYTES + slotCount * slotBytes !== buffer.byteLength
109
+ ) {
110
+ throw new Error("execution-output ring has an invalid layout");
111
+ }
112
+ return {
113
+ buffer,
114
+ control,
115
+ words: new Uint32Array(buffer),
116
+ bytes: new Uint8Array(buffer),
117
+ slotCount,
118
+ slotBytes,
119
+ delegateCapacity,
120
+ printCapacity,
121
+ };
122
+ }
123
+
124
+ function slotOffsets(ring, slot) {
125
+ const byteOffset = CONTROL_BYTES + slot * ring.slotBytes;
126
+ return {
127
+ wordOffset: byteOffset / 4,
128
+ delegateOffset: byteOffset + SLOT_HEADER_BYTES,
129
+ printOffset: byteOffset + SLOT_HEADER_BYTES + ring.delegateCapacity,
130
+ };
131
+ }
132
+
133
+ function copyBytes(destination, destinationOffset, source, length) {
134
+ for (let index = 0; index < length; index += 1) {
135
+ destination[destinationOffset + index] = source[index];
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Single-producer write. `entry` and source views may be reused by the caller; this function
141
+ * allocates nothing on either the success or saturation path.
142
+ */
143
+ export function writeExecutionOutputRing(ring, entry, delegateSource, printSource) {
144
+ const read = Atomics.load(ring.control, EXECUTION_OUTPUT_RING_READ_INDEX) >>> 0;
145
+ const write = Atomics.load(ring.control, EXECUTION_OUTPUT_RING_WRITE_INDEX) >>> 0;
146
+ if (((write - read) >>> 0) >= ring.slotCount) return false;
147
+ if (
148
+ entry.delegateUsed > ring.delegateCapacity
149
+ || entry.printUsed > ring.printCapacity
150
+ ) {
151
+ throw new Error("execution-output entry exceeds its shared ring capacity");
152
+ }
153
+ if (
154
+ entry.delegateUsed
155
+ && (!delegateSource || delegateSource.length < entry.delegateUsed)
156
+ ) {
157
+ throw new Error("delegate source is shorter than its execution-output entry");
158
+ }
159
+ if (entry.printUsed && (!printSource || printSource.length < entry.printUsed)) {
160
+ throw new Error("print source is shorter than its execution-output entry");
161
+ }
162
+
163
+ const slot = write % ring.slotCount;
164
+ const byteOffset = CONTROL_BYTES + slot * ring.slotBytes;
165
+ const wordOffset = byteOffset / 4;
166
+ const delegateOffset = byteOffset + SLOT_HEADER_BYTES;
167
+ const printOffset = delegateOffset + ring.delegateCapacity;
168
+ const words = ring.words;
169
+ words[wordOffset + SLOT_OPERATION] = entry.operation;
170
+ words[wordOffset + SLOT_OPERATION_INDEX] = entry.operationIndex;
171
+ words[wordOffset + SLOT_DELEGATE_SUBSCRIPTION] = entry.delegateSubscriptionId;
172
+ words[wordOffset + SLOT_DELEGATE_USED] = entry.delegateUsed;
173
+ words[wordOffset + SLOT_DELEGATE_RECORDS] = entry.delegateRecordCount;
174
+ words[wordOffset + SLOT_DELEGATE_OVERFLOW] = entry.delegateOverflowCount;
175
+ words[wordOffset + SLOT_DELEGATE_TRANSPORT_DROPS] = entry.delegateTransportDropCount;
176
+ words[wordOffset + SLOT_PRINT_SUBSCRIPTION] = entry.printSubscriptionId;
177
+ words[wordOffset + SLOT_PRINT_USED] = entry.printUsed;
178
+ words[wordOffset + SLOT_PRINT_RECORDS] = entry.printRecordCount;
179
+ words[wordOffset + SLOT_PRINT_OVERFLOW] = entry.printOverflowCount;
180
+ words[wordOffset + SLOT_PRINT_TRANSPORT_DROPS] = entry.printTransportDropCount;
181
+ copyBytes(ring.bytes, delegateOffset, delegateSource, entry.delegateUsed);
182
+ copyBytes(ring.bytes, printOffset, printSource, entry.printUsed);
183
+
184
+ Atomics.store(
185
+ ring.control,
186
+ EXECUTION_OUTPUT_RING_WRITE_INDEX,
187
+ (write + 1) | 0,
188
+ );
189
+ Atomics.add(ring.control, EXECUTION_OUTPUT_RING_WAKE_INDEX, 1);
190
+ Atomics.notify(ring.control, EXECUTION_OUTPUT_RING_WAKE_INDEX);
191
+ return true;
192
+ }
193
+
194
+ /** Single-consumer drain. Allocations occur only on the main-thread consumer side. */
195
+ export function drainExecutionOutputRing(ring, consume) {
196
+ let drained = 0;
197
+ for (;;) {
198
+ const read = Atomics.load(ring.control, EXECUTION_OUTPUT_RING_READ_INDEX) >>> 0;
199
+ const write = Atomics.load(ring.control, EXECUTION_OUTPUT_RING_WRITE_INDEX) >>> 0;
200
+ if (read === write) return drained;
201
+ const slot = read % ring.slotCount;
202
+ const { wordOffset, delegateOffset, printOffset } = slotOffsets(ring, slot);
203
+ const words = ring.words;
204
+ const delegateUsed = words[wordOffset + SLOT_DELEGATE_USED];
205
+ const printUsed = words[wordOffset + SLOT_PRINT_USED];
206
+ const entry = {
207
+ operation: words[wordOffset + SLOT_OPERATION],
208
+ operationIndex: words[wordOffset + SLOT_OPERATION_INDEX],
209
+ delegateSubscriptionId: words[wordOffset + SLOT_DELEGATE_SUBSCRIPTION],
210
+ delegateUsed,
211
+ delegateRecordCount: words[wordOffset + SLOT_DELEGATE_RECORDS],
212
+ delegateOverflowCount: words[wordOffset + SLOT_DELEGATE_OVERFLOW],
213
+ delegateTransportDropCount:
214
+ words[wordOffset + SLOT_DELEGATE_TRANSPORT_DROPS],
215
+ printSubscriptionId: words[wordOffset + SLOT_PRINT_SUBSCRIPTION],
216
+ printUsed,
217
+ printRecordCount: words[wordOffset + SLOT_PRINT_RECORDS],
218
+ printOverflowCount: words[wordOffset + SLOT_PRINT_OVERFLOW],
219
+ printTransportDropCount: words[wordOffset + SLOT_PRINT_TRANSPORT_DROPS],
220
+ delegateStorage: new Uint8Array(ring.buffer, delegateOffset, delegateUsed),
221
+ printStorage: new Uint8Array(ring.buffer, printOffset, printUsed),
222
+ };
223
+ try {
224
+ consume(entry);
225
+ } finally {
226
+ Atomics.store(
227
+ ring.control,
228
+ EXECUTION_OUTPUT_RING_READ_INDEX,
229
+ (read + 1) | 0,
230
+ );
231
+ drained += 1;
232
+ }
233
+ }
234
+ }
package/src/index.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  export const ONDA_AUDIO_WORKLET_PROCESSOR_NAME: "onda-wasm-processor";
2
+ export const ONDA_INIT_PRESERVE_PINNED: 0;
3
+ export const ONDA_INIT_FULL: 1;
4
+ export type OndaInitMode = 0 | 1;
2
5
 
3
6
  export type {
4
7
  OndaParamDomain,
@@ -14,10 +17,22 @@ export {
14
17
  paramPlainToNormalized,
15
18
  } from "@onda-lang/processor-abi";
16
19
  import type {
20
+ OndaPrintEntry,
17
21
  OndaProcessorArtifact,
18
22
  OndaProcessorMetadata,
19
23
  } from "@onda-lang/processor-abi";
20
24
 
25
+ export interface OndaAudioPrintBatch {
26
+ type: "onda-print";
27
+ operation: string;
28
+ text: string;
29
+ entries: OndaPrintEntry[];
30
+ overflowCount: number;
31
+ transportDropCount: number;
32
+ }
33
+
34
+ export type OndaAudioPrintListener = (batch: OndaAudioPrintBatch) => void;
35
+
21
36
  export interface OndaAudioProcessorOptions {
22
37
  workletUrl?: string | URL;
23
38
  /** Initial plain Onda parameter values, keyed or ordered by descriptor parameter. */
@@ -29,17 +44,41 @@ export interface OndaAudioProcessorOptions {
29
44
  buffers?: Record<string, unknown> | unknown[];
30
45
  /** Preallocated capacity for dynamic event payloads. Defaults to 64 KiB. */
31
46
  eventPayloadCapacityBytes?: number;
47
+ /**
48
+ * Reusable call-scoped storage for delegate records. Defaults to 64 KiB; zero disables host
49
+ * collection. Delivery uses a bounded SharedArrayBuffer ring and therefore requires cross-origin
50
+ * isolation in browsers. Capacity is a host policy because occurrence counts and slice sizes may
51
+ * be dynamic.
52
+ */
53
+ delegateCapacityBytes?: number;
54
+ /**
55
+ * Reusable call-scoped storage for print records. Defaults to 64 KiB; zero disables delivery.
56
+ * Delivery uses the same bounded SharedArrayBuffer ring as delegates.
57
+ */
58
+ printCapacityBytes?: number;
59
+ /**
60
+ * Factory-only initial print listener. Pass this to capture output from
61
+ * createOndaAudioProcessorInitialized() initialization.
62
+ */
63
+ onPrint?: OndaAudioPrintListener;
32
64
  /** Reusable module compiled outside the audio rendering thread. */
33
65
  compiledModule?: WebAssembly.Module;
34
66
  nodeOptions?: AudioWorkletNodeOptions;
35
67
  AudioWorkletNode?: typeof AudioWorkletNode;
68
+ /** Low-level node-option builders only: initialize in the worklet constructor. */
69
+ initialize?: boolean;
36
70
  }
37
71
 
38
72
  export function flattenedAudioChannelCount(ports?: unknown[]): number;
39
73
  export function ondaAudioWorkletNodeOptions(
40
74
  artifact: OndaProcessorArtifact,
41
75
  options?: OndaAudioProcessorOptions,
42
- ): AudioWorkletNodeOptions;
76
+ ): AudioWorkletNodeOptions & {
77
+ processorOptions: {
78
+ executionOutputRing: SharedArrayBuffer | null;
79
+ [key: string]: unknown;
80
+ };
81
+ };
43
82
  export function registerOndaAudioWorklet(
44
83
  context: BaseAudioContext,
45
84
  workletUrl?: string | URL,
@@ -49,12 +88,23 @@ export function createOndaAudioProcessor(
49
88
  artifact: OndaProcessorArtifact,
50
89
  options?: OndaAudioProcessorOptions,
51
90
  ): Promise<OndaAudioProcessor>;
91
+ export function createOndaAudioProcessorInitialized(
92
+ context: BaseAudioContext,
93
+ artifact: OndaProcessorArtifact,
94
+ options?: OndaAudioProcessorOptions,
95
+ ): Promise<OndaAudioProcessor>;
52
96
  export function compileOndaProcessorModule(
53
97
  artifact: OndaProcessorArtifact,
54
98
  ): Promise<WebAssembly.Module>;
55
99
 
56
100
  export class OndaAudioProcessor {
57
- constructor(node: AudioWorkletNode, metadata?: OndaProcessorMetadata | null);
101
+ constructor(
102
+ node: AudioWorkletNode,
103
+ metadata?: OndaProcessorMetadata | null,
104
+ initialPrintListener?: OndaAudioPrintListener,
105
+ /** Shared ring returned in processorOptions by ondaAudioWorkletNodeOptions(). */
106
+ executionOutputRing?: SharedArrayBuffer | null,
107
+ );
58
108
  readonly node: AudioWorkletNode;
59
109
  readonly metadata: OndaProcessorMetadata | null;
60
110
  request(type: string, fields?: Record<string, unknown>, transfer?: Transferable[]): Promise<any>;
@@ -63,10 +113,32 @@ export class OndaAudioProcessor {
63
113
  /** Map a host value in [0, 1] through the descriptor and set the resulting plain value. */
64
114
  setParamNormalized(param: string | number, value: number): Promise<any>;
65
115
  trigger(event: string | number, values?: Record<string, unknown> | unknown[]): Promise<any>;
66
- reset(): Promise<any>;
116
+ /**
117
+ * Subscribe to batches decoded after generated execution. Collection is active only while at
118
+ * least one listener is registered. overflowCount reports configured-capacity loss and
119
+ * transportDropCount reports bounded worklet-to-main queue loss. Returns an unsubscribe function.
120
+ */
121
+ onDelegates(
122
+ listener: (batch: {
123
+ type: "onda-delegates";
124
+ operation: string;
125
+ occurrences: Array<{
126
+ sequence: number;
127
+ index: number;
128
+ name: string;
129
+ values: Record<string, unknown>;
130
+ }>;
131
+ overflowCount: number;
132
+ transportDropCount: number;
133
+ }) => void,
134
+ ): () => boolean;
135
+ /** Collection is active only while at least one listener is registered. */
136
+ onPrint(listener: OndaAudioPrintListener): () => boolean;
137
+ init(mode: OndaInitMode): Promise<any>;
67
138
  snapshot(): Promise<Uint8Array>;
68
139
  restoreSnapshot(snapshot: Uint8Array | ArrayBuffer): Promise<any>;
69
140
  readControlOutputs(): Promise<Record<string, unknown>>;
70
141
  readBuffer(buffer: string | number): Promise<any>;
142
+ /** Idempotently closes this adapter. Pending and subsequent operations reject. */
71
143
  close(reason?: Error): void;
72
144
  }