No - the RingBuffer itself is the mechanism by which the JS web workers and the WASM sides communicate. At boot a bunch of memory is allocated for OSC to be written into, then copied out of for the OSC comms to work. It’s a accessed as a RingBuffer to allow for the fact that each side is running in a separate thread and so as not to ever block the audio thread/drop packets if it wants to send a small burst of traffic to JS.
Oh, and yeah, I mis-remembered - immediate messages (timetag 0 or 1) just execute at the buffer boundary, no subsample offset involved. Only scheduled bundles get that precision. The whole scheduler bit was quite tricky to work with and I got confused quite a bit
Wrt OSC timing - the AudioWorklet’s process() callback gets called with AudioContext’s current time. I calculate a fixed offset once at boot between AudioContext time and NTP time) and then just add that offset to convert the current AudioContext time to OSC/NTP time for the scheduler.
Ah, I see! In that case it totally makes sense to poll the reply FIFOs synchronously and forward the messages to the JS RingBuffers.
Wrt OSC timing - the AudioWorklet’s process() callback gets called with AudioContext’s current time. I calculate a fixed offset once at boot between AudioContext time and NTP time) and then just add that offset to convert the current AudioContext time to OSC/NTP time for the scheduler.
Aha! The problem is that the audio clock and the NTP clock run at slightly different rates. If you just calculate the offset at boot time, this will cause scheduled messages to gradually drift over time. If the audio clock runs faster than the NTP clock, you will eventually get lots of late bundles.
scsynth deals with this problem in two ways:
Jack/Portaudio: sample NTP in the audio callback and estimate the actual time with a DLL filter, see SC_TimeDLL.hpp and SC_PortAudioDriver::PortAudioCallback in SC_PortAudio.cpp
CoreAudio: periodically recalculate offset between the audio clock and the NTP clock, see resyncThreadFunc in SC_CoreAudio.cpp
I don’t know which would work better in the context of an AudioWorklet.
Side note: Supernova initially used your approach of only calculating the initial offset, but users reported problems due to clock drift. As a consequence, Supernova now uses the same time synchronization as scsynth’s Portaudio/Jack backend. (One can still switch to the old behavior by setting useSystemClock to false, but there is hardly a good reason for doing so.)
Amazing, that’s super useful - thanks. Yep, I definitely plan to account for drift. I also want to integrate Ableton Link. Do you happen to have any experience/advice on doing this?
Note that unfortunately you still need to ensure that the COOP and CORP headers are set correctly on your web server to enable the shared memory (that allows JS to talk to scsynth). Something like:
Definitely don’t look at my code then - I clearly made extensive use of agents to help navigate my way through this very tricky C++ stuff. However all the architectural decisions were mine - so I’m happy to take the flack for that
Just a heads up that I discovered that this isn’t correct. Whilst this worked on my local machine it didn’t work when I pushed the demo site. Some browsers apparently relax strictness when serving local content.
The fact that I use a SharedArrayBuffer for comms between JS and the scsynth AudioWorklet (which seems the only way of bridging the two) means that the core of SuperSonic’s scsynth code (the WASM and workers that all use the SharedArrayBuffer) must be served by the server with the correct headers and not from a CDN.
just a heads up that I just pushed SuperSonic v0.23 to GitHub and npm.
It now supports audio input, can be served over a CDN, supports FFT ugens, resume and is fully tested.
It’s well on the way to being production-ready and tagged v1 - I just want to give it a little more time to bake. It’s definitely a good time to have a play with it and please do send any feedback my way.
Another brief heads up. SuperSonic v0.63 is now out:
This introduces a new native build using JUCE to provide the audio thread and manage the audio hardware. It’s highly experimental but can already act as a drop-in replacement for scsynth inside Sonic Pi. One of the main benefits of the JUCE backend is that you can live-swap audio hardware without having to tear everything down and re-start.
There are also significant improvements to the robustness, of the OSC scheduling and pre-scheduling architecture.