Scaffolding for a midi-controlled analog synth emulator

Trying to make a midi-controlled analog synth emulator for the Minimoog. This is my first attempt to write useful and extensible supercollider code. I don’t have enough control bits on my midi controller (arriving shortly) so I will still need a GUI for some of the components.

Before I get too deep into things, I’d love some overall structural guidance on the code style. Would particularly appreciate feedback on the GUI, SynthDef, and Control Bus management. I realize that the synthedef is nowhere near any sort of minimoog yet, we’ll get there eventually, I hope =]

the code

p.s. is there an autoformatter for supercollider? something like python black or prettier for javascript or gofmt for go?

Hello apiaran ! Welcome to the forum :slight_smile: .

Your code is working great ! That’s mostly about code optimisation, to ease your development process. There’s only one issue, which is difficult to diagnose at first, regarding the way you add your SynthDef, then trigger the synth. I’ll get there in time.

I think that you should comment your code. I find commented code easier to read, because it catches the eye, and you somehow get to know what you’re looking at at first glance.

I’ve regrouped everything in a single block (except the boot command). For now you have split everything, which also works, but can make development tedious because you have to re-evaluate everything when you make a modification. And this forces you to use global variables. That’s not a bad thing, but if you do not intend to do livecoding, only to use GUI and controller, there’s no point complicating everything. Note that having a single block doesn’t prevent global variable usage.

Here’s the code I propose :


// Boot the server
s.boot;

(
var window = Window("MiniMoog Controls");
var miniMoogSynth;

var gate = 0;
var toggleGate;
var gateButton;

// Functionalities
toggleGate = {
	if(gate == 0)
	{ gate = 1 }
	{ gate = 0 };
	miniMoogSynth.set(\gate, gate);
};

// GUI
gateButton = Button(window)
.string_("G")
.action_(toggleGate);

// Server related
SynthDef(\minimoog, { | out=0, amp=1, gate=0 |

	var sound = Saw.ar(440, amp);

	var soundEnv = EnvGen.kr(
		envelope: Env.adsr(
			attackTime: 0.01,
			decayTime: 0.5,
			sustainLevel: 0.5,
			releaseTime: 1,
		),
		gate: gate
	);

	sound = sound * soundEnv;
	sound = Pan2.ar(sound, 0);

	Out.ar(out, sound);

}).add;

forkIfNeeded {
	s.sync;
	miniMoogSynth = Synth(\minimoog, [
		amp: 0.1,
		gate: gate
	]);
};

// End of initialisation
window.onClose_({ miniMoogSynth.free; });
CmdPeriod.doOnce({ window.close; });
window.front;
)

So first, I’ve removed the Bus. You’ll probably need some Busses at some point, and you found the correct way to use it, but for this particular usage, you can change the Synth’s parameter directly with a number. Busses main usage is to have a Synth control an other Synth, which is not the case here. Also, when you create a Bus, don’t forget to free when you don’t need it anymore.


This is not the recommended syntax, but I like to chain method calls when creating GUI components :


gateButton = Button(window)
.font_(Font.default)
.string_("G")
.action_(toggleGate);

This makes a single block of code, which is easy to read and to locate, and allows quick copy-pasting.


As you can see, I’ve separated the button code from the action it performs. This will help you a lot organizing your code once you start adding dozens of GUI components.

Your modulo method works to alternate between 0 and 1, but this might be confusing for a reader. I’d advocate using an if statement for clarity. Both give the same results :

toggleGate = {
	// If gate is currently at 0
	if(gate == 0)
	// Set it to 1
	{ gate = 1 }
	// Otherwise revert it to 0
	{ gate = 0 };
	// Now that the value has been updated,
	// update the synth with the new value
	miniMoogSynth.set(\gate, gate);
};

Now for the SynthDef, I’ve just rewritten it step by step so the synthesis is more apparent, and changed the ‘mul’ argument name by ‘amp’, because we usually talk about ‘Amplitude’ when referring to the volume of the sound, even if controlling this is done with a multiplication. None of these impact the algorithm.


