Saving a Preset Sequence of SynthDefs

I’d like to create a GUI where the order of SynthDefs can be saved and recalled.
I know that this would be simplified by using a NodeProxy, but I ran into issues there and I’m wondering how and if the following would be possible.

The attached example has three synthdefs. Two are effects. One is an audio source.
The order that the buttons are clicked obviously determine the order-of-operations.
I’m wondering if it is possible to “save” different arrangements of these Synths in a preset that could be recalled. At the moment, the modules don’t have any particular arguments - but I would like to expand this to also store and recall arguments.

My instinct is that each button would have an additional function where it sent incremental order to a collected list, but I have no idea how that list would be formatted or recalled.

(
SynthDef("audio",
	{|out|
		var sig = LFTri.ar(Lag.ar(LFNoise0.ar(4).range(200, 800),1))*Decay.ar(Dust.ar(1));
	Out.ar(out, sig);
}).add;

SynthDef("filter1",
	{|in, out|
	var sig;
		sig = Decimator.ar(In.ar(in), 1000, 4);
	Out.ar(out, sig);
}).add;


SynthDef("filter2",
	{|in, out|
	var sig;
		sig = Ringz.ar(In.ar(in), 200, 2);
	Out.ar(out, sig*0.2);
}).add;

)
(
w = Window("").front;

a = Button(w, Rect(20, 20, 90, 20)).states_([["audio on", Color.black, Color.white], ["audio off", Color.white, Color.black]]);
a.action_({|m|
	if (m.value==1, {x = Synth("audio")},
		{x.free;});

});

b = Button(w, Rect(20, 50, 90, 20)).states_([["filter1 on", Color.black, Color.white], ["filter1 off", Color.white, Color.black]]);
b.action_({|m|
	if (m.value==1, {y = Synth("filter1");},
		{y.free;}
	);
});

c = Button(w, Rect(20, 80, 90, 20)).states_([["filter2 on", Color.black, Color.white], ["filter2 off", Color.white, Color.black]]);
c.action_({|m|
	if (m.value==1, {z = Synth("filter2");},
		{z.free;}
	);
});





)

It’s a hard problem in general. Probably the most ready made thing that exists for this is in general is the TreeSnapshot class in Scott’s NodeSnapshot quark. See related discussions here e.g. how to use it. I haven’t looked to see if if has a permanent store function (it’s a large class). It has a storeOn method but I don’t know if it deserializes well.

It’s not clear to me what your problem with Ndefs was, but for those there’s NdefPreset in the JITLibExtensions quark that’s perhaps useful. It already has a storeToDisk option (inherited from ProxyPreset), but it won’t save entire sets of Ndefs, from what I see. It’s still your responsibility to iterate over the Ndefs of interest, in the order of dependencies, especially when restoring them.

By the way, the Archive class is generally useful for saving stuff and having it
restored on sclang startup, but it’s just persistent dictionary basically, it won’t
do any other magic.

The example you have posted has a problem that it won’t work properly if the order of clicking is different than the obvious one because you’re not enforcing any Synth execution order, so the filters can actually execute ahead of the audio, depending on the order of clicking. See \addToHead etc in Synth or Ndef sources slots how to fix that.

Merely collecting the state of the buttons in a list however is a simple matter:

~vals = [a, b, c].collect(_.value)

To restore them with those values, triggering their action too, assuming the buttons are instantiated

~buts = [a, b, c];
~buts.do { |but, i| but.valueAction_(~vals[i]) }

But generally you don’t want the state of the gui to be the place where that kind of info is stored, especially other things like synth parameters. Read about model-view-controller.

I’m having a bit of trouble with the NodeSnapshot quark (ERROR: Could not open file “/archive.sctxar” for writing…can post more, if useful)… but just to confirm, this would work outside of Ndefs?
The issue with Ndefs is not due to save states - Ndefs seem to be an excellent for saving - but there are some challenges when it comes to large systems of inter-modulating synths, due to the built-in bus management. I could try to formulate that question, if it turns out there is no way to do this within the Synths/SynthDef control protocol.

That’s some kind of problem with your SC installation. It normally tries to write in your Platform.userAppSupportDir.

