Building a Racket client for Scsynth

I finally found the language of my dreams in Racket, but I was disappointed to learn there wasn’t any fully working SC client, so I’m trying to make one myself starting from the OSC messaging level, but I’m having some trouble finding resources on that level. Basically, all I could find contains more or less the same info you can find here:
https://doc.sccode.org/Reference/Server-Command-Reference.html

This leaves me with some questions:

  • how do I call a single UGen with an OSC message? I only see message /s_new for instantiating a new synth.
  • how do I build a SynthDef from the Racket client? I’ve read some comments saying it’s particularly difficult, many clients keep doing it from Sclang. Would it be easier perhaps to write a Racket macro that generates the Sclang code and sends it to the server as a string?

Another possibilty is porting Rohan Drape’s R6 Scheme client, if he’d be okay with that, though I don’t know if he visits this forum.

That has never been supported. (Edit: Come to think of it, this would be meaningless. You always need at least two UGens. You can’t hear any signal until it’s been written to a hardware bus with Out or a cousin… but Out doesn’t produce a signal. So at minimum, you need a signal source and an Out.)

The general process is:

  1. Build Control UGens based on function arguments. (This may or may not fit into Racket’s design. It isn’t strictly necessary – you could require that controls be created explicitly.)

  2. UGen object constructors build the graph of UGens.

  3. Optimization (math operators, remove dead units).

  4. Topological sort to streamline signal paths.

  5. Write the binary format.

Of these, 2 and 5 are pretty easy. 3 is not too bad. 4, I’ve never looked into at all but I think it’s a pretty standard algorithm.

hjh

1 Like

So the way to go is making a synth for each UGen, right?

Where can I find more info on UGen object constructors? For while I toyed with a Haskell client (also by Rohan Drape), and I remember how multiplying two UGens (or perhaps they were synths containing a single UGen) returned a (BinaryOpUGen “*” UGen1 UGen2) or something alike. Is this an UGen object constructor?
4 and 5 look pretty intimidating to me, I’ve never done any low level programming.

Thanks!

A SynthDef represents a network of connections between UGens. The connections are critical – you can’t avoid them.

A SynthDef containing one UGen is like an analog oscillator module that is not plugged into anything (including speakers). It may be producing a beautiful signal, but it doesn’t matter because you’ll never hear it.

A single-UGen SynthDef is equally pointless.

It’s just a method or function that creates a UGen… It takes the input arguments and remembers them. That’s the main purpose. Have a look in the SC class library.

Yes, math operators need to be overloaded.

4 is not strictly necessary (assuming UGens are always created after their inputs, which is a requirement in sclang). 3 isn’t strictly necessary either (it just improves performance, but a first version wouldn’t necessarily require that).

5, you can steal translate the logic from the SC class library. This part is pretty straightforward actually. Don’t let “binary” scare you. It’s writing bytes – “binary” only because it’s not a human-readable text format.

hjh

1 Like

So something like a data structure, merely to have a client-side representation? But then, how do I send that to the server?

For some reason I thought you could call them as nodes and connect them. Probably some syntactic sugar from Sclang made me see it that way ({}.play)

Thanks for your patience.

Yes, the client-side representation is to maintain the connections between units.

d = SynthDef(\xyz, {
	Out.ar(0, SinOsc.ar(440))
});

d.dumpUGens
[ 0_SinOsc, audio, [ 440, 0.0 ] ]
[ 1_Out, audio, [ 0, 0_SinOsc ] ]

d.children[1].inputs
-> [ 0, a SinOsc ]

So Out knows that its signal input is a SinOsc. From this, it’s possible to trace recursively through all the relationships.

But then, how do I send that to the server?

See SynthDef:writeDef in the SC class library. (This also calls UGen:writeDef for every one of the “children.”) Between those two methods, that should cover just about all of it.

All the code is public. And, this part of the code doesn’t rely on any special primitives in C++. (The file put methods call into C, but these are just standard file-op functions which every language should have.) Basically, just translate this into Racket.

Probably some syntactic sugar from Sclang made me see it that way ({}.play)

a = { SinOsc.ar(440) }.play;
a.trace;

TRACE 1001  temp__0    #units: 7
  unit 0 SinOsc
    in  440 0
    out -0.9855
  unit 1 Control
    in 
    out 0.02
  unit 2 BinaryOpUGen
    in  0.02 0
    out 0
  unit 3 Control
    in 
    out 1
  unit 4 EnvGen
    in  1 1 0 0.02 2 0 2 1 -99 1 1 1 0 0 1 1 0
    out 1
  unit 5 BinaryOpUGen
    in  1 -0.9855
    out -0.9855
  unit 6 Out
    in  0 -0.9855
    out

a.free;

Nope – ".play"ing a SinOsc actually produces seven units.

hjh

1 Like