So now, this :

forkIfNeeded {
	s.sync;
	miniMoogSynth = Synth(\minimoog, [
		amp: 0.1,
		gate: gate
	]);
};

You might have noticed that, with the code you wrote, you had a console error and no sound the first time you evaluate your code when starting a new session. You might also have noticed that changing the SynthDef doesn’t take immediate effect.

This is because adding a SynthDef takes some times.

When you do this :

(
SynthDef.new(\minimoog, { etc }).add;
~synth = Synth(\minimoog);
)

What happens is :

  • You send a message to the server to add a new SynthDef
  • You send a message to the server to play a Synth
  • The server receives a message to add a new SynthDef and starts constructing it
  • The server receives a message to play a Synth
  • The server finishes to construct the SynthDef

(this is a simplification)

So at the time you ask the server to play the Synth, the SynthDef it needs isn’t ready yet.

You need to ask the server to finish it’s current actions (in our case, constructing the SynthDef), before playing the Synth. This is done with the following command : s.sync;. This tells the server ‘First, finish what you’re doing now, then do the next things I’m asking’.

But for some technical reasons, you can’t use s.sync; anywhere you want. You’ll have to enclose it into forkIfNeeded { }; for it to work.

Copy-pasting my example should be enough for most cases, see the documentation for more on this.


Now at the end, two useful things I like to do :

window.onClose_({ miniMoogSynth.free; });

This is optional, but usually, when you close the window, you want the program to terminate. So here, when the window is closed, it’s freeing the Synth aswell. This is were you would free your buffers too.

CmdPeriod.doOnce({ window.close; });

The inverse, if I’m using the command period, I’ll be freeing every running synth, this usually means my window has no purpose any more, so I can remove it at the same time.

This will post an error message, because the CmdPeriod frees all synth, then closes the window, which in turn is supposed to free the miniMoogSynth, which have been freed already by CmdPeriod. This error message can be ignored, or removed easily.

2 Likes

Wow, thank you so much for the detailed response! Your approach of a single command to run all the things in one go seems like it will set me up for a solid iterative cycle as I get the synth up and running.

I won’t need the control busses even if i want to wire things up to midi control signal events? As long as the function has access to the synth in scope, it can set things on it?

The grouping of gate, toggleGate, and gateButton puts me in mind of an object to encapsulate the data and logic. Is that something that would be beneficial here or do you think it would add too much complication? (I’m anticipating around 40 control points for the final system, since there’s about that many control dimensions on the minimoog.)

Indeed! In fact, one of the advantages of separating the button and the function it triggers is that you can also bind the function directly to a MIDI message :

var gateConnection= MIDIFunc.cc({
		toggleGate.value;
	},
	1);

Now, you can either control the gate from the GUI, or from the MIDI controller, both using the same function. (I don’t have any MIDI controller so this code might not be exact, but you get the point).


Ok. You don’t need busses for what you are currently implementing, and I think you should start with a V1 without Busses so things don’t get too complicated. But we’ve been discussing them a bit, and regarding V2, you will definitely want to implement busses. So I’ll post some explications about them here so you can start getting a sense of what they offer. Feel free to skip this for now if you think it might confuse you.


So those Moogs have incredible sounds and everything, and some of their controls are little switches that jump from one position to an other, allowing you, for example, to select the type of wave you want.

You can see the set method, that we used before :

miniMoogSynth.set(\gate, gate);

as a little switch that you’d push with your finger, switching, for example, the gate parameter between ON and OFF.

But what we really love with Moogs, is to be able to use ‘sound’ itself to control an other sound, using wires to interconnect oscillators (like this).

Without a Bus, set can only act as a switch, but is not designed to control things like an oscillator would control a parameter.

There’s two ways to have an oscillator control a parameter. Usually, we use the simplest way, by declaring it directly in the SynthDef. Here, a basic LFO applied to a Sine :

