A prototype of decompiling SynthDefs

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 :wink:

hjh

2 Likes

A little while ago is suggested a pipe operator.

I don’t really know how this is better/clearer/easier-to-extend than than. This is what your example would look like using that syntax.

{
	var pulse = Impulse.ar(1) |> Decay2.ar(_, 0.001, 0.05) * PinkNoise.ar;
	
	var delay = _ + (\feedback.kr * LocalIn.ar(1)) 
	|> DelayC.ar(_, 1, \freq.kr.reciprocal - ControlDur.ir);
	
	var filter = LPF.ar(_, \ffreq.kr) |> LPF.ar(_, \ffreq.kr);
	
	var outs = { |i| 
		Out.ar(\out.kr, i); 
		LocalOut.ar(i) 
	};
	
	pulse |> delay 	|> filter |> outs
}

Am I missing something?

1 Like

That’s interesting but a bit orthogonal to the purpose of JITModular, which is specifically to use JITLib features to divide synthesis among multiple modules (separate NodeProxies) and exploit audio and control rate mapping to connect them. The idea is to have a lot of very short modules (if it’s more than 10 lines then it’s probably too big a module) that can be altered and hot-swapped individually. (This was intended for classroom use, thinking that shorter synthesis functions would be easier for non-programmers to engage with.)

The pipe looks like a neat way to write a single large synthesis graph – it’s a bit punctuation-heavy for my taste but it will work in a lot of cases. But if it’s already a single synthesis graph, then there’s no need for a decompiler to merge multiple SynthDefs down into one, so the main body of my post would be moot.

What I’m talking about here is tangentially related to my post the other day in the class library dev thread, about a modular PatchDef that could be used more or less the way that we use SynthDef and Synth now, but simplify control mapping and hot-swapping – proposing an idiom where synthesis is distributed among multiple physical nodes. Apart from JITLib, this idiom doesn’t really exist in SC presently, but I think it would bring several benefits.

hjh

2 Likes

Oddly enough I was asking ChatGPT something about filters in SC the other day and it used this syntax. Now I know where it came from at least. The algorithm has looked upon you with favor :joy: