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/supersonic.js
CHANGED
|
@@ -1,301 +1,3 @@
|
|
|
1
|
-
// js/lib/scsynth_osc.js
|
|
2
|
-
var ScsynthOSC = class {
|
|
3
|
-
constructor() {
|
|
4
|
-
this.workers = {
|
|
5
|
-
oscOut: null,
|
|
6
|
-
oscIn: null,
|
|
7
|
-
debug: null
|
|
8
|
-
};
|
|
9
|
-
this.callbacks = {
|
|
10
|
-
onOSCMessage: null,
|
|
11
|
-
onDebugMessage: null,
|
|
12
|
-
onError: null,
|
|
13
|
-
onInitialized: null
|
|
14
|
-
};
|
|
15
|
-
this.initialized = false;
|
|
16
|
-
this.sharedBuffer = null;
|
|
17
|
-
this.ringBufferBase = null;
|
|
18
|
-
this.bufferConstants = null;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Initialize all workers with SharedArrayBuffer
|
|
22
|
-
*/
|
|
23
|
-
async init(sharedBuffer, ringBufferBase, bufferConstants) {
|
|
24
|
-
if (this.initialized) {
|
|
25
|
-
console.warn("[ScsynthOSC] Already initialized");
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
this.sharedBuffer = sharedBuffer;
|
|
29
|
-
this.ringBufferBase = ringBufferBase;
|
|
30
|
-
this.bufferConstants = bufferConstants;
|
|
31
|
-
try {
|
|
32
|
-
this.workers.oscOut = new Worker("./dist/workers/osc_out_worker.js");
|
|
33
|
-
this.workers.oscIn = new Worker("./dist/workers/osc_in_worker.js");
|
|
34
|
-
this.workers.debug = new Worker("./dist/workers/debug_worker.js");
|
|
35
|
-
this.setupWorkerHandlers();
|
|
36
|
-
const initPromises = [
|
|
37
|
-
this.initWorker(this.workers.oscOut, "OSC OUT"),
|
|
38
|
-
this.initWorker(this.workers.oscIn, "OSC IN"),
|
|
39
|
-
this.initWorker(this.workers.debug, "DEBUG")
|
|
40
|
-
];
|
|
41
|
-
await Promise.all(initPromises);
|
|
42
|
-
this.workers.oscIn.postMessage({ type: "start" });
|
|
43
|
-
this.workers.debug.postMessage({ type: "start" });
|
|
44
|
-
this.initialized = true;
|
|
45
|
-
if (this.callbacks.onInitialized) {
|
|
46
|
-
this.callbacks.onInitialized();
|
|
47
|
-
}
|
|
48
|
-
} catch (error) {
|
|
49
|
-
console.error("[ScsynthOSC] Initialization failed:", error);
|
|
50
|
-
if (this.callbacks.onError) {
|
|
51
|
-
this.callbacks.onError(error);
|
|
52
|
-
}
|
|
53
|
-
throw error;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Initialize a single worker
|
|
58
|
-
*/
|
|
59
|
-
initWorker(worker, name) {
|
|
60
|
-
return new Promise((resolve, reject) => {
|
|
61
|
-
const timeout = setTimeout(() => {
|
|
62
|
-
reject(new Error(`${name} worker initialization timeout`));
|
|
63
|
-
}, 5e3);
|
|
64
|
-
const handler = (event) => {
|
|
65
|
-
if (event.data.type === "initialized") {
|
|
66
|
-
clearTimeout(timeout);
|
|
67
|
-
worker.removeEventListener("message", handler);
|
|
68
|
-
resolve();
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
worker.addEventListener("message", handler);
|
|
72
|
-
worker.postMessage({
|
|
73
|
-
type: "init",
|
|
74
|
-
sharedBuffer: this.sharedBuffer,
|
|
75
|
-
ringBufferBase: this.ringBufferBase,
|
|
76
|
-
bufferConstants: this.bufferConstants
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Set up message handlers for all workers
|
|
82
|
-
*/
|
|
83
|
-
setupWorkerHandlers() {
|
|
84
|
-
this.workers.oscIn.onmessage = (event) => {
|
|
85
|
-
const data = event.data;
|
|
86
|
-
switch (data.type) {
|
|
87
|
-
case "messages":
|
|
88
|
-
if (this.callbacks.onOSCMessage) {
|
|
89
|
-
data.messages.forEach((msg) => {
|
|
90
|
-
this.callbacks.onOSCMessage(msg);
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
break;
|
|
94
|
-
case "error":
|
|
95
|
-
console.error("[ScsynthOSC] OSC IN error:", data.error);
|
|
96
|
-
if (this.callbacks.onError) {
|
|
97
|
-
this.callbacks.onError(data.error, "oscIn");
|
|
98
|
-
}
|
|
99
|
-
break;
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
this.workers.debug.onmessage = (event) => {
|
|
103
|
-
const data = event.data;
|
|
104
|
-
switch (data.type) {
|
|
105
|
-
case "debug":
|
|
106
|
-
if (this.callbacks.onDebugMessage) {
|
|
107
|
-
data.messages.forEach((msg) => {
|
|
108
|
-
this.callbacks.onDebugMessage(msg);
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
break;
|
|
112
|
-
case "error":
|
|
113
|
-
console.error("[ScsynthOSC] DEBUG error:", data.error);
|
|
114
|
-
if (this.callbacks.onError) {
|
|
115
|
-
this.callbacks.onError(data.error, "debug");
|
|
116
|
-
}
|
|
117
|
-
break;
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
this.workers.oscOut.onmessage = (event) => {
|
|
121
|
-
const data = event.data;
|
|
122
|
-
switch (data.type) {
|
|
123
|
-
case "error":
|
|
124
|
-
console.error("[ScsynthOSC] OSC OUT error:", data.error);
|
|
125
|
-
if (this.callbacks.onError) {
|
|
126
|
-
this.callbacks.onError(data.error, "oscOut");
|
|
127
|
-
}
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Send OSC data (message or bundle)
|
|
134
|
-
* - OSC messages are sent immediately
|
|
135
|
-
* - OSC bundles are scheduled based on waitTimeMs (calculated by SuperSonic)
|
|
136
|
-
*
|
|
137
|
-
* @param {Uint8Array} oscData - Binary OSC data (message or bundle)
|
|
138
|
-
* @param {Object} options - Optional metadata (editorId, runTag, waitTimeMs)
|
|
139
|
-
*/
|
|
140
|
-
send(oscData, options = {}) {
|
|
141
|
-
if (!this.initialized) {
|
|
142
|
-
console.error("[ScsynthOSC] Not initialized");
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
const { editorId = 0, runTag = "", waitTimeMs = null } = options;
|
|
146
|
-
this.workers.oscOut.postMessage({
|
|
147
|
-
type: "send",
|
|
148
|
-
oscData,
|
|
149
|
-
editorId,
|
|
150
|
-
runTag,
|
|
151
|
-
waitTimeMs
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Send OSC data immediately, ignoring any bundle timestamps
|
|
156
|
-
* - Extracts all messages from bundles
|
|
157
|
-
* - Sends all messages immediately to scsynth
|
|
158
|
-
* - For applications that don't expect server-side scheduling
|
|
159
|
-
*
|
|
160
|
-
* @param {Uint8Array} oscData - Binary OSC data (message or bundle)
|
|
161
|
-
*/
|
|
162
|
-
sendImmediate(oscData) {
|
|
163
|
-
if (!this.initialized) {
|
|
164
|
-
console.error("[ScsynthOSC] Not initialized");
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
this.workers.oscOut.postMessage({
|
|
168
|
-
type: "sendImmediate",
|
|
169
|
-
oscData
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Cancel scheduled OSC bundles by editor and tag
|
|
174
|
-
*/
|
|
175
|
-
cancelEditorTag(editorId, runTag) {
|
|
176
|
-
if (!this.initialized) return;
|
|
177
|
-
this.workers.oscOut.postMessage({
|
|
178
|
-
type: "cancelEditorTag",
|
|
179
|
-
editorId,
|
|
180
|
-
runTag
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Cancel all scheduled OSC bundles from an editor
|
|
185
|
-
*/
|
|
186
|
-
cancelEditor(editorId) {
|
|
187
|
-
if (!this.initialized) return;
|
|
188
|
-
this.workers.oscOut.postMessage({
|
|
189
|
-
type: "cancelEditor",
|
|
190
|
-
editorId
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* Cancel all scheduled OSC bundles
|
|
195
|
-
*/
|
|
196
|
-
cancelAll() {
|
|
197
|
-
if (!this.initialized) return;
|
|
198
|
-
this.workers.oscOut.postMessage({
|
|
199
|
-
type: "cancelAll"
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* Clear debug buffer
|
|
204
|
-
*/
|
|
205
|
-
clearDebug() {
|
|
206
|
-
if (!this.initialized) return;
|
|
207
|
-
this.workers.debug.postMessage({
|
|
208
|
-
type: "clear"
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* Get statistics from all workers
|
|
213
|
-
*/
|
|
214
|
-
async getStats() {
|
|
215
|
-
if (!this.initialized) {
|
|
216
|
-
return null;
|
|
217
|
-
}
|
|
218
|
-
const statsPromises = [
|
|
219
|
-
this.getWorkerStats(this.workers.oscOut, "oscOut"),
|
|
220
|
-
this.getWorkerStats(this.workers.oscIn, "oscIn"),
|
|
221
|
-
this.getWorkerStats(this.workers.debug, "debug")
|
|
222
|
-
];
|
|
223
|
-
const results = await Promise.all(statsPromises);
|
|
224
|
-
return {
|
|
225
|
-
oscOut: results[0],
|
|
226
|
-
oscIn: results[1],
|
|
227
|
-
debug: results[2]
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* Get stats from a single worker
|
|
232
|
-
*/
|
|
233
|
-
getWorkerStats(worker, name) {
|
|
234
|
-
return new Promise((resolve) => {
|
|
235
|
-
const timeout = setTimeout(() => {
|
|
236
|
-
resolve({ error: "Timeout getting stats" });
|
|
237
|
-
}, 1e3);
|
|
238
|
-
const handler = (event) => {
|
|
239
|
-
if (event.data.type === "stats") {
|
|
240
|
-
clearTimeout(timeout);
|
|
241
|
-
worker.removeEventListener("message", handler);
|
|
242
|
-
resolve(event.data.stats);
|
|
243
|
-
}
|
|
244
|
-
};
|
|
245
|
-
worker.addEventListener("message", handler);
|
|
246
|
-
worker.postMessage({ type: "getStats" });
|
|
247
|
-
});
|
|
248
|
-
}
|
|
249
|
-
/**
|
|
250
|
-
* Set callback for OSC messages received from scsynth
|
|
251
|
-
*/
|
|
252
|
-
onOSCMessage(callback) {
|
|
253
|
-
this.callbacks.onOSCMessage = callback;
|
|
254
|
-
}
|
|
255
|
-
/**
|
|
256
|
-
* Set callback for debug messages
|
|
257
|
-
*/
|
|
258
|
-
onDebugMessage(callback) {
|
|
259
|
-
this.callbacks.onDebugMessage = callback;
|
|
260
|
-
}
|
|
261
|
-
/**
|
|
262
|
-
* Set callback for errors
|
|
263
|
-
*/
|
|
264
|
-
onError(callback) {
|
|
265
|
-
this.callbacks.onError = callback;
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Set callback for initialization complete
|
|
269
|
-
*/
|
|
270
|
-
onInitialized(callback) {
|
|
271
|
-
this.callbacks.onInitialized = callback;
|
|
272
|
-
}
|
|
273
|
-
/**
|
|
274
|
-
* Terminate all workers and cleanup
|
|
275
|
-
*/
|
|
276
|
-
terminate() {
|
|
277
|
-
if (this.workers.oscOut) {
|
|
278
|
-
this.workers.oscOut.postMessage({ type: "stop" });
|
|
279
|
-
this.workers.oscOut.terminate();
|
|
280
|
-
}
|
|
281
|
-
if (this.workers.oscIn) {
|
|
282
|
-
this.workers.oscIn.postMessage({ type: "stop" });
|
|
283
|
-
this.workers.oscIn.terminate();
|
|
284
|
-
}
|
|
285
|
-
if (this.workers.debug) {
|
|
286
|
-
this.workers.debug.postMessage({ type: "stop" });
|
|
287
|
-
this.workers.debug.terminate();
|
|
288
|
-
}
|
|
289
|
-
this.workers = {
|
|
290
|
-
oscOut: null,
|
|
291
|
-
oscIn: null,
|
|
292
|
-
debug: null
|
|
293
|
-
};
|
|
294
|
-
this.initialized = false;
|
|
295
|
-
console.log("[ScsynthOSC] All workers terminated");
|
|
296
|
-
}
|
|
297
|
-
};
|
|
298
|
-
|
|
299
1
|
// js/vendor/osc.js/osc.js
|
|
300
2
|
var osc = {};
|
|
301
3
|
var osc = osc || {};
|
|
@@ -922,145 +624,467 @@ EventEmitter.prototype.removeListener = function() {
|
|
|
922
624
|
this.emit("error", err);
|
|
923
625
|
}
|
|
924
626
|
};
|
|
925
|
-
osc.SLIPPort = function(options) {
|
|
627
|
+
osc.SLIPPort = function(options) {
|
|
628
|
+
var that = this;
|
|
629
|
+
var o = this.options = options || {};
|
|
630
|
+
o.useSLIP = o.useSLIP === void 0 ? true : o.useSLIP;
|
|
631
|
+
this.decoder = new slip.Decoder({
|
|
632
|
+
onMessage: this.decodeOSC.bind(this),
|
|
633
|
+
onError: function(err) {
|
|
634
|
+
that.emit("error", err);
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
var decodeHandler = o.useSLIP ? this.decodeSLIPData : this.decodeOSC;
|
|
638
|
+
this.on("data", decodeHandler.bind(this));
|
|
639
|
+
};
|
|
640
|
+
p = osc.SLIPPort.prototype = Object.create(osc.Port.prototype);
|
|
641
|
+
p.constructor = osc.SLIPPort;
|
|
642
|
+
p.encodeOSC = function(packet) {
|
|
643
|
+
packet = packet.buffer ? packet.buffer : packet;
|
|
644
|
+
var framed;
|
|
645
|
+
try {
|
|
646
|
+
var encoded = osc.writePacket(packet, this.options);
|
|
647
|
+
framed = slip.encode(encoded);
|
|
648
|
+
} catch (err) {
|
|
649
|
+
this.emit("error", err);
|
|
650
|
+
}
|
|
651
|
+
return framed;
|
|
652
|
+
};
|
|
653
|
+
p.decodeSLIPData = function(data, packetInfo) {
|
|
654
|
+
this.decoder.decode(data, packetInfo);
|
|
655
|
+
};
|
|
656
|
+
osc.relay = function(from, to, eventName, sendFnName, transformFn, sendArgs) {
|
|
657
|
+
eventName = eventName || "message";
|
|
658
|
+
sendFnName = sendFnName || "send";
|
|
659
|
+
transformFn = transformFn || function() {
|
|
660
|
+
};
|
|
661
|
+
sendArgs = sendArgs ? [null].concat(sendArgs) : [];
|
|
662
|
+
var listener = function(data) {
|
|
663
|
+
sendArgs[0] = data;
|
|
664
|
+
data = transformFn(data);
|
|
665
|
+
to[sendFnName].apply(to, sendArgs);
|
|
666
|
+
};
|
|
667
|
+
from.on(eventName, listener);
|
|
668
|
+
return {
|
|
669
|
+
eventName,
|
|
670
|
+
listener
|
|
671
|
+
};
|
|
672
|
+
};
|
|
673
|
+
osc.relayPorts = function(from, to, o) {
|
|
674
|
+
var eventName = o.raw ? "raw" : "osc", sendFnName = o.raw ? "sendRaw" : "send";
|
|
675
|
+
return osc.relay(from, to, eventName, sendFnName, o.transform);
|
|
676
|
+
};
|
|
677
|
+
osc.stopRelaying = function(from, relaySpec) {
|
|
678
|
+
from.removeListener(relaySpec.eventName, relaySpec.listener);
|
|
679
|
+
};
|
|
680
|
+
osc.Relay = function(port1, port2, options) {
|
|
681
|
+
var o = this.options = options || {};
|
|
682
|
+
o.raw = false;
|
|
683
|
+
this.port1 = port1;
|
|
684
|
+
this.port2 = port2;
|
|
685
|
+
this.listen();
|
|
686
|
+
};
|
|
687
|
+
p = osc.Relay.prototype = Object.create(EventEmitter.prototype);
|
|
688
|
+
p.constructor = osc.Relay;
|
|
689
|
+
p.open = function() {
|
|
690
|
+
this.port1.open();
|
|
691
|
+
this.port2.open();
|
|
692
|
+
};
|
|
693
|
+
p.listen = function() {
|
|
694
|
+
if (this.port1Spec && this.port2Spec) {
|
|
695
|
+
this.close();
|
|
696
|
+
}
|
|
697
|
+
this.port1Spec = osc.relayPorts(this.port1, this.port2, this.options);
|
|
698
|
+
this.port2Spec = osc.relayPorts(this.port2, this.port1, this.options);
|
|
699
|
+
var closeListener = this.close.bind(this);
|
|
700
|
+
this.port1.on("close", closeListener);
|
|
701
|
+
this.port2.on("close", closeListener);
|
|
702
|
+
};
|
|
703
|
+
p.close = function() {
|
|
704
|
+
osc.stopRelaying(this.port1, this.port1Spec);
|
|
705
|
+
osc.stopRelaying(this.port2, this.port2Spec);
|
|
706
|
+
this.emit("close", this.port1, this.port2);
|
|
707
|
+
};
|
|
708
|
+
})();
|
|
709
|
+
(function() {
|
|
710
|
+
"use strict";
|
|
711
|
+
osc.WebSocket = typeof WebSocket !== "undefined" ? WebSocket : void 0;
|
|
712
|
+
osc.WebSocketPort = function(options) {
|
|
713
|
+
osc.Port.call(this, options);
|
|
714
|
+
this.on("open", this.listen.bind(this));
|
|
715
|
+
this.socket = options.socket;
|
|
716
|
+
if (this.socket) {
|
|
717
|
+
if (this.socket.readyState === 1) {
|
|
718
|
+
osc.WebSocketPort.setupSocketForBinary(this.socket);
|
|
719
|
+
this.emit("open", this.socket);
|
|
720
|
+
} else {
|
|
721
|
+
this.open();
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
var p = osc.WebSocketPort.prototype = Object.create(osc.Port.prototype);
|
|
726
|
+
p.constructor = osc.WebSocketPort;
|
|
727
|
+
p.open = function() {
|
|
728
|
+
if (!this.socket || this.socket.readyState > 1) {
|
|
729
|
+
this.socket = new osc.WebSocket(this.options.url);
|
|
730
|
+
}
|
|
731
|
+
osc.WebSocketPort.setupSocketForBinary(this.socket);
|
|
732
|
+
var that = this;
|
|
733
|
+
this.socket.onopen = function() {
|
|
734
|
+
that.emit("open", that.socket);
|
|
735
|
+
};
|
|
736
|
+
this.socket.onerror = function(err) {
|
|
737
|
+
that.emit("error", err);
|
|
738
|
+
};
|
|
739
|
+
};
|
|
740
|
+
p.listen = function() {
|
|
926
741
|
var that = this;
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
});
|
|
935
|
-
var decodeHandler = o.useSLIP ? this.decodeSLIPData : this.decodeOSC;
|
|
936
|
-
this.on("data", decodeHandler.bind(this));
|
|
742
|
+
this.socket.onmessage = function(e) {
|
|
743
|
+
that.emit("data", e.data, e);
|
|
744
|
+
};
|
|
745
|
+
this.socket.onclose = function(e) {
|
|
746
|
+
that.emit("close", e);
|
|
747
|
+
};
|
|
748
|
+
that.emit("ready");
|
|
937
749
|
};
|
|
938
|
-
p
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
var framed;
|
|
943
|
-
try {
|
|
944
|
-
var encoded = osc.writePacket(packet, this.options);
|
|
945
|
-
framed = slip.encode(encoded);
|
|
946
|
-
} catch (err) {
|
|
947
|
-
this.emit("error", err);
|
|
750
|
+
p.sendRaw = function(encoded) {
|
|
751
|
+
if (!this.socket || this.socket.readyState !== 1) {
|
|
752
|
+
osc.fireClosedPortSendError(this);
|
|
753
|
+
return;
|
|
948
754
|
}
|
|
949
|
-
|
|
755
|
+
this.socket.send(encoded);
|
|
950
756
|
};
|
|
951
|
-
p.
|
|
952
|
-
this.
|
|
757
|
+
p.close = function(code, reason) {
|
|
758
|
+
this.socket.close(code, reason);
|
|
953
759
|
};
|
|
954
|
-
osc.
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
760
|
+
osc.WebSocketPort.setupSocketForBinary = function(socket) {
|
|
761
|
+
socket.binaryType = osc.isNode ? "nodebuffer" : "arraybuffer";
|
|
762
|
+
};
|
|
763
|
+
})();
|
|
764
|
+
var osc_default = osc;
|
|
765
|
+
var { readPacket, writePacket, readMessage, writeMessage, readBundle, writeBundle } = osc;
|
|
766
|
+
|
|
767
|
+
// js/lib/scsynth_osc.js
|
|
768
|
+
var ScsynthOSC = class {
|
|
769
|
+
constructor() {
|
|
770
|
+
this.workers = {
|
|
771
|
+
oscOut: null,
|
|
772
|
+
// Scheduler worker (now also writes directly to ring buffer)
|
|
773
|
+
oscIn: null,
|
|
774
|
+
debug: null
|
|
958
775
|
};
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
776
|
+
this.callbacks = {
|
|
777
|
+
onRawOSC: null,
|
|
778
|
+
// Raw binary OSC callback
|
|
779
|
+
onParsedOSC: null,
|
|
780
|
+
// Parsed OSC callback
|
|
781
|
+
onDebugMessage: null,
|
|
782
|
+
onError: null,
|
|
783
|
+
onInitialized: null
|
|
964
784
|
};
|
|
965
|
-
|
|
785
|
+
this.initialized = false;
|
|
786
|
+
this.sharedBuffer = null;
|
|
787
|
+
this.ringBufferBase = null;
|
|
788
|
+
this.bufferConstants = null;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Initialize all workers with SharedArrayBuffer
|
|
792
|
+
*/
|
|
793
|
+
async init(sharedBuffer, ringBufferBase, bufferConstants) {
|
|
794
|
+
if (this.initialized) {
|
|
795
|
+
console.warn("[ScsynthOSC] Already initialized");
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
this.sharedBuffer = sharedBuffer;
|
|
799
|
+
this.ringBufferBase = ringBufferBase;
|
|
800
|
+
this.bufferConstants = bufferConstants;
|
|
801
|
+
try {
|
|
802
|
+
this.workers.oscOut = new Worker("./dist/workers/osc_out_prescheduler_worker.js");
|
|
803
|
+
this.workers.oscIn = new Worker("./dist/workers/osc_in_worker.js");
|
|
804
|
+
this.workers.debug = new Worker("./dist/workers/debug_worker.js");
|
|
805
|
+
this.setupWorkerHandlers();
|
|
806
|
+
const initPromises = [
|
|
807
|
+
this.initWorker(this.workers.oscOut, "OSC SCHEDULER+WRITER"),
|
|
808
|
+
this.initWorker(this.workers.oscIn, "OSC IN"),
|
|
809
|
+
this.initWorker(this.workers.debug, "DEBUG")
|
|
810
|
+
];
|
|
811
|
+
await Promise.all(initPromises);
|
|
812
|
+
this.workers.oscIn.postMessage({ type: "start" });
|
|
813
|
+
this.workers.debug.postMessage({ type: "start" });
|
|
814
|
+
this.initialized = true;
|
|
815
|
+
if (this.callbacks.onInitialized) {
|
|
816
|
+
this.callbacks.onInitialized();
|
|
817
|
+
}
|
|
818
|
+
} catch (error) {
|
|
819
|
+
console.error("[ScsynthOSC] Initialization failed:", error);
|
|
820
|
+
if (this.callbacks.onError) {
|
|
821
|
+
this.callbacks.onError(error);
|
|
822
|
+
}
|
|
823
|
+
throw error;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Initialize a single worker
|
|
828
|
+
*/
|
|
829
|
+
initWorker(worker, name) {
|
|
830
|
+
return new Promise((resolve, reject) => {
|
|
831
|
+
const timeout = setTimeout(() => {
|
|
832
|
+
reject(new Error(`${name} worker initialization timeout`));
|
|
833
|
+
}, 5e3);
|
|
834
|
+
const handler = (event) => {
|
|
835
|
+
if (event.data.type === "initialized") {
|
|
836
|
+
clearTimeout(timeout);
|
|
837
|
+
worker.removeEventListener("message", handler);
|
|
838
|
+
resolve();
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
worker.addEventListener("message", handler);
|
|
842
|
+
worker.postMessage({
|
|
843
|
+
type: "init",
|
|
844
|
+
sharedBuffer: this.sharedBuffer,
|
|
845
|
+
ringBufferBase: this.ringBufferBase,
|
|
846
|
+
bufferConstants: this.bufferConstants
|
|
847
|
+
});
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Set up message handlers for all workers
|
|
852
|
+
*/
|
|
853
|
+
setupWorkerHandlers() {
|
|
854
|
+
this.workers.oscIn.onmessage = (event) => {
|
|
855
|
+
const data = event.data;
|
|
856
|
+
switch (data.type) {
|
|
857
|
+
case "messages":
|
|
858
|
+
data.messages.forEach((msg) => {
|
|
859
|
+
if (!msg.oscData) return;
|
|
860
|
+
if (this.callbacks.onRawOSC) {
|
|
861
|
+
this.callbacks.onRawOSC({
|
|
862
|
+
oscData: msg.oscData,
|
|
863
|
+
sequence: msg.sequence
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
if (this.callbacks.onParsedOSC) {
|
|
867
|
+
try {
|
|
868
|
+
const options = { metadata: false, unpackSingleArgs: false };
|
|
869
|
+
const decoded = osc_default.readPacket(msg.oscData, options);
|
|
870
|
+
this.callbacks.onParsedOSC(decoded);
|
|
871
|
+
} catch (e) {
|
|
872
|
+
console.error("[ScsynthOSC] Failed to decode OSC message:", e, msg);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
break;
|
|
877
|
+
case "error":
|
|
878
|
+
console.error("[ScsynthOSC] OSC IN error:", data.error);
|
|
879
|
+
if (this.callbacks.onError) {
|
|
880
|
+
this.callbacks.onError(data.error, "oscIn");
|
|
881
|
+
}
|
|
882
|
+
break;
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
this.workers.debug.onmessage = (event) => {
|
|
886
|
+
const data = event.data;
|
|
887
|
+
switch (data.type) {
|
|
888
|
+
case "debug":
|
|
889
|
+
if (this.callbacks.onDebugMessage) {
|
|
890
|
+
data.messages.forEach((msg) => {
|
|
891
|
+
this.callbacks.onDebugMessage(msg);
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
break;
|
|
895
|
+
case "error":
|
|
896
|
+
console.error("[ScsynthOSC] DEBUG error:", data.error);
|
|
897
|
+
if (this.callbacks.onError) {
|
|
898
|
+
this.callbacks.onError(data.error, "debug");
|
|
899
|
+
}
|
|
900
|
+
break;
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
this.workers.oscOut.onmessage = (event) => {
|
|
904
|
+
const data = event.data;
|
|
905
|
+
switch (data.type) {
|
|
906
|
+
case "error":
|
|
907
|
+
console.error("[ScsynthOSC] OSC OUT error:", data.error);
|
|
908
|
+
if (this.callbacks.onError) {
|
|
909
|
+
this.callbacks.onError(data.error, "oscOut");
|
|
910
|
+
}
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Send OSC data (message or bundle)
|
|
917
|
+
* - OSC messages are sent immediately
|
|
918
|
+
* - OSC bundles are scheduled based on audioTimeS (target audio time)
|
|
919
|
+
*
|
|
920
|
+
* @param {Uint8Array} oscData - Binary OSC data (message or bundle)
|
|
921
|
+
* @param {Object} options - Optional metadata (editorId, runTag, audioTimeS, currentTimeS)
|
|
922
|
+
*/
|
|
923
|
+
send(oscData, options = {}) {
|
|
924
|
+
if (!this.initialized) {
|
|
925
|
+
console.error("[ScsynthOSC] Not initialized");
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
const { editorId = 0, runTag = "", audioTimeS = null, currentTimeS = null } = options;
|
|
929
|
+
this.workers.oscOut.postMessage({
|
|
930
|
+
type: "send",
|
|
931
|
+
oscData,
|
|
932
|
+
editorId,
|
|
933
|
+
runTag,
|
|
934
|
+
audioTimeS,
|
|
935
|
+
currentTimeS
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Send OSC data immediately, ignoring any bundle timestamps
|
|
940
|
+
* - Extracts all messages from bundles
|
|
941
|
+
* - Sends all messages immediately to scsynth
|
|
942
|
+
* - For applications that don't expect server-side scheduling
|
|
943
|
+
*
|
|
944
|
+
* @param {Uint8Array} oscData - Binary OSC data (message or bundle)
|
|
945
|
+
*/
|
|
946
|
+
sendImmediate(oscData) {
|
|
947
|
+
if (!this.initialized) {
|
|
948
|
+
console.error("[ScsynthOSC] Not initialized");
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
this.workers.oscOut.postMessage({
|
|
952
|
+
type: "sendImmediate",
|
|
953
|
+
oscData
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Cancel scheduled OSC bundles by editor and tag
|
|
958
|
+
*/
|
|
959
|
+
cancelEditorTag(editorId, runTag) {
|
|
960
|
+
if (!this.initialized) return;
|
|
961
|
+
this.workers.oscOut.postMessage({
|
|
962
|
+
type: "cancelEditorTag",
|
|
963
|
+
editorId,
|
|
964
|
+
runTag
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Cancel all scheduled OSC bundles from an editor
|
|
969
|
+
*/
|
|
970
|
+
cancelEditor(editorId) {
|
|
971
|
+
if (!this.initialized) return;
|
|
972
|
+
this.workers.oscOut.postMessage({
|
|
973
|
+
type: "cancelEditor",
|
|
974
|
+
editorId
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Cancel all scheduled OSC bundles
|
|
979
|
+
*/
|
|
980
|
+
cancelAll() {
|
|
981
|
+
if (!this.initialized) return;
|
|
982
|
+
this.workers.oscOut.postMessage({
|
|
983
|
+
type: "cancelAll"
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Clear debug buffer
|
|
988
|
+
*/
|
|
989
|
+
clearDebug() {
|
|
990
|
+
if (!this.initialized) return;
|
|
991
|
+
this.workers.debug.postMessage({
|
|
992
|
+
type: "clear"
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Get statistics from all workers
|
|
997
|
+
*/
|
|
998
|
+
async getStats() {
|
|
999
|
+
if (!this.initialized) {
|
|
1000
|
+
return null;
|
|
1001
|
+
}
|
|
1002
|
+
const statsPromises = [
|
|
1003
|
+
this.getWorkerStats(this.workers.oscOut, "oscOut"),
|
|
1004
|
+
this.getWorkerStats(this.workers.oscIn, "oscIn"),
|
|
1005
|
+
this.getWorkerStats(this.workers.debug, "debug")
|
|
1006
|
+
];
|
|
1007
|
+
const results = await Promise.all(statsPromises);
|
|
966
1008
|
return {
|
|
967
|
-
|
|
968
|
-
|
|
1009
|
+
oscOut: results[0],
|
|
1010
|
+
oscIn: results[1],
|
|
1011
|
+
debug: results[2]
|
|
969
1012
|
};
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Get stats from a single worker
|
|
1016
|
+
*/
|
|
1017
|
+
getWorkerStats(worker, name) {
|
|
1018
|
+
return new Promise((resolve) => {
|
|
1019
|
+
const timeout = setTimeout(() => {
|
|
1020
|
+
resolve({ error: "Timeout getting stats" });
|
|
1021
|
+
}, 1e3);
|
|
1022
|
+
const handler = (event) => {
|
|
1023
|
+
if (event.data.type === "stats") {
|
|
1024
|
+
clearTimeout(timeout);
|
|
1025
|
+
worker.removeEventListener("message", handler);
|
|
1026
|
+
resolve(event.data.stats);
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
worker.addEventListener("message", handler);
|
|
1030
|
+
worker.postMessage({ type: "getStats" });
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* Set callback for raw binary OSC messages received from scsynth
|
|
1035
|
+
*/
|
|
1036
|
+
onRawOSC(callback) {
|
|
1037
|
+
this.callbacks.onRawOSC = callback;
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Set callback for parsed OSC messages received from scsynth
|
|
1041
|
+
*/
|
|
1042
|
+
onParsedOSC(callback) {
|
|
1043
|
+
this.callbacks.onParsedOSC = callback;
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Set callback for debug messages
|
|
1047
|
+
*/
|
|
1048
|
+
onDebugMessage(callback) {
|
|
1049
|
+
this.callbacks.onDebugMessage = callback;
|
|
1050
|
+
}
|
|
1051
|
+
/**
|
|
1052
|
+
* Set callback for errors
|
|
1053
|
+
*/
|
|
1054
|
+
onError(callback) {
|
|
1055
|
+
this.callbacks.onError = callback;
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Set callback for initialization complete
|
|
1059
|
+
*/
|
|
1060
|
+
onInitialized(callback) {
|
|
1061
|
+
this.callbacks.onInitialized = callback;
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Terminate all workers and cleanup
|
|
1065
|
+
*/
|
|
1066
|
+
terminate() {
|
|
1067
|
+
if (this.workers.oscOut) {
|
|
1068
|
+
this.workers.oscOut.postMessage({ type: "stop" });
|
|
1069
|
+
this.workers.oscOut.terminate();
|
|
994
1070
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
this.port1.on("close", closeListener);
|
|
999
|
-
this.port2.on("close", closeListener);
|
|
1000
|
-
};
|
|
1001
|
-
p.close = function() {
|
|
1002
|
-
osc.stopRelaying(this.port1, this.port1Spec);
|
|
1003
|
-
osc.stopRelaying(this.port2, this.port2Spec);
|
|
1004
|
-
this.emit("close", this.port1, this.port2);
|
|
1005
|
-
};
|
|
1006
|
-
})();
|
|
1007
|
-
(function() {
|
|
1008
|
-
"use strict";
|
|
1009
|
-
osc.WebSocket = typeof WebSocket !== "undefined" ? WebSocket : void 0;
|
|
1010
|
-
osc.WebSocketPort = function(options) {
|
|
1011
|
-
osc.Port.call(this, options);
|
|
1012
|
-
this.on("open", this.listen.bind(this));
|
|
1013
|
-
this.socket = options.socket;
|
|
1014
|
-
if (this.socket) {
|
|
1015
|
-
if (this.socket.readyState === 1) {
|
|
1016
|
-
osc.WebSocketPort.setupSocketForBinary(this.socket);
|
|
1017
|
-
this.emit("open", this.socket);
|
|
1018
|
-
} else {
|
|
1019
|
-
this.open();
|
|
1020
|
-
}
|
|
1071
|
+
if (this.workers.oscIn) {
|
|
1072
|
+
this.workers.oscIn.postMessage({ type: "stop" });
|
|
1073
|
+
this.workers.oscIn.terminate();
|
|
1021
1074
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
p.open = function() {
|
|
1026
|
-
if (!this.socket || this.socket.readyState > 1) {
|
|
1027
|
-
this.socket = new osc.WebSocket(this.options.url);
|
|
1075
|
+
if (this.workers.debug) {
|
|
1076
|
+
this.workers.debug.postMessage({ type: "stop" });
|
|
1077
|
+
this.workers.debug.terminate();
|
|
1028
1078
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
};
|
|
1034
|
-
this.socket.onerror = function(err) {
|
|
1035
|
-
that.emit("error", err);
|
|
1036
|
-
};
|
|
1037
|
-
};
|
|
1038
|
-
p.listen = function() {
|
|
1039
|
-
var that = this;
|
|
1040
|
-
this.socket.onmessage = function(e) {
|
|
1041
|
-
that.emit("data", e.data, e);
|
|
1042
|
-
};
|
|
1043
|
-
this.socket.onclose = function(e) {
|
|
1044
|
-
that.emit("close", e);
|
|
1079
|
+
this.workers = {
|
|
1080
|
+
oscOut: null,
|
|
1081
|
+
oscIn: null,
|
|
1082
|
+
debug: null
|
|
1045
1083
|
};
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
osc.fireClosedPortSendError(this);
|
|
1051
|
-
return;
|
|
1052
|
-
}
|
|
1053
|
-
this.socket.send(encoded);
|
|
1054
|
-
};
|
|
1055
|
-
p.close = function(code, reason) {
|
|
1056
|
-
this.socket.close(code, reason);
|
|
1057
|
-
};
|
|
1058
|
-
osc.WebSocketPort.setupSocketForBinary = function(socket) {
|
|
1059
|
-
socket.binaryType = osc.isNode ? "nodebuffer" : "arraybuffer";
|
|
1060
|
-
};
|
|
1061
|
-
})();
|
|
1062
|
-
var osc_default = osc;
|
|
1063
|
-
var { readPacket, writePacket, readMessage, writeMessage, readBundle, writeBundle } = osc;
|
|
1084
|
+
this.initialized = false;
|
|
1085
|
+
console.log("[ScsynthOSC] All workers terminated");
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1064
1088
|
|
|
1065
1089
|
// node_modules/@thi.ng/api/typedarray.js
|
|
1066
1090
|
var GL2TYPE = {
|
|
@@ -1543,41 +1567,322 @@ var MemPool = class {
|
|
|
1543
1567
|
this.setBlockNext(block, next);
|
|
1544
1568
|
res = true;
|
|
1545
1569
|
}
|
|
1546
|
-
if (block + this.blockSize(block) >= this.top) {
|
|
1547
|
-
this.top = block;
|
|
1548
|
-
prev ? this.unlinkBlock(prev, block) : this._free = this.blockNext(block);
|
|
1570
|
+
if (block + this.blockSize(block) >= this.top) {
|
|
1571
|
+
this.top = block;
|
|
1572
|
+
prev ? this.unlinkBlock(prev, block) : this._free = this.blockNext(block);
|
|
1573
|
+
}
|
|
1574
|
+
prev = block;
|
|
1575
|
+
block = this.blockNext(block);
|
|
1576
|
+
}
|
|
1577
|
+
return res;
|
|
1578
|
+
}
|
|
1579
|
+
/**
|
|
1580
|
+
* Inserts given block into list of free blocks, sorted by address.
|
|
1581
|
+
*
|
|
1582
|
+
* @param block -
|
|
1583
|
+
*/
|
|
1584
|
+
insert(block) {
|
|
1585
|
+
let ptr = this._free;
|
|
1586
|
+
let prev = 0;
|
|
1587
|
+
while (ptr) {
|
|
1588
|
+
if (block <= ptr) break;
|
|
1589
|
+
prev = ptr;
|
|
1590
|
+
ptr = this.blockNext(ptr);
|
|
1591
|
+
}
|
|
1592
|
+
if (prev) {
|
|
1593
|
+
this.setBlockNext(prev, block);
|
|
1594
|
+
} else {
|
|
1595
|
+
this._free = block;
|
|
1596
|
+
}
|
|
1597
|
+
this.setBlockNext(block, ptr);
|
|
1598
|
+
}
|
|
1599
|
+
};
|
|
1600
|
+
var __blockDataAddress = (blockAddress) => blockAddress > 0 ? blockAddress + SIZEOF_MEM_BLOCK : 0;
|
|
1601
|
+
var __blockSelfAddress = (dataAddress) => dataAddress > 0 ? dataAddress - SIZEOF_MEM_BLOCK : 0;
|
|
1602
|
+
|
|
1603
|
+
// js/timing_constants.js
|
|
1604
|
+
var NTP_EPOCH_OFFSET = 2208988800;
|
|
1605
|
+
var DRIFT_UPDATE_INTERVAL_MS = 15e3;
|
|
1606
|
+
|
|
1607
|
+
// js/supersonic.js
|
|
1608
|
+
var BufferManager = class {
|
|
1609
|
+
constructor(options) {
|
|
1610
|
+
const {
|
|
1611
|
+
audioContext,
|
|
1612
|
+
sharedBuffer,
|
|
1613
|
+
bufferPool,
|
|
1614
|
+
allocatedBuffers,
|
|
1615
|
+
resolveAudioPath,
|
|
1616
|
+
registerPendingOp
|
|
1617
|
+
} = options;
|
|
1618
|
+
this.audioContext = audioContext;
|
|
1619
|
+
this.sharedBuffer = sharedBuffer;
|
|
1620
|
+
this.bufferPool = bufferPool;
|
|
1621
|
+
this.allocatedBuffers = allocatedBuffers;
|
|
1622
|
+
this.resolveAudioPath = resolveAudioPath;
|
|
1623
|
+
this.registerPendingOp = registerPendingOp;
|
|
1624
|
+
this.bufferLocks = /* @__PURE__ */ new Map();
|
|
1625
|
+
this.GUARD_BEFORE = 3;
|
|
1626
|
+
this.GUARD_AFTER = 1;
|
|
1627
|
+
this.MAX_BUFFERS = 1024;
|
|
1628
|
+
}
|
|
1629
|
+
#validateBufferNumber(bufnum) {
|
|
1630
|
+
if (!Number.isInteger(bufnum) || bufnum < 0 || bufnum >= this.MAX_BUFFERS) {
|
|
1631
|
+
throw new Error(`Invalid buffer number ${bufnum} (must be 0-${this.MAX_BUFFERS - 1})`);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
async prepareFromFile(params) {
|
|
1635
|
+
const {
|
|
1636
|
+
bufnum,
|
|
1637
|
+
path,
|
|
1638
|
+
startFrame = 0,
|
|
1639
|
+
numFrames = 0,
|
|
1640
|
+
channels = null
|
|
1641
|
+
} = params;
|
|
1642
|
+
this.#validateBufferNumber(bufnum);
|
|
1643
|
+
let allocatedPtr = null;
|
|
1644
|
+
let pendingToken = null;
|
|
1645
|
+
let allocationRegistered = false;
|
|
1646
|
+
const releaseLock = await this.#acquireBufferLock(bufnum);
|
|
1647
|
+
let lockReleased = false;
|
|
1648
|
+
try {
|
|
1649
|
+
await this.#awaitPendingReplacement(bufnum);
|
|
1650
|
+
const resolvedPath = this.resolveAudioPath(path);
|
|
1651
|
+
const response = await fetch(resolvedPath);
|
|
1652
|
+
if (!response.ok) {
|
|
1653
|
+
throw new Error(`Failed to fetch ${resolvedPath}: ${response.status} ${response.statusText}`);
|
|
1654
|
+
}
|
|
1655
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
1656
|
+
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
|
|
1657
|
+
const start = Math.max(0, Math.floor(startFrame || 0));
|
|
1658
|
+
const availableFrames = audioBuffer.length - start;
|
|
1659
|
+
const framesRequested = numFrames && numFrames > 0 ? Math.min(Math.floor(numFrames), availableFrames) : availableFrames;
|
|
1660
|
+
if (framesRequested <= 0) {
|
|
1661
|
+
throw new Error(`No audio frames available for buffer ${bufnum} from ${path}`);
|
|
1662
|
+
}
|
|
1663
|
+
const selectedChannels = this.#normalizeChannels(channels, audioBuffer.numberOfChannels);
|
|
1664
|
+
const numChannels = selectedChannels.length;
|
|
1665
|
+
const totalSamples = framesRequested * numChannels + (this.GUARD_BEFORE + this.GUARD_AFTER) * numChannels;
|
|
1666
|
+
allocatedPtr = this.#malloc(totalSamples);
|
|
1667
|
+
const interleaved = new Float32Array(totalSamples);
|
|
1668
|
+
const dataOffset = this.GUARD_BEFORE * numChannels;
|
|
1669
|
+
for (let frame = 0; frame < framesRequested; frame++) {
|
|
1670
|
+
for (let ch = 0; ch < numChannels; ch++) {
|
|
1671
|
+
const sourceChannel = selectedChannels[ch];
|
|
1672
|
+
const channelData = audioBuffer.getChannelData(sourceChannel);
|
|
1673
|
+
interleaved[dataOffset + frame * numChannels + ch] = channelData[start + frame];
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
this.#writeToSharedBuffer(allocatedPtr, interleaved);
|
|
1677
|
+
const sizeBytes = interleaved.length * 4;
|
|
1678
|
+
const { uuid, allocationComplete } = this.#registerPending(bufnum);
|
|
1679
|
+
pendingToken = uuid;
|
|
1680
|
+
this.#recordAllocation(bufnum, allocatedPtr, sizeBytes, uuid, allocationComplete);
|
|
1681
|
+
allocationRegistered = true;
|
|
1682
|
+
const managedCompletion = this.#attachFinalizer(bufnum, uuid, allocationComplete);
|
|
1683
|
+
releaseLock();
|
|
1684
|
+
lockReleased = true;
|
|
1685
|
+
return {
|
|
1686
|
+
ptr: allocatedPtr,
|
|
1687
|
+
numFrames: framesRequested,
|
|
1688
|
+
numChannels,
|
|
1689
|
+
sampleRate: audioBuffer.sampleRate,
|
|
1690
|
+
uuid,
|
|
1691
|
+
allocationComplete: managedCompletion
|
|
1692
|
+
};
|
|
1693
|
+
} catch (error) {
|
|
1694
|
+
if (allocationRegistered && pendingToken) {
|
|
1695
|
+
this.#finalizeReplacement(bufnum, pendingToken, false);
|
|
1696
|
+
} else if (allocatedPtr) {
|
|
1697
|
+
this.bufferPool.free(allocatedPtr);
|
|
1698
|
+
}
|
|
1699
|
+
throw error;
|
|
1700
|
+
} finally {
|
|
1701
|
+
if (!lockReleased) {
|
|
1702
|
+
releaseLock();
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
async prepareEmpty(params) {
|
|
1707
|
+
const {
|
|
1708
|
+
bufnum,
|
|
1709
|
+
numFrames,
|
|
1710
|
+
numChannels = 1,
|
|
1711
|
+
sampleRate = null
|
|
1712
|
+
} = params;
|
|
1713
|
+
this.#validateBufferNumber(bufnum);
|
|
1714
|
+
let allocationRegistered = false;
|
|
1715
|
+
let pendingToken = null;
|
|
1716
|
+
let allocatedPtr = null;
|
|
1717
|
+
if (!Number.isFinite(numFrames) || numFrames <= 0) {
|
|
1718
|
+
throw new Error(`/b_alloc requires a positive number of frames (got ${numFrames})`);
|
|
1719
|
+
}
|
|
1720
|
+
if (!Number.isFinite(numChannels) || numChannels <= 0) {
|
|
1721
|
+
throw new Error(`/b_alloc requires a positive channel count (got ${numChannels})`);
|
|
1722
|
+
}
|
|
1723
|
+
const roundedFrames = Math.floor(numFrames);
|
|
1724
|
+
const roundedChannels = Math.floor(numChannels);
|
|
1725
|
+
const totalSamples = roundedFrames * roundedChannels + (this.GUARD_BEFORE + this.GUARD_AFTER) * roundedChannels;
|
|
1726
|
+
const releaseLock = await this.#acquireBufferLock(bufnum);
|
|
1727
|
+
let lockReleased = false;
|
|
1728
|
+
try {
|
|
1729
|
+
await this.#awaitPendingReplacement(bufnum);
|
|
1730
|
+
allocatedPtr = this.#malloc(totalSamples);
|
|
1731
|
+
const interleaved = new Float32Array(totalSamples);
|
|
1732
|
+
this.#writeToSharedBuffer(allocatedPtr, interleaved);
|
|
1733
|
+
const sizeBytes = interleaved.length * 4;
|
|
1734
|
+
const { uuid, allocationComplete } = this.#registerPending(bufnum);
|
|
1735
|
+
pendingToken = uuid;
|
|
1736
|
+
this.#recordAllocation(bufnum, allocatedPtr, sizeBytes, uuid, allocationComplete);
|
|
1737
|
+
allocationRegistered = true;
|
|
1738
|
+
const managedCompletion = this.#attachFinalizer(bufnum, uuid, allocationComplete);
|
|
1739
|
+
releaseLock();
|
|
1740
|
+
lockReleased = true;
|
|
1741
|
+
return {
|
|
1742
|
+
ptr: allocatedPtr,
|
|
1743
|
+
numFrames: roundedFrames,
|
|
1744
|
+
numChannels: roundedChannels,
|
|
1745
|
+
sampleRate: sampleRate || this.audioContext.sampleRate,
|
|
1746
|
+
uuid,
|
|
1747
|
+
allocationComplete: managedCompletion
|
|
1748
|
+
};
|
|
1749
|
+
} catch (error) {
|
|
1750
|
+
if (allocationRegistered && pendingToken) {
|
|
1751
|
+
this.#finalizeReplacement(bufnum, pendingToken, false);
|
|
1752
|
+
} else if (allocatedPtr) {
|
|
1753
|
+
this.bufferPool.free(allocatedPtr);
|
|
1754
|
+
}
|
|
1755
|
+
throw error;
|
|
1756
|
+
} finally {
|
|
1757
|
+
if (!lockReleased) {
|
|
1758
|
+
releaseLock();
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
#normalizeChannels(requestedChannels, fileChannels) {
|
|
1763
|
+
if (!requestedChannels || requestedChannels.length === 0) {
|
|
1764
|
+
return Array.from({ length: fileChannels }, (_, i) => i);
|
|
1765
|
+
}
|
|
1766
|
+
requestedChannels.forEach((channel) => {
|
|
1767
|
+
if (!Number.isInteger(channel) || channel < 0 || channel >= fileChannels) {
|
|
1768
|
+
throw new Error(`Channel ${channel} is out of range (file has ${fileChannels} channels)`);
|
|
1769
|
+
}
|
|
1770
|
+
});
|
|
1771
|
+
return requestedChannels;
|
|
1772
|
+
}
|
|
1773
|
+
#malloc(totalSamples) {
|
|
1774
|
+
const bytesNeeded = totalSamples * 4;
|
|
1775
|
+
const ptr = this.bufferPool.malloc(bytesNeeded);
|
|
1776
|
+
if (ptr === 0) {
|
|
1777
|
+
const stats = this.bufferPool.stats();
|
|
1778
|
+
const availableMB = ((stats.available || 0) / (1024 * 1024)).toFixed(2);
|
|
1779
|
+
const totalMB = ((stats.total || 0) / (1024 * 1024)).toFixed(2);
|
|
1780
|
+
const requestedMB = (bytesNeeded / (1024 * 1024)).toFixed(2);
|
|
1781
|
+
throw new Error(
|
|
1782
|
+
`Buffer pool allocation failed: requested ${requestedMB}MB, available ${availableMB}MB of ${totalMB}MB total`
|
|
1783
|
+
);
|
|
1784
|
+
}
|
|
1785
|
+
return ptr;
|
|
1786
|
+
}
|
|
1787
|
+
#writeToSharedBuffer(ptr, data) {
|
|
1788
|
+
const heap = new Float32Array(this.sharedBuffer, ptr, data.length);
|
|
1789
|
+
heap.set(data);
|
|
1790
|
+
}
|
|
1791
|
+
#registerPending(bufnum) {
|
|
1792
|
+
if (!this.registerPendingOp) {
|
|
1793
|
+
return {
|
|
1794
|
+
uuid: crypto.randomUUID(),
|
|
1795
|
+
allocationComplete: Promise.resolve()
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
const uuid = crypto.randomUUID();
|
|
1799
|
+
const allocationComplete = this.registerPendingOp(uuid, bufnum);
|
|
1800
|
+
return { uuid, allocationComplete };
|
|
1801
|
+
}
|
|
1802
|
+
async #acquireBufferLock(bufnum) {
|
|
1803
|
+
const prev = this.bufferLocks.get(bufnum) || Promise.resolve();
|
|
1804
|
+
let releaseLock;
|
|
1805
|
+
const current = new Promise((resolve) => {
|
|
1806
|
+
releaseLock = resolve;
|
|
1807
|
+
});
|
|
1808
|
+
this.bufferLocks.set(bufnum, prev.then(() => current));
|
|
1809
|
+
await prev;
|
|
1810
|
+
return () => {
|
|
1811
|
+
if (releaseLock) {
|
|
1812
|
+
releaseLock();
|
|
1813
|
+
releaseLock = null;
|
|
1814
|
+
}
|
|
1815
|
+
if (this.bufferLocks.get(bufnum) === current) {
|
|
1816
|
+
this.bufferLocks.delete(bufnum);
|
|
1817
|
+
}
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
#recordAllocation(bufnum, ptr, sizeBytes, pendingToken, pendingPromise) {
|
|
1821
|
+
const previousEntry = this.allocatedBuffers.get(bufnum);
|
|
1822
|
+
const entry = {
|
|
1823
|
+
ptr,
|
|
1824
|
+
size: sizeBytes,
|
|
1825
|
+
pendingToken,
|
|
1826
|
+
pendingPromise,
|
|
1827
|
+
previousAllocation: previousEntry ? { ptr: previousEntry.ptr, size: previousEntry.size } : null
|
|
1828
|
+
};
|
|
1829
|
+
this.allocatedBuffers.set(bufnum, entry);
|
|
1830
|
+
return entry;
|
|
1831
|
+
}
|
|
1832
|
+
async #awaitPendingReplacement(bufnum) {
|
|
1833
|
+
const existing = this.allocatedBuffers.get(bufnum);
|
|
1834
|
+
if (existing && existing.pendingToken && existing.pendingPromise) {
|
|
1835
|
+
try {
|
|
1836
|
+
await existing.pendingPromise;
|
|
1837
|
+
} catch {
|
|
1549
1838
|
}
|
|
1550
|
-
prev = block;
|
|
1551
|
-
block = this.blockNext(block);
|
|
1552
1839
|
}
|
|
1553
|
-
return res;
|
|
1554
1840
|
}
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
*/
|
|
1560
|
-
insert(block) {
|
|
1561
|
-
let ptr = this._free;
|
|
1562
|
-
let prev = 0;
|
|
1563
|
-
while (ptr) {
|
|
1564
|
-
if (block <= ptr) break;
|
|
1565
|
-
prev = ptr;
|
|
1566
|
-
ptr = this.blockNext(ptr);
|
|
1841
|
+
#attachFinalizer(bufnum, pendingToken, promise) {
|
|
1842
|
+
if (!promise || typeof promise.then !== "function") {
|
|
1843
|
+
this.#finalizeReplacement(bufnum, pendingToken, true);
|
|
1844
|
+
return Promise.resolve();
|
|
1567
1845
|
}
|
|
1568
|
-
|
|
1569
|
-
this
|
|
1846
|
+
return promise.then((value) => {
|
|
1847
|
+
this.#finalizeReplacement(bufnum, pendingToken, true);
|
|
1848
|
+
return value;
|
|
1849
|
+
}).catch((error) => {
|
|
1850
|
+
this.#finalizeReplacement(bufnum, pendingToken, false);
|
|
1851
|
+
throw error;
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
#finalizeReplacement(bufnum, pendingToken, success) {
|
|
1855
|
+
const entry = this.allocatedBuffers.get(bufnum);
|
|
1856
|
+
if (!entry || entry.pendingToken !== pendingToken) {
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
const previous = entry.previousAllocation;
|
|
1860
|
+
if (success) {
|
|
1861
|
+
entry.pendingToken = null;
|
|
1862
|
+
entry.pendingPromise = null;
|
|
1863
|
+
entry.previousAllocation = null;
|
|
1864
|
+
if (previous?.ptr) {
|
|
1865
|
+
this.bufferPool.free(previous.ptr);
|
|
1866
|
+
}
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
if (entry.ptr) {
|
|
1870
|
+
this.bufferPool.free(entry.ptr);
|
|
1871
|
+
}
|
|
1872
|
+
entry.pendingPromise = null;
|
|
1873
|
+
if (previous?.ptr) {
|
|
1874
|
+
this.allocatedBuffers.set(bufnum, {
|
|
1875
|
+
ptr: previous.ptr,
|
|
1876
|
+
size: previous.size,
|
|
1877
|
+
pendingToken: null,
|
|
1878
|
+
previousAllocation: null
|
|
1879
|
+
});
|
|
1570
1880
|
} else {
|
|
1571
|
-
this.
|
|
1881
|
+
this.allocatedBuffers.delete(bufnum);
|
|
1572
1882
|
}
|
|
1573
|
-
this.setBlockNext(block, ptr);
|
|
1574
1883
|
}
|
|
1575
1884
|
};
|
|
1576
|
-
var
|
|
1577
|
-
var __blockSelfAddress = (dataAddress) => dataAddress > 0 ? dataAddress - SIZEOF_MEM_BLOCK : 0;
|
|
1578
|
-
|
|
1579
|
-
// js/supersonic.js
|
|
1580
|
-
var SuperSonic = class {
|
|
1885
|
+
var SuperSonic = class _SuperSonic {
|
|
1581
1886
|
// Expose OSC utilities as static methods
|
|
1582
1887
|
static osc = {
|
|
1583
1888
|
encode: (message) => osc_default.writePacket(message),
|
|
@@ -1596,10 +1901,14 @@ var SuperSonic = class {
|
|
|
1596
1901
|
this.wasmModule = null;
|
|
1597
1902
|
this.wasmInstance = null;
|
|
1598
1903
|
this.bufferPool = null;
|
|
1599
|
-
this.
|
|
1904
|
+
this.bufferManager = null;
|
|
1905
|
+
this.loadedSynthDefs = /* @__PURE__ */ new Set();
|
|
1906
|
+
this.pendingBufferOps = /* @__PURE__ */ new Map();
|
|
1600
1907
|
this._timeOffsetPromise = null;
|
|
1601
1908
|
this._resolveTimeOffset = null;
|
|
1602
|
-
this.
|
|
1909
|
+
this._localClockOffsetTimer = null;
|
|
1910
|
+
this.onOSC = null;
|
|
1911
|
+
this.onMessage = null;
|
|
1603
1912
|
this.onMessageSent = null;
|
|
1604
1913
|
this.onMetricsUpdate = null;
|
|
1605
1914
|
this.onStatusUpdate = null;
|
|
@@ -1686,21 +1995,6 @@ var SuperSonic = class {
|
|
|
1686
1995
|
});
|
|
1687
1996
|
console.log("[SuperSonic] Buffer pool initialized: 128MB at offset 64MB");
|
|
1688
1997
|
}
|
|
1689
|
-
/**
|
|
1690
|
-
* Calculate time offset (AudioContext → NTP conversion)
|
|
1691
|
-
* Called when AudioContext is in 'running' state to ensure accurate timing
|
|
1692
|
-
*/
|
|
1693
|
-
#calculateTimeOffset() {
|
|
1694
|
-
const SECONDS_1900_TO_1970 = 2208988800;
|
|
1695
|
-
const audioContextTime = this.audioContext.currentTime;
|
|
1696
|
-
const unixSeconds = Date.now() / 1e3;
|
|
1697
|
-
this.wasmTimeOffset = SECONDS_1900_TO_1970 + unixSeconds - audioContextTime;
|
|
1698
|
-
if (this._resolveTimeOffset) {
|
|
1699
|
-
this._resolveTimeOffset(this.wasmTimeOffset);
|
|
1700
|
-
this._resolveTimeOffset = null;
|
|
1701
|
-
}
|
|
1702
|
-
return this.wasmTimeOffset;
|
|
1703
|
-
}
|
|
1704
1998
|
/**
|
|
1705
1999
|
* Initialize AudioContext and set up time offset calculation
|
|
1706
2000
|
*/
|
|
@@ -1720,14 +2014,17 @@ var SuperSonic = class {
|
|
|
1720
2014
|
document.addEventListener("click", resumeContext, { once: true });
|
|
1721
2015
|
document.addEventListener("touchstart", resumeContext, { once: true });
|
|
1722
2016
|
}
|
|
1723
|
-
this.audioContext
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
2017
|
+
return this.audioContext;
|
|
2018
|
+
}
|
|
2019
|
+
#initializeBufferManager() {
|
|
2020
|
+
this.bufferManager = new BufferManager({
|
|
2021
|
+
audioContext: this.audioContext,
|
|
2022
|
+
sharedBuffer: this.sharedBuffer,
|
|
2023
|
+
bufferPool: this.bufferPool,
|
|
2024
|
+
allocatedBuffers: this.allocatedBuffers,
|
|
2025
|
+
resolveAudioPath: (path) => this._resolveAudioPath(path),
|
|
2026
|
+
registerPendingOp: (uuid, bufnum, timeoutMs) => this.#createPendingBufferOperation(uuid, bufnum, timeoutMs)
|
|
1727
2027
|
});
|
|
1728
|
-
if (this.audioContext.state === "running") {
|
|
1729
|
-
this.#calculateTimeOffset();
|
|
1730
|
-
}
|
|
1731
2028
|
}
|
|
1732
2029
|
/**
|
|
1733
2030
|
* Load WASM manifest to get the current hashed filename
|
|
@@ -1773,12 +2070,10 @@ var SuperSonic = class {
|
|
|
1773
2070
|
type: "init",
|
|
1774
2071
|
sharedBuffer: this.sharedBuffer
|
|
1775
2072
|
});
|
|
1776
|
-
const timeOffset = await this._timeOffsetPromise;
|
|
1777
2073
|
this.workletNode.port.postMessage({
|
|
1778
2074
|
type: "loadWasm",
|
|
1779
2075
|
wasmBytes,
|
|
1780
|
-
wasmMemory: this.wasmMemory
|
|
1781
|
-
timeOffset
|
|
2076
|
+
wasmMemory: this.wasmMemory
|
|
1782
2077
|
});
|
|
1783
2078
|
await this.#waitForWorkletInit();
|
|
1784
2079
|
}
|
|
@@ -1787,13 +2082,20 @@ var SuperSonic = class {
|
|
|
1787
2082
|
*/
|
|
1788
2083
|
async #initializeOSC() {
|
|
1789
2084
|
this.osc = new ScsynthOSC();
|
|
1790
|
-
this.osc.
|
|
2085
|
+
this.osc.onRawOSC((msg) => {
|
|
2086
|
+
if (this.onOSC) {
|
|
2087
|
+
this.onOSC(msg);
|
|
2088
|
+
}
|
|
2089
|
+
});
|
|
2090
|
+
this.osc.onParsedOSC((msg) => {
|
|
1791
2091
|
if (msg.address === "/buffer/freed") {
|
|
1792
2092
|
this._handleBufferFreed(msg.args);
|
|
2093
|
+
} else if (msg.address === "/buffer/allocated") {
|
|
2094
|
+
this._handleBufferAllocated(msg.args);
|
|
1793
2095
|
}
|
|
1794
|
-
if (this.
|
|
2096
|
+
if (this.onMessage) {
|
|
1795
2097
|
this.stats.messagesReceived++;
|
|
1796
|
-
this.
|
|
2098
|
+
this.onMessage(msg);
|
|
1797
2099
|
}
|
|
1798
2100
|
});
|
|
1799
2101
|
this.osc.onDebugMessage((msg) => {
|
|
@@ -1856,6 +2158,7 @@ var SuperSonic = class {
|
|
|
1856
2158
|
this.checkCapabilities();
|
|
1857
2159
|
this.#initializeSharedMemory();
|
|
1858
2160
|
this.#initializeAudioContext();
|
|
2161
|
+
this.#initializeBufferManager();
|
|
1859
2162
|
const wasmBytes = await this.#loadWasm();
|
|
1860
2163
|
await this.#initializeAudioWorklet(wasmBytes);
|
|
1861
2164
|
await this.#initializeOSC();
|
|
@@ -1900,10 +2203,20 @@ var SuperSonic = class {
|
|
|
1900
2203
|
console.warn("[SuperSonic] Warning: ringBufferBase not provided by worklet");
|
|
1901
2204
|
}
|
|
1902
2205
|
if (event.data.bufferConstants !== void 0) {
|
|
2206
|
+
console.log("[SuperSonic] Received bufferConstants from worklet");
|
|
1903
2207
|
this.bufferConstants = event.data.bufferConstants;
|
|
2208
|
+
console.log("[SuperSonic] Initializing NTP timing");
|
|
2209
|
+
this.initializeNTPTiming();
|
|
2210
|
+
this.#startDriftOffsetTimer();
|
|
2211
|
+
console.log("[SuperSonic] Resolving time offset promise, _resolveTimeOffset=", this._resolveTimeOffset);
|
|
2212
|
+
if (this._resolveTimeOffset) {
|
|
2213
|
+
this._resolveTimeOffset();
|
|
2214
|
+
this._resolveTimeOffset = null;
|
|
2215
|
+
}
|
|
1904
2216
|
} else {
|
|
1905
2217
|
console.warn("[SuperSonic] Warning: bufferConstants not provided by worklet");
|
|
1906
2218
|
}
|
|
2219
|
+
console.log("[SuperSonic] Calling resolve() for worklet initialization");
|
|
1907
2220
|
resolve();
|
|
1908
2221
|
} else {
|
|
1909
2222
|
reject(new Error(event.data.error || "AudioWorklet initialization failed"));
|
|
@@ -1986,12 +2299,7 @@ var SuperSonic = class {
|
|
|
1986
2299
|
* sonic.send('/n_set', 1000, 'freq', 440.0, 'amp', 0.5);
|
|
1987
2300
|
*/
|
|
1988
2301
|
async send(address, ...args) {
|
|
1989
|
-
|
|
1990
|
-
throw new Error("SuperSonic not initialized. Call init() first.");
|
|
1991
|
-
}
|
|
1992
|
-
if (this._isBufferAllocationCommand(address)) {
|
|
1993
|
-
return await this._handleBufferCommand(address, args);
|
|
1994
|
-
}
|
|
2302
|
+
this.#ensureInitialized("send OSC messages");
|
|
1995
2303
|
const oscArgs = args.map((arg) => {
|
|
1996
2304
|
if (typeof arg === "string") {
|
|
1997
2305
|
return { type: "s", value: arg };
|
|
@@ -2003,98 +2311,9 @@ var SuperSonic = class {
|
|
|
2003
2311
|
throw new Error(`Unsupported argument type: ${typeof arg}`);
|
|
2004
2312
|
}
|
|
2005
2313
|
});
|
|
2006
|
-
const message = {
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
};
|
|
2010
|
-
const oscData = osc_default.writePacket(message);
|
|
2011
|
-
this.sendOSC(oscData);
|
|
2012
|
-
}
|
|
2013
|
-
_isBufferAllocationCommand(address) {
|
|
2014
|
-
return [
|
|
2015
|
-
"/b_allocRead",
|
|
2016
|
-
"/b_allocReadChannel",
|
|
2017
|
-
"/b_read",
|
|
2018
|
-
"/b_readChannel"
|
|
2019
|
-
// NOTE: /b_alloc and /b_free are NOT intercepted
|
|
2020
|
-
].includes(address);
|
|
2021
|
-
}
|
|
2022
|
-
async _handleBufferCommand(address, args) {
|
|
2023
|
-
switch (address) {
|
|
2024
|
-
case "/b_allocRead":
|
|
2025
|
-
return await this._allocReadBuffer(...args);
|
|
2026
|
-
case "/b_allocReadChannel":
|
|
2027
|
-
return await this._allocReadChannelBuffer(...args);
|
|
2028
|
-
case "/b_read":
|
|
2029
|
-
return await this._readBuffer(...args);
|
|
2030
|
-
case "/b_readChannel":
|
|
2031
|
-
return await this._readChannelBuffer(...args);
|
|
2032
|
-
}
|
|
2033
|
-
}
|
|
2034
|
-
/**
|
|
2035
|
-
* /b_allocRead bufnum path [startFrame numFrames completion]
|
|
2036
|
-
*/
|
|
2037
|
-
async _allocReadBuffer(bufnum, path, startFrame = 0, numFrames = 0, completionMsg = null) {
|
|
2038
|
-
let allocatedPtr = null;
|
|
2039
|
-
const GUARD_BEFORE = 3;
|
|
2040
|
-
const GUARD_AFTER = 1;
|
|
2041
|
-
try {
|
|
2042
|
-
const url = this._resolveAudioPath(path);
|
|
2043
|
-
const response = await fetch(url);
|
|
2044
|
-
if (!response.ok) {
|
|
2045
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
2046
|
-
}
|
|
2047
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
2048
|
-
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
|
|
2049
|
-
const actualStartFrame = startFrame || 0;
|
|
2050
|
-
const actualNumFrames = numFrames || audioBuffer.length - actualStartFrame;
|
|
2051
|
-
const framesToRead = Math.min(actualNumFrames, audioBuffer.length - actualStartFrame);
|
|
2052
|
-
if (framesToRead <= 0) {
|
|
2053
|
-
throw new Error(`Invalid frame range: start=${actualStartFrame}, numFrames=${actualNumFrames}, fileLength=${audioBuffer.length}`);
|
|
2054
|
-
}
|
|
2055
|
-
const numChannels = audioBuffer.numberOfChannels;
|
|
2056
|
-
const guardSamples = (GUARD_BEFORE + GUARD_AFTER) * numChannels;
|
|
2057
|
-
const interleavedData = new Float32Array(framesToRead * numChannels + guardSamples);
|
|
2058
|
-
const dataOffset = GUARD_BEFORE * numChannels;
|
|
2059
|
-
for (let frame = 0; frame < framesToRead; frame++) {
|
|
2060
|
-
for (let ch = 0; ch < numChannels; ch++) {
|
|
2061
|
-
const channelData = audioBuffer.getChannelData(ch);
|
|
2062
|
-
interleavedData[dataOffset + frame * numChannels + ch] = channelData[actualStartFrame + frame];
|
|
2063
|
-
}
|
|
2064
|
-
}
|
|
2065
|
-
const bytesNeeded = interleavedData.length * 4;
|
|
2066
|
-
allocatedPtr = this.bufferPool.malloc(bytesNeeded);
|
|
2067
|
-
if (allocatedPtr === 0) {
|
|
2068
|
-
throw new Error("Buffer pool allocation failed (out of memory)");
|
|
2069
|
-
}
|
|
2070
|
-
const wasmHeap = new Float32Array(
|
|
2071
|
-
this.sharedBuffer,
|
|
2072
|
-
allocatedPtr,
|
|
2073
|
-
interleavedData.length
|
|
2074
|
-
);
|
|
2075
|
-
wasmHeap.set(interleavedData);
|
|
2076
|
-
this.allocatedBuffers.set(bufnum, {
|
|
2077
|
-
ptr: allocatedPtr,
|
|
2078
|
-
size: bytesNeeded
|
|
2079
|
-
});
|
|
2080
|
-
await this.send(
|
|
2081
|
-
"/b_allocPtr",
|
|
2082
|
-
bufnum,
|
|
2083
|
-
allocatedPtr,
|
|
2084
|
-
framesToRead,
|
|
2085
|
-
numChannels,
|
|
2086
|
-
audioBuffer.sampleRate
|
|
2087
|
-
);
|
|
2088
|
-
if (completionMsg) {
|
|
2089
|
-
}
|
|
2090
|
-
} catch (error) {
|
|
2091
|
-
if (allocatedPtr) {
|
|
2092
|
-
this.bufferPool.free(allocatedPtr);
|
|
2093
|
-
this.allocatedBuffers.delete(bufnum);
|
|
2094
|
-
}
|
|
2095
|
-
console.error(`[SuperSonic] Buffer ${bufnum} load failed:`, error);
|
|
2096
|
-
throw error;
|
|
2097
|
-
}
|
|
2314
|
+
const message = { address, args: oscArgs };
|
|
2315
|
+
const oscData = _SuperSonic.osc.encode(message);
|
|
2316
|
+
return this.sendOSC(oscData);
|
|
2098
2317
|
}
|
|
2099
2318
|
/**
|
|
2100
2319
|
* Resolve audio file path to full URL
|
|
@@ -2110,150 +2329,79 @@ var SuperSonic = class {
|
|
|
2110
2329
|
}
|
|
2111
2330
|
return this.sampleBaseURL + scPath;
|
|
2112
2331
|
}
|
|
2332
|
+
#ensureInitialized(actionDescription = "perform this operation") {
|
|
2333
|
+
if (!this.initialized) {
|
|
2334
|
+
throw new Error(`SuperSonic not initialized. Call init() before attempting to ${actionDescription}.`);
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
#createPendingBufferOperation(uuid, bufnum, timeoutMs = 3e4) {
|
|
2338
|
+
return new Promise((resolve, reject) => {
|
|
2339
|
+
const timeout = setTimeout(() => {
|
|
2340
|
+
this.pendingBufferOps.delete(uuid);
|
|
2341
|
+
reject(new Error(`Buffer ${bufnum} allocation timeout (${timeoutMs}ms)`));
|
|
2342
|
+
}, timeoutMs);
|
|
2343
|
+
this.pendingBufferOps.set(uuid, { resolve, reject, timeout });
|
|
2344
|
+
});
|
|
2345
|
+
}
|
|
2113
2346
|
/**
|
|
2114
2347
|
* Handle /buffer/freed message from WASM
|
|
2115
2348
|
*/
|
|
2116
2349
|
_handleBufferFreed(args) {
|
|
2117
|
-
if (args.length < 2) {
|
|
2118
|
-
console.warn("[SuperSonic] Invalid /buffer/freed message:", args);
|
|
2119
|
-
return;
|
|
2120
|
-
}
|
|
2121
2350
|
const bufnum = args[0];
|
|
2122
|
-
const
|
|
2351
|
+
const freedPtr = args[1];
|
|
2123
2352
|
const bufferInfo = this.allocatedBuffers.get(bufnum);
|
|
2124
|
-
if (bufferInfo) {
|
|
2353
|
+
if (!bufferInfo) {
|
|
2354
|
+
if (typeof freedPtr === "number" && freedPtr !== 0) {
|
|
2355
|
+
this.bufferPool.free(freedPtr);
|
|
2356
|
+
}
|
|
2357
|
+
return;
|
|
2358
|
+
}
|
|
2359
|
+
if (typeof freedPtr === "number" && freedPtr === bufferInfo.ptr) {
|
|
2125
2360
|
this.bufferPool.free(bufferInfo.ptr);
|
|
2126
2361
|
this.allocatedBuffers.delete(bufnum);
|
|
2362
|
+
return;
|
|
2127
2363
|
}
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
*/
|
|
2133
|
-
async _allocReadChannelBuffer(bufnum, path, startFrame = 0, numFrames = 0, ...channelsAndCompletion) {
|
|
2134
|
-
let allocatedPtr = null;
|
|
2135
|
-
const GUARD_BEFORE = 3;
|
|
2136
|
-
const GUARD_AFTER = 1;
|
|
2137
|
-
try {
|
|
2138
|
-
const channels = [];
|
|
2139
|
-
let completionMsg = null;
|
|
2140
|
-
for (let i = 0; i < channelsAndCompletion.length; i++) {
|
|
2141
|
-
if (typeof channelsAndCompletion[i] === "number" && Number.isInteger(channelsAndCompletion[i])) {
|
|
2142
|
-
channels.push(channelsAndCompletion[i]);
|
|
2143
|
-
} else {
|
|
2144
|
-
completionMsg = channelsAndCompletion[i];
|
|
2145
|
-
break;
|
|
2146
|
-
}
|
|
2147
|
-
}
|
|
2148
|
-
const url = this._resolveAudioPath(path);
|
|
2149
|
-
const response = await fetch(url);
|
|
2150
|
-
if (!response.ok) {
|
|
2151
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
2152
|
-
}
|
|
2153
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
2154
|
-
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
|
|
2155
|
-
const actualStartFrame = startFrame || 0;
|
|
2156
|
-
const actualNumFrames = numFrames || audioBuffer.length - actualStartFrame;
|
|
2157
|
-
const framesToRead = Math.min(actualNumFrames, audioBuffer.length - actualStartFrame);
|
|
2158
|
-
if (framesToRead <= 0) {
|
|
2159
|
-
throw new Error(`Invalid frame range: start=${actualStartFrame}, numFrames=${actualNumFrames}, fileLength=${audioBuffer.length}`);
|
|
2160
|
-
}
|
|
2161
|
-
const fileChannels = audioBuffer.numberOfChannels;
|
|
2162
|
-
const selectedChannels = channels.length > 0 ? channels : Array.from({ length: fileChannels }, (_, i) => i);
|
|
2163
|
-
for (const ch of selectedChannels) {
|
|
2164
|
-
if (ch < 0 || ch >= fileChannels) {
|
|
2165
|
-
throw new Error(`Invalid channel ${ch} (file has ${fileChannels} channels)`);
|
|
2166
|
-
}
|
|
2167
|
-
}
|
|
2168
|
-
const numChannels = selectedChannels.length;
|
|
2169
|
-
const guardSamples = (GUARD_BEFORE + GUARD_AFTER) * numChannels;
|
|
2170
|
-
const interleavedData = new Float32Array(framesToRead * numChannels + guardSamples);
|
|
2171
|
-
const dataOffset = GUARD_BEFORE * numChannels;
|
|
2172
|
-
for (let frame = 0; frame < framesToRead; frame++) {
|
|
2173
|
-
for (let ch = 0; ch < numChannels; ch++) {
|
|
2174
|
-
const fileChannel = selectedChannels[ch];
|
|
2175
|
-
const channelData = audioBuffer.getChannelData(fileChannel);
|
|
2176
|
-
interleavedData[dataOffset + frame * numChannels + ch] = channelData[actualStartFrame + frame];
|
|
2177
|
-
}
|
|
2178
|
-
}
|
|
2179
|
-
const bytesNeeded = interleavedData.length * 4;
|
|
2180
|
-
allocatedPtr = this.bufferPool.malloc(bytesNeeded);
|
|
2181
|
-
if (allocatedPtr === 0) {
|
|
2182
|
-
throw new Error("Buffer pool allocation failed (out of memory)");
|
|
2183
|
-
}
|
|
2184
|
-
const wasmHeap = new Float32Array(this.sharedBuffer, allocatedPtr, interleavedData.length);
|
|
2185
|
-
wasmHeap.set(interleavedData);
|
|
2186
|
-
this.allocatedBuffers.set(bufnum, { ptr: allocatedPtr, size: bytesNeeded });
|
|
2187
|
-
await this.send("/b_allocPtr", bufnum, allocatedPtr, framesToRead, numChannels, audioBuffer.sampleRate);
|
|
2188
|
-
if (completionMsg) {
|
|
2189
|
-
}
|
|
2190
|
-
} catch (error) {
|
|
2191
|
-
if (allocatedPtr) {
|
|
2192
|
-
this.bufferPool.free(allocatedPtr);
|
|
2193
|
-
this.allocatedBuffers.delete(bufnum);
|
|
2194
|
-
}
|
|
2195
|
-
console.error(`[SuperSonic] Buffer ${bufnum} load failed:`, error);
|
|
2196
|
-
throw error;
|
|
2364
|
+
if (typeof freedPtr === "number" && bufferInfo.previousAllocation && bufferInfo.previousAllocation.ptr === freedPtr) {
|
|
2365
|
+
this.bufferPool.free(freedPtr);
|
|
2366
|
+
bufferInfo.previousAllocation = null;
|
|
2367
|
+
return;
|
|
2197
2368
|
}
|
|
2369
|
+
this.bufferPool.free(bufferInfo.ptr);
|
|
2370
|
+
this.allocatedBuffers.delete(bufnum);
|
|
2198
2371
|
}
|
|
2199
2372
|
/**
|
|
2200
|
-
* /
|
|
2201
|
-
* Read file into existing buffer
|
|
2202
|
-
*/
|
|
2203
|
-
async _readBuffer(bufnum, path, startFrame = 0, numFrames = 0, bufStartFrame = 0, leaveOpen = 0, completionMsg = null) {
|
|
2204
|
-
console.warn("[SuperSonic] /b_read requires pre-allocated buffer - not yet implemented");
|
|
2205
|
-
throw new Error("/b_read not yet implemented (requires /b_alloc first)");
|
|
2206
|
-
}
|
|
2207
|
-
/**
|
|
2208
|
-
* /b_readChannel bufnum path [startFrame numFrames bufStartFrame leaveOpen channel1 channel2 ... completion]
|
|
2209
|
-
* Read specific channels into existing buffer
|
|
2373
|
+
* Handle /buffer/allocated message with UUID correlation
|
|
2210
2374
|
*/
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2375
|
+
_handleBufferAllocated(args) {
|
|
2376
|
+
const uuid = args[0];
|
|
2377
|
+
const bufnum = args[1];
|
|
2378
|
+
const pending = this.pendingBufferOps.get(uuid);
|
|
2379
|
+
if (pending) {
|
|
2380
|
+
clearTimeout(pending.timeout);
|
|
2381
|
+
pending.resolve({ bufnum });
|
|
2382
|
+
this.pendingBufferOps.delete(uuid);
|
|
2383
|
+
}
|
|
2214
2384
|
}
|
|
2215
2385
|
/**
|
|
2216
2386
|
* Send pre-encoded OSC bytes to scsynth
|
|
2217
2387
|
* @param {ArrayBuffer|Uint8Array} oscData - Pre-encoded OSC data
|
|
2218
2388
|
* @param {Object} options - Send options
|
|
2219
2389
|
*/
|
|
2220
|
-
sendOSC(oscData, options = {}) {
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
let uint8Data;
|
|
2225
|
-
if (oscData instanceof ArrayBuffer) {
|
|
2226
|
-
uint8Data = new Uint8Array(oscData);
|
|
2227
|
-
} else if (oscData instanceof Uint8Array) {
|
|
2228
|
-
uint8Data = oscData;
|
|
2229
|
-
} else {
|
|
2230
|
-
throw new Error("oscData must be ArrayBuffer or Uint8Array");
|
|
2231
|
-
}
|
|
2390
|
+
async sendOSC(oscData, options = {}) {
|
|
2391
|
+
this.#ensureInitialized("send OSC data");
|
|
2392
|
+
const uint8Data = this.#toUint8Array(oscData);
|
|
2393
|
+
const preparedData = await this.#prepareOutboundPacket(uint8Data);
|
|
2232
2394
|
this.stats.messagesSent++;
|
|
2233
2395
|
if (this.onMessageSent) {
|
|
2234
|
-
this.onMessageSent(
|
|
2235
|
-
}
|
|
2236
|
-
let waitTimeMs = null;
|
|
2237
|
-
if (uint8Data.length >= 16) {
|
|
2238
|
-
const header = String.fromCharCode.apply(null, uint8Data.slice(0, 8));
|
|
2239
|
-
if (header === "#bundle\0") {
|
|
2240
|
-
if (this.wasmTimeOffset === null) {
|
|
2241
|
-
console.warn("[SuperSonic] Time offset not yet calculated, calculating now");
|
|
2242
|
-
this.#calculateTimeOffset();
|
|
2243
|
-
}
|
|
2244
|
-
const view = new DataView(uint8Data.buffer, uint8Data.byteOffset);
|
|
2245
|
-
const ntpSeconds = view.getUint32(8, false);
|
|
2246
|
-
const ntpFraction = view.getUint32(12, false);
|
|
2247
|
-
if (!(ntpSeconds === 0 && (ntpFraction === 0 || ntpFraction === 1))) {
|
|
2248
|
-
const ntpTimeS = ntpSeconds + ntpFraction / 4294967296;
|
|
2249
|
-
const audioTimeS = ntpTimeS - this.wasmTimeOffset;
|
|
2250
|
-
const currentAudioTimeS = this.audioContext.currentTime;
|
|
2251
|
-
const latencyS = 0.05;
|
|
2252
|
-
waitTimeMs = (audioTimeS - currentAudioTimeS - latencyS) * 1e3;
|
|
2253
|
-
}
|
|
2254
|
-
}
|
|
2396
|
+
this.onMessageSent(preparedData);
|
|
2255
2397
|
}
|
|
2256
|
-
|
|
2398
|
+
const timing = this.#calculateBundleWait(preparedData);
|
|
2399
|
+
const sendOptions = { ...options };
|
|
2400
|
+
if (timing) {
|
|
2401
|
+
sendOptions.audioTimeS = timing.audioTimeS;
|
|
2402
|
+
sendOptions.currentTimeS = timing.currentTimeS;
|
|
2403
|
+
}
|
|
2404
|
+
this.osc.send(preparedData, sendOptions);
|
|
2257
2405
|
}
|
|
2258
2406
|
/**
|
|
2259
2407
|
* Get current status
|
|
@@ -2271,6 +2419,7 @@ var SuperSonic = class {
|
|
|
2271
2419
|
*/
|
|
2272
2420
|
async destroy() {
|
|
2273
2421
|
console.log("[SuperSonic] Destroying...");
|
|
2422
|
+
this.#stopDriftOffsetTimer();
|
|
2274
2423
|
if (this.osc) {
|
|
2275
2424
|
this.osc.terminate();
|
|
2276
2425
|
this.osc = null;
|
|
@@ -2283,10 +2432,57 @@ var SuperSonic = class {
|
|
|
2283
2432
|
await this.audioContext.close();
|
|
2284
2433
|
this.audioContext = null;
|
|
2285
2434
|
}
|
|
2435
|
+
for (const [uuid, pending] of this.pendingBufferOps.entries()) {
|
|
2436
|
+
clearTimeout(pending.timeout);
|
|
2437
|
+
pending.reject(new Error("SuperSonic instance destroyed"));
|
|
2438
|
+
}
|
|
2439
|
+
this.pendingBufferOps.clear();
|
|
2286
2440
|
this.sharedBuffer = null;
|
|
2287
2441
|
this.initialized = false;
|
|
2442
|
+
this.bufferManager = null;
|
|
2443
|
+
this.allocatedBuffers.clear();
|
|
2444
|
+
this.loadedSynthDefs.clear();
|
|
2288
2445
|
console.log("[SuperSonic] Destroyed");
|
|
2289
2446
|
}
|
|
2447
|
+
/**
|
|
2448
|
+
* Wait until NTP timing has been established.
|
|
2449
|
+
* Note: NTP calculation is now done internally in C++ process_audio().
|
|
2450
|
+
* Returns 0 for backward compatibility.
|
|
2451
|
+
*/
|
|
2452
|
+
async waitForTimeSync() {
|
|
2453
|
+
if (!this.bufferConstants) {
|
|
2454
|
+
if (this._timeOffsetPromise) {
|
|
2455
|
+
await this._timeOffsetPromise;
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
const ntpStartView = new Float64Array(this.sharedBuffer, this.ringBufferBase + this.bufferConstants.NTP_START_TIME_START, 1);
|
|
2459
|
+
return ntpStartView[0];
|
|
2460
|
+
}
|
|
2461
|
+
/**
|
|
2462
|
+
* Load a sample into a buffer and wait for confirmation
|
|
2463
|
+
* @param {number} bufnum - Buffer number
|
|
2464
|
+
* @param {string} path - Audio file path
|
|
2465
|
+
* @returns {Promise} Resolves when buffer is ready
|
|
2466
|
+
*/
|
|
2467
|
+
async loadSample(bufnum, path, startFrame = 0, numFrames = 0) {
|
|
2468
|
+
this.#ensureInitialized("load samples");
|
|
2469
|
+
const bufferInfo = await this.#requireBufferManager().prepareFromFile({
|
|
2470
|
+
bufnum,
|
|
2471
|
+
path,
|
|
2472
|
+
startFrame,
|
|
2473
|
+
numFrames
|
|
2474
|
+
});
|
|
2475
|
+
await this.send(
|
|
2476
|
+
"/b_allocPtr",
|
|
2477
|
+
bufnum,
|
|
2478
|
+
bufferInfo.ptr,
|
|
2479
|
+
bufferInfo.numFrames,
|
|
2480
|
+
bufferInfo.numChannels,
|
|
2481
|
+
bufferInfo.sampleRate,
|
|
2482
|
+
bufferInfo.uuid
|
|
2483
|
+
);
|
|
2484
|
+
return bufferInfo.allocationComplete;
|
|
2485
|
+
}
|
|
2290
2486
|
/**
|
|
2291
2487
|
* Load a binary synthdef file and send it to scsynth
|
|
2292
2488
|
* @param {string} path - Path or URL to the .scsyndef file
|
|
@@ -2305,7 +2501,11 @@ var SuperSonic = class {
|
|
|
2305
2501
|
}
|
|
2306
2502
|
const arrayBuffer = await response.arrayBuffer();
|
|
2307
2503
|
const synthdefData = new Uint8Array(arrayBuffer);
|
|
2308
|
-
this.send("/d_recv", synthdefData);
|
|
2504
|
+
await this.send("/d_recv", synthdefData);
|
|
2505
|
+
const synthName = this.#extractSynthDefName(path);
|
|
2506
|
+
if (synthName) {
|
|
2507
|
+
this.loadedSynthDefs.add(synthName);
|
|
2508
|
+
}
|
|
2309
2509
|
console.log(`[SuperSonic] Loaded synthdef from ${path} (${synthdefData.length} bytes)`);
|
|
2310
2510
|
} catch (error) {
|
|
2311
2511
|
console.error("[SuperSonic] Failed to load synthdef:", error);
|
|
@@ -2404,6 +2604,368 @@ var SuperSonic = class {
|
|
|
2404
2604
|
}
|
|
2405
2605
|
return this.bufferPool.stats();
|
|
2406
2606
|
}
|
|
2607
|
+
getDiagnostics() {
|
|
2608
|
+
this.#ensureInitialized("get diagnostics");
|
|
2609
|
+
const poolStats = this.bufferPool?.stats ? this.bufferPool.stats() : null;
|
|
2610
|
+
let bytesActive = 0;
|
|
2611
|
+
let pendingCount = 0;
|
|
2612
|
+
for (const entry of this.allocatedBuffers.values()) {
|
|
2613
|
+
if (!entry) continue;
|
|
2614
|
+
bytesActive += entry.size || 0;
|
|
2615
|
+
if (entry.pendingToken) {
|
|
2616
|
+
pendingCount++;
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
return {
|
|
2620
|
+
buffers: {
|
|
2621
|
+
active: this.allocatedBuffers.size,
|
|
2622
|
+
pending: pendingCount,
|
|
2623
|
+
bytesActive,
|
|
2624
|
+
pool: poolStats ? {
|
|
2625
|
+
total: poolStats.total || 0,
|
|
2626
|
+
available: poolStats.available || 0,
|
|
2627
|
+
freeBytes: poolStats.free?.size || 0,
|
|
2628
|
+
freeBlocks: poolStats.free?.count || 0,
|
|
2629
|
+
usedBytes: poolStats.used?.size || 0,
|
|
2630
|
+
usedBlocks: poolStats.used?.count || 0
|
|
2631
|
+
} : null
|
|
2632
|
+
},
|
|
2633
|
+
synthdefs: {
|
|
2634
|
+
count: this.loadedSynthDefs.size
|
|
2635
|
+
}
|
|
2636
|
+
};
|
|
2637
|
+
}
|
|
2638
|
+
/**
|
|
2639
|
+
* Initialize NTP timing (write-once)
|
|
2640
|
+
* Sets the NTP start time when AudioContext started
|
|
2641
|
+
* @private
|
|
2642
|
+
*/
|
|
2643
|
+
initializeNTPTiming() {
|
|
2644
|
+
if (!this.bufferConstants || !this.audioContext) {
|
|
2645
|
+
return;
|
|
2646
|
+
}
|
|
2647
|
+
const perfTimeMs = performance.timeOrigin + performance.now();
|
|
2648
|
+
const currentNTP = perfTimeMs / 1e3 + NTP_EPOCH_OFFSET;
|
|
2649
|
+
const currentAudioCtx = this.audioContext.currentTime;
|
|
2650
|
+
const ntpStartTime = currentNTP - currentAudioCtx;
|
|
2651
|
+
const ntpStartView = new Float64Array(
|
|
2652
|
+
this.sharedBuffer,
|
|
2653
|
+
this.ringBufferBase + this.bufferConstants.NTP_START_TIME_START,
|
|
2654
|
+
1
|
|
2655
|
+
);
|
|
2656
|
+
ntpStartView[0] = ntpStartTime;
|
|
2657
|
+
this._initialNTPStartTime = ntpStartTime;
|
|
2658
|
+
console.log(`[SuperSonic] NTP timing initialized: start=${ntpStartTime.toFixed(6)}s (current NTP=${currentNTP.toFixed(3)}, AudioCtx=${currentAudioCtx.toFixed(3)}), ringBufferBase=${this.ringBufferBase}`);
|
|
2659
|
+
}
|
|
2660
|
+
/**
|
|
2661
|
+
* Update drift offset (AudioContext → NTP drift correction)
|
|
2662
|
+
* CRITICAL: This REPLACES the drift value, does not accumulate
|
|
2663
|
+
* @private
|
|
2664
|
+
*/
|
|
2665
|
+
updateDriftOffset() {
|
|
2666
|
+
if (!this.bufferConstants || !this.audioContext || this._initialNTPStartTime === void 0) {
|
|
2667
|
+
return;
|
|
2668
|
+
}
|
|
2669
|
+
const perfTimeMs = performance.timeOrigin + performance.now();
|
|
2670
|
+
const currentNTP = perfTimeMs / 1e3 + NTP_EPOCH_OFFSET;
|
|
2671
|
+
const currentAudioCtx = this.audioContext.currentTime;
|
|
2672
|
+
const currentNTPStartTime = currentNTP - currentAudioCtx;
|
|
2673
|
+
const driftSeconds = currentNTPStartTime - this._initialNTPStartTime;
|
|
2674
|
+
const driftMs = Math.round(driftSeconds * 1e3);
|
|
2675
|
+
const driftView = new Int32Array(
|
|
2676
|
+
this.sharedBuffer,
|
|
2677
|
+
this.ringBufferBase + this.bufferConstants.DRIFT_OFFSET_START,
|
|
2678
|
+
1
|
|
2679
|
+
);
|
|
2680
|
+
Atomics.store(driftView, 0, driftMs);
|
|
2681
|
+
console.log(`[SuperSonic] Drift offset updated: ${driftMs}ms (current NTP start=${currentNTPStartTime.toFixed(6)}, initial=${this._initialNTPStartTime.toFixed(6)})`);
|
|
2682
|
+
}
|
|
2683
|
+
/**
|
|
2684
|
+
* Get current drift offset in milliseconds
|
|
2685
|
+
* @returns {number} Current drift in milliseconds
|
|
2686
|
+
*/
|
|
2687
|
+
getDriftOffset() {
|
|
2688
|
+
if (!this.bufferConstants) {
|
|
2689
|
+
return 0;
|
|
2690
|
+
}
|
|
2691
|
+
const driftView = new Int32Array(
|
|
2692
|
+
this.sharedBuffer,
|
|
2693
|
+
this.ringBufferBase + this.bufferConstants.DRIFT_OFFSET_START,
|
|
2694
|
+
1
|
|
2695
|
+
);
|
|
2696
|
+
return Atomics.load(driftView, 0);
|
|
2697
|
+
}
|
|
2698
|
+
/**
|
|
2699
|
+
* Start periodic drift offset updates
|
|
2700
|
+
* @private
|
|
2701
|
+
*/
|
|
2702
|
+
#startDriftOffsetTimer() {
|
|
2703
|
+
this.#stopDriftOffsetTimer();
|
|
2704
|
+
this._driftOffsetTimer = setInterval(() => {
|
|
2705
|
+
this.updateDriftOffset();
|
|
2706
|
+
}, DRIFT_UPDATE_INTERVAL_MS);
|
|
2707
|
+
console.log(`[SuperSonic] Started drift offset correction (every ${DRIFT_UPDATE_INTERVAL_MS}ms)`);
|
|
2708
|
+
}
|
|
2709
|
+
/**
|
|
2710
|
+
* Stop periodic drift offset updates
|
|
2711
|
+
* @private
|
|
2712
|
+
*/
|
|
2713
|
+
#stopDriftOffsetTimer() {
|
|
2714
|
+
if (this._driftOffsetTimer) {
|
|
2715
|
+
clearInterval(this._driftOffsetTimer);
|
|
2716
|
+
this._driftOffsetTimer = null;
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
#extractSynthDefName(path) {
|
|
2720
|
+
if (!path || typeof path !== "string") {
|
|
2721
|
+
return null;
|
|
2722
|
+
}
|
|
2723
|
+
const lastSegment = path.split("/").filter(Boolean).pop() || path;
|
|
2724
|
+
return lastSegment.replace(/\.scsyndef$/i, "");
|
|
2725
|
+
}
|
|
2726
|
+
#toUint8Array(data) {
|
|
2727
|
+
if (data instanceof Uint8Array) {
|
|
2728
|
+
return data;
|
|
2729
|
+
}
|
|
2730
|
+
if (data instanceof ArrayBuffer) {
|
|
2731
|
+
return new Uint8Array(data);
|
|
2732
|
+
}
|
|
2733
|
+
throw new Error("oscData must be ArrayBuffer or Uint8Array");
|
|
2734
|
+
}
|
|
2735
|
+
async #prepareOutboundPacket(uint8Data) {
|
|
2736
|
+
const decodeOptions = { metadata: true, unpackSingleArgs: false };
|
|
2737
|
+
try {
|
|
2738
|
+
const decodedPacket = _SuperSonic.osc.decode(uint8Data, decodeOptions);
|
|
2739
|
+
const { packet, changed } = await this.#rewritePacket(decodedPacket);
|
|
2740
|
+
if (!changed) {
|
|
2741
|
+
return uint8Data;
|
|
2742
|
+
}
|
|
2743
|
+
return _SuperSonic.osc.encode(packet);
|
|
2744
|
+
} catch (error) {
|
|
2745
|
+
console.error("[SuperSonic] Failed to prepare OSC packet:", error);
|
|
2746
|
+
throw error;
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
async #rewritePacket(packet) {
|
|
2750
|
+
if (packet && packet.address) {
|
|
2751
|
+
const { message, changed } = await this.#rewriteMessage(packet);
|
|
2752
|
+
return { packet: message, changed };
|
|
2753
|
+
}
|
|
2754
|
+
if (this.#isBundle(packet)) {
|
|
2755
|
+
const subResults = await Promise.all(
|
|
2756
|
+
packet.packets.map((subPacket) => this.#rewritePacket(subPacket))
|
|
2757
|
+
);
|
|
2758
|
+
const changed = subResults.some((result) => result.changed);
|
|
2759
|
+
if (!changed) {
|
|
2760
|
+
return { packet, changed: false };
|
|
2761
|
+
}
|
|
2762
|
+
const rewrittenPackets = subResults.map((result) => result.packet);
|
|
2763
|
+
return {
|
|
2764
|
+
packet: {
|
|
2765
|
+
timeTag: packet.timeTag,
|
|
2766
|
+
packets: rewrittenPackets
|
|
2767
|
+
},
|
|
2768
|
+
changed: true
|
|
2769
|
+
};
|
|
2770
|
+
}
|
|
2771
|
+
return { packet, changed: false };
|
|
2772
|
+
}
|
|
2773
|
+
async #rewriteMessage(message) {
|
|
2774
|
+
switch (message.address) {
|
|
2775
|
+
case "/b_alloc":
|
|
2776
|
+
return {
|
|
2777
|
+
message: await this.#rewriteAlloc(message),
|
|
2778
|
+
changed: true
|
|
2779
|
+
};
|
|
2780
|
+
case "/b_allocRead":
|
|
2781
|
+
return {
|
|
2782
|
+
message: await this.#rewriteAllocRead(message),
|
|
2783
|
+
changed: true
|
|
2784
|
+
};
|
|
2785
|
+
case "/b_allocReadChannel":
|
|
2786
|
+
return {
|
|
2787
|
+
message: await this.#rewriteAllocReadChannel(message),
|
|
2788
|
+
changed: true
|
|
2789
|
+
};
|
|
2790
|
+
default:
|
|
2791
|
+
return { message, changed: false };
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
async #rewriteAllocRead(message) {
|
|
2795
|
+
const bufferManager = this.#requireBufferManager();
|
|
2796
|
+
const bufnum = this.#requireIntArg(message.args, 0, "/b_allocRead requires a buffer number");
|
|
2797
|
+
const path = this.#requireStringArg(message.args, 1, "/b_allocRead requires a file path");
|
|
2798
|
+
const startFrame = this.#optionalIntArg(message.args, 2, 0);
|
|
2799
|
+
const numFrames = this.#optionalIntArg(message.args, 3, 0);
|
|
2800
|
+
const bufferInfo = await bufferManager.prepareFromFile({
|
|
2801
|
+
bufnum,
|
|
2802
|
+
path,
|
|
2803
|
+
startFrame,
|
|
2804
|
+
numFrames
|
|
2805
|
+
});
|
|
2806
|
+
this.#detachAllocationPromise(bufferInfo.allocationComplete, `/b_allocRead ${bufnum}`);
|
|
2807
|
+
return this.#buildAllocPtrMessage(bufnum, bufferInfo);
|
|
2808
|
+
}
|
|
2809
|
+
async #rewriteAllocReadChannel(message) {
|
|
2810
|
+
const bufferManager = this.#requireBufferManager();
|
|
2811
|
+
const bufnum = this.#requireIntArg(message.args, 0, "/b_allocReadChannel requires a buffer number");
|
|
2812
|
+
const path = this.#requireStringArg(message.args, 1, "/b_allocReadChannel requires a file path");
|
|
2813
|
+
const startFrame = this.#optionalIntArg(message.args, 2, 0);
|
|
2814
|
+
const numFrames = this.#optionalIntArg(message.args, 3, 0);
|
|
2815
|
+
const channels = [];
|
|
2816
|
+
for (let i = 4; i < (message.args?.length || 0); i++) {
|
|
2817
|
+
if (!this.#isNumericArg(message.args[i])) {
|
|
2818
|
+
break;
|
|
2819
|
+
}
|
|
2820
|
+
channels.push(Math.floor(this.#getArgValue(message.args[i])));
|
|
2821
|
+
}
|
|
2822
|
+
const bufferInfo = await bufferManager.prepareFromFile({
|
|
2823
|
+
bufnum,
|
|
2824
|
+
path,
|
|
2825
|
+
startFrame,
|
|
2826
|
+
numFrames,
|
|
2827
|
+
channels: channels.length > 0 ? channels : null
|
|
2828
|
+
});
|
|
2829
|
+
this.#detachAllocationPromise(bufferInfo.allocationComplete, `/b_allocReadChannel ${bufnum}`);
|
|
2830
|
+
return this.#buildAllocPtrMessage(bufnum, bufferInfo);
|
|
2831
|
+
}
|
|
2832
|
+
async #rewriteAlloc(message) {
|
|
2833
|
+
const bufferManager = this.#requireBufferManager();
|
|
2834
|
+
const bufnum = this.#requireIntArg(message.args, 0, "/b_alloc requires a buffer number");
|
|
2835
|
+
const numFrames = this.#requireIntArg(message.args, 1, "/b_alloc requires a frame count");
|
|
2836
|
+
let argIndex = 2;
|
|
2837
|
+
let numChannels = 1;
|
|
2838
|
+
let sampleRate = this.audioContext?.sampleRate || 44100;
|
|
2839
|
+
if (this.#isNumericArg(this.#argAt(message.args, argIndex))) {
|
|
2840
|
+
numChannels = Math.max(1, this.#optionalIntArg(message.args, argIndex, 1));
|
|
2841
|
+
argIndex++;
|
|
2842
|
+
}
|
|
2843
|
+
if (this.#argAt(message.args, argIndex)?.type === "b") {
|
|
2844
|
+
argIndex++;
|
|
2845
|
+
}
|
|
2846
|
+
if (this.#isNumericArg(this.#argAt(message.args, argIndex))) {
|
|
2847
|
+
sampleRate = this.#getArgValue(this.#argAt(message.args, argIndex));
|
|
2848
|
+
}
|
|
2849
|
+
const bufferInfo = await bufferManager.prepareEmpty({
|
|
2850
|
+
bufnum,
|
|
2851
|
+
numFrames,
|
|
2852
|
+
numChannels,
|
|
2853
|
+
sampleRate
|
|
2854
|
+
});
|
|
2855
|
+
this.#detachAllocationPromise(bufferInfo.allocationComplete, `/b_alloc ${bufnum}`);
|
|
2856
|
+
return this.#buildAllocPtrMessage(bufnum, bufferInfo);
|
|
2857
|
+
}
|
|
2858
|
+
#buildAllocPtrMessage(bufnum, bufferInfo) {
|
|
2859
|
+
return {
|
|
2860
|
+
address: "/b_allocPtr",
|
|
2861
|
+
args: [
|
|
2862
|
+
this.#intArg(bufnum),
|
|
2863
|
+
this.#intArg(bufferInfo.ptr),
|
|
2864
|
+
this.#intArg(bufferInfo.numFrames),
|
|
2865
|
+
this.#intArg(bufferInfo.numChannels),
|
|
2866
|
+
this.#floatArg(bufferInfo.sampleRate),
|
|
2867
|
+
this.#stringArg(bufferInfo.uuid)
|
|
2868
|
+
]
|
|
2869
|
+
};
|
|
2870
|
+
}
|
|
2871
|
+
#intArg(value) {
|
|
2872
|
+
return { type: "i", value: Math.floor(value) };
|
|
2873
|
+
}
|
|
2874
|
+
#floatArg(value) {
|
|
2875
|
+
return { type: "f", value };
|
|
2876
|
+
}
|
|
2877
|
+
#stringArg(value) {
|
|
2878
|
+
return { type: "s", value: String(value) };
|
|
2879
|
+
}
|
|
2880
|
+
#argAt(args, index) {
|
|
2881
|
+
if (!Array.isArray(args)) {
|
|
2882
|
+
return void 0;
|
|
2883
|
+
}
|
|
2884
|
+
return args[index];
|
|
2885
|
+
}
|
|
2886
|
+
#getArgValue(arg) {
|
|
2887
|
+
if (arg === void 0 || arg === null) {
|
|
2888
|
+
return void 0;
|
|
2889
|
+
}
|
|
2890
|
+
return typeof arg === "object" && Object.prototype.hasOwnProperty.call(arg, "value") ? arg.value : arg;
|
|
2891
|
+
}
|
|
2892
|
+
#requireIntArg(args, index, errorMessage) {
|
|
2893
|
+
const value = this.#getArgValue(this.#argAt(args, index));
|
|
2894
|
+
if (!Number.isFinite(value)) {
|
|
2895
|
+
throw new Error(errorMessage);
|
|
2896
|
+
}
|
|
2897
|
+
return Math.floor(value);
|
|
2898
|
+
}
|
|
2899
|
+
#optionalIntArg(args, index, defaultValue = 0) {
|
|
2900
|
+
const value = this.#getArgValue(this.#argAt(args, index));
|
|
2901
|
+
if (!Number.isFinite(value)) {
|
|
2902
|
+
return defaultValue;
|
|
2903
|
+
}
|
|
2904
|
+
return Math.floor(value);
|
|
2905
|
+
}
|
|
2906
|
+
#requireStringArg(args, index, errorMessage) {
|
|
2907
|
+
const value = this.#getArgValue(this.#argAt(args, index));
|
|
2908
|
+
if (typeof value !== "string") {
|
|
2909
|
+
throw new Error(errorMessage);
|
|
2910
|
+
}
|
|
2911
|
+
return value;
|
|
2912
|
+
}
|
|
2913
|
+
#isNumericArg(arg) {
|
|
2914
|
+
if (!arg) {
|
|
2915
|
+
return false;
|
|
2916
|
+
}
|
|
2917
|
+
const value = this.#getArgValue(arg);
|
|
2918
|
+
return Number.isFinite(value);
|
|
2919
|
+
}
|
|
2920
|
+
#detachAllocationPromise(promise, context) {
|
|
2921
|
+
if (!promise || typeof promise.catch !== "function") {
|
|
2922
|
+
return;
|
|
2923
|
+
}
|
|
2924
|
+
promise.catch((error) => {
|
|
2925
|
+
console.error(`[SuperSonic] ${context} allocation failed:`, error);
|
|
2926
|
+
});
|
|
2927
|
+
}
|
|
2928
|
+
#requireBufferManager() {
|
|
2929
|
+
if (!this.bufferManager) {
|
|
2930
|
+
throw new Error("Buffer manager not ready. Call init() before issuing buffer commands.");
|
|
2931
|
+
}
|
|
2932
|
+
return this.bufferManager;
|
|
2933
|
+
}
|
|
2934
|
+
#isBundle(packet) {
|
|
2935
|
+
return packet && packet.timeTag !== void 0 && Array.isArray(packet.packets);
|
|
2936
|
+
}
|
|
2937
|
+
#calculateBundleWait(uint8Data) {
|
|
2938
|
+
if (uint8Data.length < 16) {
|
|
2939
|
+
return null;
|
|
2940
|
+
}
|
|
2941
|
+
const header = String.fromCharCode.apply(null, uint8Data.slice(0, 8));
|
|
2942
|
+
if (header !== "#bundle\0") {
|
|
2943
|
+
return null;
|
|
2944
|
+
}
|
|
2945
|
+
const ntpStartView = new Float64Array(this.sharedBuffer, this.ringBufferBase + this.bufferConstants.NTP_START_TIME_START, 1);
|
|
2946
|
+
const ntpStartTime = ntpStartView[0];
|
|
2947
|
+
if (ntpStartTime === 0) {
|
|
2948
|
+
console.warn("[SuperSonic] NTP start time not yet initialized");
|
|
2949
|
+
return null;
|
|
2950
|
+
}
|
|
2951
|
+
const driftView = new Int32Array(this.sharedBuffer, this.ringBufferBase + this.bufferConstants.DRIFT_OFFSET_START, 1);
|
|
2952
|
+
const driftMs = Atomics.load(driftView, 0);
|
|
2953
|
+
const driftSeconds = driftMs / 1e3;
|
|
2954
|
+
const globalView = new Int32Array(this.sharedBuffer, this.ringBufferBase + this.bufferConstants.GLOBAL_OFFSET_START, 1);
|
|
2955
|
+
const globalMs = Atomics.load(globalView, 0);
|
|
2956
|
+
const globalSeconds = globalMs / 1e3;
|
|
2957
|
+
const totalOffset = ntpStartTime + driftSeconds + globalSeconds;
|
|
2958
|
+
const view = new DataView(uint8Data.buffer, uint8Data.byteOffset);
|
|
2959
|
+
const ntpSeconds = view.getUint32(8, false);
|
|
2960
|
+
const ntpFraction = view.getUint32(12, false);
|
|
2961
|
+
if (ntpSeconds === 0 && (ntpFraction === 0 || ntpFraction === 1)) {
|
|
2962
|
+
return null;
|
|
2963
|
+
}
|
|
2964
|
+
const ntpTimeS = ntpSeconds + ntpFraction / 4294967296;
|
|
2965
|
+
const audioTimeS = ntpTimeS - totalOffset;
|
|
2966
|
+
const currentTimeS = this.audioContext.currentTime;
|
|
2967
|
+
return { audioTimeS, currentTimeS };
|
|
2968
|
+
}
|
|
2407
2969
|
};
|
|
2408
2970
|
export {
|
|
2409
2971
|
SuperSonic
|