NodeSnapshot works for plain SynthDefs, with some limitations that if you overwrite a SynthDef locally it might not find that out; it tries to detect such problems by hashing the arg list of SynthDef. But NodeSnapshot just gives you a structure that you can walk over and save and later restore yourself. There’s no automatic restore function that I can see. (Maybe @scztt can chime in on that, because I’m guessing he uses it to actually save and restore the tree.)

Sorry, that was a mistake on the NodeSnapshot error message…
It reads as follows:

WARNING:  !
Execution warning: Class 'Material' not found
WARNING: keyword arg 'color' not found in call to Object:doesNotUnderstand
ERROR: Message 'new' not understood.
RECEIVER:
   nil
ARGS:
Instance of String {    (0x1200872c0, gc=01, fmt=07, flg=11, set=02)
  indexed slots [9]
      0 : c
      1 : h
      2 : e
      3 : c
      4 : k
      5 : _
      6 : b
      7 : o
      8 : x
}
   Integer 16
CALL STACK:
	DoesNotUnderstandError:reportError
		arg this = <instance of DoesNotUnderstandError>
	Nil:handleError
		arg this = nil
		arg error = <instance of DoesNotUnderstandError>
	Thread:handleError
		arg this = <instance of Thread>
		arg error = <instance of DoesNotUnderstandError>
	Object:throw
		arg this = <instance of DoesNotUnderstandError>
	Object:doesNotUnderstand
		arg this = nil
		arg selector = 'new'
		arg args = [*2]
	Meta_TreeSnapshotView:initClass
		arg this = <instance of Meta_TreeSnapshotView>
	Meta_Class:initClassTree
		arg this = <instance of Meta_Class>
		arg aClass = <instance of Meta_TreeSnapshotView>
		var implementsInitClass = nil
	ArrayedCollection:do
		arg this = [*5]
		arg function = <instance of Function>
		var i = 2
	Meta_Class:initClassTree
		arg this = <instance of Meta_Class>
		arg aClass = <instance of Meta_Singleton>
		var implementsInitClass = nil
	ArrayedCollection:do
		arg this = [*609]
		arg function = <instance of Function>
		var i = 556
	Meta_Class:initClassTree
		arg this = <instance of Meta_Class>
		arg aClass = <instance of Meta_Object>
		var implementsInitClass = nil
	Process:startup
		arg this = <instance of Main>
		var time = 21225.508932788
	Main:startup
		arg this = <instance of Main>
		var didWarnOverwrite = false
^^ The preceding error dump is for ERROR: Message 'new' not understood.
RECEIVER: nil

Did you install NodeSnapshot via Quarks, e.g. by using Quarks.install("https://github.com/scztt/NodeSnapshot.quark.git")? Do you have the IconSet quark listed in your includes (Preferences → Interpreter)?

See this about the Icons. You need to download manually the 3.0.2 version of the Google (Material) icons. Not the most recent one (4.0 as of now); that one doesn’t work with this quark, besides being gigantic.

This will replace running Synths when the SynthDef is changed. and could easily be amended to store and re-formulate an entire tree: extSynthDef.sc · GitHub

HOWEVER… in pragmatic terms, this is very unreliable. Specifically, “restoring” synths fails to capture the state of Env’s in the Synth - which are driven by ephemeral changes to e.g. trigger inputs. This basically means you end up with a lot of stuck notes or silent events.

In general, I would really discourage using the Server as a source of truth for the state of Synths and parameters - it’s simply not meant for this.

@polina.v - what about something like:

~synths = (
    audio: Synth(\audio),
    filter1: Synth(\filter1),
    filter2: Synth(\filter2)
);

~group = Group();

~setOrder = {
   |order|
   order.do {
      |name|
      ~synths[name].moveToTail(~group);
   }
};

~setOrder.([ \audio, \filter1, \filter2 ]);

Then your presets can be a list of names that map to Synths. If you want your Synths to be “waiting” until you give them an order, you can create them with newPaused and then use Node:run once you’ve assigned them to a group.

If you want to also capture parameters, I would suggest simply piping parameter changes through a common interface for storage:

~currentParameters = (
  audio: (), filter1: (), filter2: ()
);

~set = {
   |name, ...args| // args = [\freq, 440, \amp, 0.4, ...]
   ~currentParameters[name].putPairs(args);
   ~synths[name].set(* ~currentParameters[name].asPairs);
   )
};

~set.(\audio, \freq, 440, \amp, 0.5);