I finally found the language of my dreams in Racket, but I was disappointed to learn there wasn’t any fully working SC client, so I’m trying to make one myself starting from the OSC messaging level, but I’m having some trouble finding resources on that level. Basically, all I could find contains more or less the same info you can find here:
Server Command Reference | SuperCollider 3.12.2 Help

This leaves me with some questions:

  • how do I call a single UGen with an OSC message? I only see message /s_new for instantiating a new synth.
  • how do I build a SynthDef from the Racket client? I’ve read some comments saying it’s particularly difficult, many clients keep doing it from Sclang. Would it be easier perhaps to write a Racket macro that generates the Sclang code and sends it to the server as a string?

Building a SynthDef is not really more difficult than doing OSC messaging (which is not to say it’s easy). The reference doc you may be missing is https://doc.sccode.org/Reference/Synth-Definition-File-Format.html

Another possibilty is porting Rohan Drape’s R6 Scheme client, if he’d be okay with that, though I don’t know if he visits this forum.

This seems like a really good place to start.

3 Likes

Where can I find more info on UGen object constructors? For while I toyed with a Haskell client (also by Rohan Drape), and I remember how multiplying two UGens (or perhaps they were synths containing a single UGen) returned a (BinaryOpUGen “*” UGen1 UGen2) or something alike. Is this an UGen object constructor?

UnaryOpUGen and BinaryOpUgen are special-case UGens that behave a little differently than most/all others. If you just need + and *, I’d recommend starting with MulAdd which is a normal UGen.

  1. Topological sort to streamline signal paths.

I’m very slightly embarassed to not know that there are other reasons to topologically sort the graph other than to make sure UGens are created after their inputs. Does this have something to do with minimizing the number of wire buffers used?

As I understand it (and this is going from memory, based on a post from James McCartney from years ago)… if UGen chains are arranged in series as much as possible, then the output-to-input connection reuses wire buffers, where parallel sorting would require multiple wire buffers to be accessed alternately:

Saw
LPF
Saw
LPF
+
Saw
LPF
+
Saw
LPF
+

^^ can be done with two wire buffers (although, in current SC, the addition operators would optimize to a Sum4, and that would require more wire buffers).

Saw
Saw
Saw
Saw
LPF
LPF
LPF
LPF
+
+
+

^^ requires at least four wire buffers, and they would be accessed w0, w1, w2, w3, w0, w1, w2, w3…

… which (if I remember correctly, could be faulty), in the latter case, means reading/writing more distinct addresses in memory, reducing the likelihood of using lower-level caches near to the CPU and possibly degrading performance – that is, by addressing a smaller subset of memory for fewer wire buffers, and switching between them less often, memory access is more likely to use CPU cache and save time on memory access mid-synth = faster processing. @VIRTUALDOG can probably correct me where I’m wrong :laughing:

SC’s topo sort prefers the first arrangement over the second.

But, for the topic at hand, I’d say the top priorities are 1/ Controls, 2/ UGen data structures and connections and 5/ serializing. If you get those right, then you will have playable SynthDefs. Performance could be improved by adding optimization and a more clever sort later, but it’s not strictly necessary as long as all UGens in the def come after all of their inputs (which, in SC, will always be the case because UGen D cannot be created until all of its inputs A B and C exist).

hjh

1 Like

Guys, thank you so much. This is pretty much what I needed to get started, so I’m gonna mark this as solved.
I hope this will be as much fun as it is gonna be a headache.

1 Like

Here’s hoping you have success: racket’s ‘meta language’ stuff looks amazing for a music dsl

Seconded; I’ve been looking for an excuse to get into Racket for ages. Very curious to see where this project goes!

1 Like

If you like lispy languages, perhaps you can find some inspiration in overtone, a clojure client for supercollider.

Well, this is turning out to be much easier and much more fun than I expected!
The reference doc Amindfv pointed out has been unvaluably helpful:
https://doc.sccode.org/Reference/Synth-Definition-File-Format.html

I decided the best place to begin was doing the reverse of what I wanted: parsing binary .scsyndef files into a client side representation of a synth and its ugen-graph; this should make it easier to study how SC represents things internally. So the file generated by the sc-code:

(SynthDef(\misine, {|freq=300 , out=0, offset=0.25|
	Out.ar(out, (SinOsc.ar(freq, 0.333) + offset))
})).writeDefFile

is parsed into:

