Bus routing: ddwMixerChannel's approach

long post

Because bus routing is presenting lately as a highly confusing topic, I thought it might help cut through some of the confusion to take apart the way that my MixerChannel class organizes buses and groups.

Some observations up front:

  • The MixerChannel design stabilized around 2005 – 17 years ago. It’s robust – it handles everything I can throw at it. It’s reliable – more than a decade and a half of testing.
  • You can use or adapt parts of this design as needed. I’m not going to discuss every feature of the MixerChannel design, just how it uses groups and buses.
  • IMO following this design carefully is likely to avoid mistakes. (But, no guarantees – being unsystematic or sloppy is likely to cause mistakes!)

Basic principles

  1. Every channel lives on a bus. Period. No loose signals going directly to bus 0.
  2. The channel maintains its signal on its own bus.
  3. The channel also copies that signal to an output target, where it may be mixed with signals from other channels.
  4. Because order of execution is important, the entire channel is wrapped in a Group.
  5. And, because source synths should come before effect synths, it’s helpful to have two groups inside of the channel’s group – the first for sources, the second for effects.

Support synths

The channel is responsible for volume control and panning, as well as routing. You need a SynthDef for this.

A send is simpler – doesn’t need panning, and doesn’t need to write back to the original bus. A send will be used later, but I’ll introduce the SynthDef now.

You might think it’s easier to include additional Out units in all your SynthDefs. It’s not. I find it’s much easier to modularize the design. Source SynthDefs are only responsible for generating a signal and putting it onto the channel’s bus. The channel architecture is responsible for forwarding the signal to channel and send targets.

(
SynthDef(\mixer2x2, { |inbus, outbus, level = 1, pan = 0|
	var sig = In.ar(inbus, 2);
	sig = Balance2.ar(sig[0], sig[1], pan, level);
	ReplaceOut.ar(inbus, sig);  // "maintains on own bus"
	Out.ar(outbus, sig);  // "copies to an output target"
}).add;
)

(
SynthDef(\send2x2, { |inbus, outbus, level = 1|
	var sig = In.ar(inbus, 2);
	sig = sig * level;  // DAW sends do have a level control
	Out.ar(outbus, sig);
}).add;
)

Channel design

// let's make a channel
// obviously this needs a bus
~srcBus = Bus.audio(s, 2);

// to keep node order clear, let's also make groups
// 1. A group for everything on this channel, including faders
// 2. A group for source synths
// 3. A group for fx inserts
~srcGroup = Group.new;
~srcSynthG = Group.head(~srcGroup);
~srcFxG = Group.after(~srcSynthG);

// and the fader comes at the end
~srcFader = Synth(\mixer2x2, [inbus: ~srcBus, outbus: 0],
	target: ~srcGroup, addAction: \addToTail);

To play something into this design, it’s necessary to specify the target group and signal bus. (I’ll admit that this is annoying for plain SC objects. MixerChannel provides convenience methods play and playfx which handle SynthDef names, functions and patterns.)

(instrument: \default, sustain: 1, group: ~srcSynthG, out: ~srcBus).play;

And the node tree looks like this. Because the synth went into the right group, it’s guaranteed to be evaluated before the fader synth. (This is related to the point about consistency: If you put things in the right place, then you get a good result. If you start thinking “well, what if I cheat here or don’t allocate that there, or leave out this arg,” then things will start to go wrong. At that point, the solution is not to try to make the cheat work. The solution is to go back to known principles that will ensure correct results.)

sc-mixer-01-source-channel

FX inserts

In a DAW, FX inserts are part of the channel. So, here, we play FX synths into the effect group (so that they are guaranteed to follow audio sources).

FX synths should ReplaceOut onto the same bus (“The channel maintains its signal on its own bus”).

(
~ringmod = { |bus|
	var sig = In.ar(bus, 2);
	sig = sig * SinOsc.ar(90);
	ReplaceOut.ar(bus, sig)
}.play(target: ~srcFxG, outbus: ~srcBus, args: [bus: ~srcBus]);
)

(instrument: \default, sustain: 1, group: ~srcSynthG, out: ~srcBus).play;

FX sends

For effects like reverb, where 1/ multiple channels might mix down into the same effect and 2/ the effect will be mixed in with the dry signals, DAW-style sends are the best way.

A send needs another channel, so I’m copying the bus, group and fader code (although I’m leaving out a source-synth group, because the send channel should receive audio from other channels).

It’s also important for the send to come after any channels that are feeding into it. This is why each channel gets its own enclosing group: just move the outer group, and the order is fixed.

Finally, the source channel needs to split the signal off to the send target bus. That’s the purpose of the \send2x2 SynthDef. Most DAWs create post-fader sends by default. You do this literally exactly how it sounds: there is a fader synth, and you put the send synth after it. (If you needed a pre-fader send – guess what! – you put the send synth before the fader. That simple.)

// reverb as send
// a send channel needs a new bus
~rvbBus = Bus.audio(s, 2);

// and at least a main group, and fx group
~rvbGroup = Group.new;
~rvbFxGroup = Group.head(~rvbGroup);

