Sc.wasm - SuperCollider for the web

I am really happy to share a first prototype of SuperCollider in the browser with you!

https://sc.dennis-scheiba.com/

The last few weeks I was working on porting scsynth to wasm (again). I was already familiar with porting scsynth to wasm via emscripten (also big thx to Hanns Holger Rutz, who wrote the first implementation), but I had to redo it to make use of AudioWorklets, so scsynth runs in the audio thread of the browser.
Contrary to SuperSonic, this PR aimed to just introduce minor tweaks and additions to the source code, so it will hopefully become part of the sc repo via this PR. I am thinking about licensing the wasm part via AGPL, which I think is taking the spirit of SC into the domain of the internet. Please write if you think this would not be appropriate or if something else should be considered (the only alternative would be to use GPL 3.0 as-is).

When I finished the PR, I thought about how hard it could be to port sclang to wasm. I always thought that this would way above my head and not really feasible, but sometimes ignorance is a bliss, and since I had emscripten setup, I just gave it a try: primarily I just ripped out the network stack and had to rewrite the AppClock scheduling and replace the TerminalClient with a WasmClient and no-op some primitives, and after fixing some nasty threading and ringbuffer bugs, I managed to get sclang starting, compiling the class library and now also communicating with the server via an emulated network socket. I am about finishing a PR for this, but this also only required minor changes to the codebase (around 1k lines?).

Some things which are not implemented yet but will be looked into once things got more stable:

  • Buffer support and (persistent) Filesystem support - so no DiskIO UGens [plz write me if you have experience w/ cross-wasm-module persistent filesystems!] - maybe use this to introduce quarks/server extensions?
  • JS interaction (e.g. send JS values directly to sclang variables/functions and vice versa)
  • Improved IDE

I think there are many interesting things that can be done with this, especially new ways of collaborating within SC.

25 Likes

Hi,

thanks a lot for this! I tried it in a browser on my computer and the Ndef example worked wonderfully. Then I tried it on my phone as you mention the code can be executed by “double tapping with 3 fingers”. Frankly I don’t really understand how that’s meant. I tried to simply double-tap the screen with 3 fingers, 2 fingers, 1 finger… select the code, tapping again, and so on. Nothing happened except for keyboard coming up. How’s that gesture really meant? Thanks!

This is amazing :star_struck:

I also can’t get the mobile gesture to work and would much rather have a button or two.

Hey - I refactored the evaluate method and forgot to change the mobile code, it is fixed now. Place the cursor in a section of sc code and “double tap” with 3 fingers - I’ll think you’ll figure it out now it is working again.

BTW - make sure you are not in silent mode of your phone.

edit: I’ll may add a button for the mobile view tomorrow.

1 Like

Hey!

Yes!! It worked :partying_face: - thanks very much!

1 Like

This is supercool! I loaded track I’m working on with 500 lines of code on my Huawei P30 lite and it played just fine without stuttering (have to check with headphones).

But somehow it freezed after start playing. Is this running on your server or in my browser? Magic :slight_smile:

Amazing work! Looking forward to watching this grow :slight_smile:

2 Likes

Everything is running entirely in your browser. Please share if you some code which produces a crash - I ran into many crashes but I thought I got most of them fixed. It is still a very fragile piece of tech :wink:

That’s the snippet I tried. After running it i’m not able anymore to edit and also lines of code are missing


TempoClock.default.tempo = 90/60
s.boot
s.meter

// Reverb bus:

(
s.waitForBoot({
~verbBus = Bus.audio(s, 2);

SynthDef(\globalVerb, {
arg inbus = 0, out = 0, mix = 1, room = 1, damp = 0.2, amp = 1.5;
var dry, wet;
dry = In.ar(inbus, 2);
wet = FreeVerb2.ar(dry[0], dry[1], mix, room, damp);
Out.ar(out, wet * amp);
}).add;

s.sync;

~verbSynth = Synth.tail(s, \globalVerb, [\inbus, ~verbBus]);
});
)


