supersonic-scsynth 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +27 -0
- package/README.md +320 -0
- package/dist/README.md +21 -0
- package/dist/supersonic.js +2411 -0
- package/dist/wasm/manifest.json +8 -0
- package/dist/wasm/scsynth-nrt.wasm +0 -0
- package/dist/workers/debug_worker.js +274 -0
- package/dist/workers/osc_in_worker.js +274 -0
- package/dist/workers/osc_out_worker.js +519 -0
- package/dist/workers/scsynth_audio_worklet.js +531 -0
- package/package.json +51 -0
|
@@ -0,0 +1,2411 @@
|
|
|
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
|
+
// js/vendor/osc.js/osc.js
|
|
300
|
+
var osc = {};
|
|
301
|
+
var osc = osc || {};
|
|
302
|
+
(function() {
|
|
303
|
+
"use strict";
|
|
304
|
+
osc.SECS_70YRS = 2208988800;
|
|
305
|
+
osc.TWO_32 = 4294967296;
|
|
306
|
+
osc.defaults = {
|
|
307
|
+
metadata: false,
|
|
308
|
+
unpackSingleArgs: true
|
|
309
|
+
};
|
|
310
|
+
osc.isCommonJS = typeof module !== "undefined" && module.exports ? true : false;
|
|
311
|
+
osc.isNode = osc.isCommonJS && typeof window === "undefined";
|
|
312
|
+
osc.isElectron = typeof process !== "undefined" && process.versions && process.versions.electron ? true : false;
|
|
313
|
+
osc.isBufferEnv = osc.isNode || osc.isElectron;
|
|
314
|
+
osc.isArray = function(obj) {
|
|
315
|
+
return obj && Object.prototype.toString.call(obj) === "[object Array]";
|
|
316
|
+
};
|
|
317
|
+
osc.isTypedArrayView = function(obj) {
|
|
318
|
+
return obj.buffer && obj.buffer instanceof ArrayBuffer;
|
|
319
|
+
};
|
|
320
|
+
osc.isBuffer = function(obj) {
|
|
321
|
+
return osc.isBufferEnv && obj instanceof Buffer;
|
|
322
|
+
};
|
|
323
|
+
osc.Long = typeof Long !== "undefined" ? Long : void 0;
|
|
324
|
+
osc.TextDecoder = typeof TextDecoder !== "undefined" ? new TextDecoder("utf-8") : typeof util !== "undefined" && typeof (util.TextDecoder !== "undefined") ? new util.TextDecoder("utf-8") : void 0;
|
|
325
|
+
osc.TextEncoder = typeof TextEncoder !== "undefined" ? new TextEncoder("utf-8") : typeof util !== "undefined" && typeof (util.TextEncoder !== "undefined") ? new util.TextEncoder("utf-8") : void 0;
|
|
326
|
+
osc.dataView = function(obj, offset, length) {
|
|
327
|
+
if (obj.buffer) {
|
|
328
|
+
return new DataView(obj.buffer, offset, length);
|
|
329
|
+
}
|
|
330
|
+
if (obj instanceof ArrayBuffer) {
|
|
331
|
+
return new DataView(obj, offset, length);
|
|
332
|
+
}
|
|
333
|
+
return new DataView(new Uint8Array(obj), offset, length);
|
|
334
|
+
};
|
|
335
|
+
osc.byteArray = function(obj) {
|
|
336
|
+
if (obj instanceof Uint8Array) {
|
|
337
|
+
return obj;
|
|
338
|
+
}
|
|
339
|
+
var buf = obj.buffer ? obj.buffer : obj;
|
|
340
|
+
if (!(buf instanceof ArrayBuffer) && (typeof buf.length === "undefined" || typeof buf === "string")) {
|
|
341
|
+
throw new Error("Can't wrap a non-array-like object as Uint8Array. Object was: " + JSON.stringify(obj, null, 2));
|
|
342
|
+
}
|
|
343
|
+
return new Uint8Array(buf);
|
|
344
|
+
};
|
|
345
|
+
osc.nativeBuffer = function(obj) {
|
|
346
|
+
if (osc.isBufferEnv) {
|
|
347
|
+
return osc.isBuffer(obj) ? obj : Buffer.from(obj.buffer ? obj : new Uint8Array(obj));
|
|
348
|
+
}
|
|
349
|
+
return osc.isTypedArrayView(obj) ? obj : new Uint8Array(obj);
|
|
350
|
+
};
|
|
351
|
+
osc.copyByteArray = function(source, target, offset) {
|
|
352
|
+
if (osc.isTypedArrayView(source) && osc.isTypedArrayView(target)) {
|
|
353
|
+
target.set(source, offset);
|
|
354
|
+
} else {
|
|
355
|
+
var start = offset === void 0 ? 0 : offset, len = Math.min(target.length - offset, source.length);
|
|
356
|
+
for (var i = 0, j = start; i < len; i++, j++) {
|
|
357
|
+
target[j] = source[i];
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return target;
|
|
361
|
+
};
|
|
362
|
+
osc.readString = function(dv, offsetState) {
|
|
363
|
+
var charCodes = [], idx = offsetState.idx;
|
|
364
|
+
for (; idx < dv.byteLength; idx++) {
|
|
365
|
+
var charCode = dv.getUint8(idx);
|
|
366
|
+
if (charCode !== 0) {
|
|
367
|
+
charCodes.push(charCode);
|
|
368
|
+
} else {
|
|
369
|
+
idx++;
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
idx = idx + 3 & ~3;
|
|
374
|
+
offsetState.idx = idx;
|
|
375
|
+
var decoder = osc.isBufferEnv ? osc.readString.withBuffer : osc.TextDecoder ? osc.readString.withTextDecoder : osc.readString.raw;
|
|
376
|
+
return decoder(charCodes);
|
|
377
|
+
};
|
|
378
|
+
osc.readString.raw = function(charCodes) {
|
|
379
|
+
var str = "";
|
|
380
|
+
var sliceSize = 1e4;
|
|
381
|
+
for (var i = 0; i < charCodes.length; i += sliceSize) {
|
|
382
|
+
str += String.fromCharCode.apply(null, charCodes.slice(i, i + sliceSize));
|
|
383
|
+
}
|
|
384
|
+
return str;
|
|
385
|
+
};
|
|
386
|
+
osc.readString.withTextDecoder = function(charCodes) {
|
|
387
|
+
var data = new Int8Array(charCodes);
|
|
388
|
+
return osc.TextDecoder.decode(data);
|
|
389
|
+
};
|
|
390
|
+
osc.readString.withBuffer = function(charCodes) {
|
|
391
|
+
return Buffer.from(charCodes).toString("utf-8");
|
|
392
|
+
};
|
|
393
|
+
osc.writeString = function(str) {
|
|
394
|
+
var encoder = osc.isBufferEnv ? osc.writeString.withBuffer : osc.TextEncoder ? osc.writeString.withTextEncoder : null, terminated = str + "\0", encodedStr;
|
|
395
|
+
if (encoder) {
|
|
396
|
+
encodedStr = encoder(terminated);
|
|
397
|
+
}
|
|
398
|
+
var len = encoder ? encodedStr.length : terminated.length, paddedLen = len + 3 & ~3, arr = new Uint8Array(paddedLen);
|
|
399
|
+
for (var i = 0; i < len - 1; i++) {
|
|
400
|
+
var charCode = encoder ? encodedStr[i] : terminated.charCodeAt(i);
|
|
401
|
+
arr[i] = charCode;
|
|
402
|
+
}
|
|
403
|
+
return arr;
|
|
404
|
+
};
|
|
405
|
+
osc.writeString.withTextEncoder = function(str) {
|
|
406
|
+
return osc.TextEncoder.encode(str);
|
|
407
|
+
};
|
|
408
|
+
osc.writeString.withBuffer = function(str) {
|
|
409
|
+
return Buffer.from(str);
|
|
410
|
+
};
|
|
411
|
+
osc.readPrimitive = function(dv, readerName, numBytes, offsetState) {
|
|
412
|
+
var val = dv[readerName](offsetState.idx, false);
|
|
413
|
+
offsetState.idx += numBytes;
|
|
414
|
+
return val;
|
|
415
|
+
};
|
|
416
|
+
osc.writePrimitive = function(val, dv, writerName, numBytes, offset) {
|
|
417
|
+
offset = offset === void 0 ? 0 : offset;
|
|
418
|
+
var arr;
|
|
419
|
+
if (!dv) {
|
|
420
|
+
arr = new Uint8Array(numBytes);
|
|
421
|
+
dv = new DataView(arr.buffer);
|
|
422
|
+
} else {
|
|
423
|
+
arr = new Uint8Array(dv.buffer);
|
|
424
|
+
}
|
|
425
|
+
dv[writerName](offset, val, false);
|
|
426
|
+
return arr;
|
|
427
|
+
};
|
|
428
|
+
osc.readInt32 = function(dv, offsetState) {
|
|
429
|
+
return osc.readPrimitive(dv, "getInt32", 4, offsetState);
|
|
430
|
+
};
|
|
431
|
+
osc.writeInt32 = function(val, dv, offset) {
|
|
432
|
+
return osc.writePrimitive(val, dv, "setInt32", 4, offset);
|
|
433
|
+
};
|
|
434
|
+
osc.readInt64 = function(dv, offsetState) {
|
|
435
|
+
var high = osc.readPrimitive(dv, "getInt32", 4, offsetState), low = osc.readPrimitive(dv, "getInt32", 4, offsetState);
|
|
436
|
+
if (osc.Long) {
|
|
437
|
+
return new osc.Long(low, high);
|
|
438
|
+
} else {
|
|
439
|
+
return {
|
|
440
|
+
high,
|
|
441
|
+
low,
|
|
442
|
+
unsigned: false
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
osc.writeInt64 = function(val, dv, offset) {
|
|
447
|
+
var arr = new Uint8Array(8);
|
|
448
|
+
arr.set(osc.writePrimitive(val.high, dv, "setInt32", 4, offset), 0);
|
|
449
|
+
arr.set(osc.writePrimitive(val.low, dv, "setInt32", 4, offset + 4), 4);
|
|
450
|
+
return arr;
|
|
451
|
+
};
|
|
452
|
+
osc.readFloat32 = function(dv, offsetState) {
|
|
453
|
+
return osc.readPrimitive(dv, "getFloat32", 4, offsetState);
|
|
454
|
+
};
|
|
455
|
+
osc.writeFloat32 = function(val, dv, offset) {
|
|
456
|
+
return osc.writePrimitive(val, dv, "setFloat32", 4, offset);
|
|
457
|
+
};
|
|
458
|
+
osc.readFloat64 = function(dv, offsetState) {
|
|
459
|
+
return osc.readPrimitive(dv, "getFloat64", 8, offsetState);
|
|
460
|
+
};
|
|
461
|
+
osc.writeFloat64 = function(val, dv, offset) {
|
|
462
|
+
return osc.writePrimitive(val, dv, "setFloat64", 8, offset);
|
|
463
|
+
};
|
|
464
|
+
osc.readChar32 = function(dv, offsetState) {
|
|
465
|
+
var charCode = osc.readPrimitive(dv, "getUint32", 4, offsetState);
|
|
466
|
+
return String.fromCharCode(charCode);
|
|
467
|
+
};
|
|
468
|
+
osc.writeChar32 = function(str, dv, offset) {
|
|
469
|
+
var charCode = str.charCodeAt(0);
|
|
470
|
+
if (charCode === void 0 || charCode < -1) {
|
|
471
|
+
return void 0;
|
|
472
|
+
}
|
|
473
|
+
return osc.writePrimitive(charCode, dv, "setUint32", 4, offset);
|
|
474
|
+
};
|
|
475
|
+
osc.readBlob = function(dv, offsetState) {
|
|
476
|
+
var len = osc.readInt32(dv, offsetState), paddedLen = len + 3 & ~3, blob = new Uint8Array(dv.buffer, offsetState.idx, len);
|
|
477
|
+
offsetState.idx += paddedLen;
|
|
478
|
+
return blob;
|
|
479
|
+
};
|
|
480
|
+
osc.writeBlob = function(data) {
|
|
481
|
+
data = osc.byteArray(data);
|
|
482
|
+
var len = data.byteLength, paddedLen = len + 3 & ~3, offset = 4, blobLen = paddedLen + offset, arr = new Uint8Array(blobLen), dv = new DataView(arr.buffer);
|
|
483
|
+
osc.writeInt32(len, dv);
|
|
484
|
+
arr.set(data, offset);
|
|
485
|
+
return arr;
|
|
486
|
+
};
|
|
487
|
+
osc.readMIDIBytes = function(dv, offsetState) {
|
|
488
|
+
var midi = new Uint8Array(dv.buffer, offsetState.idx, 4);
|
|
489
|
+
offsetState.idx += 4;
|
|
490
|
+
return midi;
|
|
491
|
+
};
|
|
492
|
+
osc.writeMIDIBytes = function(bytes) {
|
|
493
|
+
bytes = osc.byteArray(bytes);
|
|
494
|
+
var arr = new Uint8Array(4);
|
|
495
|
+
arr.set(bytes);
|
|
496
|
+
return arr;
|
|
497
|
+
};
|
|
498
|
+
osc.readColor = function(dv, offsetState) {
|
|
499
|
+
var bytes = new Uint8Array(dv.buffer, offsetState.idx, 4), alpha = bytes[3] / 255;
|
|
500
|
+
offsetState.idx += 4;
|
|
501
|
+
return {
|
|
502
|
+
r: bytes[0],
|
|
503
|
+
g: bytes[1],
|
|
504
|
+
b: bytes[2],
|
|
505
|
+
a: alpha
|
|
506
|
+
};
|
|
507
|
+
};
|
|
508
|
+
osc.writeColor = function(color) {
|
|
509
|
+
var alpha = Math.round(color.a * 255), arr = new Uint8Array([color.r, color.g, color.b, alpha]);
|
|
510
|
+
return arr;
|
|
511
|
+
};
|
|
512
|
+
osc.readTrue = function() {
|
|
513
|
+
return true;
|
|
514
|
+
};
|
|
515
|
+
osc.readFalse = function() {
|
|
516
|
+
return false;
|
|
517
|
+
};
|
|
518
|
+
osc.readNull = function() {
|
|
519
|
+
return null;
|
|
520
|
+
};
|
|
521
|
+
osc.readImpulse = function() {
|
|
522
|
+
return 1;
|
|
523
|
+
};
|
|
524
|
+
osc.readTimeTag = function(dv, offsetState) {
|
|
525
|
+
var secs1900 = osc.readPrimitive(dv, "getUint32", 4, offsetState), frac = osc.readPrimitive(dv, "getUint32", 4, offsetState), native = secs1900 === 0 && frac === 1 ? Date.now() : osc.ntpToJSTime(secs1900, frac);
|
|
526
|
+
return {
|
|
527
|
+
raw: [secs1900, frac],
|
|
528
|
+
native
|
|
529
|
+
};
|
|
530
|
+
};
|
|
531
|
+
osc.writeTimeTag = function(timeTag) {
|
|
532
|
+
var raw = timeTag.raw ? timeTag.raw : osc.jsToNTPTime(timeTag.native), arr = new Uint8Array(8), dv = new DataView(arr.buffer);
|
|
533
|
+
osc.writeInt32(raw[0], dv, 0);
|
|
534
|
+
osc.writeInt32(raw[1], dv, 4);
|
|
535
|
+
return arr;
|
|
536
|
+
};
|
|
537
|
+
osc.timeTag = function(secs, now) {
|
|
538
|
+
secs = secs || 0;
|
|
539
|
+
now = now || Date.now();
|
|
540
|
+
var nowSecs = now / 1e3, nowWhole = Math.floor(nowSecs), nowFracs = nowSecs - nowWhole, secsWhole = Math.floor(secs), secsFracs = secs - secsWhole, fracs = nowFracs + secsFracs;
|
|
541
|
+
if (fracs > 1) {
|
|
542
|
+
var fracsWhole = Math.floor(fracs), fracsFracs = fracs - fracsWhole;
|
|
543
|
+
secsWhole += fracsWhole;
|
|
544
|
+
fracs = fracsFracs;
|
|
545
|
+
}
|
|
546
|
+
var ntpSecs = nowWhole + secsWhole + osc.SECS_70YRS, ntpFracs = Math.round(osc.TWO_32 * fracs);
|
|
547
|
+
return {
|
|
548
|
+
raw: [ntpSecs, ntpFracs]
|
|
549
|
+
};
|
|
550
|
+
};
|
|
551
|
+
osc.ntpToJSTime = function(secs1900, frac) {
|
|
552
|
+
var secs1970 = secs1900 - osc.SECS_70YRS, decimals = frac / osc.TWO_32, msTime = (secs1970 + decimals) * 1e3;
|
|
553
|
+
return msTime;
|
|
554
|
+
};
|
|
555
|
+
osc.jsToNTPTime = function(jsTime) {
|
|
556
|
+
var secs = jsTime / 1e3, secsWhole = Math.floor(secs), secsFrac = secs - secsWhole, ntpSecs = secsWhole + osc.SECS_70YRS, ntpFracs = Math.round(osc.TWO_32 * secsFrac);
|
|
557
|
+
return [ntpSecs, ntpFracs];
|
|
558
|
+
};
|
|
559
|
+
osc.readArguments = function(dv, options, offsetState) {
|
|
560
|
+
var typeTagString = osc.readString(dv, offsetState);
|
|
561
|
+
if (typeTagString.indexOf(",") !== 0) {
|
|
562
|
+
throw new Error("A malformed type tag string was found while reading the arguments of an OSC message. String was: " + typeTagString, " at offset: " + offsetState.idx);
|
|
563
|
+
}
|
|
564
|
+
var argTypes = typeTagString.substring(1).split(""), args = [];
|
|
565
|
+
osc.readArgumentsIntoArray(args, argTypes, typeTagString, dv, options, offsetState);
|
|
566
|
+
return args;
|
|
567
|
+
};
|
|
568
|
+
osc.readArgument = function(argType, typeTagString, dv, options, offsetState) {
|
|
569
|
+
var typeSpec = osc.argumentTypes[argType];
|
|
570
|
+
if (!typeSpec) {
|
|
571
|
+
throw new Error("'" + argType + "' is not a valid OSC type tag. Type tag string was: " + typeTagString);
|
|
572
|
+
}
|
|
573
|
+
var argReader = typeSpec.reader, arg = osc[argReader](dv, offsetState);
|
|
574
|
+
if (options.metadata) {
|
|
575
|
+
arg = {
|
|
576
|
+
type: argType,
|
|
577
|
+
value: arg
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
return arg;
|
|
581
|
+
};
|
|
582
|
+
osc.readArgumentsIntoArray = function(arr, argTypes, typeTagString, dv, options, offsetState) {
|
|
583
|
+
var i = 0;
|
|
584
|
+
while (i < argTypes.length) {
|
|
585
|
+
var argType = argTypes[i], arg;
|
|
586
|
+
if (argType === "[") {
|
|
587
|
+
var fromArrayOpen = argTypes.slice(i + 1), endArrayIdx = fromArrayOpen.indexOf("]");
|
|
588
|
+
if (endArrayIdx < 0) {
|
|
589
|
+
throw new Error("Invalid argument type tag: an open array type tag ('[') was found without a matching close array tag ('[]'). Type tag was: " + typeTagString);
|
|
590
|
+
}
|
|
591
|
+
var typesInArray = fromArrayOpen.slice(0, endArrayIdx);
|
|
592
|
+
arg = osc.readArgumentsIntoArray([], typesInArray, typeTagString, dv, options, offsetState);
|
|
593
|
+
i += endArrayIdx + 2;
|
|
594
|
+
} else {
|
|
595
|
+
arg = osc.readArgument(argType, typeTagString, dv, options, offsetState);
|
|
596
|
+
i++;
|
|
597
|
+
}
|
|
598
|
+
arr.push(arg);
|
|
599
|
+
}
|
|
600
|
+
return arr;
|
|
601
|
+
};
|
|
602
|
+
osc.writeArguments = function(args, options) {
|
|
603
|
+
var argCollection = osc.collectArguments(args, options);
|
|
604
|
+
return osc.joinParts(argCollection);
|
|
605
|
+
};
|
|
606
|
+
osc.joinParts = function(dataCollection) {
|
|
607
|
+
var buf = new Uint8Array(dataCollection.byteLength), parts = dataCollection.parts, offset = 0;
|
|
608
|
+
for (var i = 0; i < parts.length; i++) {
|
|
609
|
+
var part = parts[i];
|
|
610
|
+
osc.copyByteArray(part, buf, offset);
|
|
611
|
+
offset += part.length;
|
|
612
|
+
}
|
|
613
|
+
return buf;
|
|
614
|
+
};
|
|
615
|
+
osc.addDataPart = function(dataPart, dataCollection) {
|
|
616
|
+
dataCollection.parts.push(dataPart);
|
|
617
|
+
dataCollection.byteLength += dataPart.length;
|
|
618
|
+
};
|
|
619
|
+
osc.writeArrayArguments = function(args, dataCollection) {
|
|
620
|
+
var typeTag = "[";
|
|
621
|
+
for (var i = 0; i < args.length; i++) {
|
|
622
|
+
var arg = args[i];
|
|
623
|
+
typeTag += osc.writeArgument(arg, dataCollection);
|
|
624
|
+
}
|
|
625
|
+
typeTag += "]";
|
|
626
|
+
return typeTag;
|
|
627
|
+
};
|
|
628
|
+
osc.writeArgument = function(arg, dataCollection) {
|
|
629
|
+
if (osc.isArray(arg)) {
|
|
630
|
+
return osc.writeArrayArguments(arg, dataCollection);
|
|
631
|
+
}
|
|
632
|
+
var type = arg.type, writer = osc.argumentTypes[type].writer;
|
|
633
|
+
if (writer) {
|
|
634
|
+
var data = osc[writer](arg.value);
|
|
635
|
+
osc.addDataPart(data, dataCollection);
|
|
636
|
+
}
|
|
637
|
+
return arg.type;
|
|
638
|
+
};
|
|
639
|
+
osc.collectArguments = function(args, options, dataCollection) {
|
|
640
|
+
if (!osc.isArray(args)) {
|
|
641
|
+
args = typeof args === "undefined" ? [] : [args];
|
|
642
|
+
}
|
|
643
|
+
dataCollection = dataCollection || {
|
|
644
|
+
byteLength: 0,
|
|
645
|
+
parts: []
|
|
646
|
+
};
|
|
647
|
+
if (!options.metadata) {
|
|
648
|
+
args = osc.annotateArguments(args);
|
|
649
|
+
}
|
|
650
|
+
var typeTagString = ",", currPartIdx = dataCollection.parts.length;
|
|
651
|
+
for (var i = 0; i < args.length; i++) {
|
|
652
|
+
var arg = args[i];
|
|
653
|
+
typeTagString += osc.writeArgument(arg, dataCollection);
|
|
654
|
+
}
|
|
655
|
+
var typeData = osc.writeString(typeTagString);
|
|
656
|
+
dataCollection.byteLength += typeData.byteLength;
|
|
657
|
+
dataCollection.parts.splice(currPartIdx, 0, typeData);
|
|
658
|
+
return dataCollection;
|
|
659
|
+
};
|
|
660
|
+
osc.readMessage = function(data, options, offsetState) {
|
|
661
|
+
options = options || osc.defaults;
|
|
662
|
+
var dv = osc.dataView(data, data.byteOffset, data.byteLength);
|
|
663
|
+
offsetState = offsetState || {
|
|
664
|
+
idx: 0
|
|
665
|
+
};
|
|
666
|
+
var address = osc.readString(dv, offsetState);
|
|
667
|
+
return osc.readMessageContents(address, dv, options, offsetState);
|
|
668
|
+
};
|
|
669
|
+
osc.readMessageContents = function(address, dv, options, offsetState) {
|
|
670
|
+
if (address.indexOf("/") !== 0) {
|
|
671
|
+
throw new Error("A malformed OSC address was found while reading an OSC message. String was: " + address);
|
|
672
|
+
}
|
|
673
|
+
var args = osc.readArguments(dv, options, offsetState);
|
|
674
|
+
return {
|
|
675
|
+
address,
|
|
676
|
+
args: args.length === 1 && options.unpackSingleArgs ? args[0] : args
|
|
677
|
+
};
|
|
678
|
+
};
|
|
679
|
+
osc.collectMessageParts = function(msg, options, dataCollection) {
|
|
680
|
+
dataCollection = dataCollection || {
|
|
681
|
+
byteLength: 0,
|
|
682
|
+
parts: []
|
|
683
|
+
};
|
|
684
|
+
osc.addDataPart(osc.writeString(msg.address), dataCollection);
|
|
685
|
+
return osc.collectArguments(msg.args, options, dataCollection);
|
|
686
|
+
};
|
|
687
|
+
osc.writeMessage = function(msg, options) {
|
|
688
|
+
options = options || osc.defaults;
|
|
689
|
+
if (!osc.isValidMessage(msg)) {
|
|
690
|
+
throw new Error("An OSC message must contain a valid address. Message was: " + JSON.stringify(msg, null, 2));
|
|
691
|
+
}
|
|
692
|
+
var msgCollection = osc.collectMessageParts(msg, options);
|
|
693
|
+
return osc.joinParts(msgCollection);
|
|
694
|
+
};
|
|
695
|
+
osc.isValidMessage = function(msg) {
|
|
696
|
+
return msg.address && msg.address.indexOf("/") === 0;
|
|
697
|
+
};
|
|
698
|
+
osc.readBundle = function(dv, options, offsetState) {
|
|
699
|
+
return osc.readPacket(dv, options, offsetState);
|
|
700
|
+
};
|
|
701
|
+
osc.collectBundlePackets = function(bundle, options, dataCollection) {
|
|
702
|
+
dataCollection = dataCollection || {
|
|
703
|
+
byteLength: 0,
|
|
704
|
+
parts: []
|
|
705
|
+
};
|
|
706
|
+
osc.addDataPart(osc.writeString("#bundle"), dataCollection);
|
|
707
|
+
osc.addDataPart(osc.writeTimeTag(bundle.timeTag), dataCollection);
|
|
708
|
+
for (var i = 0; i < bundle.packets.length; i++) {
|
|
709
|
+
var packet = bundle.packets[i], collector = packet.address ? osc.collectMessageParts : osc.collectBundlePackets, packetCollection = collector(packet, options);
|
|
710
|
+
dataCollection.byteLength += packetCollection.byteLength;
|
|
711
|
+
osc.addDataPart(osc.writeInt32(packetCollection.byteLength), dataCollection);
|
|
712
|
+
dataCollection.parts = dataCollection.parts.concat(packetCollection.parts);
|
|
713
|
+
}
|
|
714
|
+
return dataCollection;
|
|
715
|
+
};
|
|
716
|
+
osc.writeBundle = function(bundle, options) {
|
|
717
|
+
if (!osc.isValidBundle(bundle)) {
|
|
718
|
+
throw new Error("An OSC bundle must contain 'timeTag' and 'packets' properties. Bundle was: " + JSON.stringify(bundle, null, 2));
|
|
719
|
+
}
|
|
720
|
+
options = options || osc.defaults;
|
|
721
|
+
var bundleCollection = osc.collectBundlePackets(bundle, options);
|
|
722
|
+
return osc.joinParts(bundleCollection);
|
|
723
|
+
};
|
|
724
|
+
osc.isValidBundle = function(bundle) {
|
|
725
|
+
return bundle.timeTag !== void 0 && bundle.packets !== void 0;
|
|
726
|
+
};
|
|
727
|
+
osc.readBundleContents = function(dv, options, offsetState, len) {
|
|
728
|
+
var timeTag = osc.readTimeTag(dv, offsetState), packets = [];
|
|
729
|
+
while (offsetState.idx < len) {
|
|
730
|
+
var packetSize = osc.readInt32(dv, offsetState), packetLen = offsetState.idx + packetSize, packet = osc.readPacket(dv, options, offsetState, packetLen);
|
|
731
|
+
packets.push(packet);
|
|
732
|
+
}
|
|
733
|
+
return {
|
|
734
|
+
timeTag,
|
|
735
|
+
packets
|
|
736
|
+
};
|
|
737
|
+
};
|
|
738
|
+
osc.readPacket = function(data, options, offsetState, len) {
|
|
739
|
+
var dv = osc.dataView(data, data.byteOffset, data.byteLength);
|
|
740
|
+
len = len === void 0 ? dv.byteLength : len;
|
|
741
|
+
offsetState = offsetState || {
|
|
742
|
+
idx: 0
|
|
743
|
+
};
|
|
744
|
+
var header = osc.readString(dv, offsetState), firstChar = header[0];
|
|
745
|
+
if (firstChar === "#") {
|
|
746
|
+
return osc.readBundleContents(dv, options, offsetState, len);
|
|
747
|
+
} else if (firstChar === "/") {
|
|
748
|
+
return osc.readMessageContents(header, dv, options, offsetState);
|
|
749
|
+
}
|
|
750
|
+
throw new Error("The header of an OSC packet didn't contain an OSC address or a #bundle string. Header was: " + header);
|
|
751
|
+
};
|
|
752
|
+
osc.writePacket = function(packet, options) {
|
|
753
|
+
if (osc.isValidMessage(packet)) {
|
|
754
|
+
return osc.writeMessage(packet, options);
|
|
755
|
+
} else if (osc.isValidBundle(packet)) {
|
|
756
|
+
return osc.writeBundle(packet, options);
|
|
757
|
+
} else {
|
|
758
|
+
throw new Error("The specified packet was not recognized as a valid OSC message or bundle. Packet was: " + JSON.stringify(packet, null, 2));
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
osc.argumentTypes = {
|
|
762
|
+
i: {
|
|
763
|
+
reader: "readInt32",
|
|
764
|
+
writer: "writeInt32"
|
|
765
|
+
},
|
|
766
|
+
h: {
|
|
767
|
+
reader: "readInt64",
|
|
768
|
+
writer: "writeInt64"
|
|
769
|
+
},
|
|
770
|
+
f: {
|
|
771
|
+
reader: "readFloat32",
|
|
772
|
+
writer: "writeFloat32"
|
|
773
|
+
},
|
|
774
|
+
s: {
|
|
775
|
+
reader: "readString",
|
|
776
|
+
writer: "writeString"
|
|
777
|
+
},
|
|
778
|
+
S: {
|
|
779
|
+
reader: "readString",
|
|
780
|
+
writer: "writeString"
|
|
781
|
+
},
|
|
782
|
+
b: {
|
|
783
|
+
reader: "readBlob",
|
|
784
|
+
writer: "writeBlob"
|
|
785
|
+
},
|
|
786
|
+
t: {
|
|
787
|
+
reader: "readTimeTag",
|
|
788
|
+
writer: "writeTimeTag"
|
|
789
|
+
},
|
|
790
|
+
T: {
|
|
791
|
+
reader: "readTrue"
|
|
792
|
+
},
|
|
793
|
+
F: {
|
|
794
|
+
reader: "readFalse"
|
|
795
|
+
},
|
|
796
|
+
N: {
|
|
797
|
+
reader: "readNull"
|
|
798
|
+
},
|
|
799
|
+
I: {
|
|
800
|
+
reader: "readImpulse"
|
|
801
|
+
},
|
|
802
|
+
d: {
|
|
803
|
+
reader: "readFloat64",
|
|
804
|
+
writer: "writeFloat64"
|
|
805
|
+
},
|
|
806
|
+
c: {
|
|
807
|
+
reader: "readChar32",
|
|
808
|
+
writer: "writeChar32"
|
|
809
|
+
},
|
|
810
|
+
r: {
|
|
811
|
+
reader: "readColor",
|
|
812
|
+
writer: "writeColor"
|
|
813
|
+
},
|
|
814
|
+
m: {
|
|
815
|
+
reader: "readMIDIBytes",
|
|
816
|
+
writer: "writeMIDIBytes"
|
|
817
|
+
}
|
|
818
|
+
// [] are special cased within read/writeArguments()
|
|
819
|
+
};
|
|
820
|
+
osc.inferTypeForArgument = function(arg) {
|
|
821
|
+
var type = typeof arg;
|
|
822
|
+
switch (type) {
|
|
823
|
+
case "boolean":
|
|
824
|
+
return arg ? "T" : "F";
|
|
825
|
+
case "string":
|
|
826
|
+
return "s";
|
|
827
|
+
case "number":
|
|
828
|
+
return "f";
|
|
829
|
+
case "undefined":
|
|
830
|
+
return "N";
|
|
831
|
+
case "object":
|
|
832
|
+
if (arg === null) {
|
|
833
|
+
return "N";
|
|
834
|
+
} else if (arg instanceof Uint8Array || arg instanceof ArrayBuffer) {
|
|
835
|
+
return "b";
|
|
836
|
+
} else if (typeof arg.high === "number" && typeof arg.low === "number") {
|
|
837
|
+
return "h";
|
|
838
|
+
}
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
841
|
+
throw new Error("Can't infer OSC argument type for value: " + JSON.stringify(arg, null, 2));
|
|
842
|
+
};
|
|
843
|
+
osc.annotateArguments = function(args) {
|
|
844
|
+
var annotated = [];
|
|
845
|
+
for (var i = 0; i < args.length; i++) {
|
|
846
|
+
var arg = args[i], msgArg;
|
|
847
|
+
if (typeof arg === "object" && arg.type && arg.value !== void 0) {
|
|
848
|
+
msgArg = arg;
|
|
849
|
+
} else if (osc.isArray(arg)) {
|
|
850
|
+
msgArg = osc.annotateArguments(arg);
|
|
851
|
+
} else {
|
|
852
|
+
var oscType = osc.inferTypeForArgument(arg);
|
|
853
|
+
msgArg = {
|
|
854
|
+
type: oscType,
|
|
855
|
+
value: arg
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
annotated.push(msgArg);
|
|
859
|
+
}
|
|
860
|
+
return annotated;
|
|
861
|
+
};
|
|
862
|
+
;
|
|
863
|
+
})();
|
|
864
|
+
var EventEmitter = function() {
|
|
865
|
+
};
|
|
866
|
+
EventEmitter.prototype.on = function() {
|
|
867
|
+
};
|
|
868
|
+
EventEmitter.prototype.emit = function() {
|
|
869
|
+
};
|
|
870
|
+
EventEmitter.prototype.removeListener = function() {
|
|
871
|
+
};
|
|
872
|
+
(function() {
|
|
873
|
+
"use strict";
|
|
874
|
+
osc.supportsSerial = false;
|
|
875
|
+
osc.firePacketEvents = function(port, packet, timeTag, packetInfo) {
|
|
876
|
+
if (packet.address) {
|
|
877
|
+
port.emit("message", packet, timeTag, packetInfo);
|
|
878
|
+
} else {
|
|
879
|
+
osc.fireBundleEvents(port, packet, timeTag, packetInfo);
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
osc.fireBundleEvents = function(port, bundle, timeTag, packetInfo) {
|
|
883
|
+
port.emit("bundle", bundle, timeTag, packetInfo);
|
|
884
|
+
for (var i = 0; i < bundle.packets.length; i++) {
|
|
885
|
+
var packet = bundle.packets[i];
|
|
886
|
+
osc.firePacketEvents(port, packet, bundle.timeTag, packetInfo);
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
osc.fireClosedPortSendError = function(port, msg) {
|
|
890
|
+
msg = msg || "Can't send packets on a closed osc.Port object. Please open (or reopen) this Port by calling open().";
|
|
891
|
+
port.emit("error", msg);
|
|
892
|
+
};
|
|
893
|
+
osc.Port = function(options) {
|
|
894
|
+
this.options = options || {};
|
|
895
|
+
this.on("data", this.decodeOSC.bind(this));
|
|
896
|
+
};
|
|
897
|
+
var p = osc.Port.prototype = Object.create(EventEmitter.prototype);
|
|
898
|
+
p.constructor = osc.Port;
|
|
899
|
+
p.send = function(oscPacket) {
|
|
900
|
+
var args = Array.prototype.slice.call(arguments), encoded = this.encodeOSC(oscPacket), buf = osc.nativeBuffer(encoded);
|
|
901
|
+
args[0] = buf;
|
|
902
|
+
this.sendRaw.apply(this, args);
|
|
903
|
+
};
|
|
904
|
+
p.encodeOSC = function(packet) {
|
|
905
|
+
packet = packet.buffer ? packet.buffer : packet;
|
|
906
|
+
var encoded;
|
|
907
|
+
try {
|
|
908
|
+
encoded = osc.writePacket(packet, this.options);
|
|
909
|
+
} catch (err) {
|
|
910
|
+
this.emit("error", err);
|
|
911
|
+
}
|
|
912
|
+
return encoded;
|
|
913
|
+
};
|
|
914
|
+
p.decodeOSC = function(data, packetInfo) {
|
|
915
|
+
data = osc.byteArray(data);
|
|
916
|
+
this.emit("raw", data, packetInfo);
|
|
917
|
+
try {
|
|
918
|
+
var packet = osc.readPacket(data, this.options);
|
|
919
|
+
this.emit("osc", packet, packetInfo);
|
|
920
|
+
osc.firePacketEvents(this, packet, void 0, packetInfo);
|
|
921
|
+
} catch (err) {
|
|
922
|
+
this.emit("error", err);
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
osc.SLIPPort = function(options) {
|
|
926
|
+
var that = this;
|
|
927
|
+
var o = this.options = options || {};
|
|
928
|
+
o.useSLIP = o.useSLIP === void 0 ? true : o.useSLIP;
|
|
929
|
+
this.decoder = new slip.Decoder({
|
|
930
|
+
onMessage: this.decodeOSC.bind(this),
|
|
931
|
+
onError: function(err) {
|
|
932
|
+
that.emit("error", err);
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
var decodeHandler = o.useSLIP ? this.decodeSLIPData : this.decodeOSC;
|
|
936
|
+
this.on("data", decodeHandler.bind(this));
|
|
937
|
+
};
|
|
938
|
+
p = osc.SLIPPort.prototype = Object.create(osc.Port.prototype);
|
|
939
|
+
p.constructor = osc.SLIPPort;
|
|
940
|
+
p.encodeOSC = function(packet) {
|
|
941
|
+
packet = packet.buffer ? packet.buffer : packet;
|
|
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);
|
|
948
|
+
}
|
|
949
|
+
return framed;
|
|
950
|
+
};
|
|
951
|
+
p.decodeSLIPData = function(data, packetInfo) {
|
|
952
|
+
this.decoder.decode(data, packetInfo);
|
|
953
|
+
};
|
|
954
|
+
osc.relay = function(from, to, eventName, sendFnName, transformFn, sendArgs) {
|
|
955
|
+
eventName = eventName || "message";
|
|
956
|
+
sendFnName = sendFnName || "send";
|
|
957
|
+
transformFn = transformFn || function() {
|
|
958
|
+
};
|
|
959
|
+
sendArgs = sendArgs ? [null].concat(sendArgs) : [];
|
|
960
|
+
var listener = function(data) {
|
|
961
|
+
sendArgs[0] = data;
|
|
962
|
+
data = transformFn(data);
|
|
963
|
+
to[sendFnName].apply(to, sendArgs);
|
|
964
|
+
};
|
|
965
|
+
from.on(eventName, listener);
|
|
966
|
+
return {
|
|
967
|
+
eventName,
|
|
968
|
+
listener
|
|
969
|
+
};
|
|
970
|
+
};
|
|
971
|
+
osc.relayPorts = function(from, to, o) {
|
|
972
|
+
var eventName = o.raw ? "raw" : "osc", sendFnName = o.raw ? "sendRaw" : "send";
|
|
973
|
+
return osc.relay(from, to, eventName, sendFnName, o.transform);
|
|
974
|
+
};
|
|
975
|
+
osc.stopRelaying = function(from, relaySpec) {
|
|
976
|
+
from.removeListener(relaySpec.eventName, relaySpec.listener);
|
|
977
|
+
};
|
|
978
|
+
osc.Relay = function(port1, port2, options) {
|
|
979
|
+
var o = this.options = options || {};
|
|
980
|
+
o.raw = false;
|
|
981
|
+
this.port1 = port1;
|
|
982
|
+
this.port2 = port2;
|
|
983
|
+
this.listen();
|
|
984
|
+
};
|
|
985
|
+
p = osc.Relay.prototype = Object.create(EventEmitter.prototype);
|
|
986
|
+
p.constructor = osc.Relay;
|
|
987
|
+
p.open = function() {
|
|
988
|
+
this.port1.open();
|
|
989
|
+
this.port2.open();
|
|
990
|
+
};
|
|
991
|
+
p.listen = function() {
|
|
992
|
+
if (this.port1Spec && this.port2Spec) {
|
|
993
|
+
this.close();
|
|
994
|
+
}
|
|
995
|
+
this.port1Spec = osc.relayPorts(this.port1, this.port2, this.options);
|
|
996
|
+
this.port2Spec = osc.relayPorts(this.port2, this.port1, this.options);
|
|
997
|
+
var closeListener = this.close.bind(this);
|
|
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
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
var p = osc.WebSocketPort.prototype = Object.create(osc.Port.prototype);
|
|
1024
|
+
p.constructor = osc.WebSocketPort;
|
|
1025
|
+
p.open = function() {
|
|
1026
|
+
if (!this.socket || this.socket.readyState > 1) {
|
|
1027
|
+
this.socket = new osc.WebSocket(this.options.url);
|
|
1028
|
+
}
|
|
1029
|
+
osc.WebSocketPort.setupSocketForBinary(this.socket);
|
|
1030
|
+
var that = this;
|
|
1031
|
+
this.socket.onopen = function() {
|
|
1032
|
+
that.emit("open", that.socket);
|
|
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);
|
|
1045
|
+
};
|
|
1046
|
+
that.emit("ready");
|
|
1047
|
+
};
|
|
1048
|
+
p.sendRaw = function(encoded) {
|
|
1049
|
+
if (!this.socket || this.socket.readyState !== 1) {
|
|
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;
|
|
1064
|
+
|
|
1065
|
+
// node_modules/@thi.ng/api/typedarray.js
|
|
1066
|
+
var GL2TYPE = {
|
|
1067
|
+
[
|
|
1068
|
+
5120
|
|
1069
|
+
/* I8 */
|
|
1070
|
+
]: "i8",
|
|
1071
|
+
[
|
|
1072
|
+
5121
|
|
1073
|
+
/* U8 */
|
|
1074
|
+
]: "u8",
|
|
1075
|
+
[
|
|
1076
|
+
5122
|
|
1077
|
+
/* I16 */
|
|
1078
|
+
]: "i16",
|
|
1079
|
+
[
|
|
1080
|
+
5123
|
|
1081
|
+
/* U16 */
|
|
1082
|
+
]: "u16",
|
|
1083
|
+
[
|
|
1084
|
+
5124
|
|
1085
|
+
/* I32 */
|
|
1086
|
+
]: "i32",
|
|
1087
|
+
[
|
|
1088
|
+
5125
|
|
1089
|
+
/* U32 */
|
|
1090
|
+
]: "u32",
|
|
1091
|
+
[
|
|
1092
|
+
5126
|
|
1093
|
+
/* F32 */
|
|
1094
|
+
]: "f32"
|
|
1095
|
+
};
|
|
1096
|
+
var SIZEOF = {
|
|
1097
|
+
u8: 1,
|
|
1098
|
+
u8c: 1,
|
|
1099
|
+
i8: 1,
|
|
1100
|
+
u16: 2,
|
|
1101
|
+
i16: 2,
|
|
1102
|
+
u32: 4,
|
|
1103
|
+
i32: 4,
|
|
1104
|
+
i64: 8,
|
|
1105
|
+
u64: 8,
|
|
1106
|
+
f32: 4,
|
|
1107
|
+
f64: 8
|
|
1108
|
+
};
|
|
1109
|
+
var FLOAT_ARRAY_CTORS = {
|
|
1110
|
+
f32: Float32Array,
|
|
1111
|
+
f64: Float64Array
|
|
1112
|
+
};
|
|
1113
|
+
var INT_ARRAY_CTORS = {
|
|
1114
|
+
i8: Int8Array,
|
|
1115
|
+
i16: Int16Array,
|
|
1116
|
+
i32: Int32Array
|
|
1117
|
+
};
|
|
1118
|
+
var UINT_ARRAY_CTORS = {
|
|
1119
|
+
u8: Uint8Array,
|
|
1120
|
+
u8c: Uint8ClampedArray,
|
|
1121
|
+
u16: Uint16Array,
|
|
1122
|
+
u32: Uint32Array
|
|
1123
|
+
};
|
|
1124
|
+
var BIGINT_ARRAY_CTORS = {
|
|
1125
|
+
i64: BigInt64Array,
|
|
1126
|
+
u64: BigUint64Array
|
|
1127
|
+
};
|
|
1128
|
+
var TYPEDARRAY_CTORS = {
|
|
1129
|
+
...FLOAT_ARRAY_CTORS,
|
|
1130
|
+
...INT_ARRAY_CTORS,
|
|
1131
|
+
...UINT_ARRAY_CTORS
|
|
1132
|
+
};
|
|
1133
|
+
var asNativeType = (type) => {
|
|
1134
|
+
const t = GL2TYPE[type];
|
|
1135
|
+
return t !== void 0 ? t : type;
|
|
1136
|
+
};
|
|
1137
|
+
function typedArray(type, ...args) {
|
|
1138
|
+
const ctor = BIGINT_ARRAY_CTORS[type];
|
|
1139
|
+
return new (ctor || TYPEDARRAY_CTORS[asNativeType(type)])(...args);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// node_modules/@thi.ng/binary/align.js
|
|
1143
|
+
var align = (addr, size) => (size--, addr + size & ~size);
|
|
1144
|
+
|
|
1145
|
+
// node_modules/@thi.ng/checks/is-number.js
|
|
1146
|
+
var isNumber = (x) => typeof x === "number";
|
|
1147
|
+
|
|
1148
|
+
// node_modules/@thi.ng/errors/deferror.js
|
|
1149
|
+
var defError = (prefix, suffix = (msg) => msg !== void 0 ? ": " + msg : "") => class extends Error {
|
|
1150
|
+
origMessage;
|
|
1151
|
+
constructor(msg) {
|
|
1152
|
+
super(prefix(msg) + suffix(msg));
|
|
1153
|
+
this.origMessage = msg !== void 0 ? String(msg) : "";
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
|
|
1157
|
+
// node_modules/@thi.ng/errors/assert.js
|
|
1158
|
+
var AssertionError = defError(() => "Assertion failed");
|
|
1159
|
+
var assert = (typeof process !== "undefined" && process.env !== void 0 ? true : import.meta.env ? import.meta.env.MODE !== "production" || !!import.meta.env.UMBRELLA_ASSERTS || !!import.meta.env.VITE_UMBRELLA_ASSERTS : true) ? (test, msg) => {
|
|
1160
|
+
if (typeof test === "function" && !test() || !test) {
|
|
1161
|
+
throw new AssertionError(
|
|
1162
|
+
typeof msg === "function" ? msg() : msg
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
} : () => {
|
|
1166
|
+
};
|
|
1167
|
+
|
|
1168
|
+
// node_modules/@thi.ng/errors/illegal-arguments.js
|
|
1169
|
+
var IllegalArgumentError = defError(() => "illegal argument(s)");
|
|
1170
|
+
var illegalArgs = (msg) => {
|
|
1171
|
+
throw new IllegalArgumentError(msg);
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
// node_modules/@thi.ng/malloc/pool.js
|
|
1175
|
+
var STATE_FREE = 0;
|
|
1176
|
+
var STATE_USED = 1;
|
|
1177
|
+
var STATE_TOP = 2;
|
|
1178
|
+
var STATE_END = 3;
|
|
1179
|
+
var STATE_ALIGN = 4;
|
|
1180
|
+
var STATE_FLAGS = 5;
|
|
1181
|
+
var STATE_MIN_SPLIT = 6;
|
|
1182
|
+
var MASK_COMPACT = 1;
|
|
1183
|
+
var MASK_SPLIT = 2;
|
|
1184
|
+
var SIZEOF_STATE = 7 * 4;
|
|
1185
|
+
var MEM_BLOCK_SIZE = 0;
|
|
1186
|
+
var MEM_BLOCK_NEXT = 1;
|
|
1187
|
+
var SIZEOF_MEM_BLOCK = 2 * 4;
|
|
1188
|
+
var MemPool = class {
|
|
1189
|
+
buf;
|
|
1190
|
+
start;
|
|
1191
|
+
u8;
|
|
1192
|
+
u32;
|
|
1193
|
+
state;
|
|
1194
|
+
constructor(opts = {}) {
|
|
1195
|
+
this.buf = opts.buf ? opts.buf : new ArrayBuffer(opts.size || 4096);
|
|
1196
|
+
this.start = opts.start != null ? align(Math.max(opts.start, 0), 4) : 0;
|
|
1197
|
+
this.u8 = new Uint8Array(this.buf);
|
|
1198
|
+
this.u32 = new Uint32Array(this.buf);
|
|
1199
|
+
this.state = new Uint32Array(this.buf, this.start, SIZEOF_STATE / 4);
|
|
1200
|
+
if (!opts.skipInitialization) {
|
|
1201
|
+
const _align = opts.align || 8;
|
|
1202
|
+
assert(
|
|
1203
|
+
_align >= 8,
|
|
1204
|
+
`invalid alignment: ${_align}, must be a pow2 and >= 8`
|
|
1205
|
+
);
|
|
1206
|
+
const top = this.initialTop(_align);
|
|
1207
|
+
const resolvedEnd = opts.end != null ? Math.min(opts.end, this.buf.byteLength) : this.buf.byteLength;
|
|
1208
|
+
if (top >= resolvedEnd) {
|
|
1209
|
+
illegalArgs(
|
|
1210
|
+
`insufficient address range (0x${this.start.toString(
|
|
1211
|
+
16
|
|
1212
|
+
)} - 0x${resolvedEnd.toString(16)})`
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
this.align = _align;
|
|
1216
|
+
this.doCompact = opts.compact !== false;
|
|
1217
|
+
this.doSplit = opts.split !== false;
|
|
1218
|
+
this.minSplit = opts.minSplit || 16;
|
|
1219
|
+
this.end = resolvedEnd;
|
|
1220
|
+
this.top = top;
|
|
1221
|
+
this._free = 0;
|
|
1222
|
+
this._used = 0;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
stats() {
|
|
1226
|
+
const listStats = (block) => {
|
|
1227
|
+
let count = 0;
|
|
1228
|
+
let size = 0;
|
|
1229
|
+
while (block) {
|
|
1230
|
+
count++;
|
|
1231
|
+
size += this.blockSize(block);
|
|
1232
|
+
block = this.blockNext(block);
|
|
1233
|
+
}
|
|
1234
|
+
return { count, size };
|
|
1235
|
+
};
|
|
1236
|
+
const free = listStats(this._free);
|
|
1237
|
+
return {
|
|
1238
|
+
free,
|
|
1239
|
+
used: listStats(this._used),
|
|
1240
|
+
top: this.top,
|
|
1241
|
+
available: this.end - this.top + free.size,
|
|
1242
|
+
total: this.buf.byteLength
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
callocAs(type, num, fill = 0) {
|
|
1246
|
+
const block = this.mallocAs(type, num);
|
|
1247
|
+
block?.fill(fill);
|
|
1248
|
+
return block;
|
|
1249
|
+
}
|
|
1250
|
+
mallocAs(type, num) {
|
|
1251
|
+
const addr = this.malloc(num * SIZEOF[type]);
|
|
1252
|
+
return addr ? typedArray(type, this.buf, addr, num) : void 0;
|
|
1253
|
+
}
|
|
1254
|
+
calloc(bytes, fill = 0) {
|
|
1255
|
+
const addr = this.malloc(bytes);
|
|
1256
|
+
addr && this.u8.fill(fill, addr, addr + bytes);
|
|
1257
|
+
return addr;
|
|
1258
|
+
}
|
|
1259
|
+
malloc(bytes) {
|
|
1260
|
+
if (bytes <= 0) {
|
|
1261
|
+
return 0;
|
|
1262
|
+
}
|
|
1263
|
+
const paddedSize = align(bytes + SIZEOF_MEM_BLOCK, this.align);
|
|
1264
|
+
const end = this.end;
|
|
1265
|
+
let top = this.top;
|
|
1266
|
+
let block = this._free;
|
|
1267
|
+
let prev = 0;
|
|
1268
|
+
while (block) {
|
|
1269
|
+
const blockSize = this.blockSize(block);
|
|
1270
|
+
const isTop = block + blockSize >= top;
|
|
1271
|
+
if (isTop || blockSize >= paddedSize) {
|
|
1272
|
+
return this.mallocTop(
|
|
1273
|
+
block,
|
|
1274
|
+
prev,
|
|
1275
|
+
blockSize,
|
|
1276
|
+
paddedSize,
|
|
1277
|
+
isTop
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
prev = block;
|
|
1281
|
+
block = this.blockNext(block);
|
|
1282
|
+
}
|
|
1283
|
+
block = top;
|
|
1284
|
+
top = block + paddedSize;
|
|
1285
|
+
if (top <= end) {
|
|
1286
|
+
this.initBlock(block, paddedSize, this._used);
|
|
1287
|
+
this._used = block;
|
|
1288
|
+
this.top = top;
|
|
1289
|
+
return __blockDataAddress(block);
|
|
1290
|
+
}
|
|
1291
|
+
return 0;
|
|
1292
|
+
}
|
|
1293
|
+
mallocTop(block, prev, blockSize, paddedSize, isTop) {
|
|
1294
|
+
if (isTop && block + paddedSize > this.end) return 0;
|
|
1295
|
+
if (prev) {
|
|
1296
|
+
this.unlinkBlock(prev, block);
|
|
1297
|
+
} else {
|
|
1298
|
+
this._free = this.blockNext(block);
|
|
1299
|
+
}
|
|
1300
|
+
this.setBlockNext(block, this._used);
|
|
1301
|
+
this._used = block;
|
|
1302
|
+
if (isTop) {
|
|
1303
|
+
this.top = block + this.setBlockSize(block, paddedSize);
|
|
1304
|
+
} else if (this.doSplit) {
|
|
1305
|
+
const excess = blockSize - paddedSize;
|
|
1306
|
+
excess >= this.minSplit && this.splitBlock(block, paddedSize, excess);
|
|
1307
|
+
}
|
|
1308
|
+
return __blockDataAddress(block);
|
|
1309
|
+
}
|
|
1310
|
+
realloc(ptr, bytes) {
|
|
1311
|
+
if (bytes <= 0) {
|
|
1312
|
+
return 0;
|
|
1313
|
+
}
|
|
1314
|
+
const oldAddr = __blockSelfAddress(ptr);
|
|
1315
|
+
let newAddr = 0;
|
|
1316
|
+
let block = this._used;
|
|
1317
|
+
let blockEnd = 0;
|
|
1318
|
+
while (block) {
|
|
1319
|
+
if (block === oldAddr) {
|
|
1320
|
+
[newAddr, blockEnd] = this.reallocBlock(block, bytes);
|
|
1321
|
+
break;
|
|
1322
|
+
}
|
|
1323
|
+
block = this.blockNext(block);
|
|
1324
|
+
}
|
|
1325
|
+
if (newAddr && newAddr !== oldAddr) {
|
|
1326
|
+
this.u8.copyWithin(
|
|
1327
|
+
__blockDataAddress(newAddr),
|
|
1328
|
+
__blockDataAddress(oldAddr),
|
|
1329
|
+
blockEnd
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
return __blockDataAddress(newAddr);
|
|
1333
|
+
}
|
|
1334
|
+
reallocBlock(block, bytes) {
|
|
1335
|
+
const blockSize = this.blockSize(block);
|
|
1336
|
+
const blockEnd = block + blockSize;
|
|
1337
|
+
const isTop = blockEnd >= this.top;
|
|
1338
|
+
const paddedSize = align(bytes + SIZEOF_MEM_BLOCK, this.align);
|
|
1339
|
+
if (paddedSize <= blockSize) {
|
|
1340
|
+
if (this.doSplit) {
|
|
1341
|
+
const excess = blockSize - paddedSize;
|
|
1342
|
+
if (excess >= this.minSplit) {
|
|
1343
|
+
this.splitBlock(block, paddedSize, excess);
|
|
1344
|
+
} else if (isTop) {
|
|
1345
|
+
this.top = block + paddedSize;
|
|
1346
|
+
}
|
|
1347
|
+
} else if (isTop) {
|
|
1348
|
+
this.top = block + paddedSize;
|
|
1349
|
+
}
|
|
1350
|
+
return [block, blockEnd];
|
|
1351
|
+
}
|
|
1352
|
+
if (isTop && block + paddedSize < this.end) {
|
|
1353
|
+
this.top = block + this.setBlockSize(block, paddedSize);
|
|
1354
|
+
return [block, blockEnd];
|
|
1355
|
+
}
|
|
1356
|
+
this.free(block);
|
|
1357
|
+
return [__blockSelfAddress(this.malloc(bytes)), blockEnd];
|
|
1358
|
+
}
|
|
1359
|
+
reallocArray(array, num) {
|
|
1360
|
+
if (array.buffer !== this.buf) {
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
const addr = this.realloc(
|
|
1364
|
+
array.byteOffset,
|
|
1365
|
+
num * array.BYTES_PER_ELEMENT
|
|
1366
|
+
);
|
|
1367
|
+
return addr ? new array.constructor(this.buf, addr, num) : void 0;
|
|
1368
|
+
}
|
|
1369
|
+
free(ptrOrArray) {
|
|
1370
|
+
let addr;
|
|
1371
|
+
if (!isNumber(ptrOrArray)) {
|
|
1372
|
+
if (ptrOrArray.buffer !== this.buf) {
|
|
1373
|
+
return false;
|
|
1374
|
+
}
|
|
1375
|
+
addr = ptrOrArray.byteOffset;
|
|
1376
|
+
} else {
|
|
1377
|
+
addr = ptrOrArray;
|
|
1378
|
+
}
|
|
1379
|
+
addr = __blockSelfAddress(addr);
|
|
1380
|
+
let block = this._used;
|
|
1381
|
+
let prev = 0;
|
|
1382
|
+
while (block) {
|
|
1383
|
+
if (block === addr) {
|
|
1384
|
+
if (prev) {
|
|
1385
|
+
this.unlinkBlock(prev, block);
|
|
1386
|
+
} else {
|
|
1387
|
+
this._used = this.blockNext(block);
|
|
1388
|
+
}
|
|
1389
|
+
this.insert(block);
|
|
1390
|
+
this.doCompact && this.compact();
|
|
1391
|
+
return true;
|
|
1392
|
+
}
|
|
1393
|
+
prev = block;
|
|
1394
|
+
block = this.blockNext(block);
|
|
1395
|
+
}
|
|
1396
|
+
return false;
|
|
1397
|
+
}
|
|
1398
|
+
freeAll() {
|
|
1399
|
+
this._free = 0;
|
|
1400
|
+
this._used = 0;
|
|
1401
|
+
this.top = this.initialTop();
|
|
1402
|
+
}
|
|
1403
|
+
release() {
|
|
1404
|
+
delete this.u8;
|
|
1405
|
+
delete this.u32;
|
|
1406
|
+
delete this.state;
|
|
1407
|
+
delete this.buf;
|
|
1408
|
+
return true;
|
|
1409
|
+
}
|
|
1410
|
+
get align() {
|
|
1411
|
+
return this.state[STATE_ALIGN];
|
|
1412
|
+
}
|
|
1413
|
+
set align(x) {
|
|
1414
|
+
this.state[STATE_ALIGN] = x;
|
|
1415
|
+
}
|
|
1416
|
+
get end() {
|
|
1417
|
+
return this.state[STATE_END];
|
|
1418
|
+
}
|
|
1419
|
+
set end(x) {
|
|
1420
|
+
this.state[STATE_END] = x;
|
|
1421
|
+
}
|
|
1422
|
+
get top() {
|
|
1423
|
+
return this.state[STATE_TOP];
|
|
1424
|
+
}
|
|
1425
|
+
set top(x) {
|
|
1426
|
+
this.state[STATE_TOP] = x;
|
|
1427
|
+
}
|
|
1428
|
+
get _free() {
|
|
1429
|
+
return this.state[STATE_FREE];
|
|
1430
|
+
}
|
|
1431
|
+
set _free(block) {
|
|
1432
|
+
this.state[STATE_FREE] = block;
|
|
1433
|
+
}
|
|
1434
|
+
get _used() {
|
|
1435
|
+
return this.state[STATE_USED];
|
|
1436
|
+
}
|
|
1437
|
+
set _used(block) {
|
|
1438
|
+
this.state[STATE_USED] = block;
|
|
1439
|
+
}
|
|
1440
|
+
get doCompact() {
|
|
1441
|
+
return !!(this.state[STATE_FLAGS] & MASK_COMPACT);
|
|
1442
|
+
}
|
|
1443
|
+
set doCompact(flag) {
|
|
1444
|
+
flag ? this.state[STATE_FLAGS] |= 1 << MASK_COMPACT - 1 : this.state[STATE_FLAGS] &= ~MASK_COMPACT;
|
|
1445
|
+
}
|
|
1446
|
+
get doSplit() {
|
|
1447
|
+
return !!(this.state[STATE_FLAGS] & MASK_SPLIT);
|
|
1448
|
+
}
|
|
1449
|
+
set doSplit(flag) {
|
|
1450
|
+
flag ? this.state[STATE_FLAGS] |= 1 << MASK_SPLIT - 1 : this.state[STATE_FLAGS] &= ~MASK_SPLIT;
|
|
1451
|
+
}
|
|
1452
|
+
get minSplit() {
|
|
1453
|
+
return this.state[STATE_MIN_SPLIT];
|
|
1454
|
+
}
|
|
1455
|
+
set minSplit(x) {
|
|
1456
|
+
assert(
|
|
1457
|
+
x > SIZEOF_MEM_BLOCK,
|
|
1458
|
+
`illegal min split threshold: ${x}, require at least ${SIZEOF_MEM_BLOCK + 1}`
|
|
1459
|
+
);
|
|
1460
|
+
this.state[STATE_MIN_SPLIT] = x;
|
|
1461
|
+
}
|
|
1462
|
+
blockSize(block) {
|
|
1463
|
+
return this.u32[(block >> 2) + MEM_BLOCK_SIZE];
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* Sets & returns given block size.
|
|
1467
|
+
*
|
|
1468
|
+
* @param block -
|
|
1469
|
+
* @param size -
|
|
1470
|
+
*/
|
|
1471
|
+
setBlockSize(block, size) {
|
|
1472
|
+
this.u32[(block >> 2) + MEM_BLOCK_SIZE] = size;
|
|
1473
|
+
return size;
|
|
1474
|
+
}
|
|
1475
|
+
blockNext(block) {
|
|
1476
|
+
return this.u32[(block >> 2) + MEM_BLOCK_NEXT];
|
|
1477
|
+
}
|
|
1478
|
+
/**
|
|
1479
|
+
* Sets block next pointer to `next`. Use zero to indicate list end.
|
|
1480
|
+
*
|
|
1481
|
+
* @param block -
|
|
1482
|
+
*/
|
|
1483
|
+
setBlockNext(block, next) {
|
|
1484
|
+
this.u32[(block >> 2) + MEM_BLOCK_NEXT] = next;
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Initializes block header with given `size` and `next` pointer. Returns `block`.
|
|
1488
|
+
*
|
|
1489
|
+
* @param block -
|
|
1490
|
+
* @param size -
|
|
1491
|
+
* @param next -
|
|
1492
|
+
*/
|
|
1493
|
+
initBlock(block, size, next) {
|
|
1494
|
+
const idx = block >>> 2;
|
|
1495
|
+
this.u32[idx + MEM_BLOCK_SIZE] = size;
|
|
1496
|
+
this.u32[idx + MEM_BLOCK_NEXT] = next;
|
|
1497
|
+
return block;
|
|
1498
|
+
}
|
|
1499
|
+
unlinkBlock(prev, block) {
|
|
1500
|
+
this.setBlockNext(prev, this.blockNext(block));
|
|
1501
|
+
}
|
|
1502
|
+
splitBlock(block, blockSize, excess) {
|
|
1503
|
+
this.insert(
|
|
1504
|
+
this.initBlock(
|
|
1505
|
+
block + this.setBlockSize(block, blockSize),
|
|
1506
|
+
excess,
|
|
1507
|
+
0
|
|
1508
|
+
)
|
|
1509
|
+
);
|
|
1510
|
+
this.doCompact && this.compact();
|
|
1511
|
+
}
|
|
1512
|
+
initialTop(_align = this.align) {
|
|
1513
|
+
return align(this.start + SIZEOF_STATE + SIZEOF_MEM_BLOCK, _align) - SIZEOF_MEM_BLOCK;
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* Traverses free list and attempts to recursively merge blocks
|
|
1517
|
+
* occupying consecutive memory regions. Returns true if any blocks
|
|
1518
|
+
* have been merged. Only called if `compact` option is enabled.
|
|
1519
|
+
*/
|
|
1520
|
+
compact() {
|
|
1521
|
+
let block = this._free;
|
|
1522
|
+
let prev = 0;
|
|
1523
|
+
let scan = 0;
|
|
1524
|
+
let scanPrev;
|
|
1525
|
+
let res = false;
|
|
1526
|
+
while (block) {
|
|
1527
|
+
scanPrev = block;
|
|
1528
|
+
scan = this.blockNext(block);
|
|
1529
|
+
while (scan && scanPrev + this.blockSize(scanPrev) === scan) {
|
|
1530
|
+
scanPrev = scan;
|
|
1531
|
+
scan = this.blockNext(scan);
|
|
1532
|
+
}
|
|
1533
|
+
if (scanPrev !== block) {
|
|
1534
|
+
const newSize = scanPrev - block + this.blockSize(scanPrev);
|
|
1535
|
+
this.setBlockSize(block, newSize);
|
|
1536
|
+
const next = this.blockNext(scanPrev);
|
|
1537
|
+
let tmp = this.blockNext(block);
|
|
1538
|
+
while (tmp && tmp !== next) {
|
|
1539
|
+
const tn = this.blockNext(tmp);
|
|
1540
|
+
this.setBlockNext(tmp, 0);
|
|
1541
|
+
tmp = tn;
|
|
1542
|
+
}
|
|
1543
|
+
this.setBlockNext(block, next);
|
|
1544
|
+
res = true;
|
|
1545
|
+
}
|
|
1546
|
+
if (block + this.blockSize(block) >= this.top) {
|
|
1547
|
+
this.top = block;
|
|
1548
|
+
prev ? this.unlinkBlock(prev, block) : this._free = this.blockNext(block);
|
|
1549
|
+
}
|
|
1550
|
+
prev = block;
|
|
1551
|
+
block = this.blockNext(block);
|
|
1552
|
+
}
|
|
1553
|
+
return res;
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Inserts given block into list of free blocks, sorted by address.
|
|
1557
|
+
*
|
|
1558
|
+
* @param block -
|
|
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);
|
|
1567
|
+
}
|
|
1568
|
+
if (prev) {
|
|
1569
|
+
this.setBlockNext(prev, block);
|
|
1570
|
+
} else {
|
|
1571
|
+
this._free = block;
|
|
1572
|
+
}
|
|
1573
|
+
this.setBlockNext(block, ptr);
|
|
1574
|
+
}
|
|
1575
|
+
};
|
|
1576
|
+
var __blockDataAddress = (blockAddress) => blockAddress > 0 ? blockAddress + SIZEOF_MEM_BLOCK : 0;
|
|
1577
|
+
var __blockSelfAddress = (dataAddress) => dataAddress > 0 ? dataAddress - SIZEOF_MEM_BLOCK : 0;
|
|
1578
|
+
|
|
1579
|
+
// js/supersonic.js
|
|
1580
|
+
var SuperSonic = class {
|
|
1581
|
+
// Expose OSC utilities as static methods
|
|
1582
|
+
static osc = {
|
|
1583
|
+
encode: (message) => osc_default.writePacket(message),
|
|
1584
|
+
decode: (data, options = { metadata: false }) => osc_default.readPacket(data, options)
|
|
1585
|
+
};
|
|
1586
|
+
constructor(options = {}) {
|
|
1587
|
+
this.initialized = false;
|
|
1588
|
+
this.initializing = false;
|
|
1589
|
+
this.capabilities = {};
|
|
1590
|
+
this.sharedBuffer = null;
|
|
1591
|
+
this.ringBufferBase = null;
|
|
1592
|
+
this.bufferConstants = null;
|
|
1593
|
+
this.audioContext = null;
|
|
1594
|
+
this.workletNode = null;
|
|
1595
|
+
this.osc = null;
|
|
1596
|
+
this.wasmModule = null;
|
|
1597
|
+
this.wasmInstance = null;
|
|
1598
|
+
this.bufferPool = null;
|
|
1599
|
+
this.wasmTimeOffset = null;
|
|
1600
|
+
this._timeOffsetPromise = null;
|
|
1601
|
+
this._resolveTimeOffset = null;
|
|
1602
|
+
this.onMessageReceived = null;
|
|
1603
|
+
this.onMessageSent = null;
|
|
1604
|
+
this.onMetricsUpdate = null;
|
|
1605
|
+
this.onStatusUpdate = null;
|
|
1606
|
+
this.onSendError = null;
|
|
1607
|
+
this.onDebugMessage = null;
|
|
1608
|
+
this.onInitialized = null;
|
|
1609
|
+
this.onError = null;
|
|
1610
|
+
this.config = {
|
|
1611
|
+
wasmUrl: "./dist/wasm/scsynth-nrt.wasm",
|
|
1612
|
+
workletUrl: "./dist/workers/scsynth_audio_worklet.js",
|
|
1613
|
+
development: false,
|
|
1614
|
+
audioContextOptions: {
|
|
1615
|
+
latencyHint: "interactive",
|
|
1616
|
+
sampleRate: 48e3
|
|
1617
|
+
}
|
|
1618
|
+
};
|
|
1619
|
+
this.audioBaseURL = options.audioBaseURL || null;
|
|
1620
|
+
this.audioPathMap = options.audioPathMap || {};
|
|
1621
|
+
this.allocatedBuffers = /* @__PURE__ */ new Map();
|
|
1622
|
+
this.stats = {
|
|
1623
|
+
initStartTime: null,
|
|
1624
|
+
initDuration: null,
|
|
1625
|
+
messagesSent: 0,
|
|
1626
|
+
messagesReceived: 0,
|
|
1627
|
+
errors: 0
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
/**
|
|
1631
|
+
* Check browser capabilities for required features
|
|
1632
|
+
*/
|
|
1633
|
+
checkCapabilities() {
|
|
1634
|
+
this.capabilities = {
|
|
1635
|
+
audioWorklet: "AudioWorklet" in window,
|
|
1636
|
+
sharedArrayBuffer: typeof SharedArrayBuffer !== "undefined",
|
|
1637
|
+
crossOriginIsolated: window.crossOriginIsolated === true,
|
|
1638
|
+
wasmThreads: typeof WebAssembly !== "undefined" && typeof WebAssembly.Memory !== "undefined" && WebAssembly.Memory.prototype.hasOwnProperty("shared"),
|
|
1639
|
+
atomics: typeof Atomics !== "undefined",
|
|
1640
|
+
webWorker: typeof Worker !== "undefined"
|
|
1641
|
+
};
|
|
1642
|
+
const required = [
|
|
1643
|
+
"audioWorklet",
|
|
1644
|
+
"sharedArrayBuffer",
|
|
1645
|
+
"crossOriginIsolated",
|
|
1646
|
+
"atomics",
|
|
1647
|
+
"webWorker"
|
|
1648
|
+
];
|
|
1649
|
+
const missing = required.filter((f) => !this.capabilities[f]);
|
|
1650
|
+
if (missing.length > 0) {
|
|
1651
|
+
const error = new Error(`Missing required features: ${missing.join(", ")}`);
|
|
1652
|
+
if (!this.capabilities.crossOriginIsolated) {
|
|
1653
|
+
if (this.capabilities.sharedArrayBuffer) {
|
|
1654
|
+
error.message += "\n\nSharedArrayBuffer is available but cross-origin isolation is not enabled. Please ensure COOP and COEP headers are set correctly:\n Cross-Origin-Opener-Policy: same-origin\n Cross-Origin-Embedder-Policy: require-corp";
|
|
1655
|
+
} else {
|
|
1656
|
+
error.message += "\n\nSharedArrayBuffer is not available. This may be due to:\n1. Missing COOP/COEP headers\n2. Browser doesn't support SharedArrayBuffer\n3. Browser security settings";
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
throw error;
|
|
1660
|
+
}
|
|
1661
|
+
return this.capabilities;
|
|
1662
|
+
}
|
|
1663
|
+
/**
|
|
1664
|
+
* Initialize shared WebAssembly memory
|
|
1665
|
+
*/
|
|
1666
|
+
#initializeSharedMemory() {
|
|
1667
|
+
const TOTAL_PAGES = 3072;
|
|
1668
|
+
this.wasmMemory = new WebAssembly.Memory({
|
|
1669
|
+
initial: TOTAL_PAGES,
|
|
1670
|
+
maximum: TOTAL_PAGES,
|
|
1671
|
+
shared: true
|
|
1672
|
+
});
|
|
1673
|
+
this.sharedBuffer = this.wasmMemory.buffer;
|
|
1674
|
+
const BUFFER_POOL_OFFSET = 64 * 1024 * 1024;
|
|
1675
|
+
const BUFFER_POOL_SIZE = 128 * 1024 * 1024;
|
|
1676
|
+
this.bufferPool = new MemPool({
|
|
1677
|
+
buf: this.sharedBuffer,
|
|
1678
|
+
start: BUFFER_POOL_OFFSET,
|
|
1679
|
+
size: BUFFER_POOL_SIZE,
|
|
1680
|
+
align: 8
|
|
1681
|
+
// 8-byte alignment (minimum required by MemPool)
|
|
1682
|
+
});
|
|
1683
|
+
console.log("[SuperSonic] Buffer pool initialized: 128MB at offset 64MB");
|
|
1684
|
+
}
|
|
1685
|
+
/**
|
|
1686
|
+
* Calculate time offset (AudioContext → NTP conversion)
|
|
1687
|
+
* Called when AudioContext is in 'running' state to ensure accurate timing
|
|
1688
|
+
*/
|
|
1689
|
+
#calculateTimeOffset() {
|
|
1690
|
+
const SECONDS_1900_TO_1970 = 2208988800;
|
|
1691
|
+
const audioContextTime = this.audioContext.currentTime;
|
|
1692
|
+
const unixSeconds = Date.now() / 1e3;
|
|
1693
|
+
this.wasmTimeOffset = SECONDS_1900_TO_1970 + unixSeconds - audioContextTime;
|
|
1694
|
+
if (this._resolveTimeOffset) {
|
|
1695
|
+
this._resolveTimeOffset(this.wasmTimeOffset);
|
|
1696
|
+
this._resolveTimeOffset = null;
|
|
1697
|
+
}
|
|
1698
|
+
return this.wasmTimeOffset;
|
|
1699
|
+
}
|
|
1700
|
+
/**
|
|
1701
|
+
* Initialize AudioContext and set up time offset calculation
|
|
1702
|
+
*/
|
|
1703
|
+
#initializeAudioContext() {
|
|
1704
|
+
this.audioContext = new (window.AudioContext || window.webkitAudioContext)(
|
|
1705
|
+
this.config.audioContextOptions
|
|
1706
|
+
);
|
|
1707
|
+
this._timeOffsetPromise = new Promise((resolve) => {
|
|
1708
|
+
this._resolveTimeOffset = resolve;
|
|
1709
|
+
});
|
|
1710
|
+
if (this.audioContext.state === "suspended") {
|
|
1711
|
+
const resumeContext = async () => {
|
|
1712
|
+
if (this.audioContext.state === "suspended") {
|
|
1713
|
+
await this.audioContext.resume();
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1716
|
+
document.addEventListener("click", resumeContext, { once: true });
|
|
1717
|
+
document.addEventListener("touchstart", resumeContext, { once: true });
|
|
1718
|
+
}
|
|
1719
|
+
this.audioContext.addEventListener("statechange", () => {
|
|
1720
|
+
if (this.audioContext.state === "running" && this._resolveTimeOffset) {
|
|
1721
|
+
this.#calculateTimeOffset();
|
|
1722
|
+
}
|
|
1723
|
+
});
|
|
1724
|
+
if (this.audioContext.state === "running") {
|
|
1725
|
+
this.#calculateTimeOffset();
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
/**
|
|
1729
|
+
* Load WASM manifest to get the current hashed filename
|
|
1730
|
+
*/
|
|
1731
|
+
async #loadWasmManifest() {
|
|
1732
|
+
try {
|
|
1733
|
+
const manifestUrl = "./dist/wasm/manifest.json";
|
|
1734
|
+
const response = await fetch(manifestUrl);
|
|
1735
|
+
if (response.ok) {
|
|
1736
|
+
const manifest = await response.json();
|
|
1737
|
+
const wasmFile = this.config.development ? manifest.wasmFile : manifest.wasmFileStable;
|
|
1738
|
+
this.config.wasmUrl = `./dist/wasm/${wasmFile}`;
|
|
1739
|
+
console.log(`[SuperSonic] Using WASM build: ${wasmFile}`);
|
|
1740
|
+
console.log(`[SuperSonic] Build: ${manifest.buildId} (git: ${manifest.gitHash})`);
|
|
1741
|
+
}
|
|
1742
|
+
} catch (error) {
|
|
1743
|
+
console.warn("[SuperSonic] WASM manifest not found, using default filename");
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Load WASM binary from network
|
|
1748
|
+
*/
|
|
1749
|
+
async #loadWasm() {
|
|
1750
|
+
await this.#loadWasmManifest();
|
|
1751
|
+
const wasmResponse = await fetch(this.config.wasmUrl);
|
|
1752
|
+
if (!wasmResponse.ok) {
|
|
1753
|
+
throw new Error(`Failed to load WASM: ${wasmResponse.status} ${wasmResponse.statusText}`);
|
|
1754
|
+
}
|
|
1755
|
+
return await wasmResponse.arrayBuffer();
|
|
1756
|
+
}
|
|
1757
|
+
/**
|
|
1758
|
+
* Initialize AudioWorklet with WASM
|
|
1759
|
+
*/
|
|
1760
|
+
async #initializeAudioWorklet(wasmBytes) {
|
|
1761
|
+
await this.audioContext.audioWorklet.addModule(this.config.workletUrl);
|
|
1762
|
+
this.workletNode = new AudioWorkletNode(this.audioContext, "scsynth-processor", {
|
|
1763
|
+
numberOfInputs: 0,
|
|
1764
|
+
numberOfOutputs: 1,
|
|
1765
|
+
outputChannelCount: [2]
|
|
1766
|
+
});
|
|
1767
|
+
this.workletNode.connect(this.audioContext.destination);
|
|
1768
|
+
this.workletNode.port.postMessage({
|
|
1769
|
+
type: "init",
|
|
1770
|
+
sharedBuffer: this.sharedBuffer
|
|
1771
|
+
});
|
|
1772
|
+
const timeOffset = await this._timeOffsetPromise;
|
|
1773
|
+
this.workletNode.port.postMessage({
|
|
1774
|
+
type: "loadWasm",
|
|
1775
|
+
wasmBytes,
|
|
1776
|
+
wasmMemory: this.wasmMemory,
|
|
1777
|
+
timeOffset
|
|
1778
|
+
});
|
|
1779
|
+
await this.#waitForWorkletInit();
|
|
1780
|
+
}
|
|
1781
|
+
/**
|
|
1782
|
+
* Initialize OSC communication layer
|
|
1783
|
+
*/
|
|
1784
|
+
async #initializeOSC() {
|
|
1785
|
+
this.osc = new ScsynthOSC();
|
|
1786
|
+
this.osc.onOSCMessage((msg) => {
|
|
1787
|
+
if (msg.address === "/buffer/freed") {
|
|
1788
|
+
this._handleBufferFreed(msg.args);
|
|
1789
|
+
}
|
|
1790
|
+
if (this.onMessageReceived) {
|
|
1791
|
+
this.stats.messagesReceived++;
|
|
1792
|
+
this.onMessageReceived(msg);
|
|
1793
|
+
}
|
|
1794
|
+
});
|
|
1795
|
+
this.osc.onDebugMessage((msg) => {
|
|
1796
|
+
if (this.onDebugMessage) {
|
|
1797
|
+
this.onDebugMessage(msg);
|
|
1798
|
+
}
|
|
1799
|
+
});
|
|
1800
|
+
this.osc.onError((error, workerName) => {
|
|
1801
|
+
console.error(`[SuperSonic] ${workerName} error:`, error);
|
|
1802
|
+
this.stats.errors++;
|
|
1803
|
+
if (this.onError) {
|
|
1804
|
+
this.onError(new Error(`${workerName}: ${error}`));
|
|
1805
|
+
}
|
|
1806
|
+
});
|
|
1807
|
+
await this.osc.init(this.sharedBuffer, this.ringBufferBase, this.bufferConstants);
|
|
1808
|
+
}
|
|
1809
|
+
/**
|
|
1810
|
+
* Complete initialization and trigger callbacks
|
|
1811
|
+
*/
|
|
1812
|
+
#finishInitialization() {
|
|
1813
|
+
this.initialized = true;
|
|
1814
|
+
this.initializing = false;
|
|
1815
|
+
this.stats.initDuration = performance.now() - this.stats.initStartTime;
|
|
1816
|
+
console.log(`[SuperSonic] Initialization complete in ${this.stats.initDuration.toFixed(2)}ms`);
|
|
1817
|
+
if (this.onInitialized) {
|
|
1818
|
+
this.onInitialized({
|
|
1819
|
+
capabilities: this.capabilities,
|
|
1820
|
+
stats: this.stats
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
/**
|
|
1825
|
+
* Initialize the audio worklet system
|
|
1826
|
+
* @param {Object} config - Optional configuration overrides
|
|
1827
|
+
* @param {boolean} config.development - Use cache-busted WASM files (default: false)
|
|
1828
|
+
* @param {string} config.wasmUrl - Custom WASM URL
|
|
1829
|
+
* @param {string} config.workletUrl - Custom worklet URL
|
|
1830
|
+
* @param {Object} config.audioContextOptions - AudioContext options
|
|
1831
|
+
*/
|
|
1832
|
+
async init(config = {}) {
|
|
1833
|
+
if (this.initialized) {
|
|
1834
|
+
console.warn("[SuperSonic] Already initialized");
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
if (this.initializing) {
|
|
1838
|
+
console.warn("[SuperSonic] Initialization already in progress");
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
this.config = {
|
|
1842
|
+
...this.config,
|
|
1843
|
+
...config,
|
|
1844
|
+
audioContextOptions: {
|
|
1845
|
+
...this.config.audioContextOptions,
|
|
1846
|
+
...config.audioContextOptions || {}
|
|
1847
|
+
}
|
|
1848
|
+
};
|
|
1849
|
+
this.initializing = true;
|
|
1850
|
+
this.stats.initStartTime = performance.now();
|
|
1851
|
+
try {
|
|
1852
|
+
this.checkCapabilities();
|
|
1853
|
+
this.#initializeSharedMemory();
|
|
1854
|
+
this.#initializeAudioContext();
|
|
1855
|
+
const wasmBytes = await this.#loadWasm();
|
|
1856
|
+
await this.#initializeAudioWorklet(wasmBytes);
|
|
1857
|
+
await this.#initializeOSC();
|
|
1858
|
+
this.#setupMessageHandlers();
|
|
1859
|
+
this.#startPerformanceMonitoring();
|
|
1860
|
+
this.#finishInitialization();
|
|
1861
|
+
} catch (error) {
|
|
1862
|
+
this.initializing = false;
|
|
1863
|
+
console.error("[SuperSonic] Initialization failed:", error);
|
|
1864
|
+
if (this.onError) {
|
|
1865
|
+
this.onError(error);
|
|
1866
|
+
}
|
|
1867
|
+
throw error;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
/**
|
|
1871
|
+
* Wait for AudioWorklet to initialize
|
|
1872
|
+
*/
|
|
1873
|
+
#waitForWorkletInit() {
|
|
1874
|
+
return new Promise((resolve, reject) => {
|
|
1875
|
+
const timeout = setTimeout(() => {
|
|
1876
|
+
reject(new Error("AudioWorklet initialization timeout"));
|
|
1877
|
+
}, 5e3);
|
|
1878
|
+
const messageHandler = (event) => {
|
|
1879
|
+
if (event.data.type === "debug") {
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
if (event.data.type === "error") {
|
|
1883
|
+
console.error("[AudioWorklet] Error:", event.data.error);
|
|
1884
|
+
clearTimeout(timeout);
|
|
1885
|
+
this.workletNode.port.removeEventListener("message", messageHandler);
|
|
1886
|
+
reject(new Error(event.data.error || "AudioWorklet error"));
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
if (event.data.type === "initialized") {
|
|
1890
|
+
clearTimeout(timeout);
|
|
1891
|
+
this.workletNode.port.removeEventListener("message", messageHandler);
|
|
1892
|
+
if (event.data.success) {
|
|
1893
|
+
if (event.data.ringBufferBase !== void 0) {
|
|
1894
|
+
this.ringBufferBase = event.data.ringBufferBase;
|
|
1895
|
+
} else {
|
|
1896
|
+
console.warn("[SuperSonic] Warning: ringBufferBase not provided by worklet");
|
|
1897
|
+
}
|
|
1898
|
+
if (event.data.bufferConstants !== void 0) {
|
|
1899
|
+
this.bufferConstants = event.data.bufferConstants;
|
|
1900
|
+
} else {
|
|
1901
|
+
console.warn("[SuperSonic] Warning: bufferConstants not provided by worklet");
|
|
1902
|
+
}
|
|
1903
|
+
resolve();
|
|
1904
|
+
} else {
|
|
1905
|
+
reject(new Error(event.data.error || "AudioWorklet initialization failed"));
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
};
|
|
1909
|
+
this.workletNode.port.addEventListener("message", messageHandler);
|
|
1910
|
+
this.workletNode.port.start();
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1913
|
+
/**
|
|
1914
|
+
* Set up message handlers for worklet
|
|
1915
|
+
*/
|
|
1916
|
+
#setupMessageHandlers() {
|
|
1917
|
+
this.workletNode.port.onmessage = (event) => {
|
|
1918
|
+
const { data } = event;
|
|
1919
|
+
switch (data.type) {
|
|
1920
|
+
case "status":
|
|
1921
|
+
if (this.onStatusUpdate) {
|
|
1922
|
+
this.onStatusUpdate(data);
|
|
1923
|
+
}
|
|
1924
|
+
break;
|
|
1925
|
+
case "metrics":
|
|
1926
|
+
if (this.onMetricsUpdate) {
|
|
1927
|
+
this.onMetricsUpdate(data.metrics);
|
|
1928
|
+
}
|
|
1929
|
+
break;
|
|
1930
|
+
case "error":
|
|
1931
|
+
console.error("[Worklet] Error:", data.error);
|
|
1932
|
+
if (data.diagnostics) {
|
|
1933
|
+
console.error("[Worklet] Diagnostics:", data.diagnostics);
|
|
1934
|
+
console.table(data.diagnostics);
|
|
1935
|
+
}
|
|
1936
|
+
this.stats.errors++;
|
|
1937
|
+
if (this.onError) {
|
|
1938
|
+
this.onError(new Error(data.error));
|
|
1939
|
+
}
|
|
1940
|
+
break;
|
|
1941
|
+
case "process_debug":
|
|
1942
|
+
break;
|
|
1943
|
+
case "debug":
|
|
1944
|
+
break;
|
|
1945
|
+
case "console":
|
|
1946
|
+
if (this.onConsoleMessage) {
|
|
1947
|
+
this.onConsoleMessage(data.message);
|
|
1948
|
+
}
|
|
1949
|
+
break;
|
|
1950
|
+
case "version":
|
|
1951
|
+
if (this.onVersion) {
|
|
1952
|
+
this.onVersion(data.version);
|
|
1953
|
+
}
|
|
1954
|
+
break;
|
|
1955
|
+
}
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
/**
|
|
1959
|
+
* Start performance monitoring
|
|
1960
|
+
*/
|
|
1961
|
+
#startPerformanceMonitoring() {
|
|
1962
|
+
setInterval(() => {
|
|
1963
|
+
if (this.osc) {
|
|
1964
|
+
this.osc.getStats().then((stats) => {
|
|
1965
|
+
if (stats && this.onMetricsUpdate) {
|
|
1966
|
+
this.onMetricsUpdate(stats);
|
|
1967
|
+
}
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
if (this.workletNode) {
|
|
1971
|
+
this.workletNode.port.postMessage({ type: "getMetrics" });
|
|
1972
|
+
}
|
|
1973
|
+
}, 50);
|
|
1974
|
+
}
|
|
1975
|
+
/**
|
|
1976
|
+
* Send OSC message with simplified syntax (auto-detects types)
|
|
1977
|
+
* @param {string} address - OSC address
|
|
1978
|
+
* @param {...*} args - Arguments (numbers, strings, Uint8Array)
|
|
1979
|
+
* @example
|
|
1980
|
+
* sonic.send('/notify', 1);
|
|
1981
|
+
* sonic.send('/s_new', 'sonic-pi-beep', -1, 0, 0);
|
|
1982
|
+
* sonic.send('/n_set', 1000, 'freq', 440.0, 'amp', 0.5);
|
|
1983
|
+
*/
|
|
1984
|
+
async send(address, ...args) {
|
|
1985
|
+
if (!this.initialized) {
|
|
1986
|
+
throw new Error("SuperSonic not initialized. Call init() first.");
|
|
1987
|
+
}
|
|
1988
|
+
if (this._isBufferAllocationCommand(address)) {
|
|
1989
|
+
return await this._handleBufferCommand(address, args);
|
|
1990
|
+
}
|
|
1991
|
+
const oscArgs = args.map((arg) => {
|
|
1992
|
+
if (typeof arg === "string") {
|
|
1993
|
+
return { type: "s", value: arg };
|
|
1994
|
+
} else if (typeof arg === "number") {
|
|
1995
|
+
return { type: Number.isInteger(arg) ? "i" : "f", value: arg };
|
|
1996
|
+
} else if (arg instanceof Uint8Array || arg instanceof ArrayBuffer) {
|
|
1997
|
+
return { type: "b", value: arg instanceof ArrayBuffer ? new Uint8Array(arg) : arg };
|
|
1998
|
+
} else {
|
|
1999
|
+
throw new Error(`Unsupported argument type: ${typeof arg}`);
|
|
2000
|
+
}
|
|
2001
|
+
});
|
|
2002
|
+
const message = {
|
|
2003
|
+
address,
|
|
2004
|
+
args: oscArgs
|
|
2005
|
+
};
|
|
2006
|
+
const oscData = osc_default.writePacket(message);
|
|
2007
|
+
this.sendOSC(oscData);
|
|
2008
|
+
}
|
|
2009
|
+
_isBufferAllocationCommand(address) {
|
|
2010
|
+
return [
|
|
2011
|
+
"/b_allocRead",
|
|
2012
|
+
"/b_allocReadChannel",
|
|
2013
|
+
"/b_read",
|
|
2014
|
+
"/b_readChannel"
|
|
2015
|
+
// NOTE: /b_alloc and /b_free are NOT intercepted
|
|
2016
|
+
].includes(address);
|
|
2017
|
+
}
|
|
2018
|
+
async _handleBufferCommand(address, args) {
|
|
2019
|
+
switch (address) {
|
|
2020
|
+
case "/b_allocRead":
|
|
2021
|
+
return await this._allocReadBuffer(...args);
|
|
2022
|
+
case "/b_allocReadChannel":
|
|
2023
|
+
return await this._allocReadChannelBuffer(...args);
|
|
2024
|
+
case "/b_read":
|
|
2025
|
+
return await this._readBuffer(...args);
|
|
2026
|
+
case "/b_readChannel":
|
|
2027
|
+
return await this._readChannelBuffer(...args);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
/**
|
|
2031
|
+
* /b_allocRead bufnum path [startFrame numFrames completion]
|
|
2032
|
+
*/
|
|
2033
|
+
async _allocReadBuffer(bufnum, path, startFrame = 0, numFrames = 0, completionMsg = null) {
|
|
2034
|
+
let allocatedPtr = null;
|
|
2035
|
+
const GUARD_BEFORE = 3;
|
|
2036
|
+
const GUARD_AFTER = 1;
|
|
2037
|
+
try {
|
|
2038
|
+
const url = this._resolveAudioPath(path);
|
|
2039
|
+
const response = await fetch(url);
|
|
2040
|
+
if (!response.ok) {
|
|
2041
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
2042
|
+
}
|
|
2043
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
2044
|
+
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
|
|
2045
|
+
const actualStartFrame = startFrame || 0;
|
|
2046
|
+
const actualNumFrames = numFrames || audioBuffer.length - actualStartFrame;
|
|
2047
|
+
const framesToRead = Math.min(actualNumFrames, audioBuffer.length - actualStartFrame);
|
|
2048
|
+
if (framesToRead <= 0) {
|
|
2049
|
+
throw new Error(`Invalid frame range: start=${actualStartFrame}, numFrames=${actualNumFrames}, fileLength=${audioBuffer.length}`);
|
|
2050
|
+
}
|
|
2051
|
+
const numChannels = audioBuffer.numberOfChannels;
|
|
2052
|
+
const guardSamples = (GUARD_BEFORE + GUARD_AFTER) * numChannels;
|
|
2053
|
+
const interleavedData = new Float32Array(framesToRead * numChannels + guardSamples);
|
|
2054
|
+
const dataOffset = GUARD_BEFORE * numChannels;
|
|
2055
|
+
for (let frame = 0; frame < framesToRead; frame++) {
|
|
2056
|
+
for (let ch = 0; ch < numChannels; ch++) {
|
|
2057
|
+
const channelData = audioBuffer.getChannelData(ch);
|
|
2058
|
+
interleavedData[dataOffset + frame * numChannels + ch] = channelData[actualStartFrame + frame];
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
const bytesNeeded = interleavedData.length * 4;
|
|
2062
|
+
allocatedPtr = this.bufferPool.malloc(bytesNeeded);
|
|
2063
|
+
if (allocatedPtr === 0) {
|
|
2064
|
+
throw new Error("Buffer pool allocation failed (out of memory)");
|
|
2065
|
+
}
|
|
2066
|
+
const wasmHeap = new Float32Array(
|
|
2067
|
+
this.sharedBuffer,
|
|
2068
|
+
allocatedPtr,
|
|
2069
|
+
interleavedData.length
|
|
2070
|
+
);
|
|
2071
|
+
wasmHeap.set(interleavedData);
|
|
2072
|
+
this.allocatedBuffers.set(bufnum, {
|
|
2073
|
+
ptr: allocatedPtr,
|
|
2074
|
+
size: bytesNeeded
|
|
2075
|
+
});
|
|
2076
|
+
await this.send(
|
|
2077
|
+
"/b_allocPtr",
|
|
2078
|
+
bufnum,
|
|
2079
|
+
allocatedPtr,
|
|
2080
|
+
framesToRead,
|
|
2081
|
+
numChannels,
|
|
2082
|
+
audioBuffer.sampleRate
|
|
2083
|
+
);
|
|
2084
|
+
if (completionMsg) {
|
|
2085
|
+
}
|
|
2086
|
+
} catch (error) {
|
|
2087
|
+
if (allocatedPtr) {
|
|
2088
|
+
this.bufferPool.free(allocatedPtr);
|
|
2089
|
+
this.allocatedBuffers.delete(bufnum);
|
|
2090
|
+
}
|
|
2091
|
+
console.error(`[SuperSonic] Buffer ${bufnum} load failed:`, error);
|
|
2092
|
+
throw error;
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
/**
|
|
2096
|
+
* Resolve audio file path to full URL
|
|
2097
|
+
*/
|
|
2098
|
+
_resolveAudioPath(scPath) {
|
|
2099
|
+
if (this.audioPathMap[scPath]) {
|
|
2100
|
+
return this.audioPathMap[scPath];
|
|
2101
|
+
}
|
|
2102
|
+
if (!this.audioBaseURL) {
|
|
2103
|
+
throw new Error(
|
|
2104
|
+
'audioBaseURL not configured. Please set it in SuperSonic constructor options.\nExample: new SuperSonic({ audioBaseURL: "https://unpkg.com/supersonic-scsynth-samples-bd@0.1.0/samples/" })\nOr install sample packages: npm install supersonic-scsynth-samples-bd'
|
|
2105
|
+
);
|
|
2106
|
+
}
|
|
2107
|
+
return this.audioBaseURL + scPath;
|
|
2108
|
+
}
|
|
2109
|
+
/**
|
|
2110
|
+
* Handle /buffer/freed message from WASM
|
|
2111
|
+
*/
|
|
2112
|
+
_handleBufferFreed(args) {
|
|
2113
|
+
if (args.length < 2) {
|
|
2114
|
+
console.warn("[SuperSonic] Invalid /buffer/freed message:", args);
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2117
|
+
const bufnum = args[0];
|
|
2118
|
+
const offset = args[1];
|
|
2119
|
+
const bufferInfo = this.allocatedBuffers.get(bufnum);
|
|
2120
|
+
if (bufferInfo) {
|
|
2121
|
+
this.bufferPool.free(bufferInfo.ptr);
|
|
2122
|
+
this.allocatedBuffers.delete(bufnum);
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
/**
|
|
2126
|
+
* /b_allocReadChannel bufnum path [startFrame numFrames channel1 channel2 ... completion]
|
|
2127
|
+
* Load specific channels from an audio file
|
|
2128
|
+
*/
|
|
2129
|
+
async _allocReadChannelBuffer(bufnum, path, startFrame = 0, numFrames = 0, ...channelsAndCompletion) {
|
|
2130
|
+
let allocatedPtr = null;
|
|
2131
|
+
const GUARD_BEFORE = 3;
|
|
2132
|
+
const GUARD_AFTER = 1;
|
|
2133
|
+
try {
|
|
2134
|
+
const channels = [];
|
|
2135
|
+
let completionMsg = null;
|
|
2136
|
+
for (let i = 0; i < channelsAndCompletion.length; i++) {
|
|
2137
|
+
if (typeof channelsAndCompletion[i] === "number" && Number.isInteger(channelsAndCompletion[i])) {
|
|
2138
|
+
channels.push(channelsAndCompletion[i]);
|
|
2139
|
+
} else {
|
|
2140
|
+
completionMsg = channelsAndCompletion[i];
|
|
2141
|
+
break;
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
const url = this._resolveAudioPath(path);
|
|
2145
|
+
const response = await fetch(url);
|
|
2146
|
+
if (!response.ok) {
|
|
2147
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
2148
|
+
}
|
|
2149
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
2150
|
+
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
|
|
2151
|
+
const actualStartFrame = startFrame || 0;
|
|
2152
|
+
const actualNumFrames = numFrames || audioBuffer.length - actualStartFrame;
|
|
2153
|
+
const framesToRead = Math.min(actualNumFrames, audioBuffer.length - actualStartFrame);
|
|
2154
|
+
if (framesToRead <= 0) {
|
|
2155
|
+
throw new Error(`Invalid frame range: start=${actualStartFrame}, numFrames=${actualNumFrames}, fileLength=${audioBuffer.length}`);
|
|
2156
|
+
}
|
|
2157
|
+
const fileChannels = audioBuffer.numberOfChannels;
|
|
2158
|
+
const selectedChannels = channels.length > 0 ? channels : Array.from({ length: fileChannels }, (_, i) => i);
|
|
2159
|
+
for (const ch of selectedChannels) {
|
|
2160
|
+
if (ch < 0 || ch >= fileChannels) {
|
|
2161
|
+
throw new Error(`Invalid channel ${ch} (file has ${fileChannels} channels)`);
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
const numChannels = selectedChannels.length;
|
|
2165
|
+
const guardSamples = (GUARD_BEFORE + GUARD_AFTER) * numChannels;
|
|
2166
|
+
const interleavedData = new Float32Array(framesToRead * numChannels + guardSamples);
|
|
2167
|
+
const dataOffset = GUARD_BEFORE * numChannels;
|
|
2168
|
+
for (let frame = 0; frame < framesToRead; frame++) {
|
|
2169
|
+
for (let ch = 0; ch < numChannels; ch++) {
|
|
2170
|
+
const fileChannel = selectedChannels[ch];
|
|
2171
|
+
const channelData = audioBuffer.getChannelData(fileChannel);
|
|
2172
|
+
interleavedData[dataOffset + frame * numChannels + ch] = channelData[actualStartFrame + frame];
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
const bytesNeeded = interleavedData.length * 4;
|
|
2176
|
+
allocatedPtr = this.bufferPool.malloc(bytesNeeded);
|
|
2177
|
+
if (allocatedPtr === 0) {
|
|
2178
|
+
throw new Error("Buffer pool allocation failed (out of memory)");
|
|
2179
|
+
}
|
|
2180
|
+
const wasmHeap = new Float32Array(this.sharedBuffer, allocatedPtr, interleavedData.length);
|
|
2181
|
+
wasmHeap.set(interleavedData);
|
|
2182
|
+
this.allocatedBuffers.set(bufnum, { ptr: allocatedPtr, size: bytesNeeded });
|
|
2183
|
+
await this.send("/b_allocPtr", bufnum, allocatedPtr, framesToRead, numChannels, audioBuffer.sampleRate);
|
|
2184
|
+
if (completionMsg) {
|
|
2185
|
+
}
|
|
2186
|
+
} catch (error) {
|
|
2187
|
+
if (allocatedPtr) {
|
|
2188
|
+
this.bufferPool.free(allocatedPtr);
|
|
2189
|
+
this.allocatedBuffers.delete(bufnum);
|
|
2190
|
+
}
|
|
2191
|
+
console.error(`[SuperSonic] Buffer ${bufnum} load failed:`, error);
|
|
2192
|
+
throw error;
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
/**
|
|
2196
|
+
* /b_read bufnum path [startFrame numFrames bufStartFrame leaveOpen completion]
|
|
2197
|
+
* Read file into existing buffer
|
|
2198
|
+
*/
|
|
2199
|
+
async _readBuffer(bufnum, path, startFrame = 0, numFrames = 0, bufStartFrame = 0, leaveOpen = 0, completionMsg = null) {
|
|
2200
|
+
console.warn("[SuperSonic] /b_read requires pre-allocated buffer - not yet implemented");
|
|
2201
|
+
throw new Error("/b_read not yet implemented (requires /b_alloc first)");
|
|
2202
|
+
}
|
|
2203
|
+
/**
|
|
2204
|
+
* /b_readChannel bufnum path [startFrame numFrames bufStartFrame leaveOpen channel1 channel2 ... completion]
|
|
2205
|
+
* Read specific channels into existing buffer
|
|
2206
|
+
*/
|
|
2207
|
+
async _readChannelBuffer(bufnum, path, startFrame = 0, numFrames = 0, bufStartFrame = 0, leaveOpen = 0, ...channelsAndCompletion) {
|
|
2208
|
+
console.warn("[SuperSonic] /b_readChannel requires pre-allocated buffer - not yet implemented");
|
|
2209
|
+
throw new Error("/b_readChannel not yet implemented (requires /b_alloc first)");
|
|
2210
|
+
}
|
|
2211
|
+
/**
|
|
2212
|
+
* Send pre-encoded OSC bytes to scsynth
|
|
2213
|
+
* @param {ArrayBuffer|Uint8Array} oscData - Pre-encoded OSC data
|
|
2214
|
+
* @param {Object} options - Send options
|
|
2215
|
+
*/
|
|
2216
|
+
sendOSC(oscData, options = {}) {
|
|
2217
|
+
if (!this.initialized) {
|
|
2218
|
+
throw new Error("Not initialized. Call init() first.");
|
|
2219
|
+
}
|
|
2220
|
+
let uint8Data;
|
|
2221
|
+
if (oscData instanceof ArrayBuffer) {
|
|
2222
|
+
uint8Data = new Uint8Array(oscData);
|
|
2223
|
+
} else if (oscData instanceof Uint8Array) {
|
|
2224
|
+
uint8Data = oscData;
|
|
2225
|
+
} else {
|
|
2226
|
+
throw new Error("oscData must be ArrayBuffer or Uint8Array");
|
|
2227
|
+
}
|
|
2228
|
+
this.stats.messagesSent++;
|
|
2229
|
+
if (this.onMessageSent) {
|
|
2230
|
+
this.onMessageSent(uint8Data);
|
|
2231
|
+
}
|
|
2232
|
+
let waitTimeMs = null;
|
|
2233
|
+
if (uint8Data.length >= 16) {
|
|
2234
|
+
const header = String.fromCharCode.apply(null, uint8Data.slice(0, 8));
|
|
2235
|
+
if (header === "#bundle\0") {
|
|
2236
|
+
if (this.wasmTimeOffset === null) {
|
|
2237
|
+
console.warn("[SuperSonic] Time offset not yet calculated, calculating now");
|
|
2238
|
+
this.#calculateTimeOffset();
|
|
2239
|
+
}
|
|
2240
|
+
const view = new DataView(uint8Data.buffer, uint8Data.byteOffset);
|
|
2241
|
+
const ntpSeconds = view.getUint32(8, false);
|
|
2242
|
+
const ntpFraction = view.getUint32(12, false);
|
|
2243
|
+
if (!(ntpSeconds === 0 && (ntpFraction === 0 || ntpFraction === 1))) {
|
|
2244
|
+
const ntpTimeS = ntpSeconds + ntpFraction / 4294967296;
|
|
2245
|
+
const audioTimeS = ntpTimeS - this.wasmTimeOffset;
|
|
2246
|
+
const currentAudioTimeS = this.audioContext.currentTime;
|
|
2247
|
+
const latencyS = 0.05;
|
|
2248
|
+
waitTimeMs = (audioTimeS - currentAudioTimeS - latencyS) * 1e3;
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
this.osc.send(uint8Data, { ...options, waitTimeMs });
|
|
2253
|
+
}
|
|
2254
|
+
/**
|
|
2255
|
+
* Get current status
|
|
2256
|
+
*/
|
|
2257
|
+
getStatus() {
|
|
2258
|
+
return {
|
|
2259
|
+
initialized: this.initialized,
|
|
2260
|
+
capabilities: this.capabilities,
|
|
2261
|
+
stats: this.stats,
|
|
2262
|
+
audioContextState: this.audioContext?.state
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
/**
|
|
2266
|
+
* Destroy the orchestrator and clean up resources
|
|
2267
|
+
*/
|
|
2268
|
+
async destroy() {
|
|
2269
|
+
console.log("[SuperSonic] Destroying...");
|
|
2270
|
+
if (this.osc) {
|
|
2271
|
+
this.osc.terminate();
|
|
2272
|
+
this.osc = null;
|
|
2273
|
+
}
|
|
2274
|
+
if (this.workletNode) {
|
|
2275
|
+
this.workletNode.disconnect();
|
|
2276
|
+
this.workletNode = null;
|
|
2277
|
+
}
|
|
2278
|
+
if (this.audioContext) {
|
|
2279
|
+
await this.audioContext.close();
|
|
2280
|
+
this.audioContext = null;
|
|
2281
|
+
}
|
|
2282
|
+
this.sharedBuffer = null;
|
|
2283
|
+
this.initialized = false;
|
|
2284
|
+
console.log("[SuperSonic] Destroyed");
|
|
2285
|
+
}
|
|
2286
|
+
/**
|
|
2287
|
+
* Load a binary synthdef file and send it to scsynth
|
|
2288
|
+
* @param {string} path - Path or URL to the .scsyndef file
|
|
2289
|
+
* @returns {Promise<void>}
|
|
2290
|
+
* @example
|
|
2291
|
+
* await sonic.loadSynthDef('./extra/synthdefs/sonic-pi-beep.scsyndef');
|
|
2292
|
+
*/
|
|
2293
|
+
async loadSynthDef(path) {
|
|
2294
|
+
if (!this.initialized) {
|
|
2295
|
+
throw new Error("SuperSonic not initialized. Call init() first.");
|
|
2296
|
+
}
|
|
2297
|
+
try {
|
|
2298
|
+
const response = await fetch(path);
|
|
2299
|
+
if (!response.ok) {
|
|
2300
|
+
throw new Error(`Failed to load synthdef from ${path}: ${response.status} ${response.statusText}`);
|
|
2301
|
+
}
|
|
2302
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
2303
|
+
const synthdefData = new Uint8Array(arrayBuffer);
|
|
2304
|
+
this.send("/d_recv", synthdefData);
|
|
2305
|
+
console.log(`[SuperSonic] Loaded synthdef from ${path} (${synthdefData.length} bytes)`);
|
|
2306
|
+
} catch (error) {
|
|
2307
|
+
console.error("[SuperSonic] Failed to load synthdef:", error);
|
|
2308
|
+
throw error;
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
/**
|
|
2312
|
+
* Load multiple synthdefs from a directory
|
|
2313
|
+
* @param {string[]} names - Array of synthdef names (without .scsyndef extension)
|
|
2314
|
+
* @param {string} baseUrl - Base URL for synthdef files (required)
|
|
2315
|
+
* @returns {Promise<Object>} Map of name -> success/error
|
|
2316
|
+
* @example
|
|
2317
|
+
* const results = await sonic.loadSynthDefs(
|
|
2318
|
+
* ['sonic-pi-beep', 'sonic-pi-tb303'],
|
|
2319
|
+
* 'https://unpkg.com/supersonic-scsynth-synthdefs@0.1.0/synthdefs/'
|
|
2320
|
+
* );
|
|
2321
|
+
*/
|
|
2322
|
+
async loadSynthDefs(names, baseUrl) {
|
|
2323
|
+
if (!this.initialized) {
|
|
2324
|
+
throw new Error("SuperSonic not initialized. Call init() first.");
|
|
2325
|
+
}
|
|
2326
|
+
if (!baseUrl) {
|
|
2327
|
+
throw new Error(
|
|
2328
|
+
'baseUrl is required. Please specify the URL to your synthdefs.\nExample: await sonic.loadSynthDefs(["sonic-pi-beep"], "https://unpkg.com/supersonic-scsynth-synthdefs@0.1.0/synthdefs/")\nOr install: npm install supersonic-scsynth-synthdefs'
|
|
2329
|
+
);
|
|
2330
|
+
}
|
|
2331
|
+
const results = {};
|
|
2332
|
+
await Promise.all(
|
|
2333
|
+
names.map(async (name) => {
|
|
2334
|
+
try {
|
|
2335
|
+
const path = `${baseUrl}${name}.scsyndef`;
|
|
2336
|
+
await this.loadSynthDef(path);
|
|
2337
|
+
results[name] = { success: true };
|
|
2338
|
+
} catch (error) {
|
|
2339
|
+
console.error(`[SuperSonic] Failed to load ${name}:`, error);
|
|
2340
|
+
results[name] = { success: false, error: error.message };
|
|
2341
|
+
}
|
|
2342
|
+
})
|
|
2343
|
+
);
|
|
2344
|
+
const successCount = Object.values(results).filter((r) => r.success).length;
|
|
2345
|
+
console.log(`[SuperSonic] Loaded ${successCount}/${names.length} synthdefs`);
|
|
2346
|
+
return results;
|
|
2347
|
+
}
|
|
2348
|
+
/**
|
|
2349
|
+
* Allocate memory for an audio buffer (includes guard samples)
|
|
2350
|
+
* @param {number} numSamples - Number of Float32 samples to allocate
|
|
2351
|
+
* @returns {number} Byte offset into SharedArrayBuffer, or 0 if allocation failed
|
|
2352
|
+
* @example
|
|
2353
|
+
* const bufferAddr = sonic.allocBuffer(44100); // Allocate 1 second at 44.1kHz
|
|
2354
|
+
*/
|
|
2355
|
+
allocBuffer(numSamples) {
|
|
2356
|
+
if (!this.initialized) {
|
|
2357
|
+
throw new Error("SuperSonic not initialized. Call init() first.");
|
|
2358
|
+
}
|
|
2359
|
+
const sizeBytes = numSamples * 4;
|
|
2360
|
+
const addr = this.bufferPool.malloc(sizeBytes);
|
|
2361
|
+
if (addr === 0) {
|
|
2362
|
+
console.error(`[SuperSonic] Buffer allocation failed: ${numSamples} samples (${sizeBytes} bytes)`);
|
|
2363
|
+
}
|
|
2364
|
+
return addr;
|
|
2365
|
+
}
|
|
2366
|
+
/**
|
|
2367
|
+
* Free a previously allocated buffer
|
|
2368
|
+
* @param {number} addr - Buffer address returned by allocBuffer()
|
|
2369
|
+
* @returns {boolean} true if freed successfully
|
|
2370
|
+
* @example
|
|
2371
|
+
* sonic.freeBuffer(bufferAddr);
|
|
2372
|
+
*/
|
|
2373
|
+
freeBuffer(addr) {
|
|
2374
|
+
if (!this.initialized) {
|
|
2375
|
+
throw new Error("SuperSonic not initialized. Call init() first.");
|
|
2376
|
+
}
|
|
2377
|
+
return this.bufferPool.free(addr);
|
|
2378
|
+
}
|
|
2379
|
+
/**
|
|
2380
|
+
* Get a Float32Array view of an allocated buffer
|
|
2381
|
+
* @param {number} addr - Buffer address returned by allocBuffer()
|
|
2382
|
+
* @param {number} numSamples - Number of Float32 samples
|
|
2383
|
+
* @returns {Float32Array} Typed array view into the buffer
|
|
2384
|
+
* @example
|
|
2385
|
+
* const view = sonic.getBufferView(bufferAddr, 44100);
|
|
2386
|
+
* view[0] = 1.0; // Write to buffer
|
|
2387
|
+
*/
|
|
2388
|
+
getBufferView(addr, numSamples) {
|
|
2389
|
+
if (!this.initialized) {
|
|
2390
|
+
throw new Error("SuperSonic not initialized. Call init() first.");
|
|
2391
|
+
}
|
|
2392
|
+
return new Float32Array(this.sharedBuffer, addr, numSamples);
|
|
2393
|
+
}
|
|
2394
|
+
/**
|
|
2395
|
+
* Get buffer pool statistics
|
|
2396
|
+
* @returns {Object} Stats including total, available, used, etc.
|
|
2397
|
+
* @example
|
|
2398
|
+
* const stats = sonic.getBufferPoolStats();
|
|
2399
|
+
* console.log(`Available: ${stats.available} bytes`);
|
|
2400
|
+
*/
|
|
2401
|
+
getBufferPoolStats() {
|
|
2402
|
+
if (!this.initialized) {
|
|
2403
|
+
throw new Error("SuperSonic not initialized. Call init() first.");
|
|
2404
|
+
}
|
|
2405
|
+
return this.bufferPool.stats();
|
|
2406
|
+
}
|
|
2407
|
+
};
|
|
2408
|
+
export {
|
|
2409
|
+
SuperSonic
|
|
2410
|
+
};
|
|
2411
|
+
/*! osc.js 2.4.5, Copyright 2024 Colin Clark | github.com/colinbdclark/osc.js */
|