supersonic-scsynth 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,11 +21,11 @@ A WebAssembly port of SuperCollider's scsynth audio synthesis engine for the bro
21
21
  await sonic.loadSynthDefs(['sonic-pi-beep']);
22
22
 
23
23
  // Trigger the synth
24
- sonic.send('/s_new', 'sonic-pi-beep', -1, 0, 1, 'note', 60);
24
+ sonic.send('/s_new', 'sonic-pi-beep', -1, 0, 0, 'note', 60);
25
25
 
26
26
  // Load and play a sample
27
27
  sonic.send('/b_allocRead', 0, 'bd_haus.flac');
28
- sonic.send('/s_new', 'sonic-pi-basic_mono_player', -1, 0, 1, 'buf', 0);
28
+ sonic.send('/s_new', 'sonic-pi-basic_mono_player', -1, 0, 0, 'buf', 0);
29
29
  </script>
30
30
  ```
31
31
 
@@ -90,7 +90,7 @@ const sonic = new SuperSonic({
90
90
  **Common OSC commands:**
91
91
  ```javascript
92
92
  sonic.send('/notify', 1); // Enable notifications
93
- sonic.send('/s_new', 'synth-name', -1, 0, 1); // Create synth
93
+ sonic.send('/s_new', 'synth-name', -1, 0, 0); // Create synth
94
94
  sonic.send('/n_set', 1000, 'freq', 440.0, 'amp', 0.5); // Set parameters
95
95
  sonic.send('/n_free', 1000); // Free node
96
96
  sonic.send('/b_allocRead', 0, 'sample.flac'); // Load audio buffer