(
SynthDef(\kick, {
arg out = 0, amp = 0.5, pan = 0, atk = 0.001, dec = 0.22;

var pitchEnv, bodyEnv, body, click, clickEnv, sig;

// kurzer Pitch-Fall fĂĽr mehr "Punch"


pitchEnv = EnvGen.kr(Env([100, 55], [0.03], -4));

// Body etwas länger
bodyEnv = EnvGen.kr(
Env.perc(atk, dec, 1, -4),
doneAction: 2
);

body = SinOsc.ar(pitchEnv) * bodyEnv;

// etwas definierterer Attack
clickEnv = EnvGen.kr(
Env.perc(0.0005, 0.01, 1, -8)
);

click = BPF.ar(WhiteNoise.ar(1), 2500, 0.8) * clickEnv * 0.25;

// leichte zusätzliche Obertöne für kleine Speaker
sig = body + click;
sig = (sig * 2.5).tanh * 0.5;

Out.ar(out, Pan2.ar(sig * amp, pan));
}).add;


SynthDef(\bass, {
arg freq = 40, amp = 0.15, pan = 0,
atk = 2, dec = 1, sus = 0.8, rel = 6,
ffreq = 120, fmax = 1200, rq = 0.3;

var osc, sub, env, fenv, drift, sig;

// very slow pitch drift
drift = SinOsc.kr(0.03).range(0.995, 1.005);

// layered oscillators
osc = Saw.ar(freq * [0.995, 1.0, 1.005] * drift);
osc = Mix(osc) * 0.5;

// soft sub layer
sub = SinOsc.ar(freq * 0.5 , 0, 0.2);

// slow amplitude envelope

env = EnvGen.ar(Env.perc(atk, rel), doneAction: 2);

// slowly opening filter
fenv = EnvGen.ar(Env([ffreq, fmax], [rel * 0.7], \exp));

sig = osc + sub;

// resonant lowpass sounds richer than plain LPF
sig = RLPF.ar(sig, fenv, rq);


// gentle s aturation
//sig = tanh(sig * 1.5);

sig = sig * env * amp;

Out.ar(0, Pan2.ar(sig, pan));
}).add;

//    Pdef(\bass).play(quant: 0);

SynthDef(\whitenoise, {
arg atk=0.01, dec=0.1, amp=0.5, pan=0, filtfreq=15000;

var env, sig;


sig = Decay.ar(Dust.ar(100), 0.1) * WhiteNoise.ar;

sig = HPF.ar(sig, filtfreq);

env = EnvGen.ar(Env.perc(atk, dec), doneAction:2);

sig = sig * env * amp;

Out.ar(0, Pan2.ar(sig, pan));
}).add;

//{Decay.ar(Dust.ar(200), 1)}.plot(3)


SynthDef(\sinenoise, {

arg amps = 0.05 ,

ampn = 0.05,
freq = 440,
pan = 0,
reln = 0.3,
rels = 0.8,
send = 0;

var envn, envs;
var white, pink, noiseMix;
var sine, sig;

// ---------- Noise envelope ----------
envn = EnvGen.ar(
Env.perc(0.0001, reln, 1, -4),
doneAction: 0
);

// ---------- Sine envelope ----------
envs = EnvGen.ar(
Env.perc(0.001, rels, 1, -4),
doneAction: 2
);

// ---------- Noise sources ----------
white = WhiteNoise.ar;
pink = PinkNoise.ar;

// Smooth morph between noise types
noiseMix = XFade2.ar(
white,
pink,
SinOsc.kr(0.2)
) * envn;

// ---------- Sine source ----------
sine = SinOsc.ar(
freq
) * envs;

// ---------- Proper mixing ----------
sig = (noiseMix * 0.3 * ampn) + (sine * amps);
sig = HPF.ar(sig, 600);


sig = Pan2.ar(sig, pan);

Out.ar(~verbBus, sig * send);
Out.ar(0, sig);


}).add;


SynthDef(\spectrumSwarm, {
arg freq = 3520, amp = 0.3, panSpread = 0.8, rel = 1, dust = 12 ,
send = 0;

var freqs, sig, grains, env;

freqs = freq * [0.5, 1, 1.01, 1.5, 2, 2.02, 3];
grains = Dust.ar(dust);
env = EnvGen.ar(Env.perc(0.001, rel), doneAction: 2);

sig = Mix.fill(freqs.size, {
var osc;
osc = SinOsc.ar(
freqs.choose * LFNoise1.kr(0.5).range(0.99, 1.01)
);
osc * grains
});

sig = HPF.ar(sig, 300);
sig = Splay.ar(sig, panSpread);
sig = sig * env * amp;
Out.ar(~verbBus, sig * send);
Out.ar(0, sig);
}).add;


// This needs some more optimization
SynthDef(\saw, {
arg freq = 220, amp = 0.08, atk = 2, rel = 6, pan = 0,
ffreq = 900, rq = 0.25, detune = 0.015, send = 0.5;

var sig, env, freqs, drift, cutoff, widthMod;

// slow independent drift for each voice
drift = LFNoise1.kr([0.03, 0.05, 0.07, 0.11, 0.13]).range(1 -
detune, 1 + detune);

freqs = freq * drift * [0.5, 0.99, 1, 1.01, 2];

sig = Saw.ar(freqs);

// slow stereo movement
widthMod = SinOsc.kr(0.02).range(0.4, 1.0);
sig = Splay.ar(sig, widthMod);

env = EnvGen.ar(Env.perc(atk, rel, 1, -4), doneAction: 2);

// slow filter drift
cutoff = ffreq * LFNoise1.kr(0.04).range(0.6, 1.2);
cutoff = cutoff.clip(80, 2000);

// darker main filter
sig = RLPF.ar(sig, cutoff, rq);

// trim harsh top a bit more
sig = LPF.ar(sig, cutoff * 1.2);

// slight hollow/complex color
sig = BRF.ar(sig, LFNoise1.kr(0.03).range(300, 1200), 0.3);

// gentle saturation
sig = tanh(sig * 1.4);

sig = sig * env * amp;
sig = Balance2.ar(sig[0], sig[1], pan);
Out.ar(~verbBus, sig * send);
Out.ar(0, sig);
}).add;


SynthDef(\klank, {
arg freq = 440, amp = 0.05, pan = 0, rel = 2, send = 0.1;

var harm, amps, ring, exc, sig, env;

harm = [1, 2, 3, 4];
amps = [0.2, 0.15, 0.1, 0.05];
ring = [1, 1.2, 0.8, 1.5];

exc = PinkNoise.ar(0.02) * Decay2.ar(Impulse.ar(1), 0.001, 0.1);

sig = Klank.ar(`[harm, amps, ring], exc, freq);

env = EnvGen.kr(Env.perc(0.01, rel), doneAction: 2);

sig = sig * env * amp;
Out.ar(~verbBus, sig * send);
Out.ar(0, Pan2.ar(sig, pan));
}).add;


// {EnvGen.kr(Env([0, 1, 0], [0.1, 5, 0.01], -8))}.plot(5)


Pdef(\drum1,
Pbind(
\instrument, \kick,

// Organic generative timing
//\dur, Pseq([1, 1, Prand([Rest(0.5),0.5,0.1]), 1, 1],inf),


\dur, Pseq([0.25, 1, Prand([0.5, Rest(0.5)]), 1, 1], inf),
//\dur, Pseq([1], inf),

//\freq, 55,
\amp, Pwhite(0.6, 0.65),
\dec, Pwhite(0.1, 0.2)
)
);


Pdef(\hh,
Pbind(
\instrument, \whitenoise,
\dur, Pwrand([2, 0.5, Rest(0.5)], [0.6, 0.25, 0.15], inf),
\amp, Pwhite(0.05, 0.1),
\dec, Pwhite(0.3, 0.52),
\pan, Pwhite(-0.3, 0.3),
\atk, Pwhite(0.01, 0.05),
\filtfreq, Pwhite(8000, 10000)
)
);


Pdef(\bass,
Pbind(
\instrument, \bass,
\dur, Pseq([4, 2], inf),
\freq, Pseq([45, 52, 60, 48].midicps, inf),
\amp, 0.12,
\atk, 0.02,
\dec, 0.5,
\rel, 5,
\ffreq, Pseg(Pseq([80], inf), 20),
\fmax, Pseg(Pseq([500, 550, 600, 650], inf), 28),
\rq, 0.1
)
);



Pdef(\sines,
Pbind(
\instrument, \sinenoise,
\scale, Scale.minor,
\root, 9,
\degree, Pwrand([0,2,4,5], [0.4,0.3,0.2,0.1], inf),
\octave, Pwhite(4,6),

\dur, Prand([0.25, 1, Prand([0.5, Rest(0.25)], Rest(4)), 1, 1],
inf),

\reln, Pwhite(0.0004, 0.002),
\rels, Pwhite(0.08, 5),
\amps, Pwhite(0.01, 0.02),
\ampn, Pwhite(0.1, 0.2),
\pan,  Pwhite(-0.8,0.8),
\send, 0.6
)
);

Pdef(\sines2,
Pbind(
\instrument, \sinenoise,
\scale, Scale.minor,
\root, 9,
\degree, Pwrand([0,2,4,5], [0.4,0.3,0.2,0.1], inf),
\octave, Pwhite(7,8),

\dur, Prand([2, 0.5, Prand([1, Rest(1)], Rest(2)), 1,4], inf),

\reln, Pwhite(0.0004, 0.02),
\rels, Pwhite(0.08, 5),
\amps, Pwhite(0.005, 0.012),
\ampn, Pwhite(0.1, 0.2),
\pan,  Pwhite(-0.8,0.8)
)
);

Pdef(\noise2,
Pbind(
\instrument, \sinenoise,
\dur, Prand([0.5, Rest(1), 0.5], inf),
\reln, Pwhite(0.0001, 0.012),
\rels, 0.1,
\amps, Pwhite(0.01, 0.02),
\ampn, Pwhite(0.3, 0.5),
\pan,  Pwhite(-0.8,0.8),
\send, 1
)
);

Pdef(\noise3,
Pbind(
\instrument, \sinenoise,
\dur, Prand([Pseq([0.1, 0.1, 0.1, 0.1]), Rest(4)], inf),
\reln, Pwhite(0.0001, 0.012),
\rels, 0.1,
\amps, Pwhite(0.002, 0.003),
\ampn, Pwhite(0.04, 0.07),
\pan,  Pwhite(-0.8,0.8),
\send, 1
)
);


Pdef(\particles,
Pbind(
\instrument, \spectrumSwarm,
\dur, Pwhite(0.05, 0.2),
\freq, Pseg(Pseq([800, 2000, 5000, 1200], inf), Pseq([12, 18,
10], inf), \exp),
\rel, Pwhite(0.03, 0.2),
\dust, Pwhite(1, 4),
\amp, Pwhite(0.05, 0.18),
\send, 0.8
)
);


Pdef(\burst,
Pbind(
\instrument, \sinenoise,
\dur, Pseq([0.25, 0.25, 0.25, Rest(8)], inf),
\freq, Pexprand(1000, 12000),
\reln, 0.005,
\rels, 0.15,
\amps, Pwhite(0.01, 0.03),
\ampn, Pwhite(0.6, 1.2),
\pan, Pwhite(-1, 1)
)
);


Pdef(\sawpad,
Pbind(
\instrument, \saw,
\dur, Pseq([4], inf),
\freq, Pseq(([45, 52, 60, 48] + 12).midicps, inf),
/*\dur, Pseq([4, 6, 8], inf),
\freq, ([220, 233.08, 261.63, 311.13].choose * [0.5, 1, 2].choose),
*/\atk, Pwhite(2.0, 4.0),
\rel, Pwhite(5.0, 10.0),
\amp, Pwhite(0.03, 0.06),
\ffreq, Pwhite(500, 1200),
\rq, Pwhite(0.18, 0.35),
\pan, Pwhite(-0.4, 0.4),
\send, 0.5
)
);


Pdef(\pklank,
Pbind(
\instrument, \klank,
\freq, Pseq([440, 659.29, 523.25, 440],inf),
\dec, Pwhite(0.001, 0.12),
\amp, Pwhite(0.8, 0.9),
\pan, Pwhite(-1,1),
\rel, Pwhite(0.5, 4),
\dur, Pwhite(0.2, 4),
\send, 0.8
)
);


// Play the whole thing




~r = Routine({

Pdef.all.do(_.stop);

Pdef(\pklank).play(quant: 0);
Pdef(\bass).play(quant: 0);


32.wait;

Pdef(\sines).play(quant: 0);
Pdef(\particles).play(quant: 0);

32.wait;


Pdef(\drum1).play(quant: 0);
Pdef(\sines2).play(quant: 0);
Pdef(\pklank).stop;

32.wait;


Pdef(\sawpad).play(quant: 0);
Pdef(\noise3).play(quant: 0);


64.wait;

Pdef(\pklank).play(quant: 0);
Pdef(\noise2).play(quant: 0);
Pdef(\hh).play(quant: 0);
Pdef(\noise3).stop;

64.wait;


Pdef(\drum1).play(quant: 0);
// Pdef(\burst).play(quant: 0);
Pdef(\sawpad).play(quant: 0);
Pdef(\pklank).stop;

32.wait;

Pdef(\pklank).play(quant: 0);

16.wait;

// Optional: fade out layers one by one
Pdef(\sawpad).stop;
8.wait;
Pdef(\burst).stop;
8.wait;
Pdef(\noise2).stop;
Pdef(\particles).stop;
    Pdef(\drum1).stop;
Pdef(\hh).stop;

16.wait;

// Leave only drums and bass for outro
Pdef(\hh).stop;
Pdef(\sines).stop;
Pdef(\sines2).stop;
16.wait;

// Stop everything
Pdef(\drum1).stop;
Pdef(\bass).stop;
Pdef(\pklank).stop;
}).play;
)


~r.stop


1 Like

Should Ctrl . work or how can I stop?

On macOS you can stop with cmd + . using the meta key - I now also added the combinations ctrl key and alt key.

Thanks for sharing. I think one reason why it crashes could be ring-buffer issues. If these ring buffers screw up for some reason, I noticed that I get corrupted memory. I’ll let it run some more and wait for a crash/freeze.

2 Likes

Crtl . is not working in Brave / Android and also not an Firefox using Ubuntu.

This is wonderful and strange to use, nice one @dscheiba! Looking forward to seeing this working with strudel and superdirt. Strudel is already AGPLv3, so the license isn’t a concern for that project.

2 Likes

Took a full day to restructure the git history, but there is now a PR for sclang.wasm @ Add wasm support for sclang by capital-G · Pull Request #7440 · supercollider/supercollider · GitHub - hopefully it will be reviewed and merged soon :slight_smile:

I deployed a new version which makes sclang run in its own thread instead of the main thread - this should avoid any hickups/freezes of the browser tabs.

Please do a full reload to reset the cache! On macOS and firefox this is cmd shift r

I also rewrote the ringbuffer implementation for printing (thx christof!) and ditched the osc-reply ring buffer for a direct async call - I think this should get rid of most observed crashes / freezes. If these still occur, please write!

Also replaced my crude interpret method with the traditional Interpreter.interpretPrintCmdLine, so lines with a postfix comment like ().play; // some comment can now be evaluated properly

Forgot to deploy the new version on the server … sorry. Should be fixed now.

Yeah - this will be great! Do you have some recommendation regarding the filesystem on web workers? This will be the next thing I want to tackle (and is a bit tricky b/c I need to sync sclang and scsynth web workers), but first I want to get the current state merged as-is.

Also thinking about writing a small backend for code sharing like on strudel - but unsure if I’ll write such a thing in python or rust. Definitely need to step up my html/css game for that though…

2 Likes

I’ve mainly only worked on the pattern library in strudel, I’ll see if the other strudel folk have tips!

There used to be a shared pattern database thingie in strudel, but unfortunately we had to switch that off when some strudel tiktoks went viral due to far-right griefing. The remaining strudel sharing just encodes everything in the URL.

1 Like

I added a first draft of a backend to https://sc.dennis-scheiba.com/ and ported to codemirror6 which allows to

  • share code snippets via url: write code, click on share button, share the url [there is currently no guarantee that shared code snippets stay in the database b/c I may have to re-structure the db code.]
  • collab editing: click on collab, share the url and people can write (and evaluate) together - this is currently using yjs and I hope I didn’t screw up too much when porting it to rust.

I noticed that sometimes there is a big delay between sclang and scsynth execution - this is caused by the loading times of each, because they both start their clock when their wasm module got loaded (yeah…this will eventually be improved at some point, also addressing the clock drift issue). Simply reload the document.

I plan to have some kind of autocomplete next, but I have to touch the C++ code for that.
I also have a version where you can load buffers from URLs - this will eventually also allow to load quarks.

3 Likes

I checked it in the afternoon with my smartphone (Android). Somehow the blinking after triple clicking does not work anymore or it’s maybe an issue with the colors. The track played though.

Many thanks for this — I think it will be very useful in class.

  • I noticed that {...}.plot and s.meter do not appear to work. It would be very helpful to know which functionalities are currently supported when using https://sc.dennis-scheiba.com/. In the previous version, a code snippet and a comment were provided, so I was wondering whether it might be possible to include a brief summary of the functionalities currently supported in this version as well.

  • It would also be helpful to have a way of clearing the messages in the post window.

  • For Collab, an automatic refresh every two seconds would also be very convenient.

I am not sure how feasible any of these suggestions would be in practice, so apologies if they are difficult to implement. In any case, thank you very much for all your work on this — I have felt for quite some time that it would be wonderful to have functionality like this.

1 Like

Ah yeah - they changed the API in CM6(yay), but spend some time now to make it available again.

sclang.wasm acts like a headless version of sclang - so no gui functionality is available. You can use JavaScript/HTML though to build a gui - I think this could be pretty interesting.

Additionally: No network/OSC dispatching is available! All incoming messages are considered coming from the server, and all outgoing messages are considered to be sent to the server.

The current behavior is that the browser tab restores the document from when you closed the window, so I didn’t know how to “re-display” a notice. But it would be probably good to have a site linked where all the information is provided - maybe I’ll add this via a modal.
This is more like a quick demo page so people can try it out and give some feedback.
I think it would also great to have at some point a collection of examples pop up at random or have a button for them, like in hydra.

Yeah - for now you can use javascript via the browser console to do this - I’ll may add a button for that, but I don’t know where - I want to keep this more minimalistic approach.

document.querySelector("#textbox").value = ""

Currently it wraps/clears after 1500 lines (back to 500 lines?) in order to not clog the UI.

Ehm - why? If you open the collab link in another browser, both tabs should be synchronized automatically - doesn’t this work for you? You should also see the cursor of the other user/tab.

1 Like

When I previously asked for an automatic refresh every two seconds, it was because synchronization did not seem to be working automatically on my side at the time. However, I have just tested it again, and it now works as you described. So the feature is no longer necessary.

Thank you as well for your time and effort in implementing this!!!

2 Likes