DynGen - Dynamic UGen

I have no idea if or how this could work but I thought it might be good that the available operators for DynGen are a subset of the inline functions which are available via the plugin interface with additional things added. Some of the things one can find in gen are already available there.

1 Like

Thank you very much for this amazing work !

I have a small question: when I use 3 outputs in this example, I get the message : Ndef(‘y’): wrapped channels from 3 to 2 channels.

How can I actually get 3 physical outputs?

~multi = DynGenDef(\multi, "
out0 = in0 * in1; 
out1 = in0 * in2 ; 
out2 = in0 * in3;
").send;


(
Ndef(\y, {DynGen.ar(3, ~multi,
	SinOscFB.ar(200.0, 1.3), // in0
	LFPulse.ar(5.2, width: 0.2), // in1
	LFPulse.ar(3.2, width: 0.3), // in2
	LFPulse.ar(3.2, width: 0.4) // in3
) * 0.2}).play;
)

José

If I’m not mistaken Ndef(...) will by default create two output channels. If you want to have a different number of output channels have a look at Ndefs ar method or use e.g. mold. Don’t know for sure - haven’t tried out DynGen yet but “wrapped channels from 3 to 2 channels.” very much looks like the problem’s within Ndef, not DynGen.

This happens when you spwaned an Ndef with 2 channels but now changed it to output 3 channels.
You can either reset the Ndef via Ndef(\y).clear or change its output routing to “elastic”, via Ndef(\y).reshaping = \elastic;.
Alternatively you can set all Ndefs to follow elastic reshaping via Ndef(\y).proxyspace.reshaping = \elastic;.

Ah ok, thanks a lot for your answer!
I’m not very used to working with Ndef, so I got a bit confused.
By the way, I know that SynthDef structures are static, but would it be possible to have a SynthDef that encapsulates a DynGen? (i.e. define a normal SynthDef but with the dynamic DynGen code inside) ?

1 Like

but would it be possible to have a SynthDef that encapsulates a DynGen?

Of course! DynGen is just a normal UGen that you can use in any SynthDef.

For some reason, all the examples use Ndef instead of SynthDef, which might have led you to the impression that Ndef might somehow be required.

1 Like

Thanks @Spacechild1 !

Something like this ?

SynthDef(\SynthDynGen, { |script|
	var sig;
	sig = DynGen.ar(1, script); 
	sig.poll;
}).add;

a = Synth(\SynthDynGen);
b = DynGenDef(\example, "out0 = 0.5;").send;
a.set(\script, b); // doesn’t work
a.set(\script, \example); // doesn’t work

Doesn’t seem to work. Do I need to send a specific message? Thanks!

The script argument must be a Symbol or DynGenDef instance. It is fixed and can not be set via a Synth argument. (Generally, Synth args cannot be Symbols.)

EDIT: Turns out that DynGen doesn’t really validate the script argument. I’ve opened an issue on GitHub: Check type for `script` argument. · Issue #36 · capital-G/DynGen · GitHub

So it’s not possible to have a SynthDef that can load scripts dynamically?
We can only create a static SynthDef, which isn’t very interesting


But this has always been true – SynthDefs are by definition static DSP graphs. And people do complain about that from time to time, but in the SC3.x line, that’s what we’ve got. (Remember also that we can add and remove synths at will, glitch-free, which Max 8 and Pd both struggle with. I’m not sure to what extent Max 9 fixes that. I do have confirmation from Pd devs that adding a new DSP object causes a global re-sorting of all DSP objects everywhere in the environment; with large patches, this is very likely to cause dropouts. James McCartney’s strict rules about UGen instantiation mean that SC’s design supported use cases a couple decades ago that the other big players can’t touch.)

Also note that synth inputs have to be floats – strings are language-side entities that synths don’t understand. You’d also have trouble passing an OSC command path as a synth input to use with SendReply.

SynthDef is a complicated object, but it’s a low-level object. I don’t think it should be expected to do everything. What we do in that case is what computer science has done for decades: build abstractions that use the provided resources. It would be possible to create an object that prepares the eel script on the server side and makes a SynthDef for it – to make the user interface more transparent. There’s a way to handle timestamps, too.

hjh

The associated DynGenDef object must be constant, but the actual script code can be set dynamically! Otherwise the object wouldn’t be called DynGen :wink:

SynthDef(\test, {
	DynGen.ar(1, \test).poll;
}).add;

// define the code
~def = DynGenDef(\test, "out0 = 0.5;").send;

Synth(\test);

// replace the code while the Synth is playing
~def.code_("out0 = 0.1;").send;
2 Likes

Ahh, nice. It’s worth noting that this is different from SynthDef and Synth.

  1. Declare a SynthDef.
  2. Play it in a Synth.
  3. Redefine the SynthDef, using the same name. The Synth continues to use the old definition.
  4. Stop the synth.
  5. Play a new one – now it uses the new definition.

So I wouldn’t have immediately guessed that mutating a DynGenDef would trickle into running synths – but the fact that they do will be great for debugging :grin:

hjh

Fyi, your example might be clearer if you named the synthdef something else!

I haven’t said this already but I think this is project great!!!

One thing, I dislike the syntax of having to name the thing twice, once as a symbol (globally) and then again as a variable. Having two names for things is confusing.

1 Like

You don’t need to keep the DynGenDef in a variable, you can also just redefine it:

// define the code
DynGenDef(\test, "out0 = 0.5;").send;

Synth(\test);

// replace the code while the Synth is playing
DynGenDef(\test, "out0 = 0.1;").send;
1 Like

There are plans to control this behavior on a per-Ugen basis: Maintain script state over hot-reload? · Issue #9 · capital-G/DynGen · GitHub

So I tried it just now (partly curiosity, partly because I’ve got a student who’s interested in prototyping DSP) and – painless build in Linux, and a quicky biquad bandpass filter was completely straightforward.

Nice work!

EDIT: Forgot this question – per the help file, “
 the initialization of the VM and the compilation of the script is defered to a non-realtime thread
” – is this per DynGen instance, per Synth?

hjh

3 Likes

Every DynGen instance has its own VM which must be initialized on the NRT thread. Same for dynamic code updates. At the moment, if you have 100 DynGen instances, the same code is compiled 100 times. Unlike Faust, EEL2 does not cleanly separate the compiled code from the audio processor. The (pretty sparse) API documentation for EEL2 suggests that code can be shared between VMs, but to me it’s not entirely clear how and to which extent. Maybe @dscheiba knows. That being said, code compilation is so quick that it probably doesn’t matter in practice.

Ok. I was thinking of the case of, say, Karplus-Strong as an instrument SynthDef. The usual SC approach of one synth per note would involve a lot of recompiling, and probably timing compromises wrt timestamps. So in that case, it would be better to have a pool of n synths that are triggered when needed, depending on event density.

Thanks,
hjh

Yes. EEL2 has really been designed for effect plugins, which traditionally are not created and destroyed on the fly. If you want to use it for synth notes (with precise timing), you indeed have to pre-allocate your voices in a pool. (This will feel very familiar to Pd users :slight_smile: )

Faust is really well-designed in this regard because it cleanly separates the code from the processor: code compilation is not realtime-safe, of course, but the actual audio processors can be created in a fully realtime-safe manner. As a consequence, FaustGen instances can be initialized synchronously, just like any other traditional SC UGen. This is not really possible with EEL2.

(There is a variant called DynGenRT which compiles the code directly on the RT thread; it is synchronous but not realtime-safe. Depending on your system and overall CPU load you might get away with it, but there are no guarantees.)

1 Like

A VM is only initiated once during server boot up. The VM has isolated scopes, such that all scripts run within the same VM, so only the compilation and allocation step is performed for each DynGen instance.

IMO this is still possible and it is impressive what one can get away with. The snippet below implements Karplus strong w/ a delay line of 1024 samples - every voice gets allocated and compiled on the RT thread w/o any dropouts on 30% CPU usage. I am running this on a M4 though - but switching to supernova could probably help to reduce the load further but this would probably crash b/c I haven’t setup thread synchronization of the VMs yet.

One bottleneck is currently the destruction of the DynGen resources in the RT thread, see Make EEL2 adapter destruction RT safe · Issue #12 · capital-G/DynGen · GitHub

(
// a very basic karplus strong
// could maybe improved by interpolated reading
// and a better filter
DynGenDef(\karplus, "
// reducing this helps to reduce the load immensly!
bufSize = 1024;
inputSig = in0;
delaySamples = in1;
fb = in2;
y0 += 0;

writePos += 1;
writePos >= bufSize ? (writePos = 0;);

readPos = writePos - delaySamples;
readPos <= 0 ? (readPos = readPos + bufSize);
// a basic filter which uses the average of cur and prev sample
delay = (((1-fb)*buf[readPos]) + (fb*y0));
y0 = delay;

sig = delay + inputSig;

buf[writePos] = sig;

out0 = sig;
").send;
)

// some ndef for live coding/debugging
(
Ndef(\karplus, {
	var inputSig = WhiteNoise.ar;
	var gate = Trig.ar(Impulse.ar(SinOsc.ar(pi/12).range(0.2, 10.0)), SinOsc.ar(pi/10).exprange(0.001, 0.001));
	var sig = DynGen.ar(1, \karplus,
		inputSig * gate,
		SinOsc.ar(0.1).exprange(10, 1024), // delay in samples
		SinOsc.ar(pi.reciprocal).range(0.5, 0.9),  // fb param
	);
	sig.dup * \amp.kr(0.2);
}).play;
)

Ndef(\karplus).stop;

(
// now as a synthdef
SynthDef(\karplus, {|out|
	var inputSig = WhiteNoise.ar * Trig1.ar(1.0, \trigLen.kr(0.01));
	
	// use RT b/c we do not  want to skip the first block of input samples
	// alternative: Use DynGen and delay the input sig by one block size
	var sig = DynGenRT.ar(1, \karplus,
		inputSig,
		\delaySamples.kr(100.0), // delay in samples
		\fb.kr(0.7),  // fb param
	);
	DetectSilence.ar(sig, 0.01, doneAction: Done.freeSelf);
	sig = sig.dup * \amp.kr(0.2);
	Out.ar(out, LeakDC.ar(sig));
}).add;
)

// test it
~x = Synth(\karplus);

// some polyphonic pattern
(
Pdef(\karplus, Pbind(
	\instrument, \karplus,
	\dur, Pseq([0.3, 0.1], inf),
	\delaySamples, Pclump(2, Prand((50, 100..500), inf)),
	\fb, Pseq([0.96, 0.8, 0.9], inf),
	\amp, Pexprand(0.2, 0.5),
)
).play;
)

// more karplus
Ndef(\karplus).play;

When adjusting the moment of the DoneAction it is even possible to do more drastic patterns

Pdef(\karplus, Pbind(
	\instrument, \karplus,
	\dur, Pseg([0.5, 0.01], 15, \exp, 2),
	\delaySamples, Pclump(2, Prand((1..20).linexp(1, 20, 10, 1024), inf)),
	\fb, Pseq([0.96, 0.8, 0.7], inf),
	\amp, Pexprand(0.2, 0.5)/2,
	\trigLen, 0.01/2,
)
).play;
)

Though it would be nice to have an abstraction to bind a Pbind to a pool of Synth instances - I
don’t know if such a thing already exists?

1 Like