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