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.
- package/LICENSE +27 -0
- package/README.md +320 -0
- package/dist/README.md +21 -0
- package/dist/supersonic.js +2411 -0
- package/dist/wasm/manifest.json +8 -0
- package/dist/wasm/scsynth-nrt.wasm +0 -0
- package/dist/workers/debug_worker.js +274 -0
- package/dist/workers/osc_in_worker.js +274 -0
- package/dist/workers/osc_out_worker.js +519 -0
- package/dist/workers/scsynth_audio_worklet.js +531 -0
- package/package.json +51 -0
|
@@ -0,0 +1,519 @@
|
|
|
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 sending OSC bundles to scsynth
|
|
11
|
+
* Handles timed bundles with priority queue scheduling
|
|
12
|
+
* Writes directly to SharedArrayBuffer ring buffer
|
|
13
|
+
* ES5-compatible for Qt WebEngine
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// Ring buffer configuration
|
|
17
|
+
var sharedBuffer = null;
|
|
18
|
+
var ringBufferBase = null;
|
|
19
|
+
var atomicView = null;
|
|
20
|
+
var dataView = null;
|
|
21
|
+
var uint8View = null;
|
|
22
|
+
|
|
23
|
+
// Ring buffer layout constants (loaded from WASM at initialization)
|
|
24
|
+
var bufferConstants = null;
|
|
25
|
+
|
|
26
|
+
// Control indices (calculated after init)
|
|
27
|
+
var CONTROL_INDICES = {};
|
|
28
|
+
|
|
29
|
+
// Scheduling state
|
|
30
|
+
var scheduledEvents = [];
|
|
31
|
+
var currentTimer = null;
|
|
32
|
+
var cachedTimeDelta = null;
|
|
33
|
+
var minimumScheduleRequirementS = 0.002; // 2ms for audio precision
|
|
34
|
+
var latencyS = 0.05; // 50ms latency compensation for scsynth
|
|
35
|
+
|
|
36
|
+
// Message queue for handling backpressure
|
|
37
|
+
var immediateQueue = []; // Queue of messages waiting to be written
|
|
38
|
+
var isWriting = false; // Flag to prevent concurrent write attempts
|
|
39
|
+
var writeRetryTimer = null;
|
|
40
|
+
|
|
41
|
+
// Statistics
|
|
42
|
+
var stats = {
|
|
43
|
+
bundlesScheduled: 0,
|
|
44
|
+
bundlesWritten: 0,
|
|
45
|
+
bundlesDropped: 0,
|
|
46
|
+
bufferOverruns: 0,
|
|
47
|
+
retries: 0,
|
|
48
|
+
queueDepth: 0,
|
|
49
|
+
maxQueueDepth: 0
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Initialize ring buffer access
|
|
54
|
+
*/
|
|
55
|
+
function initRingBuffer(buffer, base, constants) {
|
|
56
|
+
sharedBuffer = buffer;
|
|
57
|
+
ringBufferBase = base;
|
|
58
|
+
bufferConstants = constants;
|
|
59
|
+
atomicView = new Int32Array(sharedBuffer);
|
|
60
|
+
dataView = new DataView(sharedBuffer);
|
|
61
|
+
uint8View = new Uint8Array(sharedBuffer);
|
|
62
|
+
|
|
63
|
+
// Calculate control indices using constants from WASM
|
|
64
|
+
CONTROL_INDICES = {
|
|
65
|
+
IN_HEAD: (ringBufferBase + bufferConstants.CONTROL_START + 0) / 4,
|
|
66
|
+
IN_TAIL: (ringBufferBase + bufferConstants.CONTROL_START + 4) / 4
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Queue a message for writing (handles backpressure)
|
|
72
|
+
*/
|
|
73
|
+
function queueMessage(oscMessage) {
|
|
74
|
+
immediateQueue.push(oscMessage);
|
|
75
|
+
stats.queueDepth = immediateQueue.length;
|
|
76
|
+
|
|
77
|
+
if (stats.queueDepth > stats.maxQueueDepth) {
|
|
78
|
+
stats.maxQueueDepth = stats.queueDepth;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Start processing if not already running
|
|
82
|
+
if (!isWriting) {
|
|
83
|
+
processQueue();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Process the message queue - blocks until space is available
|
|
89
|
+
*/
|
|
90
|
+
function processQueue() {
|
|
91
|
+
if (isWriting || immediateQueue.length === 0) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
isWriting = true;
|
|
96
|
+
|
|
97
|
+
function processNext() {
|
|
98
|
+
if (immediateQueue.length === 0) {
|
|
99
|
+
isWriting = false;
|
|
100
|
+
stats.queueDepth = 0;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
var message = immediateQueue[0]; // Peek at first message
|
|
105
|
+
|
|
106
|
+
// Block until there's space, then write
|
|
107
|
+
var success = writeToRingBufferBlocking(message);
|
|
108
|
+
|
|
109
|
+
if (success) {
|
|
110
|
+
// Success! Remove from queue
|
|
111
|
+
immediateQueue.shift();
|
|
112
|
+
stats.queueDepth = immediateQueue.length;
|
|
113
|
+
|
|
114
|
+
// Process next message
|
|
115
|
+
if (immediateQueue.length > 0) {
|
|
116
|
+
setTimeout(processNext, 0);
|
|
117
|
+
} else {
|
|
118
|
+
isWriting = false;
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
// Fatal error (message too large or not initialized)
|
|
122
|
+
console.error('[OSCOutWorker] Fatal error, dropping message');
|
|
123
|
+
immediateQueue.shift(); // Remove bad message
|
|
124
|
+
stats.bundlesDropped++;
|
|
125
|
+
stats.queueDepth = immediateQueue.length;
|
|
126
|
+
|
|
127
|
+
// Continue with next message
|
|
128
|
+
setTimeout(processNext, 0);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
processNext();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Write OSC message to ring buffer - blocks until space available
|
|
137
|
+
* Returns true on success, false on fatal error (message too large)
|
|
138
|
+
*/
|
|
139
|
+
function writeToRingBufferBlocking(oscMessage) {
|
|
140
|
+
if (!sharedBuffer) {
|
|
141
|
+
console.error('[OSCOutWorker] Not initialized');
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
var payloadSize = oscMessage.length;
|
|
146
|
+
var totalSize = bufferConstants.MESSAGE_HEADER_SIZE + payloadSize;
|
|
147
|
+
|
|
148
|
+
// Check if message fits in buffer at all (account for padding at wrap)
|
|
149
|
+
if (totalSize > bufferConstants.IN_BUFFER_SIZE - bufferConstants.MESSAGE_HEADER_SIZE) {
|
|
150
|
+
console.error('[OSCOutWorker] Message too large:', totalSize, 'max:', bufferConstants.IN_BUFFER_SIZE - bufferConstants.MESSAGE_HEADER_SIZE);
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Keep trying until we have space
|
|
155
|
+
while (true) {
|
|
156
|
+
var head = Atomics.load(atomicView, CONTROL_INDICES.IN_HEAD);
|
|
157
|
+
var tail = Atomics.load(atomicView, CONTROL_INDICES.IN_TAIL);
|
|
158
|
+
|
|
159
|
+
// Check available space
|
|
160
|
+
var available = (bufferConstants.IN_BUFFER_SIZE - 1 - head + tail) % bufferConstants.IN_BUFFER_SIZE;
|
|
161
|
+
|
|
162
|
+
if (available >= totalSize) {
|
|
163
|
+
// Check if message fits contiguously, otherwise write padding and wrap
|
|
164
|
+
var spaceToEnd = bufferConstants.IN_BUFFER_SIZE - head;
|
|
165
|
+
|
|
166
|
+
if (totalSize > spaceToEnd) {
|
|
167
|
+
// Message won't fit at end - write padding marker and wrap to beginning
|
|
168
|
+
var paddingPos = ringBufferBase + bufferConstants.IN_BUFFER_START + head;
|
|
169
|
+
dataView.setUint32(paddingPos, bufferConstants.PADDING_MAGIC, true);
|
|
170
|
+
dataView.setUint32(paddingPos + 4, 0, true);
|
|
171
|
+
dataView.setUint32(paddingPos + 8, 0, true);
|
|
172
|
+
dataView.setUint32(paddingPos + 12, 0, true);
|
|
173
|
+
|
|
174
|
+
// Wrap head to beginning
|
|
175
|
+
head = 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// We have space! Write the message (now guaranteed contiguous)
|
|
179
|
+
var writePos = ringBufferBase + bufferConstants.IN_BUFFER_START + head;
|
|
180
|
+
|
|
181
|
+
// Write message header
|
|
182
|
+
dataView.setUint32(writePos, bufferConstants.MESSAGE_MAGIC, true);
|
|
183
|
+
dataView.setUint32(writePos + 4, totalSize, true);
|
|
184
|
+
dataView.setUint32(writePos + 8, stats.bundlesWritten, true); // sequence
|
|
185
|
+
dataView.setUint32(writePos + 12, 0, true); // padding
|
|
186
|
+
|
|
187
|
+
// Write payload
|
|
188
|
+
uint8View.set(oscMessage, writePos + bufferConstants.MESSAGE_HEADER_SIZE);
|
|
189
|
+
|
|
190
|
+
// Update head pointer (publish message)
|
|
191
|
+
var newHead = (head + totalSize) % bufferConstants.IN_BUFFER_SIZE;
|
|
192
|
+
Atomics.store(atomicView, CONTROL_INDICES.IN_HEAD, newHead);
|
|
193
|
+
|
|
194
|
+
stats.bundlesWritten++;
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Buffer is full - wait for tail to move (scsynth to consume)
|
|
199
|
+
stats.bufferOverruns++;
|
|
200
|
+
|
|
201
|
+
// Wait on the tail pointer - will wake when scsynth consumes data
|
|
202
|
+
// Timeout after 100ms to check if worker should stop
|
|
203
|
+
var result = Atomics.wait(atomicView, CONTROL_INDICES.IN_TAIL, tail, 100);
|
|
204
|
+
|
|
205
|
+
if (result === 'ok' || result === 'not-equal') {
|
|
206
|
+
// Tail moved! Loop will retry
|
|
207
|
+
stats.retries++;
|
|
208
|
+
}
|
|
209
|
+
// On timeout, loop continues to retry (allows checking for stop signal)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Get or set cached time delta for synchronization
|
|
215
|
+
*/
|
|
216
|
+
function getOrSetTimeDelta(delta) {
|
|
217
|
+
if (cachedTimeDelta === null) {
|
|
218
|
+
cachedTimeDelta = delta;
|
|
219
|
+
}
|
|
220
|
+
return cachedTimeDelta;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Check if data is an OSC bundle (starts with "#bundle\0")
|
|
225
|
+
*/
|
|
226
|
+
function isBundle(data) {
|
|
227
|
+
if (data.length < 16) return false;
|
|
228
|
+
var bundleTag = String.fromCharCode.apply(null, data.slice(0, 8));
|
|
229
|
+
return bundleTag === '#bundle\0';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Parse OSC bundle timestamp from binary data
|
|
234
|
+
* OSC bundles start with "#bundle\0" followed by 8-byte NTP timestamp
|
|
235
|
+
*/
|
|
236
|
+
function parseBundleTimestamp(data) {
|
|
237
|
+
if (!isBundle(data)) return null;
|
|
238
|
+
|
|
239
|
+
// Read NTP timestamp (8 bytes, big-endian)
|
|
240
|
+
var view = new DataView(data.buffer, data.byteOffset + 8, 8);
|
|
241
|
+
var seconds = view.getUint32(0, false); // NTP seconds since 1900
|
|
242
|
+
var fraction = view.getUint32(4, false); // NTP fractional seconds
|
|
243
|
+
|
|
244
|
+
// Convert NTP to JavaScript time
|
|
245
|
+
// NTP epoch is 1900, JS epoch is 1970 (difference: 2208988800 seconds)
|
|
246
|
+
var NTP_TO_UNIX = 2208988800;
|
|
247
|
+
|
|
248
|
+
// Special OSC timestamps
|
|
249
|
+
if (seconds === 0 && fraction === 1) {
|
|
250
|
+
return 0; // Immediate execution
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Convert to JavaScript timestamp (milliseconds since 1970)
|
|
254
|
+
var unixSeconds = seconds - NTP_TO_UNIX;
|
|
255
|
+
var milliseconds = (fraction / 4294967296) * 1000; // Convert fraction to ms
|
|
256
|
+
|
|
257
|
+
return (unixSeconds * 1000) + milliseconds;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Extract OSC messages from a bundle
|
|
262
|
+
* Returns array of message buffers
|
|
263
|
+
*/
|
|
264
|
+
function extractMessagesFromBundle(data) {
|
|
265
|
+
var messages = [];
|
|
266
|
+
|
|
267
|
+
if (!isBundle(data)) {
|
|
268
|
+
// Not a bundle, return as single message
|
|
269
|
+
return [data];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Skip "#bundle\0" (8 bytes) and timestamp (8 bytes)
|
|
273
|
+
var offset = 16;
|
|
274
|
+
|
|
275
|
+
while (offset < data.length) {
|
|
276
|
+
// Read message size (4 bytes, big-endian)
|
|
277
|
+
if (offset + 4 > data.length) break;
|
|
278
|
+
|
|
279
|
+
var view = new DataView(data.buffer, data.byteOffset + offset, 4);
|
|
280
|
+
var messageSize = view.getInt32(0, false);
|
|
281
|
+
offset += 4;
|
|
282
|
+
|
|
283
|
+
if (messageSize <= 0 || offset + messageSize > data.length) break;
|
|
284
|
+
|
|
285
|
+
// Extract message data
|
|
286
|
+
var messageData = data.slice(offset, offset + messageSize);
|
|
287
|
+
|
|
288
|
+
// Check if this is a nested bundle
|
|
289
|
+
if (isBundle(messageData)) {
|
|
290
|
+
// Recursively extract from nested bundle
|
|
291
|
+
var nestedMessages = extractMessagesFromBundle(messageData);
|
|
292
|
+
messages = messages.concat(nestedMessages);
|
|
293
|
+
} else {
|
|
294
|
+
// It's a message, add it
|
|
295
|
+
messages.push(messageData);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
offset += messageSize;
|
|
299
|
+
|
|
300
|
+
// Align to 4-byte boundary if needed
|
|
301
|
+
while (offset % 4 !== 0 && offset < data.length) {
|
|
302
|
+
offset++;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return messages;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Process incoming OSC data (message or bundle)
|
|
311
|
+
* Pre-scheduler: waits for calculated time then sends to ring buffer
|
|
312
|
+
* waitTimeMs is calculated by SuperSonic based on AudioContext time
|
|
313
|
+
*/
|
|
314
|
+
function processOSC(oscData, editorId, runTag, waitTimeMs) {
|
|
315
|
+
stats.bundlesScheduled++;
|
|
316
|
+
|
|
317
|
+
// If no wait time provided, or wait time is 0 or negative, send immediately
|
|
318
|
+
if (waitTimeMs === null || waitTimeMs === undefined || waitTimeMs <= 0) {
|
|
319
|
+
queueMessage(oscData);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Schedule to send after waitTimeMs
|
|
324
|
+
setTimeout(function() {
|
|
325
|
+
queueMessage(oscData);
|
|
326
|
+
}, waitTimeMs);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Process immediate send - forces immediate execution by unpacking bundles
|
|
331
|
+
* Bundles are unpacked to individual messages (stripping timestamps)
|
|
332
|
+
* Messages are sent as-is
|
|
333
|
+
* Used when the caller wants immediate execution without scheduling
|
|
334
|
+
*/
|
|
335
|
+
function processImmediate(oscData) {
|
|
336
|
+
if (isBundle(oscData)) {
|
|
337
|
+
// Extract all messages from the bundle (removes timestamp wrapper)
|
|
338
|
+
// Send each message individually for immediate execution
|
|
339
|
+
var messages = extractMessagesFromBundle(oscData);
|
|
340
|
+
for (var i = 0; i < messages.length; i++) {
|
|
341
|
+
queueMessage(messages[i]);
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
// Regular message - send as-is
|
|
345
|
+
queueMessage(oscData);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Insert event into priority queue
|
|
351
|
+
*/
|
|
352
|
+
function insertEvent(userId, editorId, runId, runTag, adjustedTimeS, oscBundle) {
|
|
353
|
+
var info = { userId: userId, editorId: editorId, runTag: runTag, runId: runId };
|
|
354
|
+
scheduledEvents.push([adjustedTimeS, info, oscBundle]);
|
|
355
|
+
scheduledEvents.sort(function(a, b) { return a[0] - b[0]; });
|
|
356
|
+
scheduleNextEvent();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Schedule the next event timer
|
|
361
|
+
*/
|
|
362
|
+
function scheduleNextEvent() {
|
|
363
|
+
if (scheduledEvents.length === 0) {
|
|
364
|
+
clearCurrentTimer();
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
var nextEvent = scheduledEvents[0];
|
|
369
|
+
var adjustedTimeS = nextEvent[0];
|
|
370
|
+
|
|
371
|
+
if (!currentTimer || (currentTimer && currentTimer.timeS > adjustedTimeS)) {
|
|
372
|
+
addRunNextEventTimer(adjustedTimeS);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Clear current timer
|
|
378
|
+
*/
|
|
379
|
+
function clearCurrentTimer() {
|
|
380
|
+
if (currentTimer) {
|
|
381
|
+
clearTimeout(currentTimer.timerId);
|
|
382
|
+
currentTimer = null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Add timer for next event
|
|
388
|
+
*/
|
|
389
|
+
function addRunNextEventTimer(adjustedTimeS) {
|
|
390
|
+
clearCurrentTimer();
|
|
391
|
+
|
|
392
|
+
var nowS = Date.now() / 1000;
|
|
393
|
+
var timeDeltaS = adjustedTimeS - nowS;
|
|
394
|
+
|
|
395
|
+
if (timeDeltaS <= minimumScheduleRequirementS) {
|
|
396
|
+
runNextEvent();
|
|
397
|
+
} else {
|
|
398
|
+
var delayMs = (timeDeltaS - minimumScheduleRequirementS) * 1000;
|
|
399
|
+
currentTimer = {
|
|
400
|
+
timeS: adjustedTimeS,
|
|
401
|
+
timerId: setTimeout(function() {
|
|
402
|
+
currentTimer = null;
|
|
403
|
+
runNextEvent();
|
|
404
|
+
}, delayMs)
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Run the next scheduled event
|
|
411
|
+
*/
|
|
412
|
+
function runNextEvent() {
|
|
413
|
+
clearCurrentTimer();
|
|
414
|
+
|
|
415
|
+
if (scheduledEvents.length === 0) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
var event = scheduledEvents.shift();
|
|
420
|
+
var data = event[2];
|
|
421
|
+
|
|
422
|
+
// Send the complete bundle unchanged (with original timestamp)
|
|
423
|
+
queueMessage(data);
|
|
424
|
+
|
|
425
|
+
scheduleNextEvent();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Cancel events by editor and tag
|
|
430
|
+
*/
|
|
431
|
+
function cancelEditorTag(editorId, runTag) {
|
|
432
|
+
scheduledEvents = scheduledEvents.filter(function(e) {
|
|
433
|
+
return e[1].runTag !== runTag || e[1].editorId !== editorId;
|
|
434
|
+
});
|
|
435
|
+
scheduleNextEvent();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Cancel all events from an editor
|
|
440
|
+
*/
|
|
441
|
+
function cancelEditor(editorId) {
|
|
442
|
+
scheduledEvents = scheduledEvents.filter(function(e) {
|
|
443
|
+
return e[1].editorId !== editorId;
|
|
444
|
+
});
|
|
445
|
+
scheduleNextEvent();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Cancel all scheduled events
|
|
450
|
+
*/
|
|
451
|
+
function cancelAllTags() {
|
|
452
|
+
scheduledEvents = [];
|
|
453
|
+
clearCurrentTimer();
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Reset time delta for resync
|
|
458
|
+
*/
|
|
459
|
+
function resetTimeDelta() {
|
|
460
|
+
cachedTimeDelta = null;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Handle messages from main thread
|
|
465
|
+
*/
|
|
466
|
+
self.onmessage = function(event) {
|
|
467
|
+
var data = event.data;
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
switch (data.type) {
|
|
471
|
+
case 'init':
|
|
472
|
+
initRingBuffer(data.sharedBuffer, data.ringBufferBase, data.bufferConstants);
|
|
473
|
+
self.postMessage({ type: 'initialized' });
|
|
474
|
+
break;
|
|
475
|
+
|
|
476
|
+
case 'send':
|
|
477
|
+
// Single send method for both messages and bundles
|
|
478
|
+
// waitTimeMs is calculated by SuperSonic based on AudioContext time
|
|
479
|
+
processOSC(data.oscData, data.editorId, data.runTag, data.waitTimeMs);
|
|
480
|
+
break;
|
|
481
|
+
|
|
482
|
+
case 'sendImmediate':
|
|
483
|
+
// Force immediate send, extracting all messages from bundles
|
|
484
|
+
// Ignores timestamps - for apps that don't expect scheduling
|
|
485
|
+
processImmediate(data.oscData);
|
|
486
|
+
break;
|
|
487
|
+
|
|
488
|
+
case 'cancelEditorTag':
|
|
489
|
+
cancelEditorTag(data.editorId, data.runTag);
|
|
490
|
+
break;
|
|
491
|
+
|
|
492
|
+
case 'cancelEditor':
|
|
493
|
+
cancelEditor(data.editorId);
|
|
494
|
+
break;
|
|
495
|
+
|
|
496
|
+
case 'cancelAll':
|
|
497
|
+
cancelAllTags();
|
|
498
|
+
break;
|
|
499
|
+
|
|
500
|
+
case 'getStats':
|
|
501
|
+
self.postMessage({
|
|
502
|
+
type: 'stats',
|
|
503
|
+
stats: stats
|
|
504
|
+
});
|
|
505
|
+
break;
|
|
506
|
+
|
|
507
|
+
default:
|
|
508
|
+
console.warn('[OSCOutWorker] Unknown message type:', data.type);
|
|
509
|
+
}
|
|
510
|
+
} catch (error) {
|
|
511
|
+
console.error('[OSCOutWorker] Error:', error);
|
|
512
|
+
self.postMessage({
|
|
513
|
+
type: 'error',
|
|
514
|
+
error: error.message
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
console.log('[OSCOutWorker] Script loaded');
|