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.
@@ -0,0 +1,8 @@
1
+ {
2
+ "wasmFile": "scsynth-nrt.ca1dd592.wasm",
3
+ "wasmFileStable": "scsynth-nrt.wasm",
4
+ "hash": "ca1dd592",
5
+ "buildId": "20251102-111057",
6
+ "buildTime": "2025-11-02T11:10:57Z",
7
+ "gitHash": "cc71875"
8
+ }
Binary file
@@ -0,0 +1,274 @@
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
+ * DEBUG Worker - Receives debug messages from AudioWorklet
11
+ * Uses Atomics.wait() for instant wake when debug logs arrive
12
+ * Reads from DEBUG ring buffer and forwards to main thread
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
+ // Worker state
30
+ var running = false;
31
+
32
+ // Statistics
33
+ var stats = {
34
+ messagesReceived: 0,
35
+ wakeups: 0,
36
+ timeouts: 0,
37
+ bytesRead: 0
38
+ };
39
+
40
+ /**
41
+ * Initialize ring buffer access
42
+ */
43
+ function initRingBuffer(buffer, base, constants) {
44
+ sharedBuffer = buffer;
45
+ ringBufferBase = base;
46
+ bufferConstants = constants;
47
+ atomicView = new Int32Array(sharedBuffer);
48
+ dataView = new DataView(sharedBuffer);
49
+ uint8View = new Uint8Array(sharedBuffer);
50
+
51
+ // Calculate control indices using constants from WASM
52
+ CONTROL_INDICES = {
53
+ DEBUG_HEAD: (ringBufferBase + bufferConstants.CONTROL_START + 16) / 4,
54
+ DEBUG_TAIL: (ringBufferBase + bufferConstants.CONTROL_START + 20) / 4
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Read debug messages from buffer
60
+ */
61
+ function readDebugMessages() {
62
+ var head = Atomics.load(atomicView, CONTROL_INDICES.DEBUG_HEAD);
63
+ var tail = Atomics.load(atomicView, CONTROL_INDICES.DEBUG_TAIL);
64
+
65
+ if (head === tail) {
66
+ return null; // No messages
67
+ }
68
+
69
+ var messages = [];
70
+ var currentTail = tail;
71
+ var messagesRead = 0;
72
+ var maxMessages = 10; // Process up to 10 messages per wake
73
+
74
+ while (currentTail !== head && messagesRead < maxMessages) {
75
+ var readPos = ringBufferBase + bufferConstants.DEBUG_BUFFER_START + currentTail;
76
+
77
+ // Read message header (now always contiguous due to padding)
78
+ var magic = dataView.getUint32(readPos, true);
79
+
80
+ // Check for padding marker - skip to beginning
81
+ if (magic === bufferConstants.PADDING_MAGIC) {
82
+ currentTail = 0;
83
+ continue;
84
+ }
85
+
86
+ // Validate message magic
87
+ if (magic !== bufferConstants.MESSAGE_MAGIC) {
88
+ console.error('[DebugWorker] Corrupted message at position', currentTail);
89
+ // Skip this byte and continue
90
+ currentTail = (currentTail + 1) % bufferConstants.DEBUG_BUFFER_SIZE;
91
+ continue;
92
+ }
93
+
94
+ var length = dataView.getUint32(readPos + 4, true);
95
+ var sequence = dataView.getUint32(readPos + 8, true);
96
+
97
+ // Validate message length
98
+ if (length < bufferConstants.MESSAGE_HEADER_SIZE || length > bufferConstants.DEBUG_BUFFER_SIZE) {
99
+ console.error('[DebugWorker] Invalid message length:', length);
100
+ currentTail = (currentTail + 1) % bufferConstants.DEBUG_BUFFER_SIZE;
101
+ continue;
102
+ }
103
+
104
+ // Read payload (debug text) - now contiguous due to padding
105
+ var payloadLength = length - bufferConstants.MESSAGE_HEADER_SIZE;
106
+ var payloadStart = readPos + bufferConstants.MESSAGE_HEADER_SIZE;
107
+
108
+ // Convert bytes to string using TextDecoder for proper UTF-8 handling
109
+ var payloadBytes = uint8View.slice(payloadStart, payloadStart + payloadLength);
110
+ var decoder = new TextDecoder('utf-8');
111
+ var messageText = decoder.decode(payloadBytes);
112
+
113
+ // Remove trailing newline if present
114
+ if (messageText.endsWith('\n')) {
115
+ messageText = messageText.slice(0, -1);
116
+ }
117
+
118
+ messages.push({
119
+ text: messageText,
120
+ timestamp: performance.now(),
121
+ sequence: sequence
122
+ });
123
+
124
+ // Move to next message
125
+ currentTail = (currentTail + length) % bufferConstants.DEBUG_BUFFER_SIZE;
126
+ messagesRead++;
127
+ stats.messagesReceived++;
128
+ }
129
+
130
+ // Update tail pointer (consume messages)
131
+ if (messagesRead > 0) {
132
+ Atomics.store(atomicView, CONTROL_INDICES.DEBUG_TAIL, currentTail);
133
+ stats.bytesRead += messagesRead;
134
+ }
135
+
136
+ return messages.length > 0 ? messages : null;
137
+ }
138
+
139
+ /**
140
+ * Main wait loop using Atomics.wait for instant wake
141
+ */
142
+ function waitLoop() {
143
+ while (running) {
144
+ try {
145
+ // Get current DEBUG_HEAD value
146
+ var currentHead = Atomics.load(atomicView, CONTROL_INDICES.DEBUG_HEAD);
147
+ var currentTail = Atomics.load(atomicView, CONTROL_INDICES.DEBUG_TAIL);
148
+
149
+ // If buffer is empty, wait for AudioWorklet to notify us
150
+ if (currentHead === currentTail) {
151
+ // Wait for up to 100ms (allows checking stop signal)
152
+ var result = Atomics.wait(atomicView, CONTROL_INDICES.DEBUG_HEAD, currentHead, 100);
153
+
154
+ if (result === 'ok' || result === 'not-equal') {
155
+ // We were notified or value changed!
156
+ stats.wakeups++;
157
+ } else if (result === 'timed-out') {
158
+ stats.timeouts++;
159
+ continue; // Check running flag
160
+ }
161
+ }
162
+
163
+ // Read all available debug messages
164
+ var messages = readDebugMessages();
165
+
166
+ if (messages && messages.length > 0) {
167
+ // Send to main thread
168
+ self.postMessage({
169
+ type: 'debug',
170
+ messages: messages,
171
+ stats: {
172
+ wakeups: stats.wakeups,
173
+ timeouts: stats.timeouts,
174
+ messagesReceived: stats.messagesReceived,
175
+ bytesRead: stats.bytesRead
176
+ }
177
+ });
178
+ }
179
+
180
+ } catch (error) {
181
+ console.error('[DebugWorker] Error in wait loop:', error);
182
+ self.postMessage({
183
+ type: 'error',
184
+ error: error.message
185
+ });
186
+
187
+ // Brief pause on error before retrying (use existing atomicView)
188
+ // Wait on a value that won't change for 10ms as a simple delay
189
+ Atomics.wait(atomicView, 0, atomicView[0], 10);
190
+ }
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Start the wait loop
196
+ */
197
+ function start() {
198
+ if (!sharedBuffer) {
199
+ console.error('[DebugWorker] Cannot start - not initialized');
200
+ return;
201
+ }
202
+
203
+ if (running) {
204
+ console.warn('[DebugWorker] Already running');
205
+ return;
206
+ }
207
+
208
+ running = true;
209
+ waitLoop();
210
+ }
211
+
212
+ /**
213
+ * Stop the wait loop
214
+ */
215
+ function stop() {
216
+ running = false;
217
+ }
218
+
219
+ /**
220
+ * Clear debug buffer
221
+ */
222
+ function clear() {
223
+ if (!sharedBuffer) return;
224
+
225
+ // Reset head and tail to 0
226
+ Atomics.store(atomicView, CONTROL_INDICES.DEBUG_HEAD, 0);
227
+ Atomics.store(atomicView, CONTROL_INDICES.DEBUG_TAIL, 0);
228
+ }
229
+
230
+ /**
231
+ * Handle messages from main thread
232
+ */
233
+ self.onmessage = function(event) {
234
+ var data = event.data;
235
+
236
+ try {
237
+ switch (data.type) {
238
+ case 'init':
239
+ initRingBuffer(data.sharedBuffer, data.ringBufferBase, data.bufferConstants);
240
+ self.postMessage({ type: 'initialized' });
241
+ break;
242
+
243
+ case 'start':
244
+ start();
245
+ break;
246
+
247
+ case 'stop':
248
+ stop();
249
+ break;
250
+
251
+ case 'clear':
252
+ clear();
253
+ break;
254
+
255
+ case 'getStats':
256
+ self.postMessage({
257
+ type: 'stats',
258
+ stats: stats
259
+ });
260
+ break;
261
+
262
+ default:
263
+ console.warn('[DebugWorker] Unknown message type:', data.type);
264
+ }
265
+ } catch (error) {
266
+ console.error('[DebugWorker] Error:', error);
267
+ self.postMessage({
268
+ type: 'error',
269
+ error: error.message
270
+ });
271
+ }
272
+ };
273
+
274
+ console.log('[DebugWorker] Script loaded');
@@ -0,0 +1,274 @@
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 IN Worker - Receives OSC messages from scsynth
11
+ * Uses Atomics.wait() for instant wake when data arrives
12
+ * Reads from OUT ring buffer and forwards to main thread
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
+ // Worker state
30
+ var running = false;
31
+
32
+ // Statistics
33
+ var stats = {
34
+ messagesReceived: 0,
35
+ lastSequenceReceived: -1,
36
+ droppedMessages: 0,
37
+ wakeups: 0,
38
+ timeouts: 0
39
+ };
40
+
41
+ /**
42
+ * Initialize ring buffer access
43
+ */
44
+ function initRingBuffer(buffer, base, constants) {
45
+ sharedBuffer = buffer;
46
+ ringBufferBase = base;
47
+ bufferConstants = constants;
48
+ atomicView = new Int32Array(sharedBuffer);
49
+ dataView = new DataView(sharedBuffer);
50
+ uint8View = new Uint8Array(sharedBuffer);
51
+
52
+ // Calculate control indices using constants from WASM
53
+ CONTROL_INDICES = {
54
+ OUT_HEAD: (ringBufferBase + bufferConstants.CONTROL_START + 8) / 4,
55
+ OUT_TAIL: (ringBufferBase + bufferConstants.CONTROL_START + 12) / 4
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Read all available messages from OUT buffer
61
+ */
62
+ function readMessages() {
63
+ var head = Atomics.load(atomicView, CONTROL_INDICES.OUT_HEAD);
64
+ var tail = Atomics.load(atomicView, CONTROL_INDICES.OUT_TAIL);
65
+
66
+ var messages = [];
67
+
68
+ if (head === tail) {
69
+ return messages; // No messages
70
+ }
71
+
72
+ var currentTail = tail;
73
+ var messagesRead = 0;
74
+ // Process up to 10 messages per wake to balance:
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;
79
+
80
+ while (currentTail !== head && messagesRead < maxMessages) {
81
+ var readPos = ringBufferBase + bufferConstants.OUT_BUFFER_START + currentTail;
82
+
83
+ // Read message header (now always contiguous due to padding)
84
+ var magic = dataView.getUint32(readPos, true);
85
+
86
+ // Check for padding marker - skip to beginning
87
+ if (magic === bufferConstants.PADDING_MAGIC) {
88
+ currentTail = 0;
89
+ continue;
90
+ }
91
+
92
+ if (magic !== bufferConstants.MESSAGE_MAGIC) {
93
+ console.error('[OSCInWorker] Corrupted message at position', currentTail);
94
+ stats.droppedMessages++;
95
+ // Skip this byte and continue
96
+ currentTail = (currentTail + 1) % bufferConstants.OUT_BUFFER_SIZE;
97
+ continue;
98
+ }
99
+
100
+ var length = dataView.getUint32(readPos + 4, true);
101
+ var sequence = dataView.getUint32(readPos + 8, true);
102
+ var padding = dataView.getUint32(readPos + 12, true); // unused padding field
103
+
104
+ // Validate message length
105
+ if (length < bufferConstants.MESSAGE_HEADER_SIZE || length > bufferConstants.OUT_BUFFER_SIZE) {
106
+ console.error('[OSCInWorker] Invalid message length:', length);
107
+ stats.droppedMessages++;
108
+ currentTail = (currentTail + 1) % bufferConstants.OUT_BUFFER_SIZE;
109
+ continue;
110
+ }
111
+
112
+ // Check for dropped messages via sequence
113
+ if (stats.lastSequenceReceived >= 0) {
114
+ var expectedSeq = (stats.lastSequenceReceived + 1) & 0xFFFFFFFF;
115
+ if (sequence !== expectedSeq) {
116
+ var dropped = (sequence - expectedSeq + 0x100000000) & 0xFFFFFFFF;
117
+ if (dropped < 1000) { // Sanity check
118
+ console.warn('[OSCInWorker] Detected', dropped, 'dropped messages');
119
+ stats.droppedMessages += dropped;
120
+ }
121
+ }
122
+ }
123
+ stats.lastSequenceReceived = sequence;
124
+
125
+ // Read payload (OSC binary data) - now contiguous due to padding
126
+ var payloadLength = length - bufferConstants.MESSAGE_HEADER_SIZE;
127
+ var payloadStart = readPos + bufferConstants.MESSAGE_HEADER_SIZE;
128
+
129
+ // Create a proper copy (not a view into SharedArrayBuffer)
130
+ var payload = new Uint8Array(payloadLength);
131
+ for (var i = 0; i < payloadLength; i++) {
132
+ payload[i] = uint8View[payloadStart + i];
133
+ }
134
+
135
+ messages.push({
136
+ oscData: payload,
137
+ sequence: sequence
138
+ });
139
+
140
+ // Move to next message
141
+ currentTail = (currentTail + length) % bufferConstants.OUT_BUFFER_SIZE;
142
+ messagesRead++;
143
+ stats.messagesReceived++;
144
+ }
145
+
146
+ // Update tail pointer (consume messages)
147
+ if (messagesRead > 0) {
148
+ Atomics.store(atomicView, CONTROL_INDICES.OUT_TAIL, currentTail);
149
+ }
150
+
151
+ return messages;
152
+ }
153
+
154
+ /**
155
+ * Main wait loop using Atomics.wait for instant wake
156
+ */
157
+ function waitLoop() {
158
+ while (running) {
159
+ try {
160
+ // Get current OUT_HEAD value
161
+ var currentHead = Atomics.load(atomicView, CONTROL_INDICES.OUT_HEAD);
162
+ var currentTail = Atomics.load(atomicView, CONTROL_INDICES.OUT_TAIL);
163
+
164
+ // If buffer is empty, wait for AudioWorklet to notify us
165
+ if (currentHead === currentTail) {
166
+ // Wait for up to 100ms (allows checking stop signal)
167
+ var result = Atomics.wait(atomicView, CONTROL_INDICES.OUT_HEAD, currentHead, 100);
168
+
169
+ if (result === 'ok' || result === 'not-equal') {
170
+ // We were notified or value changed!
171
+ stats.wakeups++;
172
+ } else if (result === 'timed-out') {
173
+ stats.timeouts++;
174
+ continue; // Check running flag
175
+ }
176
+ }
177
+
178
+ // Read all available messages
179
+ var messages = readMessages();
180
+
181
+ if (messages.length > 0) {
182
+ // Send to main thread
183
+ self.postMessage({
184
+ type: 'messages',
185
+ messages: messages,
186
+ stats: {
187
+ wakeups: stats.wakeups,
188
+ timeouts: stats.timeouts,
189
+ messagesReceived: stats.messagesReceived,
190
+ droppedMessages: stats.droppedMessages
191
+ }
192
+ });
193
+ }
194
+
195
+ } catch (error) {
196
+ console.error('[OSCInWorker] Error in wait loop:', error);
197
+ self.postMessage({
198
+ type: 'error',
199
+ error: error.message
200
+ });
201
+
202
+ // Brief pause on error before retrying (use existing atomicView)
203
+ // Wait on a value that won't change for 10ms as a simple delay
204
+ Atomics.wait(atomicView, 0, atomicView[0], 10);
205
+ }
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Start the wait loop
211
+ */
212
+ function start() {
213
+ if (!sharedBuffer) {
214
+ console.error('[OSCInWorker] Cannot start - not initialized');
215
+ return;
216
+ }
217
+
218
+ if (running) {
219
+ console.warn('[OSCInWorker] Already running');
220
+ return;
221
+ }
222
+
223
+ running = true;
224
+ waitLoop();
225
+ }
226
+
227
+ /**
228
+ * Stop the wait loop
229
+ */
230
+ function stop() {
231
+ running = false;
232
+ }
233
+
234
+ /**
235
+ * Handle messages from main thread
236
+ */
237
+ self.onmessage = function(event) {
238
+ var data = event.data;
239
+
240
+ try {
241
+ switch (data.type) {
242
+ case 'init':
243
+ initRingBuffer(data.sharedBuffer, data.ringBufferBase, data.bufferConstants);
244
+ self.postMessage({ type: 'initialized' });
245
+ break;
246
+
247
+ case 'start':
248
+ start();
249
+ break;
250
+
251
+ case 'stop':
252
+ stop();
253
+ break;
254
+
255
+ case 'getStats':
256
+ self.postMessage({
257
+ type: 'stats',
258
+ stats: stats
259
+ });
260
+ break;
261
+
262
+ default:
263
+ console.warn('[OSCInWorker] Unknown message type:', data.type);
264
+ }
265
+ } catch (error) {
266
+ console.error('[OSCInWorker] Error:', error);
267
+ self.postMessage({
268
+ type: 'error',
269
+ error: error.message
270
+ });
271
+ }
272
+ };
273
+
274
+ console.log('[OSCInWorker] Script loaded');