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