@@ -1,301 +1,3 @@
1
- // js/lib/scsynth_osc.js
2
- var ScsynthOSC = class {
3
- constructor() {
4
- this.workers = {
5
- oscOut: null,
6
- oscIn: null,
7
- debug: null
8
- };
9
- this.callbacks = {
10
- onOSCMessage: null,
11
- onDebugMessage: null,
12
- onError: null,
13
- onInitialized: null
14
- };
15
- this.initialized = false;
16
- this.sharedBuffer = null;
17
- this.ringBufferBase = null;
18
- this.bufferConstants = null;
19
- }
20
- /**
21
- * Initialize all workers with SharedArrayBuffer
22
- */
23
- async init(sharedBuffer, ringBufferBase, bufferConstants) {
24
- if (this.initialized) {
25
- console.warn("[ScsynthOSC] Already initialized");
26
- return;
27
- }
28
- this.sharedBuffer = sharedBuffer;
29
- this.ringBufferBase = ringBufferBase;
30
- this.bufferConstants = bufferConstants;
31
- try {
32
- this.workers.oscOut = new Worker("./dist/workers/osc_out_worker.js");
33
- this.workers.oscIn = new Worker("./dist/workers/osc_in_worker.js");
34
- this.workers.debug = new Worker("./dist/workers/debug_worker.js");
35
- this.setupWorkerHandlers();
36
- const initPromises = [
37
- this.initWorker(this.workers.oscOut, "OSC OUT"),
38
- this.initWorker(this.workers.oscIn, "OSC IN"),
39
- this.initWorker(this.workers.debug, "DEBUG")
40
- ];
41
- await Promise.all(initPromises);
42
- this.workers.oscIn.postMessage({ type: "start" });
43
- this.workers.debug.postMessage({ type: "start" });
44
- this.initialized = true;
45
- if (this.callbacks.onInitialized) {
46
- this.callbacks.onInitialized();
47
- }
48
- } catch (error) {
49
- console.error("[ScsynthOSC] Initialization failed:", error);
50
- if (this.callbacks.onError) {
51
- this.callbacks.onError(error);
52
- }
53
- throw error;
54
- }
55
- }
56
- /**
57
- * Initialize a single worker
58
- */
59
- initWorker(worker, name) {
60
- return new Promise((resolve, reject) => {
61
- const timeout = setTimeout(() => {
62
- reject(new Error(`${name} worker initialization timeout`));
63
- }, 5e3);
64
- const handler = (event) => {
65
- if (event.data.type === "initialized") {
66
- clearTimeout(timeout);
67
- worker.removeEventListener("message", handler);
68
- resolve();
69
- }
70
- };
71
- worker.addEventListener("message", handler);
72
- worker.postMessage({
73
- type: "init",
74
- sharedBuffer: this.sharedBuffer,
75
- ringBufferBase: this.ringBufferBase,
76
- bufferConstants: this.bufferConstants
77
- });
78
- });
79
- }
80
- /**
81
- * Set up message handlers for all workers
82
- */
83
- setupWorkerHandlers() {
84
- this.workers.oscIn.onmessage = (event) => {
85
- const data = event.data;
86
- switch (data.type) {
87
- case "messages":
88
- if (this.callbacks.onOSCMessage) {
89
- data.messages.forEach((msg) => {
90
- this.callbacks.onOSCMessage(msg);
91
- });
92
- }
93
- break;
94
- case "error":
95
- console.error("[ScsynthOSC] OSC IN error:", data.error);
96
- if (this.callbacks.onError) {
97
- this.callbacks.onError(data.error, "oscIn");
98
- }
99
- break;
100
- }
101
- };
102
- this.workers.debug.onmessage = (event) => {
103
- const data = event.data;
104
- switch (data.type) {
105
- case "debug":
106
- if (this.callbacks.onDebugMessage) {
107
- data.messages.forEach((msg) => {
108
- this.callbacks.onDebugMessage(msg);
109
- });
110
- }
111
- break;
112
- case "error":
113
- console.error("[ScsynthOSC] DEBUG error:", data.error);
114
- if (this.callbacks.onError) {
115
- this.callbacks.onError(data.error, "debug");
116
- }
117
- break;
118
- }
119
- };
120
- this.workers.oscOut.onmessage = (event) => {
121
- const data = event.data;
122
- switch (data.type) {
123
- case "error":
124
- console.error("[ScsynthOSC] OSC OUT error:", data.error);
125
- if (this.callbacks.onError) {
126
- this.callbacks.onError(data.error, "oscOut");
127
- }
128
- break;
129
- }
130
- };
131
- }
132
- /**
133
- * Send OSC data (message or bundle)
134
- * - OSC messages are sent immediately
135
- * - OSC bundles are scheduled based on waitTimeMs (calculated by SuperSonic)
136
- *
137
- * @param {Uint8Array} oscData - Binary OSC data (message or bundle)
138
- * @param {Object} options - Optional metadata (editorId, runTag, waitTimeMs)
139
- */
140
- send(oscData, options = {}) {
141
- if (!this.initialized) {
142
- console.error("[ScsynthOSC] Not initialized");
143
- return;
144
- }
145
- const { editorId = 0, runTag = "", waitTimeMs = null } = options;
146
- this.workers.oscOut.postMessage({
147
- type: "send",
148
- oscData,
149
- editorId,
150
- runTag,
151
- waitTimeMs
152
- });
153
- }
154
- /**
155
- * Send OSC data immediately, ignoring any bundle timestamps
156
- * - Extracts all messages from bundles
157
- * - Sends all messages immediately to scsynth
158
- * - For applications that don't expect server-side scheduling
159
- *
160
- * @param {Uint8Array} oscData - Binary OSC data (message or bundle)
161
- */
162
- sendImmediate(oscData) {
163
- if (!this.initialized) {
164
- console.error("[ScsynthOSC] Not initialized");
165
- return;
166
- }
167
- this.workers.oscOut.postMessage({
168
- type: "sendImmediate",
169
- oscData
170
- });
171
- }
172
- /**
173
- * Cancel scheduled OSC bundles by editor and tag
174
- */
175
- cancelEditorTag(editorId, runTag) {
176
- if (!this.initialized) return;
177
- this.workers.oscOut.postMessage({
178
- type: "cancelEditorTag",
179
- editorId,
180
- runTag
181
- });
182
- }
183
- /**
184
- * Cancel all scheduled OSC bundles from an editor
185
- */
186
- cancelEditor(editorId) {
187
- if (!this.initialized) return;
188
- this.workers.oscOut.postMessage({
189
- type: "cancelEditor",
190
- editorId
191
- });
192
- }
193
- /**
194
- * Cancel all scheduled OSC bundles
195
- */
196
- cancelAll() {
197
- if (!this.initialized) return;
198
- this.workers.oscOut.postMessage({
199
- type: "cancelAll"
200
- });
201
- }
202
- /**
203
- * Clear debug buffer
204
- */
205
- clearDebug() {
206
- if (!this.initialized) return;
207
- this.workers.debug.postMessage({
208
- type: "clear"
209
- });
210
- }
211
- /**
212
- * Get statistics from all workers
213
- */
214
- async getStats() {
215
- if (!this.initialized) {
216
- return null;
217
- }
218
- const statsPromises = [
219
- this.getWorkerStats(this.workers.oscOut, "oscOut"),
220
- this.getWorkerStats(this.workers.oscIn, "oscIn"),
221
- this.getWorkerStats(this.workers.debug, "debug")
222
- ];
223
- const results = await Promise.all(statsPromises);
224
- return {
225
- oscOut: results[0],
226
- oscIn: results[1],
227
- debug: results[2]
228
- };
229
- }
230
- /**
231
- * Get stats from a single worker
232
- */
233
- getWorkerStats(worker, name) {
234
- return new Promise((resolve) => {
235
- const timeout = setTimeout(() => {
236
- resolve({ error: "Timeout getting stats" });
237
- }, 1e3);
238
- const handler = (event) => {
239
- if (event.data.type === "stats") {
240
- clearTimeout(timeout);
241
- worker.removeEventListener("message", handler);
242
- resolve(event.data.stats);
243
- }
244
- };
245
- worker.addEventListener("message", handler);
246
- worker.postMessage({ type: "getStats" });
247
- });
248
- }
249
- /**
250
- * Set callback for OSC messages received from scsynth
251
- */
252
- onOSCMessage(callback) {
253
- this.callbacks.onOSCMessage = callback;
254
- }
255
- /**
256
- * Set callback for debug messages
257
- */
258
- onDebugMessage(callback) {
259
- this.callbacks.onDebugMessage = callback;
260
- }
261
- /**
262
- * Set callback for errors
263
- */
264
- onError(callback) {
265
- this.callbacks.onError = callback;
266
- }
267
- /**
268
- * Set callback for initialization complete
269
- */
270
- onInitialized(callback) {
271
- this.callbacks.onInitialized = callback;
272
- }
273
- /**
274
- * Terminate all workers and cleanup
275
- */
276
- terminate() {
277
- if (this.workers.oscOut) {
278
- this.workers.oscOut.postMessage({ type: "stop" });
279
- this.workers.oscOut.terminate();
280
- }
281
- if (this.workers.oscIn) {
282
- this.workers.oscIn.postMessage({ type: "stop" });
283
- this.workers.oscIn.terminate();
284
- }
285
- if (this.workers.debug) {
286
- this.workers.debug.postMessage({ type: "stop" });
287
- this.workers.debug.terminate();
288
- }
289
- this.workers = {
290
- oscOut: null,
291
- oscIn: null,
292
- debug: null
293
- };
294
- this.initialized = false;
295
- console.log("[ScsynthOSC] All workers terminated");
296
- }
297
- };
298
-
299
1
  // js/vendor/osc.js/osc.js
