SuperSonic: SuperCollider in the browser

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 :slight_smile:

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:

  1. 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

  2. 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?

1 Like

I have no experience with AbletonLink, but others have :slight_smile:

Hiya,

just a heads up that I pushed SuperSonic v0.2 to npm.

Whilst things are still very much alpha-quality, I’m really happy with how things are progressing. Pretty much all the foundational aspects are there:

  • Support for most OSC API methods (those that don’t rely on disk or IO)
  • Support for allocating buffers
  • Support for loading buffers with audio files (i.e. pre-recorded samples) - these need to be fetchable via JS.
  • Drift correction
  • Sub-sample scheduling accuracy

I’ve also inserted a pre-scheduler which supports cancelling (this is something I’ve always wanted with scsynth).

As it’s published on npm, it means you can use CDNs to fetch everything (scsynth + the optional Sonic Pi synthdefs and samples).

Here’s the simplest web-page that triggers the Amen Break as a sample using supersonic-scsynth:

<!DOCTYPE html>
<html>
  <head>
    <title>SuperSonic Simple Demo (CDN)</title>
  </head>
  <body>
    <h1>SuperSimple SuperSonic (CDN)</h1>
    <button id="play">Play Amen Break</button>

    <script type="module">
      import { SuperSonic } from "https://unpkg.com/supersonic-scsynth@latest";

      const button = document.getElementById("play");
      let sonic = null;

      button.onclick = async () => {
        if (!sonic) {
          button.textContent = "Booting scsynth...";

          sonic = new SuperSonic({
            sampleBaseURL:
              "https://unpkg.com/supersonic-scsynth-samples@latest/samples/",
            synthdefBaseURL:
              "https://unpkg.com/supersonic-scsynth-synthdefs@latest/synthdefs/",
          });
          await sonic.init();
          await sonic.loadSynthDefs(["sonic-pi-basic_stereo_player"]);
          await sonic.loadSample(0, "loop_amen.flac");
          button.textContent = "Play Amen Break";
        }
        sonic.send("/s_new", "sonic-pi-basic_stereo_player", -1, 0, 0, "buf", 0 );
      };
    </script>
  </body>
</html>

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:

    response['Cross-Origin-Opener-Policy'] = 'same-origin'
    response['Cross-Origin-Embedder-Policy'] = 'require-corp'
    response['Cross-Origin-Resource-Policy'] = 'cross-origin'
    response['X-Content-Type-Options'] = 'nosniff'
5 Likes

Just FYI - I will work on making the existing PR make use of audio worklets, such that it becomes part of mainline SC.

5 Likes

Awesome. That would be amazing!

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 :wink:

1 Like

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.

Also, I should add that I spruced up the example to demo a working synth arp, sample playback and live FX manipulation: SuperSonic - SuperCollider's Synthesis Engine in the Browser

4 Likes

Hi there,

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.

I’m really excited to see what you do with it…

5 Likes

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.

You can read more about this work here: https://www.patreon.com/posts/supersonic-152978806

Happy to hear feedback.

5 Likes