SuperCollider 4: First Thoughts

Sclang is a strange hybrid between a compiled and interpreted language which I think has its roots in performance limitations of dynamic languages in the late 90s. It is an impressive project indeed, but no one would design a language like this in 2021. I think SuperCollider would be much more approachable (and maintainable!) if it would leverage an existing popular and stable scripting language. However, transitioning to another language would be an enormous undertaking and I don’t really see it happening soon…

SC3 is incredibly useful for scientific work that involves real-time audio processing. It would be a huge setback to ongoing academic work if a future SC4 were not compatible with SC3. Of course, sclang has its drawbacks, but the system shines in so many important ways: quick turnaround, decent speed for an interpreted language, great community, many contributed libraries, some students already know it, etc.

Here is my wish list for any upcoming version:

  1. Please don’t break SC3 code in such a way that existing sclang code and UGen code cannot be automatically converted by an upgrade procedure.

  2. In SynthDefs, allow functions to be evaluated on an arbitrary audio-rate trigger (on demand) instead of on every sample, maybe with a construct such as {…}.when(trigger), or OnDemand.ar(trigger, {…}); where “…” is any code allowed in a SynthDef that does not need an isochronous history, such as unops, binops, muladds, etc. On samples when there is no trigger, the function would just reiterate its latest output value, without recomputing. Why? There are many examples of processing that is event-driven, such as once per oscillatory period of the sound, especially in voice, speech and physically modelled musical instruments. This would of course increase CPU spiking, so SynthDefs using this mechanism a lot would need to be coded carefully, e.g. with several staggered triggers, but it could reduce server load by hundreds of times for those operations, running once per signal cycle rather than on every audio sample. I know that there is a small repertoire of Demand UGens, but the beauty would be for this to be a general mechanism.

  3. There appears to exist a scheme for sharing of memory buffers between the local server and the sclang process, that is used in the Scope classes, but this scheme seems not to be documented anywhere. I would love to be able to use it.

best,
sternsc

1 Like

A couple little ideas. I don’t think these are possible in sclang right now, but I’ve implented them on my own scsynth interface, so I know they’re feasible from that point of view. (Also they’re probably feasible without waiting for SC 4…)

Idea: SynthDefs can “call” other SynthDefs.
Why: code re-use, composability.
Implementation strategy: sclang macros. we’re not calling one bytecode synthdef from another, we’re basically just pasting in the sclang code from the first synthdef when we’re generating the bytecode for the second.

Idea: SynthDefs can take an Env as an input.
Why: we can already almost take an env as an input, but it needs to be input as a list, and the maximum length of the input list adds significant weight to the SynthDef bytecode, especially if we want multiple incoming Envs.
Implementation strategy: Have a secondary, behind the scenes SynthDef that loads the Env and sends it to a kbus, and bus it in to the main SynthDef.

1 Like

Check out the Demand UGen, and the rest of the demand rate UGens - these are even based, and can be driven by audio-rate triggers. This might be a solution for what you’re describing.