(
SynthDef(\internalLfo, { |amp = 0.25, lfoRate = 1|
	
	var lfo = SinOsc.kr(lfoRate);
	
	var sound = SinOsc.ar(
		440,
		mul: amp * lfo
	);
	
	Out.ar(0, sound!2);
}).add;

forkIfNeeded {
	s.sync;
	~synth = Synth(\internalLfo);
};
)

~synth.set(\lfoRate, 4);
~synth.free;

So now, the same audio result, but with a more complex architecture :

(
~lfoBus = Bus.control(s, 1);

SynthDef(\lfo, { |rate = 1, outBus|
	var lfo = SinOsc.kr(rate);
	Out.kr(outBus, lfo);
}).add;

SynthDef(\externalLfo, { |amp = 0.25, lfoControlBus|
	var lfo = In.kr(lfoControlBus);
	
	var sound = SinOsc.ar(
		440,
		mul: amp * lfo
	);
	
	Out.ar(0, sound!2);
}).add;

forkIfNeeded {
	s.sync;
	~lfoControl = Synth(\lfo, [\outBus, ~lfoBus]);
	~synth = Synth(\externalLfo, [\lfoControlBus, ~lfoBus]);
};
)

~lfoControl.set(\rate, 4);

(
// Free the bus when done!
~lfoBus.free;
~lfoControl.free;
~synth.free;
)

Now we have two SynthDefs instead of one, and the Bus is acting like a ‘wire’, transmitting the output of the first Synth into the second Synth. The main point of this is modularity. In the first example, the lfo is contained within the SynthDef, which means you cannot change it without changing the whole SynthDef. In the second example, we don’t have this problem, because the two SynthDefs are separated. We could copy the LFO, change it a bit (use a Saw instead of SinOsc), and start a new synth, with a new Bus. While we did this, the other two synths are still running. Then, by ‘swapping’ the two busses, i.e. telling the main synth ‘you’re not listening the SinOsc lfo as your lfo control, but the Saw lfo from now’, we’d be changing our control while it’s running. This equivalent to swapping two wires using a modular synth.

As you can see, this adds a lot of complexity to the design (almost plumbing issues). Notably, you are responsible for placing your synths in the correct order so that controls are calculated before the synths that use them. But you can chain controls, and that gets hilarious pretty quick.

End of the ‘can skip’ section.


That’s a difficult question, and I don’t really have an answer.

I’ve done the extreme version of this. I have a collection of 40 SynthDefs, and a program that reads the files, then automatically creates the correct GUI, plug them inside the master, bind them to a pitch and a scale, etc. This took me several months of concentration, error and trial. For me the equation was ‘If I waste a year implementing this, then I have it for the rest of my life’. So now, I only have to write a SynthDef and some metadatas and I have my new instrument with a GUI and interconnections with every other synths and functionalities I wrote. So I’m sure happy about it.

The main problem is about those metadatas. Inside the SynthDef, parameters look all the same : numbers.

So your gate parameter is 0 or 1. And controlled by a button.
Amplitude is also related to 0 and 1. But it’s a range of numbers instead, controlled by a slider.
Frequency is also a range, but between 20 and 20000, and on an exponential scale.
…etc…
What about more complex types of parameters?

I think the best thing for you is to have a function for every type of control, that allows you to type less code when creating a new parameter. For example, the slider function will take min, max, default value, and the function to evaluate. It will return the slider. The ControlSpec class helps with this.

As a more general answer, I don’t know how comfortable at programming you are, but factorisation is one of the most important skill to get early, or you’ll waste a lot of time when dealing with large projects. You can copy-paste something once by laziness, but three iterations usually means you should create a function instead (also for readability purposes).

But again, let’s say you only have 4 hours before you need to go to sleep, would you rather spend 1 hour doing dirty copy-pasting then 3 hours of music, or 3 hours of elegant programming and 1 hour of music?

Hello Friends!

Mind taking another look at the mostly pretty much working minimoog synth (currently here). It mostly works and I’m able to control it with my Arturia MiniLab3. Looking forward to wiring it up to some patterns and stuff. I think the one extra thing I will want to add is a way to save presets by name.