Beginner bussing/routing question

Hello! New SC user here. I’m attempting to build a simple modular synthesizer, something along the lines of the EMS Synthi, and I’m looking to create a matrix-style patching system. For each of the modules (osc, env, amp etc.) I’m planning to create individual SynthDefs, but what I’m confused about is the routing/bussing them together.

My initial plan was to create a big grid of GUI buttons, and have each one create a routing synth. Pretty much exactly like described here by jamshark70: Does a UGen like Audiomulch 8x8 Matrix exist?

On the other hand… I’ve dissappeard down the ProxySpace rabbit hole. I’m intrigued by the way it handles fading and routing, but I can’t find an example of someone using it the way I’d like. I’m thinking this would the “correct” way to do my routing, but I’m really not sure how to use it with SynthDefs, or how to implement it in a matrix of buttons.

Am I getting totally off track here? Should I stick with my original plan with synth bussing, or is ProxySpace the way to go?

Hi Maxkablaam, firstly welcome to the community.

As far as the problem of choice over Busses or ProxySpace, I’ll leave that to someone better placed to explain the pros and cons (which often depend on how you wish to use what you’ve designed) of Busses, ProxySpace, et al.

However, since you say you’re a new SC user I’d recommend heading over to YouTube and watching Eli Fieldsteel’s tutorial on Server Architecture, which include Busses and Groups, both of which (when combined) might be what you’re looking for. You can control a lot either individually, or even change/send new values to groups, and much more.

If you haven’t already seen this, and you’re interested here’s the link: https://www.youtube.com/watch?v=VGs_lMw2hQg

I hope that helps.

Thanks, glad to be here :slight_smile:

I’ve been watching Eli’s tutorials, they are really fantastic, and I probably wouldn’t have been so brave to dive into all this coding nonsense if it wasn’t for them. He is a treasure :joy:

I just stumbled upon this little example, and it’s pretty fascinating. http://danielnouri.org/docs/SuperColliderHelp/Libraries/JITLib/tutorials/basic_live_coding_techniques.html

Is this a SynthDef being defined, then pushed to ProxySpace? It looks like the args can then be “patched” with the line:

~s1.map(\freq, ~b1);

If that is the case, this looks like a pretty good contender for my “matrix buss”.

A very straightforward way of making a modular-ish setup is to use proxy’s plus mapping of audio parameters. This allows you to specify routing separately from where you define your synths, making it easy to re-configure a small number of modules in a wide variety of ways.

Here’s a quickie example - I’ll use the Ndef interface for proxys rather than ~environmentVariables and a proxyspace (Ndefs are just NodeProxys) - I prefer this, and it has some benefits, but if you’re working in a proxyspace you can swap any Ndef(\foo, { ... }) to ~foo = { ... }. And, I’m using the NamedControl way of specifying synth params, where { SinOsc.ar(\freq.kr) } is equivalent to { |freq| SinOsc.ar(freq) }. I’m doing this because I can do e.g. \input.ar, where the .ar specifies an audio input.

Ndef(\click, {
	Impulse.kr(1);
});

Ndef(\envelope, {
	Env.adsr().kr(gate: \trigger.kr);
});

Ndef(\osc1, {
	LFSaw.ar(\freq.kr, 0, \amp.kr);
});

Ndef(\osc2, {
	SinOsc.kr(\freq.kr).range(\min.kr, \max.kr);
});

Ndef(\filter, {
	BLowPass.ar(\in.ar, \freq.kr(400));
});

Okay, now that I have my modules… I can map outputs to inputs using a.set(b.asMap):

Ndef(\envelope).set(
	\trigger, Ndef(\click).asMap
);
Ndef(\osc1).set(
	\freq, 140, 
	\amp, Ndef(\envelope).asMap
);
Ndef(\osc2).set(
	\freq, 1/8,
	\min, 100, \max, 600
);
Ndef(\filter).set(
	\in, Ndef(\osc1).asMap,
	\freq, Ndef(\osc2).asMap
);
Ndef(\filter).play;

You can easily switch between arguments with a fixed value (e.g. my \freq, 140) and dynamic inputs (\freq, Ndef(\osc2).asMap). The only gotcha with all of this is ---- you MUST map kr outputs to kr inputs and ar with ar, else you probably won’t get any output (note that my \osc2 is a SinOsc.kr, and I’m mapping it to a kr input of my filter). You can easily sidestep this by simply sticking to ar values for everything - it will come at a higher CPU cost, but you can always go back and switch some things back to kr if you build something complex enough that efficiency becomes important.

Also be aware of the wonderful xset variation, which will smoothly crossfade between one mapping any another based on the fadeTime set for each synth - this means you can smoothly crossfade between completely different routing setups without interrupting playback.

1 Like

There’s an alternative syntax to this kind of routing workflow that can sometimes be more clear, using the <<> operator. My routing above could equivaleently be expressed as:

Ndef(\osc1).set(\freq, 140);
Ndef(\osc2).set(\freq, 1/8, \min, 100, \max, 600);

Ndef(\envelope) <<>.trigger  Ndef(\click).asMap;
Ndef(\osc1)     <<>.amp      Ndef(\envelope).asMap;
Ndef(\filter)   <<>.in       Ndef(\osc1).asMap;
Ndef(\filter)   <<>.freq     Ndef(\osc2).asMap;

or, expressed left-to-right with the flipped operator <>> if it’s more intuitive…:

Ndef(\osc1).set(\freq, 140);
Ndef(\osc2).set(\freq, 1/8, \min, 100, \max, 600);

Ndef(\click)    <>>.trigger  Ndef(\envelope).asMap;
Ndef(\envelope) <>>.amp      Ndef(\osc1).asMap;
Ndef(\osc1)     <>>.in       Ndef(\filter).asMap;
Ndef(\osc2)     <>>.freq     Ndef(\filter).asMap;
1 Like

Thanks! This is a great explanation. I’ll be trying this out soon.