supersonic-scsynth 0.6.0 → 0.6.3
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/dist/supersonic.js +20 -3440
- package/dist/wasm/manifest.json +3 -3
- package/dist/wasm/scsynth-nrt.wasm +0 -0
- package/dist/workers/debug_worker.js +2 -281
- package/dist/workers/osc_in_worker.js +1 -279
- package/dist/workers/osc_out_prescheduler_worker.js +1 -705
- package/dist/workers/scsynth_audio_worklet.js +2 -543
- package/package.json +1 -1
package/dist/supersonic.js
CHANGED
|
@@ -1,3441 +1,21 @@
|
|
|
1
|
-
// js/vendor/osc.js/osc.js
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
(function() {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
};
|
|
22
|
-
osc.isBuffer = function(obj) {
|
|
23
|
-
return osc.isBufferEnv && obj instanceof Buffer;
|
|
24
|
-
};
|
|
25
|
-
osc.Long = typeof Long !== "undefined" ? Long : void 0;
|
|
26
|
-
osc.TextDecoder = typeof TextDecoder !== "undefined" ? new TextDecoder("utf-8") : typeof util !== "undefined" && typeof (util.TextDecoder !== "undefined") ? new util.TextDecoder("utf-8") : void 0;
|
|
27
|
-
osc.TextEncoder = typeof TextEncoder !== "undefined" ? new TextEncoder("utf-8") : typeof util !== "undefined" && typeof (util.TextEncoder !== "undefined") ? new util.TextEncoder("utf-8") : void 0;
|
|
28
|
-
osc.dataView = function(obj, offset, length) {
|
|
29
|
-
if (obj.buffer) {
|
|
30
|
-
return new DataView(obj.buffer, offset, length);
|
|
31
|
-
}
|
|
32
|
-
if (obj instanceof ArrayBuffer) {
|
|
33
|
-
return new DataView(obj, offset, length);
|
|
34
|
-
}
|
|
35
|
-
return new DataView(new Uint8Array(obj), offset, length);
|
|
36
|
-
};
|
|
37
|
-
osc.byteArray = function(obj) {
|
|
38
|
-
if (obj instanceof Uint8Array) {
|
|
39
|
-
return obj;
|
|
40
|
-
}
|
|
41
|
-
var buf = obj.buffer ? obj.buffer : obj;
|
|
42
|
-
if (!(buf instanceof ArrayBuffer) && (typeof buf.length === "undefined" || typeof buf === "string")) {
|
|
43
|
-
throw new Error("Can't wrap a non-array-like object as Uint8Array. Object was: " + JSON.stringify(obj, null, 2));
|
|
44
|
-
}
|
|
45
|
-
return new Uint8Array(buf);
|
|
46
|
-
};
|
|
47
|
-
osc.nativeBuffer = function(obj) {
|
|
48
|
-
if (osc.isBufferEnv) {
|
|
49
|
-
return osc.isBuffer(obj) ? obj : Buffer.from(obj.buffer ? obj : new Uint8Array(obj));
|
|
50
|
-
}
|
|
51
|
-
return osc.isTypedArrayView(obj) ? obj : new Uint8Array(obj);
|
|
52
|
-
};
|
|
53
|
-
osc.copyByteArray = function(source, target, offset) {
|
|
54
|
-
if (osc.isTypedArrayView(source) && osc.isTypedArrayView(target)) {
|
|
55
|
-
target.set(source, offset);
|
|
56
|
-
} else {
|
|
57
|
-
var start = offset === void 0 ? 0 : offset, len = Math.min(target.length - offset, source.length);
|
|
58
|
-
for (var i = 0, j = start; i < len; i++, j++) {
|
|
59
|
-
target[j] = source[i];
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return target;
|
|
63
|
-
};
|
|
64
|
-
osc.readString = function(dv, offsetState) {
|
|
65
|
-
var charCodes = [], idx = offsetState.idx;
|
|
66
|
-
for (; idx < dv.byteLength; idx++) {
|
|
67
|
-
var charCode = dv.getUint8(idx);
|
|
68
|
-
if (charCode !== 0) {
|
|
69
|
-
charCodes.push(charCode);
|
|
70
|
-
} else {
|
|
71
|
-
idx++;
|
|
72
|
-
break;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
idx = idx + 3 & ~3;
|
|
76
|
-
offsetState.idx = idx;
|
|
77
|
-
var decoder = osc.isBufferEnv ? osc.readString.withBuffer : osc.TextDecoder ? osc.readString.withTextDecoder : osc.readString.raw;
|
|
78
|
-
return decoder(charCodes);
|
|
79
|
-
};
|
|
80
|
-
osc.readString.raw = function(charCodes) {
|
|
81
|
-
var str = "";
|
|
82
|
-
var sliceSize = 1e4;
|
|
83
|
-
for (var i = 0; i < charCodes.length; i += sliceSize) {
|
|
84
|
-
str += String.fromCharCode.apply(null, charCodes.slice(i, i + sliceSize));
|
|
85
|
-
}
|
|
86
|
-
return str;
|
|
87
|
-
};
|
|
88
|
-
osc.readString.withTextDecoder = function(charCodes) {
|
|
89
|
-
var data = new Int8Array(charCodes);
|
|
90
|
-
return osc.TextDecoder.decode(data);
|
|
91
|
-
};
|
|
92
|
-
osc.readString.withBuffer = function(charCodes) {
|
|
93
|
-
return Buffer.from(charCodes).toString("utf-8");
|
|
94
|
-
};
|
|
95
|
-
osc.writeString = function(str) {
|
|
96
|
-
var encoder = osc.isBufferEnv ? osc.writeString.withBuffer : osc.TextEncoder ? osc.writeString.withTextEncoder : null, terminated = str + "\0", encodedStr;
|
|
97
|
-
if (encoder) {
|
|
98
|
-
encodedStr = encoder(terminated);
|
|
99
|
-
}
|
|
100
|
-
var len = encoder ? encodedStr.length : terminated.length, paddedLen = len + 3 & ~3, arr = new Uint8Array(paddedLen);
|
|
101
|
-
for (var i = 0; i < len - 1; i++) {
|
|
102
|
-
var charCode = encoder ? encodedStr[i] : terminated.charCodeAt(i);
|
|
103
|
-
arr[i] = charCode;
|
|
104
|
-
}
|
|
105
|
-
return arr;
|
|
106
|
-
};
|
|
107
|
-
osc.writeString.withTextEncoder = function(str) {
|
|
108
|
-
return osc.TextEncoder.encode(str);
|
|
109
|
-
};
|
|
110
|
-
osc.writeString.withBuffer = function(str) {
|
|
111
|
-
return Buffer.from(str);
|
|
112
|
-
};
|
|
113
|
-
osc.readPrimitive = function(dv, readerName, numBytes, offsetState) {
|
|
114
|
-
var val = dv[readerName](offsetState.idx, false);
|
|
115
|
-
offsetState.idx += numBytes;
|
|
116
|
-
return val;
|
|
117
|
-
};
|
|
118
|
-
osc.writePrimitive = function(val, dv, writerName, numBytes, offset) {
|
|
119
|
-
offset = offset === void 0 ? 0 : offset;
|
|
120
|
-
var arr;
|
|
121
|
-
if (!dv) {
|
|
122
|
-
arr = new Uint8Array(numBytes);
|
|
123
|
-
dv = new DataView(arr.buffer);
|
|
124
|
-
} else {
|
|
125
|
-
arr = new Uint8Array(dv.buffer);
|
|
126
|
-
}
|
|
127
|
-
dv[writerName](offset, val, false);
|
|
128
|
-
return arr;
|
|
129
|
-
};
|
|
130
|
-
osc.readInt32 = function(dv, offsetState) {
|
|
131
|
-
return osc.readPrimitive(dv, "getInt32", 4, offsetState);
|
|
132
|
-
};
|
|
133
|
-
osc.writeInt32 = function(val, dv, offset) {
|
|
134
|
-
return osc.writePrimitive(val, dv, "setInt32", 4, offset);
|
|
135
|
-
};
|
|
136
|
-
osc.readInt64 = function(dv, offsetState) {
|
|
137
|
-
var high = osc.readPrimitive(dv, "getInt32", 4, offsetState), low = osc.readPrimitive(dv, "getInt32", 4, offsetState);
|
|
138
|
-
if (osc.Long) {
|
|
139
|
-
return new osc.Long(low, high);
|
|
140
|
-
} else {
|
|
141
|
-
return {
|
|
142
|
-
high,
|
|
143
|
-
low,
|
|
144
|
-
unsigned: false
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
};
|
|
148
|
-
osc.writeInt64 = function(val, dv, offset) {
|
|
149
|
-
var arr = new Uint8Array(8);
|
|
150
|
-
arr.set(osc.writePrimitive(val.high, dv, "setInt32", 4, offset), 0);
|
|
151
|
-
arr.set(osc.writePrimitive(val.low, dv, "setInt32", 4, offset + 4), 4);
|
|
152
|
-
return arr;
|
|
153
|
-
};
|
|
154
|
-
osc.readFloat32 = function(dv, offsetState) {
|
|
155
|
-
return osc.readPrimitive(dv, "getFloat32", 4, offsetState);
|
|
156
|
-
};
|
|
157
|
-
osc.writeFloat32 = function(val, dv, offset) {
|
|
158
|
-
return osc.writePrimitive(val, dv, "setFloat32", 4, offset);
|
|
159
|
-
};
|
|
160
|
-
osc.readFloat64 = function(dv, offsetState) {
|
|
161
|
-
return osc.readPrimitive(dv, "getFloat64", 8, offsetState);
|
|
162
|
-
};
|
|
163
|
-
osc.writeFloat64 = function(val, dv, offset) {
|
|
164
|
-
return osc.writePrimitive(val, dv, "setFloat64", 8, offset);
|
|
165
|
-
};
|
|
166
|
-
osc.readChar32 = function(dv, offsetState) {
|
|
167
|
-
var charCode = osc.readPrimitive(dv, "getUint32", 4, offsetState);
|
|
168
|
-
return String.fromCharCode(charCode);
|
|
169
|
-
};
|
|
170
|
-
osc.writeChar32 = function(str, dv, offset) {
|
|
171
|
-
var charCode = str.charCodeAt(0);
|
|
172
|
-
if (charCode === void 0 || charCode < -1) {
|
|
173
|
-
return void 0;
|
|
174
|
-
}
|
|
175
|
-
return osc.writePrimitive(charCode, dv, "setUint32", 4, offset);
|
|
176
|
-
};
|
|
177
|
-
osc.readBlob = function(dv, offsetState) {
|
|
178
|
-
var len = osc.readInt32(dv, offsetState), paddedLen = len + 3 & ~3, blob = new Uint8Array(dv.buffer, offsetState.idx, len);
|
|
179
|
-
offsetState.idx += paddedLen;
|
|
180
|
-
return blob;
|
|
181
|
-
};
|
|
182
|
-
osc.writeBlob = function(data) {
|
|
183
|
-
data = osc.byteArray(data);
|
|
184
|
-
var len = data.byteLength, paddedLen = len + 3 & ~3, offset = 4, blobLen = paddedLen + offset, arr = new Uint8Array(blobLen), dv = new DataView(arr.buffer);
|
|
185
|
-
osc.writeInt32(len, dv);
|
|
186
|
-
arr.set(data, offset);
|
|
187
|
-
return arr;
|
|
188
|
-
};
|
|
189
|
-
osc.readMIDIBytes = function(dv, offsetState) {
|
|
190
|
-
var midi = new Uint8Array(dv.buffer, offsetState.idx, 4);
|
|
191
|
-
offsetState.idx += 4;
|
|
192
|
-
return midi;
|
|
193
|
-
};
|
|
194
|
-
osc.writeMIDIBytes = function(bytes) {
|
|
195
|
-
bytes = osc.byteArray(bytes);
|
|
196
|
-
var arr = new Uint8Array(4);
|
|
197
|
-
arr.set(bytes);
|
|
198
|
-
return arr;
|
|
199
|
-
};
|
|
200
|
-
osc.readColor = function(dv, offsetState) {
|
|
201
|
-
var bytes = new Uint8Array(dv.buffer, offsetState.idx, 4), alpha = bytes[3] / 255;
|
|
202
|
-
offsetState.idx += 4;
|
|
203
|
-
return {
|
|
204
|
-
r: bytes[0],
|
|
205
|
-
g: bytes[1],
|
|
206
|
-
b: bytes[2],
|
|
207
|
-
a: alpha
|
|
208
|
-
};
|
|
209
|
-
};
|
|
210
|
-
osc.writeColor = function(color) {
|
|
211
|
-
var alpha = Math.round(color.a * 255), arr = new Uint8Array([color.r, color.g, color.b, alpha]);
|
|
212
|
-
return arr;
|
|
213
|
-
};
|
|
214
|
-
osc.readTrue = function() {
|
|
215
|
-
return true;
|
|
216
|
-
};
|
|
217
|
-
osc.readFalse = function() {
|
|
218
|
-
return false;
|
|
219
|
-
};
|
|
220
|
-
osc.readNull = function() {
|
|
221
|
-
return null;
|
|
222
|
-
};
|
|
223
|
-
osc.readImpulse = function() {
|
|
224
|
-
return 1;
|
|
225
|
-
};
|
|
226
|
-
osc.readTimeTag = function(dv, offsetState) {
|
|
227
|
-
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);
|
|
228
|
-
return {
|
|
229
|
-
raw: [secs1900, frac],
|
|
230
|
-
native
|
|
231
|
-
};
|
|
232
|
-
};
|
|
233
|
-
osc.writeTimeTag = function(timeTag) {
|
|
234
|
-
var raw = timeTag.raw ? timeTag.raw : osc.jsToNTPTime(timeTag.native), arr = new Uint8Array(8), dv = new DataView(arr.buffer);
|
|
235
|
-
osc.writeInt32(raw[0], dv, 0);
|
|
236
|
-
osc.writeInt32(raw[1], dv, 4);
|
|
237
|
-
return arr;
|
|
238
|
-
};
|
|
239
|
-
osc.timeTag = function(secs, now) {
|
|
240
|
-
secs = secs || 0;
|
|
241
|
-
now = now || Date.now();
|
|
242
|
-
var nowSecs = now / 1e3, nowWhole = Math.floor(nowSecs), nowFracs = nowSecs - nowWhole, secsWhole = Math.floor(secs), secsFracs = secs - secsWhole, fracs = nowFracs + secsFracs;
|
|
243
|
-
if (fracs > 1) {
|
|
244
|
-
var fracsWhole = Math.floor(fracs), fracsFracs = fracs - fracsWhole;
|
|
245
|
-
secsWhole += fracsWhole;
|
|
246
|
-
fracs = fracsFracs;
|
|
247
|
-
}
|
|
248
|
-
var ntpSecs = nowWhole + secsWhole + osc.SECS_70YRS, ntpFracs = Math.round(osc.TWO_32 * fracs);
|
|
249
|
-
return {
|
|
250
|
-
raw: [ntpSecs, ntpFracs]
|
|
251
|
-
};
|
|
252
|
-
};
|
|
253
|
-
osc.ntpToJSTime = function(secs1900, frac) {
|
|
254
|
-
var secs1970 = secs1900 - osc.SECS_70YRS, decimals = frac / osc.TWO_32, msTime = (secs1970 + decimals) * 1e3;
|
|
255
|
-
return msTime;
|
|
256
|
-
};
|
|
257
|
-
osc.jsToNTPTime = function(jsTime) {
|
|
258
|
-
var secs = jsTime / 1e3, secsWhole = Math.floor(secs), secsFrac = secs - secsWhole, ntpSecs = secsWhole + osc.SECS_70YRS, ntpFracs = Math.round(osc.TWO_32 * secsFrac);
|
|
259
|
-
return [ntpSecs, ntpFracs];
|
|
260
|
-
};
|
|
261
|
-
osc.readArguments = function(dv, options, offsetState) {
|
|
262
|
-
var typeTagString = osc.readString(dv, offsetState);
|
|
263
|
-
if (typeTagString.indexOf(",") !== 0) {
|
|
264
|
-
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);
|
|
265
|
-
}
|
|
266
|
-
var argTypes = typeTagString.substring(1).split(""), args = [];
|
|
267
|
-
osc.readArgumentsIntoArray(args, argTypes, typeTagString, dv, options, offsetState);
|
|
268
|
-
return args;
|
|
269
|
-
};
|
|
270
|
-
osc.readArgument = function(argType, typeTagString, dv, options, offsetState) {
|
|
271
|
-
var typeSpec = osc.argumentTypes[argType];
|
|
272
|
-
if (!typeSpec) {
|
|
273
|
-
throw new Error("'" + argType + "' is not a valid OSC type tag. Type tag string was: " + typeTagString);
|
|
274
|
-
}
|
|
275
|
-
var argReader = typeSpec.reader, arg = osc[argReader](dv, offsetState);
|
|
276
|
-
if (options.metadata) {
|
|
277
|
-
arg = {
|
|
278
|
-
type: argType,
|
|
279
|
-
value: arg
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
return arg;
|
|
283
|
-
};
|
|
284
|
-
osc.readArgumentsIntoArray = function(arr, argTypes, typeTagString, dv, options, offsetState) {
|
|
285
|
-
var i = 0;
|
|
286
|
-
while (i < argTypes.length) {
|
|
287
|
-
var argType = argTypes[i], arg;
|
|
288
|
-
if (argType === "[") {
|
|
289
|
-
var fromArrayOpen = argTypes.slice(i + 1), endArrayIdx = fromArrayOpen.indexOf("]");
|
|
290
|
-
if (endArrayIdx < 0) {
|
|
291
|
-
throw new Error("Invalid argument type tag: an open array type tag ('[') was found without a matching close array tag ('[]'). Type tag was: " + typeTagString);
|
|
292
|
-
}
|
|
293
|
-
var typesInArray = fromArrayOpen.slice(0, endArrayIdx);
|
|
294
|
-
arg = osc.readArgumentsIntoArray([], typesInArray, typeTagString, dv, options, offsetState);
|
|
295
|
-
i += endArrayIdx + 2;
|
|
296
|
-
} else {
|
|
297
|
-
arg = osc.readArgument(argType, typeTagString, dv, options, offsetState);
|
|
298
|
-
i++;
|
|
299
|
-
}
|
|
300
|
-
arr.push(arg);
|
|
301
|
-
}
|
|
302
|
-
return arr;
|
|
303
|
-
};
|
|
304
|
-
osc.writeArguments = function(args, options) {
|
|
305
|
-
var argCollection = osc.collectArguments(args, options);
|
|
306
|
-
return osc.joinParts(argCollection);
|
|
307
|
-
};
|
|
308
|
-
osc.joinParts = function(dataCollection) {
|
|
309
|
-
var buf = new Uint8Array(dataCollection.byteLength), parts = dataCollection.parts, offset = 0;
|
|
310
|
-
for (var i = 0; i < parts.length; i++) {
|
|
311
|
-
var part = parts[i];
|
|
312
|
-
osc.copyByteArray(part, buf, offset);
|
|
313
|
-
offset += part.length;
|
|
314
|
-
}
|
|
315
|
-
return buf;
|
|
316
|
-
};
|
|
317
|
-
osc.addDataPart = function(dataPart, dataCollection) {
|
|
318
|
-
dataCollection.parts.push(dataPart);
|
|
319
|
-
dataCollection.byteLength += dataPart.length;
|
|
320
|
-
};
|
|
321
|
-
osc.writeArrayArguments = function(args, dataCollection) {
|
|
322
|
-
var typeTag = "[";
|
|
323
|
-
for (var i = 0; i < args.length; i++) {
|
|
324
|
-
var arg = args[i];
|
|
325
|
-
typeTag += osc.writeArgument(arg, dataCollection);
|
|
326
|
-
}
|
|
327
|
-
typeTag += "]";
|
|
328
|
-
return typeTag;
|
|
329
|
-
};
|
|
330
|
-
osc.writeArgument = function(arg, dataCollection) {
|
|
331
|
-
if (osc.isArray(arg)) {
|
|
332
|
-
return osc.writeArrayArguments(arg, dataCollection);
|
|
333
|
-
}
|
|
334
|
-
var type = arg.type, writer = osc.argumentTypes[type].writer;
|
|
335
|
-
if (writer) {
|
|
336
|
-
var data = osc[writer](arg.value);
|
|
337
|
-
osc.addDataPart(data, dataCollection);
|
|
338
|
-
}
|
|
339
|
-
return arg.type;
|
|
340
|
-
};
|
|
341
|
-
osc.collectArguments = function(args, options, dataCollection) {
|
|
342
|
-
if (!osc.isArray(args)) {
|
|
343
|
-
args = typeof args === "undefined" ? [] : [args];
|
|
344
|
-
}
|
|
345
|
-
dataCollection = dataCollection || {
|
|
346
|
-
byteLength: 0,
|
|
347
|
-
parts: []
|
|
348
|
-
};
|
|
349
|
-
if (!options.metadata) {
|
|
350
|
-
args = osc.annotateArguments(args);
|
|
351
|
-
}
|
|
352
|
-
var typeTagString = ",", currPartIdx = dataCollection.parts.length;
|
|
353
|
-
for (var i = 0; i < args.length; i++) {
|
|
354
|
-
var arg = args[i];
|
|
355
|
-
typeTagString += osc.writeArgument(arg, dataCollection);
|
|
356
|
-
}
|
|
357
|
-
var typeData = osc.writeString(typeTagString);
|
|
358
|
-
dataCollection.byteLength += typeData.byteLength;
|
|
359
|
-
dataCollection.parts.splice(currPartIdx, 0, typeData);
|
|
360
|
-
return dataCollection;
|
|
361
|
-
};
|
|
362
|
-
osc.readMessage = function(data, options, offsetState) {
|
|
363
|
-
options = options || osc.defaults;
|
|
364
|
-
var dv = osc.dataView(data, data.byteOffset, data.byteLength);
|
|
365
|
-
offsetState = offsetState || {
|
|
366
|
-
idx: 0
|
|
367
|
-
};
|
|
368
|
-
var address = osc.readString(dv, offsetState);
|
|
369
|
-
return osc.readMessageContents(address, dv, options, offsetState);
|
|
370
|
-
};
|
|
371
|
-
osc.readMessageContents = function(address, dv, options, offsetState) {
|
|
372
|
-
if (address.indexOf("/") !== 0) {
|
|
373
|
-
throw new Error("A malformed OSC address was found while reading an OSC message. String was: " + address);
|
|
374
|
-
}
|
|
375
|
-
var args = osc.readArguments(dv, options, offsetState);
|
|
376
|
-
return {
|
|
377
|
-
address,
|
|
378
|
-
args: args.length === 1 && options.unpackSingleArgs ? args[0] : args
|
|
379
|
-
};
|
|
380
|
-
};
|
|
381
|
-
osc.collectMessageParts = function(msg, options, dataCollection) {
|
|
382
|
-
dataCollection = dataCollection || {
|
|
383
|
-
byteLength: 0,
|
|
384
|
-
parts: []
|
|
385
|
-
};
|
|
386
|
-
osc.addDataPart(osc.writeString(msg.address), dataCollection);
|
|
387
|
-
return osc.collectArguments(msg.args, options, dataCollection);
|
|
388
|
-
};
|
|
389
|
-
osc.writeMessage = function(msg, options) {
|
|
390
|
-
options = options || osc.defaults;
|
|
391
|
-
if (!osc.isValidMessage(msg)) {
|
|
392
|
-
throw new Error("An OSC message must contain a valid address. Message was: " + JSON.stringify(msg, null, 2));
|
|
393
|
-
}
|
|
394
|
-
var msgCollection = osc.collectMessageParts(msg, options);
|
|
395
|
-
return osc.joinParts(msgCollection);
|
|
396
|
-
};
|
|
397
|
-
osc.isValidMessage = function(msg) {
|
|
398
|
-
return msg.address && msg.address.indexOf("/") === 0;
|
|
399
|
-
};
|
|
400
|
-
osc.readBundle = function(dv, options, offsetState) {
|
|
401
|
-
return osc.readPacket(dv, options, offsetState);
|
|
402
|
-
};
|
|
403
|
-
osc.collectBundlePackets = function(bundle, options, dataCollection) {
|
|
404
|
-
dataCollection = dataCollection || {
|
|
405
|
-
byteLength: 0,
|
|
406
|
-
parts: []
|
|
407
|
-
};
|
|
408
|
-
osc.addDataPart(osc.writeString("#bundle"), dataCollection);
|
|
409
|
-
osc.addDataPart(osc.writeTimeTag(bundle.timeTag), dataCollection);
|
|
410
|
-
for (var i = 0; i < bundle.packets.length; i++) {
|
|
411
|
-
var packet = bundle.packets[i], collector = packet.address ? osc.collectMessageParts : osc.collectBundlePackets, packetCollection = collector(packet, options);
|
|
412
|
-
dataCollection.byteLength += packetCollection.byteLength;
|
|
413
|
-
osc.addDataPart(osc.writeInt32(packetCollection.byteLength), dataCollection);
|
|
414
|
-
dataCollection.parts = dataCollection.parts.concat(packetCollection.parts);
|
|
415
|
-
}
|
|
416
|
-
return dataCollection;
|
|
417
|
-
};
|
|
418
|
-
osc.writeBundle = function(bundle, options) {
|
|
419
|
-
if (!osc.isValidBundle(bundle)) {
|
|
420
|
-
throw new Error("An OSC bundle must contain 'timeTag' and 'packets' properties. Bundle was: " + JSON.stringify(bundle, null, 2));
|
|
421
|
-
}
|
|
422
|
-
options = options || osc.defaults;
|
|
423
|
-
var bundleCollection = osc.collectBundlePackets(bundle, options);
|
|
424
|
-
return osc.joinParts(bundleCollection);
|
|
425
|
-
};
|
|
426
|
-
osc.isValidBundle = function(bundle) {
|
|
427
|
-
return bundle.timeTag !== void 0 && bundle.packets !== void 0;
|
|
428
|
-
};
|
|
429
|
-
osc.readBundleContents = function(dv, options, offsetState, len) {
|
|
430
|
-
var timeTag = osc.readTimeTag(dv, offsetState), packets = [];
|
|
431
|
-
while (offsetState.idx < len) {
|
|
432
|
-
var packetSize = osc.readInt32(dv, offsetState), packetLen = offsetState.idx + packetSize, packet = osc.readPacket(dv, options, offsetState, packetLen);
|
|
433
|
-
packets.push(packet);
|
|
434
|
-
}
|
|
435
|
-
return {
|
|
436
|
-
timeTag,
|
|
437
|
-
packets
|
|
438
|
-
};
|
|
439
|
-
};
|
|
440
|
-
osc.readPacket = function(data, options, offsetState, len) {
|
|
441
|
-
var dv = osc.dataView(data, data.byteOffset, data.byteLength);
|
|
442
|
-
len = len === void 0 ? dv.byteLength : len;
|
|
443
|
-
offsetState = offsetState || {
|
|
444
|
-
idx: 0
|
|
445
|
-
};
|
|
446
|
-
var header = osc.readString(dv, offsetState), firstChar = header[0];
|
|
447
|
-
if (firstChar === "#") {
|
|
448
|
-
return osc.readBundleContents(dv, options, offsetState, len);
|
|
449
|
-
} else if (firstChar === "/") {
|
|
450
|
-
return osc.readMessageContents(header, dv, options, offsetState);
|
|
451
|
-
}
|
|
452
|
-
throw new Error("The header of an OSC packet didn't contain an OSC address or a #bundle string. Header was: " + header);
|
|
453
|
-
};
|
|
454
|
-
osc.writePacket = function(packet, options) {
|
|
455
|
-
if (osc.isValidMessage(packet)) {
|
|
456
|
-
return osc.writeMessage(packet, options);
|
|
457
|
-
} else if (osc.isValidBundle(packet)) {
|
|
458
|
-
return osc.writeBundle(packet, options);
|
|
459
|
-
} else {
|
|
460
|
-
throw new Error("The specified packet was not recognized as a valid OSC message or bundle. Packet was: " + JSON.stringify(packet, null, 2));
|
|
461
|
-
}
|
|
462
|
-
};
|
|
463
|
-
osc.argumentTypes = {
|
|
464
|
-
i: {
|
|
465
|
-
reader: "readInt32",
|
|
466
|
-
writer: "writeInt32"
|
|
467
|
-
},
|
|
468
|
-
h: {
|
|
469
|
-
reader: "readInt64",
|
|
470
|
-
writer: "writeInt64"
|
|
471
|
-
},
|
|
472
|
-
f: {
|
|
473
|
-
reader: "readFloat32",
|
|
474
|
-
writer: "writeFloat32"
|
|
475
|
-
},
|
|
476
|
-
s: {
|
|
477
|
-
reader: "readString",
|
|
478
|
-
writer: "writeString"
|
|
479
|
-
},
|
|
480
|
-
S: {
|
|
481
|
-
reader: "readString",
|
|
482
|
-
writer: "writeString"
|
|
483
|
-
},
|
|
484
|
-
b: {
|
|
485
|
-
reader: "readBlob",
|
|
486
|
-
writer: "writeBlob"
|
|
487
|
-
},
|
|
488
|
-
t: {
|
|
489
|
-
reader: "readTimeTag",
|
|
490
|
-
writer: "writeTimeTag"
|
|
491
|
-
},
|
|
492
|
-
T: {
|
|
493
|
-
reader: "readTrue"
|
|
494
|
-
},
|
|
495
|
-
F: {
|
|
496
|
-
reader: "readFalse"
|
|
497
|
-
},
|
|
498
|
-
N: {
|
|
499
|
-
reader: "readNull"
|
|
500
|
-
},
|
|
501
|
-
I: {
|
|
502
|
-
reader: "readImpulse"
|
|
503
|
-
},
|
|
504
|
-
d: {
|
|
505
|
-
reader: "readFloat64",
|
|
506
|
-
writer: "writeFloat64"
|
|
507
|
-
},
|
|
508
|
-
c: {
|
|
509
|
-
reader: "readChar32",
|
|
510
|
-
writer: "writeChar32"
|
|
511
|
-
},
|
|
512
|
-
r: {
|
|
513
|
-
reader: "readColor",
|
|
514
|
-
writer: "writeColor"
|
|
515
|
-
},
|
|
516
|
-
m: {
|
|
517
|
-
reader: "readMIDIBytes",
|
|
518
|
-
writer: "writeMIDIBytes"
|
|
519
|
-
}
|
|
520
|
-
// [] are special cased within read/writeArguments()
|
|
521
|
-
};
|
|
522
|
-
osc.inferTypeForArgument = function(arg) {
|
|
523
|
-
var type = typeof arg;
|
|
524
|
-
switch (type) {
|
|
525
|
-
case "boolean":
|
|
526
|
-
return arg ? "T" : "F";
|
|
527
|
-
case "string":
|
|
528
|
-
return "s";
|
|
529
|
-
case "number":
|
|
530
|
-
return "f";
|
|
531
|
-
case "undefined":
|
|
532
|
-
return "N";
|
|
533
|
-
case "object":
|
|
534
|
-
if (arg === null) {
|
|
535
|
-
return "N";
|
|
536
|
-
} else if (arg instanceof Uint8Array || arg instanceof ArrayBuffer) {
|
|
537
|
-
return "b";
|
|
538
|
-
} else if (typeof arg.high === "number" && typeof arg.low === "number") {
|
|
539
|
-
return "h";
|
|
540
|
-
}
|
|
541
|
-
break;
|
|
542
|
-
}
|
|
543
|
-
throw new Error("Can't infer OSC argument type for value: " + JSON.stringify(arg, null, 2));
|
|
544
|
-
};
|
|
545
|
-
osc.annotateArguments = function(args) {
|
|
546
|
-
var annotated = [];
|
|
547
|
-
for (var i = 0; i < args.length; i++) {
|
|
548
|
-
var arg = args[i], msgArg;
|
|
549
|
-
if (typeof arg === "object" && arg.type && arg.value !== void 0) {
|
|
550
|
-
msgArg = arg;
|
|
551
|
-
} else if (osc.isArray(arg)) {
|
|
552
|
-
msgArg = osc.annotateArguments(arg);
|
|
553
|
-
} else {
|
|
554
|
-
var oscType = osc.inferTypeForArgument(arg);
|
|
555
|
-
msgArg = {
|
|
556
|
-
type: oscType,
|
|
557
|
-
value: arg
|
|
558
|
-
};
|
|
559
|
-
}
|
|
560
|
-
annotated.push(msgArg);
|
|
561
|
-
}
|
|
562
|
-
return annotated;
|
|
563
|
-
};
|
|
564
|
-
;
|
|
565
|
-
})();
|
|
566
|
-
var EventEmitter = function() {
|
|
567
|
-
};
|
|
568
|
-
EventEmitter.prototype.on = function() {
|
|
569
|
-
};
|
|
570
|
-
EventEmitter.prototype.emit = function() {
|
|
571
|
-
};
|
|
572
|
-
EventEmitter.prototype.removeListener = function() {
|
|
573
|
-
};
|
|
574
|
-
(function() {
|
|
575
|
-
"use strict";
|
|
576
|
-
osc.supportsSerial = false;
|
|
577
|
-
osc.firePacketEvents = function(port, packet, timeTag, packetInfo) {
|
|
578
|
-
if (packet.address) {
|
|
579
|
-
port.emit("message", packet, timeTag, packetInfo);
|
|
580
|
-
} else {
|
|
581
|
-
osc.fireBundleEvents(port, packet, timeTag, packetInfo);
|
|
582
|
-
}
|
|
583
|
-
};
|
|
584
|
-
osc.fireBundleEvents = function(port, bundle, timeTag, packetInfo) {
|
|
585
|
-
port.emit("bundle", bundle, timeTag, packetInfo);
|
|
586
|
-
for (var i = 0; i < bundle.packets.length; i++) {
|
|
587
|
-
var packet = bundle.packets[i];
|
|
588
|
-
osc.firePacketEvents(port, packet, bundle.timeTag, packetInfo);
|
|
589
|
-
}
|
|
590
|
-
};
|
|
591
|
-
osc.fireClosedPortSendError = function(port, msg) {
|
|
592
|
-
msg = msg || "Can't send packets on a closed osc.Port object. Please open (or reopen) this Port by calling open().";
|
|
593
|
-
port.emit("error", msg);
|
|
594
|
-
};
|
|
595
|
-
osc.Port = function(options) {
|
|
596
|
-
this.options = options || {};
|
|
597
|
-
this.on("data", this.decodeOSC.bind(this));
|
|
598
|
-
};
|
|
599
|
-
var p = osc.Port.prototype = Object.create(EventEmitter.prototype);
|
|
600
|
-
p.constructor = osc.Port;
|
|
601
|
-
p.send = function(oscPacket) {
|
|
602
|
-
var args = Array.prototype.slice.call(arguments), encoded = this.encodeOSC(oscPacket), buf = osc.nativeBuffer(encoded);
|
|
603
|
-
args[0] = buf;
|
|
604
|
-
this.sendRaw.apply(this, args);
|
|
605
|
-
};
|
|
606
|
-
p.encodeOSC = function(packet) {
|
|
607
|
-
packet = packet.buffer ? packet.buffer : packet;
|
|
608
|
-
var encoded;
|
|
609
|
-
try {
|
|
610
|
-
encoded = osc.writePacket(packet, this.options);
|
|
611
|
-
} catch (err) {
|
|
612
|
-
this.emit("error", err);
|
|
613
|
-
}
|
|
614
|
-
return encoded;
|
|
615
|
-
};
|
|
616
|
-
p.decodeOSC = function(data, packetInfo) {
|
|
617
|
-
data = osc.byteArray(data);
|
|
618
|
-
this.emit("raw", data, packetInfo);
|
|
619
|
-
try {
|
|
620
|
-
var packet = osc.readPacket(data, this.options);
|
|
621
|
-
this.emit("osc", packet, packetInfo);
|
|
622
|
-
osc.firePacketEvents(this, packet, void 0, packetInfo);
|
|
623
|
-
} catch (err) {
|
|
624
|
-
this.emit("error", err);
|
|
625
|
-
}
|
|
626
|
-
};
|
|
627
|
-
osc.SLIPPort = function(options) {
|
|
628
|
-
var that = this;
|
|
629
|
-
var o = this.options = options || {};
|
|
630
|
-
o.useSLIP = o.useSLIP === void 0 ? true : o.useSLIP;
|
|
631
|
-
this.decoder = new slip.Decoder({
|
|
632
|
-
onMessage: this.decodeOSC.bind(this),
|
|
633
|
-
onError: function(err) {
|
|
634
|
-
that.emit("error", err);
|
|
635
|
-
}
|
|
636
|
-
});
|
|
637
|
-
var decodeHandler = o.useSLIP ? this.decodeSLIPData : this.decodeOSC;
|
|
638
|
-
this.on("data", decodeHandler.bind(this));
|
|
639
|
-
};
|
|
640
|
-
p = osc.SLIPPort.prototype = Object.create(osc.Port.prototype);
|
|
641
|
-
p.constructor = osc.SLIPPort;
|
|
642
|
-
p.encodeOSC = function(packet) {
|
|
643
|
-
packet = packet.buffer ? packet.buffer : packet;
|
|
644
|
-
var framed;
|
|
645
|
-
try {
|
|
646
|
-
var encoded = osc.writePacket(packet, this.options);
|
|
647
|
-
framed = slip.encode(encoded);
|
|
648
|
-
} catch (err) {
|
|
649
|
-
this.emit("error", err);
|
|
650
|
-
}
|
|
651
|
-
return framed;
|
|
652
|
-
};
|
|
653
|
-
p.decodeSLIPData = function(data, packetInfo) {
|
|
654
|
-
this.decoder.decode(data, packetInfo);
|
|
655
|
-
};
|
|
656
|
-
osc.relay = function(from, to, eventName, sendFnName, transformFn, sendArgs) {
|
|
657
|
-
eventName = eventName || "message";
|
|
658
|
-
sendFnName = sendFnName || "send";
|
|
659
|
-
transformFn = transformFn || function() {
|
|
660
|
-
};
|
|
661
|
-
sendArgs = sendArgs ? [null].concat(sendArgs) : [];
|
|
662
|
-
var listener = function(data) {
|
|
663
|
-
sendArgs[0] = data;
|
|
664
|
-
data = transformFn(data);
|
|
665
|
-
to[sendFnName].apply(to, sendArgs);
|
|
666
|
-
};
|
|
667
|
-
from.on(eventName, listener);
|
|
668
|
-
return {
|
|
669
|
-
eventName,
|
|
670
|
-
listener
|
|
671
|
-
};
|
|
672
|
-
};
|
|
673
|
-
osc.relayPorts = function(from, to, o) {
|
|
674
|
-
var eventName = o.raw ? "raw" : "osc", sendFnName = o.raw ? "sendRaw" : "send";
|
|
675
|
-
return osc.relay(from, to, eventName, sendFnName, o.transform);
|
|
676
|
-
};
|
|
677
|
-
osc.stopRelaying = function(from, relaySpec) {
|
|
678
|
-
from.removeListener(relaySpec.eventName, relaySpec.listener);
|
|
679
|
-
};
|
|
680
|
-
osc.Relay = function(port1, port2, options) {
|
|
681
|
-
var o = this.options = options || {};
|
|
682
|
-
o.raw = false;
|
|
683
|
-
this.port1 = port1;
|
|
684
|
-
this.port2 = port2;
|
|
685
|
-
this.listen();
|
|
686
|
-
};
|
|
687
|
-
p = osc.Relay.prototype = Object.create(EventEmitter.prototype);
|
|
688
|
-
p.constructor = osc.Relay;
|
|
689
|
-
p.open = function() {
|
|
690
|
-
this.port1.open();
|
|
691
|
-
this.port2.open();
|
|
692
|
-
};
|
|
693
|
-
p.listen = function() {
|
|
694
|
-
if (this.port1Spec && this.port2Spec) {
|
|
695
|
-
this.close();
|
|
696
|
-
}
|
|
697
|
-
this.port1Spec = osc.relayPorts(this.port1, this.port2, this.options);
|
|
698
|
-
this.port2Spec = osc.relayPorts(this.port2, this.port1, this.options);
|
|
699
|
-
var closeListener = this.close.bind(this);
|
|
700
|
-
this.port1.on("close", closeListener);
|
|
701
|
-
this.port2.on("close", closeListener);
|
|
702
|
-
};
|
|
703
|
-
p.close = function() {
|
|
704
|
-
osc.stopRelaying(this.port1, this.port1Spec);
|
|
705
|
-
osc.stopRelaying(this.port2, this.port2Spec);
|
|
706
|
-
this.emit("close", this.port1, this.port2);
|
|
707
|
-
};
|
|
708
|
-
})();
|
|
709
|
-
(function() {
|
|
710
|
-
"use strict";
|
|
711
|
-
osc.WebSocket = typeof WebSocket !== "undefined" ? WebSocket : void 0;
|
|
712
|
-
osc.WebSocketPort = function(options) {
|
|
713
|
-
osc.Port.call(this, options);
|
|
714
|
-
this.on("open", this.listen.bind(this));
|
|
715
|
-
this.socket = options.socket;
|
|
716
|
-
if (this.socket) {
|
|
717
|
-
if (this.socket.readyState === 1) {
|
|
718
|
-
osc.WebSocketPort.setupSocketForBinary(this.socket);
|
|
719
|
-
this.emit("open", this.socket);
|
|
720
|
-
} else {
|
|
721
|
-
this.open();
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
};
|
|
725
|
-
var p = osc.WebSocketPort.prototype = Object.create(osc.Port.prototype);
|
|
726
|
-
p.constructor = osc.WebSocketPort;
|
|
727
|
-
p.open = function() {
|
|
728
|
-
if (!this.socket || this.socket.readyState > 1) {
|
|
729
|
-
this.socket = new osc.WebSocket(this.options.url);
|
|
730
|
-
}
|
|
731
|
-
osc.WebSocketPort.setupSocketForBinary(this.socket);
|
|
732
|
-
var that = this;
|
|
733
|
-
this.socket.onopen = function() {
|
|
734
|
-
that.emit("open", that.socket);
|
|
735
|
-
};
|
|
736
|
-
this.socket.onerror = function(err) {
|
|
737
|
-
that.emit("error", err);
|
|
738
|
-
};
|
|
739
|
-
};
|
|
740
|
-
p.listen = function() {
|
|
741
|
-
var that = this;
|
|
742
|
-
this.socket.onmessage = function(e) {
|
|
743
|
-
that.emit("data", e.data, e);
|
|
744
|
-
};
|
|
745
|
-
this.socket.onclose = function(e) {
|
|
746
|
-
that.emit("close", e);
|
|
747
|
-
};
|
|
748
|
-
that.emit("ready");
|
|
749
|
-
};
|
|
750
|
-
p.sendRaw = function(encoded) {
|
|
751
|
-
if (!this.socket || this.socket.readyState !== 1) {
|
|
752
|
-
osc.fireClosedPortSendError(this);
|
|
753
|
-
return;
|
|
754
|
-
}
|
|
755
|
-
this.socket.send(encoded);
|
|
756
|
-
};
|
|
757
|
-
p.close = function(code, reason) {
|
|
758
|
-
this.socket.close(code, reason);
|
|
759
|
-
};
|
|
760
|
-
osc.WebSocketPort.setupSocketForBinary = function(socket) {
|
|
761
|
-
socket.binaryType = osc.isNode ? "nodebuffer" : "arraybuffer";
|
|
762
|
-
};
|
|
763
|
-
})();
|
|
764
|
-
var osc_default = osc;
|
|
765
|
-
var { readPacket, writePacket, readMessage, writeMessage, readBundle, writeBundle } = osc;
|
|
766
|
-
|
|
767
|
-
// js/lib/scsynth_osc.js
|
|
768
|
-
var ScsynthOSC = class {
|
|
769
|
-
constructor(workerBaseURL = null) {
|
|
770
|
-
this.workerBaseURL = workerBaseURL;
|
|
771
|
-
this.workers = {
|
|
772
|
-
oscOut: null,
|
|
773
|
-
// Scheduler worker (now also writes directly to ring buffer)
|
|
774
|
-
oscIn: null,
|
|
775
|
-
debug: null
|
|
776
|
-
};
|
|
777
|
-
this.callbacks = {
|
|
778
|
-
onRawOSC: null,
|
|
779
|
-
// Raw binary OSC callback
|
|
780
|
-
onParsedOSC: null,
|
|
781
|
-
// Parsed OSC callback
|
|
782
|
-
onDebugMessage: null,
|
|
783
|
-
onError: null,
|
|
784
|
-
onInitialized: null
|
|
785
|
-
};
|
|
786
|
-
this.initialized = false;
|
|
787
|
-
this.sharedBuffer = null;
|
|
788
|
-
this.ringBufferBase = null;
|
|
789
|
-
this.bufferConstants = null;
|
|
790
|
-
}
|
|
791
|
-
/**
|
|
792
|
-
* Initialize all workers with SharedArrayBuffer
|
|
793
|
-
*/
|
|
794
|
-
async init(sharedBuffer, ringBufferBase, bufferConstants) {
|
|
795
|
-
if (this.initialized) {
|
|
796
|
-
console.warn("[ScsynthOSC] Already initialized");
|
|
797
|
-
return;
|
|
798
|
-
}
|
|
799
|
-
this.sharedBuffer = sharedBuffer;
|
|
800
|
-
this.ringBufferBase = ringBufferBase;
|
|
801
|
-
this.bufferConstants = bufferConstants;
|
|
802
|
-
try {
|
|
803
|
-
this.workers.oscOut = new Worker(this.workerBaseURL + "osc_out_prescheduler_worker.js", { type: "module" });
|
|
804
|
-
this.workers.oscIn = new Worker(this.workerBaseURL + "osc_in_worker.js", { type: "module" });
|
|
805
|
-
this.workers.debug = new Worker(this.workerBaseURL + "debug_worker.js", { type: "module" });
|
|
806
|
-
this.setupWorkerHandlers();
|
|
807
|
-
const initPromises = [
|
|
808
|
-
this.initWorker(this.workers.oscOut, "OSC SCHEDULER+WRITER"),
|
|
809
|
-
this.initWorker(this.workers.oscIn, "OSC IN"),
|
|
810
|
-
this.initWorker(this.workers.debug, "DEBUG")
|
|
811
|
-
];
|
|
812
|
-
await Promise.all(initPromises);
|
|
813
|
-
this.workers.oscIn.postMessage({ type: "start" });
|
|
814
|
-
this.workers.debug.postMessage({ type: "start" });
|
|
815
|
-
this.initialized = true;
|
|
816
|
-
if (this.callbacks.onInitialized) {
|
|
817
|
-
this.callbacks.onInitialized();
|
|
818
|
-
}
|
|
819
|
-
} catch (error) {
|
|
820
|
-
console.error("[ScsynthOSC] Initialization failed:", error);
|
|
821
|
-
if (this.callbacks.onError) {
|
|
822
|
-
this.callbacks.onError(error);
|
|
823
|
-
}
|
|
824
|
-
throw error;
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
/**
|
|
828
|
-
* Initialize a single worker
|
|
829
|
-
*/
|
|
830
|
-
initWorker(worker, name) {
|
|
831
|
-
return new Promise((resolve, reject) => {
|
|
832
|
-
const timeout = setTimeout(() => {
|
|
833
|
-
reject(new Error(`${name} worker initialization timeout`));
|
|
834
|
-
}, 5e3);
|
|
835
|
-
const handler = (event) => {
|
|
836
|
-
if (event.data.type === "initialized") {
|
|
837
|
-
clearTimeout(timeout);
|
|
838
|
-
worker.removeEventListener("message", handler);
|
|
839
|
-
resolve();
|
|
840
|
-
}
|
|
841
|
-
};
|
|
842
|
-
worker.addEventListener("message", handler);
|
|
843
|
-
worker.postMessage({
|
|
844
|
-
type: "init",
|
|
845
|
-
sharedBuffer: this.sharedBuffer,
|
|
846
|
-
ringBufferBase: this.ringBufferBase,
|
|
847
|
-
bufferConstants: this.bufferConstants
|
|
848
|
-
});
|
|
849
|
-
});
|
|
850
|
-
}
|
|
851
|
-
/**
|
|
852
|
-
* Set up message handlers for all workers
|
|
853
|
-
*/
|
|
854
|
-
setupWorkerHandlers() {
|
|
855
|
-
this.workers.oscIn.onmessage = (event) => {
|
|
856
|
-
const data = event.data;
|
|
857
|
-
switch (data.type) {
|
|
858
|
-
case "messages":
|
|
859
|
-
data.messages.forEach((msg) => {
|
|
860
|
-
if (!msg.oscData) return;
|
|
861
|
-
if (this.callbacks.onRawOSC) {
|
|
862
|
-
this.callbacks.onRawOSC({
|
|
863
|
-
oscData: msg.oscData,
|
|
864
|
-
sequence: msg.sequence
|
|
865
|
-
});
|
|
866
|
-
}
|
|
867
|
-
if (this.callbacks.onParsedOSC) {
|
|
868
|
-
try {
|
|
869
|
-
const options = { metadata: false, unpackSingleArgs: false };
|
|
870
|
-
const decoded = osc_default.readPacket(msg.oscData, options);
|
|
871
|
-
this.callbacks.onParsedOSC(decoded);
|
|
872
|
-
} catch (e) {
|
|
873
|
-
console.error("[ScsynthOSC] Failed to decode OSC message:", e, msg);
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
});
|
|
877
|
-
break;
|
|
878
|
-
case "error":
|
|
879
|
-
console.error("[ScsynthOSC] OSC IN error:", data.error);
|
|
880
|
-
if (this.callbacks.onError) {
|
|
881
|
-
this.callbacks.onError(data.error, "oscIn");
|
|
882
|
-
}
|
|
883
|
-
break;
|
|
884
|
-
}
|
|
885
|
-
};
|
|
886
|
-
this.workers.debug.onmessage = (event) => {
|
|
887
|
-
const data = event.data;
|
|
888
|
-
switch (data.type) {
|
|
889
|
-
case "debug":
|
|
890
|
-
if (this.callbacks.onDebugMessage) {
|
|
891
|
-
data.messages.forEach((msg) => {
|
|
892
|
-
this.callbacks.onDebugMessage(msg);
|
|
893
|
-
});
|
|
894
|
-
}
|
|
895
|
-
break;
|
|
896
|
-
case "error":
|
|
897
|
-
console.error("[ScsynthOSC] DEBUG error:", data.error);
|
|
898
|
-
if (this.callbacks.onError) {
|
|
899
|
-
this.callbacks.onError(data.error, "debug");
|
|
900
|
-
}
|
|
901
|
-
break;
|
|
902
|
-
}
|
|
903
|
-
};
|
|
904
|
-
this.workers.oscOut.onmessage = (event) => {
|
|
905
|
-
const data = event.data;
|
|
906
|
-
switch (data.type) {
|
|
907
|
-
case "error":
|
|
908
|
-
console.error("[ScsynthOSC] OSC OUT error:", data.error);
|
|
909
|
-
if (this.callbacks.onError) {
|
|
910
|
-
this.callbacks.onError(data.error, "oscOut");
|
|
911
|
-
}
|
|
912
|
-
break;
|
|
913
|
-
}
|
|
914
|
-
};
|
|
915
|
-
}
|
|
916
|
-
/**
|
|
917
|
-
* Send OSC data (message or bundle)
|
|
918
|
-
* - OSC messages are sent immediately
|
|
919
|
-
* - OSC bundles are scheduled based on audioTimeS (target audio time)
|
|
920
|
-
*
|
|
921
|
-
* @param {Uint8Array} oscData - Binary OSC data (message or bundle)
|
|
922
|
-
* @param {Object} options - Optional metadata (editorId, runTag, audioTimeS, currentTimeS)
|
|
923
|
-
*/
|
|
924
|
-
send(oscData, options = {}) {
|
|
925
|
-
if (!this.initialized) {
|
|
926
|
-
console.error("[ScsynthOSC] Not initialized");
|
|
927
|
-
return;
|
|
928
|
-
}
|
|
929
|
-
const { editorId = 0, runTag = "", audioTimeS = null, currentTimeS = null } = options;
|
|
930
|
-
this.workers.oscOut.postMessage({
|
|
931
|
-
type: "send",
|
|
932
|
-
oscData,
|
|
933
|
-
editorId,
|
|
934
|
-
runTag,
|
|
935
|
-
audioTimeS,
|
|
936
|
-
currentTimeS
|
|
937
|
-
});
|
|
938
|
-
}
|
|
939
|
-
/**
|
|
940
|
-
* Send OSC data immediately, ignoring any bundle timestamps
|
|
941
|
-
* - Extracts all messages from bundles
|
|
942
|
-
* - Sends all messages immediately to scsynth
|
|
943
|
-
* - For applications that don't expect server-side scheduling
|
|
944
|
-
*
|
|
945
|
-
* @param {Uint8Array} oscData - Binary OSC data (message or bundle)
|
|
946
|
-
*/
|
|
947
|
-
sendImmediate(oscData) {
|
|
948
|
-
if (!this.initialized) {
|
|
949
|
-
console.error("[ScsynthOSC] Not initialized");
|
|
950
|
-
return;
|
|
951
|
-
}
|
|
952
|
-
this.workers.oscOut.postMessage({
|
|
953
|
-
type: "sendImmediate",
|
|
954
|
-
oscData
|
|
955
|
-
});
|
|
956
|
-
}
|
|
957
|
-
/**
|
|
958
|
-
* Cancel scheduled OSC bundles by editor and tag
|
|
959
|
-
*/
|
|
960
|
-
cancelEditorTag(editorId, runTag) {
|
|
961
|
-
if (!this.initialized) return;
|
|
962
|
-
this.workers.oscOut.postMessage({
|
|
963
|
-
type: "cancelEditorTag",
|
|
964
|
-
editorId,
|
|
965
|
-
runTag
|
|
966
|
-
});
|
|
967
|
-
}
|
|
968
|
-
/**
|
|
969
|
-
* Cancel all scheduled OSC bundles from an editor
|
|
970
|
-
*/
|
|
971
|
-
cancelEditor(editorId) {
|
|
972
|
-
if (!this.initialized) return;
|
|
973
|
-
this.workers.oscOut.postMessage({
|
|
974
|
-
type: "cancelEditor",
|
|
975
|
-
editorId
|
|
976
|
-
});
|
|
977
|
-
}
|
|
978
|
-
/**
|
|
979
|
-
* Cancel all scheduled OSC bundles
|
|
980
|
-
*/
|
|
981
|
-
cancelAll() {
|
|
982
|
-
if (!this.initialized) return;
|
|
983
|
-
this.workers.oscOut.postMessage({
|
|
984
|
-
type: "cancelAll"
|
|
985
|
-
});
|
|
986
|
-
}
|
|
987
|
-
/**
|
|
988
|
-
* Clear debug buffer
|
|
989
|
-
*/
|
|
990
|
-
clearDebug() {
|
|
991
|
-
if (!this.initialized) return;
|
|
992
|
-
this.workers.debug.postMessage({
|
|
993
|
-
type: "clear"
|
|
994
|
-
});
|
|
995
|
-
}
|
|
996
|
-
/**
|
|
997
|
-
* Set callback for raw binary OSC messages received from scsynth
|
|
998
|
-
*/
|
|
999
|
-
onRawOSC(callback) {
|
|
1000
|
-
this.callbacks.onRawOSC = callback;
|
|
1001
|
-
}
|
|
1002
|
-
/**
|
|
1003
|
-
* Set callback for parsed OSC messages received from scsynth
|
|
1004
|
-
*/
|
|
1005
|
-
onParsedOSC(callback) {
|
|
1006
|
-
this.callbacks.onParsedOSC = callback;
|
|
1007
|
-
}
|
|
1008
|
-
/**
|
|
1009
|
-
* Set callback for debug messages
|
|
1010
|
-
*/
|
|
1011
|
-
onDebugMessage(callback) {
|
|
1012
|
-
this.callbacks.onDebugMessage = callback;
|
|
1013
|
-
}
|
|
1014
|
-
/**
|
|
1015
|
-
* Set callback for errors
|
|
1016
|
-
*/
|
|
1017
|
-
onError(callback) {
|
|
1018
|
-
this.callbacks.onError = callback;
|
|
1019
|
-
}
|
|
1020
|
-
/**
|
|
1021
|
-
* Set callback for initialization complete
|
|
1022
|
-
*/
|
|
1023
|
-
onInitialized(callback) {
|
|
1024
|
-
this.callbacks.onInitialized = callback;
|
|
1025
|
-
}
|
|
1026
|
-
/**
|
|
1027
|
-
* Terminate all workers and cleanup
|
|
1028
|
-
*/
|
|
1029
|
-
terminate() {
|
|
1030
|
-
if (this.workers.oscOut) {
|
|
1031
|
-
this.workers.oscOut.postMessage({ type: "stop" });
|
|
1032
|
-
this.workers.oscOut.terminate();
|
|
1033
|
-
}
|
|
1034
|
-
if (this.workers.oscIn) {
|
|
1035
|
-
this.workers.oscIn.postMessage({ type: "stop" });
|
|
1036
|
-
this.workers.oscIn.terminate();
|
|
1037
|
-
}
|
|
1038
|
-
if (this.workers.debug) {
|
|
1039
|
-
this.workers.debug.postMessage({ type: "stop" });
|
|
1040
|
-
this.workers.debug.terminate();
|
|
1041
|
-
}
|
|
1042
|
-
this.workers = {
|
|
1043
|
-
oscOut: null,
|
|
1044
|
-
oscIn: null,
|
|
1045
|
-
debug: null
|
|
1046
|
-
};
|
|
1047
|
-
this.initialized = false;
|
|
1048
|
-
console.log("[ScsynthOSC] All workers terminated");
|
|
1049
|
-
}
|
|
1050
|
-
};
|
|
1051
|
-
|
|
1052
|
-
// node_modules/@thi.ng/api/typedarray.js
|
|
1053
|
-
var GL2TYPE = {
|
|
1054
|
-
[
|
|
1055
|
-
5120
|
|
1056
|
-
/* I8 */
|
|
1057
|
-
]: "i8",
|
|
1058
|
-
[
|
|
1059
|
-
5121
|
|
1060
|
-
/* U8 */
|
|
1061
|
-
]: "u8",
|
|
1062
|
-
[
|
|
1063
|
-
5122
|
|
1064
|
-
/* I16 */
|
|
1065
|
-
]: "i16",
|
|
1066
|
-
[
|
|
1067
|
-
5123
|
|
1068
|
-
/* U16 */
|
|
1069
|
-
]: "u16",
|
|
1070
|
-
[
|
|
1071
|
-
5124
|
|
1072
|
-
/* I32 */
|
|
1073
|
-
]: "i32",
|
|
1074
|
-
[
|
|
1075
|
-
5125
|
|
1076
|
-
/* U32 */
|
|
1077
|
-
]: "u32",
|
|
1078
|
-
[
|
|
1079
|
-
5126
|
|
1080
|
-
/* F32 */
|
|
1081
|
-
]: "f32"
|
|
1082
|
-
};
|
|
1083
|
-
var SIZEOF = {
|
|
1084
|
-
u8: 1,
|
|
1085
|
-
u8c: 1,
|
|
1086
|
-
i8: 1,
|
|
1087
|
-
u16: 2,
|
|
1088
|
-
i16: 2,
|
|
1089
|
-
u32: 4,
|
|
1090
|
-
i32: 4,
|
|
1091
|
-
i64: 8,
|
|
1092
|
-
u64: 8,
|
|
1093
|
-
f32: 4,
|
|
1094
|
-
f64: 8
|
|
1095
|
-
};
|
|
1096
|
-
var FLOAT_ARRAY_CTORS = {
|
|
1097
|
-
f32: Float32Array,
|
|
1098
|
-
f64: Float64Array
|
|
1099
|
-
};
|
|
1100
|
-
var INT_ARRAY_CTORS = {
|
|
1101
|
-
i8: Int8Array,
|
|
1102
|
-
i16: Int16Array,
|
|
1103
|
-
i32: Int32Array
|
|
1104
|
-
};
|
|
1105
|
-
var UINT_ARRAY_CTORS = {
|
|
1106
|
-
u8: Uint8Array,
|
|
1107
|
-
u8c: Uint8ClampedArray,
|
|
1108
|
-
u16: Uint16Array,
|
|
1109
|
-
u32: Uint32Array
|
|
1110
|
-
};
|
|
1111
|
-
var BIGINT_ARRAY_CTORS = {
|
|
1112
|
-
i64: BigInt64Array,
|
|
1113
|
-
u64: BigUint64Array
|
|
1114
|
-
};
|
|
1115
|
-
var TYPEDARRAY_CTORS = {
|
|
1116
|
-
...FLOAT_ARRAY_CTORS,
|
|
1117
|
-
...INT_ARRAY_CTORS,
|
|
1118
|
-
...UINT_ARRAY_CTORS
|
|
1119
|
-
};
|
|
1120
|
-
var asNativeType = (type) => {
|
|
1121
|
-
const t = GL2TYPE[type];
|
|
1122
|
-
return t !== void 0 ? t : type;
|
|
1123
|
-
};
|
|
1124
|
-
function typedArray(type, ...args) {
|
|
1125
|
-
const ctor = BIGINT_ARRAY_CTORS[type];
|
|
1126
|
-
return new (ctor || TYPEDARRAY_CTORS[asNativeType(type)])(...args);
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
// node_modules/@thi.ng/binary/align.js
|
|
1130
|
-
var align = (addr, size) => (size--, addr + size & ~size);
|
|
1131
|
-
|
|
1132
|
-
// node_modules/@thi.ng/checks/is-number.js
|
|
1133
|
-
var isNumber = (x) => typeof x === "number";
|
|
1134
|
-
|
|
1135
|
-
// node_modules/@thi.ng/errors/deferror.js
|
|
1136
|
-
var defError = (prefix, suffix = (msg) => msg !== void 0 ? ": " + msg : "") => class extends Error {
|
|
1137
|
-
origMessage;
|
|
1138
|
-
constructor(msg) {
|
|
1139
|
-
super(prefix(msg) + suffix(msg));
|
|
1140
|
-
this.origMessage = msg !== void 0 ? String(msg) : "";
|
|
1141
|
-
}
|
|
1142
|
-
};
|
|
1143
|
-
|
|
1144
|
-
// node_modules/@thi.ng/errors/assert.js
|
|
1145
|
-
var AssertionError = defError(() => "Assertion failed");
|
|
1146
|
-
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) => {
|
|
1147
|
-
if (typeof test === "function" && !test() || !test) {
|
|
1148
|
-
throw new AssertionError(
|
|
1149
|
-
typeof msg === "function" ? msg() : msg
|
|
1150
|
-
);
|
|
1151
|
-
}
|
|
1152
|
-
} : () => {
|
|
1153
|
-
};
|
|
1154
|
-
|
|
1155
|
-
// node_modules/@thi.ng/errors/illegal-arguments.js
|
|
1156
|
-
var IllegalArgumentError = defError(() => "illegal argument(s)");
|
|
1157
|
-
var illegalArgs = (msg) => {
|
|
1158
|
-
throw new IllegalArgumentError(msg);
|
|
1159
|
-
};
|
|
1160
|
-
|
|
1161
|
-
// node_modules/@thi.ng/malloc/pool.js
|
|
1162
|
-
var STATE_FREE = 0;
|
|
1163
|
-
var STATE_USED = 1;
|
|
1164
|
-
var STATE_TOP = 2;
|
|
1165
|
-
var STATE_END = 3;
|
|
1166
|
-
var STATE_ALIGN = 4;
|
|
1167
|
-
var STATE_FLAGS = 5;
|
|
1168
|
-
var STATE_MIN_SPLIT = 6;
|
|
1169
|
-
var MASK_COMPACT = 1;
|
|
1170
|
-
var MASK_SPLIT = 2;
|
|
1171
|
-
var SIZEOF_STATE = 7 * 4;
|
|
1172
|
-
var MEM_BLOCK_SIZE = 0;
|
|
1173
|
-
var MEM_BLOCK_NEXT = 1;
|
|
1174
|
-
var SIZEOF_MEM_BLOCK = 2 * 4;
|
|
1175
|
-
var MemPool = class {
|
|
1176
|
-
buf;
|
|
1177
|
-
start;
|
|
1178
|
-
u8;
|
|
1179
|
-
u32;
|
|
1180
|
-
state;
|
|
1181
|
-
constructor(opts = {}) {
|
|
1182
|
-
this.buf = opts.buf ? opts.buf : new ArrayBuffer(opts.size || 4096);
|
|
1183
|
-
this.start = opts.start != null ? align(Math.max(opts.start, 0), 4) : 0;
|
|
1184
|
-
this.u8 = new Uint8Array(this.buf);
|
|
1185
|
-
this.u32 = new Uint32Array(this.buf);
|
|
1186
|
-
this.state = new Uint32Array(this.buf, this.start, SIZEOF_STATE / 4);
|
|
1187
|
-
if (!opts.skipInitialization) {
|
|
1188
|
-
const _align = opts.align || 8;
|
|
1189
|
-
assert(
|
|
1190
|
-
_align >= 8,
|
|
1191
|
-
`invalid alignment: ${_align}, must be a pow2 and >= 8`
|
|
1192
|
-
);
|
|
1193
|
-
const top = this.initialTop(_align);
|
|
1194
|
-
const resolvedEnd = opts.end != null ? Math.min(opts.end, this.buf.byteLength) : this.buf.byteLength;
|
|
1195
|
-
if (top >= resolvedEnd) {
|
|
1196
|
-
illegalArgs(
|
|
1197
|
-
`insufficient address range (0x${this.start.toString(
|
|
1198
|
-
16
|
|
1199
|
-
)} - 0x${resolvedEnd.toString(16)})`
|
|
1200
|
-
);
|
|
1201
|
-
}
|
|
1202
|
-
this.align = _align;
|
|
1203
|
-
this.doCompact = opts.compact !== false;
|
|
1204
|
-
this.doSplit = opts.split !== false;
|
|
1205
|
-
this.minSplit = opts.minSplit || 16;
|
|
1206
|
-
this.end = resolvedEnd;
|
|
1207
|
-
this.top = top;
|
|
1208
|
-
this._free = 0;
|
|
1209
|
-
this._used = 0;
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
stats() {
|
|
1213
|
-
const listStats = (block) => {
|
|
1214
|
-
let count = 0;
|
|
1215
|
-
let size = 0;
|
|
1216
|
-
while (block) {
|
|
1217
|
-
count++;
|
|
1218
|
-
size += this.blockSize(block);
|
|
1219
|
-
block = this.blockNext(block);
|
|
1220
|
-
}
|
|
1221
|
-
return { count, size };
|
|
1222
|
-
};
|
|
1223
|
-
const free = listStats(this._free);
|
|
1224
|
-
return {
|
|
1225
|
-
free,
|
|
1226
|
-
used: listStats(this._used),
|
|
1227
|
-
top: this.top,
|
|
1228
|
-
available: this.end - this.top + free.size,
|
|
1229
|
-
total: this.buf.byteLength
|
|
1230
|
-
};
|
|
1231
|
-
}
|
|
1232
|
-
callocAs(type, num, fill = 0) {
|
|
1233
|
-
const block = this.mallocAs(type, num);
|
|
1234
|
-
block?.fill(fill);
|
|
1235
|
-
return block;
|
|
1236
|
-
}
|
|
1237
|
-
mallocAs(type, num) {
|
|
1238
|
-
const addr = this.malloc(num * SIZEOF[type]);
|
|
1239
|
-
return addr ? typedArray(type, this.buf, addr, num) : void 0;
|
|
1240
|
-
}
|
|
1241
|
-
calloc(bytes, fill = 0) {
|
|
1242
|
-
const addr = this.malloc(bytes);
|
|
1243
|
-
addr && this.u8.fill(fill, addr, addr + bytes);
|
|
1244
|
-
return addr;
|
|
1245
|
-
}
|
|
1246
|
-
malloc(bytes) {
|
|
1247
|
-
if (bytes <= 0) {
|
|
1248
|
-
return 0;
|
|
1249
|
-
}
|
|
1250
|
-
const paddedSize = align(bytes + SIZEOF_MEM_BLOCK, this.align);
|
|
1251
|
-
const end = this.end;
|
|
1252
|
-
let top = this.top;
|
|
1253
|
-
let block = this._free;
|
|
1254
|
-
let prev = 0;
|
|
1255
|
-
while (block) {
|
|
1256
|
-
const blockSize = this.blockSize(block);
|
|
1257
|
-
const isTop = block + blockSize >= top;
|
|
1258
|
-
if (isTop || blockSize >= paddedSize) {
|
|
1259
|
-
return this.mallocTop(
|
|
1260
|
-
block,
|
|
1261
|
-
prev,
|
|
1262
|
-
blockSize,
|
|
1263
|
-
paddedSize,
|
|
1264
|
-
isTop
|
|
1265
|
-
);
|
|
1266
|
-
}
|
|
1267
|
-
prev = block;
|
|
1268
|
-
block = this.blockNext(block);
|
|
1269
|
-
}
|
|
1270
|
-
block = top;
|
|
1271
|
-
top = block + paddedSize;
|
|
1272
|
-
if (top <= end) {
|
|
1273
|
-
this.initBlock(block, paddedSize, this._used);
|
|
1274
|
-
this._used = block;
|
|
1275
|
-
this.top = top;
|
|
1276
|
-
return __blockDataAddress(block);
|
|
1277
|
-
}
|
|
1278
|
-
return 0;
|
|
1279
|
-
}
|
|
1280
|
-
mallocTop(block, prev, blockSize, paddedSize, isTop) {
|
|
1281
|
-
if (isTop && block + paddedSize > this.end) return 0;
|
|
1282
|
-
if (prev) {
|
|
1283
|
-
this.unlinkBlock(prev, block);
|
|
1284
|
-
} else {
|
|
1285
|
-
this._free = this.blockNext(block);
|
|
1286
|
-
}
|
|
1287
|
-
this.setBlockNext(block, this._used);
|
|
1288
|
-
this._used = block;
|
|
1289
|
-
if (isTop) {
|
|
1290
|
-
this.top = block + this.setBlockSize(block, paddedSize);
|
|
1291
|
-
} else if (this.doSplit) {
|
|
1292
|
-
const excess = blockSize - paddedSize;
|
|
1293
|
-
excess >= this.minSplit && this.splitBlock(block, paddedSize, excess);
|
|
1294
|
-
}
|
|
1295
|
-
return __blockDataAddress(block);
|
|
1296
|
-
}
|
|
1297
|
-
realloc(ptr, bytes) {
|
|
1298
|
-
if (bytes <= 0) {
|
|
1299
|
-
return 0;
|
|
1300
|
-
}
|
|
1301
|
-
const oldAddr = __blockSelfAddress(ptr);
|
|
1302
|
-
let newAddr = 0;
|
|
1303
|
-
let block = this._used;
|
|
1304
|
-
let blockEnd = 0;
|
|
1305
|
-
while (block) {
|
|
1306
|
-
if (block === oldAddr) {
|
|
1307
|
-
[newAddr, blockEnd] = this.reallocBlock(block, bytes);
|
|
1308
|
-
break;
|
|
1309
|
-
}
|
|
1310
|
-
block = this.blockNext(block);
|
|
1311
|
-
}
|
|
1312
|
-
if (newAddr && newAddr !== oldAddr) {
|
|
1313
|
-
this.u8.copyWithin(
|
|
1314
|
-
__blockDataAddress(newAddr),
|
|
1315
|
-
__blockDataAddress(oldAddr),
|
|
1316
|
-
blockEnd
|
|
1317
|
-
);
|
|
1318
|
-
}
|
|
1319
|
-
return __blockDataAddress(newAddr);
|
|
1320
|
-
}
|
|
1321
|
-
reallocBlock(block, bytes) {
|
|
1322
|
-
const blockSize = this.blockSize(block);
|
|
1323
|
-
const blockEnd = block + blockSize;
|
|
1324
|
-
const isTop = blockEnd >= this.top;
|
|
1325
|
-
const paddedSize = align(bytes + SIZEOF_MEM_BLOCK, this.align);
|
|
1326
|
-
if (paddedSize <= blockSize) {
|
|
1327
|
-
if (this.doSplit) {
|
|
1328
|
-
const excess = blockSize - paddedSize;
|
|
1329
|
-
if (excess >= this.minSplit) {
|
|
1330
|
-
this.splitBlock(block, paddedSize, excess);
|
|
1331
|
-
} else if (isTop) {
|
|
1332
|
-
this.top = block + paddedSize;
|
|
1333
|
-
}
|
|
1334
|
-
} else if (isTop) {
|
|
1335
|
-
this.top = block + paddedSize;
|
|
1336
|
-
}
|
|
1337
|
-
return [block, blockEnd];
|
|
1338
|
-
}
|
|
1339
|
-
if (isTop && block + paddedSize < this.end) {
|
|
1340
|
-
this.top = block + this.setBlockSize(block, paddedSize);
|
|
1341
|
-
return [block, blockEnd];
|
|
1342
|
-
}
|
|
1343
|
-
this.free(block);
|
|
1344
|
-
return [__blockSelfAddress(this.malloc(bytes)), blockEnd];
|
|
1345
|
-
}
|
|
1346
|
-
reallocArray(array, num) {
|
|
1347
|
-
if (array.buffer !== this.buf) {
|
|
1348
|
-
return;
|
|
1349
|
-
}
|
|
1350
|
-
const addr = this.realloc(
|
|
1351
|
-
array.byteOffset,
|
|
1352
|
-
num * array.BYTES_PER_ELEMENT
|
|
1353
|
-
);
|
|
1354
|
-
return addr ? new array.constructor(this.buf, addr, num) : void 0;
|
|
1355
|
-
}
|
|
1356
|
-
free(ptrOrArray) {
|
|
1357
|
-
let addr;
|
|
1358
|
-
if (!isNumber(ptrOrArray)) {
|
|
1359
|
-
if (ptrOrArray.buffer !== this.buf) {
|
|
1360
|
-
return false;
|
|
1361
|
-
}
|
|
1362
|
-
addr = ptrOrArray.byteOffset;
|
|
1363
|
-
} else {
|
|
1364
|
-
addr = ptrOrArray;
|
|
1365
|
-
}
|
|
1366
|
-
addr = __blockSelfAddress(addr);
|
|
1367
|
-
let block = this._used;
|
|
1368
|
-
let prev = 0;
|
|
1369
|
-
while (block) {
|
|
1370
|
-
if (block === addr) {
|
|
1371
|
-
if (prev) {
|
|
1372
|
-
this.unlinkBlock(prev, block);
|
|
1373
|
-
} else {
|
|
1374
|
-
this._used = this.blockNext(block);
|
|
1375
|
-
}
|
|
1376
|
-
this.insert(block);
|
|
1377
|
-
this.doCompact && this.compact();
|
|
1378
|
-
return true;
|
|
1379
|
-
}
|
|
1380
|
-
prev = block;
|
|
1381
|
-
block = this.blockNext(block);
|
|
1382
|
-
}
|
|
1383
|
-
return false;
|
|
1384
|
-
}
|
|
1385
|
-
freeAll() {
|
|
1386
|
-
this._free = 0;
|
|
1387
|
-
this._used = 0;
|
|
1388
|
-
this.top = this.initialTop();
|
|
1389
|
-
}
|
|
1390
|
-
release() {
|
|
1391
|
-
delete this.u8;
|
|
1392
|
-
delete this.u32;
|
|
1393
|
-
delete this.state;
|
|
1394
|
-
delete this.buf;
|
|
1395
|
-
return true;
|
|
1396
|
-
}
|
|
1397
|
-
get align() {
|
|
1398
|
-
return this.state[STATE_ALIGN];
|
|
1399
|
-
}
|
|
1400
|
-
set align(x) {
|
|
1401
|
-
this.state[STATE_ALIGN] = x;
|
|
1402
|
-
}
|
|
1403
|
-
get end() {
|
|
1404
|
-
return this.state[STATE_END];
|
|
1405
|
-
}
|
|
1406
|
-
set end(x) {
|
|
1407
|
-
this.state[STATE_END] = x;
|
|
1408
|
-
}
|
|
1409
|
-
get top() {
|
|
1410
|
-
return this.state[STATE_TOP];
|
|
1411
|
-
}
|
|
1412
|
-
set top(x) {
|
|
1413
|
-
this.state[STATE_TOP] = x;
|
|
1414
|
-
}
|
|
1415
|
-
get _free() {
|
|
1416
|
-
return this.state[STATE_FREE];
|
|
1417
|
-
}
|
|
1418
|
-
set _free(block) {
|
|
1419
|
-
this.state[STATE_FREE] = block;
|
|
1420
|
-
}
|
|
1421
|
-
get _used() {
|
|
1422
|
-
return this.state[STATE_USED];
|
|
1423
|
-
}
|
|
1424
|
-
set _used(block) {
|
|
1425
|
-
this.state[STATE_USED] = block;
|
|
1426
|
-
}
|
|
1427
|
-
get doCompact() {
|
|
1428
|
-
return !!(this.state[STATE_FLAGS] & MASK_COMPACT);
|
|
1429
|
-
}
|
|
1430
|
-
set doCompact(flag) {
|
|
1431
|
-
flag ? this.state[STATE_FLAGS] |= 1 << MASK_COMPACT - 1 : this.state[STATE_FLAGS] &= ~MASK_COMPACT;
|
|
1432
|
-
}
|
|
1433
|
-
get doSplit() {
|
|
1434
|
-
return !!(this.state[STATE_FLAGS] & MASK_SPLIT);
|
|
1435
|
-
}
|
|
1436
|
-
set doSplit(flag) {
|
|
1437
|
-
flag ? this.state[STATE_FLAGS] |= 1 << MASK_SPLIT - 1 : this.state[STATE_FLAGS] &= ~MASK_SPLIT;
|
|
1438
|
-
}
|
|
1439
|
-
get minSplit() {
|
|
1440
|
-
return this.state[STATE_MIN_SPLIT];
|
|
1441
|
-
}
|
|
1442
|
-
set minSplit(x) {
|
|
1443
|
-
assert(
|
|
1444
|
-
x > SIZEOF_MEM_BLOCK,
|
|
1445
|
-
`illegal min split threshold: ${x}, require at least ${SIZEOF_MEM_BLOCK + 1}`
|
|
1446
|
-
);
|
|
1447
|
-
this.state[STATE_MIN_SPLIT] = x;
|
|
1448
|
-
}
|
|
1449
|
-
blockSize(block) {
|
|
1450
|
-
return this.u32[(block >> 2) + MEM_BLOCK_SIZE];
|
|
1451
|
-
}
|
|
1452
|
-
/**
|
|
1453
|
-
* Sets & returns given block size.
|
|
1454
|
-
*
|
|
1455
|
-
* @param block -
|
|
1456
|
-
* @param size -
|
|
1457
|
-
*/
|
|
1458
|
-
setBlockSize(block, size) {
|
|
1459
|
-
this.u32[(block >> 2) + MEM_BLOCK_SIZE] = size;
|
|
1460
|
-
return size;
|
|
1461
|
-
}
|
|
1462
|
-
blockNext(block) {
|
|
1463
|
-
return this.u32[(block >> 2) + MEM_BLOCK_NEXT];
|
|
1464
|
-
}
|
|
1465
|
-
/**
|
|
1466
|
-
* Sets block next pointer to `next`. Use zero to indicate list end.
|
|
1467
|
-
*
|
|
1468
|
-
* @param block -
|
|
1469
|
-
*/
|
|
1470
|
-
setBlockNext(block, next) {
|
|
1471
|
-
this.u32[(block >> 2) + MEM_BLOCK_NEXT] = next;
|
|
1472
|
-
}
|
|
1473
|
-
/**
|
|
1474
|
-
* Initializes block header with given `size` and `next` pointer. Returns `block`.
|
|
1475
|
-
*
|
|
1476
|
-
* @param block -
|
|
1477
|
-
* @param size -
|
|
1478
|
-
* @param next -
|
|
1479
|
-
*/
|
|
1480
|
-
initBlock(block, size, next) {
|
|
1481
|
-
const idx = block >>> 2;
|
|
1482
|
-
this.u32[idx + MEM_BLOCK_SIZE] = size;
|
|
1483
|
-
this.u32[idx + MEM_BLOCK_NEXT] = next;
|
|
1484
|
-
return block;
|
|
1485
|
-
}
|
|
1486
|
-
unlinkBlock(prev, block) {
|
|
1487
|
-
this.setBlockNext(prev, this.blockNext(block));
|
|
1488
|
-
}
|
|
1489
|
-
splitBlock(block, blockSize, excess) {
|
|
1490
|
-
this.insert(
|
|
1491
|
-
this.initBlock(
|
|
1492
|
-
block + this.setBlockSize(block, blockSize),
|
|
1493
|
-
excess,
|
|
1494
|
-
0
|
|
1495
|
-
)
|
|
1496
|
-
);
|
|
1497
|
-
this.doCompact && this.compact();
|
|
1498
|
-
}
|
|
1499
|
-
initialTop(_align = this.align) {
|
|
1500
|
-
return align(this.start + SIZEOF_STATE + SIZEOF_MEM_BLOCK, _align) - SIZEOF_MEM_BLOCK;
|
|
1501
|
-
}
|
|
1502
|
-
/**
|
|
1503
|
-
* Traverses free list and attempts to recursively merge blocks
|
|
1504
|
-
* occupying consecutive memory regions. Returns true if any blocks
|
|
1505
|
-
* have been merged. Only called if `compact` option is enabled.
|
|
1506
|
-
*/
|
|
1507
|
-
compact() {
|
|
1508
|
-
let block = this._free;
|
|
1509
|
-
let prev = 0;
|
|
1510
|
-
let scan = 0;
|
|
1511
|
-
let scanPrev;
|
|
1512
|
-
let res = false;
|
|
1513
|
-
while (block) {
|
|
1514
|
-
scanPrev = block;
|
|
1515
|
-
scan = this.blockNext(block);
|
|
1516
|
-
while (scan && scanPrev + this.blockSize(scanPrev) === scan) {
|
|
1517
|
-
scanPrev = scan;
|
|
1518
|
-
scan = this.blockNext(scan);
|
|
1519
|
-
}
|
|
1520
|
-
if (scanPrev !== block) {
|
|
1521
|
-
const newSize = scanPrev - block + this.blockSize(scanPrev);
|
|
1522
|
-
this.setBlockSize(block, newSize);
|
|
1523
|
-
const next = this.blockNext(scanPrev);
|
|
1524
|
-
let tmp = this.blockNext(block);
|
|
1525
|
-
while (tmp && tmp !== next) {
|
|
1526
|
-
const tn = this.blockNext(tmp);
|
|
1527
|
-
this.setBlockNext(tmp, 0);
|
|
1528
|
-
tmp = tn;
|
|
1529
|
-
}
|
|
1530
|
-
this.setBlockNext(block, next);
|
|
1531
|
-
res = true;
|
|
1532
|
-
}
|
|
1533
|
-
if (block + this.blockSize(block) >= this.top) {
|
|
1534
|
-
this.top = block;
|
|
1535
|
-
prev ? this.unlinkBlock(prev, block) : this._free = this.blockNext(block);
|
|
1536
|
-
}
|
|
1537
|
-
prev = block;
|
|
1538
|
-
block = this.blockNext(block);
|
|
1539
|
-
}
|
|
1540
|
-
return res;
|
|
1541
|
-
}
|
|
1542
|
-
/**
|
|
1543
|
-
* Inserts given block into list of free blocks, sorted by address.
|
|
1544
|
-
*
|
|
1545
|
-
* @param block -
|
|
1546
|
-
*/
|
|
1547
|
-
insert(block) {
|
|
1548
|
-
let ptr = this._free;
|
|
1549
|
-
let prev = 0;
|
|
1550
|
-
while (ptr) {
|
|
1551
|
-
if (block <= ptr) break;
|
|
1552
|
-
prev = ptr;
|
|
1553
|
-
ptr = this.blockNext(ptr);
|
|
1554
|
-
}
|
|
1555
|
-
if (prev) {
|
|
1556
|
-
this.setBlockNext(prev, block);
|
|
1557
|
-
} else {
|
|
1558
|
-
this._free = block;
|
|
1559
|
-
}
|
|
1560
|
-
this.setBlockNext(block, ptr);
|
|
1561
|
-
}
|
|
1562
|
-
};
|
|
1563
|
-
var __blockDataAddress = (blockAddress) => blockAddress > 0 ? blockAddress + SIZEOF_MEM_BLOCK : 0;
|
|
1564
|
-
var __blockSelfAddress = (dataAddress) => dataAddress > 0 ? dataAddress - SIZEOF_MEM_BLOCK : 0;
|
|
1565
|
-
|
|
1566
|
-
// js/lib/buffer_manager.js
|
|
1567
|
-
var BUFFER_POOL_ALIGNMENT = 8;
|
|
1568
|
-
var BufferManager = class {
|
|
1569
|
-
// Private configuration
|
|
1570
|
-
#sampleBaseURL;
|
|
1571
|
-
#audioPathMap;
|
|
1572
|
-
// Private implementation
|
|
1573
|
-
#audioContext;
|
|
1574
|
-
#sharedBuffer;
|
|
1575
|
-
#bufferPool;
|
|
1576
|
-
#allocatedBuffers;
|
|
1577
|
-
#pendingBufferOps;
|
|
1578
|
-
#bufferLocks;
|
|
1579
|
-
constructor(options) {
|
|
1580
|
-
const {
|
|
1581
|
-
audioContext,
|
|
1582
|
-
sharedBuffer,
|
|
1583
|
-
bufferPoolConfig,
|
|
1584
|
-
sampleBaseURL,
|
|
1585
|
-
audioPathMap = {},
|
|
1586
|
-
maxBuffers = 1024
|
|
1587
|
-
} = options;
|
|
1588
|
-
if (!audioContext) {
|
|
1589
|
-
throw new Error("BufferManager requires audioContext");
|
|
1590
|
-
}
|
|
1591
|
-
if (!sharedBuffer || !(sharedBuffer instanceof SharedArrayBuffer)) {
|
|
1592
|
-
throw new Error("BufferManager requires sharedBuffer (SharedArrayBuffer)");
|
|
1593
|
-
}
|
|
1594
|
-
if (!bufferPoolConfig || typeof bufferPoolConfig !== "object") {
|
|
1595
|
-
throw new Error("BufferManager requires bufferPoolConfig (object with start, size, align)");
|
|
1596
|
-
}
|
|
1597
|
-
if (!Number.isFinite(bufferPoolConfig.start) || bufferPoolConfig.start < 0) {
|
|
1598
|
-
throw new Error("bufferPoolConfig.start must be a non-negative number");
|
|
1599
|
-
}
|
|
1600
|
-
if (!Number.isFinite(bufferPoolConfig.size) || bufferPoolConfig.size <= 0) {
|
|
1601
|
-
throw new Error("bufferPoolConfig.size must be a positive number");
|
|
1602
|
-
}
|
|
1603
|
-
if (audioPathMap && typeof audioPathMap !== "object") {
|
|
1604
|
-
throw new Error("audioPathMap must be an object");
|
|
1605
|
-
}
|
|
1606
|
-
if (!Number.isInteger(maxBuffers) || maxBuffers <= 0) {
|
|
1607
|
-
throw new Error("maxBuffers must be a positive integer");
|
|
1608
|
-
}
|
|
1609
|
-
this.#audioContext = audioContext;
|
|
1610
|
-
this.#sharedBuffer = sharedBuffer;
|
|
1611
|
-
this.#sampleBaseURL = sampleBaseURL;
|
|
1612
|
-
this.#audioPathMap = audioPathMap;
|
|
1613
|
-
this.#bufferPool = new MemPool({
|
|
1614
|
-
buf: sharedBuffer,
|
|
1615
|
-
start: bufferPoolConfig.start,
|
|
1616
|
-
size: bufferPoolConfig.size,
|
|
1617
|
-
align: BUFFER_POOL_ALIGNMENT
|
|
1618
|
-
});
|
|
1619
|
-
this.#allocatedBuffers = /* @__PURE__ */ new Map();
|
|
1620
|
-
this.#pendingBufferOps = /* @__PURE__ */ new Map();
|
|
1621
|
-
this.#bufferLocks = /* @__PURE__ */ new Map();
|
|
1622
|
-
this.GUARD_BEFORE = 3;
|
|
1623
|
-
this.GUARD_AFTER = 1;
|
|
1624
|
-
this.MAX_BUFFERS = maxBuffers;
|
|
1625
|
-
const poolSizeMB = (bufferPoolConfig.size / (1024 * 1024)).toFixed(0);
|
|
1626
|
-
const poolOffsetMB = (bufferPoolConfig.start / (1024 * 1024)).toFixed(0);
|
|
1627
|
-
console.log(`[BufferManager] Initialized: ${poolSizeMB}MB pool at offset ${poolOffsetMB}MB`);
|
|
1628
|
-
}
|
|
1629
|
-
#resolveAudioPath(scPath) {
|
|
1630
|
-
if (typeof scPath !== "string" || scPath.length === 0) {
|
|
1631
|
-
throw new Error(`Invalid audio path: must be a non-empty string`);
|
|
1632
|
-
}
|
|
1633
|
-
if (scPath.includes("..")) {
|
|
1634
|
-
throw new Error(`Invalid audio path: path cannot contain '..' (got: ${scPath})`);
|
|
1635
|
-
}
|
|
1636
|
-
if (scPath.startsWith("/") || /^[a-zA-Z]:/.test(scPath)) {
|
|
1637
|
-
throw new Error(`Invalid audio path: path must be relative (got: ${scPath})`);
|
|
1638
|
-
}
|
|
1639
|
-
if (scPath.includes("%2e") || scPath.includes("%2E")) {
|
|
1640
|
-
throw new Error(`Invalid audio path: path cannot contain URL-encoded characters (got: ${scPath})`);
|
|
1641
|
-
}
|
|
1642
|
-
if (scPath.includes("\\")) {
|
|
1643
|
-
throw new Error(`Invalid audio path: use forward slashes only (got: ${scPath})`);
|
|
1644
|
-
}
|
|
1645
|
-
if (this.#audioPathMap[scPath]) {
|
|
1646
|
-
return this.#audioPathMap[scPath];
|
|
1647
|
-
}
|
|
1648
|
-
if (!this.#sampleBaseURL) {
|
|
1649
|
-
throw new Error(
|
|
1650
|
-
'sampleBaseURL not configured. Please set it in SuperSonic constructor options.\nExample: new SuperSonic({ sampleBaseURL: "./dist/samples/" })\nOr use CDN: new SuperSonic({ sampleBaseURL: "https://unpkg.com/supersonic-scsynth-samples@latest/samples/" })\nOr install: npm install supersonic-scsynth-samples'
|
|
1651
|
-
);
|
|
1652
|
-
}
|
|
1653
|
-
return this.#sampleBaseURL + scPath;
|
|
1654
|
-
}
|
|
1655
|
-
#validateBufferNumber(bufnum) {
|
|
1656
|
-
if (!Number.isInteger(bufnum) || bufnum < 0 || bufnum >= this.MAX_BUFFERS) {
|
|
1657
|
-
throw new Error(`Invalid buffer number ${bufnum} (must be 0-${this.MAX_BUFFERS - 1})`);
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
|
-
/**
|
|
1661
|
-
* Execute a buffer operation with proper locking, registration, and cleanup
|
|
1662
|
-
* @private
|
|
1663
|
-
* @param {number} bufnum - Buffer number
|
|
1664
|
-
* @param {number} timeoutMs - Operation timeout
|
|
1665
|
-
* @param {Function} operation - Async function that performs the actual buffer work
|
|
1666
|
-
* Should return {ptr, sizeBytes, ...extraProps}
|
|
1667
|
-
* @returns {Promise<Object>} Result object with ptr, uuid, allocationComplete, and extra props
|
|
1668
|
-
*/
|
|
1669
|
-
async #executeBufferOperation(bufnum, timeoutMs, operation) {
|
|
1670
|
-
let allocatedPtr = null;
|
|
1671
|
-
let pendingToken = null;
|
|
1672
|
-
let allocationRegistered = false;
|
|
1673
|
-
const releaseLock = await this.#acquireBufferLock(bufnum);
|
|
1674
|
-
let lockReleased = false;
|
|
1675
|
-
try {
|
|
1676
|
-
await this.#awaitPendingReplacement(bufnum);
|
|
1677
|
-
const { ptr, sizeBytes, ...extraProps } = await operation();
|
|
1678
|
-
allocatedPtr = ptr;
|
|
1679
|
-
const { uuid, allocationComplete } = this.#registerPending(bufnum, timeoutMs);
|
|
1680
|
-
pendingToken = uuid;
|
|
1681
|
-
this.#recordAllocation(bufnum, allocatedPtr, sizeBytes, uuid, allocationComplete);
|
|
1682
|
-
allocationRegistered = true;
|
|
1683
|
-
const managedCompletion = this.#attachFinalizer(bufnum, uuid, allocationComplete);
|
|
1684
|
-
releaseLock();
|
|
1685
|
-
lockReleased = true;
|
|
1686
|
-
return {
|
|
1687
|
-
ptr: allocatedPtr,
|
|
1688
|
-
uuid,
|
|
1689
|
-
allocationComplete: managedCompletion,
|
|
1690
|
-
...extraProps
|
|
1691
|
-
};
|
|
1692
|
-
} catch (error) {
|
|
1693
|
-
if (allocationRegistered && pendingToken) {
|
|
1694
|
-
this.#finalizeReplacement(bufnum, pendingToken, false);
|
|
1695
|
-
} else if (allocatedPtr) {
|
|
1696
|
-
this.#bufferPool.free(allocatedPtr);
|
|
1697
|
-
}
|
|
1698
|
-
throw error;
|
|
1699
|
-
} finally {
|
|
1700
|
-
if (!lockReleased) {
|
|
1701
|
-
releaseLock();
|
|
1702
|
-
}
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
async prepareFromFile(params) {
|
|
1706
|
-
const {
|
|
1707
|
-
bufnum,
|
|
1708
|
-
path,
|
|
1709
|
-
startFrame = 0,
|
|
1710
|
-
numFrames = 0,
|
|
1711
|
-
channels = null
|
|
1712
|
-
} = params;
|
|
1713
|
-
this.#validateBufferNumber(bufnum);
|
|
1714
|
-
return this.#executeBufferOperation(bufnum, 6e4, async () => {
|
|
1715
|
-
const resolvedPath = this.#resolveAudioPath(path);
|
|
1716
|
-
const response = await fetch(resolvedPath);
|
|
1717
|
-
if (!response.ok) {
|
|
1718
|
-
throw new Error(`Failed to fetch ${resolvedPath}: ${response.status} ${response.statusText}`);
|
|
1719
|
-
}
|
|
1720
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
1721
|
-
const audioBuffer = await this.#audioContext.decodeAudioData(arrayBuffer);
|
|
1722
|
-
const start = Math.max(0, Math.floor(startFrame || 0));
|
|
1723
|
-
const availableFrames = audioBuffer.length - start;
|
|
1724
|
-
const framesRequested = numFrames && numFrames > 0 ? Math.min(Math.floor(numFrames), availableFrames) : availableFrames;
|
|
1725
|
-
if (framesRequested <= 0) {
|
|
1726
|
-
throw new Error(`No audio frames available for buffer ${bufnum} from ${path}`);
|
|
1727
|
-
}
|
|
1728
|
-
const selectedChannels = this.#normalizeChannels(channels, audioBuffer.numberOfChannels);
|
|
1729
|
-
const numChannels = selectedChannels.length;
|
|
1730
|
-
const totalSamples = framesRequested * numChannels + (this.GUARD_BEFORE + this.GUARD_AFTER) * numChannels;
|
|
1731
|
-
const ptr = this.#malloc(totalSamples);
|
|
1732
|
-
const interleaved = new Float32Array(totalSamples);
|
|
1733
|
-
const dataOffset = this.GUARD_BEFORE * numChannels;
|
|
1734
|
-
for (let frame = 0; frame < framesRequested; frame++) {
|
|
1735
|
-
for (let ch = 0; ch < numChannels; ch++) {
|
|
1736
|
-
const sourceChannel = selectedChannels[ch];
|
|
1737
|
-
const channelData = audioBuffer.getChannelData(sourceChannel);
|
|
1738
|
-
interleaved[dataOffset + frame * numChannels + ch] = channelData[start + frame];
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
|
-
this.#writeToSharedBuffer(ptr, interleaved);
|
|
1742
|
-
const sizeBytes = interleaved.length * 4;
|
|
1743
|
-
return {
|
|
1744
|
-
ptr,
|
|
1745
|
-
sizeBytes,
|
|
1746
|
-
numFrames: framesRequested,
|
|
1747
|
-
numChannels,
|
|
1748
|
-
sampleRate: audioBuffer.sampleRate
|
|
1749
|
-
};
|
|
1750
|
-
});
|
|
1751
|
-
}
|
|
1752
|
-
async prepareEmpty(params) {
|
|
1753
|
-
const {
|
|
1754
|
-
bufnum,
|
|
1755
|
-
numFrames,
|
|
1756
|
-
numChannels = 1,
|
|
1757
|
-
sampleRate = null
|
|
1758
|
-
} = params;
|
|
1759
|
-
this.#validateBufferNumber(bufnum);
|
|
1760
|
-
if (!Number.isFinite(numFrames) || numFrames <= 0) {
|
|
1761
|
-
throw new Error(`/b_alloc requires a positive number of frames (got ${numFrames})`);
|
|
1762
|
-
}
|
|
1763
|
-
if (!Number.isFinite(numChannels) || numChannels <= 0) {
|
|
1764
|
-
throw new Error(`/b_alloc requires a positive channel count (got ${numChannels})`);
|
|
1765
|
-
}
|
|
1766
|
-
const roundedFrames = Math.floor(numFrames);
|
|
1767
|
-
const roundedChannels = Math.floor(numChannels);
|
|
1768
|
-
return this.#executeBufferOperation(bufnum, 5e3, async () => {
|
|
1769
|
-
const totalSamples = roundedFrames * roundedChannels + (this.GUARD_BEFORE + this.GUARD_AFTER) * roundedChannels;
|
|
1770
|
-
const ptr = this.#malloc(totalSamples);
|
|
1771
|
-
const interleaved = new Float32Array(totalSamples);
|
|
1772
|
-
this.#writeToSharedBuffer(ptr, interleaved);
|
|
1773
|
-
const sizeBytes = interleaved.length * 4;
|
|
1774
|
-
return {
|
|
1775
|
-
ptr,
|
|
1776
|
-
sizeBytes,
|
|
1777
|
-
numFrames: roundedFrames,
|
|
1778
|
-
numChannels: roundedChannels,
|
|
1779
|
-
sampleRate: sampleRate || this.#audioContext.sampleRate
|
|
1780
|
-
};
|
|
1781
|
-
});
|
|
1782
|
-
}
|
|
1783
|
-
#normalizeChannels(requestedChannels, fileChannels) {
|
|
1784
|
-
if (!requestedChannels || requestedChannels.length === 0) {
|
|
1785
|
-
return Array.from({ length: fileChannels }, (_, i) => i);
|
|
1786
|
-
}
|
|
1787
|
-
requestedChannels.forEach((channel) => {
|
|
1788
|
-
if (!Number.isInteger(channel) || channel < 0 || channel >= fileChannels) {
|
|
1789
|
-
throw new Error(`Channel ${channel} is out of range (file has ${fileChannels} channels)`);
|
|
1790
|
-
}
|
|
1791
|
-
});
|
|
1792
|
-
return requestedChannels;
|
|
1793
|
-
}
|
|
1794
|
-
#malloc(totalSamples) {
|
|
1795
|
-
const bytesNeeded = totalSamples * 4;
|
|
1796
|
-
const ptr = this.#bufferPool.malloc(bytesNeeded);
|
|
1797
|
-
if (ptr === 0) {
|
|
1798
|
-
const stats = this.#bufferPool.stats();
|
|
1799
|
-
const availableMB = ((stats.available || 0) / (1024 * 1024)).toFixed(2);
|
|
1800
|
-
const totalMB = ((stats.total || 0) / (1024 * 1024)).toFixed(2);
|
|
1801
|
-
const requestedMB = (bytesNeeded / (1024 * 1024)).toFixed(2);
|
|
1802
|
-
throw new Error(
|
|
1803
|
-
`Buffer pool allocation failed: requested ${requestedMB}MB, available ${availableMB}MB of ${totalMB}MB total`
|
|
1804
|
-
);
|
|
1805
|
-
}
|
|
1806
|
-
return ptr;
|
|
1807
|
-
}
|
|
1808
|
-
#writeToSharedBuffer(ptr, data) {
|
|
1809
|
-
const heap = new Float32Array(this.#sharedBuffer, ptr, data.length);
|
|
1810
|
-
heap.set(data);
|
|
1811
|
-
}
|
|
1812
|
-
#createPendingOperation(uuid, bufnum, timeoutMs) {
|
|
1813
|
-
return new Promise((resolve, reject) => {
|
|
1814
|
-
const timeout = setTimeout(() => {
|
|
1815
|
-
this.#pendingBufferOps.delete(uuid);
|
|
1816
|
-
reject(new Error(`Buffer ${bufnum} allocation timeout (${timeoutMs}ms)`));
|
|
1817
|
-
}, timeoutMs);
|
|
1818
|
-
this.#pendingBufferOps.set(uuid, { resolve, reject, timeout });
|
|
1819
|
-
});
|
|
1820
|
-
}
|
|
1821
|
-
#registerPending(bufnum, timeoutMs) {
|
|
1822
|
-
const uuid = crypto.randomUUID();
|
|
1823
|
-
const allocationComplete = this.#createPendingOperation(uuid, bufnum, timeoutMs);
|
|
1824
|
-
return { uuid, allocationComplete };
|
|
1825
|
-
}
|
|
1826
|
-
async #acquireBufferLock(bufnum) {
|
|
1827
|
-
const prev = this.#bufferLocks.get(bufnum) || Promise.resolve();
|
|
1828
|
-
let releaseLock;
|
|
1829
|
-
const current = new Promise((resolve) => {
|
|
1830
|
-
releaseLock = resolve;
|
|
1831
|
-
});
|
|
1832
|
-
this.#bufferLocks.set(bufnum, prev.then(() => current));
|
|
1833
|
-
await prev;
|
|
1834
|
-
return () => {
|
|
1835
|
-
if (releaseLock) {
|
|
1836
|
-
releaseLock();
|
|
1837
|
-
releaseLock = null;
|
|
1838
|
-
}
|
|
1839
|
-
if (this.#bufferLocks.get(bufnum) === current) {
|
|
1840
|
-
this.#bufferLocks.delete(bufnum);
|
|
1841
|
-
}
|
|
1842
|
-
};
|
|
1843
|
-
}
|
|
1844
|
-
#recordAllocation(bufnum, ptr, sizeBytes, pendingToken, pendingPromise) {
|
|
1845
|
-
const previousEntry = this.#allocatedBuffers.get(bufnum);
|
|
1846
|
-
const entry = {
|
|
1847
|
-
ptr,
|
|
1848
|
-
size: sizeBytes,
|
|
1849
|
-
pendingToken,
|
|
1850
|
-
pendingPromise,
|
|
1851
|
-
previousAllocation: previousEntry ? { ptr: previousEntry.ptr, size: previousEntry.size } : null
|
|
1852
|
-
};
|
|
1853
|
-
this.#allocatedBuffers.set(bufnum, entry);
|
|
1854
|
-
return entry;
|
|
1855
|
-
}
|
|
1856
|
-
async #awaitPendingReplacement(bufnum) {
|
|
1857
|
-
const existing = this.#allocatedBuffers.get(bufnum);
|
|
1858
|
-
if (existing && existing.pendingToken && existing.pendingPromise) {
|
|
1859
|
-
try {
|
|
1860
|
-
await existing.pendingPromise;
|
|
1861
|
-
} catch {
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
1864
|
-
}
|
|
1865
|
-
#attachFinalizer(bufnum, pendingToken, promise) {
|
|
1866
|
-
if (!promise || typeof promise.then !== "function") {
|
|
1867
|
-
this.#finalizeReplacement(bufnum, pendingToken, true);
|
|
1868
|
-
return Promise.resolve();
|
|
1869
|
-
}
|
|
1870
|
-
return promise.then((value) => {
|
|
1871
|
-
this.#finalizeReplacement(bufnum, pendingToken, true);
|
|
1872
|
-
return value;
|
|
1873
|
-
}).catch((error) => {
|
|
1874
|
-
this.#finalizeReplacement(bufnum, pendingToken, false);
|
|
1875
|
-
throw error;
|
|
1876
|
-
});
|
|
1877
|
-
}
|
|
1878
|
-
#finalizeReplacement(bufnum, pendingToken, success) {
|
|
1879
|
-
const entry = this.#allocatedBuffers.get(bufnum);
|
|
1880
|
-
if (!entry || entry.pendingToken !== pendingToken) {
|
|
1881
|
-
return;
|
|
1882
|
-
}
|
|
1883
|
-
const previous = entry.previousAllocation;
|
|
1884
|
-
if (success) {
|
|
1885
|
-
entry.pendingToken = null;
|
|
1886
|
-
entry.pendingPromise = null;
|
|
1887
|
-
entry.previousAllocation = null;
|
|
1888
|
-
if (previous?.ptr) {
|
|
1889
|
-
this.#bufferPool.free(previous.ptr);
|
|
1890
|
-
}
|
|
1891
|
-
return;
|
|
1892
|
-
}
|
|
1893
|
-
if (entry.ptr) {
|
|
1894
|
-
this.#bufferPool.free(entry.ptr);
|
|
1895
|
-
}
|
|
1896
|
-
entry.pendingPromise = null;
|
|
1897
|
-
if (previous?.ptr) {
|
|
1898
|
-
this.#allocatedBuffers.set(bufnum, {
|
|
1899
|
-
ptr: previous.ptr,
|
|
1900
|
-
size: previous.size,
|
|
1901
|
-
pendingToken: null,
|
|
1902
|
-
previousAllocation: null
|
|
1903
|
-
});
|
|
1904
|
-
} else {
|
|
1905
|
-
this.#allocatedBuffers.delete(bufnum);
|
|
1906
|
-
}
|
|
1907
|
-
}
|
|
1908
|
-
/**
|
|
1909
|
-
* Handle /buffer/freed notification from scsynth
|
|
1910
|
-
* Called by SuperSonic when /buffer/freed OSC message is received
|
|
1911
|
-
* @param {Array} args - [bufnum, freedPtr]
|
|
1912
|
-
*/
|
|
1913
|
-
handleBufferFreed(args) {
|
|
1914
|
-
const bufnum = args[0];
|
|
1915
|
-
const freedPtr = args[1];
|
|
1916
|
-
const bufferInfo = this.#allocatedBuffers.get(bufnum);
|
|
1917
|
-
if (!bufferInfo) {
|
|
1918
|
-
if (typeof freedPtr === "number" && freedPtr !== 0) {
|
|
1919
|
-
this.#bufferPool.free(freedPtr);
|
|
1920
|
-
}
|
|
1921
|
-
return;
|
|
1922
|
-
}
|
|
1923
|
-
if (typeof freedPtr === "number" && freedPtr === bufferInfo.ptr) {
|
|
1924
|
-
this.#bufferPool.free(bufferInfo.ptr);
|
|
1925
|
-
this.#allocatedBuffers.delete(bufnum);
|
|
1926
|
-
return;
|
|
1927
|
-
}
|
|
1928
|
-
if (typeof freedPtr === "number" && bufferInfo.previousAllocation && bufferInfo.previousAllocation.ptr === freedPtr) {
|
|
1929
|
-
this.#bufferPool.free(freedPtr);
|
|
1930
|
-
bufferInfo.previousAllocation = null;
|
|
1931
|
-
return;
|
|
1932
|
-
}
|
|
1933
|
-
this.#bufferPool.free(bufferInfo.ptr);
|
|
1934
|
-
this.#allocatedBuffers.delete(bufnum);
|
|
1935
|
-
}
|
|
1936
|
-
/**
|
|
1937
|
-
* Handle /buffer/allocated notification from scsynth
|
|
1938
|
-
* Called by SuperSonic when /buffer/allocated OSC message is received
|
|
1939
|
-
* @param {Array} args - [uuid, bufnum]
|
|
1940
|
-
*/
|
|
1941
|
-
handleBufferAllocated(args) {
|
|
1942
|
-
const uuid = args[0];
|
|
1943
|
-
const bufnum = args[1];
|
|
1944
|
-
const pending = this.#pendingBufferOps.get(uuid);
|
|
1945
|
-
if (pending) {
|
|
1946
|
-
clearTimeout(pending.timeout);
|
|
1947
|
-
pending.resolve({ bufnum });
|
|
1948
|
-
this.#pendingBufferOps.delete(uuid);
|
|
1949
|
-
}
|
|
1950
|
-
}
|
|
1951
|
-
/**
|
|
1952
|
-
* Allocate raw buffer memory
|
|
1953
|
-
* @param {number} numSamples - Number of Float32 samples
|
|
1954
|
-
* @returns {number} Byte offset, or 0 if failed
|
|
1955
|
-
*/
|
|
1956
|
-
allocate(numSamples) {
|
|
1957
|
-
const sizeBytes = numSamples * 4;
|
|
1958
|
-
const addr = this.#bufferPool.malloc(sizeBytes);
|
|
1959
|
-
if (addr === 0) {
|
|
1960
|
-
const stats = this.#bufferPool.stats();
|
|
1961
|
-
const availableMB = ((stats.available || 0) / (1024 * 1024)).toFixed(2);
|
|
1962
|
-
const totalMB = ((stats.total || 0) / (1024 * 1024)).toFixed(2);
|
|
1963
|
-
const requestedMB = (sizeBytes / (1024 * 1024)).toFixed(2);
|
|
1964
|
-
console.error(
|
|
1965
|
-
`[BufferManager] Allocation failed: requested ${requestedMB}MB, available ${availableMB}MB of ${totalMB}MB total`
|
|
1966
|
-
);
|
|
1967
|
-
}
|
|
1968
|
-
return addr;
|
|
1969
|
-
}
|
|
1970
|
-
/**
|
|
1971
|
-
* Free previously allocated buffer
|
|
1972
|
-
* @param {number} addr - Buffer address
|
|
1973
|
-
* @returns {boolean} true if freed successfully
|
|
1974
|
-
*/
|
|
1975
|
-
free(addr) {
|
|
1976
|
-
return this.#bufferPool.free(addr);
|
|
1977
|
-
}
|
|
1978
|
-
/**
|
|
1979
|
-
* Get Float32Array view of buffer
|
|
1980
|
-
* @param {number} addr - Buffer address
|
|
1981
|
-
* @param {number} numSamples - Number of samples
|
|
1982
|
-
* @returns {Float32Array} Typed array view
|
|
1983
|
-
*/
|
|
1984
|
-
getView(addr, numSamples) {
|
|
1985
|
-
return new Float32Array(this.#sharedBuffer, addr, numSamples);
|
|
1986
|
-
}
|
|
1987
|
-
/**
|
|
1988
|
-
* Get buffer pool statistics
|
|
1989
|
-
* @returns {Object} Stats including total, available, used
|
|
1990
|
-
*/
|
|
1991
|
-
getStats() {
|
|
1992
|
-
return this.#bufferPool.stats();
|
|
1993
|
-
}
|
|
1994
|
-
/**
|
|
1995
|
-
* Get buffer diagnostics
|
|
1996
|
-
* @returns {Object} Buffer state and pool statistics
|
|
1997
|
-
*/
|
|
1998
|
-
getDiagnostics() {
|
|
1999
|
-
const poolStats = this.#bufferPool.stats();
|
|
2000
|
-
let bytesActive = 0;
|
|
2001
|
-
let pendingCount = 0;
|
|
2002
|
-
for (const entry of this.#allocatedBuffers.values()) {
|
|
2003
|
-
if (!entry) continue;
|
|
2004
|
-
bytesActive += entry.size || 0;
|
|
2005
|
-
if (entry.pendingToken) {
|
|
2006
|
-
pendingCount++;
|
|
2007
|
-
}
|
|
2008
|
-
}
|
|
2009
|
-
return {
|
|
2010
|
-
active: this.#allocatedBuffers.size,
|
|
2011
|
-
pending: pendingCount,
|
|
2012
|
-
bytesActive,
|
|
2013
|
-
pool: {
|
|
2014
|
-
total: poolStats.total || 0,
|
|
2015
|
-
available: poolStats.available || 0,
|
|
2016
|
-
freeBytes: poolStats.free?.size || 0,
|
|
2017
|
-
freeBlocks: poolStats.free?.count || 0,
|
|
2018
|
-
usedBytes: poolStats.used?.size || 0,
|
|
2019
|
-
usedBlocks: poolStats.used?.count || 0
|
|
2020
|
-
}
|
|
2021
|
-
};
|
|
2022
|
-
}
|
|
2023
|
-
/**
|
|
2024
|
-
* Clean up resources
|
|
2025
|
-
*/
|
|
2026
|
-
destroy() {
|
|
2027
|
-
for (const [uuid, pending] of this.#pendingBufferOps.entries()) {
|
|
2028
|
-
clearTimeout(pending.timeout);
|
|
2029
|
-
pending.reject(new Error("BufferManager destroyed"));
|
|
2030
|
-
}
|
|
2031
|
-
this.#pendingBufferOps.clear();
|
|
2032
|
-
for (const [bufnum, entry] of this.#allocatedBuffers.entries()) {
|
|
2033
|
-
if (entry.ptr) {
|
|
2034
|
-
this.#bufferPool.free(entry.ptr);
|
|
2035
|
-
}
|
|
2036
|
-
}
|
|
2037
|
-
this.#allocatedBuffers.clear();
|
|
2038
|
-
this.#bufferLocks.clear();
|
|
2039
|
-
console.log("[BufferManager] Destroyed");
|
|
2040
|
-
}
|
|
2041
|
-
};
|
|
2042
|
-
|
|
2043
|
-
// js/timing_constants.js
|
|
2044
|
-
var NTP_EPOCH_OFFSET = 2208988800;
|
|
2045
|
-
var DRIFT_UPDATE_INTERVAL_MS = 15e3;
|
|
2046
|
-
|
|
2047
|
-
// js/memory_layout.js
|
|
2048
|
-
var MemoryLayout = {
|
|
2049
|
-
/**
|
|
2050
|
-
* Total WebAssembly memory in pages (1 page = 64KB)
|
|
2051
|
-
* Current: 1280 pages = 80MB
|
|
2052
|
-
*
|
|
2053
|
-
* This value is used by build.sh to set -sINITIAL_MEMORY
|
|
2054
|
-
* Must match: totalPages * 65536 = bufferPoolOffset + bufferPoolSize
|
|
2055
|
-
*/
|
|
2056
|
-
totalPages: 1280,
|
|
2057
|
-
/**
|
|
2058
|
-
* WASM heap size (implicit, first section of memory)
|
|
2059
|
-
* Not directly configurable here - defined by bufferPoolOffset - ringBufferReserved
|
|
2060
|
-
* Current: 0-16MB (16 * 1024 * 1024 = 16777216 bytes)
|
|
2061
|
-
*/
|
|
2062
|
-
// wasmHeapSize is implicitly: bufferPoolOffset - ringBufferReserved
|
|
2063
|
-
/**
|
|
2064
|
-
* Ring buffer reserved space (between WASM heap and buffer pool)
|
|
2065
|
-
* Actual ring buffer usage: IN: 768KB, OUT: 128KB, DEBUG: 64KB = 960KB
|
|
2066
|
-
* Plus control structures: CONTROL_SIZE (40B) + METRICS_SIZE (48B) + NTP_START_TIME_SIZE (8B) ≈ 96B
|
|
2067
|
-
* Total actual usage: ~960KB
|
|
2068
|
-
* Reserved: 1MB (provides ~64KB headroom for alignment and future expansion)
|
|
2069
|
-
* Current: 1MB reserved (starts where WASM heap ends at 16MB)
|
|
2070
|
-
*/
|
|
2071
|
-
ringBufferReserved: 1 * 1024 * 1024,
|
|
2072
|
-
// 1MB reserved
|
|
2073
|
-
/**
|
|
2074
|
-
* Buffer pool byte offset from start of SharedArrayBuffer
|
|
2075
|
-
* Audio samples are allocated from this pool using @thi.ng/malloc
|
|
2076
|
-
* Must be after WASM heap + ring buffer area
|
|
2077
|
-
* Current: 17MB offset = after 16MB heap + 1MB ring buffers
|
|
2078
|
-
*/
|
|
2079
|
-
bufferPoolOffset: 17 * 1024 * 1024,
|
|
2080
|
-
// 17825792 bytes
|
|
2081
|
-
/**
|
|
2082
|
-
* Buffer pool size in bytes
|
|
2083
|
-
* Used for audio sample storage (loaded files + allocated buffers)
|
|
2084
|
-
* Current: 63MB (enough for ~3.5 minutes of stereo at 48kHz uncompressed)
|
|
2085
|
-
*/
|
|
2086
|
-
bufferPoolSize: 63 * 1024 * 1024,
|
|
2087
|
-
// 66060288 bytes
|
|
2088
|
-
/**
|
|
2089
|
-
* Total memory calculation (should equal totalPages * 65536)
|
|
2090
|
-
* wasmHeap (16MB) + ringReserve (1MB) + bufferPool (63MB) = 80MB
|
|
2091
|
-
*/
|
|
2092
|
-
get totalMemory() {
|
|
2093
|
-
return this.bufferPoolOffset + this.bufferPoolSize;
|
|
2094
|
-
},
|
|
2095
|
-
/**
|
|
2096
|
-
* Effective WASM heap size (derived)
|
|
2097
|
-
* This is the space available for scsynth C++ allocations
|
|
2098
|
-
*/
|
|
2099
|
-
get wasmHeapSize() {
|
|
2100
|
-
return this.bufferPoolOffset - this.ringBufferReserved;
|
|
2101
|
-
}
|
|
2102
|
-
};
|
|
2103
|
-
|
|
2104
|
-
// js/scsynth_options.js
|
|
2105
|
-
var defaultWorldOptions = {
|
|
2106
|
-
/**
|
|
2107
|
-
* Maximum number of audio buffers (SndBuf slots)
|
|
2108
|
-
* Each buffer slot: 104 bytes overhead (2x SndBuf + SndBufUpdates structs)
|
|
2109
|
-
* Actual audio data is stored in buffer pool (separate from heap)
|
|
2110
|
-
* Default: 1024 (matching SuperCollider default)
|
|
2111
|
-
* Range: 1-65535 (limited by practical memory constraints)
|
|
2112
|
-
*/
|
|
2113
|
-
numBuffers: 1024,
|
|
2114
|
-
/**
|
|
2115
|
-
* Maximum number of synthesis nodes (synths + groups)
|
|
2116
|
-
* Each node: ~200-500 bytes depending on synth complexity
|
|
2117
|
-
* Default: 1024 (matching SuperCollider default)
|
|
2118
|
-
*/
|
|
2119
|
-
maxNodes: 1024,
|
|
2120
|
-
/**
|
|
2121
|
-
* Maximum number of synth definitions (SynthDef count)
|
|
2122
|
-
* Each definition: variable size (typically 1-10KB)
|
|
2123
|
-
* Default: 1024 (matching SuperCollider default)
|
|
2124
|
-
*/
|
|
2125
|
-
maxGraphDefs: 1024,
|
|
2126
|
-
/**
|
|
2127
|
-
* Maximum wire buffers for internal audio routing
|
|
2128
|
-
* Wire buffers: temporary buffers for UGen connections
|
|
2129
|
-
* Each: bufLength * 8 bytes (128 samples * 8 = 1024 bytes)
|
|
2130
|
-
* Default: 64 (matching SuperCollider default)
|
|
2131
|
-
*/
|
|
2132
|
-
maxWireBufs: 64,
|
|
2133
|
-
/**
|
|
2134
|
-
* Number of audio bus channels
|
|
2135
|
-
* Audio buses: real-time audio routing between synths
|
|
2136
|
-
* Memory: bufLength * numChannels * 4 bytes (128 * 128 * 4 = 64KB)
|
|
2137
|
-
* Default: 128 (SuperSonic default, SC uses 1024)
|
|
2138
|
-
*/
|
|
2139
|
-
numAudioBusChannels: 128,
|
|
2140
|
-
/**
|
|
2141
|
-
* Number of input bus channels (hardware audio input)
|
|
2142
|
-
* AudioWorklet can support input, but SuperSonic doesn't currently route it
|
|
2143
|
-
* Default: 0 (audio input not implemented)
|
|
2144
|
-
*/
|
|
2145
|
-
numInputBusChannels: 0,
|
|
2146
|
-
/**
|
|
2147
|
-
* Number of output bus channels (hardware audio output)
|
|
2148
|
-
* WebAudio/AudioWorklet output
|
|
2149
|
-
* Default: 2 (stereo)
|
|
2150
|
-
*/
|
|
2151
|
-
numOutputBusChannels: 2,
|
|
2152
|
-
/**
|
|
2153
|
-
* Number of control bus channels
|
|
2154
|
-
* Control buses: control-rate data sharing between synths
|
|
2155
|
-
* Memory: numChannels * 4 bytes (4096 * 4 = 16KB)
|
|
2156
|
-
* Default: 4096 (SuperSonic default, SC uses 16384)
|
|
2157
|
-
*/
|
|
2158
|
-
numControlBusChannels: 4096,
|
|
2159
|
-
/**
|
|
2160
|
-
* Audio buffer length in samples (AudioWorklet quantum)
|
|
2161
|
-
*
|
|
2162
|
-
* FIXED at 128 (WebAudio API spec - cannot be changed)
|
|
2163
|
-
* Unlike SuperCollider (configurable 32/64/128), AudioWorklet has a fixed quantum.
|
|
2164
|
-
* Overriding this value will cause initialization to fail.
|
|
2165
|
-
*
|
|
2166
|
-
* Default: 128
|
|
2167
|
-
*/
|
|
2168
|
-
bufLength: 128,
|
|
2169
|
-
/**
|
|
2170
|
-
* Real-time memory pool size in kilobytes
|
|
2171
|
-
* AllocPool for synthesis-time allocations (UGen memory, etc.)
|
|
2172
|
-
* This is the largest single allocation from WASM heap
|
|
2173
|
-
* Memory: realTimeMemorySize * 1024 bytes (8192 * 1024 = 8MB)
|
|
2174
|
-
* Default: 8192 KB (8MB, matching Sonic Pi and SuperCollider defaults)
|
|
2175
|
-
*/
|
|
2176
|
-
realTimeMemorySize: 8192,
|
|
2177
|
-
/**
|
|
2178
|
-
* Number of random number generators
|
|
2179
|
-
* Each synth can have its own RNG for reproducible randomness
|
|
2180
|
-
* Default: 64 (matching SuperCollider default)
|
|
2181
|
-
*/
|
|
2182
|
-
numRGens: 64,
|
|
2183
|
-
/**
|
|
2184
|
-
* Clock source mode
|
|
2185
|
-
* false = Externally clocked (driven by AudioWorklet process() callback)
|
|
2186
|
-
* true = Internally clocked (not applicable in WebAudio context)
|
|
2187
|
-
* Note: In SC terminology, this is "NRT mode" but we're still doing real-time audio
|
|
2188
|
-
* Default: false (SuperSonic is always externally clocked by AudioWorklet)
|
|
2189
|
-
*/
|
|
2190
|
-
realTime: false,
|
|
2191
|
-
/**
|
|
2192
|
-
* Memory locking (mlock)
|
|
2193
|
-
* Not applicable in WebAssembly/browser environment
|
|
2194
|
-
* Default: false
|
|
2195
|
-
*/
|
|
2196
|
-
memoryLocking: false,
|
|
2197
|
-
/**
|
|
2198
|
-
* Auto-load SynthDefs from disk
|
|
2199
|
-
* 0 = don't auto-load (synths sent via /d_recv)
|
|
2200
|
-
* 1 = auto-load from plugin path
|
|
2201
|
-
* Default: 0 (SuperSonic loads synthdefs via network)
|
|
2202
|
-
*/
|
|
2203
|
-
loadGraphDefs: 0,
|
|
2204
|
-
/**
|
|
2205
|
-
* Preferred sample rate (if not specified, uses AudioContext.sampleRate)
|
|
2206
|
-
* Common values: 44100, 48000, 96000
|
|
2207
|
-
* Default: 0 (use AudioContext default, typically 48000)
|
|
2208
|
-
*/
|
|
2209
|
-
preferredSampleRate: 0,
|
|
2210
|
-
/**
|
|
2211
|
-
* Debug verbosity level
|
|
2212
|
-
* 0 = quiet, 1 = errors, 2 = warnings, 3 = info, 4 = debug
|
|
2213
|
-
* Default: 0
|
|
2214
|
-
*/
|
|
2215
|
-
verbosity: 0
|
|
2216
|
-
};
|
|
2217
|
-
|
|
2218
|
-
// js/supersonic.js
|
|
2219
|
-
var SuperSonic = class _SuperSonic {
|
|
2220
|
-
// Expose OSC utilities as static methods
|
|
2221
|
-
static osc = {
|
|
2222
|
-
encode: (message) => osc_default.writePacket(message),
|
|
2223
|
-
decode: (data, options = { metadata: false }) => osc_default.readPacket(data, options)
|
|
2224
|
-
};
|
|
2225
|
-
// Private implementation
|
|
2226
|
-
#audioContext;
|
|
2227
|
-
#workletNode;
|
|
2228
|
-
#osc;
|
|
2229
|
-
#wasmMemory;
|
|
2230
|
-
#sharedBuffer;
|
|
2231
|
-
#ringBufferBase;
|
|
2232
|
-
#bufferConstants;
|
|
2233
|
-
#bufferManager;
|
|
2234
|
-
#driftOffsetTimer;
|
|
2235
|
-
#syncListeners;
|
|
2236
|
-
#initialNTPStartTime;
|
|
2237
|
-
#sampleBaseURL;
|
|
2238
|
-
#synthdefBaseURL;
|
|
2239
|
-
#audioPathMap;
|
|
2240
|
-
#initialized;
|
|
2241
|
-
#initializing;
|
|
2242
|
-
#capabilities;
|
|
2243
|
-
// Runtime metrics (private counters)
|
|
2244
|
-
#metrics_messagesSent = 0;
|
|
2245
|
-
#metrics_messagesReceived = 0;
|
|
2246
|
-
#metrics_errors = 0;
|
|
2247
|
-
#metricsIntervalId = null;
|
|
2248
|
-
#metricsGatherInProgress = false;
|
|
2249
|
-
constructor(options = {}) {
|
|
2250
|
-
this.#initialized = false;
|
|
2251
|
-
this.#initializing = false;
|
|
2252
|
-
this.#capabilities = {};
|
|
2253
|
-
this.#sharedBuffer = null;
|
|
2254
|
-
this.#ringBufferBase = null;
|
|
2255
|
-
this.#bufferConstants = null;
|
|
2256
|
-
this.#audioContext = null;
|
|
2257
|
-
this.#workletNode = null;
|
|
2258
|
-
this.#osc = null;
|
|
2259
|
-
this.#bufferManager = null;
|
|
2260
|
-
this.loadedSynthDefs = /* @__PURE__ */ new Set();
|
|
2261
|
-
this.onOSC = null;
|
|
2262
|
-
this.onMessage = null;
|
|
2263
|
-
this.onMessageSent = null;
|
|
2264
|
-
this.onMetricsUpdate = null;
|
|
2265
|
-
this.onDebugMessage = null;
|
|
2266
|
-
this.onInitialized = null;
|
|
2267
|
-
this.onError = null;
|
|
2268
|
-
if (!options.workerBaseURL || !options.wasmBaseURL) {
|
|
2269
|
-
throw new Error('SuperSonic requires workerBaseURL and wasmBaseURL options. Example:\nnew SuperSonic({\n workerBaseURL: "/supersonic/workers/",\n wasmBaseURL: "/supersonic/wasm/"\n})');
|
|
2270
|
-
}
|
|
2271
|
-
const workerBaseURL = options.workerBaseURL;
|
|
2272
|
-
const wasmBaseURL = options.wasmBaseURL;
|
|
2273
|
-
const worldOptions = { ...defaultWorldOptions, ...options.scsynthOptions };
|
|
2274
|
-
this.config = {
|
|
2275
|
-
wasmUrl: options.wasmUrl || wasmBaseURL + "scsynth-nrt.wasm",
|
|
2276
|
-
wasmBaseURL,
|
|
2277
|
-
workletUrl: options.workletUrl || workerBaseURL + "scsynth_audio_worklet.js",
|
|
2278
|
-
workerBaseURL,
|
|
2279
|
-
development: false,
|
|
2280
|
-
audioContextOptions: {
|
|
2281
|
-
latencyHint: "interactive",
|
|
2282
|
-
// hint to push for lowest latency possible
|
|
2283
|
-
sampleRate: 48e3
|
|
2284
|
-
// only requested rate - actual rate is determined by hardware
|
|
2285
|
-
},
|
|
2286
|
-
// Build-time memory layout (constant)
|
|
2287
|
-
memory: MemoryLayout,
|
|
2288
|
-
// Runtime world options (merged defaults + user overrides)
|
|
2289
|
-
worldOptions
|
|
2290
|
-
};
|
|
2291
|
-
this.#sampleBaseURL = options.sampleBaseURL || null;
|
|
2292
|
-
this.#synthdefBaseURL = options.synthdefBaseURL || null;
|
|
2293
|
-
this.#audioPathMap = options.audioPathMap || {};
|
|
2294
|
-
this.bootStats = {
|
|
2295
|
-
initStartTime: null,
|
|
2296
|
-
initDuration: null
|
|
2297
|
-
};
|
|
2298
|
-
}
|
|
2299
|
-
/**
|
|
2300
|
-
* Get initialization status (read-only)
|
|
2301
|
-
*/
|
|
2302
|
-
get initialized() {
|
|
2303
|
-
return this.#initialized;
|
|
2304
|
-
}
|
|
2305
|
-
/**
|
|
2306
|
-
* Get initialization in-progress status (read-only)
|
|
2307
|
-
*/
|
|
2308
|
-
get initializing() {
|
|
2309
|
-
return this.#initializing;
|
|
2310
|
-
}
|
|
2311
|
-
/**
|
|
2312
|
-
* Get browser capabilities (read-only)
|
|
2313
|
-
*/
|
|
2314
|
-
get capabilities() {
|
|
2315
|
-
return this.#capabilities;
|
|
2316
|
-
}
|
|
2317
|
-
/**
|
|
2318
|
-
* Set and validate browser capabilities for required features
|
|
2319
|
-
*/
|
|
2320
|
-
setAndValidateCapabilities() {
|
|
2321
|
-
this.#capabilities = {
|
|
2322
|
-
audioWorklet: typeof AudioWorklet !== "undefined",
|
|
2323
|
-
sharedArrayBuffer: typeof SharedArrayBuffer !== "undefined",
|
|
2324
|
-
crossOriginIsolated: window.crossOriginIsolated === true,
|
|
2325
|
-
atomics: typeof Atomics !== "undefined",
|
|
2326
|
-
webWorker: typeof Worker !== "undefined"
|
|
2327
|
-
};
|
|
2328
|
-
const required = [
|
|
2329
|
-
"audioWorklet",
|
|
2330
|
-
"sharedArrayBuffer",
|
|
2331
|
-
"crossOriginIsolated",
|
|
2332
|
-
"atomics",
|
|
2333
|
-
"webWorker"
|
|
2334
|
-
];
|
|
2335
|
-
const missing = required.filter((f) => !this.#capabilities[f]);
|
|
2336
|
-
if (missing.length > 0) {
|
|
2337
|
-
const error = new Error(`Missing required features: ${missing.join(", ")}`);
|
|
2338
|
-
if (!this.#capabilities.crossOriginIsolated) {
|
|
2339
|
-
if (this.#capabilities.sharedArrayBuffer) {
|
|
2340
|
-
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";
|
|
2341
|
-
} else {
|
|
2342
|
-
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";
|
|
2343
|
-
}
|
|
2344
|
-
}
|
|
2345
|
-
throw error;
|
|
2346
|
-
}
|
|
2347
|
-
return this.#capabilities;
|
|
2348
|
-
}
|
|
2349
|
-
/**
|
|
2350
|
-
* Merge user-provided world options with defaults
|
|
2351
|
-
* @private
|
|
2352
|
-
*/
|
|
2353
|
-
/**
|
|
2354
|
-
* Initialize shared WebAssembly memory
|
|
2355
|
-
*/
|
|
2356
|
-
#initializeSharedMemory() {
|
|
2357
|
-
const memConfig = this.config.memory;
|
|
2358
|
-
this.#wasmMemory = new WebAssembly.Memory({
|
|
2359
|
-
initial: memConfig.totalPages,
|
|
2360
|
-
maximum: memConfig.totalPages,
|
|
2361
|
-
shared: true
|
|
2362
|
-
});
|
|
2363
|
-
this.#sharedBuffer = this.#wasmMemory.buffer;
|
|
2364
|
-
}
|
|
2365
|
-
#initializeAudioContext() {
|
|
2366
|
-
this.#audioContext = new AudioContext(this.config.audioContextOptions);
|
|
2367
|
-
return this.#audioContext;
|
|
2368
|
-
}
|
|
2369
|
-
#initializeBufferManager() {
|
|
2370
|
-
this.#bufferManager = new BufferManager({
|
|
2371
|
-
audioContext: this.#audioContext,
|
|
2372
|
-
sharedBuffer: this.#sharedBuffer,
|
|
2373
|
-
bufferPoolConfig: {
|
|
2374
|
-
start: this.config.memory.bufferPoolOffset,
|
|
2375
|
-
size: this.config.memory.bufferPoolSize
|
|
2376
|
-
},
|
|
2377
|
-
sampleBaseURL: this.#sampleBaseURL,
|
|
2378
|
-
audioPathMap: this.#audioPathMap,
|
|
2379
|
-
maxBuffers: this.config.worldOptions.numBuffers
|
|
2380
|
-
});
|
|
2381
|
-
}
|
|
2382
|
-
async #loadWasmManifest() {
|
|
2383
|
-
const manifestUrl = this.config.wasmBaseURL + "manifest.json";
|
|
2384
|
-
try {
|
|
2385
|
-
const response = await fetch(manifestUrl);
|
|
2386
|
-
if (!response.ok) {
|
|
2387
|
-
console.warn(`[SuperSonic] WASM manifest not found (${response.status}), using default`);
|
|
2388
|
-
return;
|
|
2389
|
-
}
|
|
2390
|
-
const manifest = await response.json();
|
|
2391
|
-
this.config.wasmUrl = this.config.wasmBaseURL + manifest.wasmFile;
|
|
2392
|
-
console.log(`[SuperSonic] WASM: ${manifest.wasmFile} (${manifest.buildId}, git: ${manifest.gitHash})`);
|
|
2393
|
-
} catch (error) {
|
|
2394
|
-
console.warn("[SuperSonic] Failed to load WASM manifest, using default");
|
|
2395
|
-
}
|
|
2396
|
-
}
|
|
2397
|
-
/**
|
|
2398
|
-
* Load WASM binary from network
|
|
2399
|
-
*/
|
|
2400
|
-
async #loadWasm() {
|
|
2401
|
-
await this.#loadWasmManifest();
|
|
2402
|
-
const wasmResponse = await fetch(this.config.wasmUrl);
|
|
2403
|
-
if (!wasmResponse.ok) {
|
|
2404
|
-
throw new Error(`Failed to load WASM: ${wasmResponse.status} ${wasmResponse.statusText}`);
|
|
2405
|
-
}
|
|
2406
|
-
return await wasmResponse.arrayBuffer();
|
|
2407
|
-
}
|
|
2408
|
-
/**
|
|
2409
|
-
* Initialize AudioWorklet with WASM
|
|
2410
|
-
*/
|
|
2411
|
-
async #initializeAudioWorklet(wasmBytes) {
|
|
2412
|
-
await this.#audioContext.audioWorklet.addModule(this.config.workletUrl);
|
|
2413
|
-
this.#workletNode = new AudioWorkletNode(this.#audioContext, "scsynth-processor", {
|
|
2414
|
-
numberOfInputs: 0,
|
|
2415
|
-
numberOfOutputs: 1,
|
|
2416
|
-
outputChannelCount: [2]
|
|
2417
|
-
});
|
|
2418
|
-
this.#workletNode.connect(this.#audioContext.destination);
|
|
2419
|
-
this.#workletNode.port.postMessage({
|
|
2420
|
-
type: "init",
|
|
2421
|
-
sharedBuffer: this.#sharedBuffer
|
|
2422
|
-
});
|
|
2423
|
-
this.#workletNode.port.postMessage({
|
|
2424
|
-
type: "loadWasm",
|
|
2425
|
-
wasmBytes,
|
|
2426
|
-
wasmMemory: this.#wasmMemory,
|
|
2427
|
-
worldOptions: this.config.worldOptions,
|
|
2428
|
-
sampleRate: this.#audioContext.sampleRate
|
|
2429
|
-
// Pass actual AudioContext sample rate
|
|
2430
|
-
});
|
|
2431
|
-
await this.#waitForWorkletInit();
|
|
2432
|
-
}
|
|
2433
|
-
/**
|
|
2434
|
-
* Initialize OSC communication layer
|
|
2435
|
-
*/
|
|
2436
|
-
async #initializeOSC() {
|
|
2437
|
-
this.#osc = new ScsynthOSC(this.config.workerBaseURL);
|
|
2438
|
-
this.#osc.onRawOSC((msg) => {
|
|
2439
|
-
if (this.onOSC) {
|
|
2440
|
-
this.onOSC(msg);
|
|
2441
|
-
}
|
|
2442
|
-
});
|
|
2443
|
-
this.#osc.onParsedOSC((msg) => {
|
|
2444
|
-
if (msg.address === "/buffer/freed") {
|
|
2445
|
-
this.#bufferManager?.handleBufferFreed(msg.args);
|
|
2446
|
-
} else if (msg.address === "/buffer/allocated") {
|
|
2447
|
-
this.#bufferManager?.handleBufferAllocated(msg.args);
|
|
2448
|
-
} else if (msg.address === "/synced" && msg.args.length > 0) {
|
|
2449
|
-
const syncId = msg.args[0];
|
|
2450
|
-
if (this.#syncListeners && this.#syncListeners.has(syncId)) {
|
|
2451
|
-
const listener = this.#syncListeners.get(syncId);
|
|
2452
|
-
listener(msg);
|
|
2453
|
-
}
|
|
2454
|
-
}
|
|
2455
|
-
if (this.onMessage) {
|
|
2456
|
-
this.#metrics_messagesReceived++;
|
|
2457
|
-
this.onMessage(msg);
|
|
2458
|
-
}
|
|
2459
|
-
});
|
|
2460
|
-
this.#osc.onDebugMessage((msg) => {
|
|
2461
|
-
if (this.onDebugMessage) {
|
|
2462
|
-
this.onDebugMessage(msg);
|
|
2463
|
-
}
|
|
2464
|
-
});
|
|
2465
|
-
this.#osc.onError((error, workerName) => {
|
|
2466
|
-
console.error(`[SuperSonic] ${workerName} error:`, error);
|
|
2467
|
-
this.#metrics_errors++;
|
|
2468
|
-
if (this.onError) {
|
|
2469
|
-
this.onError(new Error(`${workerName}: ${error}`));
|
|
2470
|
-
}
|
|
2471
|
-
});
|
|
2472
|
-
await this.#osc.init(this.#sharedBuffer, this.#ringBufferBase, this.#bufferConstants);
|
|
2473
|
-
}
|
|
2474
|
-
/**
|
|
2475
|
-
* Complete initialization and trigger callbacks
|
|
2476
|
-
*/
|
|
2477
|
-
#finishInitialization() {
|
|
2478
|
-
this.#initialized = true;
|
|
2479
|
-
this.#initializing = false;
|
|
2480
|
-
this.bootStats.initDuration = performance.now() - this.bootStats.initStartTime;
|
|
2481
|
-
console.log(`[SuperSonic] Initialization complete in ${this.bootStats.initDuration.toFixed(2)}ms`);
|
|
2482
|
-
if (this.onInitialized) {
|
|
2483
|
-
this.onInitialized({
|
|
2484
|
-
capabilities: this.#capabilities,
|
|
2485
|
-
bootStats: this.bootStats
|
|
2486
|
-
});
|
|
2487
|
-
}
|
|
2488
|
-
}
|
|
2489
|
-
/**
|
|
2490
|
-
* Initialize the audio worklet system
|
|
2491
|
-
* @param {Object} config - Optional configuration overrides
|
|
2492
|
-
* @param {boolean} config.development - Use cache-busted WASM files (default: false)
|
|
2493
|
-
* @param {string} config.wasmUrl - Custom WASM URL
|
|
2494
|
-
* @param {string} config.workletUrl - Custom worklet URL
|
|
2495
|
-
* @param {Object} config.audioContextOptions - AudioContext options
|
|
2496
|
-
*/
|
|
2497
|
-
async init(config = {}) {
|
|
2498
|
-
if (this.#initialized) {
|
|
2499
|
-
console.warn("[SuperSonic] Already initialized");
|
|
2500
|
-
return;
|
|
2501
|
-
}
|
|
2502
|
-
if (this.#initializing) {
|
|
2503
|
-
console.warn("[SuperSonic] Initialization already in progress");
|
|
2504
|
-
return;
|
|
2505
|
-
}
|
|
2506
|
-
this.config = {
|
|
2507
|
-
...this.config,
|
|
2508
|
-
...config,
|
|
2509
|
-
audioContextOptions: {
|
|
2510
|
-
...this.config.audioContextOptions,
|
|
2511
|
-
...config.audioContextOptions || {}
|
|
2512
|
-
}
|
|
2513
|
-
};
|
|
2514
|
-
this.#initializing = true;
|
|
2515
|
-
this.bootStats.initStartTime = performance.now();
|
|
2516
|
-
try {
|
|
2517
|
-
this.setAndValidateCapabilities();
|
|
2518
|
-
this.#initializeSharedMemory();
|
|
2519
|
-
this.#initializeAudioContext();
|
|
2520
|
-
this.#initializeBufferManager();
|
|
2521
|
-
const wasmBytes = await this.#loadWasm();
|
|
2522
|
-
await this.#initializeAudioWorklet(wasmBytes);
|
|
2523
|
-
await this.#initializeOSC();
|
|
2524
|
-
this.#setupMessageHandlers();
|
|
2525
|
-
this.#startPerformanceMonitoring();
|
|
2526
|
-
this.#finishInitialization();
|
|
2527
|
-
} catch (error) {
|
|
2528
|
-
this.#initializing = false;
|
|
2529
|
-
console.error("[SuperSonic] Initialization failed:", error);
|
|
2530
|
-
if (this.onError) {
|
|
2531
|
-
this.onError(error);
|
|
2532
|
-
}
|
|
2533
|
-
throw error;
|
|
2534
|
-
}
|
|
2535
|
-
}
|
|
2536
|
-
/**
|
|
2537
|
-
* Wait for AudioWorklet to initialize
|
|
2538
|
-
*/
|
|
2539
|
-
#waitForWorkletInit() {
|
|
2540
|
-
return new Promise((resolve, reject) => {
|
|
2541
|
-
const timeout = setTimeout(() => {
|
|
2542
|
-
reject(new Error("AudioWorklet initialization timeout"));
|
|
2543
|
-
}, 5e3);
|
|
2544
|
-
const messageHandler = (event) => {
|
|
2545
|
-
if (event.data.type === "debug") {
|
|
2546
|
-
return;
|
|
2547
|
-
}
|
|
2548
|
-
if (event.data.type === "error") {
|
|
2549
|
-
console.error("[AudioWorklet] Error:", event.data.error);
|
|
2550
|
-
clearTimeout(timeout);
|
|
2551
|
-
this.#workletNode.port.removeEventListener("message", messageHandler);
|
|
2552
|
-
reject(new Error(event.data.error || "AudioWorklet error"));
|
|
2553
|
-
return;
|
|
2554
|
-
}
|
|
2555
|
-
if (event.data.type === "initialized") {
|
|
2556
|
-
clearTimeout(timeout);
|
|
2557
|
-
this.#workletNode.port.removeEventListener("message", messageHandler);
|
|
2558
|
-
if (event.data.success) {
|
|
2559
|
-
if (event.data.ringBufferBase !== void 0) {
|
|
2560
|
-
this.#ringBufferBase = event.data.ringBufferBase;
|
|
2561
|
-
} else {
|
|
2562
|
-
console.warn("[SuperSonic] Warning: ringBufferBase not provided by worklet");
|
|
2563
|
-
}
|
|
2564
|
-
if (event.data.bufferConstants !== void 0) {
|
|
2565
|
-
console.log("[SuperSonic] Received bufferConstants from worklet");
|
|
2566
|
-
this.#bufferConstants = event.data.bufferConstants;
|
|
2567
|
-
console.log("[SuperSonic] Initializing NTP timing");
|
|
2568
|
-
this.initializeNTPTiming();
|
|
2569
|
-
this.#startDriftOffsetTimer();
|
|
2570
|
-
} else {
|
|
2571
|
-
console.warn("[SuperSonic] Warning: bufferConstants not provided by worklet");
|
|
2572
|
-
}
|
|
2573
|
-
console.log("[SuperSonic] Calling resolve() for worklet initialization");
|
|
2574
|
-
resolve();
|
|
2575
|
-
} else {
|
|
2576
|
-
reject(new Error(event.data.error || "AudioWorklet initialization failed"));
|
|
2577
|
-
}
|
|
2578
|
-
}
|
|
2579
|
-
};
|
|
2580
|
-
this.#workletNode.port.addEventListener("message", messageHandler);
|
|
2581
|
-
this.#workletNode.port.start();
|
|
2582
|
-
});
|
|
2583
|
-
}
|
|
2584
|
-
/**
|
|
2585
|
-
* Set up message handlers for worklet
|
|
2586
|
-
*/
|
|
2587
|
-
#setupMessageHandlers() {
|
|
2588
|
-
this.#workletNode.port.onmessage = (event) => {
|
|
2589
|
-
const { data } = event;
|
|
2590
|
-
switch (data.type) {
|
|
2591
|
-
case "error":
|
|
2592
|
-
console.error("[Worklet] Error:", data.error);
|
|
2593
|
-
if (data.diagnostics) {
|
|
2594
|
-
console.error("[Worklet] Diagnostics:", data.diagnostics);
|
|
2595
|
-
console.table(data.diagnostics);
|
|
2596
|
-
}
|
|
2597
|
-
this.#metrics_errors++;
|
|
2598
|
-
if (this.onError) {
|
|
2599
|
-
this.onError(new Error(data.error));
|
|
2600
|
-
}
|
|
2601
|
-
break;
|
|
2602
|
-
case "process_debug":
|
|
2603
|
-
break;
|
|
2604
|
-
case "debug":
|
|
2605
|
-
break;
|
|
2606
|
-
case "console":
|
|
2607
|
-
if (this.onConsoleMessage) {
|
|
2608
|
-
this.onConsoleMessage(data.message);
|
|
2609
|
-
}
|
|
2610
|
-
break;
|
|
2611
|
-
case "version":
|
|
2612
|
-
if (this.onVersion) {
|
|
2613
|
-
this.onVersion(data.version);
|
|
2614
|
-
}
|
|
2615
|
-
break;
|
|
2616
|
-
}
|
|
2617
|
-
};
|
|
2618
|
-
}
|
|
2619
|
-
/**
|
|
2620
|
-
* Get metrics from SharedArrayBuffer (worklet metrics written by WASM)
|
|
2621
|
-
* @returns {Object|null}
|
|
2622
|
-
* @private
|
|
2623
|
-
*/
|
|
2624
|
-
#getWorkletMetrics() {
|
|
2625
|
-
if (!this.#sharedBuffer || !this.#bufferConstants || !this.#ringBufferBase) {
|
|
2626
|
-
return null;
|
|
2627
|
-
}
|
|
2628
|
-
const metricsBase = this.#ringBufferBase + this.#bufferConstants.METRICS_START;
|
|
2629
|
-
const metricsCount = this.#bufferConstants.METRICS_SIZE / 4;
|
|
2630
|
-
const metricsView = new Uint32Array(this.#sharedBuffer, metricsBase, metricsCount);
|
|
2631
|
-
return {
|
|
2632
|
-
processCount: Atomics.load(metricsView, 0),
|
|
2633
|
-
// PROCESS_COUNT offset / 4
|
|
2634
|
-
bufferOverruns: Atomics.load(metricsView, 1),
|
|
2635
|
-
// BUFFER_OVERRUNS offset / 4
|
|
2636
|
-
messagesProcessed: Atomics.load(metricsView, 2),
|
|
2637
|
-
// MESSAGES_PROCESSED offset / 4
|
|
2638
|
-
messagesDropped: Atomics.load(metricsView, 3),
|
|
2639
|
-
// MESSAGES_DROPPED offset / 4
|
|
2640
|
-
schedulerQueueDepth: Atomics.load(metricsView, 4),
|
|
2641
|
-
// SCHEDULER_QUEUE_DEPTH offset / 4
|
|
2642
|
-
schedulerQueueMax: Atomics.load(metricsView, 5),
|
|
2643
|
-
// SCHEDULER_QUEUE_MAX offset / 4
|
|
2644
|
-
schedulerQueueDropped: Atomics.load(metricsView, 6)
|
|
2645
|
-
// SCHEDULER_QUEUE_DROPPED offset / 4
|
|
2646
|
-
};
|
|
2647
|
-
}
|
|
2648
|
-
/**
|
|
2649
|
-
* Get buffer usage statistics from SAB head/tail pointers
|
|
2650
|
-
* @returns {Object|null}
|
|
2651
|
-
* @private
|
|
2652
|
-
*/
|
|
2653
|
-
#getBufferUsage() {
|
|
2654
|
-
if (!this.#sharedBuffer || !this.#bufferConstants || !this.#ringBufferBase) {
|
|
2655
|
-
return null;
|
|
2656
|
-
}
|
|
2657
|
-
const atomicView = new Int32Array(this.#sharedBuffer);
|
|
2658
|
-
const controlBase = this.#ringBufferBase + this.#bufferConstants.CONTROL_START;
|
|
2659
|
-
const inHead = Atomics.load(atomicView, (controlBase + 0) / 4);
|
|
2660
|
-
const inTail = Atomics.load(atomicView, (controlBase + 4) / 4);
|
|
2661
|
-
const outHead = Atomics.load(atomicView, (controlBase + 8) / 4);
|
|
2662
|
-
const outTail = Atomics.load(atomicView, (controlBase + 12) / 4);
|
|
2663
|
-
const debugHead = Atomics.load(atomicView, (controlBase + 16) / 4);
|
|
2664
|
-
const debugTail = Atomics.load(atomicView, (controlBase + 20) / 4);
|
|
2665
|
-
const inUsed = (inHead - inTail + this.#bufferConstants.IN_BUFFER_SIZE) % this.#bufferConstants.IN_BUFFER_SIZE;
|
|
2666
|
-
const outUsed = (outHead - outTail + this.#bufferConstants.OUT_BUFFER_SIZE) % this.#bufferConstants.OUT_BUFFER_SIZE;
|
|
2667
|
-
const debugUsed = (debugHead - debugTail + this.#bufferConstants.DEBUG_BUFFER_SIZE) % this.#bufferConstants.DEBUG_BUFFER_SIZE;
|
|
2668
|
-
return {
|
|
2669
|
-
inBufferUsed: {
|
|
2670
|
-
bytes: inUsed,
|
|
2671
|
-
percentage: Math.round(inUsed / this.#bufferConstants.IN_BUFFER_SIZE * 100)
|
|
2672
|
-
},
|
|
2673
|
-
outBufferUsed: {
|
|
2674
|
-
bytes: outUsed,
|
|
2675
|
-
percentage: Math.round(outUsed / this.#bufferConstants.OUT_BUFFER_SIZE * 100)
|
|
2676
|
-
},
|
|
2677
|
-
debugBufferUsed: {
|
|
2678
|
-
bytes: debugUsed,
|
|
2679
|
-
percentage: Math.round(debugUsed / this.#bufferConstants.DEBUG_BUFFER_SIZE * 100)
|
|
2680
|
-
}
|
|
2681
|
-
};
|
|
2682
|
-
}
|
|
2683
|
-
/**
|
|
2684
|
-
* Get OSC worker metrics from SharedArrayBuffer (written by OSC workers)
|
|
2685
|
-
* @returns {Object|null}
|
|
2686
|
-
* @private
|
|
2687
|
-
*/
|
|
2688
|
-
#getOSCMetrics() {
|
|
2689
|
-
if (!this.#sharedBuffer || !this.#bufferConstants || !this.#ringBufferBase) {
|
|
2690
|
-
return null;
|
|
2691
|
-
}
|
|
2692
|
-
const metricsBase = this.#ringBufferBase + this.#bufferConstants.METRICS_START;
|
|
2693
|
-
const metricsCount = this.#bufferConstants.METRICS_SIZE / 4;
|
|
2694
|
-
const metricsView = new Uint32Array(this.#sharedBuffer, metricsBase, metricsCount);
|
|
2695
|
-
return {
|
|
2696
|
-
// OSC Out (prescheduler) - offsets 7-18
|
|
2697
|
-
preschedulerPending: metricsView[7],
|
|
2698
|
-
preschedulerPeak: metricsView[8],
|
|
2699
|
-
preschedulerSent: metricsView[9],
|
|
2700
|
-
bundlesDropped: metricsView[10],
|
|
2701
|
-
retriesSucceeded: metricsView[11],
|
|
2702
|
-
retriesFailed: metricsView[12],
|
|
2703
|
-
bundlesScheduled: metricsView[13],
|
|
2704
|
-
eventsCancelled: metricsView[14],
|
|
2705
|
-
totalDispatches: metricsView[15],
|
|
2706
|
-
messagesRetried: metricsView[16],
|
|
2707
|
-
retryQueueSize: metricsView[17],
|
|
2708
|
-
retryQueueMax: metricsView[18],
|
|
2709
|
-
// OSC In - offsets 19-22
|
|
2710
|
-
oscInMessagesReceived: metricsView[19],
|
|
2711
|
-
oscInDroppedMessages: metricsView[20],
|
|
2712
|
-
oscInWakeups: metricsView[21],
|
|
2713
|
-
oscInTimeouts: metricsView[22],
|
|
2714
|
-
// Debug - offsets 23-26
|
|
2715
|
-
debugMessagesReceived: metricsView[23],
|
|
2716
|
-
debugWakeups: metricsView[24],
|
|
2717
|
-
debugTimeouts: metricsView[25],
|
|
2718
|
-
debugBytesRead: metricsView[26]
|
|
2719
|
-
};
|
|
2720
|
-
}
|
|
2721
|
-
/**
|
|
2722
|
-
* Gather metrics from all sources (worklet, OSC, internal counters)
|
|
2723
|
-
* All metrics are read synchronously from SAB
|
|
2724
|
-
* @returns {SuperSonicMetrics}
|
|
2725
|
-
* @private
|
|
2726
|
-
*/
|
|
2727
|
-
#gatherMetrics() {
|
|
2728
|
-
const startTime = performance.now();
|
|
2729
|
-
const metrics = {
|
|
2730
|
-
// SuperSonic counters (in-memory, fast)
|
|
2731
|
-
messagesSent: this.#metrics_messagesSent,
|
|
2732
|
-
messagesReceived: this.#metrics_messagesReceived,
|
|
2733
|
-
errors: this.#metrics_errors
|
|
2734
|
-
};
|
|
2735
|
-
const workletMetrics = this.#getWorkletMetrics();
|
|
2736
|
-
if (workletMetrics) {
|
|
2737
|
-
Object.assign(metrics, workletMetrics);
|
|
2738
|
-
}
|
|
2739
|
-
const bufferUsage = this.#getBufferUsage();
|
|
2740
|
-
if (bufferUsage) {
|
|
2741
|
-
Object.assign(metrics, bufferUsage);
|
|
2742
|
-
}
|
|
2743
|
-
const oscMetrics = this.#getOSCMetrics();
|
|
2744
|
-
if (oscMetrics) {
|
|
2745
|
-
Object.assign(metrics, oscMetrics);
|
|
2746
|
-
}
|
|
2747
|
-
const totalDuration = performance.now() - startTime;
|
|
2748
|
-
if (totalDuration > 1) {
|
|
2749
|
-
console.warn(`[SuperSonic] Slow metrics gathering: ${totalDuration.toFixed(2)}ms`);
|
|
2750
|
-
}
|
|
2751
|
-
return metrics;
|
|
2752
|
-
}
|
|
2753
|
-
/**
|
|
2754
|
-
* Start performance monitoring - gathers metrics from all sources
|
|
2755
|
-
* and calls onMetricsUpdate with consolidated snapshot
|
|
2756
|
-
*/
|
|
2757
|
-
#startPerformanceMonitoring() {
|
|
2758
|
-
if (this.#metricsIntervalId) {
|
|
2759
|
-
clearInterval(this.#metricsIntervalId);
|
|
2760
|
-
}
|
|
2761
|
-
this.#metricsIntervalId = setInterval(() => {
|
|
2762
|
-
if (!this.onMetricsUpdate) return;
|
|
2763
|
-
if (this.#metricsGatherInProgress) {
|
|
2764
|
-
console.warn("[SuperSonic] Metrics gathering took >100ms, skipping this interval");
|
|
2765
|
-
return;
|
|
2766
|
-
}
|
|
2767
|
-
this.#metricsGatherInProgress = true;
|
|
2768
|
-
try {
|
|
2769
|
-
const metrics = this.#gatherMetrics();
|
|
2770
|
-
this.onMetricsUpdate(metrics);
|
|
2771
|
-
} catch (error) {
|
|
2772
|
-
console.error("[SuperSonic] Metrics gathering failed:", error);
|
|
2773
|
-
} finally {
|
|
2774
|
-
this.#metricsGatherInProgress = false;
|
|
2775
|
-
}
|
|
2776
|
-
}, 100);
|
|
2777
|
-
}
|
|
2778
|
-
/**
|
|
2779
|
-
* Stop performance monitoring
|
|
2780
|
-
* @private
|
|
2781
|
-
*/
|
|
2782
|
-
#stopPerformanceMonitoring() {
|
|
2783
|
-
if (this.#metricsIntervalId) {
|
|
2784
|
-
clearInterval(this.#metricsIntervalId);
|
|
2785
|
-
this.#metricsIntervalId = null;
|
|
2786
|
-
}
|
|
2787
|
-
}
|
|
2788
|
-
/**
|
|
2789
|
-
* Send OSC message with simplified syntax (auto-detects types)
|
|
2790
|
-
* @param {string} address - OSC address
|
|
2791
|
-
* @param {...*} args - Arguments (numbers, strings, Uint8Array)
|
|
2792
|
-
* @example
|
|
2793
|
-
* sonic.send('/notify', 1);
|
|
2794
|
-
* sonic.send('/s_new', 'sonic-pi-beep', -1, 0, 0);
|
|
2795
|
-
* sonic.send('/n_set', 1000, 'freq', 440.0, 'amp', 0.5);
|
|
2796
|
-
*/
|
|
2797
|
-
async send(address, ...args) {
|
|
2798
|
-
this.#ensureInitialized("send OSC messages");
|
|
2799
|
-
const oscArgs = args.map((arg) => {
|
|
2800
|
-
if (typeof arg === "string") {
|
|
2801
|
-
return { type: "s", value: arg };
|
|
2802
|
-
} else if (typeof arg === "number") {
|
|
2803
|
-
return { type: Number.isInteger(arg) ? "i" : "f", value: arg };
|
|
2804
|
-
} else if (arg instanceof Uint8Array || arg instanceof ArrayBuffer) {
|
|
2805
|
-
return { type: "b", value: arg instanceof ArrayBuffer ? new Uint8Array(arg) : arg };
|
|
2806
|
-
} else {
|
|
2807
|
-
throw new Error(`Unsupported argument type: ${typeof arg}`);
|
|
2808
|
-
}
|
|
2809
|
-
});
|
|
2810
|
-
const message = { address, args: oscArgs };
|
|
2811
|
-
const oscData = _SuperSonic.osc.encode(message);
|
|
2812
|
-
return this.sendOSC(oscData);
|
|
2813
|
-
}
|
|
2814
|
-
#ensureInitialized(actionDescription = "perform this operation") {
|
|
2815
|
-
if (!this.#initialized) {
|
|
2816
|
-
throw new Error(`SuperSonic not initialized. Call init() before attempting to ${actionDescription}.`);
|
|
2817
|
-
}
|
|
2818
|
-
}
|
|
2819
|
-
/**
|
|
2820
|
-
* Send pre-encoded OSC bytes to scsynth
|
|
2821
|
-
* @param {ArrayBuffer|Uint8Array} oscData - Pre-encoded OSC data
|
|
2822
|
-
* @param {Object} options - Send options
|
|
2823
|
-
*/
|
|
2824
|
-
async sendOSC(oscData, options = {}) {
|
|
2825
|
-
this.#ensureInitialized("send OSC data");
|
|
2826
|
-
const uint8Data = this.#toUint8Array(oscData);
|
|
2827
|
-
const preparedData = await this.#prepareOutboundPacket(uint8Data);
|
|
2828
|
-
this.#metrics_messagesSent++;
|
|
2829
|
-
if (this.onMessageSent) {
|
|
2830
|
-
this.onMessageSent(preparedData);
|
|
2831
|
-
}
|
|
2832
|
-
const timing = this.#calculateBundleWait(preparedData);
|
|
2833
|
-
const sendOptions = { ...options };
|
|
2834
|
-
if (timing) {
|
|
2835
|
-
sendOptions.audioTimeS = timing.audioTimeS;
|
|
2836
|
-
sendOptions.currentTimeS = timing.currentTimeS;
|
|
2837
|
-
}
|
|
2838
|
-
this.#osc.send(preparedData, sendOptions);
|
|
2839
|
-
}
|
|
2840
|
-
/**
|
|
2841
|
-
* Get AudioContext instance (read-only)
|
|
2842
|
-
* @returns {AudioContext} The AudioContext instance
|
|
2843
|
-
*/
|
|
2844
|
-
get audioContext() {
|
|
2845
|
-
return this.#audioContext;
|
|
2846
|
-
}
|
|
2847
|
-
/**
|
|
2848
|
-
* Get AudioWorkletNode instance (read-only)
|
|
2849
|
-
* @returns {AudioWorkletNode} The AudioWorkletNode instance
|
|
2850
|
-
*/
|
|
2851
|
-
get workletNode() {
|
|
2852
|
-
return this.#workletNode;
|
|
2853
|
-
}
|
|
2854
|
-
/**
|
|
2855
|
-
* Get ScsynthOSC instance (read-only)
|
|
2856
|
-
* @returns {ScsynthOSC} The OSC communication layer instance
|
|
2857
|
-
*/
|
|
2858
|
-
get osc() {
|
|
2859
|
-
return this.#osc;
|
|
2860
|
-
}
|
|
2861
|
-
/**
|
|
2862
|
-
* Get current status
|
|
2863
|
-
*/
|
|
2864
|
-
getStatus() {
|
|
2865
|
-
return {
|
|
2866
|
-
initialized: this.#initialized,
|
|
2867
|
-
capabilities: this.#capabilities,
|
|
2868
|
-
bootStats: this.bootStats,
|
|
2869
|
-
audioContextState: this.#audioContext?.state
|
|
2870
|
-
};
|
|
2871
|
-
}
|
|
2872
|
-
/**
|
|
2873
|
-
* Get current configuration (merged defaults + user overrides)
|
|
2874
|
-
* Useful for debugging and displaying in UI
|
|
2875
|
-
* @returns {Object} Current scsynth configuration
|
|
2876
|
-
* @example
|
|
2877
|
-
* const config = sonic.getConfig();
|
|
2878
|
-
* console.log('Buffer limit:', config.worldOptions.numBuffers);
|
|
2879
|
-
* console.log('Memory layout:', config.memory);
|
|
2880
|
-
*/
|
|
2881
|
-
getConfig() {
|
|
2882
|
-
if (!this.config) {
|
|
2883
|
-
return null;
|
|
2884
|
-
}
|
|
2885
|
-
return {
|
|
2886
|
-
memory: { ...this.config.memory },
|
|
2887
|
-
worldOptions: { ...this.config.worldOptions }
|
|
2888
|
-
};
|
|
2889
|
-
}
|
|
2890
|
-
/**
|
|
2891
|
-
* Destroy the orchestrator and clean up resources
|
|
2892
|
-
*/
|
|
2893
|
-
async destroy() {
|
|
2894
|
-
console.log("[SuperSonic] Destroying...");
|
|
2895
|
-
this.#stopDriftOffsetTimer();
|
|
2896
|
-
this.#stopPerformanceMonitoring();
|
|
2897
|
-
if (this.#osc) {
|
|
2898
|
-
this.#osc.terminate();
|
|
2899
|
-
this.#osc = null;
|
|
2900
|
-
}
|
|
2901
|
-
if (this.#workletNode) {
|
|
2902
|
-
this.#workletNode.disconnect();
|
|
2903
|
-
this.#workletNode = null;
|
|
2904
|
-
}
|
|
2905
|
-
if (this.#audioContext) {
|
|
2906
|
-
await this.#audioContext.close();
|
|
2907
|
-
this.#audioContext = null;
|
|
2908
|
-
}
|
|
2909
|
-
if (this.#bufferManager) {
|
|
2910
|
-
this.#bufferManager.destroy();
|
|
2911
|
-
this.#bufferManager = null;
|
|
2912
|
-
}
|
|
2913
|
-
this.#sharedBuffer = null;
|
|
2914
|
-
this.#initialized = false;
|
|
2915
|
-
this.loadedSynthDefs.clear();
|
|
2916
|
-
console.log("[SuperSonic] Destroyed");
|
|
2917
|
-
}
|
|
2918
|
-
/**
|
|
2919
|
-
* Get NTP start time for bundle creation.
|
|
2920
|
-
* This is the NTP timestamp when AudioContext.currentTime was 0.
|
|
2921
|
-
* Bundles should have timestamp = audioContextTime + ntpStartTime
|
|
2922
|
-
*/
|
|
2923
|
-
waitForTimeSync() {
|
|
2924
|
-
this.#ensureInitialized("wait for time sync");
|
|
2925
|
-
const ntpStartView = new Float64Array(this.#sharedBuffer, this.#ringBufferBase + this.#bufferConstants.NTP_START_TIME_START, 1);
|
|
2926
|
-
return ntpStartView[0];
|
|
2927
|
-
}
|
|
2928
|
-
/**
|
|
2929
|
-
* Load a sample into a buffer and wait for confirmation
|
|
2930
|
-
* @param {number} bufnum - Buffer number
|
|
2931
|
-
* @param {string} path - Audio file path
|
|
2932
|
-
* @returns {Promise} Resolves when buffer is ready
|
|
2933
|
-
*/
|
|
2934
|
-
async loadSample(bufnum, path, startFrame = 0, numFrames = 0) {
|
|
2935
|
-
this.#ensureInitialized("load samples");
|
|
2936
|
-
const bufferInfo = await this.#requireBufferManager().prepareFromFile({
|
|
2937
|
-
bufnum,
|
|
2938
|
-
path,
|
|
2939
|
-
startFrame,
|
|
2940
|
-
numFrames
|
|
2941
|
-
});
|
|
2942
|
-
await this.send(
|
|
2943
|
-
"/b_allocPtr",
|
|
2944
|
-
bufnum,
|
|
2945
|
-
bufferInfo.ptr,
|
|
2946
|
-
bufferInfo.numFrames,
|
|
2947
|
-
bufferInfo.numChannels,
|
|
2948
|
-
bufferInfo.sampleRate,
|
|
2949
|
-
bufferInfo.uuid
|
|
2950
|
-
);
|
|
2951
|
-
return bufferInfo.allocationComplete;
|
|
2952
|
-
}
|
|
2953
|
-
/**
|
|
2954
|
-
* Load a binary synthdef file and send it to scsynth
|
|
2955
|
-
* @param {string} path - Path or URL to the .scsyndef file
|
|
2956
|
-
* @returns {Promise<void>}
|
|
2957
|
-
* @example
|
|
2958
|
-
* await sonic.loadSynthDef('./extra/synthdefs/sonic-pi-beep.scsyndef');
|
|
2959
|
-
*/
|
|
2960
|
-
async loadSynthDef(path) {
|
|
2961
|
-
if (!this.#initialized) {
|
|
2962
|
-
throw new Error("SuperSonic not initialized. Call init() first.");
|
|
2963
|
-
}
|
|
2964
|
-
try {
|
|
2965
|
-
const response = await fetch(path);
|
|
2966
|
-
if (!response.ok) {
|
|
2967
|
-
throw new Error(`Failed to load synthdef from ${path}: ${response.status} ${response.statusText}`);
|
|
2968
|
-
}
|
|
2969
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
2970
|
-
const synthdefData = new Uint8Array(arrayBuffer);
|
|
2971
|
-
await this.send("/d_recv", synthdefData);
|
|
2972
|
-
const synthName = this.#extractSynthDefName(path);
|
|
2973
|
-
if (synthName) {
|
|
2974
|
-
this.loadedSynthDefs.add(synthName);
|
|
2975
|
-
}
|
|
2976
|
-
console.log(`[SuperSonic] Sent synthdef from ${path} (${synthdefData.length} bytes)`);
|
|
2977
|
-
} catch (error) {
|
|
2978
|
-
console.error("[SuperSonic] Failed to load synthdef:", error);
|
|
2979
|
-
throw error;
|
|
2980
|
-
}
|
|
2981
|
-
}
|
|
2982
|
-
/**
|
|
2983
|
-
* Load multiple synthdefs from a directory
|
|
2984
|
-
* @param {string[]} names - Array of synthdef names (without .scsyndef extension)
|
|
2985
|
-
* @returns {Promise<Object>} Map of name -> success/error
|
|
2986
|
-
* @example
|
|
2987
|
-
* const results = await sonic.loadSynthDefs(['sonic-pi-beep', 'sonic-pi-tb303']);
|
|
2988
|
-
*/
|
|
2989
|
-
async loadSynthDefs(names) {
|
|
2990
|
-
if (!this.#initialized) {
|
|
2991
|
-
throw new Error("SuperSonic not initialized. Call init() first.");
|
|
2992
|
-
}
|
|
2993
|
-
if (!this.#synthdefBaseURL) {
|
|
2994
|
-
throw new Error(
|
|
2995
|
-
'synthdefBaseURL not configured. Please set it in SuperSonic constructor options.\nExample: new SuperSonic({ synthdefBaseURL: "./dist/synthdefs/" })\nOr use CDN: new SuperSonic({ synthdefBaseURL: "https://unpkg.com/supersonic-scsynth-synthdefs@latest/synthdefs/" })\nOr install: npm install supersonic-scsynth-synthdefs'
|
|
2996
|
-
);
|
|
2997
|
-
}
|
|
2998
|
-
const results = {};
|
|
2999
|
-
await Promise.all(
|
|
3000
|
-
names.map(async (name) => {
|
|
3001
|
-
try {
|
|
3002
|
-
const path = `${this.#synthdefBaseURL}${name}.scsyndef`;
|
|
3003
|
-
await this.loadSynthDef(path);
|
|
3004
|
-
results[name] = { success: true };
|
|
3005
|
-
} catch (error) {
|
|
3006
|
-
console.error(`[SuperSonic] Failed to load ${name}:`, error);
|
|
3007
|
-
results[name] = { success: false, error: error.message };
|
|
3008
|
-
}
|
|
3009
|
-
})
|
|
3010
|
-
);
|
|
3011
|
-
const successCount = Object.values(results).filter((r) => r.success).length;
|
|
3012
|
-
console.log(`[SuperSonic] Sent ${successCount}/${names.length} synthdef loads`);
|
|
3013
|
-
return results;
|
|
3014
|
-
}
|
|
3015
|
-
/**
|
|
3016
|
-
* Send /sync command and wait for /synced response
|
|
3017
|
-
* Use this to ensure all previous asynchronous commands have completed
|
|
3018
|
-
* @param {number} syncId - Unique integer identifier for this sync operation
|
|
3019
|
-
* @returns {Promise<void>}
|
|
3020
|
-
* @example
|
|
3021
|
-
* await sonic.loadSynthDefs(['synth1', 'synth2']);
|
|
3022
|
-
* await sonic.sync(12345); // Wait for all synthdefs to be processed
|
|
3023
|
-
*/
|
|
3024
|
-
async sync(syncId) {
|
|
3025
|
-
if (!this.#initialized) {
|
|
3026
|
-
throw new Error("SuperSonic not initialized. Call init() first.");
|
|
3027
|
-
}
|
|
3028
|
-
if (!Number.isInteger(syncId)) {
|
|
3029
|
-
throw new Error("sync() requires an integer syncId parameter");
|
|
3030
|
-
}
|
|
3031
|
-
const syncPromise = new Promise((resolve, reject) => {
|
|
3032
|
-
const timeout = setTimeout(() => {
|
|
3033
|
-
if (this.#syncListeners) {
|
|
3034
|
-
this.#syncListeners.delete(syncId);
|
|
3035
|
-
}
|
|
3036
|
-
reject(new Error("Timeout waiting for /synced response"));
|
|
3037
|
-
}, 1e4);
|
|
3038
|
-
const messageHandler = (msg) => {
|
|
3039
|
-
clearTimeout(timeout);
|
|
3040
|
-
this.#syncListeners.delete(syncId);
|
|
3041
|
-
resolve();
|
|
3042
|
-
};
|
|
3043
|
-
if (!this.#syncListeners) {
|
|
3044
|
-
this.#syncListeners = /* @__PURE__ */ new Map();
|
|
3045
|
-
}
|
|
3046
|
-
this.#syncListeners.set(syncId, messageHandler);
|
|
3047
|
-
});
|
|
3048
|
-
await this.send("/sync", syncId);
|
|
3049
|
-
await syncPromise;
|
|
3050
|
-
}
|
|
3051
|
-
/**
|
|
3052
|
-
* Allocate memory for an audio buffer (includes guard samples)
|
|
3053
|
-
* @param {number} numSamples - Number of Float32 samples to allocate
|
|
3054
|
-
* @returns {number} Byte offset into SharedArrayBuffer, or 0 if allocation failed
|
|
3055
|
-
* @example
|
|
3056
|
-
* const bufferAddr = sonic.allocBuffer(44100); // Allocate 1 second at 44.1kHz
|
|
3057
|
-
*/
|
|
3058
|
-
allocBuffer(numSamples) {
|
|
3059
|
-
this.#ensureInitialized("allocate buffers");
|
|
3060
|
-
return this.#bufferManager.allocate(numSamples);
|
|
3061
|
-
}
|
|
3062
|
-
/**
|
|
3063
|
-
* Free a previously allocated buffer
|
|
3064
|
-
* @param {number} addr - Buffer address returned by allocBuffer()
|
|
3065
|
-
* @returns {boolean} true if freed successfully
|
|
3066
|
-
* @example
|
|
3067
|
-
* sonic.freeBuffer(bufferAddr);
|
|
3068
|
-
*/
|
|
3069
|
-
freeBuffer(addr) {
|
|
3070
|
-
this.#ensureInitialized("free buffers");
|
|
3071
|
-
return this.#bufferManager.free(addr);
|
|
3072
|
-
}
|
|
3073
|
-
/**
|
|
3074
|
-
* Get a Float32Array view of an allocated buffer
|
|
3075
|
-
* @param {number} addr - Buffer address returned by allocBuffer()
|
|
3076
|
-
* @param {number} numSamples - Number of Float32 samples
|
|
3077
|
-
* @returns {Float32Array} Typed array view into the buffer
|
|
3078
|
-
* @example
|
|
3079
|
-
* const view = sonic.getBufferView(bufferAddr, 44100);
|
|
3080
|
-
* view[0] = 1.0; // Write to buffer
|
|
3081
|
-
*/
|
|
3082
|
-
getBufferView(addr, numSamples) {
|
|
3083
|
-
this.#ensureInitialized("get buffer views");
|
|
3084
|
-
return this.#bufferManager.getView(addr, numSamples);
|
|
3085
|
-
}
|
|
3086
|
-
/**
|
|
3087
|
-
* Get buffer pool statistics
|
|
3088
|
-
* @returns {Object} Stats including total, available, used, etc.
|
|
3089
|
-
* @example
|
|
3090
|
-
* const stats = sonic.getBufferPoolStats();
|
|
3091
|
-
* console.log(`Available: ${stats.available} bytes`);
|
|
3092
|
-
*/
|
|
3093
|
-
getBufferPoolStats() {
|
|
3094
|
-
this.#ensureInitialized("get buffer pool stats");
|
|
3095
|
-
return this.#bufferManager.getStats();
|
|
3096
|
-
}
|
|
3097
|
-
getDiagnostics() {
|
|
3098
|
-
this.#ensureInitialized("get diagnostics");
|
|
3099
|
-
return {
|
|
3100
|
-
buffers: this.#bufferManager.getDiagnostics(),
|
|
3101
|
-
synthdefs: {
|
|
3102
|
-
count: this.loadedSynthDefs.size
|
|
3103
|
-
}
|
|
3104
|
-
};
|
|
3105
|
-
}
|
|
3106
|
-
/**
|
|
3107
|
-
* Initialize NTP timing (write-once)
|
|
3108
|
-
* Sets the NTP start time when AudioContext started
|
|
3109
|
-
* @private
|
|
3110
|
-
*/
|
|
3111
|
-
initializeNTPTiming() {
|
|
3112
|
-
if (!this.#bufferConstants || !this.#audioContext) {
|
|
3113
|
-
return;
|
|
3114
|
-
}
|
|
3115
|
-
const timestamp = this.#audioContext.getOutputTimestamp();
|
|
3116
|
-
const perfTimeMs = performance.timeOrigin + timestamp.performanceTime;
|
|
3117
|
-
const currentNTP = perfTimeMs / 1e3 + NTP_EPOCH_OFFSET;
|
|
3118
|
-
const ntpStartTime = currentNTP - timestamp.contextTime;
|
|
3119
|
-
const ntpStartView = new Float64Array(
|
|
3120
|
-
this.#sharedBuffer,
|
|
3121
|
-
this.#ringBufferBase + this.#bufferConstants.NTP_START_TIME_START,
|
|
3122
|
-
1
|
|
3123
|
-
);
|
|
3124
|
-
ntpStartView[0] = ntpStartTime;
|
|
3125
|
-
this.#initialNTPStartTime = ntpStartTime;
|
|
3126
|
-
console.log(`[SuperSonic] NTP timing initialized: start=${ntpStartTime.toFixed(6)}s (NTP=${currentNTP.toFixed(3)}s, contextTime=${timestamp.contextTime.toFixed(3)}s)`);
|
|
3127
|
-
}
|
|
3128
|
-
/**
|
|
3129
|
-
* Update drift offset (AudioContext → NTP drift correction)
|
|
3130
|
-
* CRITICAL: This REPLACES the drift value, does not accumulate
|
|
3131
|
-
* @private
|
|
3132
|
-
*/
|
|
3133
|
-
updateDriftOffset() {
|
|
3134
|
-
if (!this.#bufferConstants || !this.#audioContext || this.#initialNTPStartTime === void 0) {
|
|
3135
|
-
return;
|
|
3136
|
-
}
|
|
3137
|
-
const timestamp = this.#audioContext.getOutputTimestamp();
|
|
3138
|
-
const perfTimeMs = performance.timeOrigin + timestamp.performanceTime;
|
|
3139
|
-
const currentNTP = perfTimeMs / 1e3 + NTP_EPOCH_OFFSET;
|
|
3140
|
-
const expectedContextTime = currentNTP - this.#initialNTPStartTime;
|
|
3141
|
-
const driftSeconds = expectedContextTime - timestamp.contextTime;
|
|
3142
|
-
const driftMs = Math.round(driftSeconds * 1e3);
|
|
3143
|
-
const driftView = new Int32Array(
|
|
3144
|
-
this.#sharedBuffer,
|
|
3145
|
-
this.#ringBufferBase + this.#bufferConstants.DRIFT_OFFSET_START,
|
|
3146
|
-
1
|
|
3147
|
-
);
|
|
3148
|
-
Atomics.store(driftView, 0, driftMs);
|
|
3149
|
-
console.log(`[SuperSonic] Drift offset: ${driftMs}ms (expected=${expectedContextTime.toFixed(3)}s, actual=${timestamp.contextTime.toFixed(3)}s, NTP=${currentNTP.toFixed(3)}s)`);
|
|
3150
|
-
}
|
|
3151
|
-
/**
|
|
3152
|
-
* Get current drift offset in milliseconds
|
|
3153
|
-
* @returns {number} Current drift in milliseconds
|
|
3154
|
-
*/
|
|
3155
|
-
getDriftOffset() {
|
|
3156
|
-
if (!this.#bufferConstants) {
|
|
3157
|
-
return 0;
|
|
3158
|
-
}
|
|
3159
|
-
const driftView = new Int32Array(
|
|
3160
|
-
this.#sharedBuffer,
|
|
3161
|
-
this.#ringBufferBase + this.#bufferConstants.DRIFT_OFFSET_START,
|
|
3162
|
-
1
|
|
3163
|
-
);
|
|
3164
|
-
return Atomics.load(driftView, 0);
|
|
3165
|
-
}
|
|
3166
|
-
/**
|
|
3167
|
-
* Start periodic drift offset updates
|
|
3168
|
-
* @private
|
|
3169
|
-
*/
|
|
3170
|
-
#startDriftOffsetTimer() {
|
|
3171
|
-
this.#stopDriftOffsetTimer();
|
|
3172
|
-
this.#driftOffsetTimer = setInterval(() => {
|
|
3173
|
-
this.updateDriftOffset();
|
|
3174
|
-
}, DRIFT_UPDATE_INTERVAL_MS);
|
|
3175
|
-
console.log(`[SuperSonic] Started drift offset correction (every ${DRIFT_UPDATE_INTERVAL_MS}ms)`);
|
|
3176
|
-
}
|
|
3177
|
-
/**
|
|
3178
|
-
* Stop periodic drift offset updates
|
|
3179
|
-
* @private
|
|
3180
|
-
*/
|
|
3181
|
-
#stopDriftOffsetTimer() {
|
|
3182
|
-
if (this.#driftOffsetTimer) {
|
|
3183
|
-
clearInterval(this.#driftOffsetTimer);
|
|
3184
|
-
this.#driftOffsetTimer = null;
|
|
3185
|
-
}
|
|
3186
|
-
}
|
|
3187
|
-
#extractSynthDefName(path) {
|
|
3188
|
-
if (!path || typeof path !== "string") {
|
|
3189
|
-
return null;
|
|
3190
|
-
}
|
|
3191
|
-
const lastSegment = path.split("/").filter(Boolean).pop() || path;
|
|
3192
|
-
return lastSegment.replace(/\.scsyndef$/i, "");
|
|
3193
|
-
}
|
|
3194
|
-
#toUint8Array(data) {
|
|
3195
|
-
if (data instanceof Uint8Array) {
|
|
3196
|
-
return data;
|
|
3197
|
-
}
|
|
3198
|
-
if (data instanceof ArrayBuffer) {
|
|
3199
|
-
return new Uint8Array(data);
|
|
3200
|
-
}
|
|
3201
|
-
throw new Error("oscData must be ArrayBuffer or Uint8Array");
|
|
3202
|
-
}
|
|
3203
|
-
async #prepareOutboundPacket(uint8Data) {
|
|
3204
|
-
const decodeOptions = { metadata: true, unpackSingleArgs: false };
|
|
3205
|
-
try {
|
|
3206
|
-
const decodedPacket = _SuperSonic.osc.decode(uint8Data, decodeOptions);
|
|
3207
|
-
const { packet, changed } = await this.#rewritePacket(decodedPacket);
|
|
3208
|
-
if (!changed) {
|
|
3209
|
-
return uint8Data;
|
|
3210
|
-
}
|
|
3211
|
-
return _SuperSonic.osc.encode(packet);
|
|
3212
|
-
} catch (error) {
|
|
3213
|
-
console.error("[SuperSonic] Failed to prepare OSC packet:", error);
|
|
3214
|
-
throw error;
|
|
3215
|
-
}
|
|
3216
|
-
}
|
|
3217
|
-
async #rewritePacket(packet) {
|
|
3218
|
-
if (packet && packet.address) {
|
|
3219
|
-
const { message, changed } = await this.#rewriteMessage(packet);
|
|
3220
|
-
return { packet: message, changed };
|
|
3221
|
-
}
|
|
3222
|
-
if (this.#isBundle(packet)) {
|
|
3223
|
-
const subResults = await Promise.all(
|
|
3224
|
-
packet.packets.map((subPacket) => this.#rewritePacket(subPacket))
|
|
3225
|
-
);
|
|
3226
|
-
const changed = subResults.some((result) => result.changed);
|
|
3227
|
-
if (!changed) {
|
|
3228
|
-
return { packet, changed: false };
|
|
3229
|
-
}
|
|
3230
|
-
const rewrittenPackets = subResults.map((result) => result.packet);
|
|
3231
|
-
return {
|
|
3232
|
-
packet: {
|
|
3233
|
-
timeTag: packet.timeTag,
|
|
3234
|
-
packets: rewrittenPackets
|
|
3235
|
-
},
|
|
3236
|
-
changed: true
|
|
3237
|
-
};
|
|
3238
|
-
}
|
|
3239
|
-
return { packet, changed: false };
|
|
3240
|
-
}
|
|
3241
|
-
async #rewriteMessage(message) {
|
|
3242
|
-
switch (message.address) {
|
|
3243
|
-
case "/b_alloc":
|
|
3244
|
-
return {
|
|
3245
|
-
message: await this.#rewriteAlloc(message),
|
|
3246
|
-
changed: true
|
|
3247
|
-
};
|
|
3248
|
-
case "/b_allocRead":
|
|
3249
|
-
return {
|
|
3250
|
-
message: await this.#rewriteAllocRead(message),
|
|
3251
|
-
changed: true
|
|
3252
|
-
};
|
|
3253
|
-
case "/b_allocReadChannel":
|
|
3254
|
-
return {
|
|
3255
|
-
message: await this.#rewriteAllocReadChannel(message),
|
|
3256
|
-
changed: true
|
|
3257
|
-
};
|
|
3258
|
-
default:
|
|
3259
|
-
return { message, changed: false };
|
|
3260
|
-
}
|
|
3261
|
-
}
|
|
3262
|
-
async #rewriteAllocRead(message) {
|
|
3263
|
-
const bufferManager = this.#requireBufferManager();
|
|
3264
|
-
const bufnum = this.#requireIntArg(message.args, 0, "/b_allocRead requires a buffer number");
|
|
3265
|
-
const path = this.#requireStringArg(message.args, 1, "/b_allocRead requires a file path");
|
|
3266
|
-
const startFrame = this.#optionalIntArg(message.args, 2, 0);
|
|
3267
|
-
const numFrames = this.#optionalIntArg(message.args, 3, 0);
|
|
3268
|
-
const bufferInfo = await bufferManager.prepareFromFile({
|
|
3269
|
-
bufnum,
|
|
3270
|
-
path,
|
|
3271
|
-
startFrame,
|
|
3272
|
-
numFrames
|
|
3273
|
-
});
|
|
3274
|
-
this.#detachAllocationPromise(bufferInfo.allocationComplete, `/b_allocRead ${bufnum}`);
|
|
3275
|
-
return this.#buildAllocPtrMessage(bufnum, bufferInfo);
|
|
3276
|
-
}
|
|
3277
|
-
async #rewriteAllocReadChannel(message) {
|
|
3278
|
-
const bufferManager = this.#requireBufferManager();
|
|
3279
|
-
const bufnum = this.#requireIntArg(message.args, 0, "/b_allocReadChannel requires a buffer number");
|
|
3280
|
-
const path = this.#requireStringArg(message.args, 1, "/b_allocReadChannel requires a file path");
|
|
3281
|
-
const startFrame = this.#optionalIntArg(message.args, 2, 0);
|
|
3282
|
-
const numFrames = this.#optionalIntArg(message.args, 3, 0);
|
|
3283
|
-
const channels = [];
|
|
3284
|
-
for (let i = 4; i < (message.args?.length || 0); i++) {
|
|
3285
|
-
if (!this.#isNumericArg(message.args[i])) {
|
|
3286
|
-
break;
|
|
3287
|
-
}
|
|
3288
|
-
channels.push(Math.floor(this.#getArgValue(message.args[i])));
|
|
3289
|
-
}
|
|
3290
|
-
const bufferInfo = await bufferManager.prepareFromFile({
|
|
3291
|
-
bufnum,
|
|
3292
|
-
path,
|
|
3293
|
-
startFrame,
|
|
3294
|
-
numFrames,
|
|
3295
|
-
channels: channels.length > 0 ? channels : null
|
|
3296
|
-
});
|
|
3297
|
-
this.#detachAllocationPromise(bufferInfo.allocationComplete, `/b_allocReadChannel ${bufnum}`);
|
|
3298
|
-
return this.#buildAllocPtrMessage(bufnum, bufferInfo);
|
|
3299
|
-
}
|
|
3300
|
-
async #rewriteAlloc(message) {
|
|
3301
|
-
const bufferManager = this.#requireBufferManager();
|
|
3302
|
-
const bufnum = this.#requireIntArg(message.args, 0, "/b_alloc requires a buffer number");
|
|
3303
|
-
const numFrames = this.#requireIntArg(message.args, 1, "/b_alloc requires a frame count");
|
|
3304
|
-
let argIndex = 2;
|
|
3305
|
-
let numChannels = 1;
|
|
3306
|
-
let sampleRate = this.#audioContext?.sampleRate || 44100;
|
|
3307
|
-
if (this.#isNumericArg(this.#argAt(message.args, argIndex))) {
|
|
3308
|
-
numChannels = Math.max(1, this.#optionalIntArg(message.args, argIndex, 1));
|
|
3309
|
-
argIndex++;
|
|
3310
|
-
}
|
|
3311
|
-
if (this.#argAt(message.args, argIndex)?.type === "b") {
|
|
3312
|
-
argIndex++;
|
|
3313
|
-
}
|
|
3314
|
-
if (this.#isNumericArg(this.#argAt(message.args, argIndex))) {
|
|
3315
|
-
sampleRate = this.#getArgValue(this.#argAt(message.args, argIndex));
|
|
3316
|
-
}
|
|
3317
|
-
const bufferInfo = await bufferManager.prepareEmpty({
|
|
3318
|
-
bufnum,
|
|
3319
|
-
numFrames,
|
|
3320
|
-
numChannels,
|
|
3321
|
-
sampleRate
|
|
3322
|
-
});
|
|
3323
|
-
this.#detachAllocationPromise(bufferInfo.allocationComplete, `/b_alloc ${bufnum}`);
|
|
3324
|
-
return this.#buildAllocPtrMessage(bufnum, bufferInfo);
|
|
3325
|
-
}
|
|
3326
|
-
#buildAllocPtrMessage(bufnum, bufferInfo) {
|
|
3327
|
-
return {
|
|
3328
|
-
address: "/b_allocPtr",
|
|
3329
|
-
args: [
|
|
3330
|
-
this.#intArg(bufnum),
|
|
3331
|
-
this.#intArg(bufferInfo.ptr),
|
|
3332
|
-
this.#intArg(bufferInfo.numFrames),
|
|
3333
|
-
this.#intArg(bufferInfo.numChannels),
|
|
3334
|
-
this.#floatArg(bufferInfo.sampleRate),
|
|
3335
|
-
this.#stringArg(bufferInfo.uuid)
|
|
3336
|
-
]
|
|
3337
|
-
};
|
|
3338
|
-
}
|
|
3339
|
-
#intArg(value) {
|
|
3340
|
-
return { type: "i", value: Math.floor(value) };
|
|
3341
|
-
}
|
|
3342
|
-
#floatArg(value) {
|
|
3343
|
-
return { type: "f", value };
|
|
3344
|
-
}
|
|
3345
|
-
#stringArg(value) {
|
|
3346
|
-
return { type: "s", value: String(value) };
|
|
3347
|
-
}
|
|
3348
|
-
#argAt(args, index) {
|
|
3349
|
-
if (!Array.isArray(args)) {
|
|
3350
|
-
return void 0;
|
|
3351
|
-
}
|
|
3352
|
-
return args[index];
|
|
3353
|
-
}
|
|
3354
|
-
#getArgValue(arg) {
|
|
3355
|
-
if (arg === void 0 || arg === null) {
|
|
3356
|
-
return void 0;
|
|
3357
|
-
}
|
|
3358
|
-
return typeof arg === "object" && Object.prototype.hasOwnProperty.call(arg, "value") ? arg.value : arg;
|
|
3359
|
-
}
|
|
3360
|
-
#requireIntArg(args, index, errorMessage) {
|
|
3361
|
-
const value = this.#getArgValue(this.#argAt(args, index));
|
|
3362
|
-
if (!Number.isFinite(value)) {
|
|
3363
|
-
throw new Error(errorMessage);
|
|
3364
|
-
}
|
|
3365
|
-
return Math.floor(value);
|
|
3366
|
-
}
|
|
3367
|
-
#optionalIntArg(args, index, defaultValue = 0) {
|
|
3368
|
-
const value = this.#getArgValue(this.#argAt(args, index));
|
|
3369
|
-
if (!Number.isFinite(value)) {
|
|
3370
|
-
return defaultValue;
|
|
3371
|
-
}
|
|
3372
|
-
return Math.floor(value);
|
|
3373
|
-
}
|
|
3374
|
-
#requireStringArg(args, index, errorMessage) {
|
|
3375
|
-
const value = this.#getArgValue(this.#argAt(args, index));
|
|
3376
|
-
if (typeof value !== "string") {
|
|
3377
|
-
throw new Error(errorMessage);
|
|
3378
|
-
}
|
|
3379
|
-
return value;
|
|
3380
|
-
}
|
|
3381
|
-
#isNumericArg(arg) {
|
|
3382
|
-
if (!arg) {
|
|
3383
|
-
return false;
|
|
3384
|
-
}
|
|
3385
|
-
const value = this.#getArgValue(arg);
|
|
3386
|
-
return Number.isFinite(value);
|
|
3387
|
-
}
|
|
3388
|
-
#detachAllocationPromise(promise, context) {
|
|
3389
|
-
if (!promise || typeof promise.catch !== "function") {
|
|
3390
|
-
return;
|
|
3391
|
-
}
|
|
3392
|
-
promise.catch((error) => {
|
|
3393
|
-
console.error(`[SuperSonic] ${context} allocation failed:`, error);
|
|
3394
|
-
});
|
|
3395
|
-
}
|
|
3396
|
-
#requireBufferManager() {
|
|
3397
|
-
if (!this.#bufferManager) {
|
|
3398
|
-
throw new Error("Buffer manager not ready. Call init() before issuing buffer commands.");
|
|
3399
|
-
}
|
|
3400
|
-
return this.#bufferManager;
|
|
3401
|
-
}
|
|
3402
|
-
#isBundle(packet) {
|
|
3403
|
-
return packet && packet.timeTag !== void 0 && Array.isArray(packet.packets);
|
|
3404
|
-
}
|
|
3405
|
-
#calculateBundleWait(uint8Data) {
|
|
3406
|
-
if (uint8Data.length < 16) {
|
|
3407
|
-
return null;
|
|
3408
|
-
}
|
|
3409
|
-
const header = String.fromCharCode.apply(null, uint8Data.slice(0, 8));
|
|
3410
|
-
if (header !== "#bundle\0") {
|
|
3411
|
-
return null;
|
|
3412
|
-
}
|
|
3413
|
-
const ntpStartView = new Float64Array(this.#sharedBuffer, this.#ringBufferBase + this.#bufferConstants.NTP_START_TIME_START, 1);
|
|
3414
|
-
const ntpStartTime = ntpStartView[0];
|
|
3415
|
-
if (ntpStartTime === 0) {
|
|
3416
|
-
console.warn("[SuperSonic] NTP start time not yet initialized");
|
|
3417
|
-
return null;
|
|
3418
|
-
}
|
|
3419
|
-
const driftView = new Int32Array(this.#sharedBuffer, this.#ringBufferBase + this.#bufferConstants.DRIFT_OFFSET_START, 1);
|
|
3420
|
-
const driftMs = Atomics.load(driftView, 0);
|
|
3421
|
-
const driftSeconds = driftMs / 1e3;
|
|
3422
|
-
const globalView = new Int32Array(this.#sharedBuffer, this.#ringBufferBase + this.#bufferConstants.GLOBAL_OFFSET_START, 1);
|
|
3423
|
-
const globalMs = Atomics.load(globalView, 0);
|
|
3424
|
-
const globalSeconds = globalMs / 1e3;
|
|
3425
|
-
const totalOffset = ntpStartTime + driftSeconds + globalSeconds;
|
|
3426
|
-
const view = new DataView(uint8Data.buffer, uint8Data.byteOffset);
|
|
3427
|
-
const ntpSeconds = view.getUint32(8, false);
|
|
3428
|
-
const ntpFraction = view.getUint32(12, false);
|
|
3429
|
-
if (ntpSeconds === 0 && (ntpFraction === 0 || ntpFraction === 1)) {
|
|
3430
|
-
return null;
|
|
3431
|
-
}
|
|
3432
|
-
const ntpTimeS = ntpSeconds + ntpFraction / 4294967296;
|
|
3433
|
-
const audioTimeS = ntpTimeS - totalOffset;
|
|
3434
|
-
const currentTimeS = this.#audioContext.currentTime;
|
|
3435
|
-
return { audioTimeS, currentTimeS };
|
|
3436
|
-
}
|
|
3437
|
-
};
|
|
3438
|
-
export {
|
|
3439
|
-
SuperSonic
|
|
3440
|
-
};
|
|
1
|
+
var n={},n=n||{};(function(){"use strict";n.SECS_70YRS=2208988800,n.TWO_32=4294967296,n.defaults={metadata:!1,unpackSingleArgs:!0},n.isCommonJS=!!(typeof module<"u"&&module.exports),n.isNode=n.isCommonJS&&typeof window>"u",n.isElectron=!!(typeof process<"u"&&process.versions&&process.versions.electron),n.isBufferEnv=n.isNode||n.isElectron,n.isArray=function(s){return s&&Object.prototype.toString.call(s)==="[object Array]"},n.isTypedArrayView=function(s){return s.buffer&&s.buffer instanceof ArrayBuffer},n.isBuffer=function(s){return n.isBufferEnv&&s instanceof Buffer},n.Long=typeof Long<"u"?Long:void 0,n.TextDecoder=typeof TextDecoder<"u"?new TextDecoder("utf-8"):typeof util<"u"&&typeof(util.TextDecoder!=="undefined")?new util.TextDecoder("utf-8"):void 0,n.TextEncoder=typeof TextEncoder<"u"?new TextEncoder("utf-8"):typeof util<"u"&&typeof(util.TextEncoder!=="undefined")?new util.TextEncoder("utf-8"):void 0,n.dataView=function(s,e,t){return s.buffer?new DataView(s.buffer,e,t):s instanceof ArrayBuffer?new DataView(s,e,t):new DataView(new Uint8Array(s),e,t)},n.byteArray=function(s){if(s instanceof Uint8Array)return s;var e=s.buffer?s.buffer:s;if(!(e instanceof ArrayBuffer)&&(typeof e.length>"u"||typeof e=="string"))throw new Error("Can't wrap a non-array-like object as Uint8Array. Object was: "+JSON.stringify(s,null,2));return new Uint8Array(e)},n.nativeBuffer=function(s){return n.isBufferEnv?n.isBuffer(s)?s:Buffer.from(s.buffer?s:new Uint8Array(s)):n.isTypedArrayView(s)?s:new Uint8Array(s)},n.copyByteArray=function(s,e,t){if(n.isTypedArrayView(s)&&n.isTypedArrayView(e))e.set(s,t);else for(var r=t===void 0?0:t,i=Math.min(e.length-t,s.length),o=0,a=r;o<i;o++,a++)e[a]=s[o];return e},n.readString=function(s,e){for(var t=[],r=e.idx;r<s.byteLength;r++){var i=s.getUint8(r);if(i!==0)t.push(i);else{r++;break}}r=r+3&-4,e.idx=r;var o=n.isBufferEnv?n.readString.withBuffer:n.TextDecoder?n.readString.withTextDecoder:n.readString.raw;return o(t)},n.readString.raw=function(s){for(var e="",t=1e4,r=0;r<s.length;r+=t)e+=String.fromCharCode.apply(null,s.slice(r,r+t));return e},n.readString.withTextDecoder=function(s){var e=new Int8Array(s);return n.TextDecoder.decode(e)},n.readString.withBuffer=function(s){return Buffer.from(s).toString("utf-8")},n.writeString=function(s){var e=n.isBufferEnv?n.writeString.withBuffer:n.TextEncoder?n.writeString.withTextEncoder:null,t=s+"\0",r;e&&(r=e(t));for(var i=e?r.length:t.length,o=i+3&-4,a=new Uint8Array(o),l=0;l<i-1;l++){var u=e?r[l]:t.charCodeAt(l);a[l]=u}return a},n.writeString.withTextEncoder=function(s){return n.TextEncoder.encode(s)},n.writeString.withBuffer=function(s){return Buffer.from(s)},n.readPrimitive=function(s,e,t,r){var i=s[e](r.idx,!1);return r.idx+=t,i},n.writePrimitive=function(s,e,t,r,i){i=i===void 0?0:i;var o;return e?o=new Uint8Array(e.buffer):(o=new Uint8Array(r),e=new DataView(o.buffer)),e[t](i,s,!1),o},n.readInt32=function(s,e){return n.readPrimitive(s,"getInt32",4,e)},n.writeInt32=function(s,e,t){return n.writePrimitive(s,e,"setInt32",4,t)},n.readInt64=function(s,e){var t=n.readPrimitive(s,"getInt32",4,e),r=n.readPrimitive(s,"getInt32",4,e);return n.Long?new n.Long(r,t):{high:t,low:r,unsigned:!1}},n.writeInt64=function(s,e,t){var r=new Uint8Array(8);return r.set(n.writePrimitive(s.high,e,"setInt32",4,t),0),r.set(n.writePrimitive(s.low,e,"setInt32",4,t+4),4),r},n.readFloat32=function(s,e){return n.readPrimitive(s,"getFloat32",4,e)},n.writeFloat32=function(s,e,t){return n.writePrimitive(s,e,"setFloat32",4,t)},n.readFloat64=function(s,e){return n.readPrimitive(s,"getFloat64",8,e)},n.writeFloat64=function(s,e,t){return n.writePrimitive(s,e,"setFloat64",8,t)},n.readChar32=function(s,e){var t=n.readPrimitive(s,"getUint32",4,e);return String.fromCharCode(t)},n.writeChar32=function(s,e,t){var r=s.charCodeAt(0);if(!(r===void 0||r<-1))return n.writePrimitive(r,e,"setUint32",4,t)},n.readBlob=function(s,e){var t=n.readInt32(s,e),r=t+3&-4,i=new Uint8Array(s.buffer,e.idx,t);return e.idx+=r,i},n.writeBlob=function(s){s=n.byteArray(s);var e=s.byteLength,t=e+3&-4,r=4,i=t+r,o=new Uint8Array(i),a=new DataView(o.buffer);return n.writeInt32(e,a),o.set(s,r),o},n.readMIDIBytes=function(s,e){var t=new Uint8Array(s.buffer,e.idx,4);return e.idx+=4,t},n.writeMIDIBytes=function(s){s=n.byteArray(s);var e=new Uint8Array(4);return e.set(s),e},n.readColor=function(s,e){var t=new Uint8Array(s.buffer,e.idx,4),r=t[3]/255;return e.idx+=4,{r:t[0],g:t[1],b:t[2],a:r}},n.writeColor=function(s){var e=Math.round(s.a*255),t=new Uint8Array([s.r,s.g,s.b,e]);return t},n.readTrue=function(){return!0},n.readFalse=function(){return!1},n.readNull=function(){return null},n.readImpulse=function(){return 1},n.readTimeTag=function(s,e){var t=n.readPrimitive(s,"getUint32",4,e),r=n.readPrimitive(s,"getUint32",4,e),i=t===0&&r===1?Date.now():n.ntpToJSTime(t,r);return{raw:[t,r],native:i}},n.writeTimeTag=function(s){var e=s.raw?s.raw:n.jsToNTPTime(s.native),t=new Uint8Array(8),r=new DataView(t.buffer);return n.writeInt32(e[0],r,0),n.writeInt32(e[1],r,4),t},n.timeTag=function(s,e){s=s||0,e=e||Date.now();var t=e/1e3,r=Math.floor(t),i=t-r,o=Math.floor(s),a=s-o,l=i+a;if(l>1){var u=Math.floor(l),c=l-u;o+=u,l=c}var f=r+o+n.SECS_70YRS,h=Math.round(n.TWO_32*l);return{raw:[f,h]}},n.ntpToJSTime=function(s,e){var t=s-n.SECS_70YRS,r=e/n.TWO_32,i=(t+r)*1e3;return i},n.jsToNTPTime=function(s){var e=s/1e3,t=Math.floor(e),r=e-t,i=t+n.SECS_70YRS,o=Math.round(n.TWO_32*r);return[i,o]},n.readArguments=function(s,e,t){var r=n.readString(s,t);if(r.indexOf(",")!==0)throw new Error("A malformed type tag string was found while reading the arguments of an OSC message. String was: "+r," at offset: "+t.idx);var i=r.substring(1).split(""),o=[];return n.readArgumentsIntoArray(o,i,r,s,e,t),o},n.readArgument=function(s,e,t,r,i){var o=n.argumentTypes[s];if(!o)throw new Error("'"+s+"' is not a valid OSC type tag. Type tag string was: "+e);var a=o.reader,l=n[a](t,i);return r.metadata&&(l={type:s,value:l}),l},n.readArgumentsIntoArray=function(s,e,t,r,i,o){for(var a=0;a<e.length;){var l=e[a],u;if(l==="["){var c=e.slice(a+1),f=c.indexOf("]");if(f<0)throw new Error("Invalid argument type tag: an open array type tag ('[') was found without a matching close array tag ('[]'). Type tag was: "+t);var h=c.slice(0,f);u=n.readArgumentsIntoArray([],h,t,r,i,o),a+=f+2}else u=n.readArgument(l,t,r,i,o),a++;s.push(u)}return s},n.writeArguments=function(s,e){var t=n.collectArguments(s,e);return n.joinParts(t)},n.joinParts=function(s){for(var e=new Uint8Array(s.byteLength),t=s.parts,r=0,i=0;i<t.length;i++){var o=t[i];n.copyByteArray(o,e,r),r+=o.length}return e},n.addDataPart=function(s,e){e.parts.push(s),e.byteLength+=s.length},n.writeArrayArguments=function(s,e){for(var t="[",r=0;r<s.length;r++){var i=s[r];t+=n.writeArgument(i,e)}return t+="]",t},n.writeArgument=function(s,e){if(n.isArray(s))return n.writeArrayArguments(s,e);var t=s.type,r=n.argumentTypes[t].writer;if(r){var i=n[r](s.value);n.addDataPart(i,e)}return s.type},n.collectArguments=function(s,e,t){n.isArray(s)||(s=typeof s>"u"?[]:[s]),t=t||{byteLength:0,parts:[]},e.metadata||(s=n.annotateArguments(s));for(var r=",",i=t.parts.length,o=0;o<s.length;o++){var a=s[o];r+=n.writeArgument(a,t)}var l=n.writeString(r);return t.byteLength+=l.byteLength,t.parts.splice(i,0,l),t},n.readMessage=function(s,e,t){e=e||n.defaults;var r=n.dataView(s,s.byteOffset,s.byteLength);t=t||{idx:0};var i=n.readString(r,t);return n.readMessageContents(i,r,e,t)},n.readMessageContents=function(s,e,t,r){if(s.indexOf("/")!==0)throw new Error("A malformed OSC address was found while reading an OSC message. String was: "+s);var i=n.readArguments(e,t,r);return{address:s,args:i.length===1&&t.unpackSingleArgs?i[0]:i}},n.collectMessageParts=function(s,e,t){return t=t||{byteLength:0,parts:[]},n.addDataPart(n.writeString(s.address),t),n.collectArguments(s.args,e,t)},n.writeMessage=function(s,e){if(e=e||n.defaults,!n.isValidMessage(s))throw new Error("An OSC message must contain a valid address. Message was: "+JSON.stringify(s,null,2));var t=n.collectMessageParts(s,e);return n.joinParts(t)},n.isValidMessage=function(s){return s.address&&s.address.indexOf("/")===0},n.readBundle=function(s,e,t){return n.readPacket(s,e,t)},n.collectBundlePackets=function(s,e,t){t=t||{byteLength:0,parts:[]},n.addDataPart(n.writeString("#bundle"),t),n.addDataPart(n.writeTimeTag(s.timeTag),t);for(var r=0;r<s.packets.length;r++){var i=s.packets[r],o=i.address?n.collectMessageParts:n.collectBundlePackets,a=o(i,e);t.byteLength+=a.byteLength,n.addDataPart(n.writeInt32(a.byteLength),t),t.parts=t.parts.concat(a.parts)}return t},n.writeBundle=function(s,e){if(!n.isValidBundle(s))throw new Error("An OSC bundle must contain 'timeTag' and 'packets' properties. Bundle was: "+JSON.stringify(s,null,2));e=e||n.defaults;var t=n.collectBundlePackets(s,e);return n.joinParts(t)},n.isValidBundle=function(s){return s.timeTag!==void 0&&s.packets!==void 0},n.readBundleContents=function(s,e,t,r){for(var i=n.readTimeTag(s,t),o=[];t.idx<r;){var a=n.readInt32(s,t),l=t.idx+a,u=n.readPacket(s,e,t,l);o.push(u)}return{timeTag:i,packets:o}},n.readPacket=function(s,e,t,r){var i=n.dataView(s,s.byteOffset,s.byteLength);r=r===void 0?i.byteLength:r,t=t||{idx:0};var o=n.readString(i,t),a=o[0];if(a==="#")return n.readBundleContents(i,e,t,r);if(a==="/")return n.readMessageContents(o,i,e,t);throw new Error("The header of an OSC packet didn't contain an OSC address or a #bundle string. Header was: "+o)},n.writePacket=function(s,e){if(n.isValidMessage(s))return n.writeMessage(s,e);if(n.isValidBundle(s))return n.writeBundle(s,e);throw new Error("The specified packet was not recognized as a valid OSC message or bundle. Packet was: "+JSON.stringify(s,null,2))},n.argumentTypes={i:{reader:"readInt32",writer:"writeInt32"},h:{reader:"readInt64",writer:"writeInt64"},f:{reader:"readFloat32",writer:"writeFloat32"},s:{reader:"readString",writer:"writeString"},S:{reader:"readString",writer:"writeString"},b:{reader:"readBlob",writer:"writeBlob"},t:{reader:"readTimeTag",writer:"writeTimeTag"},T:{reader:"readTrue"},F:{reader:"readFalse"},N:{reader:"readNull"},I:{reader:"readImpulse"},d:{reader:"readFloat64",writer:"writeFloat64"},c:{reader:"readChar32",writer:"writeChar32"},r:{reader:"readColor",writer:"writeColor"},m:{reader:"readMIDIBytes",writer:"writeMIDIBytes"}},n.inferTypeForArgument=function(s){var e=typeof s;switch(e){case"boolean":return s?"T":"F";case"string":return"s";case"number":return"f";case"undefined":return"N";case"object":if(s===null)return"N";if(s instanceof Uint8Array||s instanceof ArrayBuffer)return"b";if(typeof s.high=="number"&&typeof s.low=="number")return"h";break}throw new Error("Can't infer OSC argument type for value: "+JSON.stringify(s,null,2))},n.annotateArguments=function(s){for(var e=[],t=0;t<s.length;t++){var r=s[t],i;if(typeof r=="object"&&r.type&&r.value!==void 0)i=r;else if(n.isArray(r))i=n.annotateArguments(r);else{var o=n.inferTypeForArgument(r);i={type:o,value:r}}e.push(i)}return e}})();var S=function(){};S.prototype.on=function(){};S.prototype.emit=function(){};S.prototype.removeListener=function(){};(function(){"use strict";n.supportsSerial=!1,n.firePacketEvents=function(e,t,r,i){t.address?e.emit("message",t,r,i):n.fireBundleEvents(e,t,r,i)},n.fireBundleEvents=function(e,t,r,i){e.emit("bundle",t,r,i);for(var o=0;o<t.packets.length;o++){var a=t.packets[o];n.firePacketEvents(e,a,t.timeTag,i)}},n.fireClosedPortSendError=function(e,t){t=t||"Can't send packets on a closed osc.Port object. Please open (or reopen) this Port by calling open().",e.emit("error",t)},n.Port=function(e){this.options=e||{},this.on("data",this.decodeOSC.bind(this))};var s=n.Port.prototype=Object.create(S.prototype);s.constructor=n.Port,s.send=function(e){var t=Array.prototype.slice.call(arguments),r=this.encodeOSC(e),i=n.nativeBuffer(r);t[0]=i,this.sendRaw.apply(this,t)},s.encodeOSC=function(e){e=e.buffer?e.buffer:e;var t;try{t=n.writePacket(e,this.options)}catch(r){this.emit("error",r)}return t},s.decodeOSC=function(e,t){e=n.byteArray(e),this.emit("raw",e,t);try{var r=n.readPacket(e,this.options);this.emit("osc",r,t),n.firePacketEvents(this,r,void 0,t)}catch(i){this.emit("error",i)}},n.SLIPPort=function(e){var t=this,r=this.options=e||{};r.useSLIP=r.useSLIP===void 0?!0:r.useSLIP,this.decoder=new slip.Decoder({onMessage:this.decodeOSC.bind(this),onError:function(o){t.emit("error",o)}});var i=r.useSLIP?this.decodeSLIPData:this.decodeOSC;this.on("data",i.bind(this))},s=n.SLIPPort.prototype=Object.create(n.Port.prototype),s.constructor=n.SLIPPort,s.encodeOSC=function(e){e=e.buffer?e.buffer:e;var t;try{var r=n.writePacket(e,this.options);t=slip.encode(r)}catch(i){this.emit("error",i)}return t},s.decodeSLIPData=function(e,t){this.decoder.decode(e,t)},n.relay=function(e,t,r,i,o,a){r=r||"message",i=i||"send",o=o||function(){},a=a?[null].concat(a):[];var l=function(u){a[0]=u,u=o(u),t[i].apply(t,a)};return e.on(r,l),{eventName:r,listener:l}},n.relayPorts=function(e,t,r){var i=r.raw?"raw":"osc",o=r.raw?"sendRaw":"send";return n.relay(e,t,i,o,r.transform)},n.stopRelaying=function(e,t){e.removeListener(t.eventName,t.listener)},n.Relay=function(e,t,r){var i=this.options=r||{};i.raw=!1,this.port1=e,this.port2=t,this.listen()},s=n.Relay.prototype=Object.create(S.prototype),s.constructor=n.Relay,s.open=function(){this.port1.open(),this.port2.open()},s.listen=function(){this.port1Spec&&this.port2Spec&&this.close(),this.port1Spec=n.relayPorts(this.port1,this.port2,this.options),this.port2Spec=n.relayPorts(this.port2,this.port1,this.options);var e=this.close.bind(this);this.port1.on("close",e),this.port2.on("close",e)},s.close=function(){n.stopRelaying(this.port1,this.port1Spec),n.stopRelaying(this.port2,this.port2Spec),this.emit("close",this.port1,this.port2)}})();(function(){"use strict";n.WebSocket=typeof WebSocket<"u"?WebSocket:void 0,n.WebSocketPort=function(e){n.Port.call(this,e),this.on("open",this.listen.bind(this)),this.socket=e.socket,this.socket&&(this.socket.readyState===1?(n.WebSocketPort.setupSocketForBinary(this.socket),this.emit("open",this.socket)):this.open())};var s=n.WebSocketPort.prototype=Object.create(n.Port.prototype);s.constructor=n.WebSocketPort,s.open=function(){(!this.socket||this.socket.readyState>1)&&(this.socket=new n.WebSocket(this.options.url)),n.WebSocketPort.setupSocketForBinary(this.socket);var e=this;this.socket.onopen=function(){e.emit("open",e.socket)},this.socket.onerror=function(t){e.emit("error",t)}},s.listen=function(){var e=this;this.socket.onmessage=function(t){e.emit("data",t.data,t)},this.socket.onclose=function(t){e.emit("close",t)},e.emit("ready")},s.sendRaw=function(e){if(!this.socket||this.socket.readyState!==1){n.fireClosedPortSendError(this);return}this.socket.send(e)},s.close=function(e,t){this.socket.close(e,t)},n.WebSocketPort.setupSocketForBinary=function(e){e.binaryType=n.isNode?"nodebuffer":"arraybuffer"}})();var b=n,{readPacket:pe,writePacket:me,readMessage:we,writeMessage:ge,readBundle:ye,writeBundle:Se}=n;var A=class{constructor(e=null){this.workerBaseURL=e,this.workers={oscOut:null,oscIn:null,debug:null},this.callbacks={onRawOSC:null,onParsedOSC:null,onDebugMessage:null,onError:null,onInitialized:null},this.initialized=!1,this.sharedBuffer=null,this.ringBufferBase=null,this.bufferConstants=null}async init(e,t,r){if(this.initialized){console.warn("[ScsynthOSC] Already initialized");return}this.sharedBuffer=e,this.ringBufferBase=t,this.bufferConstants=r;try{this.workers.oscOut=new Worker(this.workerBaseURL+"osc_out_prescheduler_worker.js",{type:"module"}),this.workers.oscIn=new Worker(this.workerBaseURL+"osc_in_worker.js",{type:"module"}),this.workers.debug=new Worker(this.workerBaseURL+"debug_worker.js",{type:"module"}),this.setupWorkerHandlers();let i=[this.initWorker(this.workers.oscOut,"OSC SCHEDULER+WRITER"),this.initWorker(this.workers.oscIn,"OSC IN"),this.initWorker(this.workers.debug,"DEBUG")];await Promise.all(i),this.workers.oscIn.postMessage({type:"start"}),this.workers.debug.postMessage({type:"start"}),this.initialized=!0,this.callbacks.onInitialized&&this.callbacks.onInitialized()}catch(i){throw console.error("[ScsynthOSC] Initialization failed:",i),this.callbacks.onError&&this.callbacks.onError(i),i}}initWorker(e,t){return new Promise((r,i)=>{let o=setTimeout(()=>{i(new Error(`${t} worker initialization timeout`))},5e3),a=l=>{l.data.type==="initialized"&&(clearTimeout(o),e.removeEventListener("message",a),r())};e.addEventListener("message",a),e.postMessage({type:"init",sharedBuffer:this.sharedBuffer,ringBufferBase:this.ringBufferBase,bufferConstants:this.bufferConstants})})}setupWorkerHandlers(){this.workers.oscIn.onmessage=e=>{let t=e.data;switch(t.type){case"messages":t.messages.forEach(r=>{if(r.oscData&&(this.callbacks.onRawOSC&&this.callbacks.onRawOSC({oscData:r.oscData,sequence:r.sequence}),this.callbacks.onParsedOSC))try{let i={metadata:!1,unpackSingleArgs:!1},o=b.readPacket(r.oscData,i);this.callbacks.onParsedOSC(o)}catch(i){console.error("[ScsynthOSC] Failed to decode OSC message:",i,r)}});break;case"error":console.error("[ScsynthOSC] OSC IN error:",t.error),this.callbacks.onError&&this.callbacks.onError(t.error,"oscIn");break}},this.workers.debug.onmessage=e=>{let t=e.data;switch(t.type){case"debug":this.callbacks.onDebugMessage&&t.messages.forEach(r=>{this.callbacks.onDebugMessage(r)});break;case"error":console.error("[ScsynthOSC] DEBUG error:",t.error),this.callbacks.onError&&this.callbacks.onError(t.error,"debug");break}},this.workers.oscOut.onmessage=e=>{let t=e.data;switch(t.type){case"error":console.error("[ScsynthOSC] OSC OUT error:",t.error),this.callbacks.onError&&this.callbacks.onError(t.error,"oscOut");break}}}send(e,t={}){if(!this.initialized){console.error("[ScsynthOSC] Not initialized");return}let{editorId:r=0,runTag:i="",audioTimeS:o=null,currentTimeS:a=null}=t;this.workers.oscOut.postMessage({type:"send",oscData:e,editorId:r,runTag:i,audioTimeS:o,currentTimeS:a})}sendImmediate(e){if(!this.initialized){console.error("[ScsynthOSC] Not initialized");return}this.workers.oscOut.postMessage({type:"sendImmediate",oscData:e})}cancelEditorTag(e,t){this.initialized&&this.workers.oscOut.postMessage({type:"cancelEditorTag",editorId:e,runTag:t})}cancelEditor(e){this.initialized&&this.workers.oscOut.postMessage({type:"cancelEditor",editorId:e})}cancelAll(){this.initialized&&this.workers.oscOut.postMessage({type:"cancelAll"})}clearDebug(){this.initialized&&this.workers.debug.postMessage({type:"clear"})}onRawOSC(e){this.callbacks.onRawOSC=e}onParsedOSC(e){this.callbacks.onParsedOSC=e}onDebugMessage(e){this.callbacks.onDebugMessage=e}onError(e){this.callbacks.onError=e}onInitialized(e){this.callbacks.onInitialized=e}terminate(){this.workers.oscOut&&(this.workers.oscOut.postMessage({type:"stop"}),this.workers.oscOut.terminate()),this.workers.oscIn&&(this.workers.oscIn.postMessage({type:"stop"}),this.workers.oscIn.terminate()),this.workers.debug&&(this.workers.debug.postMessage({type:"stop"}),this.workers.debug.terminate()),this.workers={oscOut:null,oscIn:null,debug:null},this.initialized=!1}};var ie={5120:"i8",5121:"u8",5122:"i16",5123:"u16",5124:"i32",5125:"u32",5126:"f32"};var z={u8:1,u8c:1,i8:1,u16:2,i16:2,u32:4,i32:4,i64:8,u64:8,f32:4,f64:8};var se={f32:Float32Array,f64:Float64Array},ne={i8:Int8Array,i16:Int16Array,i32:Int32Array},oe={u8:Uint8Array,u8c:Uint8ClampedArray,u16:Uint16Array,u32:Uint32Array},ae={i64:BigInt64Array,u64:BigUint64Array},le={...se,...ne,...oe},ue=s=>{let e=ie[s];return e!==void 0?e:s};function N(s,...e){let t=ae[s];return new(t||le[ue(s)])(...e)}var B=(s,e)=>(e--,s+e&~e);var L=s=>typeof s=="number";var O=(s,e=t=>t!==void 0?": "+t:"")=>class extends Error{origMessage;constructor(t){super(s(t)+e(t)),this.origMessage=t!==void 0?String(t):""}};var ce=O(()=>"Assertion failed"),I=(typeof process<"u"&&process.env!==void 0?process.env.UMBRELLA_ASSERTS:!import.meta.env||import.meta.env.MODE!=="production"||import.meta.env.UMBRELLA_ASSERTS||import.meta.env.VITE_UMBRELLA_ASSERTS)?(s,e)=>{if(typeof s=="function"&&!s()||!s)throw new ce(typeof e=="function"?e():e)}:()=>{};var fe=O(()=>"illegal argument(s)"),$=s=>{throw new fe(s)};var V=0,W=1,q=2,H=3,Z=4,y=5,j=6,R=1,F=2,Y=7*4,x=0,U=1,w=2*4,M=class{buf;start;u8;u32;state;constructor(e={}){if(this.buf=e.buf?e.buf:new ArrayBuffer(e.size||4096),this.start=e.start!=null?B(Math.max(e.start,0),4):0,this.u8=new Uint8Array(this.buf),this.u32=new Uint32Array(this.buf),this.state=new Uint32Array(this.buf,this.start,Y/4),!e.skipInitialization){let t=e.align||8;I(t>=8,`invalid alignment: ${t}, must be a pow2 and >= 8`);let r=this.initialTop(t),i=e.end!=null?Math.min(e.end,this.buf.byteLength):this.buf.byteLength;r>=i&&$(`insufficient address range (0x${this.start.toString(16)} - 0x${i.toString(16)})`),this.align=t,this.doCompact=e.compact!==!1,this.doSplit=e.split!==!1,this.minSplit=e.minSplit||16,this.end=i,this.top=r,this._free=0,this._used=0}}stats(){let e=r=>{let i=0,o=0;for(;r;)i++,o+=this.blockSize(r),r=this.blockNext(r);return{count:i,size:o}},t=e(this._free);return{free:t,used:e(this._used),top:this.top,available:this.end-this.top+t.size,total:this.buf.byteLength}}callocAs(e,t,r=0){let i=this.mallocAs(e,t);return i?.fill(r),i}mallocAs(e,t){let r=this.malloc(t*z[e]);return r?N(e,this.buf,r,t):void 0}calloc(e,t=0){let r=this.malloc(e);return r&&this.u8.fill(t,r,r+e),r}malloc(e){if(e<=0)return 0;let t=B(e+w,this.align),r=this.end,i=this.top,o=this._free,a=0;for(;o;){let l=this.blockSize(o),u=o+l>=i;if(u||l>=t)return this.mallocTop(o,a,l,t,u);a=o,o=this.blockNext(o)}return o=i,i=o+t,i<=r?(this.initBlock(o,t,this._used),this._used=o,this.top=i,T(o)):0}mallocTop(e,t,r,i,o){if(o&&e+i>this.end)return 0;if(t?this.unlinkBlock(t,e):this._free=this.blockNext(e),this.setBlockNext(e,this._used),this._used=e,o)this.top=e+this.setBlockSize(e,i);else if(this.doSplit){let a=r-i;a>=this.minSplit&&this.splitBlock(e,i,a)}return T(e)}realloc(e,t){if(t<=0)return 0;let r=D(e),i=0,o=this._used,a=0;for(;o;){if(o===r){[i,a]=this.reallocBlock(o,t);break}o=this.blockNext(o)}return i&&i!==r&&this.u8.copyWithin(T(i),T(r),a),T(i)}reallocBlock(e,t){let r=this.blockSize(e),i=e+r,o=i>=this.top,a=B(t+w,this.align);if(a<=r){if(this.doSplit){let l=r-a;l>=this.minSplit?this.splitBlock(e,a,l):o&&(this.top=e+a)}else o&&(this.top=e+a);return[e,i]}return o&&e+a<this.end?(this.top=e+this.setBlockSize(e,a),[e,i]):(this.free(e),[D(this.malloc(t)),i])}reallocArray(e,t){if(e.buffer!==this.buf)return;let r=this.realloc(e.byteOffset,t*e.BYTES_PER_ELEMENT);return r?new e.constructor(this.buf,r,t):void 0}free(e){let t;if(L(e))t=e;else{if(e.buffer!==this.buf)return!1;t=e.byteOffset}t=D(t);let r=this._used,i=0;for(;r;){if(r===t)return i?this.unlinkBlock(i,r):this._used=this.blockNext(r),this.insert(r),this.doCompact&&this.compact(),!0;i=r,r=this.blockNext(r)}return!1}freeAll(){this._free=0,this._used=0,this.top=this.initialTop()}release(){return delete this.u8,delete this.u32,delete this.state,delete this.buf,!0}get align(){return this.state[Z]}set align(e){this.state[Z]=e}get end(){return this.state[H]}set end(e){this.state[H]=e}get top(){return this.state[q]}set top(e){this.state[q]=e}get _free(){return this.state[V]}set _free(e){this.state[V]=e}get _used(){return this.state[W]}set _used(e){this.state[W]=e}get doCompact(){return!!(this.state[y]&R)}set doCompact(e){e?this.state[y]|=1<<R-1:this.state[y]&=~R}get doSplit(){return!!(this.state[y]&F)}set doSplit(e){e?this.state[y]|=1<<F-1:this.state[y]&=~F}get minSplit(){return this.state[j]}set minSplit(e){I(e>w,`illegal min split threshold: ${e}, require at least ${w+1}`),this.state[j]=e}blockSize(e){return this.u32[(e>>2)+x]}setBlockSize(e,t){return this.u32[(e>>2)+x]=t,t}blockNext(e){return this.u32[(e>>2)+U]}setBlockNext(e,t){this.u32[(e>>2)+U]=t}initBlock(e,t,r){let i=e>>>2;return this.u32[i+x]=t,this.u32[i+U]=r,e}unlinkBlock(e,t){this.setBlockNext(e,this.blockNext(t))}splitBlock(e,t,r){this.insert(this.initBlock(e+this.setBlockSize(e,t),r,0)),this.doCompact&&this.compact()}initialTop(e=this.align){return B(this.start+Y+w,e)-w}compact(){let e=this._free,t=0,r=0,i,o=!1;for(;e;){for(i=e,r=this.blockNext(e);r&&i+this.blockSize(i)===r;)i=r,r=this.blockNext(r);if(i!==e){let a=i-e+this.blockSize(i);this.setBlockSize(e,a);let l=this.blockNext(i),u=this.blockNext(e);for(;u&&u!==l;){let c=this.blockNext(u);this.setBlockNext(u,0),u=c}this.setBlockNext(e,l),o=!0}e+this.blockSize(e)>=this.top&&(this.top=e,t?this.unlinkBlock(t,e):this._free=this.blockNext(e)),t=e,e=this.blockNext(e)}return o}insert(e){let t=this._free,r=0;for(;t&&!(e<=t);)r=t,t=this.blockNext(t);r?this.setBlockNext(r,e):this._free=e,this.setBlockNext(e,t)}},T=s=>s>0?s+w:0,D=s=>s>0?s-w:0;var he=8,P=class{#i;#n;#o;#d;#t;#r;#e;#s;constructor(e){let{audioContext:t,sharedBuffer:r,bufferPoolConfig:i,sampleBaseURL:o,audioPathMap:a={},maxBuffers:l=1024}=e;if(!t)throw new Error("BufferManager requires audioContext");if(!r||!(r instanceof SharedArrayBuffer))throw new Error("BufferManager requires sharedBuffer (SharedArrayBuffer)");if(!i||typeof i!="object")throw new Error("BufferManager requires bufferPoolConfig (object with start, size, align)");if(!Number.isFinite(i.start)||i.start<0)throw new Error("bufferPoolConfig.start must be a non-negative number");if(!Number.isFinite(i.size)||i.size<=0)throw new Error("bufferPoolConfig.size must be a positive number");if(a&&typeof a!="object")throw new Error("audioPathMap must be an object");if(!Number.isInteger(l)||l<=0)throw new Error("maxBuffers must be a positive integer");this.#o=t,this.#d=r,this.#i=o,this.#n=a,this.#t=new M({buf:r,start:i.start,size:i.size,align:he}),this.#r=new Map,this.#e=new Map,this.#s=new Map,this.GUARD_BEFORE=3,this.GUARD_AFTER=1,this.MAX_BUFFERS=l;let u=(i.size/(1024*1024)).toFixed(0),c=(i.start/(1024*1024)).toFixed(0)}#m(e){if(typeof e!="string"||e.length===0)throw new Error("Invalid audio path: must be a non-empty string");if(e.includes(".."))throw new Error(`Invalid audio path: path cannot contain '..' (got: ${e})`);if(e.startsWith("/")||/^[a-zA-Z]:/.test(e))throw new Error(`Invalid audio path: path must be relative (got: ${e})`);if(e.includes("%2e")||e.includes("%2E"))throw new Error(`Invalid audio path: path cannot contain URL-encoded characters (got: ${e})`);if(e.includes("\\"))throw new Error(`Invalid audio path: use forward slashes only (got: ${e})`);if(this.#n[e])return this.#n[e];if(!this.#i)throw new Error(`sampleBaseURL not configured. Please set it in SuperSonic constructor options.
|
|
2
|
+
Example: new SuperSonic({ sampleBaseURL: "./dist/samples/" })
|
|
3
|
+
Or use CDN: new SuperSonic({ sampleBaseURL: "https://unpkg.com/supersonic-scsynth-samples@latest/samples/" })
|
|
4
|
+
Or install: npm install supersonic-scsynth-samples`);return this.#i+e}#a(e){if(!Number.isInteger(e)||e<0||e>=this.MAX_BUFFERS)throw new Error(`Invalid buffer number ${e} (must be 0-${this.MAX_BUFFERS-1})`)}async#w(e,t,r){let i=null,o=null,a=!1,l=await this.#u(e),u=!1;try{await this.#E(e);let{ptr:c,sizeBytes:f,...h}=await r();i=c;let{uuid:d,allocationComplete:p}=this.#h(e,t);o=d,this.#T(e,i,f,d,p),a=!0;let m=this.#b(e,d,p);return l(),u=!0,{ptr:i,uuid:d,allocationComplete:m,...h}}catch(c){throw a&&o?this.#c(e,o,!1):i&&this.#t.free(i),c}finally{u||l()}}async prepareFromFile(e){let{bufnum:t,path:r,startFrame:i=0,numFrames:o=0,channels:a=null}=e;return this.#a(t),this.#w(t,6e4,async()=>{let l=this.#m(r),u=await fetch(l);if(!u.ok)throw new Error(`Failed to fetch ${l}: ${u.status} ${u.statusText}`);let c=await u.arrayBuffer(),f=await this.#o.decodeAudioData(c),h=Math.max(0,Math.floor(i||0)),d=f.length-h,p=o&&o>0?Math.min(Math.floor(o),d):d;if(p<=0)throw new Error(`No audio frames available for buffer ${t} from ${r}`);let m=this.#B(a,f.numberOfChannels),g=m.length,E=p*g+(this.GUARD_BEFORE+this.GUARD_AFTER)*g,v=this.#g(E),C=new Float32Array(E),X=this.GUARD_BEFORE*g;for(let k=0;k<p;k++)for(let _=0;_<g;_++){let te=m[_],re=f.getChannelData(te);C[X+k*g+_]=re[h+k]}this.#S(v,C);let ee=C.length*4;return{ptr:v,sizeBytes:ee,numFrames:p,numChannels:g,sampleRate:f.sampleRate}})}async prepareEmpty(e){let{bufnum:t,numFrames:r,numChannels:i=1,sampleRate:o=null}=e;if(this.#a(t),!Number.isFinite(r)||r<=0)throw new Error(`/b_alloc requires a positive number of frames (got ${r})`);if(!Number.isFinite(i)||i<=0)throw new Error(`/b_alloc requires a positive channel count (got ${i})`);let a=Math.floor(r),l=Math.floor(i);return this.#w(t,5e3,async()=>{let u=a*l+(this.GUARD_BEFORE+this.GUARD_AFTER)*l,c=this.#g(u),f=new Float32Array(u);this.#S(c,f);let h=f.length*4;return{ptr:c,sizeBytes:h,numFrames:a,numChannels:l,sampleRate:o||this.#o.sampleRate}})}#B(e,t){return!e||e.length===0?Array.from({length:t},(r,i)=>i):(e.forEach(r=>{if(!Number.isInteger(r)||r<0||r>=t)throw new Error(`Channel ${r} is out of range (file has ${t} channels)`)}),e)}#g(e){let t=e*4,r=this.#t.malloc(t);if(r===0){let i=this.#t.stats(),o=((i.available||0)/(1024*1024)).toFixed(2),a=((i.total||0)/(1024*1024)).toFixed(2),l=(t/(1024*1024)).toFixed(2);throw new Error(`Buffer pool allocation failed: requested ${l}MB, available ${o}MB of ${a}MB total`)}return r}#S(e,t){new Float32Array(this.#d,e,t.length).set(t)}#l(e,t,r){return new Promise((i,o)=>{let a=setTimeout(()=>{this.#e.delete(e),o(new Error(`Buffer ${t} allocation timeout (${r}ms)`))},r);this.#e.set(e,{resolve:i,reject:o,timeout:a})})}#h(e,t){let r=crypto.randomUUID(),i=this.#l(r,e,t);return{uuid:r,allocationComplete:i}}async#u(e){let t=this.#s.get(e)||Promise.resolve(),r,i=new Promise(o=>{r=o});return this.#s.set(e,t.then(()=>i)),await t,()=>{r&&(r(),r=null),this.#s.get(e)===i&&this.#s.delete(e)}}#T(e,t,r,i,o){let a=this.#r.get(e),l={ptr:t,size:r,pendingToken:i,pendingPromise:o,previousAllocation:a?{ptr:a.ptr,size:a.size}:null};return this.#r.set(e,l),l}async#E(e){let t=this.#r.get(e);if(t&&t.pendingToken&&t.pendingPromise)try{await t.pendingPromise}catch{}}#b(e,t,r){return!r||typeof r.then!="function"?(this.#c(e,t,!0),Promise.resolve()):r.then(i=>(this.#c(e,t,!0),i)).catch(i=>{throw this.#c(e,t,!1),i})}#c(e,t,r){let i=this.#r.get(e);if(!i||i.pendingToken!==t)return;let o=i.previousAllocation;if(r){i.pendingToken=null,i.pendingPromise=null,i.previousAllocation=null,o?.ptr&&this.#t.free(o.ptr);return}i.ptr&&this.#t.free(i.ptr),i.pendingPromise=null,o?.ptr?this.#r.set(e,{ptr:o.ptr,size:o.size,pendingToken:null,previousAllocation:null}):this.#r.delete(e)}handleBufferFreed(e){let t=e[0],r=e[1],i=this.#r.get(t);if(!i){typeof r=="number"&&r!==0&&this.#t.free(r);return}if(typeof r=="number"&&r===i.ptr){this.#t.free(i.ptr),this.#r.delete(t);return}if(typeof r=="number"&&i.previousAllocation&&i.previousAllocation.ptr===r){this.#t.free(r),i.previousAllocation=null;return}this.#t.free(i.ptr),this.#r.delete(t)}handleBufferAllocated(e){let t=e[0],r=e[1],i=this.#e.get(t);i&&(clearTimeout(i.timeout),i.resolve({bufnum:r}),this.#e.delete(t))}allocate(e){let t=e*4,r=this.#t.malloc(t);if(r===0){let i=this.#t.stats(),o=((i.available||0)/(1024*1024)).toFixed(2),a=((i.total||0)/(1024*1024)).toFixed(2),l=(t/(1024*1024)).toFixed(2);console.error(`[BufferManager] Allocation failed: requested ${l}MB, available ${o}MB of ${a}MB total`)}return r}free(e){return this.#t.free(e)}getView(e,t){return new Float32Array(this.#d,e,t)}getStats(){return this.#t.stats()}getDiagnostics(){let e=this.#t.stats(),t=0,r=0;for(let i of this.#r.values())i&&(t+=i.size||0,i.pendingToken&&r++);return{active:this.#r.size,pending:r,bytesActive:t,pool:{total:e.total||0,available:e.available||0,freeBytes:e.free?.size||0,freeBlocks:e.free?.count||0,usedBytes:e.used?.size||0,usedBlocks:e.used?.count||0}}}destroy(){for(let[e,t]of this.#e.entries())clearTimeout(t.timeout),t.reject(new Error("BufferManager destroyed"));this.#e.clear();for(let[e,t]of this.#r.entries())t.ptr&&this.#t.free(t.ptr);this.#r.clear(),this.#s.clear()}};var G={totalPages:1280,ringBufferReserved:1048576,bufferPoolOffset:17825792,bufferPoolSize:66060288,get totalMemory(){return this.bufferPoolOffset+this.bufferPoolSize},get wasmHeapSize(){return this.bufferPoolOffset-this.ringBufferReserved}};var J={numBuffers:1024,maxNodes:1024,maxGraphDefs:1024,maxWireBufs:64,numAudioBusChannels:128,numInputBusChannels:0,numOutputBusChannels:2,numControlBusChannels:4096,bufLength:128,realTimeMemorySize:8192,numRGens:64,realTime:!1,memoryLocking:!1,loadGraphDefs:0,preferredSampleRate:0,verbosity:0};var Q=class s{static osc={encode:e=>b.writePacket(e),decode:(e,t={metadata:!1})=>b.readPacket(e,t)};#i;#n;#o;#d;#t;#r;#e;#s;#m;#a;#w;#B;#g;#S;#l;#h;#u;#T=0;#E=0;#b=0;#c=null;#O=!1;constructor(e={}){if(this.#l=!1,this.#h=!1,this.#u={},this.#t=null,this.#r=null,this.#e=null,this.#i=null,this.#n=null,this.#o=null,this.#s=null,this.loadedSynthDefs=new Set,this.onOSC=null,this.onMessage=null,this.onMessageSent=null,this.onMetricsUpdate=null,this.onDebugMessage=null,this.onInitialized=null,this.onError=null,!e.workerBaseURL||!e.wasmBaseURL)throw new Error(`SuperSonic requires workerBaseURL and wasmBaseURL options. Example:
|
|
5
|
+
new SuperSonic({
|
|
6
|
+
workerBaseURL: "/supersonic/workers/",
|
|
7
|
+
wasmBaseURL: "/supersonic/wasm/"
|
|
8
|
+
})`);let t=e.workerBaseURL,r=e.wasmBaseURL,i={...J,...e.scsynthOptions};this.config={wasmUrl:e.wasmUrl||r+"scsynth-nrt.wasm",wasmBaseURL:r,workletUrl:e.workletUrl||t+"scsynth_audio_worklet.js",workerBaseURL:t,development:!1,audioContextOptions:{latencyHint:"interactive",sampleRate:48e3},memory:G,worldOptions:i},this.#B=e.sampleBaseURL||null,this.#g=e.synthdefBaseURL||null,this.#S=e.audioPathMap||{},this.bootStats={initStartTime:null,initDuration:null}}get initialized(){return this.#l}get initializing(){return this.#h}get capabilities(){return this.#u}setAndValidateCapabilities(){this.#u={audioWorklet:typeof AudioWorklet<"u",sharedArrayBuffer:typeof SharedArrayBuffer<"u",crossOriginIsolated:window.crossOriginIsolated===!0,atomics:typeof Atomics<"u",webWorker:typeof Worker<"u"};let t=["audioWorklet","sharedArrayBuffer","crossOriginIsolated","atomics","webWorker"].filter(r=>!this.#u[r]);if(t.length>0){let r=new Error(`Missing required features: ${t.join(", ")}`);throw this.#u.crossOriginIsolated||(this.#u.sharedArrayBuffer?r.message+=`
|
|
9
|
+
|
|
10
|
+
SharedArrayBuffer is available but cross-origin isolation is not enabled. Please ensure COOP and COEP headers are set correctly:
|
|
11
|
+
Cross-Origin-Opener-Policy: same-origin
|
|
12
|
+
Cross-Origin-Embedder-Policy: require-corp`:r.message+=`
|
|
13
|
+
|
|
14
|
+
SharedArrayBuffer is not available. This may be due to:
|
|
15
|
+
1. Missing COOP/COEP headers
|
|
16
|
+
2. Browser doesn't support SharedArrayBuffer
|
|
17
|
+
3. Browser security settings`),r}return this.#u}#x(){let e=this.config.memory;this.#d=new WebAssembly.Memory({initial:e.totalPages,maximum:e.totalPages,shared:!0}),this.#t=this.#d.buffer}#U(){return this.#i=new AudioContext(this.config.audioContextOptions),this.#i}#D(){this.#s=new P({audioContext:this.#i,sharedBuffer:this.#t,bufferPoolConfig:{start:this.config.memory.bufferPoolOffset,size:this.config.memory.bufferPoolSize},sampleBaseURL:this.#B,audioPathMap:this.#S,maxBuffers:this.config.worldOptions.numBuffers})}async#z(){let e=this.config.wasmBaseURL+"manifest.json";try{let t=await fetch(e);if(!t.ok)return;let r=await t.json();this.config.wasmUrl=this.config.wasmBaseURL+r.wasmFile}catch{}}async#N(){this.config.development&&await this.#z();let e=await fetch(this.config.wasmUrl);if(!e.ok)throw new Error(`Failed to load WASM: ${e.status} ${e.statusText}`);return await e.arrayBuffer()}async#L(e){await this.#i.audioWorklet.addModule(this.config.workletUrl),this.#n=new AudioWorkletNode(this.#i,"scsynth-processor",{numberOfInputs:0,numberOfOutputs:1,outputChannelCount:[2]}),this.#n.connect(this.#i.destination),this.#n.port.postMessage({type:"init",sharedBuffer:this.#t}),this.#n.port.postMessage({type:"loadWasm",wasmBytes:e,wasmMemory:this.#d,worldOptions:this.config.worldOptions,sampleRate:this.#i.sampleRate}),await this.#W()}async#$(){this.#o=new A(this.config.workerBaseURL),this.#o.onRawOSC(e=>{this.onOSC&&this.onOSC(e)}),this.#o.onParsedOSC(e=>{if(e.address==="/buffer/freed")this.#s?.handleBufferFreed(e.args);else if(e.address==="/buffer/allocated")this.#s?.handleBufferAllocated(e.args);else if(e.address==="/synced"&&e.args.length>0){let t=e.args[0];this.#a&&this.#a.has(t)&&this.#a.get(t)(e)}this.onMessage&&(this.#E++,this.onMessage(e))}),this.#o.onDebugMessage(e=>{this.onDebugMessage&&this.onDebugMessage(e)}),this.#o.onError((e,t)=>{console.error(`[SuperSonic] ${t} error:`,e),this.#b++,this.onError&&this.onError(new Error(`${t}: ${e}`))}),await this.#o.init(this.#t,this.#r,this.#e)}#V(){this.#l=!0,this.#h=!1,this.bootStats.initDuration=performance.now()-this.bootStats.initStartTime,this.onInitialized&&this.onInitialized({capabilities:this.#u,bootStats:this.bootStats})}async init(e={}){if(this.#l){console.warn("[SuperSonic] Already initialized");return}if(this.#h){console.warn("[SuperSonic] Initialization already in progress");return}this.config={...this.config,...e,audioContextOptions:{...this.config.audioContextOptions,...e.audioContextOptions||{}}},this.#h=!0,this.bootStats.initStartTime=performance.now();try{this.setAndValidateCapabilities(),this.#x(),this.#U(),this.#D();let t=await this.#N();await this.#L(t),await this.#$(),this.#q(),this.#G(),this.#V()}catch(t){throw this.#h=!1,console.error("[SuperSonic] Initialization failed:",t),this.onError&&this.onError(t),t}}#W(){return new Promise((e,t)=>{let r=setTimeout(()=>{t(new Error("AudioWorklet initialization timeout"))},5e3),i=async o=>{if(o.data.type!=="debug"){if(o.data.type==="error"){console.error("[AudioWorklet] Error:",o.data.error),clearTimeout(r),this.#n.port.removeEventListener("message",i),t(new Error(o.data.error||"AudioWorklet error"));return}o.data.type==="initialized"&&(clearTimeout(r),this.#n.port.removeEventListener("message",i),o.data.success?(o.data.ringBufferBase!==void 0?this.#r=o.data.ringBufferBase:console.warn("[SuperSonic] Warning: ringBufferBase not provided by worklet"),o.data.bufferConstants!==void 0?(this.#e=o.data.bufferConstants,await this.initializeNTPTiming(),this.#K()):console.warn("[SuperSonic] Warning: bufferConstants not provided by worklet"),e()):t(new Error(o.data.error||"AudioWorklet initialization failed")))}};this.#n.port.addEventListener("message",i),this.#n.port.start()})}#q(){this.#n.port.onmessage=e=>{let{data:t}=e;switch(t.type){case"error":console.error("[Worklet] Error:",t.error),t.diagnostics&&(console.error("[Worklet] Diagnostics:",t.diagnostics),console.table(t.diagnostics)),this.#b++,this.onError&&this.onError(new Error(t.error));break;case"process_debug":break;case"debug":break;case"console":this.onConsoleMessage&&this.onConsoleMessage(t.message);break;case"version":this.onVersion&&this.onVersion(t.version);break}}}#H(){if(!this.#t||!this.#e||!this.#r)return null;let e=this.#r+this.#e.METRICS_START,t=this.#e.METRICS_SIZE/4,r=new Uint32Array(this.#t,e,t);return{processCount:Atomics.load(r,0),bufferOverruns:Atomics.load(r,1),messagesProcessed:Atomics.load(r,2),messagesDropped:Atomics.load(r,3),schedulerQueueDepth:Atomics.load(r,4),schedulerQueueMax:Atomics.load(r,5),schedulerQueueDropped:Atomics.load(r,6)}}#Z(){if(!this.#t||!this.#e||!this.#r)return null;let e=new Int32Array(this.#t),t=this.#r+this.#e.CONTROL_START,r=Atomics.load(e,(t+0)/4),i=Atomics.load(e,(t+4)/4),o=Atomics.load(e,(t+8)/4),a=Atomics.load(e,(t+12)/4),l=Atomics.load(e,(t+16)/4),u=Atomics.load(e,(t+20)/4),c=(r-i+this.#e.IN_BUFFER_SIZE)%this.#e.IN_BUFFER_SIZE,f=(o-a+this.#e.OUT_BUFFER_SIZE)%this.#e.OUT_BUFFER_SIZE,h=(l-u+this.#e.DEBUG_BUFFER_SIZE)%this.#e.DEBUG_BUFFER_SIZE;return{inBufferUsed:{bytes:c,percentage:Math.round(c/this.#e.IN_BUFFER_SIZE*100)},outBufferUsed:{bytes:f,percentage:Math.round(f/this.#e.OUT_BUFFER_SIZE*100)},debugBufferUsed:{bytes:h,percentage:Math.round(h/this.#e.DEBUG_BUFFER_SIZE*100)}}}#j(){if(!this.#t||!this.#e||!this.#r)return null;let e=this.#r+this.#e.METRICS_START,t=this.#e.METRICS_SIZE/4,r=new Uint32Array(this.#t,e,t);return{preschedulerPending:r[7],preschedulerPeak:r[8],preschedulerSent:r[9],bundlesDropped:r[10],retriesSucceeded:r[11],retriesFailed:r[12],bundlesScheduled:r[13],eventsCancelled:r[14],totalDispatches:r[15],messagesRetried:r[16],retryQueueSize:r[17],retryQueueMax:r[18],oscInMessagesReceived:r[19],oscInDroppedMessages:r[20],oscInWakeups:r[21],oscInTimeouts:r[22],debugMessagesReceived:r[23],debugWakeups:r[24],debugTimeouts:r[25],debugBytesRead:r[26]}}#Y(){let e=performance.now(),t={messagesSent:this.#T,messagesReceived:this.#E,errors:this.#b},r=this.#H();r&&Object.assign(t,r);let i=this.#Z();i&&Object.assign(t,i);let o=this.#j();o&&Object.assign(t,o);let a=performance.now()-e;return a>1&&console.warn(`[SuperSonic] Slow metrics gathering: ${a.toFixed(2)}ms`),t}#G(){this.#c&&clearInterval(this.#c),this.#c=setInterval(()=>{if(this.onMetricsUpdate){if(this.#O){console.warn("[SuperSonic] Metrics gathering took >100ms, skipping this interval");return}this.#O=!0;try{let e=this.#Y();this.onMetricsUpdate(e)}catch(e){console.error("[SuperSonic] Metrics gathering failed:",e)}finally{this.#O=!1}}},100)}#J(){this.#c&&(clearInterval(this.#c),this.#c=null)}async send(e,...t){this.#f("send OSC messages");let r=t.map(a=>{if(typeof a=="string")return{type:"s",value:a};if(typeof a=="number")return{type:Number.isInteger(a)?"i":"f",value:a};if(a instanceof Uint8Array||a instanceof ArrayBuffer)return{type:"b",value:a instanceof ArrayBuffer?new Uint8Array(a):a};throw new Error(`Unsupported argument type: ${typeof a}`)}),i={address:e,args:r},o=s.osc.encode(i);return this.sendOSC(o)}#f(e="perform this operation"){if(!this.#l)throw new Error(`SuperSonic not initialized. Call init() before attempting to ${e}.`)}async sendOSC(e,t={}){this.#f("send OSC data");let r=this.#X(e),i=await this.#ee(r);this.#T++,this.onMessageSent&&this.onMessageSent(i);let o=this.#le(i),a={...t};o&&(a.audioTimeS=o.audioTimeS,a.currentTimeS=o.currentTimeS),this.#o.send(i,a)}get audioContext(){return this.#i}get workletNode(){return this.#n}get osc(){return this.#o}getStatus(){return{initialized:this.#l,capabilities:this.#u,bootStats:this.bootStats,audioContextState:this.#i?.state}}getConfig(){return this.config?{memory:{...this.config.memory},worldOptions:{...this.config.worldOptions}}:null}async destroy(){this.#I(),this.#J(),this.#o&&(this.#o.terminate(),this.#o=null),this.#n&&(this.#n.disconnect(),this.#n=null),this.#i&&(await this.#i.close(),this.#i=null),this.#s&&(this.#s.destroy(),this.#s=null),this.#t=null,this.#l=!1,this.loadedSynthDefs.clear()}waitForTimeSync(){return this.#f("wait for time sync"),new Float64Array(this.#t,this.#r+this.#e.NTP_START_TIME_START,1)[0]}async loadSample(e,t,r=0,i=0){this.#f("load samples");let o=await this.#_().prepareFromFile({bufnum:e,path:t,startFrame:r,numFrames:i});return await this.send("/b_allocPtr",e,o.ptr,o.numFrames,o.numChannels,o.sampleRate,o.uuid),o.allocationComplete}async loadSynthDef(e){if(!this.#l)throw new Error("SuperSonic not initialized. Call init() first.");try{let t=await fetch(e);if(!t.ok)throw new Error(`Failed to load synthdef from ${e}: ${t.status} ${t.statusText}`);let r=await t.arrayBuffer(),i=new Uint8Array(r);await this.send("/d_recv",i);let o=this.#Q(e);o&&this.loadedSynthDefs.add(o)}catch(t){throw console.error("[SuperSonic] Failed to load synthdef:",t),t}}async loadSynthDefs(e){if(!this.#l)throw new Error("SuperSonic not initialized. Call init() first.");if(!this.#g)throw new Error(`synthdefBaseURL not configured. Please set it in SuperSonic constructor options.
|
|
18
|
+
Example: new SuperSonic({ synthdefBaseURL: "./dist/synthdefs/" })
|
|
19
|
+
Or use CDN: new SuperSonic({ synthdefBaseURL: "https://unpkg.com/supersonic-scsynth-synthdefs@latest/synthdefs/" })
|
|
20
|
+
Or install: npm install supersonic-scsynth-synthdefs`);let t={};await Promise.all(e.map(async i=>{try{let o=`${this.#g}${i}.scsyndef`;await this.loadSynthDef(o),t[i]={success:!0}}catch(o){console.error(`[SuperSonic] Failed to load ${i}:`,o),t[i]={success:!1,error:o.message}}}));let r=Object.values(t).filter(i=>i.success).length;return t}async sync(e){if(!this.#l)throw new Error("SuperSonic not initialized. Call init() first.");if(!Number.isInteger(e))throw new Error("sync() requires an integer syncId parameter");let t=new Promise((r,i)=>{let o=setTimeout(()=>{this.#a&&this.#a.delete(e),i(new Error("Timeout waiting for /synced response"))},1e4),a=l=>{clearTimeout(o),this.#a.delete(e),r()};this.#a||(this.#a=new Map),this.#a.set(e,a)});await this.send("/sync",e),await t}allocBuffer(e){return this.#f("allocate buffers"),this.#s.allocate(e)}freeBuffer(e){return this.#f("free buffers"),this.#s.free(e)}getBufferView(e,t){return this.#f("get buffer views"),this.#s.getView(e,t)}getBufferPoolStats(){return this.#f("get buffer pool stats"),this.#s.getStats()}getDiagnostics(){return this.#f("get diagnostics"),{buffers:this.#s.getDiagnostics(),synthdefs:{count:this.loadedSynthDefs.size}}}async initializeNTPTiming(){if(!this.#e||!this.#i)return;let e;for(;e=this.#i.getOutputTimestamp(),!(e.contextTime>0);)await new Promise(a=>setTimeout(a,50));let i=(performance.timeOrigin+e.performanceTime)/1e3+2208988800-e.contextTime,o=new Float64Array(this.#t,this.#r+this.#e.NTP_START_TIME_START,1);o[0]=i,this.#w=i}updateDriftOffset(){if(!this.#e||!this.#i||this.#w===void 0)return;let e=this.#i.getOutputTimestamp(),o=(performance.timeOrigin+e.performanceTime)/1e3+2208988800-this.#w-e.contextTime,a=Math.round(o*1e3),l=new Int32Array(this.#t,this.#r+this.#e.DRIFT_OFFSET_START,1);Atomics.store(l,0,a)}getDriftOffset(){if(!this.#e)return 0;let e=new Int32Array(this.#t,this.#r+this.#e.DRIFT_OFFSET_START,1);return Atomics.load(e,0)}#K(){this.#I(),this.#m=setInterval(()=>{this.updateDriftOffset()},15e3)}#I(){this.#m&&(clearInterval(this.#m),this.#m=null)}#Q(e){return!e||typeof e!="string"?null:(e.split("/").filter(Boolean).pop()||e).replace(/\.scsyndef$/i,"")}#X(e){if(e instanceof Uint8Array)return e;if(e instanceof ArrayBuffer)return new Uint8Array(e);throw new Error("oscData must be ArrayBuffer or Uint8Array")}async#ee(e){let t={metadata:!0,unpackSingleArgs:!1};try{let r=s.osc.decode(e,t),{packet:i,changed:o}=await this.#R(r);return o?s.osc.encode(i):e}catch(r){throw console.error("[SuperSonic] Failed to prepare OSC packet:",r),r}}async#R(e){if(e&&e.address){let{message:t,changed:r}=await this.#te(e);return{packet:t,changed:r}}if(this.#ae(e)){let t=await Promise.all(e.packets.map(o=>this.#R(o)));if(!t.some(o=>o.changed))return{packet:e,changed:!1};let i=t.map(o=>o.packet);return{packet:{timeTag:e.timeTag,packets:i},changed:!0}}return{packet:e,changed:!1}}async#te(e){switch(e.address){case"/b_alloc":return{message:await this.#se(e),changed:!0};case"/b_allocRead":return{message:await this.#re(e),changed:!0};case"/b_allocReadChannel":return{message:await this.#ie(e),changed:!0};default:return{message:e,changed:!1}}}async#re(e){let t=this.#_(),r=this.#k(e.args,0,"/b_allocRead requires a buffer number"),i=this.#F(e.args,1,"/b_allocRead requires a file path"),o=this.#A(e.args,2,0),a=this.#A(e.args,3,0),l=await t.prepareFromFile({bufnum:r,path:i,startFrame:o,numFrames:a});return this.#C(l.allocationComplete,`/b_allocRead ${r}`),this.#M(r,l)}async#ie(e){let t=this.#_(),r=this.#k(e.args,0,"/b_allocReadChannel requires a buffer number"),i=this.#F(e.args,1,"/b_allocReadChannel requires a file path"),o=this.#A(e.args,2,0),a=this.#A(e.args,3,0),l=[];for(let c=4;c<(e.args?.length||0)&&this.#P(e.args[c]);c++)l.push(Math.floor(this.#y(e.args[c])));let u=await t.prepareFromFile({bufnum:r,path:i,startFrame:o,numFrames:a,channels:l.length>0?l:null});return this.#C(u.allocationComplete,`/b_allocReadChannel ${r}`),this.#M(r,u)}async#se(e){let t=this.#_(),r=this.#k(e.args,0,"/b_alloc requires a buffer number"),i=this.#k(e.args,1,"/b_alloc requires a frame count"),o=2,a=1,l=this.#i?.sampleRate||44100;this.#P(this.#p(e.args,o))&&(a=Math.max(1,this.#A(e.args,o,1)),o++),this.#p(e.args,o)?.type==="b"&&o++,this.#P(this.#p(e.args,o))&&(l=this.#y(this.#p(e.args,o)));let u=await t.prepareEmpty({bufnum:r,numFrames:i,numChannels:a,sampleRate:l});return this.#C(u.allocationComplete,`/b_alloc ${r}`),this.#M(r,u)}#M(e,t){return{address:"/b_allocPtr",args:[this.#v(e),this.#v(t.ptr),this.#v(t.numFrames),this.#v(t.numChannels),this.#ne(t.sampleRate),this.#oe(t.uuid)]}}#v(e){return{type:"i",value:Math.floor(e)}}#ne(e){return{type:"f",value:e}}#oe(e){return{type:"s",value:String(e)}}#p(e,t){if(Array.isArray(e))return e[t]}#y(e){if(e!=null)return typeof e=="object"&&Object.prototype.hasOwnProperty.call(e,"value")?e.value:e}#k(e,t,r){let i=this.#y(this.#p(e,t));if(!Number.isFinite(i))throw new Error(r);return Math.floor(i)}#A(e,t,r=0){let i=this.#y(this.#p(e,t));return Number.isFinite(i)?Math.floor(i):r}#F(e,t,r){let i=this.#y(this.#p(e,t));if(typeof i!="string")throw new Error(r);return i}#P(e){if(!e)return!1;let t=this.#y(e);return Number.isFinite(t)}#C(e,t){!e||typeof e.catch!="function"||e.catch(r=>{console.error(`[SuperSonic] ${t} allocation failed:`,r)})}#_(){if(!this.#s)throw new Error("Buffer manager not ready. Call init() before issuing buffer commands.");return this.#s}#ae(e){return e&&e.timeTag!==void 0&&Array.isArray(e.packets)}#le(e){if(e.length<16||String.fromCharCode.apply(null,e.slice(0,8))!=="#bundle\0")return null;let i=new Float64Array(this.#t,this.#r+this.#e.NTP_START_TIME_START,1)[0];if(i===0)return console.warn("[SuperSonic] NTP start time not yet initialized"),null;let o=new Int32Array(this.#t,this.#r+this.#e.DRIFT_OFFSET_START,1),l=Atomics.load(o,0)/1e3,u=new Int32Array(this.#t,this.#r+this.#e.GLOBAL_OFFSET_START,1),f=Atomics.load(u,0)/1e3,h=i+l+f,d=new DataView(e.buffer,e.byteOffset),p=d.getUint32(8,!1),m=d.getUint32(12,!1);if(p===0&&(m===0||m===1))return null;let E=p+m/4294967296-h,v=this.#i.currentTime;return{audioTimeS:E,currentTimeS:v}}};export{Q as SuperSonic};
|
|
3441
21
|
/*! osc.js 2.4.5, Copyright 2024 Colin Clark | github.com/colinbdclark/osc.js */
|