// and a fader synth to xfer to the main output
// note here that it is referring ONLY to ITS OWN inbus
// and the desired final output bus
// -- modularity and encapsulation, don't mess with other channels' stuff
~rvbFader = Synth(\mixer2x2, [inbus: ~rvbBus, outbus: 0], ~rvbGroup, \addToTail);

// FX writes back to the same bus
(
~rvb = { |bus|
	var sig = In.ar(bus, 2);
	sig = FreeVerb2.ar(sig[0], sig[1], mix: 1, room: 0.8, damp: 0.2);
	ReplaceOut.ar(bus, sig);
}.play(target: ~rvbFxGroup, outbus: ~rvbBus, args: [bus: ~rvbBus]);
)

// and ensure the reverb comes after the source(s)
~rvbGroup.moveToTail(s.defaultGroup);

// now we need to send the source signal to the reverb channel
(
~srcRvbSend = Synth(\send2x2, [inbus: ~srcBus, outbus: ~rvbBus],
	target: ~srcFader, addAction: \addAfter
);
)

Let’s play something and hear the reverb.

(
p = Pbind(
	\instrument, \default,
	\degree, Pwhite(-7, 7, inf),
	\dur, Pwhite(1, 3, inf) * 0.25,
	\legato, 0.4,
	\amp, 0.2,
	\group, ~srcSynthG,
	\out, ~srcBus
).play;
)

~srcRvbSend.set(\level, 2);  // extreme! lol

p.stop;

Isn’t that too complicated?

True, this is not the simplest way. But recent conversations about bus routing should reveal that taking shortcuts with routing results in either limitations, or confusion.

  • Limitations: The approach here scales up. It’s a bit annoying to rebuild the structure every time by hand, but a helper object (MixerChannel) makes it more convenient. In particular, the fact that any number of sends can be added or removed at any time greatly increases flexibility.

  • Confusion: The best way to avoid confusion with bus routing is to be consistent. Find a way that works, and always do it that way. I modeled this approach after DAW signal routing because DAW signal routing works (and has been refined over more than a half century of recording engineering practice). Choosing a robust and well-functioning model means that a lot of questions and problems simply disappear: the focus is on doing it correctly rather than on seeing what I can get away with.

Hope this is helpful –

hjh

6 Likes

To show how it’s more convenient with MixerChannel, here is the same example written MC style:

s.boot;

// 3 groups and fader synth, just this one line
~srcMc = MixerChannel(\src, s, 2, 2, level: 1);

// no need to write group and bus by hand
~srcMc.play((instrument: \default, sustain: 1));

// ringmod fx insert - group bus etc also automatic
// (but I have to use a different arg name)
// playfx does ReplaceOut for you!
(
~ringmod = ~srcMc.playfx { |outbus|
	var sig = In.ar(outbus, 2);
	sig * SinOsc.ar(90);
};
)

~srcMc.play((instrument: \default, sustain: 1));


// reverb send
// we know in advance what fx insert is needed!
// so we can write it at init time, save a step
(
~rvbMc = MixerChannel(\rvb, s, 2, 2, level: 1,
	completionFunc: { |chan|
		~rvb = chan.playfx { |outbus|
			var sig = In.ar(outbus, 2);
			FreeVerb2.ar(sig[0], sig[1], mix: 1, room: 0.8, damp: 0.2);
		}
	}
);

~srcMc.newPostSend(~rvbMc, 0.9);  // one liner to make the send
)

(
p = ~srcMc.play(
	Pbind(
		\instrument, \default,
		\degree, Pwhite(-7, 7, inf),
		\dur, Pwhite(1, 3, inf) * 0.25,
		\legato, 0.4,
		\amp, 0.2
	)
);
)

p.stop;

~srcMc.free; ~rvbMc.free;

hjh

5 Likes

Thanks for this! I’ve been wanting to get going with MixerChannel for a while now, and this inspired me to finally try it.

Is it possible to use MixerChannel in less of the DAW channel paradigm, and more like a physical mixing board: where whatever stuff I have going can get “plugged” into the mixer. If that makes sense?

I’ve been using Alga for most of my sound creation and routing needs. Using it like a modular synth. Alga creates and orders its own busses, and is able to handle a lot of on the fly connecting and disconnecting.

My question is, is it possible to set up MixerChannel in a way that it listens to a bus? What I mean is, to not use its play method for routing a source into it.

I see that I can set a MixerChannel's inbus when I initialize it, but it seems like I can’t easily change it after… is that correct? MixerChannel's inbus method only returns the bus, there’s no setter.

I found that I can go a step lower, and access MixerChannel's Synth directly, and use the set method, eg- ~mixer.synth.set(\busin, ~src.synthBus.bus) (where ~src.synthBus.bus is the bus that the AlgaNode is outputting to).

My issue is that since Alga handles all the routing and bus management already, certain actions and methods cause the signal output bus to change.

I’m hoping there’s an easier way to handle the routing. Or am I trying to use MixerChannel in a way it’s not really intended for? Seems like that are some overlaps in Alga’s and MixerChannel’s capabilities.

1 Like