With something like this, ~currentParameters can easily be saved and re-applied. There are more sophisticated ways of doing this, e.g. via the Connection quark and ControlValueEnvir, but I think something like this avoids complex systems and is easy to build yourself if you’re sticking with a relatively simple setup.

1 Like

As a general approach, you might try to plan out what you’re building like this:

  1. What is your data model, e.g. how are you describing what you want? Construct this entirely out of plain-old stateless data, e.g. Dictionary’s / Arrays of Strings, Symbols, and Numbers? You can sketch this out easily, without having any other code in place.

  2. What “initial” setup do you need on the Server, e.g. what Groups and Synths are expected to be running already? Try to ignore any “state” things that are described by your data model in [1] - e.g. parameter values, node order, etc. - keep it as slim as possible. Put this into a function like ~initialize = {}.

  3. How do you apply your whole data model [1] to your whole running server objects [2]? For a first iteration, think about the apply as a single function, where you simply grab values from your data model and set the state of nodes. As your system grows, you may need to break up the ~apply function into multiple parts, for efficiency or code clarity - but for your initial setup, ignore that: keep it simple and apply all changes.

  4. Now, connecting your UI looks like:

~slider.action = { ~dataModel[\synth][\freq] = ~slider.value.linlin(0, 1, 20, 20000); ~apply() };

Basically: change the model, and then apply.

Thinking about things this way — the data model stays very simple. And, UI actions should almost always be 1-2 lines of code. The only complexity is in the apply function, but even this should basically look like … stepping through your data model and deciding how to apply each bit of it to your server objects.
As a side-effect, you already have a preset system - the entire data model can be easily saved, and applying a preset just consists of ~dataModel = ~loadPreset.(\foo); ~apply.()

2 Likes

Good point. It’s generally a hard problem to restore all the synths phases and what not, of which the Env is just one issue. One of the main Faust devs added something like this as todo for himself. :smiley:

However, what one can reasonably hope achieve is something that every DAW manages for plug-ins, which is basically to restore the Synths as if they had just been primed in Ndef speak or newPaused in pure Synth terms. Best to do this in a large bundle, of course, so you can start all of them at once. Saying this for @polina.v as I’m sure you know it.

Re: NodeSnapshot - it seems like I missed that information about the icons. I will have to dig around a bit to understand how that all works, but it sounds like NodeSnapshot might not be recommended for what I’m trying to do, after all.

As for the overall concept, I’m hoping to build a GUI system modeled on a modular synthesizer and finding it quite difficult because of SC’s hierarchical organization. I know that other people have successfully done this, so I know it must be my own limitations as a programmer.

My initial approach was to use NodeProxy, but let’s say you have a branch built like this:

a = NodeProxy(s, \audio).play;
a.source= {SinOsc.ar(LFNoise0.ar(1).range(100, 500), 0, 0.1)};
a[1] = \filter-> {|sig| Decimator.ar(sig, 920, 9)};
a[2] = \filter-> {|sig| Ringz.ar(sig, 920, 1)};

As far as I know, there’s no way to take an extra line from a[1] and bring it to a new NodeProxy.

Also, with the following example… it seems that I would need a different protocol to use a[0] as the modulator. There are some hard distinctions made between “control” and “audio” rate signals that seem to be a continuing problem for me… and the NamedControls for the filters used at a[1] and a[2]would also need to have individual naming conventions.

(
~filt = { |sig|
	Ringz.ar(sig, (\cutoff.kr(1)*1000)+100, 0.9, 0.5);};
~a = Ndef(\node, {Silent.ar(2)});
~a[0]={ LFSaw.ar(200, 0, 0.05) };
~a[1] = \filter -> ~filt;
//this instance of filter would have the same named control: 
//~a[2] = \filter -> ~filt;   
Ndef(\node) <<> .cutoff Ndef(\sinewave);
~x = Ndef(\sinewave, { SinOsc.ar(0.3).abs });
~a.play;
)

Having said all of this, maybe some inaccuracy in regards to phase or envelopes would not be the worst thing in the world - if I could pull up general patching that worked along these lines.
So, knowing all that, maybe it will be easier to guide me towards a solution or maybe the above still holds to be the best route forward?

1 Like

