supersonic-scsynth 0.2.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,6 +11,8 @@ A WebAssembly port of SuperCollider's scsynth audio synthesis engine for the bro
11
11
  import { SuperSonic } from './dist/supersonic.js';
12
12
 
13
13
  const sonic = new SuperSonic({
14
+ workerBaseURL: './dist/workers/',
15
+ wasmBaseURL: './dist/wasm/',
14
16
  sampleBaseURL: './dist/samples/',
15
17
  synthdefBaseURL: './dist/synthdefs/'
16
18
  });
@@ -69,8 +71,10 @@ All synthdefs and samples are from [Sonic Pi](https://github.com/sonic-pi-net/so
69
71
  **Creating an instance:**
70
72
  ```javascript
71
73
  const sonic = new SuperSonic({
72
- sampleBaseURL: './dist/samples/',
73
- synthdefBaseURL: './dist/synthdefs/',
74
+ workerBaseURL: './dist/workers/', // Required: Path to worker files
75
+ wasmBaseURL: './dist/wasm/', // Required: Path to WASM files
76
+ sampleBaseURL: './dist/samples/', // Optional: Path to audio samples
77
+ synthdefBaseURL: './dist/synthdefs/', // Optional: Path to synthdefs
74
78
  audioPathMap: { /* optional custom path mappings */ }
75
79
  });
76
80
  ```
@@ -144,6 +148,8 @@ import { SuperSonic } from './dist/supersonic.js';
144
148
 
145
149
  // CDN-hosted synthdefs and samples work fine
146
150
  const sonic = new SuperSonic({
151
+ workerBaseURL: './dist/workers/', // Must be self-hosted
152
+ wasmBaseURL: './dist/wasm/', // Must be self-hosted
147
153
  sampleBaseURL: 'https://unpkg.com/supersonic-scsynth-samples@0.1.6/samples/',
148
154
  synthdefBaseURL: 'https://unpkg.com/supersonic-scsynth-synthdefs@0.1.6/synthdefs/'
149
155
  });
@@ -202,7 +208,7 @@ dist/
202
208
  └── debug_worker.js # Debug logger
203
209
  ```
204
210
 
205
- The engine expects these files at `./dist/` relative to your HTML. Paths are currently not configurable.
211
+ You must specify the paths to `workers/` and `wasm/` directories when creating a SuperSonic instance using the `workerBaseURL` and `wasmBaseURL` options.
206
212
 
207
213
  ## License
208
214
 
@@ -766,7 +766,8 @@ var { readPacket, writePacket, readMessage, writeMessage, readBundle, writeBundl
766
766
 
767
767
  // js/lib/scsynth_osc.js
768
768
  var ScsynthOSC = class {
769
- constructor() {
769
+ constructor(workerBaseURL = null) {
770
+ this.workerBaseURL = workerBaseURL;
770
771
  this.workers = {
771
772
  oscOut: null,
772
773
  // Scheduler worker (now also writes directly to ring buffer)
@@ -799,9 +800,9 @@ var ScsynthOSC = class {
799
800
  this.ringBufferBase = ringBufferBase;
800
801
  this.bufferConstants = bufferConstants;
801
802
  try {
802
- this.workers.oscOut = new Worker(new URL("./workers/osc_out_prescheduler_worker.js", import.meta.url), { type: "module" });
803
- this.workers.oscIn = new Worker(new URL("./workers/osc_in_worker.js", import.meta.url), { type: "module" });
804
- this.workers.debug = new Worker(new URL("./workers/debug_worker.js", import.meta.url), { type: "module" });
803
+ this.workers.oscOut = new Worker(this.workerBaseURL + "osc_out_prescheduler_worker.js", { type: "module" });
804
+ this.workers.oscIn = new Worker(this.workerBaseURL + "osc_in_worker.js", { type: "module" });
805
+ this.workers.debug = new Worker(this.workerBaseURL + "debug_worker.js", { type: "module" });
805
806
  this.setupWorkerHandlers();
806
807
  const initPromises = [
807
808
  this.initWorker(this.workers.oscOut, "OSC SCHEDULER+WRITER"),
@@ -1916,12 +1917,16 @@ var SuperSonic = class _SuperSonic {
1916
1917
  this.onDebugMessage = null;
1917
1918
  this.onInitialized = null;
1918
1919
  this.onError = null;
1919
- const moduleUrl = new URL(import.meta.url);
1920
- const basePath = new URL(".", moduleUrl).href;
1921
- this.basePath = basePath;
1920
+ if (!options.workerBaseURL || !options.wasmBaseURL) {
1921
+ throw new Error('SuperSonic requires workerBaseURL and wasmBaseURL options. Example:\nnew SuperSonic({\n workerBaseURL: "/supersonic/workers/",\n wasmBaseURL: "/supersonic/wasm/"\n})');
1922
+ }
1923
+ const workerBaseURL = options.workerBaseURL;
1924
+ const wasmBaseURL = options.wasmBaseURL;
1922
1925
  this.config = {
1923
- wasmUrl: new URL("wasm/scsynth-nrt.wasm", basePath).href,
1924
- workletUrl: new URL("workers/scsynth_audio_worklet.js", basePath).href,
1926
+ wasmUrl: options.wasmUrl || wasmBaseURL + "scsynth-nrt.wasm",
1927
+ workletUrl: options.workletUrl || workerBaseURL + "scsynth_audio_worklet.js",
1928
+ workerBaseURL,
1929
+ // Store for worker creation
1925
1930
  development: false,
1926
1931
  audioContextOptions: {
1927
1932
  latencyHint: "interactive",
@@ -2031,12 +2036,13 @@ var SuperSonic = class _SuperSonic {
2031
2036
  */
2032
2037
  async #loadWasmManifest() {
2033
2038
  try {
2034
- const manifestUrl = new URL("wasm/manifest.json", this.basePath).href;
2039
+ const wasmBaseURL = this.config.workerBaseURL.replace("/workers/", "/wasm/");
2040
+ const manifestUrl = wasmBaseURL + "manifest.json";
2035
2041
  const response = await fetch(manifestUrl);
2036
2042
  if (response.ok) {
2037
2043
  const manifest = await response.json();
2038
2044
  const wasmFile = manifest.wasmFile;
2039
- this.config.wasmUrl = new URL(`wasm/${wasmFile}`, this.basePath).href;
2045
+ this.config.wasmUrl = wasmBaseURL + wasmFile;
2040
2046
  console.log(`[SuperSonic] Using WASM build: ${wasmFile}`);
2041
2047
  console.log(`[SuperSonic] Build: ${manifest.buildId} (git: ${manifest.gitHash})`);
2042
2048
  }
@@ -2081,7 +2087,7 @@ var SuperSonic = class _SuperSonic {
2081
2087
  * Initialize OSC communication layer
2082
2088
  */
2083
2089
  async #initializeOSC() {
2084
- this.osc = new ScsynthOSC();
2090
+ this.osc = new ScsynthOSC(this.config.workerBaseURL);
2085
2091
  this.osc.onRawOSC((msg) => {
2086
2092
  if (this.onOSC) {
2087
2093
  this.onOSC(msg);
@@ -2092,6 +2098,12 @@ var SuperSonic = class _SuperSonic {
2092
2098
  this._handleBufferFreed(msg.args);
2093
2099
  } else if (msg.address === "/buffer/allocated") {
2094
2100
  this._handleBufferAllocated(msg.args);
2101
+ } else if (msg.address === "/synced" && msg.args.length > 0) {
2102
+ const syncId = msg.args[0];
2103
+ if (this._syncListeners && this._syncListeners.has(syncId)) {
2104
+ const listener = this._syncListeners.get(syncId);
2105
+ listener(msg);
2106
+ }
2095
2107
  }
2096
2108
  if (this.onMessage) {
2097
2109
  this.stats.messagesReceived++;
@@ -2506,7 +2518,7 @@ var SuperSonic = class _SuperSonic {
2506
2518
  if (synthName) {
2507
2519
  this.loadedSynthDefs.add(synthName);
2508
2520
  }
2509
- console.log(`[SuperSonic] Loaded synthdef from ${path} (${synthdefData.length} bytes)`);
2521
+ console.log(`[SuperSonic] Sent synthdef from ${path} (${synthdefData.length} bytes)`);
2510
2522
  } catch (error) {
2511
2523
  console.error("[SuperSonic] Failed to load synthdef:", error);
2512
2524
  throw error;
@@ -2542,9 +2554,45 @@ var SuperSonic = class _SuperSonic {
2542
2554
  })
2543
2555
  );
2544
2556
  const successCount = Object.values(results).filter((r) => r.success).length;
2545
- console.log(`[SuperSonic] Loaded ${successCount}/${names.length} synthdefs`);
2557
+ console.log(`[SuperSonic] Sent ${successCount}/${names.length} synthdef loads`);
2546
2558
  return results;
2547
2559
  }
2560
+ /**
2561
+ * Send /sync command and wait for /synced response
2562
+ * Use this to ensure all previous asynchronous commands have completed
2563
+ * @param {number} syncId - Unique integer identifier for this sync operation
2564
+ * @returns {Promise<void>}
2565
+ * @example
2566
+ * await sonic.loadSynthDefs(['synth1', 'synth2']);
2567
+ * await sonic.sync(12345); // Wait for all synthdefs to be processed
2568
+ */
2569
+ async sync(syncId) {
2570
+ if (!this.initialized) {
2571
+ throw new Error("SuperSonic not initialized. Call init() first.");
2572
+ }
2573
+ if (!Number.isInteger(syncId)) {
2574
+ throw new Error("sync() requires an integer syncId parameter");
2575
+ }
2576
+ const syncPromise = new Promise((resolve, reject) => {
2577
+ const timeout = setTimeout(() => {
2578
+ if (this._syncListeners) {
2579
+ this._syncListeners.delete(syncId);
2580
+ }
2581
+ reject(new Error("Timeout waiting for /synced response"));
2582
+ }, 1e4);
2583
+ const messageHandler = (msg) => {
2584
+ clearTimeout(timeout);
2585
+ this._syncListeners.delete(syncId);
2586
+ resolve();
2587
+ };
2588
+ if (!this._syncListeners) {
2589
+ this._syncListeners = /* @__PURE__ */ new Map();
2590
+ }
2591
+ this._syncListeners.set(syncId, messageHandler);
2592
+ });
2593
+ await this.send("/sync", syncId);
2594
+ await syncPromise;
2595
+ }
2548
2596
  /**
2549
2597
  * Allocate memory for an audio buffer (includes guard samples)
2550
2598
  * @param {number} numSamples - Number of Float32 samples to allocate
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "wasmFile": "scsynth-nrt.wasm",
3
- "buildId": "20251115-234621",
4
- "buildTime": "2025-11-15T23:46:21Z",
5
- "gitHash": "ec0ed48"
3
+ "buildId": "20251118-093603",
4
+ "buildTime": "2025-11-18T09:36:03Z",
5
+ "gitHash": "d43c4be"
6
6
  }
Binary file
@@ -24,6 +24,11 @@ var periodicTimer = null; // Single periodic timer (25ms interval)
24
24
  var sequenceCounter = 0;
25
25
  var isDispatching = false; // Prevent reentrancy into dispatch loop
26
26
 
27
+ // Retry queue for failed writes
28
+ var retryQueue = [];
29
+ var MAX_RETRY_QUEUE_SIZE = 100;
30
+ var MAX_RETRIES_PER_MESSAGE = 5;
31
+
27
32
  // Statistics
28
33
  var stats = {
29
34
  bundlesScheduled: 0,
@@ -38,7 +43,12 @@ var stats = {
38
43
  maxLateDispatchMs: 0,
39
44
  totalSendTasks: 0,
40
45
  totalSendProcessMs: 0,
41
- maxSendProcessMs: 0
46
+ maxSendProcessMs: 0,
47
+ messagesRetried: 0,
48
+ retriesSucceeded: 0,
49
+ retriesFailed: 0,
50
+ retryQueueSize: 0,
51
+ maxRetryQueueSize: 0
42
52
  };
43
53
 
44
54
  // Timing constants
@@ -123,8 +133,9 @@ function initSharedBuffer() {
123
133
  /**
124
134
  * Write OSC message directly to ring buffer (replaces MessagePort to writer worker)
125
135
  * This is now the ONLY place that writes to the ring buffer
136
+ * Returns true if successful, false if failed (caller should queue for retry)
126
137
  */
127
- function writeToRingBuffer(oscMessage) {
138
+ function writeToRingBuffer(oscMessage, isRetry) {
128
139
  if (!sharedBuffer || !atomicView) {
129
140
  console.error('[PreScheduler] Not initialized for ring buffer writing');
130
141
  stats.bundlesDropped++;
@@ -149,47 +160,77 @@ function writeToRingBuffer(oscMessage) {
149
160
  var available = (bufferConstants.IN_BUFFER_SIZE - 1 - head + tail) % bufferConstants.IN_BUFFER_SIZE;
150
161
 
151
162
  if (available < totalSize) {
152
- // Buffer full - drop message (prescheduler should not block)
163
+ // Buffer full - return false so caller can queue for retry
153
164
  stats.bufferOverruns++;
154
- stats.bundlesDropped++;
155
- console.warn('[PreScheduler] Ring buffer full, dropping message');
165
+ if (!isRetry) {
166
+ // Only increment bundlesDropped on initial attempt
167
+ // Retries increment different counters
168
+ stats.bundlesDropped++;
169
+ console.warn('[PreScheduler] Ring buffer full, message will be queued for retry');
170
+ }
156
171
  return false;
157
172
  }
158
173
 
159
- // Check if message fits contiguously
174
+ // ringbuf.js approach: split writes across wrap boundary
175
+ // No padding markers - just split the write into two parts if it wraps
176
+
160
177
  var spaceToEnd = bufferConstants.IN_BUFFER_SIZE - head;
161
178
 
162
179
  if (totalSize > spaceToEnd) {
180
+ // Message will wrap - write in two parts
181
+ // Create header as byte array to simplify split writes
182
+ var headerBytes = new Uint8Array(bufferConstants.MESSAGE_HEADER_SIZE);
183
+ var headerView = new DataView(headerBytes.buffer);
184
+ headerView.setUint32(0, bufferConstants.MESSAGE_MAGIC, true);
185
+ headerView.setUint32(4, totalSize, true);
186
+ headerView.setUint32(8, stats.bundlesWritten, true);
187
+ headerView.setUint32(12, 0, true);
188
+
189
+ var writePos1 = ringBufferBase + bufferConstants.IN_BUFFER_START + head;
190
+ var writePos2 = ringBufferBase + bufferConstants.IN_BUFFER_START;
191
+
192
+ // Write header (may be split)
163
193
  if (spaceToEnd >= bufferConstants.MESSAGE_HEADER_SIZE) {
164
- // Write padding marker and wrap
165
- var paddingPos = ringBufferBase + bufferConstants.IN_BUFFER_START + head;
166
- dataView.setUint32(paddingPos, bufferConstants.PADDING_MAGIC, true);
167
- dataView.setUint32(paddingPos + 4, 0, true);
168
- dataView.setUint32(paddingPos + 8, 0, true);
169
- dataView.setUint32(paddingPos + 12, 0, true);
170
- } else if (spaceToEnd > 0) {
171
- // Not enough room for a padding header - clear remaining bytes
172
- var padStart = ringBufferBase + bufferConstants.IN_BUFFER_START + head;
173
- for (var i = 0; i < spaceToEnd; i++) {
174
- uint8View[padStart + i] = 0;
175
- }
194
+ // Header fits contiguously
195
+ uint8View.set(headerBytes, writePos1);
196
+
197
+ // Write payload (split across boundary)
198
+ var payloadBytesInFirstPart = spaceToEnd - bufferConstants.MESSAGE_HEADER_SIZE;
199
+ uint8View.set(oscMessage.subarray(0, payloadBytesInFirstPart), writePos1 + bufferConstants.MESSAGE_HEADER_SIZE);
200
+ uint8View.set(oscMessage.subarray(payloadBytesInFirstPart), writePos2);
201
+ } else {
202
+ // Header is split across boundary
203
+ uint8View.set(headerBytes.subarray(0, spaceToEnd), writePos1);
204
+ uint8View.set(headerBytes.subarray(spaceToEnd), writePos2);
205
+
206
+ // All payload goes at beginning
207
+ var payloadOffset = bufferConstants.MESSAGE_HEADER_SIZE - spaceToEnd;
208
+ uint8View.set(oscMessage, writePos2 + payloadOffset);
176
209
  }
210
+ } else {
211
+ // Message fits contiguously - write normally
212
+ var writePos = ringBufferBase + bufferConstants.IN_BUFFER_START + head;
177
213
 
178
- // Wrap to beginning
179
- head = 0;
180
- }
214
+ // Write header
215
+ dataView.setUint32(writePos, bufferConstants.MESSAGE_MAGIC, true);
216
+ dataView.setUint32(writePos + 4, totalSize, true);
217
+ dataView.setUint32(writePos + 8, stats.bundlesWritten, true);
218
+ dataView.setUint32(writePos + 12, 0, true);
181
219
 
182
- // Write message
183
- var writePos = ringBufferBase + bufferConstants.IN_BUFFER_START + head;
220
+ // Write payload
221
+ uint8View.set(oscMessage, writePos + bufferConstants.MESSAGE_HEADER_SIZE);
222
+ }
184
223
 
185
- // Write header
186
- dataView.setUint32(writePos, bufferConstants.MESSAGE_MAGIC, true);
187
- dataView.setUint32(writePos + 4, totalSize, true);
188
- dataView.setUint32(writePos + 8, stats.bundlesWritten, true); // sequence
189
- dataView.setUint32(writePos + 12, 0, true); // padding
224
+ // Diagnostic: Log first few writes
225
+ if (stats.bundlesWritten < 5) {
226
+ schedulerLog('[PreScheduler] Write:', 'seq=' + stats.bundlesWritten,
227
+ 'pos=' + head, 'size=' + totalSize, 'newHead=' + ((head + totalSize) % bufferConstants.IN_BUFFER_SIZE));
228
+ }
190
229
 
191
- // Write payload
192
- uint8View.set(oscMessage, writePos + bufferConstants.MESSAGE_HEADER_SIZE);
230
+ // CRITICAL: Ensure memory barrier before publishing head pointer
231
+ // All previous writes (header + payload) must be visible to C++ reader
232
+ // Atomics.load provides necessary memory fence/barrier
233
+ Atomics.load(atomicView, CONTROL_INDICES.IN_HEAD);
193
234
 
194
235
  // Update head pointer (publish message)
195
236
  var newHead = (head + totalSize) % bufferConstants.IN_BUFFER_SIZE;
@@ -199,6 +240,77 @@ function writeToRingBuffer(oscMessage) {
199
240
  return true;
200
241
  }
201
242
 
243
+ /**
244
+ * Add a message to the retry queue
245
+ */
246
+ function queueForRetry(oscData, context) {
247
+ if (retryQueue.length >= MAX_RETRY_QUEUE_SIZE) {
248
+ console.error('[PreScheduler] Retry queue full, dropping message permanently');
249
+ stats.retriesFailed++;
250
+ return;
251
+ }
252
+
253
+ retryQueue.push({
254
+ oscData: oscData,
255
+ retryCount: 0,
256
+ context: context || 'unknown',
257
+ queuedAt: performance.now()
258
+ });
259
+
260
+ stats.retryQueueSize = retryQueue.length;
261
+ if (stats.retryQueueSize > stats.maxRetryQueueSize) {
262
+ stats.maxRetryQueueSize = stats.retryQueueSize;
263
+ }
264
+
265
+ schedulerLog('[PreScheduler] Queued message for retry:', context, 'queue size:', retryQueue.length);
266
+ }
267
+
268
+ /**
269
+ * Attempt to retry queued messages
270
+ * Called periodically from checkAndDispatch
271
+ */
272
+ function processRetryQueue() {
273
+ if (retryQueue.length === 0) {
274
+ return;
275
+ }
276
+
277
+ var i = 0;
278
+ while (i < retryQueue.length) {
279
+ var item = retryQueue[i];
280
+
281
+ // Try to write
282
+ var success = writeToRingBuffer(item.oscData, true);
283
+
284
+ if (success) {
285
+ // Success - remove from queue
286
+ retryQueue.splice(i, 1);
287
+ stats.retriesSucceeded++;
288
+ stats.messagesRetried++;
289
+ stats.retryQueueSize = retryQueue.length;
290
+ schedulerLog('[PreScheduler] Retry succeeded for:', item.context,
291
+ 'after', item.retryCount + 1, 'attempts');
292
+ // Don't increment i - we removed an item
293
+ } else {
294
+ // Failed - increment retry count
295
+ item.retryCount++;
296
+ stats.messagesRetried++;
297
+
298
+ if (item.retryCount >= MAX_RETRIES_PER_MESSAGE) {
299
+ // Give up on this message
300
+ console.error('[PreScheduler] Giving up on message after',
301
+ MAX_RETRIES_PER_MESSAGE, 'retries:', item.context);
302
+ retryQueue.splice(i, 1);
303
+ stats.retriesFailed++;
304
+ stats.retryQueueSize = retryQueue.length;
305
+ // Don't increment i - we removed an item
306
+ } else {
307
+ // Keep in queue, try again next cycle
308
+ i++;
309
+ }
310
+ }
311
+ }
312
+ }
313
+
202
314
  /**
203
315
  * Schedule an OSC bundle by its NTP timestamp
204
316
  * Non-bundles or bundles without timestamps are dispatched immediately
@@ -209,7 +321,11 @@ function scheduleEvent(oscData, editorId, runTag) {
209
321
  if (ntpTime === null) {
210
322
  // Not a bundle - dispatch immediately to ring buffer
211
323
  schedulerLog('[PreScheduler] Non-bundle message, dispatching immediately');
212
- writeToRingBuffer(oscData);
324
+ var success = writeToRingBuffer(oscData, false);
325
+ if (!success) {
326
+ // Queue for retry
327
+ queueForRetry(oscData, 'immediate message');
328
+ }
213
329
  return;
214
330
  }
215
331
 
@@ -338,6 +454,9 @@ function stopPeriodicPolling() {
338
454
  function checkAndDispatch() {
339
455
  isDispatching = true;
340
456
 
457
+ // First, try to process any queued retries
458
+ processRetryQueue();
459
+
341
460
  var currentNTP = getCurrentNTP();
342
461
  var lookaheadTime = currentNTP + LOOKAHEAD_S;
343
462
  var dispatchCount = 0;
@@ -361,7 +480,11 @@ function checkAndDispatch() {
361
480
  'early=' + (timeUntilExec * 1000).toFixed(1) + 'ms',
362
481
  'remaining=' + stats.eventsPending);
363
482
 
364
- writeToRingBuffer(nextEvent.oscData);
483
+ var success = writeToRingBuffer(nextEvent.oscData, false);
484
+ if (!success) {
485
+ // Queue for retry
486
+ queueForRetry(nextEvent.oscData, 'scheduled bundle NTP=' + nextEvent.ntpTime.toFixed(3));
487
+ }
365
488
  dispatchCount++;
366
489
  } else {
367
490
  // Rest aren't ready yet (heap is sorted)
@@ -369,10 +492,11 @@ function checkAndDispatch() {
369
492
  }
370
493
  }
371
494
 
372
- if (dispatchCount > 0 || eventHeap.length > 0) {
495
+ if (dispatchCount > 0 || eventHeap.length > 0 || retryQueue.length > 0) {
373
496
  schedulerLog('[PreScheduler] Dispatch cycle complete:',
374
497
  'dispatched=' + dispatchCount,
375
- 'pending=' + eventHeap.length);
498
+ 'pending=' + eventHeap.length,
499
+ 'retrying=' + retryQueue.length);
376
500
  }
377
501
 
378
502
  isDispatching = false;
@@ -474,10 +598,16 @@ function processImmediate(oscData) {
474
598
  if (isBundle(oscData)) {
475
599
  var messages = extractMessagesFromBundle(oscData);
476
600
  for (var i = 0; i < messages.length; i++) {
477
- writeToRingBuffer(messages[i]);
601
+ var success = writeToRingBuffer(messages[i], false);
602
+ if (!success) {
603
+ queueForRetry(messages[i], 'immediate bundle message ' + i);
604
+ }
478
605
  }
479
606
  } else {
480
- writeToRingBuffer(oscData);
607
+ var success = writeToRingBuffer(oscData, false);
608
+ if (!success) {
609
+ queueForRetry(oscData, 'immediate message');
610
+ }
481
611
  }
482
612
  }
483
613
 
@@ -555,7 +685,12 @@ self.addEventListener('message', function(event) {
555
685
  maxLateDispatchMs: stats.maxLateDispatchMs,
556
686
  totalSendTasks: stats.totalSendTasks,
557
687
  totalSendProcessMs: stats.totalSendProcessMs,
558
- maxSendProcessMs: stats.maxSendProcessMs
688
+ maxSendProcessMs: stats.maxSendProcessMs,
689
+ messagesRetried: stats.messagesRetried,
690
+ retriesSucceeded: stats.retriesSucceeded,
691
+ retriesFailed: stats.retriesFailed,
692
+ retryQueueSize: stats.retryQueueSize,
693
+ maxRetryQueueSize: stats.maxRetryQueueSize
559
694
  }
560
695
  });
561
696
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supersonic-scsynth",
3
- "version": "0.2.3",
3
+ "version": "0.4.0",
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",
@@ -1,382 +0,0 @@
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
- * OSC OUT Worker - Scheduler for OSC bundles
11
- * Handles timed bundles and forwards them to the writer worker
12
- * NO LONGER writes to ring buffer directly - all writes go through osc_writer_worker.js
13
- * ES5-compatible for Qt WebEngine
14
- */
15
-
16
- // Reference to the writer worker
17
- var writerWorker = null;
18
-
19
- // Scheduling state
20
- var scheduledEvents = [];
21
- var currentTimer = null;
22
- var cachedTimeDelta = null;
23
- var minimumScheduleRequirementS = 0.002; // 2ms for audio precision
24
- var latencyS = 0.05; // 50ms latency compensation for scsynth
25
-
26
- var DEBUG_SCHED_LOGS = false;
27
- function schedulerLog() {
28
- if (DEBUG_SCHED_LOGS) {
29
- console.log.apply(console, arguments);
30
- }
31
- }
32
- function schedulerWarn() {
33
- if (DEBUG_SCHED_LOGS) {
34
- console.warn.apply(console, arguments);
35
- }
36
- }
37
-
38
- // Statistics
39
- var stats = {
40
- bundlesScheduled: 0,
41
- bundlesSentToWriter: 0
42
- };
43
-
44
- /**
45
- * Initialize scheduler
46
- */
47
- function init(buffer, base, constants) {
48
- // We don't need the ring buffer anymore, but keep the params for compatibility
49
- schedulerLog('[OSCSchedulerWorker] Initialized (scheduler only, no ring buffer access)');
50
- }
51
-
52
- /**
53
- * Send message to writer worker
54
- */
55
- function sendToWriter(oscMessage) {
56
- if (!writerWorker) {
57
- console.error('[OSCSchedulerWorker] Writer worker not set');
58
- return;
59
- }
60
-
61
- // Send to writer worker via postMessage
62
- writerWorker.postMessage({
63
- type: 'write',
64
- oscData: oscMessage
65
- });
66
-
67
- stats.bundlesSentToWriter++;
68
- }
69
-
70
- /**
71
- * Get or set cached time delta for synchronization
72
- */
73
- function getOrSetTimeDelta(delta) {
74
- if (cachedTimeDelta === null) {
75
- cachedTimeDelta = delta;
76
- }
77
- return cachedTimeDelta;
78
- }
79
-
80
- /**
81
- * Check if data is an OSC bundle (starts with "#bundle\0")
82
- */
83
- function isBundle(data) {
84
- if (data.length < 16) return false;
85
- var bundleTag = String.fromCharCode.apply(null, data.slice(0, 8));
86
- return bundleTag === '#bundle\0';
87
- }
88
-
89
- /**
90
- * Parse OSC bundle timestamp from binary data
91
- * OSC bundles start with "#bundle\0" followed by 8-byte NTP timestamp
92
- */
93
- function parseBundleTimestamp(data) {
94
- if (!isBundle(data)) return null;
95
-
96
- // Read NTP timestamp (8 bytes, big-endian)
97
- var view = new DataView(data.buffer, data.byteOffset + 8, 8);
98
- var seconds = view.getUint32(0, false); // NTP seconds since 1900
99
- var fraction = view.getUint32(4, false); // NTP fractional seconds
100
-
101
- // Convert NTP to JavaScript time
102
- // NTP epoch is 1900, JS epoch is 1970 (difference: 2208988800 seconds)
103
- var NTP_TO_UNIX = 2208988800;
104
-
105
- // Special OSC timestamps
106
- if (seconds === 0 && fraction === 1) {
107
- return 0; // Immediate execution
108
- }
109
-
110
- // Convert to JavaScript timestamp (milliseconds since 1970)
111
- var unixSeconds = seconds - NTP_TO_UNIX;
112
- var milliseconds = (fraction / 4294967296) * 1000; // Convert fraction to ms
113
-
114
- return (unixSeconds * 1000) + milliseconds;
115
- }
116
-
117
- /**
118
- * Extract OSC messages from a bundle
119
- * Returns array of message buffers
120
- */
121
- function extractMessagesFromBundle(data) {
122
- var messages = [];
123
-
124
- if (!isBundle(data)) {
125
- // Not a bundle, return as single message
126
- return [data];
127
- }
128
-
129
- // Skip "#bundle\0" (8 bytes) and timestamp (8 bytes)
130
- var offset = 16;
131
-
132
- while (offset < data.length) {
133
- // Read message size (4 bytes, big-endian)
134
- if (offset + 4 > data.length) break;
135
-
136
- var view = new DataView(data.buffer, data.byteOffset + offset, 4);
137
- var messageSize = view.getInt32(0, false);
138
- offset += 4;
139
-
140
- if (messageSize <= 0 || offset + messageSize > data.length) break;
141
-
142
- // Extract message data
143
- var messageData = data.slice(offset, offset + messageSize);
144
-
145
- // Check if this is a nested bundle
146
- if (isBundle(messageData)) {
147
- // Recursively extract from nested bundle
148
- var nestedMessages = extractMessagesFromBundle(messageData);
149
- messages = messages.concat(nestedMessages);
150
- } else {
151
- // It's a message, add it
152
- messages.push(messageData);
153
- }
154
-
155
- offset += messageSize;
156
-
157
- // Align to 4-byte boundary if needed
158
- while (offset % 4 !== 0 && offset < data.length) {
159
- offset++;
160
- }
161
- }
162
-
163
- return messages;
164
- }
165
-
166
- /**
167
- * Process incoming OSC data (message or bundle)
168
- * Pre-scheduler: waits for calculated time then sends to writer
169
- * waitTimeMs is calculated by SuperSonic based on AudioContext time
170
- */
171
- function processOSC(oscData, editorId, runTag, waitTimeMs) {
172
- stats.bundlesScheduled++;
173
-
174
- // If no wait time provided, or wait time is 0 or negative, send immediately
175
- if (waitTimeMs === null || waitTimeMs === undefined || waitTimeMs <= 0) {
176
- sendToWriter(oscData);
177
- return;
178
- }
179
-
180
- // Schedule to send after waitTimeMs
181
- setTimeout(function() {
182
- sendToWriter(oscData);
183
- }, waitTimeMs);
184
- }
185
-
186
- /**
187
- * Process immediate send - forces immediate execution by unpacking bundles
188
- * Bundles are unpacked to individual messages (stripping timestamps)
189
- * Messages are sent as-is
190
- * Used when the caller wants immediate execution without scheduling
191
- */
192
- function processImmediate(oscData) {
193
- if (isBundle(oscData)) {
194
- // Extract all messages from the bundle (removes timestamp wrapper)
195
- // Send each message individually for immediate execution
196
- var messages = extractMessagesFromBundle(oscData);
197
- for (var i = 0; i < messages.length; i++) {
198
- sendToWriter(messages[i]);
199
- }
200
- } else {
201
- // Regular message - send as-is
202
- sendToWriter(oscData);
203
- }
204
- }
205
-
206
- /**
207
- * Insert event into priority queue
208
- */
209
- function insertEvent(userId, editorId, runId, runTag, adjustedTimeS, oscBundle) {
210
- var info = { userId: userId, editorId: editorId, runTag: runTag, runId: runId };
211
- scheduledEvents.push([adjustedTimeS, info, oscBundle]);
212
- scheduledEvents.sort(function(a, b) { return a[0] - b[0]; });
213
- scheduleNextEvent();
214
- }
215
-
216
- /**
217
- * Schedule the next event timer
218
- */
219
- function scheduleNextEvent() {
220
- if (scheduledEvents.length === 0) {
221
- clearCurrentTimer();
222
- return;
223
- }
224
-
225
- var nextEvent = scheduledEvents[0];
226
- var adjustedTimeS = nextEvent[0];
227
-
228
- if (!currentTimer || (currentTimer && currentTimer.timeS > adjustedTimeS)) {
229
- addRunNextEventTimer(adjustedTimeS);
230
- }
231
- }
232
-
233
- /**
234
- * Clear current timer
235
- */
236
- function clearCurrentTimer() {
237
- if (currentTimer) {
238
- clearTimeout(currentTimer.timerId);
239
- currentTimer = null;
240
- }
241
- }
242
-
243
- /**
244
- * Add timer for next event
245
- */
246
- function addRunNextEventTimer(adjustedTimeS) {
247
- clearCurrentTimer();
248
-
249
- var nowS = Date.now() / 1000;
250
- var timeDeltaS = adjustedTimeS - nowS;
251
-
252
- if (timeDeltaS <= minimumScheduleRequirementS) {
253
- runNextEvent();
254
- } else {
255
- var delayMs = (timeDeltaS - minimumScheduleRequirementS) * 1000;
256
- currentTimer = {
257
- timeS: adjustedTimeS,
258
- timerId: setTimeout(function() {
259
- currentTimer = null;
260
- runNextEvent();
261
- }, delayMs)
262
- };
263
- }
264
- }
265
-
266
- /**
267
- * Run the next scheduled event
268
- */
269
- function runNextEvent() {
270
- clearCurrentTimer();
271
-
272
- if (scheduledEvents.length === 0) {
273
- return;
274
- }
275
-
276
- var event = scheduledEvents.shift();
277
- var data = event[2];
278
-
279
- // Send the complete bundle unchanged (with original timestamp)
280
- sendToWriter(data);
281
-
282
- scheduleNextEvent();
283
- }
284
-
285
- /**
286
- * Cancel events by editor and tag
287
- */
288
- function cancelEditorTag(editorId, runTag) {
289
- scheduledEvents = scheduledEvents.filter(function(e) {
290
- return e[1].runTag !== runTag || e[1].editorId !== editorId;
291
- });
292
- scheduleNextEvent();
293
- }
294
-
295
- /**
296
- * Cancel all events from an editor
297
- */
298
- function cancelEditor(editorId) {
299
- scheduledEvents = scheduledEvents.filter(function(e) {
300
- return e[1].editorId !== editorId;
301
- });
302
- scheduleNextEvent();
303
- }
304
-
305
- /**
306
- * Cancel all scheduled events
307
- */
308
- function cancelAllTags() {
309
- scheduledEvents = [];
310
- clearCurrentTimer();
311
- }
312
-
313
- /**
314
- * Reset time delta for resync
315
- */
316
- function resetTimeDelta() {
317
- cachedTimeDelta = null;
318
- }
319
-
320
- /**
321
- * Handle messages from main thread
322
- */
323
- self.onmessage = function(event) {
324
- var data = event.data;
325
-
326
- try {
327
- switch (data.type) {
328
- case 'init':
329
- init(data.sharedBuffer, data.ringBufferBase, data.bufferConstants);
330
- self.postMessage({ type: 'initialized' });
331
- break;
332
-
333
- case 'setWriterWorker':
334
- // Set reference to writer worker (passed as MessagePort)
335
- writerWorker = data.port;
336
- schedulerLog('[OSCSchedulerWorker] Writer worker connected');
337
- break;
338
-
339
- case 'send':
340
- // Single send method for both messages and bundles
341
- // waitTimeMs is calculated by SuperSonic based on AudioContext time
342
- processOSC(data.oscData, data.editorId, data.runTag, data.waitTimeMs);
343
- break;
344
-
345
- case 'sendImmediate':
346
- // Force immediate send, extracting all messages from bundles
347
- // Ignores timestamps - for apps that don't expect scheduling
348
- processImmediate(data.oscData);
349
- break;
350
-
351
- case 'cancelEditorTag':
352
- cancelEditorTag(data.editorId, data.runTag);
353
- break;
354
-
355
- case 'cancelEditor':
356
- cancelEditor(data.editorId);
357
- break;
358
-
359
- case 'cancelAll':
360
- cancelAllTags();
361
- break;
362
-
363
- case 'getStats':
364
- self.postMessage({
365
- type: 'stats',
366
- stats: stats
367
- });
368
- break;
369
-
370
- default:
371
- schedulerWarn('[OSCOutWorker] Unknown message type:', data.type);
372
- }
373
- } catch (error) {
374
- console.error('[OSCOutWorker] Error:', error);
375
- self.postMessage({
376
- type: 'error',
377
- error: error.message
378
- });
379
- }
380
- };
381
-
382
- schedulerLog('[OSCSchedulerWorker] Script loaded - scheduler only, delegates to writer worker');
@@ -1,305 +0,0 @@
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
- * Base class for ring buffer workers
11
- * Provides common functionality for polling ring buffers with Atomics.wait()
12
- * ES5-compatible for Qt WebEngine
13
- */
14
-
15
- // Ring buffer configuration
16
- var RingBufferWorkerBase = function(bufferType) {
17
- this.bufferType = bufferType; // 'OUT', 'SYSTEM', etc.
18
- this.sharedBuffer = null;
19
- this.ringBufferBase = null;
20
- this.atomicView = null;
21
- this.dataView = null;
22
- this.uint8View = null;
23
- this.bufferConstants = null;
24
- this.CONTROL_INDICES = {};
25
- this.running = false;
26
- this.loggedCorruptionState = false;
27
-
28
- // Statistics
29
- this.stats = {
30
- messagesReceived: 0,
31
- lastSequenceReceived: -1,
32
- droppedMessages: 0,
33
- wakeups: 0,
34
- timeouts: 0
35
- };
36
- };
37
-
38
- /**
39
- * Initialize ring buffer access
40
- */
41
- RingBufferWorkerBase.prototype.initRingBuffer = function(buffer, base, constants) {
42
- this.sharedBuffer = buffer;
43
- this.ringBufferBase = base;
44
- this.bufferConstants = constants;
45
- this.atomicView = new Int32Array(this.sharedBuffer);
46
- this.dataView = new DataView(this.sharedBuffer);
47
- this.uint8View = new Uint8Array(this.sharedBuffer);
48
-
49
- // Calculate control indices based on buffer type
50
- var headOffset, tailOffset;
51
- if (this.bufferType === 'OUT') {
52
- headOffset = 8;
53
- tailOffset = 12;
54
- } else if (this.bufferType === 'SYSTEM') {
55
- headOffset = 24; // system_head offset
56
- tailOffset = 28; // system_tail offset
57
- }
58
-
59
- // Debug logging for SYSTEM worker
60
- if (this.bufferType === 'SYSTEM') {
61
- console.log('[' + this.bufferType + 'Worker] Init params:', {
62
- ringBufferBase: base,
63
- CONTROL_START: constants.CONTROL_START,
64
- headOffset: headOffset,
65
- tailOffset: tailOffset,
66
- calculatedHeadByteOffset: base + constants.CONTROL_START + headOffset,
67
- calculatedHeadIndex: (base + constants.CONTROL_START + headOffset) / 4
68
- });
69
- console.log('[' + this.bufferType + 'Worker] Buffer constants:', {
70
- SYSTEM_BUFFER_START: constants.SYSTEM_BUFFER_START,
71
- SYSTEM_BUFFER_SIZE: constants.SYSTEM_BUFFER_SIZE,
72
- MESSAGE_MAGIC: constants.MESSAGE_MAGIC,
73
- PADDING_MAGIC: constants.PADDING_MAGIC
74
- });
75
- }
76
-
77
- this.CONTROL_INDICES = {
78
- HEAD: (this.ringBufferBase + this.bufferConstants.CONTROL_START + headOffset) / 4,
79
- TAIL: (this.ringBufferBase + this.bufferConstants.CONTROL_START + tailOffset) / 4
80
- };
81
-
82
- // Debug: log buffer info for SYSTEM
83
- if (this.bufferType === 'SYSTEM') {
84
- var bufferInfo = this.getBufferInfo();
85
- console.log('[' + this.bufferType + 'Worker] Buffer info:', bufferInfo);
86
- console.log('[' + this.bufferType + 'Worker] Control indices:', this.CONTROL_INDICES);
87
-
88
- // Check initial buffer state - read first 64 bytes
89
- var startPos = this.ringBufferBase + bufferInfo.start;
90
- console.log('[' + this.bufferType + 'Worker] First 64 bytes of buffer at position', startPos + ':');
91
- for (var i = 0; i < 64; i += 4) {
92
- var value = this.dataView.getUint32(startPos + i, true);
93
- console.log(' Offset', i + ':', '0x' + value.toString(16).padStart(8, '0'));
94
- }
95
-
96
- // Check current head/tail values
97
- var currentHead = Atomics.load(this.atomicView, this.CONTROL_INDICES.HEAD);
98
- var currentTail = Atomics.load(this.atomicView, this.CONTROL_INDICES.TAIL);
99
- console.log('[' + this.bufferType + 'Worker] Initial head:', currentHead, 'tail:', currentTail);
100
- }
101
- };
102
-
103
- /**
104
- * Get buffer start and size based on type
105
- */
106
- RingBufferWorkerBase.prototype.getBufferInfo = function() {
107
- if (this.bufferType === 'OUT') {
108
- return {
109
- start: this.bufferConstants.OUT_BUFFER_START,
110
- size: this.bufferConstants.OUT_BUFFER_SIZE
111
- };
112
- } else if (this.bufferType === 'SYSTEM') {
113
- return {
114
- start: this.bufferConstants.SYSTEM_BUFFER_START,
115
- size: this.bufferConstants.SYSTEM_BUFFER_SIZE
116
- };
117
- }
118
- return null;
119
- };
120
-
121
- /**
122
- * Read all available messages from buffer
123
- */
124
- RingBufferWorkerBase.prototype.readMessages = function() {
125
- var head = Atomics.load(this.atomicView, this.CONTROL_INDICES.HEAD);
126
- var tail = Atomics.load(this.atomicView, this.CONTROL_INDICES.TAIL);
127
-
128
- var messages = [];
129
-
130
- if (head === tail) {
131
- return messages; // No messages
132
- }
133
-
134
- var bufferInfo = this.getBufferInfo();
135
- var currentTail = tail;
136
- var messagesRead = 0;
137
- var maxMessages = 100;
138
-
139
- while (currentTail !== head && messagesRead < maxMessages) {
140
- var readPos = this.ringBufferBase + bufferInfo.start + currentTail;
141
-
142
- // Read message header (now always contiguous due to padding)
143
- var magic = this.dataView.getUint32(readPos, true);
144
-
145
- // Check for padding marker - skip to beginning
146
- if (magic === this.bufferConstants.PADDING_MAGIC) {
147
- currentTail = 0;
148
- continue;
149
- }
150
-
151
- if (magic !== this.bufferConstants.MESSAGE_MAGIC) {
152
- // Only log first few corrupted messages to avoid spamming
153
- if (this.stats.droppedMessages < 5) {
154
- console.error('[' + this.bufferType + 'Worker] Corrupted message at position', currentTail, 'magic:', magic, 'expected:', this.bufferConstants.MESSAGE_MAGIC);
155
- }
156
- this.stats.droppedMessages++;
157
- // Skip this byte and continue
158
- currentTail = (currentTail + 1) % bufferInfo.size;
159
- continue;
160
- }
161
-
162
- var length = this.dataView.getUint32(readPos + 4, true);
163
- var sequence = this.dataView.getUint32(readPos + 8, true);
164
- var padding = this.dataView.getUint32(readPos + 12, true); // unused padding field
165
-
166
- // Validate message length
167
- if (length < this.bufferConstants.MESSAGE_HEADER_SIZE || length > bufferInfo.size) {
168
- // Only log first few invalid lengths to avoid spamming
169
- if (this.stats.droppedMessages < 5) {
170
- console.error('[' + this.bufferType + 'Worker] Invalid message length:', length);
171
- }
172
- this.stats.droppedMessages++;
173
- currentTail = (currentTail + 1) % bufferInfo.size;
174
- continue;
175
- }
176
-
177
- // Check for dropped messages via sequence
178
- if (this.stats.lastSequenceReceived >= 0) {
179
- var expectedSeq = (this.stats.lastSequenceReceived + 1) & 0xFFFFFFFF;
180
- if (sequence !== expectedSeq) {
181
- var dropped = (sequence - expectedSeq + 0x100000000) & 0xFFFFFFFF;
182
- if (dropped < 1000) { // Sanity check
183
- console.warn('[' + this.bufferType + 'Worker] Detected', dropped, 'dropped messages');
184
- this.stats.droppedMessages += dropped;
185
- }
186
- }
187
- }
188
- this.stats.lastSequenceReceived = sequence;
189
-
190
- // Read payload (OSC binary data) - now contiguous due to padding
191
- var payloadLength = length - this.bufferConstants.MESSAGE_HEADER_SIZE;
192
- var payloadStart = readPos + this.bufferConstants.MESSAGE_HEADER_SIZE;
193
-
194
- // Create a proper copy (not a view into SharedArrayBuffer)
195
- var payload = new Uint8Array(payloadLength);
196
- for (var i = 0; i < payloadLength; i++) {
197
- payload[i] = this.uint8View[payloadStart + i];
198
- }
199
-
200
- messages.push({
201
- oscData: payload,
202
- sequence: sequence
203
- });
204
-
205
- // Move to next message
206
- currentTail = (currentTail + length) % bufferInfo.size;
207
- messagesRead++;
208
- this.stats.messagesReceived++;
209
- }
210
-
211
- // Update tail pointer (consume messages)
212
- if (messagesRead > 0) {
213
- Atomics.store(this.atomicView, this.CONTROL_INDICES.TAIL, currentTail);
214
- }
215
-
216
- return messages;
217
- };
218
-
219
- /**
220
- * Main wait loop using Atomics.wait for instant wake
221
- */
222
- RingBufferWorkerBase.prototype.waitLoop = function() {
223
- var self = this;
224
- while (this.running) {
225
- try {
226
- // Get current HEAD value
227
- var currentHead = Atomics.load(this.atomicView, this.CONTROL_INDICES.HEAD);
228
- var currentTail = Atomics.load(this.atomicView, this.CONTROL_INDICES.TAIL);
229
-
230
- // If buffer is empty, wait for AudioWorklet to notify us
231
- if (currentHead === currentTail) {
232
- // Wait for up to 100ms (allows checking stop signal)
233
- var result = Atomics.wait(this.atomicView, this.CONTROL_INDICES.HEAD, currentHead, 100);
234
-
235
- if (result === 'ok' || result === 'not-equal') {
236
- // We were notified or value changed!
237
- this.stats.wakeups++;
238
- } else if (result === 'timed-out') {
239
- this.stats.timeouts++;
240
- continue; // Check running flag
241
- }
242
- }
243
-
244
- // Read all available messages
245
- var messages = this.readMessages();
246
-
247
- // Debug: log ONCE if we tried to read but got no valid messages due to corruption
248
- if (messages.length === 0 && currentHead !== currentTail && this.stats.droppedMessages > 0) {
249
- if (this.bufferType === 'SYSTEM' && !this.loggedCorruptionState) {
250
- console.log('[' + this.bufferType + 'Worker] Head != tail (head=' + currentHead + ', tail=' + currentTail + ') but all messages corrupted. Dropped:', this.stats.droppedMessages);
251
- this.loggedCorruptionState = true;
252
- }
253
- }
254
-
255
- if (messages.length > 0) {
256
- // Send to main thread
257
- self.postMessage({
258
- type: 'messages',
259
- messages: messages,
260
- stats: {
261
- wakeups: this.stats.wakeups,
262
- timeouts: this.stats.timeouts,
263
- messagesReceived: this.stats.messagesReceived,
264
- droppedMessages: this.stats.droppedMessages
265
- }
266
- });
267
- }
268
-
269
- } catch (error) {
270
- console.error('[' + this.bufferType + 'Worker] Error in wait loop:', error);
271
- self.postMessage({
272
- type: 'error',
273
- error: error.message
274
- });
275
-
276
- // Brief pause on error before retrying
277
- Atomics.wait(this.atomicView, 0, this.atomicView[0], 10);
278
- }
279
- }
280
- };
281
-
282
- /**
283
- * Start the wait loop
284
- */
285
- RingBufferWorkerBase.prototype.start = function() {
286
- if (!this.sharedBuffer) {
287
- console.error('[' + this.bufferType + 'Worker] Cannot start - not initialized');
288
- return;
289
- }
290
-
291
- if (this.running) {
292
- console.warn('[' + this.bufferType + 'Worker] Already running');
293
- return;
294
- }
295
-
296
- this.running = true;
297
- this.waitLoop();
298
- };
299
-
300
- /**
301
- * Stop the wait loop
302
- */
303
- RingBufferWorkerBase.prototype.stop = function() {
304
- this.running = false;
305
- };
@@ -1,64 +0,0 @@
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
- * System Message Worker - Handles internal SuperSonic messages
11
- * Uses the shared ring buffer base class
12
- * ES5-compatible for Qt WebEngine
13
- */
14
-
15
- // Import the base class
16
- importScripts('ring_buffer_worker_base.js');
17
-
18
- // Create worker instance for SYSTEM buffer
19
- var worker = new RingBufferWorkerBase('SYSTEM');
20
-
21
- // Override postMessage to maintain compatibility
22
- worker.postMessage = self.postMessage.bind(self);
23
-
24
- /**
25
- * Handle messages from main thread
26
- */
27
- self.onmessage = function(event) {
28
- var data = event.data;
29
-
30
- try {
31
- switch (data.type) {
32
- case 'init':
33
- worker.initRingBuffer(data.sharedBuffer, data.ringBufferBase, data.bufferConstants);
34
- self.postMessage({ type: 'initialized' });
35
- break;
36
-
37
- case 'start':
38
- worker.start();
39
- break;
40
-
41
- case 'stop':
42
- worker.stop();
43
- break;
44
-
45
- case 'getStats':
46
- self.postMessage({
47
- type: 'stats',
48
- stats: worker.stats
49
- });
50
- break;
51
-
52
- default:
53
- console.warn('[SystemWorker] Unknown message type:', data.type);
54
- }
55
- } catch (error) {
56
- console.error('[SystemWorker] Error:', error);
57
- self.postMessage({
58
- type: 'error',
59
- error: error.message
60
- });
61
- }
62
- };
63
-
64
- console.log('[SystemWorker] Script loaded');