supersonic-scsynth 0.6.2 → 0.6.4
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/dist/supersonic.js +20 -3448
- package/dist/wasm/manifest.json +3 -3
- package/dist/wasm/scsynth-nrt.wasm +0 -0
- package/dist/workers/debug_worker.js +2 -281
- package/dist/workers/osc_in_worker.js +1 -279
- package/dist/workers/osc_out_prescheduler_worker.js +1 -705
- package/dist/workers/scsynth_audio_worklet.js +2 -543
- package/package.json +1 -1
|
@@ -1,543 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 Sam Aaron
|
|
4
|
-
|
|
5
|
-
Based on SuperCollider by James McCartney and community
|
|
6
|
-
GPL v3 or later
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* AudioWorklet Processor for scsynth WASM
|
|
11
|
-
* Runs in AudioWorkletGlobalScope with real-time priority
|
|
12
|
-
* VERSION: 16 - Reduced atomic operation frequency
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
class ScsynthProcessor extends AudioWorkletProcessor {
|
|
16
|
-
constructor() {
|
|
17
|
-
super();
|
|
18
|
-
|
|
19
|
-
this.sharedBuffer = null;
|
|
20
|
-
this.wasmModule = null;
|
|
21
|
-
this.wasmInstance = null;
|
|
22
|
-
this.isInitialized = false;
|
|
23
|
-
this.processCallCount = 0;
|
|
24
|
-
this.lastStatusCheck = 0;
|
|
25
|
-
this.ringBufferBase = null;
|
|
26
|
-
|
|
27
|
-
// Pre-allocated audio view to avoid per-frame allocations
|
|
28
|
-
this.audioView = null;
|
|
29
|
-
this.lastAudioBufferPtr = 0;
|
|
30
|
-
this.lastWasmBufferSize = 0;
|
|
31
|
-
|
|
32
|
-
// Views into SharedArrayBuffer
|
|
33
|
-
this.atomicView = null;
|
|
34
|
-
this.uint8View = null;
|
|
35
|
-
this.dataView = null;
|
|
36
|
-
this.localClockOffsetView = null; // Float64Array for reading local clock offset
|
|
37
|
-
|
|
38
|
-
// Buffer constants (loaded from WASM at initialization)
|
|
39
|
-
this.bufferConstants = null;
|
|
40
|
-
|
|
41
|
-
// Control region indices (Int32Array indices) - will be calculated dynamically
|
|
42
|
-
this.CONTROL_INDICES = null;
|
|
43
|
-
|
|
44
|
-
// Metrics indices - will be calculated dynamically
|
|
45
|
-
this.METRICS_INDICES = null;
|
|
46
|
-
|
|
47
|
-
// Status flag masks
|
|
48
|
-
this.STATUS_FLAGS = {
|
|
49
|
-
OK: 0,
|
|
50
|
-
BUFFER_FULL: 1 << 0,
|
|
51
|
-
OVERRUN: 1 << 1,
|
|
52
|
-
WASM_ERROR: 1 << 2,
|
|
53
|
-
FRAGMENTED_MSG: 1 << 3
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
// Listen for messages from main thread
|
|
57
|
-
this.port.onmessage = this.handleMessage.bind(this);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Load buffer constants from WASM module
|
|
61
|
-
// Reads the BufferLayout struct exported by C++
|
|
62
|
-
loadBufferConstants() {
|
|
63
|
-
if (!this.wasmInstance || !this.wasmInstance.exports.get_buffer_layout) {
|
|
64
|
-
throw new Error('WASM instance does not export get_buffer_layout');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Get pointer to BufferLayout struct
|
|
68
|
-
const layoutPtr = this.wasmInstance.exports.get_buffer_layout();
|
|
69
|
-
|
|
70
|
-
// Get WASM memory (imported, not exported - stored in this.wasmMemory)
|
|
71
|
-
const memory = this.wasmMemory;
|
|
72
|
-
if (!memory) {
|
|
73
|
-
throw new Error('WASM memory not available');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Read the struct (20 uint32_t fields + 1 uint8_t + 3 padding bytes)
|
|
77
|
-
const uint32View = new Uint32Array(memory.buffer, layoutPtr, 21);
|
|
78
|
-
const uint8View = new Uint8Array(memory.buffer, layoutPtr, 84);
|
|
79
|
-
|
|
80
|
-
// Extract constants (order matches BufferLayout struct in shared_memory.h)
|
|
81
|
-
this.bufferConstants = {
|
|
82
|
-
IN_BUFFER_START: uint32View[0],
|
|
83
|
-
IN_BUFFER_SIZE: uint32View[1],
|
|
84
|
-
OUT_BUFFER_START: uint32View[2],
|
|
85
|
-
OUT_BUFFER_SIZE: uint32View[3],
|
|
86
|
-
DEBUG_BUFFER_START: uint32View[4],
|
|
87
|
-
DEBUG_BUFFER_SIZE: uint32View[5],
|
|
88
|
-
CONTROL_START: uint32View[6],
|
|
89
|
-
CONTROL_SIZE: uint32View[7],
|
|
90
|
-
METRICS_START: uint32View[8],
|
|
91
|
-
METRICS_SIZE: uint32View[9],
|
|
92
|
-
NTP_START_TIME_START: uint32View[10],
|
|
93
|
-
NTP_START_TIME_SIZE: uint32View[11],
|
|
94
|
-
DRIFT_OFFSET_START: uint32View[12],
|
|
95
|
-
DRIFT_OFFSET_SIZE: uint32View[13],
|
|
96
|
-
GLOBAL_OFFSET_START: uint32View[14],
|
|
97
|
-
GLOBAL_OFFSET_SIZE: uint32View[15],
|
|
98
|
-
TOTAL_BUFFER_SIZE: uint32View[16],
|
|
99
|
-
MAX_MESSAGE_SIZE: uint32View[17],
|
|
100
|
-
MESSAGE_MAGIC: uint32View[18],
|
|
101
|
-
PADDING_MAGIC: uint32View[19],
|
|
102
|
-
DEBUG_PADDING_MARKER: uint8View[80],
|
|
103
|
-
MESSAGE_HEADER_SIZE: 16 // sizeof(Message) - 4 x uint32_t (magic, length, sequence, padding)
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
// Validate
|
|
107
|
-
if (this.bufferConstants.MESSAGE_MAGIC !== 0xDEADBEEF) {
|
|
108
|
-
throw new Error('Invalid buffer constants from WASM');
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Calculate buffer indices based on dynamic ring buffer base address
|
|
113
|
-
// Uses constants loaded from WASM via loadBufferConstants()
|
|
114
|
-
calculateBufferIndices(ringBufferBase) {
|
|
115
|
-
if (!this.bufferConstants) {
|
|
116
|
-
throw new Error('Buffer constants not loaded. Call loadBufferConstants() first.');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const CONTROL_START = this.bufferConstants.CONTROL_START;
|
|
120
|
-
const METRICS_START = this.bufferConstants.METRICS_START;
|
|
121
|
-
|
|
122
|
-
// Calculate Int32Array indices (divide byte offsets by 4)
|
|
123
|
-
this.CONTROL_INDICES = {
|
|
124
|
-
IN_HEAD: (ringBufferBase + CONTROL_START + 0) / 4,
|
|
125
|
-
IN_TAIL: (ringBufferBase + CONTROL_START + 4) / 4,
|
|
126
|
-
OUT_HEAD: (ringBufferBase + CONTROL_START + 8) / 4,
|
|
127
|
-
OUT_TAIL: (ringBufferBase + CONTROL_START + 12) / 4,
|
|
128
|
-
DEBUG_HEAD: (ringBufferBase + CONTROL_START + 16) / 4,
|
|
129
|
-
DEBUG_TAIL: (ringBufferBase + CONTROL_START + 20) / 4,
|
|
130
|
-
SEQUENCE: (ringBufferBase + CONTROL_START + 24) / 4,
|
|
131
|
-
STATUS_FLAGS: (ringBufferBase + CONTROL_START + 28) / 4
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
this.METRICS_INDICES = {
|
|
135
|
-
PROCESS_COUNT: (ringBufferBase + METRICS_START + 0) / 4,
|
|
136
|
-
BUFFER_OVERRUNS: (ringBufferBase + METRICS_START + 4) / 4,
|
|
137
|
-
MESSAGES_PROCESSED: (ringBufferBase + METRICS_START + 8) / 4,
|
|
138
|
-
MESSAGES_DROPPED: (ringBufferBase + METRICS_START + 12) / 4,
|
|
139
|
-
SCHEDULER_QUEUE_DEPTH: (ringBufferBase + METRICS_START + 16) / 4,
|
|
140
|
-
SCHEDULER_QUEUE_MAX: (ringBufferBase + METRICS_START + 20) / 4,
|
|
141
|
-
SCHEDULER_QUEUE_DROPPED: (ringBufferBase + METRICS_START + 24) / 4
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Write worldOptions to SharedArrayBuffer for C++ to read
|
|
147
|
-
// WorldOptions are written after ring buffer storage (65536 bytes)
|
|
148
|
-
writeWorldOptionsToMemory() {
|
|
149
|
-
if (!this.worldOptions || !this.wasmMemory) {
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// WorldOptions location: ringBufferBase + 65536 (after ring_buffer_storage)
|
|
154
|
-
const WORLD_OPTIONS_OFFSET = this.ringBufferBase + 65536;
|
|
155
|
-
const uint32View = new Uint32Array(this.wasmMemory.buffer, WORLD_OPTIONS_OFFSET, 32);
|
|
156
|
-
const float32View = new Float32Array(this.wasmMemory.buffer, WORLD_OPTIONS_OFFSET, 32);
|
|
157
|
-
|
|
158
|
-
// Write worldOptions as uint32/float32 values
|
|
159
|
-
// Order must match C++ reading code in audio_processor.cpp
|
|
160
|
-
uint32View[0] = this.worldOptions.numBuffers || 1024;
|
|
161
|
-
uint32View[1] = this.worldOptions.maxNodes || 1024;
|
|
162
|
-
uint32View[2] = this.worldOptions.maxGraphDefs || 1024;
|
|
163
|
-
uint32View[3] = this.worldOptions.maxWireBufs || 64;
|
|
164
|
-
uint32View[4] = this.worldOptions.numAudioBusChannels || 128;
|
|
165
|
-
uint32View[5] = this.worldOptions.numInputBusChannels || 0;
|
|
166
|
-
uint32View[6] = this.worldOptions.numOutputBusChannels || 2;
|
|
167
|
-
uint32View[7] = this.worldOptions.numControlBusChannels || 4096;
|
|
168
|
-
uint32View[8] = this.worldOptions.bufLength || 128;
|
|
169
|
-
uint32View[9] = this.worldOptions.realTimeMemorySize || 16384;
|
|
170
|
-
uint32View[10] = this.worldOptions.numRGens || 64;
|
|
171
|
-
uint32View[11] = this.worldOptions.realTime ? 1 : 0;
|
|
172
|
-
uint32View[12] = this.worldOptions.memoryLocking ? 1 : 0;
|
|
173
|
-
uint32View[13] = this.worldOptions.loadGraphDefs || 0;
|
|
174
|
-
uint32View[14] = this.worldOptions.preferredSampleRate || 0;
|
|
175
|
-
uint32View[15] = this.worldOptions.verbosity || 0;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Write debug message to SharedArrayBuffer DEBUG ring buffer
|
|
179
|
-
js_debug(message) {
|
|
180
|
-
if (!this.uint8View || !this.atomicView || !this.CONTROL_INDICES || !this.ringBufferBase) {
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
try {
|
|
185
|
-
// Use constants from WASM
|
|
186
|
-
const DEBUG_BUFFER_START = this.bufferConstants.DEBUG_BUFFER_START;
|
|
187
|
-
const DEBUG_BUFFER_SIZE = this.bufferConstants.DEBUG_BUFFER_SIZE;
|
|
188
|
-
const DEBUG_PADDING_MARKER = this.bufferConstants.DEBUG_PADDING_MARKER;
|
|
189
|
-
|
|
190
|
-
const prefixedMessage = '[JS] ' + message + '\n';
|
|
191
|
-
const encoder = new TextEncoder();
|
|
192
|
-
const bytes = encoder.encode(prefixedMessage);
|
|
193
|
-
|
|
194
|
-
// Drop message if too large for buffer
|
|
195
|
-
if (bytes.length > DEBUG_BUFFER_SIZE) {
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const debugHeadIndex = this.CONTROL_INDICES.DEBUG_HEAD;
|
|
200
|
-
const currentHead = Atomics.load(this.atomicView, debugHeadIndex);
|
|
201
|
-
const spaceToEnd = DEBUG_BUFFER_SIZE - currentHead;
|
|
202
|
-
|
|
203
|
-
let writePos = currentHead;
|
|
204
|
-
if (bytes.length > spaceToEnd) {
|
|
205
|
-
// Message won't fit - write padding marker and wrap to beginning
|
|
206
|
-
this.uint8View[this.ringBufferBase + DEBUG_BUFFER_START + currentHead] = DEBUG_PADDING_MARKER;
|
|
207
|
-
writePos = 0;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Write message (now guaranteed to fit contiguously)
|
|
211
|
-
const debugBufferStart = this.ringBufferBase + DEBUG_BUFFER_START;
|
|
212
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
213
|
-
this.uint8View[debugBufferStart + writePos + i] = bytes[i];
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Update head pointer (publish message)
|
|
217
|
-
const newHead = writePos + bytes.length;
|
|
218
|
-
Atomics.store(this.atomicView, debugHeadIndex, newHead);
|
|
219
|
-
} catch (err) {
|
|
220
|
-
// Silently fail in real-time audio context
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
async handleMessage(event) {
|
|
225
|
-
const { data } = event;
|
|
226
|
-
|
|
227
|
-
try {
|
|
228
|
-
if (data.type === 'init' && data.sharedBuffer) {
|
|
229
|
-
// Receive SharedArrayBuffer
|
|
230
|
-
this.sharedBuffer = data.sharedBuffer;
|
|
231
|
-
this.atomicView = new Int32Array(this.sharedBuffer);
|
|
232
|
-
this.uint8View = new Uint8Array(this.sharedBuffer);
|
|
233
|
-
this.dataView = new DataView(this.sharedBuffer);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (data.type === 'loadWasm') {
|
|
237
|
-
// Load WASM module (standalone version)
|
|
238
|
-
if (data.wasmBytes) {
|
|
239
|
-
// Use the memory passed from orchestrator (already created)
|
|
240
|
-
const memory = data.wasmMemory;
|
|
241
|
-
if (!memory) {
|
|
242
|
-
this.port.postMessage({
|
|
243
|
-
type: 'error',
|
|
244
|
-
error: 'No WASM memory provided!'
|
|
245
|
-
});
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Save memory reference for later use (WASM imports memory, doesn't export it)
|
|
250
|
-
this.wasmMemory = memory;
|
|
251
|
-
|
|
252
|
-
// Store worldOptions and sampleRate for C++ initialization
|
|
253
|
-
this.worldOptions = data.worldOptions || {};
|
|
254
|
-
this.sampleRate = data.sampleRate || 48000; // Fallback to 48000 if not provided
|
|
255
|
-
|
|
256
|
-
// Import object for WASM
|
|
257
|
-
// scsynth with pthread support requires these imports
|
|
258
|
-
// (pthread stubs are no-ops - AudioWorklet is single-threaded)
|
|
259
|
-
const imports = {
|
|
260
|
-
env: {
|
|
261
|
-
memory: memory,
|
|
262
|
-
// Time
|
|
263
|
-
emscripten_asm_const_double: () => Date.now() * 1000,
|
|
264
|
-
// Filesystem syscalls
|
|
265
|
-
__syscall_getdents64: () => 0,
|
|
266
|
-
__syscall_unlinkat: () => 0,
|
|
267
|
-
// pthread stubs (no-ops - AudioWorklet doesn't support threading)
|
|
268
|
-
_emscripten_init_main_thread_js: () => {},
|
|
269
|
-
_emscripten_thread_mailbox_await: () => {},
|
|
270
|
-
_emscripten_thread_set_strongref: () => {},
|
|
271
|
-
emscripten_exit_with_live_runtime: () => {},
|
|
272
|
-
_emscripten_receive_on_main_thread_js: () => {},
|
|
273
|
-
emscripten_check_blocking_allowed: () => {},
|
|
274
|
-
_emscripten_thread_cleanup: () => {},
|
|
275
|
-
emscripten_num_logical_cores: () => 1, // Report 1 core
|
|
276
|
-
_emscripten_notify_mailbox_postmessage: () => {}
|
|
277
|
-
},
|
|
278
|
-
wasi_snapshot_preview1: {
|
|
279
|
-
clock_time_get: (clockid, precision, timestamp_ptr) => {
|
|
280
|
-
const view = new DataView(memory.buffer);
|
|
281
|
-
const nanos = BigInt(Math.floor(Date.now() * 1000000));
|
|
282
|
-
view.setBigUint64(timestamp_ptr, nanos, true);
|
|
283
|
-
return 0;
|
|
284
|
-
},
|
|
285
|
-
environ_sizes_get: () => 0,
|
|
286
|
-
environ_get: () => 0,
|
|
287
|
-
fd_close: () => 0,
|
|
288
|
-
fd_write: () => 0,
|
|
289
|
-
fd_seek: () => 0,
|
|
290
|
-
fd_read: () => 0,
|
|
291
|
-
proc_exit: (code) => {
|
|
292
|
-
console.error('[AudioWorklet] WASM tried to exit with code:', code);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
};
|
|
296
|
-
|
|
297
|
-
// Compile and instantiate WASM
|
|
298
|
-
const module = await WebAssembly.compile(data.wasmBytes);
|
|
299
|
-
this.wasmInstance = await WebAssembly.instantiate(module, imports);
|
|
300
|
-
|
|
301
|
-
// Get the ring buffer base address from WASM
|
|
302
|
-
if (this.wasmInstance.exports.get_ring_buffer_base) {
|
|
303
|
-
this.ringBufferBase = this.wasmInstance.exports.get_ring_buffer_base();
|
|
304
|
-
|
|
305
|
-
// Load buffer constants from WASM (single source of truth)
|
|
306
|
-
this.loadBufferConstants();
|
|
307
|
-
|
|
308
|
-
this.calculateBufferIndices(this.ringBufferBase);
|
|
309
|
-
|
|
310
|
-
// Write worldOptions to SharedArrayBuffer for C++ to read
|
|
311
|
-
this.writeWorldOptionsToMemory();
|
|
312
|
-
|
|
313
|
-
// Initialize WASM memory
|
|
314
|
-
if (this.wasmInstance.exports.init_memory) {
|
|
315
|
-
// Pass actual sample rate from AudioContext (not hardcoded!)
|
|
316
|
-
this.wasmInstance.exports.init_memory(this.sampleRate);
|
|
317
|
-
|
|
318
|
-
this.isInitialized = true;
|
|
319
|
-
|
|
320
|
-
this.port.postMessage({
|
|
321
|
-
type: 'initialized',
|
|
322
|
-
success: true,
|
|
323
|
-
ringBufferBase: this.ringBufferBase,
|
|
324
|
-
bufferConstants: this.bufferConstants,
|
|
325
|
-
exports: Object.keys(this.wasmInstance.exports)
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
} else if (data.wasmInstance) {
|
|
330
|
-
// Pre-instantiated WASM (from Emscripten)
|
|
331
|
-
this.wasmInstance = data.wasmInstance;
|
|
332
|
-
|
|
333
|
-
// Get the ring buffer base address from WASM
|
|
334
|
-
if (this.wasmInstance.exports.get_ring_buffer_base) {
|
|
335
|
-
this.ringBufferBase = this.wasmInstance.exports.get_ring_buffer_base();
|
|
336
|
-
|
|
337
|
-
// Load buffer constants from WASM (single source of truth)
|
|
338
|
-
this.loadBufferConstants();
|
|
339
|
-
|
|
340
|
-
this.calculateBufferIndices(this.ringBufferBase);
|
|
341
|
-
|
|
342
|
-
// Write worldOptions to SharedArrayBuffer for C++ to read
|
|
343
|
-
this.writeWorldOptionsToMemory();
|
|
344
|
-
|
|
345
|
-
// Initialize WASM memory
|
|
346
|
-
if (this.wasmInstance.exports.init_memory) {
|
|
347
|
-
// Pass actual sample rate from AudioContext (not hardcoded!)
|
|
348
|
-
this.wasmInstance.exports.init_memory(this.sampleRate);
|
|
349
|
-
|
|
350
|
-
this.isInitialized = true;
|
|
351
|
-
|
|
352
|
-
this.port.postMessage({
|
|
353
|
-
type: 'initialized',
|
|
354
|
-
success: true,
|
|
355
|
-
ringBufferBase: this.ringBufferBase,
|
|
356
|
-
bufferConstants: this.bufferConstants,
|
|
357
|
-
exports: Object.keys(this.wasmInstance.exports)
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
if (data.type === 'getVersion') {
|
|
365
|
-
// Return Supersonic/SuperCollider version string
|
|
366
|
-
if (this.wasmInstance && this.wasmInstance.exports.get_supersonic_version_string) {
|
|
367
|
-
const versionPtr = this.wasmInstance.exports.get_supersonic_version_string();
|
|
368
|
-
const memory = new Uint8Array(this.wasmMemory.buffer);
|
|
369
|
-
// Read null-terminated C string
|
|
370
|
-
let version = '';
|
|
371
|
-
for (let i = versionPtr; memory[i] !== 0; i++) {
|
|
372
|
-
version += String.fromCharCode(memory[i]);
|
|
373
|
-
}
|
|
374
|
-
this.port.postMessage({
|
|
375
|
-
type: 'version',
|
|
376
|
-
version: version
|
|
377
|
-
});
|
|
378
|
-
} else {
|
|
379
|
-
this.port.postMessage({
|
|
380
|
-
type: 'version',
|
|
381
|
-
version: 'unknown'
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (data.type === 'getTimeOffset') {
|
|
387
|
-
// Return time offset (NTP seconds when AudioContext was at 0)
|
|
388
|
-
if (this.wasmInstance && this.wasmInstance.exports.get_time_offset) {
|
|
389
|
-
const offset = this.wasmInstance.exports.get_time_offset();
|
|
390
|
-
this.port.postMessage({
|
|
391
|
-
type: 'timeOffset',
|
|
392
|
-
offset: offset
|
|
393
|
-
});
|
|
394
|
-
} else {
|
|
395
|
-
console.error('[AudioWorklet] get_time_offset not available! wasmInstance:', !!this.wasmInstance);
|
|
396
|
-
this.port.postMessage({
|
|
397
|
-
type: 'error',
|
|
398
|
-
error: 'get_time_offset function not available in WASM exports'
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
} catch (error) {
|
|
404
|
-
console.error('[AudioWorklet] Error handling message:', error);
|
|
405
|
-
this.port.postMessage({
|
|
406
|
-
type: 'error',
|
|
407
|
-
error: error.message,
|
|
408
|
-
stack: error.stack
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
process(inputs, outputs, parameters) {
|
|
414
|
-
// DEBUG: Log first call
|
|
415
|
-
if (!this._everCalled) {
|
|
416
|
-
this._everCalled = true;
|
|
417
|
-
console.log('[AudioWorklet] process() called for first time');
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
if (!this.isInitialized) {
|
|
421
|
-
return true;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
try {
|
|
425
|
-
if (this.wasmInstance && this.wasmInstance.exports.process_audio) {
|
|
426
|
-
// CRITICAL: Access AudioContext currentTime correctly
|
|
427
|
-
// In AudioWorkletGlobalScope, currentTime is a bare global variable (not on globalThis)
|
|
428
|
-
// We use a different variable name to avoid shadowing
|
|
429
|
-
const audioContextTime = currentTime; // Access the global currentTime directly
|
|
430
|
-
|
|
431
|
-
// C++ process_audio() now calculates NTP time internally from:
|
|
432
|
-
// - NTP_START_TIME (write-once, set during initialization)
|
|
433
|
-
// - DRIFT_OFFSET (updated every 15s by main thread)
|
|
434
|
-
// - GLOBAL_OFFSET (for future multi-system sync)
|
|
435
|
-
// DEPRECATED: Legacy timing offset views kept in memory for compatibility but unused
|
|
436
|
-
|
|
437
|
-
const keepAlive = this.wasmInstance.exports.process_audio(audioContextTime);
|
|
438
|
-
|
|
439
|
-
// Copy scsynth audio output to AudioWorklet outputs
|
|
440
|
-
if (this.wasmInstance.exports.get_audio_output_bus && outputs[0] && outputs[0].length >= 2) {
|
|
441
|
-
try {
|
|
442
|
-
const audioBufferPtr = this.wasmInstance.exports.get_audio_output_bus();
|
|
443
|
-
const numSamples = this.wasmInstance.exports.get_audio_buffer_samples();
|
|
444
|
-
|
|
445
|
-
if (audioBufferPtr && audioBufferPtr > 0) {
|
|
446
|
-
const wasmMemory = this.wasmInstance.exports.memory || this.wasmMemory;
|
|
447
|
-
|
|
448
|
-
if (!wasmMemory || !wasmMemory.buffer) {
|
|
449
|
-
return true;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
const currentBuffer = wasmMemory.buffer;
|
|
453
|
-
const bufferSize = currentBuffer.byteLength;
|
|
454
|
-
const requiredBytes = audioBufferPtr + (numSamples * 2 * 4);
|
|
455
|
-
|
|
456
|
-
if (audioBufferPtr < 0 || audioBufferPtr > bufferSize || requiredBytes > bufferSize) {
|
|
457
|
-
return true;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// Reuse Float32Array view if possible (avoid allocation in hot path)
|
|
461
|
-
if (!this.audioView ||
|
|
462
|
-
this.lastAudioBufferPtr !== audioBufferPtr ||
|
|
463
|
-
this.lastWasmBufferSize !== bufferSize ||
|
|
464
|
-
currentBuffer !== this.audioView.buffer) {
|
|
465
|
-
this.audioView = new Float32Array(currentBuffer, audioBufferPtr, numSamples * 2);
|
|
466
|
-
this.lastAudioBufferPtr = audioBufferPtr;
|
|
467
|
-
this.lastWasmBufferSize = bufferSize;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// Direct copy using pre-allocated view
|
|
471
|
-
outputs[0][0].set(this.audioView.subarray(0, numSamples));
|
|
472
|
-
outputs[0][1].set(this.audioView.subarray(numSamples, numSamples * 2));
|
|
473
|
-
}
|
|
474
|
-
} catch (err) {
|
|
475
|
-
// Silently fail in real-time audio context
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// Notify waiting worker only occasionally to reduce overhead
|
|
480
|
-
// Most of the time the worker isn't waiting anyway
|
|
481
|
-
if (this.atomicView && (this.processCallCount % 16 === 0)) {
|
|
482
|
-
Atomics.notify(this.atomicView, this.CONTROL_INDICES.OUT_HEAD, 1);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// Periodic status check - reduced frequency
|
|
486
|
-
this.processCallCount++;
|
|
487
|
-
if (this.processCallCount % 3750 === 0) { // Every ~10 seconds instead of 1
|
|
488
|
-
this.checkStatus();
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
return keepAlive !== 0;
|
|
492
|
-
}
|
|
493
|
-
} catch (error) {
|
|
494
|
-
console.error('[AudioWorklet] process() error:', error);
|
|
495
|
-
console.error('[AudioWorklet] Stack:', error.stack);
|
|
496
|
-
if (this.atomicView) {
|
|
497
|
-
Atomics.or(this.atomicView, this.CONTROL_INDICES.STATUS_FLAGS, this.STATUS_FLAGS.WASM_ERROR);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
return true;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
checkStatus() {
|
|
505
|
-
if (!this.atomicView) return;
|
|
506
|
-
|
|
507
|
-
const statusFlags = Atomics.load(this.atomicView, this.CONTROL_INDICES.STATUS_FLAGS);
|
|
508
|
-
|
|
509
|
-
if (statusFlags !== this.STATUS_FLAGS.OK) {
|
|
510
|
-
const status = {
|
|
511
|
-
bufferFull: !!(statusFlags & this.STATUS_FLAGS.BUFFER_FULL),
|
|
512
|
-
overrun: !!(statusFlags & this.STATUS_FLAGS.OVERRUN),
|
|
513
|
-
wasmError: !!(statusFlags & this.STATUS_FLAGS.WASM_ERROR),
|
|
514
|
-
fragmented: !!(statusFlags & this.STATUS_FLAGS.FRAGMENTED_MSG)
|
|
515
|
-
};
|
|
516
|
-
|
|
517
|
-
// Get current metrics
|
|
518
|
-
const metrics = {
|
|
519
|
-
processCount: Atomics.load(this.atomicView, this.METRICS_INDICES.PROCESS_COUNT),
|
|
520
|
-
messagesProcessed: Atomics.load(this.atomicView, this.METRICS_INDICES.MESSAGES_PROCESSED),
|
|
521
|
-
messagesDropped: Atomics.load(this.atomicView, this.METRICS_INDICES.MESSAGES_DROPPED),
|
|
522
|
-
bufferOverruns: Atomics.load(this.atomicView, this.METRICS_INDICES.BUFFER_OVERRUNS),
|
|
523
|
-
schedulerQueueDepth: Atomics.load(this.atomicView, this.METRICS_INDICES.SCHEDULER_QUEUE_DEPTH),
|
|
524
|
-
schedulerQueueMax: Atomics.load(this.atomicView, this.METRICS_INDICES.SCHEDULER_QUEUE_MAX),
|
|
525
|
-
schedulerQueueDropped: Atomics.load(this.atomicView, this.METRICS_INDICES.SCHEDULER_QUEUE_DROPPED)
|
|
526
|
-
};
|
|
527
|
-
|
|
528
|
-
this.port.postMessage({
|
|
529
|
-
type: 'status',
|
|
530
|
-
flags: statusFlags,
|
|
531
|
-
status: status,
|
|
532
|
-
metrics: metrics
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
// Clear non-persistent flags
|
|
536
|
-
const persistentFlags = statusFlags & (this.STATUS_FLAGS.BUFFER_FULL);
|
|
537
|
-
Atomics.store(this.atomicView, this.CONTROL_INDICES.STATUS_FLAGS, persistentFlags);
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
// Register the processor
|
|
543
|
-
registerProcessor('scsynth-processor', ScsynthProcessor);
|
|
1
|
+
(()=>{var u=class extends AudioWorkletProcessor{constructor(){super(),this.sharedBuffer=null,this.wasmModule=null,this.wasmInstance=null,this.isInitialized=!1,this.processCallCount=0,this.lastStatusCheck=0,this.ringBufferBase=null,this.audioView=null,this.lastAudioBufferPtr=0,this.lastWasmBufferSize=0,this.atomicView=null,this.uint8View=null,this.dataView=null,this.localClockOffsetView=null,this.bufferConstants=null,this.CONTROL_INDICES=null,this.METRICS_INDICES=null,this.STATUS_FLAGS={OK:0,BUFFER_FULL:1,OVERRUN:2,WASM_ERROR:4,FRAGMENTED_MSG:8},this.port.onmessage=this.handleMessage.bind(this)}loadBufferConstants(){if(!this.wasmInstance||!this.wasmInstance.exports.get_buffer_layout)throw new Error("WASM instance does not export get_buffer_layout");let e=this.wasmInstance.exports.get_buffer_layout(),t=this.wasmMemory;if(!t)throw new Error("WASM memory not available");let s=new Uint32Array(t.buffer,e,21),r=new Uint8Array(t.buffer,e,84);if(this.bufferConstants={IN_BUFFER_START:s[0],IN_BUFFER_SIZE:s[1],OUT_BUFFER_START:s[2],OUT_BUFFER_SIZE:s[3],DEBUG_BUFFER_START:s[4],DEBUG_BUFFER_SIZE:s[5],CONTROL_START:s[6],CONTROL_SIZE:s[7],METRICS_START:s[8],METRICS_SIZE:s[9],NTP_START_TIME_START:s[10],NTP_START_TIME_SIZE:s[11],DRIFT_OFFSET_START:s[12],DRIFT_OFFSET_SIZE:s[13],GLOBAL_OFFSET_START:s[14],GLOBAL_OFFSET_SIZE:s[15],TOTAL_BUFFER_SIZE:s[16],MAX_MESSAGE_SIZE:s[17],MESSAGE_MAGIC:s[18],PADDING_MAGIC:s[19],DEBUG_PADDING_MARKER:r[80],MESSAGE_HEADER_SIZE:16},this.bufferConstants.MESSAGE_MAGIC!==3735928559)throw new Error("Invalid buffer constants from WASM")}calculateBufferIndices(e){if(!this.bufferConstants)throw new Error("Buffer constants not loaded. Call loadBufferConstants() first.");let t=this.bufferConstants.CONTROL_START,s=this.bufferConstants.METRICS_START;this.CONTROL_INDICES={IN_HEAD:(e+t+0)/4,IN_TAIL:(e+t+4)/4,OUT_HEAD:(e+t+8)/4,OUT_TAIL:(e+t+12)/4,DEBUG_HEAD:(e+t+16)/4,DEBUG_TAIL:(e+t+20)/4,SEQUENCE:(e+t+24)/4,STATUS_FLAGS:(e+t+28)/4},this.METRICS_INDICES={PROCESS_COUNT:(e+s+0)/4,MESSAGES_PROCESSED:(e+s+4)/4,MESSAGES_DROPPED:(e+s+8)/4,SCHEDULER_QUEUE_DEPTH:(e+s+12)/4,SCHEDULER_QUEUE_MAX:(e+s+16)/4,SCHEDULER_QUEUE_DROPPED:(e+s+20)/4}}writeWorldOptionsToMemory(){if(!this.worldOptions||!this.wasmMemory)return;let e=this.ringBufferBase+65536,t=new Uint32Array(this.wasmMemory.buffer,e,32),s=new Float32Array(this.wasmMemory.buffer,e,32);t[0]=this.worldOptions.numBuffers||1024,t[1]=this.worldOptions.maxNodes||1024,t[2]=this.worldOptions.maxGraphDefs||1024,t[3]=this.worldOptions.maxWireBufs||64,t[4]=this.worldOptions.numAudioBusChannels||128,t[5]=this.worldOptions.numInputBusChannels||0,t[6]=this.worldOptions.numOutputBusChannels||2,t[7]=this.worldOptions.numControlBusChannels||4096,t[8]=this.worldOptions.bufLength||128,t[9]=this.worldOptions.realTimeMemorySize||16384,t[10]=this.worldOptions.numRGens||64,t[11]=this.worldOptions.realTime?1:0,t[12]=this.worldOptions.memoryLocking?1:0,t[13]=this.worldOptions.loadGraphDefs||0,t[14]=this.worldOptions.preferredSampleRate||0,t[15]=this.worldOptions.verbosity||0}js_debug(e){if(!(!this.uint8View||!this.atomicView||!this.CONTROL_INDICES||!this.ringBufferBase))try{let t=this.bufferConstants.DEBUG_BUFFER_START,s=this.bufferConstants.DEBUG_BUFFER_SIZE,r=this.bufferConstants.DEBUG_PADDING_MARKER,a="[JS] "+e+`
|
|
2
|
+
`,o=new TextEncoder().encode(a);if(o.length>s)return;let _=this.CONTROL_INDICES.DEBUG_HEAD,n=Atomics.load(this.atomicView,_),h=s-n,c=n;o.length>h&&(this.uint8View[this.ringBufferBase+t+n]=r,c=0);let l=this.ringBufferBase+t;for(let f=0;f<o.length;f++)this.uint8View[l+c+f]=o[f];let m=c+o.length;Atomics.store(this.atomicView,_,m)}catch{}}async handleMessage(e){let{data:t}=e;try{if(t.type==="init"&&t.sharedBuffer&&(this.sharedBuffer=t.sharedBuffer,this.atomicView=new Int32Array(this.sharedBuffer),this.uint8View=new Uint8Array(this.sharedBuffer),this.dataView=new DataView(this.sharedBuffer)),t.type==="loadWasm")if(t.wasmBytes){let s=t.wasmMemory;if(!s){this.port.postMessage({type:"error",error:"No WASM memory provided!"});return}this.wasmMemory=s,this.worldOptions=t.worldOptions||{},this.sampleRate=t.sampleRate||48e3;let r={env:{memory:s,emscripten_asm_const_double:()=>Date.now()*1e3,__syscall_getdents64:()=>0,__syscall_unlinkat:()=>0,_emscripten_init_main_thread_js:()=>{},_emscripten_thread_mailbox_await:()=>{},_emscripten_thread_set_strongref:()=>{},emscripten_exit_with_live_runtime:()=>{},_emscripten_receive_on_main_thread_js:()=>{},emscripten_check_blocking_allowed:()=>{},_emscripten_thread_cleanup:()=>{},emscripten_num_logical_cores:()=>1,_emscripten_notify_mailbox_postmessage:()=>{}},wasi_snapshot_preview1:{clock_time_get:(i,o,_)=>{let n=new DataView(s.buffer),h=BigInt(Math.floor(Date.now()*1e6));return n.setBigUint64(_,h,!0),0},environ_sizes_get:()=>0,environ_get:()=>0,fd_close:()=>0,fd_write:()=>0,fd_seek:()=>0,fd_read:()=>0,proc_exit:i=>{console.error("[AudioWorklet] WASM tried to exit with code:",i)}}},a=await WebAssembly.compile(t.wasmBytes);this.wasmInstance=await WebAssembly.instantiate(a,r),this.wasmInstance.exports.get_ring_buffer_base&&(this.ringBufferBase=this.wasmInstance.exports.get_ring_buffer_base(),this.loadBufferConstants(),this.calculateBufferIndices(this.ringBufferBase),this.writeWorldOptionsToMemory(),this.wasmInstance.exports.init_memory&&(this.wasmInstance.exports.init_memory(this.sampleRate),this.isInitialized=!0,this.port.postMessage({type:"initialized",success:!0,ringBufferBase:this.ringBufferBase,bufferConstants:this.bufferConstants,exports:Object.keys(this.wasmInstance.exports)})))}else t.wasmInstance&&(this.wasmInstance=t.wasmInstance,this.wasmInstance.exports.get_ring_buffer_base&&(this.ringBufferBase=this.wasmInstance.exports.get_ring_buffer_base(),this.loadBufferConstants(),this.calculateBufferIndices(this.ringBufferBase),this.writeWorldOptionsToMemory(),this.wasmInstance.exports.init_memory&&(this.wasmInstance.exports.init_memory(this.sampleRate),this.isInitialized=!0,this.port.postMessage({type:"initialized",success:!0,ringBufferBase:this.ringBufferBase,bufferConstants:this.bufferConstants,exports:Object.keys(this.wasmInstance.exports)}))));if(t.type==="getVersion")if(this.wasmInstance&&this.wasmInstance.exports.get_supersonic_version_string){let s=this.wasmInstance.exports.get_supersonic_version_string(),r=new Uint8Array(this.wasmMemory.buffer),a="";for(let i=s;r[i]!==0;i++)a+=String.fromCharCode(r[i]);this.port.postMessage({type:"version",version:a})}else this.port.postMessage({type:"version",version:"unknown"});if(t.type==="getTimeOffset")if(this.wasmInstance&&this.wasmInstance.exports.get_time_offset){let s=this.wasmInstance.exports.get_time_offset();this.port.postMessage({type:"timeOffset",offset:s})}else console.error("[AudioWorklet] get_time_offset not available! wasmInstance:",!!this.wasmInstance),this.port.postMessage({type:"error",error:"get_time_offset function not available in WASM exports"})}catch(s){console.error("[AudioWorklet] Error handling message:",s),this.port.postMessage({type:"error",error:s.message,stack:s.stack})}}process(e,t,s){if(!this.isInitialized)return!0;try{if(this.wasmInstance&&this.wasmInstance.exports.process_audio){let r=currentTime,a=this.wasmInstance.exports.process_audio(r);if(this.wasmInstance.exports.get_audio_output_bus&&t[0]&&t[0].length>=2)try{let i=this.wasmInstance.exports.get_audio_output_bus(),o=this.wasmInstance.exports.get_audio_buffer_samples();if(i&&i>0){let _=this.wasmInstance.exports.memory||this.wasmMemory;if(!_||!_.buffer)return!0;let n=_.buffer,h=n.byteLength,c=i+o*2*4;if(i<0||i>h||c>h)return!0;(!this.audioView||this.lastAudioBufferPtr!==i||this.lastWasmBufferSize!==h||n!==this.audioView.buffer)&&(this.audioView=new Float32Array(n,i,o*2),this.lastAudioBufferPtr=i,this.lastWasmBufferSize=h),t[0][0].set(this.audioView.subarray(0,o)),t[0][1].set(this.audioView.subarray(o,o*2))}}catch{}return this.atomicView&&this.processCallCount%16===0&&Atomics.notify(this.atomicView,this.CONTROL_INDICES.OUT_HEAD,1),this.processCallCount++,this.processCallCount%3750===0&&this.checkStatus(),a!==0}}catch(r){console.error("[AudioWorklet] process() error:",r),console.error("[AudioWorklet] Stack:",r.stack),this.atomicView&&Atomics.or(this.atomicView,this.CONTROL_INDICES.STATUS_FLAGS,this.STATUS_FLAGS.WASM_ERROR)}return!0}checkStatus(){if(!this.atomicView)return;let e=Atomics.load(this.atomicView,this.CONTROL_INDICES.STATUS_FLAGS);if(e!==this.STATUS_FLAGS.OK){let t={bufferFull:!!(e&this.STATUS_FLAGS.BUFFER_FULL),overrun:!!(e&this.STATUS_FLAGS.OVERRUN),wasmError:!!(e&this.STATUS_FLAGS.WASM_ERROR),fragmented:!!(e&this.STATUS_FLAGS.FRAGMENTED_MSG)},s={processCount:Atomics.load(this.atomicView,this.METRICS_INDICES.PROCESS_COUNT),messagesProcessed:Atomics.load(this.atomicView,this.METRICS_INDICES.MESSAGES_PROCESSED),messagesDropped:Atomics.load(this.atomicView,this.METRICS_INDICES.MESSAGES_DROPPED),schedulerQueueDepth:Atomics.load(this.atomicView,this.METRICS_INDICES.SCHEDULER_QUEUE_DEPTH),schedulerQueueMax:Atomics.load(this.atomicView,this.METRICS_INDICES.SCHEDULER_QUEUE_MAX),schedulerQueueDropped:Atomics.load(this.atomicView,this.METRICS_INDICES.SCHEDULER_QUEUE_DROPPED)};this.port.postMessage({type:"status",flags:e,status:t,metrics:s});let r=e&this.STATUS_FLAGS.BUFFER_FULL;Atomics.store(this.atomicView,this.CONTROL_INDICES.STATUS_FLAGS,r)}}};registerProcessor("scsynth-processor",u);})();
|
package/package.json
CHANGED