300
2
  var osc = {};
301
3
  var osc = osc || {};
@@ -879,188 +581,494 @@ EventEmitter.prototype.removeListener = function() {
879
581
  osc.fireBundleEvents(port, packet, timeTag, packetInfo);
880
582
  }
881
583
  };
882
- osc.fireBundleEvents = function(port, bundle, timeTag, packetInfo) {
883
- port.emit("bundle", bundle, timeTag, packetInfo);
884
- for (var i = 0; i < bundle.packets.length; i++) {
885
- var packet = bundle.packets[i];
886
- osc.firePacketEvents(port, packet, bundle.timeTag, packetInfo);
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);
887
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
+ };
888
739
  };
889
- osc.fireClosedPortSendError = function(port, msg) {
890
- msg = msg || "Can't send packets on a closed osc.Port object. Please open (or reopen) this Port by calling open().";
891
- port.emit("error", msg);
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");
892
749
  };
893
- osc.Port = function(options) {
894
- this.options = options || {};
895
- this.on("data", this.decodeOSC.bind(this));
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);
896
756
  };
897
- var p = osc.Port.prototype = Object.create(EventEmitter.prototype);
898
- p.constructor = osc.Port;
899
- p.send = function(oscPacket) {
900
- var args = Array.prototype.slice.call(arguments), encoded = this.encodeOSC(oscPacket), buf = osc.nativeBuffer(encoded);
901
- args[0] = buf;
902
- this.sendRaw.apply(this, args);
757
+ p.close = function(code, reason) {
758
+ this.socket.close(code, reason);
903
759
  };
904
- p.encodeOSC = function(packet) {
905
- packet = packet.buffer ? packet.buffer : packet;
906
- var encoded;
907
- try {
908
- encoded = osc.writePacket(packet, this.options);
909
- } catch (err) {
910
- this.emit("error", err);
911
- }
912
- return encoded;
760
+ osc.WebSocketPort.setupSocketForBinary = function(socket) {
761
+ socket.binaryType = osc.isNode ? "nodebuffer" : "arraybuffer";
913
762
  };
914
- p.decodeOSC = function(data, packetInfo) {
915
- data = osc.byteArray(data);
916
- this.emit("raw", data, packetInfo);
763
+ })();
764
+ var osc_default = osc;
765
+ var { readPacket, writePacket, readMessage, writeMessage, readBundle, writeBundle } = osc;
766
+
767
+ // js/lib/scsynth_osc.js
768
+ var ScsynthOSC = class {
769
+ constructor() {
770
+ this.workers = {
771
+ oscOut: null,
772
+ oscIn: null,
773
+ debug: null
774
+ };
775
+ this.callbacks = {
776
+ onOSCMessage: null,
777
+ onDebugMessage: null,
778
+ onError: null,
779
+ onInitialized: null
780
+ };
781
+ this.initialized = false;
782
+ this.sharedBuffer = null;
783
+ this.ringBufferBase = null;
784
+ this.bufferConstants = null;
785
+ }
786
+ /**
787
+ * Initialize all workers with SharedArrayBuffer
788
+ */
789
+ async init(sharedBuffer, ringBufferBase, bufferConstants) {
790
+ if (this.initialized) {
791
+ console.warn("[ScsynthOSC] Already initialized");
792
+ return;
793
+ }
794
+ this.sharedBuffer = sharedBuffer;
795
+ this.ringBufferBase = ringBufferBase;
796
+ this.bufferConstants = bufferConstants;
917
797
  try {
918
- var packet = osc.readPacket(data, this.options);
919
- this.emit("osc", packet, packetInfo);
920
- osc.firePacketEvents(this, packet, void 0, packetInfo);
921
- } catch (err) {
922
- this.emit("error", err);
798
+ this.workers.oscOut = new Worker("./dist/workers/osc_out_worker.js");
799
+ this.workers.oscIn = new Worker("./dist/workers/osc_in_worker.js");
800
+ this.workers.debug = new Worker("./dist/workers/debug_worker.js");
801
+ this.setupWorkerHandlers();
802
+ const initPromises = [
803
+ this.initWorker(this.workers.oscOut, "OSC OUT"),
804
+ this.initWorker(this.workers.oscIn, "OSC IN"),
805
+ this.initWorker(this.workers.debug, "DEBUG")
806
+ ];
807
+ await Promise.all(initPromises);
808
+ this.workers.oscIn.postMessage({ type: "start" });
809
+ this.workers.debug.postMessage({ type: "start" });
810
+ this.initialized = true;
811
+ if (this.callbacks.onInitialized) {
812
+ this.callbacks.onInitialized();
813
+ }
814
+ } catch (error) {
815
+ console.error("[ScsynthOSC] Initialization failed:", error);
816
+ if (this.callbacks.onError) {
817
+ this.callbacks.onError(error);
818
+ }
819
+ throw error;
923
820
  }
924
- };
925
- osc.SLIPPort = function(options) {
926
- var that = this;
927
- var o = this.options = options || {};
928
- o.useSLIP = o.useSLIP === void 0 ? true : o.useSLIP;
929
- this.decoder = new slip.Decoder({
930
- onMessage: this.decodeOSC.bind(this),
931
- onError: function(err) {
932
- that.emit("error", err);
821
+ }
822
+ /**
823
+ * Initialize a single worker
824
+ */
825
+ initWorker(worker, name) {
826
+ return new Promise((resolve, reject) => {
827
+ const timeout = setTimeout(() => {
828
+ reject(new Error(`${name} worker initialization timeout`));
829
+ }, 5e3);
830
+ const handler = (event) => {
831
+ if (event.data.type === "initialized") {
832
+ clearTimeout(timeout);
833
+ worker.removeEventListener("message", handler);
834
+ resolve();
835
+ }
836
+ };
837
+ worker.addEventListener("message", handler);
838
+ worker.postMessage({
839
+ type: "init",
840
+ sharedBuffer: this.sharedBuffer,
841
+ ringBufferBase: this.ringBufferBase,
842
+ bufferConstants: this.bufferConstants
843
+ });
844
+ });
845
+ }
846
+ /**
847
+ * Set up message handlers for all workers
848
+ */
849
+ setupWorkerHandlers() {
850
+ this.workers.oscIn.onmessage = (event) => {
851
+ const data = event.data;
852
+ switch (data.type) {
853
+ case "messages":
854
+ if (this.callbacks.onOSCMessage) {
855
+ data.messages.forEach((msg) => {
856
+ if (msg.oscData) {
857
+ try {
858
+ const options = { metadata: false, unpackSingleArgs: false };
859
+ const decoded = osc_default.readPacket(msg.oscData, options);
860
+ this.callbacks.onOSCMessage(decoded);
861
+ } catch (e) {
862
+ console.error("[ScsynthOSC] Failed to decode OSC message:", e, msg);
863
+ }
864
+ }
865
+ });
866
+ }
867
+ break;
868
+ case "error":
869
+ console.error("[ScsynthOSC] OSC IN error:", data.error);
870
+ if (this.callbacks.onError) {
871
+ this.callbacks.onError(data.error, "oscIn");
872
+ }
873
+ break;
874
+ }
875
+ };
876
+ this.workers.debug.onmessage = (event) => {
877
+ const data = event.data;
878
+ switch (data.type) {
879
+ case "debug":
880
+ if (this.callbacks.onDebugMessage) {
881
+ data.messages.forEach((msg) => {
882
+ this.callbacks.onDebugMessage(msg);
883
+ });
884
+ }
885
+ break;
886
+ case "error":
887
+ console.error("[ScsynthOSC] DEBUG error:", data.error);
888
+ if (this.callbacks.onError) {
889
+ this.callbacks.onError(data.error, "debug");
890
+ }
891
+ break;
892
+ }
893
+ };
894
+ this.workers.oscOut.onmessage = (event) => {
895
+ const data = event.data;
896
+ switch (data.type) {
897
+ case "error":
898
+ console.error("[ScsynthOSC] OSC OUT error:", data.error);
899
+ if (this.callbacks.onError) {
900
+ this.callbacks.onError(data.error, "oscOut");
901
+ }
902
+ break;
933
903
  }
904
+ };
905
+ }
906
+ /**
907
+ * Send OSC data (message or bundle)
908
+ * - OSC messages are sent immediately
909
+ * - OSC bundles are scheduled based on waitTimeMs (calculated by SuperSonic)
910
+ *
911
+ * @param {Uint8Array} oscData - Binary OSC data (message or bundle)
912
+ * @param {Object} options - Optional metadata (editorId, runTag, waitTimeMs)
913
+ */
914
+ send(oscData, options = {}) {
915
+ if (!this.initialized) {
916
+ console.error("[ScsynthOSC] Not initialized");
917
+ return;
918
+ }
919
+ const { editorId = 0, runTag = "", waitTimeMs = null } = options;
920
+ this.workers.oscOut.postMessage({
921
+ type: "send",
922
+ oscData,
923
+ editorId,
924
+ runTag,
925
+ waitTimeMs
926
+ });
927
+ }
928
+ /**
929
+ * Send OSC data immediately, ignoring any bundle timestamps
930
+ * - Extracts all messages from bundles
931
+ * - Sends all messages immediately to scsynth
932
+ * - For applications that don't expect server-side scheduling
933
+ *
934
+ * @param {Uint8Array} oscData - Binary OSC data (message or bundle)
935
+ */
936
+ sendImmediate(oscData) {
937
+ if (!this.initialized) {
938
+ console.error("[ScsynthOSC] Not initialized");
939
+ return;
940
+ }
941
+ this.workers.oscOut.postMessage({
942
+ type: "sendImmediate",
943
+ oscData
934
944
  });
935
- var decodeHandler = o.useSLIP ? this.decodeSLIPData : this.decodeOSC;
936
- this.on("data", decodeHandler.bind(this));
937
- };
938
- p = osc.SLIPPort.prototype = Object.create(osc.Port.prototype);
939
- p.constructor = osc.SLIPPort;
940
- p.encodeOSC = function(packet) {
941
- packet = packet.buffer ? packet.buffer : packet;
942
- var framed;
943
- try {
944
- var encoded = osc.writePacket(packet, this.options);
945
- framed = slip.encode(encoded);
946
- } catch (err) {
947
- this.emit("error", err);
945
+ }
946
+ /**
947
+ * Cancel scheduled OSC bundles by editor and tag
948
+ */
949
+ cancelEditorTag(editorId, runTag) {
950
+ if (!this.initialized) return;
951
+ this.workers.oscOut.postMessage({
952
+ type: "cancelEditorTag",
953
+ editorId,
954
+ runTag
955
+ });
956
+ }
957
+ /**
958
+ * Cancel all scheduled OSC bundles from an editor
959
+ */
960
+ cancelEditor(editorId) {
961
+ if (!this.initialized) return;
962
+ this.workers.oscOut.postMessage({
963
+ type: "cancelEditor",
964
+ editorId
965
+ });
966
+ }
967
+ /**
968
+ * Cancel all scheduled OSC bundles
969
+ */
970
+ cancelAll() {
971
+ if (!this.initialized) return;
972
+ this.workers.oscOut.postMessage({
973
+ type: "cancelAll"
974
+ });
975
+ }
976
+ /**
977
+ * Clear debug buffer
978
+ */
979
+ clearDebug() {
980
+ if (!this.initialized) return;
981
+ this.workers.debug.postMessage({
982
+ type: "clear"
983
+ });
984
+ }
985
+ /**
986
+ * Get statistics from all workers
987
+ */
988
+ async getStats() {
989
+ if (!this.initialized) {
990
+ return null;
948
991
  }
949
- return framed;
950
- };
951
- p.decodeSLIPData = function(data, packetInfo) {
952
- this.decoder.decode(data, packetInfo);
953
- };
954
- osc.relay = function(from, to, eventName, sendFnName, transformFn, sendArgs) {
955
- eventName = eventName || "message";
956
- sendFnName = sendFnName || "send";
957
- transformFn = transformFn || function() {
958
- };
959
- sendArgs = sendArgs ? [null].concat(sendArgs) : [];
960
- var listener = function(data) {
961
- sendArgs[0] = data;
962
- data = transformFn(data);
963
- to[sendFnName].apply(to, sendArgs);
964
- };
965
- from.on(eventName, listener);
992
+ const statsPromises = [
993
+ this.getWorkerStats(this.workers.oscOut, "oscOut"),
994
+ this.getWorkerStats(this.workers.oscIn, "oscIn"),
995
+ this.getWorkerStats(this.workers.debug, "debug")
996
+ ];
997
+ const results = await Promise.all(statsPromises);
966
998
  return {
967
- eventName,
968
- listener
999
+ oscOut: results[0],
1000
+ oscIn: results[1],
1001
+ debug: results[2]
969
1002
  };
970
- };
971
- osc.relayPorts = function(from, to, o) {
972
- var eventName = o.raw ? "raw" : "osc", sendFnName = o.raw ? "sendRaw" : "send";
973
- return osc.relay(from, to, eventName, sendFnName, o.transform);
974
- };
975
- osc.stopRelaying = function(from, relaySpec) {
976
- from.removeListener(relaySpec.eventName, relaySpec.listener);
977
- };
978
- osc.Relay = function(port1, port2, options) {
979
- var o = this.options = options || {};
980
- o.raw = false;
981
- this.port1 = port1;
982
- this.port2 = port2;
983
- this.listen();
984
- };
985
- p = osc.Relay.prototype = Object.create(EventEmitter.prototype);
986
- p.constructor = osc.Relay;
987
- p.open = function() {
988
- this.port1.open();
989
- this.port2.open();
990
- };
991
- p.listen = function() {
992
- if (this.port1Spec && this.port2Spec) {
993
- this.close();
1003
+ }
1004
+ /**
1005
+ * Get stats from a single worker
1006
+ */
1007
+ getWorkerStats(worker, name) {
1008
+ return new Promise((resolve) => {
1009
+ const timeout = setTimeout(() => {
1010
+ resolve({ error: "Timeout getting stats" });
1011
+ }, 1e3);
1012
+ const handler = (event) => {
1013
+ if (event.data.type === "stats") {
1014
+ clearTimeout(timeout);
1015
+ worker.removeEventListener("message", handler);
1016
+ resolve(event.data.stats);
1017
+ }
1018
+ };
1019
+ worker.addEventListener("message", handler);
1020
+ worker.postMessage({ type: "getStats" });
1021
+ });
1022
+ }
1023
+ /**
1024
+ * Set callback for OSC messages received from scsynth
1025
+ */
1026
+ onOSCMessage(callback) {
1027
+ this.callbacks.onOSCMessage = callback;
1028
+ }
1029
+ /**
1030
+ * Set callback for debug messages
1031
+ */
1032
+ onDebugMessage(callback) {
1033
+ this.callbacks.onDebugMessage = callback;
1034
+ }
1035
+ /**
1036
+ * Set callback for errors
1037
+ */
1038
+ onError(callback) {
1039
+ this.callbacks.onError = callback;
1040
+ }
1041
+ /**
1042
+ * Set callback for initialization complete
1043
+ */
1044
+ onInitialized(callback) {
1045
+ this.callbacks.onInitialized = callback;
1046
+ }
1047
+ /**
1048
+ * Terminate all workers and cleanup
1049
+ */
1050
+ terminate() {
1051
+ if (this.workers.oscOut) {
1052
+ this.workers.oscOut.postMessage({ type: "stop" });
1053
+ this.workers.oscOut.terminate();
994
1054
  }
995
- this.port1Spec = osc.relayPorts(this.port1, this.port2, this.options);
996
- this.port2Spec = osc.relayPorts(this.port2, this.port1, this.options);
997
- var closeListener = this.close.bind(this);
998
- this.port1.on("close", closeListener);
999
- this.port2.on("close", closeListener);
1000
- };
1001
- p.close = function() {
1002
- osc.stopRelaying(this.port1, this.port1Spec);
1003
- osc.stopRelaying(this.port2, this.port2Spec);
1004
- this.emit("close", this.port1, this.port2);
1005
- };
1006
- })();
1007
- (function() {
1008
- "use strict";
1009
- osc.WebSocket = typeof WebSocket !== "undefined" ? WebSocket : void 0;
1010
- osc.WebSocketPort = function(options) {
1011
- osc.Port.call(this, options);
1012
- this.on("open", this.listen.bind(this));
1013
- this.socket = options.socket;
1014
- if (this.socket) {
1015
- if (this.socket.readyState === 1) {
1016
- osc.WebSocketPort.setupSocketForBinary(this.socket);
1017
- this.emit("open", this.socket);
1018
- } else {
1019
- this.open();
1020
- }
1055
+ if (this.workers.oscIn) {
1056
+ this.workers.oscIn.postMessage({ type: "stop" });
1057
+ this.workers.oscIn.terminate();
1021
1058
  }
1022
- };
1023
- var p = osc.WebSocketPort.prototype = Object.create(osc.Port.prototype);
1024
- p.constructor = osc.WebSocketPort;
1025
- p.open = function() {
1026
- if (!this.socket || this.socket.readyState > 1) {
1027
- this.socket = new osc.WebSocket(this.options.url);
1059
+ if (this.workers.debug) {
1060
+ this.workers.debug.postMessage({ type: "stop" });
1061
+ this.workers.debug.terminate();
1028
1062
  }
1029
- osc.WebSocketPort.setupSocketForBinary(this.socket);
1030
- var that = this;
1031
- this.socket.onopen = function() {
1032
- that.emit("open", that.socket);
1033
- };
1034
- this.socket.onerror = function(err) {
1035
- that.emit("error", err);
1036
- };
1037
- };
1038
- p.listen = function() {
1039
- var that = this;
1040
- this.socket.onmessage = function(e) {
1041
- that.emit("data", e.data, e);
1042
- };
1043
- this.socket.onclose = function(e) {
1044
- that.emit("close", e);
1063
+ this.workers = {
1064
+ oscOut: null,
1065
+ oscIn: null,
1066
+ debug: null
1045
1067
  };
1046
- that.emit("ready");
1047
- };
1048
- p.sendRaw = function(encoded) {
1049
- if (!this.socket || this.socket.readyState !== 1) {
1050
- osc.fireClosedPortSendError(this);
1051
- return;
1052
- }
1053
- this.socket.send(encoded);
1054
- };
1055
- p.close = function(code, reason) {
1056
- this.socket.close(code, reason);
1057
- };
1058
- osc.WebSocketPort.setupSocketForBinary = function(socket) {
1059
- socket.binaryType = osc.isNode ? "nodebuffer" : "arraybuffer";
1060
- };
1061
- })();
1062
- var osc_default = osc;
1063
- var { readPacket, writePacket, readMessage, writeMessage, readBundle, writeBundle } = osc;
1068
+ this.initialized = false;
1069
+ console.log("[ScsynthOSC] All workers terminated");
1070
+ }
1071
+ };
1064
1072
 
1065
1073
  // node_modules/@thi.ng/api/typedarray.js
1066
1074
  var GL2TYPE = {
@@ -1596,6 +1604,7 @@ var SuperSonic = class {
1596
1604
  this.wasmModule = null;
1597
1605
  this.wasmInstance = null;
1598
1606
  this.bufferPool = null;
1607
+ this.pendingBufferOps = /* @__PURE__ */ new Map();
1599
1608
  this.wasmTimeOffset = null;
1600
1609
  this._timeOffsetPromise = null;
1601
1610
  this._resolveTimeOffset = null;
@@ -1790,6 +1799,8 @@ var SuperSonic = class {
1790
1799
  this.osc.onOSCMessage((msg) => {
1791
1800
  if (msg.address === "/buffer/freed") {
1792
1801
  this._handleBufferFreed(msg.args);
1802
+ } else if (msg.address === "/buffer/allocated") {
1803
+ this._handleBufferAllocated(msg.args);
1793
1804
  }
1794
1805
  if (this.onMessageReceived) {
1795
1806
  this.stats.messagesReceived++;
@@ -2077,14 +2088,24 @@ var SuperSonic = class {
2077
2088
  ptr: allocatedPtr,
2078
2089
  size: bytesNeeded
2079
2090
  });
2091
+ const uuid = crypto.randomUUID();
2092
+ const allocationComplete = new Promise((resolve, reject) => {
2093
+ const timeout = setTimeout(() => {
2094
+ this.pendingBufferOps.delete(uuid);
2095
+ reject(new Error(`Timeout waiting for buffer ${bufnum} allocation`));
2096
+ }, 5e3);
2097
+ this.pendingBufferOps.set(uuid, { resolve, reject, timeout });
2098
+ });
2080
2099
  await this.send(
2081
2100
  "/b_allocPtr",
2082
2101
  bufnum,
2083
2102
  allocatedPtr,
2084
2103
  framesToRead,
2085
2104
  numChannels,
2086
- audioBuffer.sampleRate
2105
+ audioBuffer.sampleRate,
2106
+ uuid
2087
2107
  );
2108
+ await allocationComplete;
2088
2109
  if (completionMsg) {
2089
2110
  }
2090
2111
  } catch (error) {
@@ -2114,10 +2135,6 @@ var SuperSonic = class {
2114
2135
  * Handle /buffer/freed message from WASM
2115
2136
  */
2116
2137
  _handleBufferFreed(args) {
2117
- if (args.length < 2) {
2118
- console.warn("[SuperSonic] Invalid /buffer/freed message:", args);
2119
- return;
2120
- }
2121
2138
  const bufnum = args[0];
2122
2139
  const offset = args[1];
2123
2140
  const bufferInfo = this.allocatedBuffers.get(bufnum);
@@ -2126,6 +2143,19 @@ var SuperSonic = class {
2126
2143
  this.allocatedBuffers.delete(bufnum);
2127
2144
  }
2128
2145
  }
2146
+ /**
2147
+ * Handle /buffer/allocated message with UUID correlation
2148
+ */
2149
+ _handleBufferAllocated(args) {
2150
+ const uuid = args[0];
2151
+ const bufnum = args[1];
2152
+ const pending = this.pendingBufferOps.get(uuid);
2153
+ if (pending) {
2154
+ clearTimeout(pending.timeout);
2155
+ pending.resolve({ bufnum });
2156
+ this.pendingBufferOps.delete(uuid);
2157
+ }
2158
+ }
2129
2159
  /**
2130
2160
  * /b_allocReadChannel bufnum path [startFrame numFrames channel1 channel2 ... completion]
2131
2161
  * Load specific channels from an audio file
@@ -2287,6 +2317,18 @@ var SuperSonic = class {
2287
2317
  this.initialized = false;
2288
2318
  console.log("[SuperSonic] Destroyed");
2289
2319
  }
2320
+ /**
2321
+ * Load a sample into a buffer and wait for confirmation
2322
+ * @param {number} bufnum - Buffer number
2323
+ * @param {string} path - Audio file path
2324
+ * @returns {Promise} Resolves when buffer is ready
2325
+ */
2326
+ async loadSample(bufnum, path, startFrame = 0, numFrames = 0) {
2327
+ if (!this.initialized) {
2328
+ throw new Error("SuperSonic not initialized. Call init() first.");
2329
+ }
2330
+ await this._allocReadBuffer(bufnum, path, startFrame, numFrames);
2331
+ }
2290
2332
  /**
2291
2333
  * Load a binary synthdef file and send it to scsynth
2292
2334
  * @param {string} path - Path or URL to the .scsyndef file
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "wasmFile": "scsynth-nrt.wasm",
3
- "buildId": "20251104-230238",
4
- "buildTime": "2025-11-04T23:02:38Z",
5
- "gitHash": "5d25004"
3
+ "buildId": "20251105-102519",
4
+ "buildTime": "2025-11-05T10:25:19Z",
5
+ "gitHash": "4ea9bb5"
6
6
  }
Binary file
@@ -71,11 +71,7 @@ function readMessages() {
71
71
 
72
72
  var currentTail = tail;
73
73
  var messagesRead = 0;
74
- // Process up to 10 messages per wake to balance:
75
- // - Low latency (don't block too long processing)
76
- // - Efficiency (batch processing when busy)
77
- // - Prevents starvation of other tasks
78
- var maxMessages = 10;
74
+ var maxMessages = 100;
79
75
 
80
76
  while (currentTail !== head && messagesRead < maxMessages) {
81
77
  var readPos = ringBufferBase + bufferConstants.OUT_BUFFER_START + currentTail;
@@ -115,7 +111,7 @@ function readMessages() {
115
111
  if (sequence !== expectedSeq) {
116
112
  var dropped = (sequence - expectedSeq + 0x100000000) & 0xFFFFFFFF;
117
113
  if (dropped < 1000) { // Sanity check
118
- console.warn('[OSCInWorker] Detected', dropped, 'dropped messages');
114
+ console.warn('[OSCInWorker] Detected', dropped, 'dropped messages (expected seq', expectedSeq, 'got', sequence, ')');
119
115
  stats.droppedMessages += dropped;
120
116
  }
121
117
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supersonic-scsynth",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "SuperCollider scsynth WebAssembly port for AudioWorklet - Run SuperCollider synthesis in the browser",
5
5
  "main": "dist/supersonic.js",
6
6
  "unpkg": "dist/supersonic.js",