The routing structure of a dirt orbit

Because there were some thoughts about how global effects should behave, here is a diagram that shows how the routing withn an orbit is organised.

Each supercollider synth is a horizontal bar. In principle, any one could read from any bus above it and write to oany bus below it (adding, silencing, crossfading, replacing its signal).

This page can be used to discuss what one can do with this.

8 Likes

That's a cool diagram! I'd also point out (since I often forget), that multiple simultaneous "source" synths ultimately stack up their output onto the single dryBus, so that under this scheme the routing of sounds to a global effect is an "all or nothing" kind of thing. Which is one of the defining features of an orbit, really.

The main alternative that I've tried is to have an input bus for each global effect, with dirt_gate handling the routing. So then the global effects each read from their own bus and don't need to see everything on the orbit, instead it can be controlled by a "send" parameter (slightly repurposing room, delay, leslie, etc). And I usually add a "dry" parameter to control the amount of original signal dirt_monitor mixes in.

In practice you could usually do almost same thing using multiple orbits, but sometimes I find this version easier to handle mentally. The downside is I've never found a completely elegant way to handle the extra buses and routing necessary in the SC code, and I think the extra overhead in general does cost CPU.

For anyone curious, the implementation is here (it's unfortunately a bit behind develop branch at this point):

ah yes, good way, too. Do you have a way in which global effects can read each other's outputs? It seemed to me that you probably want that in most cases, right?

If we wanted to allow all global effects to overwrite the input, we could add a control for each of them (like delaysrc) how much of their audio they take from the globalEffectsBus and how much from the dryBus.

Another control (like delaywet) would have to specify how much it would overwrite the previous signal on the globalEffectsBus.

And for the monitor, one can specify the final wet/dry balance. Then, I think, all the basic cases are covered (apart from cross-routing)?

1 Like

Thanks for the diagram @julian

My use case is quite specific and is maybe a good test for this proposal. I am trying to use this as an effect: GitHub - victor-shepardson/rave-supercollider where the output should be 100% wet, or with a controllable dry/wet.

It would normally be great as an effect module rather than a global effect, however the model files are large / CPU intensive (~30% on an M1) so the UGen cannot be created/destroyed constantly, and they also need to be fed a string in order to load the model (rave-supercollider/test.scd at master · victor-shepardson/rave-supercollider · GitHub).

If I understand your proposal right, it would cover this use case appropriately, so it looks great so far!

yes, it would need some time to fiddle with the different combinations to see which would be best.

Like this you'd directly overwrite the dry sound:

SynthDef("dirt_rave" ++ numChannels, { |dryBus, effectBus, gate = 1, rave=1|
	var in, snd;
	
	in = In.ar(dryBus, numChannels); // you could also read from the effectBus of course
	
	snd = <.... your code here ...>
	

	XOut.ar(dryBus, snd, rave);
}).add
2 Likes

Indeed. That looks great to me.

@julian here's a PoC of what I'm trying to do, gloriously hacked together, but hopefully illustrates the issues I'm having:

looks good! You may only have to take care that the node order is correct, but this is definitely not dirtier than superdirt :slight_smile:

Ha, well, I wouldn't mind it being "cleaner" in a few ways:

  • It would be nice to have this inserted before the global effects so I can add e.g. reverb on top of the RAVE model
  • With this configuration I am stuck in mono and can't do panning... maybe I can use DirtPan in there somehow still?
  • Setting up a separate OSC target and OSCFuncs outside of SuperDirt just for a couple of params seems over the top, and I don't know if it introduces timing issues as well

Nice .. RAVE ...

well i cannot understand how to interpret this map of the orbit. To me it seems like for eg. the globalEffectbus only has [ n global effects ] feeding into it. After which the output goes nowhere.

You're right about the input, but then dirt_monitor reads the globalEffectBus (and the dryBus). It then mixes them together to send output to the speaker.

So the drybus goes into the global effects and the dirt_monitor, and the global effects also goes into the (drybus and the globaleffectbus ) -> dirt_monitor ?

yes, in principle the horizontal bars always have access to all of the busses above them and can mix that in any way, different outputs go to where it has a tip downwards. The dirt_gate silences the synthBus, so that the next event can reuse it. You don't want to have separate busses for each overlapping event with effects. The global effects can either read from the dryBus or from the global effect bus, reusing already processed audio.

The thing is: there are a lot of options and trade-offs, and to avoid feature creep, it is made programmeable so you can hack the system as you need it.

Ok so the synth event can output into the outbus, synthbus,globaleffectbus and drybus? I'm trying to figure out if the In inverted triangle has a special meaning. Also the Xout symbol. But seeing as how in supercollider, these busses could be in scope anyway, everything is accessible.

Well, not the synth event itself, it outputs into synthBus. The dirt_gate routs the content of the synthBus into dryBus, where it is read by the dirt_monitor, which routs it to the outBus.

yes, it means replace with silence, so the next block of synths can reuse that bus

it means that you could crossfade the bus input with the effect output

Yes, you can do what you want in your synth def:

SynthDef(\breakItAll, { |effectBus, dryBus, out| 
	ReplaceOut.ar(effectBus, Silent.ar);
	ReplaceOut.ar(dryBus, WhiteNoise.ar);
	Out.ar(out, SinOsc.ar)
}).add;

I wonder how you could introduce a flexible cross-orbit FX bus. For example, I would like to change only the Dry/Wet control per orbit for a reverb effect. But orbits are independent constructions. I know I can hard code this, but I wonder if this could be done better.

I use a SuperMassive VST reverb. My motivation is to only initialize one VST instance and route the orbit busses through this VST plugin. This is my SynthDef example for the first four orbits:

var numChannels = ~dirt.numChannels;

SynthDef(\valhalla2, { |dry1,dry2, dry3, dry4, reverb1 = 0, reverb2 = 0, reverb3 = 0, reverb4 = 0|
	var sound1 = InFeedback.ar(dry1, numChannels);
	var sound2 = InFeedback.ar(dry2, numChannels);
	var sound3 = InFeedback.ar(dry3, numChannels);
	var sound4 = InFeedback.ar(dry4, numChannels);

	var fxSound;

    ReplaceOut.ar(dry1,  sound1 * 0);
    ReplaceOut.ar(dry2,  sound2 * 0);
    ReplaceOut.ar(dry3,  sound3 * 0);
    ReplaceOut.ar(dry4,  sound4 * 0);

	fxSound = (sound1 * reverb1) + (sound2 * reverb2) + (sound3 * reverb3) + (sound4 * reverb4);

	fxSound = VSTPlugin.ar(fxSound, numOut: numChannels, id: \valhalla);

	Out.ar(0,fxSound); // First orbit starts at 2 -> Hard coded for my hardware output bus; I route every orbit to this bus.
}).add;

And here is the code to initialize this cross orbit fx bus and make it controllable with TidalCycles:

var numChannels = ~dirt.numChannels;

~valhallaSynth = Synth(\valhalla2, [
	\dry1, ~dirt.orbits[0].dryBus,
	\dry2, ~dirt.orbits[1].dryBus,
	\dry3, ~dirt.orbits[2].dryBus,
	\dry4, ~dirt.orbits[3].dryBus,
	id: \valhalla
]);

~valhalla = VSTPluginController(~valhallaSynth, id: \valhalla);
~valhalla.open("ValhallaSupermassive.vst3");

~dirt.receiveAction = { |event|
    var e = event.copy;
	var orbit = ~dirt.orbits[e.at(\orbit)];

	~valhallaSynth.set(("reverb" ++ (orbit.orbitIndex + 1)).asSymbol, e.at(\reverb) ?? orbit.defaultParentEvent.at(\reverb));
};

Okay so I finally get the idea how "NamedControls" are workin'. This makes it a lot more flexible and prettier. So this is how I implemented my global reverb bus and made it more scalable. These are my SynthDefs that I use:

var numChannels = ~dirt.numChannels;

SynthDef(\valhalla2, {|out|
	var size = 14; // Works for 14 orbits -> this value needs to be fixed in a UGen
	var dryBusses = NamedControl.kr(\dryBusses, (0 ! size ));
	var wetReverbs = NamedControl.kr(\wetReverbs, (0 ! size));
	var fxSound = size.collect({arg i; In.ar(dryBusses[i], numChannels) * wetReverbs[i]}).sum;

	fxSound = VSTPlugin.ar(fxSound, numOut: numChannels, id: \valhalla);

	Out.ar(out ,fxSound);

}).add;


SynthDef(\masterSynth, { |out, fxBus|
	var size = 14;
	var wetSound = In.ar(fxBus, numChannels);
	var dryBusses = NamedControl.kr(\dryBusses, (0 ! size ));
	var wetSums = NamedControl.kr(\wetSums, (0 ! size));

	var drySound = size.collect({
		arg i;
		ReplaceOut.ar (dryBusses[i], In.ar(dryBusses[i], numChannels) * (1/(wetSums[i] +1) ));
	});

	Out.ar(out, wetSound);
}).add;

Basically I added a master Synth to write the sum of i.e. three fx busses and write them to my output bus (so to say I replace all audio signals on my hardware output with my global fx bus signal and reduce the gain of my dry sounds respectively).

And this is how I initialize it:

var numChannels = ~dirt.numChannels;
var masterBus = Bus.new;
var globalReverbBus = Bus.audio(~dirt.server, ~dirt.numChannels);
var wetReverbs = Array.fill(~dirt.orbits.size, 0);
var wetSums = Array.fill(~dirt.orbits.size, 0);
var dryBusses = ~dirt.orbits.collect({arg item; item.dryBus});

~valhallaSynth = Synth(\valhalla2, [
	\out, globalReverbBus,
	id: \valhalla
], addAction: 'addToTail');

~valhallaSynth.set(\dryBusses, dryBusses);

~valhalla = VSTPluginController(~valhallaSynth, id: \valhalla);
~valhalla.open("ValhallaSupermassive.vst3");

~masterSynth = Synth(\masterSynth, [
	\out, masterBus,
	\fxBus, globalReverbBus,
], addAction: 'addToTail');

~masterSynth.set(\dryBusses, dryBusses);

~dirt.receiveAction = { |event|
    var e = event.copy;
	var orbit = ~dirt.orbits[e.at(\orbit)];
	var reverb = e.at(\reverb) ?? orbit.defaultParentEvent.at(\reverb);
	var wet = reverb;

	wetReverbs.put(orbit.orbitIndex, reverb);
	wetSums.put(orbit.orbitIndex, wet);

	~valhallaSynth.set(\wetReverbs, wetReverbs);
	~masterSynth.set(\wetSums, wetSums);
};

So maybe this is helpful for someone or I would be glad of having a discussion about this.

1 Like