Use NodeProxy to write "effects" on main Out channels

Hi there, I’d like to be able to use the NodeProxy/Ndef approach to live code effects or “filters” on the main SC output (channel 0). So basically it should run after the default Group, and read from channels 0,1 and replace their output.

Maybe this is a wrong approach (it’s quite easy to do this with regular Synths and ReplaceOut), but I like the convenience of fadeTime and stacking and replacing running effects “on the fly”, with nice cross-fading, etc.

For example, I’d like to do something like this (almost works as I’d like):

s.boot;
// Play something (on output 0)
q = { SinOsc.ar(LFDNoise3.kr(0.5).exprange(220,440), 0, 0.1) ! 2 }.play;

// Create a Group for our NodeProxy after the Server's default
g = Group.after(s.defaultGroup).register;

// Define a passthrough
SynthDef(\passthrough, { arg out = 0; ReplaceOut.ar(out, In.ar(0, 2)) }).add;
Ndef(\outfx).parentGroup_(g);
Ndef(\outfx).source = \passthrough;

// "Live code" some effect(s)
Ndef(\outfx)[10] = \filter -> { arg in; PitchShift.ar(in, 0.2, 0.5) };

// When playing, the following "adds" the effect to bus 0, but I'd want to replace it
Ndef(\outfx).play;

Thanks,
Glen.

You can replace the automatically-assigned bus with one of your choosing.

Ndef(\x).ar(2);  // initialize to audio rate

// throw away private bus,
// tell this proxy to play onto the hardware bus
Ndef(\x).bus = Bus(\audio, 0, 2, s);

Ndef(\x, { SinOsc.ar(440, 0, 0.1).dup });

// let's check the Out bus
s.sendMsg(\n_trace, Ndef(\x).objects[0].nodeID);

  unit 7 Out
    in  0 -0.012414 -0.012414
    out

// yep, it's 0

Ndef(\x).clear;

hjh

Thanks. I’d tried using the bus property, but I’d set it to 0, not Bus(\audio, 0, 2, s) – that works great, thanks.

This works but loses the play functionality (semantics) in NodeProxy this way, i.e. one would have to rely on pause and resume.

A more elaborate way to do this that preserves play functionality is to define your own custom Monitor. The default one is almost flexible because its playNToBundle method does take in a defName, but this only happens at the last level in the call hierachy and ther are like 5 nested calls before reaching that one so one would have to replicate a lot Monitor machinery in user code to leverage that directly. The more flexible approach is to make defName an instance variable (in a subclass) like so

FlexMonitor : Monitor {

	var <>defName = "system_link_audio_1";

 	copy { // not tested
		^this.class.newCopyArgs(*[ins, outs, amps, fadeTimes, vol, defaults, defName].deepCopy)
	}

	playNToBundle {
		| bundle, argOuts, argAmps, argIns, argVol, argFadeTime, inGroup, addAction,
		defName, multi = false |

		^super.playNToBundle(
			bundle, argOuts, argAmps, argIns, argVol, argFadeTime, inGroup, addAction,
			defName ? this.defName, multi)
	}
}

The one can make a modified copy of the system_link_audio_1 synthdef (from SystemSynthDefs) to do ReplaceOut instead of Out.

(
var i = 1; // only 1-chan version is ever used by JITlib
SynthDef("system_replace_audio_" ++ i,
	{ arg out=0, in=16, vol=1, level=1, lag=0.05, doneAction=2;
		var env = EnvGate(i_level: 0, doneAction:doneAction, curve:'sin')
		* Lag.kr(vol * level, lag);
		ReplaceOut.ar(out, InFeedback.ar(in, i) * env)
}, [\kr, \kr, \kr, \kr, \kr, \ir]).add;
)

And now for tests of the above:

Ndef.clear;
a = Ndef(\ah, { 0.5 * SinOsc.ar(777) !2 }).play;
b = Ndef(\bbh, { 0.5 * SinOsc.ar(333) !2 }).play;

s.queryAllNodes; // shows use of the default system_link_audio_1
s.scope; // normal Monitor play mixes

a.stop; b.stop;
// won't have effect to change monitor type while playing
a.monitor = FlexMonitor.new.defName_("system_replace_audio_1");
b.monitor = FlexMonitor.new.defName_("system_replace_audio_1");

a.play; b.play; // only one heard (should be b)
s.queryAllNodes; // should see system_replace_audio_1
b.stop; // a heard now

// with explicit order control:
a.play; b.play(addAction: \addToHead) // a heard because the order is b then a.
a.stop; // b heard

I’m guessing few people needed this because the default Monitor is a bit clumsy to use flexibly like this.

There are also a bunch of additional synth defs in SystemSynthDefs, but none of these seem used by anything: system_link_control is not used unlike _audio variant; and likewise system_setbus_hold_audio and system_setbus_audio. Only
system_setbus_control is used by something, but that’s in the test suite, by TestEvent.

I suppose the only time when this is default monitor synth replacement useful is when one mixes NodeProxies with synths generated in some other way, either directly or through some other framework or library.

Otherwise, any NodeProxy essentially acts as a bus mixer on its array of sources. The more obscure feature of the \filter NodeProxy role is that it replaces output by default.

a = Ndef(\ah, { 0.5 * SinOsc.ar(777) !2 }).play
a[1] = { 0.5 * SinOsc.ar(333) !2 } // mixes with a[0] def above

a[1] = nil // removes

a[1] = \filter -> { 0.5 * SinOsc.ar(333) !2 } 
// ^^ replaces output, because \wet1 is 1 by default

s.queryAllNodes(true) // can see \wet1 is 1

a.set(\wet1, 0.5) // now it's a mix
a.set(\wet1, 0) // and now passthrough

The only slight gotcha with this is that the syntax is a bit non-obvious when mapping other proxies this way:

Ndef.clear
a = Ndef(\ah, { 0.5 * SinOsc.ar(777) !2 }).play
b = Ndef(\bbh, { 0.5 * SinOsc.ar(333) !2 })

a[1] = \filter -> b // won't work like this
a[1] = \filter -> { b.ar(2) } // ok