supersonic-scsynth 0.6.0 → 0.6.3

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.
@@ -1,543 +1,2 @@
1
- /*
2
- SuperSonic - SuperCollider AudioWorklet WebAssembly port
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 f=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,BUFFER_OVERRUNS:(e+s+4)/4,MESSAGES_PROCESSED:(e+s+8)/4,MESSAGES_DROPPED:(e+s+12)/4,SCHEDULER_QUEUE_DEPTH:(e+s+16)/4,SCHEDULER_QUEUE_MAX:(e+s+20)/4,SCHEDULER_QUEUE_DROPPED:(e+s+24)/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 u=0;u<o.length;u++)this.uint8View[l+c+u]=o[u];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),bufferOverruns:Atomics.load(this.atomicView,this.METRICS_INDICES.BUFFER_OVERRUNS),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",f);})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supersonic-scsynth",
3
- "version": "0.6.0",
3
+ "version": "0.6.3",
4
4
  "description": "SuperCollider scsynth WebAssembly port for AudioWorklet - Run SuperCollider synthesis in the browser",
5
5
  "main": "dist/supersonic.js",
6
6
  "unpkg": "dist/supersonic.js",