supersonic-scsynth 0.2.2 → 0.3.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
@@ -8,11 +8,13 @@ A WebAssembly port of SuperCollider's scsynth audio synthesis engine for the bro
8
8
 
9
9
  ```html
10
10
  <script type="module">
11
- import { SuperSonic } from 'https://unpkg.com/supersonic-scsynth@latest';
11
+ import { SuperSonic } from './dist/supersonic.js';
12
12
 
13
13
  const sonic = new SuperSonic({
14
- sampleBaseURL: 'https://unpkg.com/supersonic-scsynth-samples@latest/samples/',
15
- synthdefBaseURL: 'https://unpkg.com/supersonic-scsynth-synthdefs@latest/synthdefs/'
14
+ workerBaseURL: './dist/workers/',
15
+ wasmBaseURL: './dist/wasm/',
16
+ sampleBaseURL: './dist/samples/',
17
+ synthdefBaseURL: './dist/synthdefs/'
16
18
  });
17
19
 
18
20
  await sonic.init();
@@ -29,11 +31,11 @@ A WebAssembly port of SuperCollider's scsynth audio synthesis engine for the bro
29
31
  </script>
30
32
  ```
31
33
 
32
- **Note:** Requires specific HTTP headers (COOP/COEP) for SharedArrayBuffer support. See [Browser Requirements](#browser-requirements) below.
34
+ **Important:** SuperSonic requires self-hosting (cannot load from CDN). See [CDN Usage](#cdn-usage) below.
33
35
 
34
36
  ## Installation
35
37
 
36
- **Via npm:**
38
+ **Via npm (for local bundling):**
37
39
  ```bash
38
40
  # Core engine only (~450KB)
39
41
  npm install supersonic-scsynth
@@ -42,15 +44,15 @@ npm install supersonic-scsynth
42
44
  npm install supersonic-scsynth-bundle
43
45
  ```
44
46
 
45
- **Via CDN:**
47
+ **Pre-built distribution (recommended):**
48
+ Download the pre-built package (~35MB with all synthdefs and samples) and serve from your own domain:
49
+ https://samaaron.github.io/supersonic/supersonic-dist.zip
50
+
51
+ Extract to your web server and import as:
46
52
  ```javascript
47
- import { SuperSonic } from 'https://unpkg.com/supersonic-scsynth@latest';
53
+ import { SuperSonic } from './dist/supersonic.js';
48
54
  ```
49
55
 
50
- **Pre-built distribution:**
51
- Download the 'nightly' build (~35MB with all synthdefs and samples):
52
- https://samaaron.github.io/supersonic/supersonic-dist.zip
53
-
54
56
  ## Packages
55
57
 
56
58
  SuperSonic is split into multiple packages:
@@ -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: 'https://unpkg.com/supersonic-scsynth-samples@latest/samples/',
73
- synthdefBaseURL: 'https://unpkg.com/supersonic-scsynth-synthdefs@latest/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
  ```
@@ -121,6 +125,43 @@ Cross-Origin-Resource-Policy: cross-origin
121
125
 
122
126
  See `example/server.rb` for a reference implementation.
123
127
 
128
+ ## CDN Usage
129
+
130
+ SuperSonic cannot be loaded from a CDN. The core library must be self-hosted on your domain.
131
+
132
+ ### Why Self-Hosting is Required
133
+
134
+ SuperSonic uses `SharedArrayBuffer` for real-time audio performance. Browsers require workers that use `SharedArrayBuffer` to come from the same origin as the page. Even with proper COOP/COEP headers, cross-origin workers with shared memory are blocked. This is a fundamental browser security requirement stemming from Spectre attack mitigation.
135
+
136
+ What this means:
137
+ - You cannot use `import { SuperSonic } from 'https://unpkg.com/supersonic/...'`
138
+ - You must download and self-host the core library on your own domain
139
+ - The npm packages exist for convenience but must be bundled and deployed to your server
140
+
141
+ ### Synthdefs and Samples Can Use CDN
142
+
143
+ Pre-compiled synthdefs and audio samples can be loaded from CDNs. They're just data files, not workers.
144
+
145
+ ```javascript
146
+ // Self-hosted core library
147
+ import { SuperSonic } from './dist/supersonic.js';
148
+
149
+ // CDN-hosted synthdefs and samples work fine
150
+ const sonic = new SuperSonic({
151
+ workerBaseURL: './dist/workers/', // Must be self-hosted
152
+ wasmBaseURL: './dist/wasm/', // Must be self-hosted
153
+ sampleBaseURL: 'https://unpkg.com/supersonic-scsynth-samples@0.1.6/samples/',
154
+ synthdefBaseURL: 'https://unpkg.com/supersonic-scsynth-synthdefs@0.1.6/synthdefs/'
155
+ });
156
+
157
+ await sonic.init();
158
+ await sonic.loadSynthDefs(['sonic-pi-beep', 'sonic-pi-tb303']);
159
+ ```
160
+
161
+ ### Hybrid Approach
162
+
163
+ Self-host the SuperSonic core (JS, WASM, workers) with COOP/COEP headers. Use CDN for synthdefs and samples to save bandwidth. See `example/simple-cdn.html` for a working example.
164
+
124
165
  ## Building from Source
125
166
 
126
167
  **Prerequisites:**
@@ -167,7 +208,7 @@ dist/
167
208
  └── debug_worker.js # Debug logger
168
209
  ```
169
210
 
170
- 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.
171
212
 
172
213
  ## License
173
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);
@@ -2324,7 +2330,7 @@ var SuperSonic = class _SuperSonic {
2324
2330
  }
2325
2331
  if (!this.sampleBaseURL) {
2326
2332
  throw new Error(
2327
- 'sampleBaseURL not configured. Please set it in SuperSonic constructor options.\nExample: new SuperSonic({ sampleBaseURL: "https://unpkg.com/supersonic-scsynth-samples@latest/samples/" })\nOr install sample packages: npm install supersonic-scsynth-samples'
2333
+ 'sampleBaseURL not configured. Please set it in SuperSonic constructor options.\nExample: new SuperSonic({ sampleBaseURL: "./dist/samples/" })\nOr use CDN: new SuperSonic({ sampleBaseURL: "https://unpkg.com/supersonic-scsynth-samples@latest/samples/" })\nOr install: npm install supersonic-scsynth-samples'
2328
2334
  );
2329
2335
  }
2330
2336
  return this.sampleBaseURL + scPath;
@@ -2525,7 +2531,7 @@ var SuperSonic = class _SuperSonic {
2525
2531
  }
2526
2532
  if (!this.synthdefBaseURL) {
2527
2533
  throw new Error(
2528
- 'synthdefBaseURL not configured. Please set it in SuperSonic constructor options.\nExample: new SuperSonic({ synthdefBaseURL: "https://unpkg.com/supersonic-scsynth-synthdefs@latest/synthdefs/" })\nOr install: npm install supersonic-scsynth-synthdefs'
2534
+ 'synthdefBaseURL not configured. Please set it in SuperSonic constructor options.\nExample: new SuperSonic({ synthdefBaseURL: "./dist/synthdefs/" })\nOr use CDN: new SuperSonic({ synthdefBaseURL: "https://unpkg.com/supersonic-scsynth-synthdefs@latest/synthdefs/" })\nOr install: npm install supersonic-scsynth-synthdefs'
2529
2535
  );
2530
2536
  }
2531
2537
  const results = {};
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "wasmFile": "scsynth-nrt.wasm",
3
- "buildId": "20251114-221419",
4
- "buildTime": "2025-11-14T22:14:19Z",
5
- "gitHash": "cfd4b75"
3
+ "buildId": "20251117-205541",
4
+ "buildTime": "2025-11-17T20:55:41Z",
5
+ "gitHash": "264e2ae"
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,10 +160,14 @@ 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
 
@@ -191,6 +206,17 @@ function writeToRingBuffer(oscMessage) {
191
206
  // Write payload
192
207
  uint8View.set(oscMessage, writePos + bufferConstants.MESSAGE_HEADER_SIZE);
193
208
 
209
+ // Diagnostic: Log first few writes
210
+ if (stats.bundlesWritten < 5) {
211
+ schedulerLog('[PreScheduler] Write:', 'seq=' + stats.bundlesWritten,
212
+ 'pos=' + head, 'size=' + totalSize, 'newHead=' + ((head + totalSize) % bufferConstants.IN_BUFFER_SIZE));
213
+ }
214
+
215
+ // CRITICAL: Ensure memory barrier before publishing head pointer
216
+ // All previous writes (header + payload) must be visible to C++ reader
217
+ // Atomics.load provides necessary memory fence/barrier
218
+ Atomics.load(atomicView, CONTROL_INDICES.IN_HEAD);
219
+
194
220
  // Update head pointer (publish message)
195
221
  var newHead = (head + totalSize) % bufferConstants.IN_BUFFER_SIZE;
196
222
  Atomics.store(atomicView, CONTROL_INDICES.IN_HEAD, newHead);
@@ -199,6 +225,77 @@ function writeToRingBuffer(oscMessage) {
199
225
  return true;
200
226
  }
201
227
 
228
+ /**
229
+ * Add a message to the retry queue
230
+ */
231
+ function queueForRetry(oscData, context) {
232
+ if (retryQueue.length >= MAX_RETRY_QUEUE_SIZE) {
233
+ console.error('[PreScheduler] Retry queue full, dropping message permanently');
234
+ stats.retriesFailed++;
235
+ return;
236
+ }
237
+
238
+ retryQueue.push({
239
+ oscData: oscData,
240
+ retryCount: 0,
241
+ context: context || 'unknown',
242
+ queuedAt: performance.now()
243
+ });
244
+
245
+ stats.retryQueueSize = retryQueue.length;
246
+ if (stats.retryQueueSize > stats.maxRetryQueueSize) {
247
+ stats.maxRetryQueueSize = stats.retryQueueSize;
248
+ }
249
+
250
+ schedulerLog('[PreScheduler] Queued message for retry:', context, 'queue size:', retryQueue.length);
251
+ }
252
+
253
+ /**
254
+ * Attempt to retry queued messages
255
+ * Called periodically from checkAndDispatch
256
+ */
257
+ function processRetryQueue() {
258
+ if (retryQueue.length === 0) {
259
+ return;
260
+ }
261
+
262
+ var i = 0;
263
+ while (i < retryQueue.length) {
264
+ var item = retryQueue[i];
265
+
266
+ // Try to write
267
+ var success = writeToRingBuffer(item.oscData, true);
268
+
269
+ if (success) {
270
+ // Success - remove from queue
271
+ retryQueue.splice(i, 1);
272
+ stats.retriesSucceeded++;
273
+ stats.messagesRetried++;
274
+ stats.retryQueueSize = retryQueue.length;
275
+ schedulerLog('[PreScheduler] Retry succeeded for:', item.context,
276
+ 'after', item.retryCount + 1, 'attempts');
277
+ // Don't increment i - we removed an item
278
+ } else {
279
+ // Failed - increment retry count
280
+ item.retryCount++;
281
+ stats.messagesRetried++;
282
+
283
+ if (item.retryCount >= MAX_RETRIES_PER_MESSAGE) {
284
+ // Give up on this message
285
+ console.error('[PreScheduler] Giving up on message after',
286
+ MAX_RETRIES_PER_MESSAGE, 'retries:', item.context);
287
+ retryQueue.splice(i, 1);
288
+ stats.retriesFailed++;
289
+ stats.retryQueueSize = retryQueue.length;
290
+ // Don't increment i - we removed an item
291
+ } else {
292
+ // Keep in queue, try again next cycle
293
+ i++;
294
+ }
295
+ }
296
+ }
297
+ }
298
+
202
299
  /**
203
300
  * Schedule an OSC bundle by its NTP timestamp
204
301
  * Non-bundles or bundles without timestamps are dispatched immediately
@@ -209,7 +306,11 @@ function scheduleEvent(oscData, editorId, runTag) {
209
306
  if (ntpTime === null) {
210
307
  // Not a bundle - dispatch immediately to ring buffer
211
308
  schedulerLog('[PreScheduler] Non-bundle message, dispatching immediately');
212
- writeToRingBuffer(oscData);
309
+ var success = writeToRingBuffer(oscData, false);
310
+ if (!success) {
311
+ // Queue for retry
312
+ queueForRetry(oscData, 'immediate message');
313
+ }
213
314
  return;
214
315
  }
215
316
 
@@ -338,6 +439,9 @@ function stopPeriodicPolling() {
338
439
  function checkAndDispatch() {
339
440
  isDispatching = true;
340
441
 
442
+ // First, try to process any queued retries
443
+ processRetryQueue();
444
+
341
445
  var currentNTP = getCurrentNTP();
342
446
  var lookaheadTime = currentNTP + LOOKAHEAD_S;
343
447
  var dispatchCount = 0;
@@ -361,7 +465,11 @@ function checkAndDispatch() {
361
465
  'early=' + (timeUntilExec * 1000).toFixed(1) + 'ms',
362
466
  'remaining=' + stats.eventsPending);
363
467
 
364
- writeToRingBuffer(nextEvent.oscData);
468
+ var success = writeToRingBuffer(nextEvent.oscData, false);
469
+ if (!success) {
470
+ // Queue for retry
471
+ queueForRetry(nextEvent.oscData, 'scheduled bundle NTP=' + nextEvent.ntpTime.toFixed(3));
472
+ }
365
473
  dispatchCount++;
366
474
  } else {
367
475
  // Rest aren't ready yet (heap is sorted)
@@ -369,10 +477,11 @@ function checkAndDispatch() {
369
477
  }
370
478
  }
371
479
 
372
- if (dispatchCount > 0 || eventHeap.length > 0) {
480
+ if (dispatchCount > 0 || eventHeap.length > 0 || retryQueue.length > 0) {
373
481
  schedulerLog('[PreScheduler] Dispatch cycle complete:',
374
482
  'dispatched=' + dispatchCount,
375
- 'pending=' + eventHeap.length);
483
+ 'pending=' + eventHeap.length,
484
+ 'retrying=' + retryQueue.length);
376
485
  }
377
486
 
378
487
  isDispatching = false;
@@ -474,10 +583,16 @@ function processImmediate(oscData) {
474
583
  if (isBundle(oscData)) {
475
584
  var messages = extractMessagesFromBundle(oscData);
476
585
  for (var i = 0; i < messages.length; i++) {
477
- writeToRingBuffer(messages[i]);
586
+ var success = writeToRingBuffer(messages[i], false);
587
+ if (!success) {
588
+ queueForRetry(messages[i], 'immediate bundle message ' + i);
589
+ }
478
590
  }
479
591
  } else {
480
- writeToRingBuffer(oscData);
592
+ var success = writeToRingBuffer(oscData, false);
593
+ if (!success) {
594
+ queueForRetry(oscData, 'immediate message');
595
+ }
481
596
  }
482
597
  }
483
598
 
@@ -555,7 +670,12 @@ self.addEventListener('message', function(event) {
555
670
  maxLateDispatchMs: stats.maxLateDispatchMs,
556
671
  totalSendTasks: stats.totalSendTasks,
557
672
  totalSendProcessMs: stats.totalSendProcessMs,
558
- maxSendProcessMs: stats.maxSendProcessMs
673
+ maxSendProcessMs: stats.maxSendProcessMs,
674
+ messagesRetried: stats.messagesRetried,
675
+ retriesSucceeded: stats.retriesSucceeded,
676
+ retriesFailed: stats.retriesFailed,
677
+ retryQueueSize: stats.retryQueueSize,
678
+ maxRetryQueueSize: stats.maxRetryQueueSize
559
679
  }
560
680
  });
561
681
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supersonic-scsynth",
3
- "version": "0.2.2",
3
+ "version": "0.3.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');