See SynthDef:wrap, which is probably the best-supported way of composing functions together into a single SynthDef. FWIW a compiled SynthDef (e.g. what you get when you do SynthDef(\foo, {}) loses some high-level information about its overall structure - this makes it not ideal / impossible to properly compose inside another SynthDef. The best thing to is write and store SynthDef “components” as functions and call them via wrap or e.g. ~filterModule.value(input, freq).

This is possible now. Envelope passing for Synths and Patterns

This came up in a different thread, but to re-iterate:

Adding classes without recompiling, and defining vars anywhere, are both VERY do-able projects that don’t even require super deep arcane expertise (though they would require a lot of research).

If anyone is interested and serious about taking on either of these projects, I would happily work with you to outline areas for investigation, the changes that would be required, and build a technical roadmap to get it implemented. These are both fun topics if you are interested in or want to build your skills around parsers, compilers.

1 Like

This is possible now. Envelope passing for Synths and Patterns

Thanks for that example. The bytecode compilation limits the size and complexity of the Envelope to a fixed number of points. For many common use cases, like an ADSR or perc type env, of course this is sufficient. But it may not work for envelopes that may be flat or arbitrarily curvy over a long timespan. Or at least the bytecode default would have to be much longer. My bytecode default of 128 points (~ 128 * 4 bytes) was making my compiled synthdefs very heavy and slow to load.

Glad to know that code composition is well-developed! I work primarily with the server and not the sc language, and since bytecode synthdefs can’t call each other, I had to reconstruct that on my end. I came to the same conclusion as you did, that because of the structure of the compiled bytecode, it has to be done on the language side, not the bytecode side.

How quickly this thread got forgotten: Composite notes: Auto-mapping synth controls to events – it’s doing literally exactly what you propose here.

hjh

Oh, nice!
Sorry for the noise… my unfamiliarity with sclang (as opposed to scsynth) really kinda skews things for me. :stuck_out_tongue:

It’s not really noise – that thread is just a proof of concept, showing that it’s possible, and not even impossibly complex. But it doesn’t seem to have gained much traction either – you’re interested in that feature as is, but a couple of other people on the thread seemed interested mainly in an automation mechanism (iow the idea of single notes made of a composite of multiple synth nodes seems to lie outside of SC culture’s usual bounds, and my example wasn’t wholly successful in dislodging this mental bias).

hjh

@scztt:

Adding classes without recompiling, and defining vars anywhere, are both VERY do-able projects that don’t even require super deep arcane expertise (though they would require a lot of research).

Hello,

I’m curious about the variable declaration idea.

Do people actually want to be able to write things like this?

{var x
;{x = 0
 ;var x
 ;x = 1
 ;[\inner,x].postln}.value
;[\outer,x].postln}.value

Would this print [inner,1] and then [outer,0]?

I can’t see how this is particularly easy to implement?

You can’t write this is Scheme, or ML/Haskell, or Smalltalk.

You can, naturally, write it in Javascript, but…

(function()
{var x
;(function ()
 {x = 0
 ;var x
 ;x = 1
 ;console.log(["inner",x])})()
;console.log(["outer",x])})()

> ["inner", 1]
> ["outer", undefined]

Is the interpreter just shuffling the “var x” to the start of the function?

(function()
{var x
;(function ()
 {x = 0
 ;console.log(["outer?",x])
 ;var x
 ;x = 1
 ;console.log(["inner",x])})()
;console.log(["outer",x])})()

> ["outer?", 0]
> ["inner", 1]
> ["outer", undefined]

(Chromium, Version 89.0.4389.114)

I’m not sure Javascript is always the best model?

Are there nicer implementations?

Best,
Rohan

Do people actually want to be able to write things like this?

{var x
;{x = 0
 ;var x
 ;x = 1
 ;[\inner,x].postln}.value
;[\outer,x].postln}.value

Your example is more about variable shadowing - which is completely orthogonal to the proposal of allowing variable declarations anywhere in a block (not only at the start).

I can’t see how this is particularly easy to implement?

It is quite easy to implement and it’s how lexical scoping usually works. In a dynamic language, a variable declaration would just insert the variable in the current block environment, so a subsequent look up would find it there, otherwise it would look in the parent environment (recursively). This automatically allows shadowing and it also doesn’t impose any restrictions on where you put the declaration. This is of course a bit simplified. If you’re interested in this kind of stuff, I would recommend studying the Lua source code.

You can’t write this is Scheme, or ML/Haskell, or Smalltalk.

I’m not sure if you refer to shadowing or declaring variables anywhere in a block, but both are supported by virtually every mainstream language(JS, C++, Java, C#, Lua, etc.). Python is a bit special because it doesn’t have explicit variable declarations.

BTW, variable shadowing is supported by sclang:

f = {
	var x = 1, b;
	x.postln;
	b = {
		var x = 2;
		x.postln;
	};
	b.();
	x.postln;
}
f.()

What you can not do is declare variables in the middle of a block.

I’m not sure Javascript is always the best model?

It’s certainly not the best model. var in JavaScript has a peculiar behavior called hoisting (JavaScript Hoisting). In fact, usage of var is now discouraged in favor of let (which doesn’t support hoisting).

Again, any modern programming language allows to declare variables anywhere. Even C allows it since C99 :slight_smile:

Ah, Lua, I don’t know it!

(function ()
 local x;
 (function ()
   x = 0;
   local x;
   x = 1;
   print("inner",x);
   end) ();
  print("outer",x);
  end) ()

This does work as expected (at https://www.lua.org/cgi-bin/demo)

I’m not sure if you refer to shadowing or declaring variables anywhere in a block […]

I was thinking about scoping. In the languages I know you always write something that has the shape let ... in .....

The Lua rules (https://www.lua.org/manual/5.3/manual.html#3.5) seem very simple! Thanks for the reference.

I guess local x simply translates as let x in! And so a simple implementation would just create a new frame for each local.

Thanks again,
Rohan

BTW, if you’re interested in programming language design/implementation, I can warmly recommend the following book: https://craftinginterpreters.com/ You can read it online for free!

1 Like

@Spacechild1 :

Thanks for the Nystrom reference!

In fact, usage of var is now discouraged in favor of let (which doesn’t support hoisting).

Curiously, replacing var with let gives:

(function()
{let x
;(function ()
 {x = 0
 ;let x
 ;x = 1
 ;console.log(["inner",x])})()
;console.log(["outer",x])})()

> Uncaught ReferenceError: Cannot access 'x' before initialization

This is described as “TDZ combined with lexical scoping” at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let. It seems quite strange!

Best,
Rohan

I don’t think we have any obligation to support obscurantist corner cases. I’d vote no on hoisting.

hjh

if “sclang” gets a speed boost from a strongly typed system I’d be up for that. But I do like the flexibility of weakly typed.

it would be great to be able to use SC4 however you want and not worry about the licensing

On the open licensing remark… I would make standalone & deployable applications for every possible platform a reality… granting full rights and power of licensing to the author who compiles it…

Compilation would black box all of the underlying source/SC code… the author’s work could remain “Do what thou wilt”

Looks like let is even stricter than variable declarations in other languages, which is probably not a bad thing. Generally, shadowing should be avoided.

I don’t think we have any obligation to support obscurantist corner cases. I’d vote no on hoisting.

No one argued for hoisting! The example demonstrates shadowing. It’s just a quirk of JS that it decides to hoist the second variable declaration to the top of the block.

Note that sclang does allow variable shadowing. Confusing situations like above just don’t occur because variables can only be declared at the top. Once this restriction is lifted, the “corner case” above would just be a natural result of the scoping rules. You could make it a syntax error, but it probably takes extra work.

Needless to say, variable shadowing is confusing and error prone and should be avoided in the first place.