Simple way for continuous control over synth args in a pattern?

I feel like I am forgetting some simpler way to do this, especially given how common this must be… maybe someone can help me out.

Basically I am trying to pass a continuous control from an envelope to a Synth that gets its pitches from a pattern. In the example it’s the modulation index that I’ve helpfully named “param”.
I can’t use Pbind(param: Env()) here, because that will just embed whatever value the Env has when the Event is created.
So what one needs to do is set up a bus, map to it, start the synth, then send that Pattern to the Synth with the \set type, which is what the below code does. So the problem isn’t that it doesn’t work, but rather that I suspect I’m forgetting some other obvious simpler option. Any help appreciated!

(
~voice = {|name, envdef, synthdef, patdef|
    Tdef(name, {
        var bus = Bus.control(s, 1);
        var env = {envdef.kr(gate:1)}.play(target:s, outbus: bus);
        var syn = Synth(synthdef);
        syn.map(\param, bus);
        Pchain(Pbind(\type, \set, \id, syn.nodeID), Pdef(patdef)).play;
    }
    )
};

SynthDef(\testSynth, { | out, amp, freq, param |
    var sound = PMOsc.ar(freq, freq * 277/37, param) * amp;
    Out.ar(out, sound);
}).add;

Pdef(\testPat,
    Pfindur(20,
        Pbind(
            dur: Pbrown(0.5, 1.5, 0.125),
            db: -15,
            midinote: Pseq((60..71),inf),
        )
    )
);

~voice.(\testTask, Env([0,1,0], [10, 10]), \testSynth, \testPat);

Tdef(\testTask).play;
)

I’d use Ndef to simplify the bus management.

Two options here:

(
SynthDef(\testSynth, { | out, amp, freq, param, gate = 1 |
	var eg = EnvGen.kr(Env.adsr, gate, doneAction: 2);
	var sound = PMOsc.ar(freq, freq * 277/37, param) * (amp * eg);
	Out.ar(out, sound.dup);
}).add;
)

// option 1: envelope applies within each note while also spanning notes
(
Ndef(\env, { EnvGen.kr(Env([0,1,0], [10, 10])) });

Pdef(\testPat,
	Pfindur(20,
		Pbind(
			instrument: \testSynth,
			dur: Pbrown(0.5, 1.5, 0.125),
			db: -15,
			midinote: Pseq((60..71),inf),
			param: Ndef(\env).asMap
		)
	)
).play;
)

// or, envelope value is latched for each note
(
Ndef(\env, { EnvGen.kr(Env([0,1,0], [10, 10])) });

Pdef(\testPat,
	Pfindur(20,
		Pbind(
			instrument: \testSynth,
			dur: Pbrown(0.5, 1.5, 0.125),
			db: -15,
			midinote: Pseq((60..71),inf),
			param: Pfunc { Ndef(\env).bus.getSynchronous }
		)
	)
).play;
)

hjh

1 Like

Thanks! I knew there was something. I wonder where this should go in the docs, if it isn’t there already…

(Also I was lazy with the test synthdef, amazing what a simple amplitude envelope does for the sound!)

I’m noticing that in the above version, this only works once, presumably because the Envelope won’t retrigger. instead writing, inside the Pbind,

...
param: Ndef(\env {EnvGen.kr(Env([0,1,0], [10, 10])) }).asMap
...

will work because the object is recreated each time. Not a problem in my current project, but how would I go about making the Node retriggerable each time the pattern is played?

As for your second option, is there any relevant difference from just passing the Env instead of the Pfunc that gets it from the bus?

I’d written both Ndef and Pdef in the same ( ... ) block so that they’d both run at the same time, resetting the Ndef synth.

IMO a pattern itself isn’t always the best focal point for .play. In practice, patterns are seldom self-contained, and efforts to make them self-contained (to initialize everything they are going to need, and clean up after themselves) work in some cases but become quite awkward in complex scenarios. So, in a way, my ideal solution would be to manage the Ndef/Pdef dependency outside of either of those objects, e.g.:

(
~runEnvAndPat = { |key = \process,
	env( Env([0, 1, 0], [10, 10]) ),
	pattern({ |key|
		Pfindur(20,
			Pbind(
				instrument: \testSynth,
				dur: Pbrown(0.5, 1.5, 0.125),
				db: -15,
				midinote: Pseq((60..71),inf),
				param: Ndef(key).asMap
			)
		)
	}) |
	
	Ndef(key, { EnvGen.kr(env) });
	Pdef(key, pattern.value(key)).play;
};
)

… and now you get the possibility of running different envelopes and different patterns concurrently.

If you really need it specifically in the pattern object, there are a couple of options:

(
Pdef(\testPat, Pfindur(20,
	Pbind(
		instrument: \testSynth,
		dur: Pbrown(0.5, 1.5, 0.125),
		db: -15,
		midinote: Pseq((60..71),inf),
		param: Plazy {
			Ndef(\env, { EnvGen.kr(Env([0,1,0], [10, 10])) });
			Pn(Ndef(\env).asMap, inf)
		}
	)
));
)

Pdef(\testPat).play;

Pdef(\testPat).stop;

Pdef(\testPat).reset;

// now the Plazy also resets, causing the envelope to reset
Pdef(\testPat).play;

Pdef(\testPat).stop;


// or, clean up the Ndef too
(
Pdef(\testPat, Pfset(
	{
		Ndef(\env, { EnvGen.kr(Env([0,1,0], [10, 10])) });
	},
	Pfindur(20,
		Pbind(
			instrument: \testSynth,
			dur: Pbrown(0.5, 1.5, 0.125),
			db: -15,
			midinote: Pseq((60..71),inf),
			param: Ndef(\env).asMap
		)
	),
	cleanupFunc: { Ndef(\env).clear }
));
)

hjh

1 Like

A common demand but a lot of ways to do this or similar. You can check miSCellaneous_lib’s tutorial Event patterns and LFOs suggesting various strategies. I didn’t include Tasks in there but most setups can be transferred to Tasks as well.

1 Like

I’d written both Ndef and Pdef in the same ( ... ) block so that they’d both run at the same time, resetting the Ndef synth.

Thanks! I tend to ignore the (…) blocks, but maybe I shouldn’t… I guess I was looking for existing language features that make it easier for me as a user/composer to associate the envelope with the pattern. In the end the brackets are exactly that…

Thanks @dkmayer, I’ll check out the tutorial. I have the bad habit of mostly looking at the (unextended) documentation and trying to figure it out from there, when that’s not always the best starting point for finding out how to do a particular thing… as there are so many other resources around