(list
 "misine"
  (constants '(0.3330000042915344))
  (list (parameter "freq" 300.0) (parameter "out" 0.0) (parameter "offset" 0.25))
  (ugen  "Out" 'ar 
      (list (argref "out") 
             (ugen "op:+" 'ar
                       (list (ugen "SinOsc" 'ar (list (argref "freq") (constant 0.3330000042915344)) '(ar))
                       (argref "offset")) '(ar)))
 '()) )

Which is pretty friendly, considering how ugly the server-side representation is. Later, it should be easy to do the inverse process: from such a client representation, write a .scsyndef file.

But to further simplify the client-side representation, I’d like to ask a couple of generalities:

  • I’m declaring the UGens recursively, starting from the last UGen; is it always the case that the full UGen tree can be recovered from the last UGen?

  • In this UGen representation, I’ve kept a list at the end declaring the rate of different outputs, something that came from the server-side representation. But most UGens I know have one single output and it’s the same rate as the UGen itself - what UGens used from the client side are exceptions to this? I’m thinking of doing away with this list of output rates, and using some wrapping or different constructor for the cases were we might need it. After all, I don’t see anything like it in Sclang.

  • the first UGen is a Control UGen which seems to have a number of outputs equivalent to the number of declared parameters; so, I’m assuming it can be omitted (of course, when doing the inverse process we’ll have to create it).

  • BinaryOpUGen uses an int16 (“special index”, which is unvariably 0 for most of UGens) to decide which operation to perform, but from the client side I’d prefer to represent different operations as different UGens; for instance, in the above code you can see an (ugen “op:+” …).
    I’m trying different operators on SC to see what “special-index” value they produce, but it’s a cumbersome way of getting them all. It must be somewhere in the source code, but I can’t find it. Where should I look?

Thanks for the good wishes! Indeed Racket DSL capabilities are something I intend to take advantage of later, to build some kind of sequencer (like lilypond, but microtonal and with relative notation) which is one of my main (and distant) goals.

Thanks for the suggestion, but I’m in honey-moon phase with Racket.

Silly me, this would refer to multichannel expansion, which I never used without Mix.ar.

Nevermind, I found such a list.

This is going well.

1 Like

Usually, but it’s not quite a safe assumption. Typically, the last UGen is an Out writing the main signal to a bus. But there may be multiple Out UGens, or other UGens with side effects, e.g.

SynthDef(\lfo, { |out, freq = 0.1|
    var sig = LFTri.kr(freq);
    var sendTrig = Impulse.kr(10);
    Out.kr(out, sig);
    // this may be a totally different signal of course
    SendReply.kr(sendTrig, '/lfo', sig);
}).add;

We do have multi-out UGens (not quite the same thing as multichannel expansion). I’m not aware of any cases where a single UGen has both ar and kr outputs. It’s probably ok to have a single rate for the unit.

If the object is representing a binary SynthDef, then the Control doesn’t have any information that isn’t already in the parameter list. (Note though that a SynthDef may have multiple Control objects.) You’re right that the server requires the Control UGen to receive values from the client.

It’s nice to watch this evolving :grin:

hjh

Thanks! This going much better than I expected, though I must say I owe a lot to your help, the lockdown and some really windy weather these days.
I hope I’m not bothering the forum with these updates and questions.

I think I found a way to kill both birds with one stone: by having different subtypes of UGen. One of them would be the sender-ugen, which would include things like Out.ar/kr, Trigger UGens, or the SendReply.kr you mentioned. Those don’t have any output spec (no other UGen can directly refer to them as their input, they would have to refer to an In.ar/kr instead).
Now I can do the retrieval recursion I mentioned above, starting not from the last UGen, but from every sender-ugen in the synthdef.

I’m very glad I chose to begin from the ground up: from .scsyndef files to a representation equivalent to how you would describe a ugen-graph from the client side.

In the end, what is taking me the most time is deciding on the proper naming of things; or structural design questions, depending on their practicality and elegance.
One of such questions that plagues me now is:

  • whether I should tag client ugen constructors the moment I create them with some id (like a global variable containing an int counter, every time I create an ugen constructor I give it that int id, and add 1 to the counter)

  • or doing nothing of that sort, but instead, considering each UGen equivalent to its client side description. This means two instances of the same UGen with the same arguments will be rendered as one single UGen in the graph, which in itself is an optimization. But then this would mean that non-deterministic/random UGens like WhiteNoise.ar will need some kind of identifier. I believe this is the case in Rohan Drape’s Haskell client, where random UGens take a string argument so that they can be told apart.

  • BinaryOpUGen uses an int16 (“special index”, which is unvariably 0 for most of UGens) to decide which operation to perform, but from the client side I’d prefer to represent different operations as different UGens; for instance, in the above code you can see an (ugen “op:+” …).
    I’m trying different operators on SC to see what “special-index” value they produce, but it’s a cumbersome way of getting them all. It must be somewhere in the source code, but I can’t find it. Where should I look?

As far as I know, BinaryOpUGen and UnaryOpUGen are the only two ugens which use the special index.

A list of the operators can be found in the enum definitions in server/plugins/BinaryOpUGens.cpp and server/plugins/UnaryOpUGens.cpp in the SC repo. The special index is just the enum number, e.g. “neg” is 0, “not” is 1, “isNil” is 2, etc.