You could play a synth into the mixer channel’s synthgroup that reads from the alga bus. It isn’t necessary to alter the mixer’s architecture – just give it an audio source that pulls from wherever you want.

hjh

Ahh right! If I’m understanding you correctly, at its simplest, it could be a SynthDef with an In UGen passing directly to an Out.

Actually that’s great because I can build some variations with pre-amps or EQ sections.

Cheers!

Yep, could be that simple.

Something similar is also the way to implement feedback between channels. It won’t let you do a.outbus = b; b.outbus = a where a and b are mixers, but you can set a.outbus = b and then play an InFeedback synth on a, which reads from b’s bus.

hjh

Oh, that can be fun!

Due to the way Alga sets up busses, at the moment I think’s its going to be better if I send the Alga stuff to a MixerChannel, but I like the channel strip idea. Haven’t gotten far enough to think about any sound processing stuff for it yet, but right now I’ve got this very simple input section.

SynthDef(\mixerCh, { |outbus| Out.ar(outbus, In.ar(outbus, 2)) }).add

Gain staging in SuperCollider has been a bit tough so far, this’ll make things a lot more obvious going forward.

I’m going to look into MIDI control next!

Thanks for your help!

I just looked at this thread and I can add something about the Alga side.

The play method supports outputting to a custom Bus index:

(
Alga.boot({
	b = Bus.audio(s);
	a = AN({ SinOsc.ar }).play(out: b.index);
	{ In.ar(b.index) }.play(addAction: \addToTail);
})
)

//The internal busses are taken care of
a.replace({ Saw.ar * 0.5 }, time: 2)
1 Like

Yep! That’s sort of how I currently have it patched:

(
Alga.boot({
	SynthDef(\mixerCh, { |outbus| Out.ar(outbus, In.ar(outbus, 2)) }).add;
	s.sync;
	m = MixerChannel(\a, level: 0.1);
	m.play(\mixerCh);
	a = AN({ SinOsc.ar }).play(out: m.inbus.index);
	z = MixingBoard(mixers: [m]);
})
)
//The internal busses are taken care of
a.replace({ Saw.ar * 0.5 }, time: 2)

I run into a little trouble if I cmd + ., as MixerChannels regenerate automatically, and end up being ahead of anything else that I launch, but it’s not so bad to free them and re-evaluate.

I’m loving it!

1 Like

I just pushed a change that puts the mixer groups at the end of the node tree, rather than the beginning – because this is a good use case: a framework that doesn’t put synths inside mixer groups (in which case the mixers had better be toward the end). Update from git and give it a try.

hjh

1 Like

That’s working very nicely. Thank you!

Is there a way to de-couple MixerGUIDef from a MixerChannelDef and thus the MixerChannel itself, and associate the GUIDef with the instance of MixingBoard instead?

When I try to do:

z = MixingBoard(mixers: [~ch1, ~ch2])
z.setGuiDefs(~myTestGui)

This just returns -> a MixingBoard, and the gui itself doesn’t change. z.refresh also doesn’t update anything. If I try to do z.setGuiDefs(~myTestGui, [~ch1, ~ch2]), I get an error: ERROR: Message 'guidef_' not understood.

z = MixingBoard.setGuiDefs(~myTestGui, [~ch1, ~ch2]) returns the same error.

Since it seems possible to have several different MixingBoards displaying different combinations of of the same channels - kind of allowing for a view of everything, or a view of just the submix main channels, or just a submix out of the bunch, etc - it would be nice to have different views which display different aspects, for example a MixingBoard that’s streamlined with just volume and pan faders, or one with lots of sends, or additional Widgets.

At some point I’d love to hack together some combination of your PeakMonitor's read-out and clip indicator Synth and responder along with the ServerMeter's green/orange/red bars, and make that into a widget that’s possible to display alongside, or overlay on top of the volume fader. Being able to have different MixingBoard GUI views for this would be really awesome, since it wouldn’t always be necessary for this view.

Hm, this is a relatively weak area because I didn’t really understand model-view-controller at that time.

To do it right would take some surgery. I’m not sure how quickly I can get to it.

Another approach, though, is to use changed notifications from the MixerControl objects to sync any arbitrary GUI view. I’m not sure that MixerPre/PostSend broadcast these notifications though, would have to check. In my live setup, I don’t use MixingBoard at all – I link up MixerControls with GUI slots (with real MVC). I really should refactor the GUI.

I had coupled the GUI design to the MCDef because it would be wrong to use a GUI with a quad panner widget for a stereo channel. But you raise a good point about different views.

hjh

I’ll look into setting something like this up. I’m still intimidated by setting up GUIs, and MVC, but it’s something I’ve been meaning to learn and get more comfortable with, and I’ve had the Mapping and Visualization with SuperCollider book laying around near my computer for a while now for exactly this reason.

As it stands, just having a window available to see faders with settings relative to all the channels is really helpful already. Mixing in decimals is pretty tough for me!

This is what I was actually after years ago. But starting out from the beginning meant I didn’t understand all of the issues. Thank you for the post and all of your work on SC!! This should be in the help system if it’s not.