Backstory: A few years ago, I made an add-on to JITLib to facilitate using a ProxySpace as a (single-voice) modular synth, with save/restore (including buffers and MIDI control mapping). A NodeProxy is a module; connections are defined by e.g. ~source <>> ~target
.
This has been a really nice way to experiment with synthesis, hot-swapping modules without destroying the entire arrangement.
But I found that it was annoying to pull all the modules together into a single SynthDef, for use as an instrument. There were too many little bits of code to copy (modules should be short), and the ~envVar syntax needed to be hand-edited, and it was too easy to make mistakes with control or audio inputs mapped to other proxies’ outputs.
So I’ve just been playing with analyzing the object structure and writing code that would collapse the synthesis graph into one SynthDef. Not quite ready for release (in particular, there are a lot of UGen special cases that I haven’t addressed yet), but as a prototype, it’s kind of cool.
As an example, I defined modules for a Karplus-Strong plucked string as follows:
// I find it helpful to have a single point of output
~out = #{ JMInput.ar }; ~out.play(vol: 0.2);
// Noise burst
~pulse = #{
var eg = Decay2.ar(Impulse.ar(1), 0.001, 0.05);
PinkNoise.ar * eg
};
// Delay module has two audio inputs:
// Implicit name 'in' for the noise burst
// 'fb' input for the feedback signal from the filter
~delay.addSpec(\freq, [2, 300, \exp], \feedback, [0, 0.999, -4]);
~delay = #{ |freq = 200, feedback = 0.99|
var time = freq.reciprocal - ControlDur.ir;
DelayC.ar(JMInput.ar + (feedback * JMInput.ar(\fb)),
1,
time
)
};
~filt.addSpec(\ffreq, \freq);
~filt = #{ |ffreq = 8000|
LPF.ar(LPF.ar(JMInput.ar, ffreq), ffreq)
};
// Main signal chain
~pulse <>> ~delay <>> ~filt <>> ~out;
// Feed filter back into delay
~filt <>>.fb ~delay;
The biggest problem in this example is recognizing the feedback loop and converting to LocalIn / LocalOut. (Because the target is a single SynthDef, normal buses aren’t appropriate.) Input mapping is also important: ar or kr controls that are taking their value from other modules should not render as controls, but rather refer to the source.
If p
is the JITModPatch, then with the experimental (unreleased) new stuff:
d = JMDecompiler(p);
d.streamCode(Post);
->
var localInAr = LocalIn.ar(2).asArray;
var pulse_0 = Impulse.ar(1, 0.0);
var pulse_1 = Decay2.ar(pulse_0, 0.001, 0.05);
var pulse_2 = PinkNoise.ar();
var pulse_3 = (pulse_2 * pulse_1);
var pulseOut = [pulse_3, pulse_3];
var delay_0 = Control.names(['freq', 'feedback']).kr([200, 0.99]);
var delay_1 = reciprocal(delay_0[0]);
var delay_2 = ControlDur.ir;
var delay_3 = (delay_1 - delay_2);
var delay_4 = [pulseOut[0], pulseOut[1]] /* pulse --> AudioControl */;
var delay_5 = [localInAr[0], localInAr[1]];
var delay_6 = MulAdd(localInAr[0], delay_0[1], pulseOut[0]);
var delay_7 = DelayC.ar(delay_6, 1, delay_3);
var delay_8 = MulAdd(localInAr[1], delay_0[1], pulseOut[1]);
var delay_9 = DelayC.ar(delay_8, 1, delay_3);
var delayOut = [delay_7, delay_9];
var filt_0 = Control.names(['ffreq']).kr([8000]);
var filt_1 = [delayOut[0], delayOut[1]] /* delay --> AudioControl */;
var filt_2 = LPF.ar(delayOut[0], filt_0);
var filt_3 = LPF.ar(filt_2, filt_0);
var filt_4 = LPF.ar(delayOut[1], filt_0);
var filt_5 = LPF.ar(filt_4, filt_0);
var filtOut = [filt_3, filt_5];
var out_0 = [filtOut[0], filtOut[1]] /* filt --> AudioControl */;
var outOut = [out_0[0], out_0[1]];
LocalOut.ar([filtOut[0], filtOut[1]]);
If you wrap this in a SynthDef template and add Out.ar(\out.kr(0), outOut)
, it sounds correct.
One could quibble over some redundancies – e.g. delay_4 = [pulseOut[0], pulseOut[1]]
renders the in
AudioControl, but the downstream references to these channels, in delay_6 and delay_8, ignore delay_4 and go back to the pulseOut
source. TBH I don’t think I will do anything about that, though.
There was a question a while back about recovering SynthDef code from a binary scsyndef file. This prototype could be simplified to do that (although I don’t think it’s an improvement over just saving the original code as scd). Here, the magic is in transforming signal connections via buses into variable references, automating something that I had found painful to do by hand.
Hope this is at least amusing to some
hjh