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.
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) ?
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.
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 ![]()
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;
Ahh, nice. Itâs worth noting that this is different from SynthDef and Synth.
- Declare a SynthDef.
- Play it in a Synth.
- Redefine the SynthDef, using the same name. The Synth continues to use the old definition.
- Stop the synth.
- 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 ![]()
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.
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;
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
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
)
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.)
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?