One way to avoid worrying about differences between control rate and audio rate is to simply use audio rate everywhere. This is more reflective of what you’d get in a modular system anyway - it’s less efficient, but this won’t matter unless your system gets very large, at which you can worry about optimizing :slight_smile:

Your last chunk of code rewritten with audio rate all around…

(
~filt = { 
	|sig|
	
	Ringz.ar(
		sig, 
		\cutoff.ar(500), 
		\decay.ar(0.5), 
		mul: \amp.ar(1)
	);
};

~a = Ndef(\node, {Silent.ar(2)});

~a[0]={ LFSaw.ar(200, 0, 0.1) };
~a[1] = \filter -> ~filt;
~a[2] = \filter -> ~filt;

Ndef(\node).set(\ratio, 10);
Ndef(\node) <<> .pdisper Ndef(\sinewave);
Ndef(\node) <<> .ratio Ndef(\sinewave);
Ndef(\node) <<> .amp Ndef(\sinewave);
~x = Ndef(\sinewave, { SinOsc.ar(0.3).abs * 4 });

~a.play;
)

Indeed. The linearity of Ndef is a problem if you want to express (sig1 * env1) + (sig2 * env2). It’s not possible to have that as single Ndef (and still have all of sigs and envs be individually “hot-swappabe”. But Ndefs can refer to each other.

So you can can write something like (simplifying a bit the)

Ndef(\chain1)[0] = sig1;
Ndef(\chain1)[1] = \filter -> env1; // pseudocode

Ndef(\chain2)[0] = sig2;
Ndef(\chain2)[1] = \filter -> env2; // pseudocode

Ndef(\bigmix) = Ndef(\chain1) + Ndef(\chain2)

That does work because of a not so advertised feature that “Ndef math” mostly works.

You can even write

Ndef(\bigmix) = { (\wet1.kr * Ndef(\chain1).ar) + (\wet2.kr * Ndef(\chain2).ar) }

to give yourself mixer level controls for each. The simpler “Ndef math” version right above just does that as if you had written { Ndef(\chain1).ar + Ndef(\chain2).ar }.

But \bigmix will not have all the controls that \chain1 and \chain2 have in a single convenient location, because it’s a different Ndef on a different Group. Basically Ndef’s can’t really wrap one another as plain Groups can. Ndefs can only refer to another Ndefs.

You can just wrap all 3 Ndef groups \bigmix, \chain1 and \chain2 in a single Group using the rather obscure parentGroup feature of Ndef, so can set parameters in one place form the command line, but that Group wont be a Ndef itself, so you’ll have this unpleasant asymmetry in interfaces, e.g. it won’t be representable as a NdefGui.

What you really need to consider in cases like this is whether you really need the ability to swap the filters on each chain at runtime, while the “base” signal is still running. Because if you don’t need that much flexibility, and e.g. you can restart the 'base" signal when the filter changes, then it’s much simpler to write a single Ndef (or SynthDef) for each chain so that it includes both the “base” signal and the filter. Basically, the difference between:

Ndef(\combo1) { sig1 * env1}

and

Ndef(\chailn)[0] = { sig1 }
Ndef(\chailn)[1] = \filter -> env1 // pseudocode

is that in the latter case you can later do just

Ndef(\chailn)[1] = \filter -> something else // pseudocode

and the phase of sig1 will stay exactly the same during the change, so no “drop outs” etc. In the case of Ndef(\combo1) changes you’d probably have to do at least a cross fade for things to sound acceptable, and the xfade will sound different than merely changing filter.

Yeah, it really depends. Sometimes it does matter, but mostly in cases where you need to keep phase alignment.

This is yet another issue. If you want to automagically rename the controls used in Ndef role slots to have them suffixed with the slot number, you can use my #2 solution from here. Pasting it here for convenience. You probably want to comment out the debugging postlns, but they are informative on a first run to see what it does.

(~massPostmaglerInstaller = { arg newRolesSuffix = "M"; // so \mixM etc.
	var targetRoles = #[\mix, \filter, \filterIn];
	var defaultSkipNames = #["out", "i_out", "gate", "fadeTime"];
	var specificSkipNames = (mix: ["mix"], filter: ["wet"], filterIn: ["wet"]);
	var postmangler = { arg name, index, role;
		var skipNames = defaultSkipNames ++ (specificSkipNames[role] +++ index);
		name = name.asString;
		//skipNames.postln;
		if(skipNames.indexOfEqual(name).isNil) {
			name = name.asString ++ index;
			("Renamed::" + name).postln;
		} {
			("Skipped::" + name).postln;
		};
		name.asSymbol
	};
	var wrapperGen = { arg roleName, roleBuildFunc;  // curried targets
		{ arg func, proxy, channelOffset = 0, index;
			var psd = roleBuildFunc.value(func, proxy, channelOffset, index);
			psd.allControlNames.do { arg cno;
				cno.name = postmangler.value(cno.name, index, roleName) };
			psd.allControlNames.do(_.postln);
			psd
		}
	};
	targetRoles.collect { arg roleName;
		var origBuildFunc = AbstractPlayControl.buildMethods[roleName];
		var newBuildFunc = wrapperGen.value(roleName, origBuildFunc);
		var newRoleName = (roleName.asString ++ newRolesSuffix).asSymbol;
		AbstractPlayControl.buildMethods.put(newRoleName, newBuildFunc);
		(newRoleName.asString + "installed").postln;
		[newRoleName, newBuildFunc]  // ret val somewhat irrelevant
	}
})
// actually call it
~massPostmaglerInstaller.()

The default Ndef(\x)[num] = {/* blah */} is basically almost the same as Ndef(\x)[num] = \mix -> {/* blah */}, except the later add a control for the mix value, but it defaults to one. So there’s not much need to have a role that enhances the mere = “direct” slot assignment; just use \mixM produced by the above wrapper gen. Which you can of course adapt for your needs to maybe number things differently e.g. based on a global Bag count for each name per Ndef rather than slot index suffix.

1 Like

Thanks, Avid and Scztt - I am trying to walk through the information provided, but I’m having a hard time understanding everything. I’m not sure if it’s outside of the purview of this forum. The concept seems simple, but getting it to work within SuperCollider seems quite complex.

So, I am trying to walk through these ideas with code instead of pseudocode.

Now, let’s say a hypothetical patch would look like this:
LFTri → Ringz → SinOsc1.
With a separate branch from the same Ringz:
→ Decimator.

Maybe not the most glamorous-sounding example, but a pretty basic concept in modular synthesis, easy to draw in another fashion, as so (hard-panning included for clarity):


{   var sig1, sig2, sig3;
	sig1 = Ringz.ar(LFSaw.ar(0.92, 0, 0.1), 10, 1, 0.2);
	sig2 = SinOsc.ar(sig1.range(0, 1000));
	sig3 = Decimator.ar(sig1, 4000, 2, 0.8);
	 [sig3, sig2];
}.play;

Now, if I decide “LFTri” is a source, I can create any number of separate Ndef chains that use filters/processing/effects on the source. Is it perhaps best to break off into a new Ndef every time I add a new filter? It seems like it would be demanding on the CPU, but maybe if it plays nicely with this NodeSnapshot idea, it would be the most clean way to do this.

Ndef(\sourceSound)[0] = {LFSaw.ar(200, 0, 0.15)};
Ndef(\chain1)[0] = {Ringz.ar(Ndef(\sourceSound), \cutoff.ar(100), 0.1, 0.1)};
Ndef(\chain2)[0] = {SinOsc.ar(Ndef(\chain1)*1000, 0, 0.2)};
Ndef(\chain3)[0] = {Decimator.ar(Ndef(\chain1), 1000, 4)};
Ndef(\chain1) <<> .cutoff Ndef(\sine);
~x = Ndef(\sine, { SinOsc.ar(0.3).abs * 400 + 100 });

Ndef(\chain2).play;
Ndef(\chain3).play;

I think the only complicated thing with this part would be figuring out which things are “audible” and which things are being sent into larger connections. Maybe this is where the wet/dry concept you’re talking about comes into play?

In this particular framework, NodeSnapshot would work well for creating save states?

I’m still a little unclear on how to use the massPostmaglerInstaller - is there any way I could see it in an example?

Thank you - and please do let me know if this is appropriate for the forum. I don’t want to monopolize anyone’s time.

Let’s say I want to add multiples of these to the same Ndef, which is what you were trying to do above:

~filt = { |sig| Ringz.ar(sig, (\cutoff.kr(1)*1000)+100, 0.9, 0.5);};

~a = Ndef(\node) { LFSaw.ar(200, 0, 0.05) }; // sets ~a[0]
~a[1] = \filter -> ~filt;
//this instance of filter would have the same named control: 
~a[2] = \filter -> ~filt

Indeed:

~a.controlNames do: _.postln
/*
ControlName  P 1 wet1 control 1.0
ControlName  P 4 cutoff control 1
ControlName  P 1 wet2 control 1.0
*/

But using my new filter roles you do get different control names for stuff generated from the same function, for every control in your function (not just the auto-generated wets):

~massPostmaglerInstaller.()

// Note the 'M' added to the role name
~a[1] = \filterM -> ~filt;
~a[2] = \filterM -> ~filt

~a.controlNames do: _.postln
/*
ControlName  P 1 wet1 control 1.0
ControlName  P 4 cutoff1 control 1
ControlName  P 1 wet2 control 1.0
ControlName  P 4 cutoff2 control 1
*/
2 Likes

@scztt cztt -
I’m still having a little trouble getting this stuff installed.
Ran:

Quarks.install("https://github.com/scztt/IconSet.quark");

Material.fetch;

Now getting:

ERROR: Couldn't find icon with name 'ic_check_box' in paths: /Users/polvo/Library/Application Support/SuperCollider/downloaded-quarks/material-design-icons/android/svg/production/ic_check_box_24px.svg, /Users/polvo/Library/Application Support/SuperCollider/downloaded-quarks/material-design-icons/font/svg/production/ic_check_box_24px.svg, /Users/polvo/Library/Application Support/SuperCollider/downloaded-quarks/material-design-icons/ios/svg/production/ic_check_box_24px.svg, /Users/polvo/Library/Application Support/SuperCollider/downloaded-quarks/material-design-icons/png/svg/production/ic_check_box_24px.svg, /Users/polvo/Library/Application Support/SuperCollider/downloaded-quarks/material-design-icons/src/svg/production/ic_check_box_24px.svg, /Users/polvo/Library/Application Support/SuperCollider/downloaded-quarks/material-design-icons/update/svg/production/ic_check_box_24px.svg, /Users/polvo/Library/Application Support/SuperCollider/downloaded-quarks/material-design-icons/android/svg/production/ic_check_box_26x24px.svg, /Users/polvo/Library/Application Support/SuperCollider/downloaded-quarks/material-design-icons/font/svg/production/ic_check_box_26x24px.svg, /Users/polvo/Library/Application Support/SuperCollider/downloaded-quarks/material-design-icons/ios/svg/production/ic_check_box_26x24px.svg, /Users/polvo/Library/Application Support/SuperCollider/downloaded-quarks/material-design-icons/png/svg/production/ic_check_box_26x24px.svg, /Users/polvo/Library/Application Support/SuperCollider/downloaded-quarks/material-design-icons/src/svg/production/ic_check_box_26x24px.svg, /Users/polvo/Library/Application Support/SuperCollider/downloaded-quarks/material-design-icons/update/svg/production/ic_check_box_26x24px.svg, /Users/polvo/Library/Application Support/SuperCollider/downloaded-quarks/material-design-icons/android/svg/production/ic_check_box_48px.svg, /Users/polvo/Library/Application Support/SuperCollider/downloaded-quarks/material-design-icons/font/svg/production/ic_check_box_48px.svg, /Users/polvo/Library/Application Support/SuperCollider/downloaded-quarks/material-design-icons/ios/svg/production/ic_check_box_48px.svg, /Users/polvo/Library/Application Support/SuperCollider/downloaded-quarks/material-design-icons/png/svg/production/ic_check_box_48px.svg, /Users/polvo/Library/Application Support/SuperCollider/downloaded-quarks/material-design-icons/src/svg/production/ic_check_box_48px.svg, /Users/polvo/Library/Application Support/SuperCollider/downloaded-quarks/material-design-icons/update/svg/production/ic_check_box_48px.svg

One thing I’m recalling is that some people have suggested using some sort of registry to store code and then interpreting that material, rather than converting to an archive format.

Has anyone done this with Ndefs?

Is there a general purpose tutorial somewhere about saving dynamic code? I think there a lot of people trying to solve similar problems, but I’m not sure I’ve seen anything from Eli Fieldsteel or Alikthename on this subject…