supersonic-scsynth 0.1.9 → 0.2.1
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 +1 -1
- package/dist/supersonic.js +810 -290
- package/dist/wasm/manifest.json +3 -3
- package/dist/wasm/scsynth-nrt.wasm +0 -0
- package/dist/workers/debug_worker.js +16 -3
- package/dist/workers/osc_in_worker.js +15 -2
- package/dist/workers/osc_out_prescheduler_worker.js +575 -0
- package/dist/workers/osc_out_worker.js +46 -183
- package/dist/workers/ring_buffer_worker_base.js +305 -0
- package/dist/workers/scsynth_audio_worklet.js +44 -23
- package/dist/workers/system_worker.js +64 -0
- package/package.json +1 -1
|
@@ -7,24 +7,14 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* OSC OUT Worker - Scheduler for
|
|
11
|
-
* Handles timed bundles
|
|
12
|
-
*
|
|
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
13
|
* ES5-compatible for Qt WebEngine
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
//
|
|
17
|
-
var
|
|
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 = {};
|
|
16
|
+
// Reference to the writer worker
|
|
17
|
+
var writerWorker = null;
|
|
28
18
|
|
|
29
19
|
// Scheduling state
|
|
30
20
|
var scheduledEvents = [];
|
|
@@ -33,181 +23,48 @@ var cachedTimeDelta = null;
|
|
|
33
23
|
var minimumScheduleRequirementS = 0.002; // 2ms for audio precision
|
|
34
24
|
var latencyS = 0.05; // 50ms latency compensation for scsynth
|
|
35
25
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
+
}
|
|
40
37
|
|
|
41
38
|
// Statistics
|
|
42
39
|
var stats = {
|
|
43
40
|
bundlesScheduled: 0,
|
|
44
|
-
|
|
45
|
-
bundlesDropped: 0,
|
|
46
|
-
bufferOverruns: 0,
|
|
47
|
-
retries: 0,
|
|
48
|
-
queueDepth: 0,
|
|
49
|
-
maxQueueDepth: 0
|
|
41
|
+
bundlesSentToWriter: 0
|
|
50
42
|
};
|
|
51
43
|
|
|
52
44
|
/**
|
|
53
|
-
* Initialize
|
|
45
|
+
* Initialize scheduler
|
|
54
46
|
*/
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
};
|
|
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)');
|
|
68
50
|
}
|
|
69
51
|
|
|
70
52
|
/**
|
|
71
|
-
*
|
|
53
|
+
* Send message to writer worker
|
|
72
54
|
*/
|
|
73
|
-
function
|
|
74
|
-
|
|
75
|
-
|
|
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) {
|
|
55
|
+
function sendToWriter(oscMessage) {
|
|
56
|
+
if (!writerWorker) {
|
|
57
|
+
console.error('[OSCSchedulerWorker] Writer worker not set');
|
|
92
58
|
return;
|
|
93
59
|
}
|
|
94
60
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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);
|
|
61
|
+
// Send to writer worker via postMessage
|
|
62
|
+
writerWorker.postMessage({
|
|
63
|
+
type: 'write',
|
|
64
|
+
oscData: oscMessage
|
|
65
|
+
});
|
|
204
66
|
|
|
205
|
-
|
|
206
|
-
// Tail moved! Loop will retry
|
|
207
|
-
stats.retries++;
|
|
208
|
-
}
|
|
209
|
-
// On timeout, loop continues to retry (allows checking for stop signal)
|
|
210
|
-
}
|
|
67
|
+
stats.bundlesSentToWriter++;
|
|
211
68
|
}
|
|
212
69
|
|
|
213
70
|
/**
|
|
@@ -308,7 +165,7 @@ function extractMessagesFromBundle(data) {
|
|
|
308
165
|
|
|
309
166
|
/**
|
|
310
167
|
* Process incoming OSC data (message or bundle)
|
|
311
|
-
* Pre-scheduler: waits for calculated time then sends to
|
|
168
|
+
* Pre-scheduler: waits for calculated time then sends to writer
|
|
312
169
|
* waitTimeMs is calculated by SuperSonic based on AudioContext time
|
|
313
170
|
*/
|
|
314
171
|
function processOSC(oscData, editorId, runTag, waitTimeMs) {
|
|
@@ -316,13 +173,13 @@ function processOSC(oscData, editorId, runTag, waitTimeMs) {
|
|
|
316
173
|
|
|
317
174
|
// If no wait time provided, or wait time is 0 or negative, send immediately
|
|
318
175
|
if (waitTimeMs === null || waitTimeMs === undefined || waitTimeMs <= 0) {
|
|
319
|
-
|
|
176
|
+
sendToWriter(oscData);
|
|
320
177
|
return;
|
|
321
178
|
}
|
|
322
179
|
|
|
323
180
|
// Schedule to send after waitTimeMs
|
|
324
181
|
setTimeout(function() {
|
|
325
|
-
|
|
182
|
+
sendToWriter(oscData);
|
|
326
183
|
}, waitTimeMs);
|
|
327
184
|
}
|
|
328
185
|
|
|
@@ -338,11 +195,11 @@ function processImmediate(oscData) {
|
|
|
338
195
|
// Send each message individually for immediate execution
|
|
339
196
|
var messages = extractMessagesFromBundle(oscData);
|
|
340
197
|
for (var i = 0; i < messages.length; i++) {
|
|
341
|
-
|
|
198
|
+
sendToWriter(messages[i]);
|
|
342
199
|
}
|
|
343
200
|
} else {
|
|
344
201
|
// Regular message - send as-is
|
|
345
|
-
|
|
202
|
+
sendToWriter(oscData);
|
|
346
203
|
}
|
|
347
204
|
}
|
|
348
205
|
|
|
@@ -420,7 +277,7 @@ function runNextEvent() {
|
|
|
420
277
|
var data = event[2];
|
|
421
278
|
|
|
422
279
|
// Send the complete bundle unchanged (with original timestamp)
|
|
423
|
-
|
|
280
|
+
sendToWriter(data);
|
|
424
281
|
|
|
425
282
|
scheduleNextEvent();
|
|
426
283
|
}
|
|
@@ -469,10 +326,16 @@ self.onmessage = function(event) {
|
|
|
469
326
|
try {
|
|
470
327
|
switch (data.type) {
|
|
471
328
|
case 'init':
|
|
472
|
-
|
|
329
|
+
init(data.sharedBuffer, data.ringBufferBase, data.bufferConstants);
|
|
473
330
|
self.postMessage({ type: 'initialized' });
|
|
474
331
|
break;
|
|
475
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
|
+
|
|
476
339
|
case 'send':
|
|
477
340
|
// Single send method for both messages and bundles
|
|
478
341
|
// waitTimeMs is calculated by SuperSonic based on AudioContext time
|
|
@@ -505,7 +368,7 @@ self.onmessage = function(event) {
|
|
|
505
368
|
break;
|
|
506
369
|
|
|
507
370
|
default:
|
|
508
|
-
|
|
371
|
+
schedulerWarn('[OSCOutWorker] Unknown message type:', data.type);
|
|
509
372
|
}
|
|
510
373
|
} catch (error) {
|
|
511
374
|
console.error('[OSCOutWorker] Error:', error);
|
|
@@ -516,4 +379,4 @@ self.onmessage = function(event) {
|
|
|
516
379
|
}
|
|
517
380
|
};
|
|
518
381
|
|
|
519
|
-
|
|
382
|
+
schedulerLog('[OSCSchedulerWorker] Script loaded - scheduler only, delegates to writer worker');
|
|
@@ -0,0 +1,305 @@
|
|
|
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
|
+
};
|