supersonic-scsynth 0.1.8 → 0.2.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 +4 -4
- package/dist/supersonic.js +1264 -702
- 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 +17 -8
- 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
package/dist/wasm/manifest.json
CHANGED
|
Binary file
|
|
@@ -29,6 +29,13 @@ var CONTROL_INDICES = {};
|
|
|
29
29
|
// Worker state
|
|
30
30
|
var running = false;
|
|
31
31
|
|
|
32
|
+
var DEBUG_DEBUGWORKER_LOGS = false;
|
|
33
|
+
function debugWorkerLog() {
|
|
34
|
+
if (DEBUG_DEBUGWORKER_LOGS) {
|
|
35
|
+
console.log.apply(console, arguments);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
32
39
|
// Statistics
|
|
33
40
|
var stats = {
|
|
34
41
|
messagesReceived: 0,
|
|
@@ -69,12 +76,18 @@ function readDebugMessages() {
|
|
|
69
76
|
var messages = [];
|
|
70
77
|
var currentTail = tail;
|
|
71
78
|
var messagesRead = 0;
|
|
72
|
-
var maxMessages =
|
|
79
|
+
var maxMessages = 1000; // Process up to 1000 messages per wake
|
|
73
80
|
|
|
74
81
|
while (currentTail !== head && messagesRead < maxMessages) {
|
|
82
|
+
var bytesToEnd = bufferConstants.DEBUG_BUFFER_SIZE - currentTail;
|
|
83
|
+
if (bytesToEnd < bufferConstants.MESSAGE_HEADER_SIZE) {
|
|
84
|
+
currentTail = 0;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
75
88
|
var readPos = ringBufferBase + bufferConstants.DEBUG_BUFFER_START + currentTail;
|
|
76
89
|
|
|
77
|
-
// Read message header (now
|
|
90
|
+
// Read message header (now contiguous or wrapped)
|
|
78
91
|
var magic = dataView.getUint32(readPos, true);
|
|
79
92
|
|
|
80
93
|
// Check for padding marker - skip to beginning
|
|
@@ -271,4 +284,4 @@ self.onmessage = function(event) {
|
|
|
271
284
|
}
|
|
272
285
|
};
|
|
273
286
|
|
|
274
|
-
|
|
287
|
+
debugWorkerLog('[DebugWorker] Script loaded');
|
|
@@ -29,6 +29,13 @@ var CONTROL_INDICES = {};
|
|
|
29
29
|
// Worker state
|
|
30
30
|
var running = false;
|
|
31
31
|
|
|
32
|
+
var DEBUG_OSCIN_LOGS = false;
|
|
33
|
+
function oscInLog() {
|
|
34
|
+
if (DEBUG_OSCIN_LOGS) {
|
|
35
|
+
console.log.apply(console, arguments);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
32
39
|
// Statistics
|
|
33
40
|
var stats = {
|
|
34
41
|
messagesReceived: 0,
|
|
@@ -71,16 +78,18 @@ function readMessages() {
|
|
|
71
78
|
|
|
72
79
|
var currentTail = tail;
|
|
73
80
|
var messagesRead = 0;
|
|
74
|
-
|
|
75
|
-
// - Low latency (don't block too long processing)
|
|
76
|
-
// - Efficiency (batch processing when busy)
|
|
77
|
-
// - Prevents starvation of other tasks
|
|
78
|
-
var maxMessages = 10;
|
|
81
|
+
var maxMessages = 100;
|
|
79
82
|
|
|
80
83
|
while (currentTail !== head && messagesRead < maxMessages) {
|
|
84
|
+
var bytesToEnd = bufferConstants.OUT_BUFFER_SIZE - currentTail;
|
|
85
|
+
if (bytesToEnd < bufferConstants.MESSAGE_HEADER_SIZE) {
|
|
86
|
+
currentTail = 0;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
81
90
|
var readPos = ringBufferBase + bufferConstants.OUT_BUFFER_START + currentTail;
|
|
82
91
|
|
|
83
|
-
// Read message header (now
|
|
92
|
+
// Read message header (now contiguous or wrapped)
|
|
84
93
|
var magic = dataView.getUint32(readPos, true);
|
|
85
94
|
|
|
86
95
|
// Check for padding marker - skip to beginning
|
|
@@ -115,7 +124,7 @@ function readMessages() {
|
|
|
115
124
|
if (sequence !== expectedSeq) {
|
|
116
125
|
var dropped = (sequence - expectedSeq + 0x100000000) & 0xFFFFFFFF;
|
|
117
126
|
if (dropped < 1000) { // Sanity check
|
|
118
|
-
console.warn('[OSCInWorker] Detected', dropped, 'dropped messages');
|
|
127
|
+
console.warn('[OSCInWorker] Detected', dropped, 'dropped messages (expected seq', expectedSeq, 'got', sequence, ')');
|
|
119
128
|
stats.droppedMessages += dropped;
|
|
120
129
|
}
|
|
121
130
|
}
|
|
@@ -271,4 +280,4 @@ self.onmessage = function(event) {
|
|
|
271
280
|
}
|
|
272
281
|
};
|
|
273
282
|
|
|
274
|
-
|
|
283
|
+
oscInLog('[OSCInWorker] Script loaded');
|
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
/*
|
|
2
|
+
SuperSonic - OSC Pre-Scheduler Worker
|
|
3
|
+
Ports the Bleep pre-scheduler design:
|
|
4
|
+
- Single priority queue of future bundles/events
|
|
5
|
+
- One timer driving dispatch (no per-event setTimeout storm)
|
|
6
|
+
- Tag-based cancellation to drop pending runs before they hit WASM
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Shared memory for ring buffer writing
|
|
10
|
+
var sharedBuffer = null;
|
|
11
|
+
var ringBufferBase = null;
|
|
12
|
+
var bufferConstants = null;
|
|
13
|
+
var atomicView = null;
|
|
14
|
+
var dataView = null;
|
|
15
|
+
var uint8View = null;
|
|
16
|
+
|
|
17
|
+
// Ring buffer control indices
|
|
18
|
+
var CONTROL_INDICES = {};
|
|
19
|
+
|
|
20
|
+
// Priority queue implemented as binary min-heap
|
|
21
|
+
// Entries: { ntpTime, seq, editorId, runTag, oscData }
|
|
22
|
+
var eventHeap = [];
|
|
23
|
+
var periodicTimer = null; // Single periodic timer (25ms interval)
|
|
24
|
+
var sequenceCounter = 0;
|
|
25
|
+
var isDispatching = false; // Prevent reentrancy into dispatch loop
|
|
26
|
+
|
|
27
|
+
// Statistics
|
|
28
|
+
var stats = {
|
|
29
|
+
bundlesScheduled: 0,
|
|
30
|
+
bundlesWritten: 0,
|
|
31
|
+
bundlesDropped: 0,
|
|
32
|
+
bufferOverruns: 0,
|
|
33
|
+
eventsPending: 0,
|
|
34
|
+
maxEventsPending: 0,
|
|
35
|
+
eventsCancelled: 0,
|
|
36
|
+
totalDispatches: 0,
|
|
37
|
+
totalLateDispatchMs: 0,
|
|
38
|
+
maxLateDispatchMs: 0,
|
|
39
|
+
totalSendTasks: 0,
|
|
40
|
+
totalSendProcessMs: 0,
|
|
41
|
+
maxSendProcessMs: 0
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Timing constants
|
|
45
|
+
var NTP_EPOCH_OFFSET = 2208988800; // Seconds from 1900-01-01 to 1970-01-01
|
|
46
|
+
var POLL_INTERVAL_MS = 25; // Check every 25ms
|
|
47
|
+
var LOOKAHEAD_S = 0.100; // 100ms lookahead window
|
|
48
|
+
|
|
49
|
+
function schedulerLog() {
|
|
50
|
+
// Toggle to true for verbose diagnostics
|
|
51
|
+
var DEBUG = true; // Enable for debugging
|
|
52
|
+
if (DEBUG) {
|
|
53
|
+
console.log.apply(console, arguments);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// NTP TIME HELPERS
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get current NTP time from system clock
|
|
63
|
+
*
|
|
64
|
+
* Bundles contain full NTP timestamps. We just need to compare them against
|
|
65
|
+
* current NTP time (system clock) to know when to dispatch.
|
|
66
|
+
*
|
|
67
|
+
* AudioContext timing, drift correction, etc. are handled by the C++ side.
|
|
68
|
+
* The prescheduler only needs to know "what time is it now in NTP?"
|
|
69
|
+
*/
|
|
70
|
+
function getCurrentNTP() {
|
|
71
|
+
// Convert current system time to NTP
|
|
72
|
+
var perfTimeMs = performance.timeOrigin + performance.now();
|
|
73
|
+
return (perfTimeMs / 1000) + NTP_EPOCH_OFFSET;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Extract NTP timestamp from OSC bundle
|
|
78
|
+
* Returns NTP time in seconds (double), or null if not a bundle
|
|
79
|
+
*/
|
|
80
|
+
function extractNTPFromBundle(oscData) {
|
|
81
|
+
if (oscData.length >= 16 && oscData[0] === 0x23) { // '#bundle'
|
|
82
|
+
var view = new DataView(oscData.buffer, oscData.byteOffset);
|
|
83
|
+
var ntpSeconds = view.getUint32(8, false);
|
|
84
|
+
var ntpFraction = view.getUint32(12, false);
|
|
85
|
+
return ntpSeconds + ntpFraction / 0x100000000;
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Legacy wrapper for backwards compatibility
|
|
92
|
+
*/
|
|
93
|
+
function getBundleTimestamp(oscMessage) {
|
|
94
|
+
return extractNTPFromBundle(oscMessage);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ============================================================================
|
|
98
|
+
// SHARED ARRAY BUFFER ACCESS
|
|
99
|
+
// ============================================================================
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Initialize ring buffer access for writing directly to SharedArrayBuffer
|
|
103
|
+
*/
|
|
104
|
+
function initSharedBuffer() {
|
|
105
|
+
if (!sharedBuffer || !bufferConstants) {
|
|
106
|
+
console.error('[PreScheduler] Cannot init - missing buffer or constants');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
atomicView = new Int32Array(sharedBuffer);
|
|
111
|
+
dataView = new DataView(sharedBuffer);
|
|
112
|
+
uint8View = new Uint8Array(sharedBuffer);
|
|
113
|
+
|
|
114
|
+
// Calculate control indices for ring buffer
|
|
115
|
+
CONTROL_INDICES = {
|
|
116
|
+
IN_HEAD: (ringBufferBase + bufferConstants.CONTROL_START + 0) / 4,
|
|
117
|
+
IN_TAIL: (ringBufferBase + bufferConstants.CONTROL_START + 4) / 4
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
console.log('[PreScheduler] SharedArrayBuffer initialized with direct ring buffer writing');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Write OSC message directly to ring buffer (replaces MessagePort to writer worker)
|
|
125
|
+
* This is now the ONLY place that writes to the ring buffer
|
|
126
|
+
*/
|
|
127
|
+
function writeToRingBuffer(oscMessage) {
|
|
128
|
+
if (!sharedBuffer || !atomicView) {
|
|
129
|
+
console.error('[PreScheduler] Not initialized for ring buffer writing');
|
|
130
|
+
stats.bundlesDropped++;
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
var payloadSize = oscMessage.length;
|
|
135
|
+
var totalSize = bufferConstants.MESSAGE_HEADER_SIZE + payloadSize;
|
|
136
|
+
|
|
137
|
+
// Check if message fits in buffer at all
|
|
138
|
+
if (totalSize > bufferConstants.IN_BUFFER_SIZE - bufferConstants.MESSAGE_HEADER_SIZE) {
|
|
139
|
+
console.error('[PreScheduler] Message too large:', totalSize);
|
|
140
|
+
stats.bundlesDropped++;
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Try to write (non-blocking, single attempt)
|
|
145
|
+
var head = Atomics.load(atomicView, CONTROL_INDICES.IN_HEAD);
|
|
146
|
+
var tail = Atomics.load(atomicView, CONTROL_INDICES.IN_TAIL);
|
|
147
|
+
|
|
148
|
+
// Calculate available space
|
|
149
|
+
var available = (bufferConstants.IN_BUFFER_SIZE - 1 - head + tail) % bufferConstants.IN_BUFFER_SIZE;
|
|
150
|
+
|
|
151
|
+
if (available < totalSize) {
|
|
152
|
+
// Buffer full - drop message (prescheduler should not block)
|
|
153
|
+
stats.bufferOverruns++;
|
|
154
|
+
stats.bundlesDropped++;
|
|
155
|
+
console.warn('[PreScheduler] Ring buffer full, dropping message');
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Check if message fits contiguously
|
|
160
|
+
var spaceToEnd = bufferConstants.IN_BUFFER_SIZE - head;
|
|
161
|
+
|
|
162
|
+
if (totalSize > spaceToEnd) {
|
|
163
|
+
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
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Wrap to beginning
|
|
179
|
+
head = 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Write message
|
|
183
|
+
var writePos = ringBufferBase + bufferConstants.IN_BUFFER_START + head;
|
|
184
|
+
|
|
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
|
|
190
|
+
|
|
191
|
+
// Write payload
|
|
192
|
+
uint8View.set(oscMessage, writePos + bufferConstants.MESSAGE_HEADER_SIZE);
|
|
193
|
+
|
|
194
|
+
// Update head pointer (publish message)
|
|
195
|
+
var newHead = (head + totalSize) % bufferConstants.IN_BUFFER_SIZE;
|
|
196
|
+
Atomics.store(atomicView, CONTROL_INDICES.IN_HEAD, newHead);
|
|
197
|
+
|
|
198
|
+
stats.bundlesWritten++;
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Schedule an OSC bundle by its NTP timestamp
|
|
204
|
+
* Non-bundles or bundles without timestamps are dispatched immediately
|
|
205
|
+
*/
|
|
206
|
+
function scheduleEvent(oscData, editorId, runTag) {
|
|
207
|
+
var ntpTime = extractNTPFromBundle(oscData);
|
|
208
|
+
|
|
209
|
+
if (ntpTime === null) {
|
|
210
|
+
// Not a bundle - dispatch immediately to ring buffer
|
|
211
|
+
schedulerLog('[PreScheduler] Non-bundle message, dispatching immediately');
|
|
212
|
+
writeToRingBuffer(oscData);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
var currentNTP = getCurrentNTP();
|
|
217
|
+
var timeUntilExec = ntpTime - currentNTP;
|
|
218
|
+
|
|
219
|
+
// Create event with NTP timestamp
|
|
220
|
+
var event = {
|
|
221
|
+
ntpTime: ntpTime,
|
|
222
|
+
seq: sequenceCounter++,
|
|
223
|
+
editorId: editorId || 0,
|
|
224
|
+
runTag: runTag || '',
|
|
225
|
+
oscData: oscData
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
heapPush(event);
|
|
229
|
+
|
|
230
|
+
stats.bundlesScheduled++;
|
|
231
|
+
stats.eventsPending = eventHeap.length;
|
|
232
|
+
if (stats.eventsPending > stats.maxEventsPending) {
|
|
233
|
+
stats.maxEventsPending = stats.eventsPending;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
schedulerLog('[PreScheduler] Scheduled bundle:',
|
|
237
|
+
'NTP=' + ntpTime.toFixed(3),
|
|
238
|
+
'current=' + currentNTP.toFixed(3),
|
|
239
|
+
'wait=' + (timeUntilExec * 1000).toFixed(1) + 'ms',
|
|
240
|
+
'pending=' + stats.eventsPending);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function heapPush(event) {
|
|
244
|
+
eventHeap.push(event);
|
|
245
|
+
siftUp(eventHeap.length - 1);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function heapPeek() {
|
|
249
|
+
return eventHeap.length > 0 ? eventHeap[0] : null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function heapPop() {
|
|
253
|
+
if (eventHeap.length === 0) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
var top = eventHeap[0];
|
|
257
|
+
var last = eventHeap.pop();
|
|
258
|
+
if (eventHeap.length > 0) {
|
|
259
|
+
eventHeap[0] = last;
|
|
260
|
+
siftDown(0);
|
|
261
|
+
}
|
|
262
|
+
return top;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function siftUp(index) {
|
|
266
|
+
while (index > 0) {
|
|
267
|
+
var parent = Math.floor((index - 1) / 2);
|
|
268
|
+
if (compareEvents(eventHeap[index], eventHeap[parent]) >= 0) {
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
swap(index, parent);
|
|
272
|
+
index = parent;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function siftDown(index) {
|
|
277
|
+
var length = eventHeap.length;
|
|
278
|
+
while (true) {
|
|
279
|
+
var left = 2 * index + 1;
|
|
280
|
+
var right = 2 * index + 2;
|
|
281
|
+
var smallest = index;
|
|
282
|
+
|
|
283
|
+
if (left < length && compareEvents(eventHeap[left], eventHeap[smallest]) < 0) {
|
|
284
|
+
smallest = left;
|
|
285
|
+
}
|
|
286
|
+
if (right < length && compareEvents(eventHeap[right], eventHeap[smallest]) < 0) {
|
|
287
|
+
smallest = right;
|
|
288
|
+
}
|
|
289
|
+
if (smallest === index) {
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
swap(index, smallest);
|
|
293
|
+
index = smallest;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function compareEvents(a, b) {
|
|
298
|
+
if (a.ntpTime === b.ntpTime) {
|
|
299
|
+
return a.seq - b.seq;
|
|
300
|
+
}
|
|
301
|
+
return a.ntpTime - b.ntpTime;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function swap(i, j) {
|
|
305
|
+
var tmp = eventHeap[i];
|
|
306
|
+
eventHeap[i] = eventHeap[j];
|
|
307
|
+
eventHeap[j] = tmp;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Start periodic polling (called once on init)
|
|
312
|
+
*/
|
|
313
|
+
function startPeriodicPolling() {
|
|
314
|
+
if (periodicTimer !== null) {
|
|
315
|
+
console.warn('[PreScheduler] Polling already started');
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
console.log('[PreScheduler] Starting periodic polling (every ' + POLL_INTERVAL_MS + 'ms)');
|
|
320
|
+
checkAndDispatch(); // Start immediately
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Stop periodic polling
|
|
325
|
+
*/
|
|
326
|
+
function stopPeriodicPolling() {
|
|
327
|
+
if (periodicTimer !== null) {
|
|
328
|
+
clearTimeout(periodicTimer);
|
|
329
|
+
periodicTimer = null;
|
|
330
|
+
console.log('[PreScheduler] Stopped periodic polling');
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Periodic check and dispatch function
|
|
336
|
+
* Uses NTP timestamps and global offset for drift-free timing
|
|
337
|
+
*/
|
|
338
|
+
function checkAndDispatch() {
|
|
339
|
+
isDispatching = true;
|
|
340
|
+
|
|
341
|
+
var currentNTP = getCurrentNTP();
|
|
342
|
+
var lookaheadTime = currentNTP + LOOKAHEAD_S;
|
|
343
|
+
var dispatchCount = 0;
|
|
344
|
+
var dispatchStart = performance.now();
|
|
345
|
+
|
|
346
|
+
// Dispatch all bundles that are ready
|
|
347
|
+
while (eventHeap.length > 0) {
|
|
348
|
+
var nextEvent = heapPeek();
|
|
349
|
+
|
|
350
|
+
if (nextEvent.ntpTime <= lookaheadTime) {
|
|
351
|
+
// Ready to dispatch
|
|
352
|
+
heapPop();
|
|
353
|
+
stats.eventsPending = eventHeap.length;
|
|
354
|
+
|
|
355
|
+
var timeUntilExec = nextEvent.ntpTime - currentNTP;
|
|
356
|
+
stats.totalDispatches++;
|
|
357
|
+
|
|
358
|
+
schedulerLog('[PreScheduler] Dispatching bundle:',
|
|
359
|
+
'NTP=' + nextEvent.ntpTime.toFixed(3),
|
|
360
|
+
'current=' + currentNTP.toFixed(3),
|
|
361
|
+
'early=' + (timeUntilExec * 1000).toFixed(1) + 'ms',
|
|
362
|
+
'remaining=' + stats.eventsPending);
|
|
363
|
+
|
|
364
|
+
writeToRingBuffer(nextEvent.oscData);
|
|
365
|
+
dispatchCount++;
|
|
366
|
+
} else {
|
|
367
|
+
// Rest aren't ready yet (heap is sorted)
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (dispatchCount > 0 || eventHeap.length > 0) {
|
|
373
|
+
schedulerLog('[PreScheduler] Dispatch cycle complete:',
|
|
374
|
+
'dispatched=' + dispatchCount,
|
|
375
|
+
'pending=' + eventHeap.length);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
isDispatching = false;
|
|
379
|
+
|
|
380
|
+
// Reschedule for next check (fixed interval)
|
|
381
|
+
periodicTimer = setTimeout(checkAndDispatch, POLL_INTERVAL_MS);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function cancelBy(predicate) {
|
|
385
|
+
if (eventHeap.length === 0) {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
var before = eventHeap.length;
|
|
390
|
+
var remaining = [];
|
|
391
|
+
|
|
392
|
+
for (var i = 0; i < eventHeap.length; i++) {
|
|
393
|
+
var event = eventHeap[i];
|
|
394
|
+
if (!predicate(event)) {
|
|
395
|
+
remaining.push(event);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
var removed = before - remaining.length;
|
|
400
|
+
if (removed > 0) {
|
|
401
|
+
eventHeap = remaining;
|
|
402
|
+
heapify();
|
|
403
|
+
stats.eventsCancelled += removed;
|
|
404
|
+
stats.eventsPending = eventHeap.length;
|
|
405
|
+
console.log('[PreScheduler] Cancelled ' + removed + ' events, ' + eventHeap.length + ' remaining');
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function heapify() {
|
|
410
|
+
for (var i = Math.floor(eventHeap.length / 2) - 1; i >= 0; i--) {
|
|
411
|
+
siftDown(i);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function cancelEditorTag(editorId, runTag) {
|
|
416
|
+
cancelBy(function(event) {
|
|
417
|
+
return event.editorId === editorId && event.runTag === runTag;
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function cancelEditor(editorId) {
|
|
422
|
+
cancelBy(function(event) {
|
|
423
|
+
return event.editorId === editorId;
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function cancelAllTags() {
|
|
428
|
+
if (eventHeap.length === 0) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
var cancelled = eventHeap.length;
|
|
432
|
+
stats.eventsCancelled += cancelled;
|
|
433
|
+
eventHeap = [];
|
|
434
|
+
stats.eventsPending = 0;
|
|
435
|
+
console.log('[PreScheduler] Cancelled all ' + cancelled + ' events');
|
|
436
|
+
// Note: Periodic timer continues running (it will just find empty queue)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Helpers reused from legacy worker for immediate send
|
|
440
|
+
function isBundle(data) {
|
|
441
|
+
if (!data || data.length < 8) {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
return data[0] === 0x23 && data[1] === 0x62 && data[2] === 0x75 && data[3] === 0x6e &&
|
|
445
|
+
data[4] === 0x64 && data[5] === 0x6c && data[6] === 0x65 && data[7] === 0x00;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function extractMessagesFromBundle(data) {
|
|
449
|
+
var messages = [];
|
|
450
|
+
var view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
451
|
+
var offset = 16; // skip "#bundle\0" + timetag
|
|
452
|
+
|
|
453
|
+
while (offset < data.length) {
|
|
454
|
+
var messageSize = view.getInt32(offset, false);
|
|
455
|
+
offset += 4;
|
|
456
|
+
|
|
457
|
+
if (messageSize <= 0 || offset + messageSize > data.length) {
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
var messageData = data.slice(offset, offset + messageSize);
|
|
462
|
+
messages.push(messageData);
|
|
463
|
+
offset += messageSize;
|
|
464
|
+
|
|
465
|
+
while (offset % 4 !== 0 && offset < data.length) {
|
|
466
|
+
offset++;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return messages;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function processImmediate(oscData) {
|
|
474
|
+
if (isBundle(oscData)) {
|
|
475
|
+
var messages = extractMessagesFromBundle(oscData);
|
|
476
|
+
for (var i = 0; i < messages.length; i++) {
|
|
477
|
+
writeToRingBuffer(messages[i]);
|
|
478
|
+
}
|
|
479
|
+
} else {
|
|
480
|
+
writeToRingBuffer(oscData);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Message handling
|
|
485
|
+
self.onmessage = function(event) {
|
|
486
|
+
var data = event.data;
|
|
487
|
+
|
|
488
|
+
try {
|
|
489
|
+
switch (data.type) {
|
|
490
|
+
case 'init':
|
|
491
|
+
sharedBuffer = data.sharedBuffer;
|
|
492
|
+
ringBufferBase = data.ringBufferBase;
|
|
493
|
+
bufferConstants = data.bufferConstants;
|
|
494
|
+
|
|
495
|
+
// Initialize SharedArrayBuffer views (including offset)
|
|
496
|
+
initSharedBuffer();
|
|
497
|
+
|
|
498
|
+
// Start periodic polling
|
|
499
|
+
startPeriodicPolling();
|
|
500
|
+
|
|
501
|
+
schedulerLog('[OSCPreSchedulerWorker] Initialized with NTP-based scheduling and direct ring buffer writing');
|
|
502
|
+
self.postMessage({ type: 'initialized' });
|
|
503
|
+
break;
|
|
504
|
+
|
|
505
|
+
case 'send':
|
|
506
|
+
var sendStart = performance.now();
|
|
507
|
+
|
|
508
|
+
// New NTP-based scheduling: extract NTP from bundle
|
|
509
|
+
// scheduleEvent() will dispatch immediately if not a bundle
|
|
510
|
+
scheduleEvent(
|
|
511
|
+
data.oscData,
|
|
512
|
+
data.editorId || 0,
|
|
513
|
+
data.runTag || ''
|
|
514
|
+
);
|
|
515
|
+
|
|
516
|
+
var sendDuration = performance.now() - sendStart;
|
|
517
|
+
stats.totalSendTasks++;
|
|
518
|
+
stats.totalSendProcessMs += sendDuration;
|
|
519
|
+
if (sendDuration > stats.maxSendProcessMs) {
|
|
520
|
+
stats.maxSendProcessMs = sendDuration;
|
|
521
|
+
}
|
|
522
|
+
break;
|
|
523
|
+
|
|
524
|
+
case 'sendImmediate':
|
|
525
|
+
processImmediate(data.oscData);
|
|
526
|
+
break;
|
|
527
|
+
|
|
528
|
+
case 'cancelEditorTag':
|
|
529
|
+
if (data.runTag !== undefined && data.runTag !== null && data.runTag !== '') {
|
|
530
|
+
cancelEditorTag(data.editorId || 0, data.runTag);
|
|
531
|
+
}
|
|
532
|
+
break;
|
|
533
|
+
|
|
534
|
+
case 'cancelEditor':
|
|
535
|
+
cancelEditor(data.editorId || 0);
|
|
536
|
+
break;
|
|
537
|
+
|
|
538
|
+
case 'cancelAll':
|
|
539
|
+
cancelAllTags();
|
|
540
|
+
break;
|
|
541
|
+
|
|
542
|
+
case 'getStats':
|
|
543
|
+
self.postMessage({
|
|
544
|
+
type: 'stats',
|
|
545
|
+
stats: {
|
|
546
|
+
bundlesScheduled: stats.bundlesScheduled,
|
|
547
|
+
bundlesWritten: stats.bundlesWritten,
|
|
548
|
+
bundlesDropped: stats.bundlesDropped,
|
|
549
|
+
bufferOverruns: stats.bufferOverruns,
|
|
550
|
+
eventsPending: stats.eventsPending,
|
|
551
|
+
maxEventsPending: stats.maxEventsPending,
|
|
552
|
+
eventsCancelled: stats.eventsCancelled,
|
|
553
|
+
totalDispatches: stats.totalDispatches,
|
|
554
|
+
totalLateDispatchMs: stats.totalLateDispatchMs,
|
|
555
|
+
maxLateDispatchMs: stats.maxLateDispatchMs,
|
|
556
|
+
totalSendTasks: stats.totalSendTasks,
|
|
557
|
+
totalSendProcessMs: stats.totalSendProcessMs,
|
|
558
|
+
maxSendProcessMs: stats.maxSendProcessMs
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
break;
|
|
562
|
+
|
|
563
|
+
default:
|
|
564
|
+
console.warn('[OSCPreSchedulerWorker] Unknown message type:', data.type);
|
|
565
|
+
}
|
|
566
|
+
} catch (error) {
|
|
567
|
+
console.error('[OSCPreSchedulerWorker] Error:', error);
|
|
568
|
+
self.postMessage({
|
|
569
|
+
type: 'error',
|
|
570
|
+
error: error.message
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
schedulerLog('[OSCPreSchedulerWorker] Script loaded');
|