Protyping Object for creating a NodeProxy GUI with Sliders and RangeSliders

hey, i have been trying to prototype an object which takes a SynthDef, prepares a NodeProxy using .prime and you specify which parameters should be controlled by Sliders and which ones by RangeSliders. The basic functionality is working already.

(
SynthDef(\test, {

	var freqRange = \freqRange.kr([2500, 3500], spec: ControlSpec(100, 8000));
	var ringRange = \ringRange.kr([0.1, 0.15], spec: ControlSpec(0.1, 2.0));
	var ampRange = \ampRange.kr([0.1, 0.2], spec: ControlSpec(0.1, 1.0));

	var sig = Splay.ar(
		Array.fill(3, {
			Ringz.ar(
				Dust.ar(\dens.kr(5, spec: ControlSpec(1, 10))),
				exprand(freqRange[0], freqRange[1]),
				exprand(ringRange[0], ringRange[1]),
				exprand(ampRange[0], ampRange[1])
			)
		})
	).distort;

	sig = sig * \amp.kr(0.5, spec: ControlSpec(0, 1));

	Out.ar(\out.kr(0), sig);
}).add;
)

(
~makeNodeProxy = { |synthDefName, singleParams, rangeParams, initArgs =#[], numChannels = 2|

	Environment.make{ |self|

		// NodeProxy Definitions
		~synthDef = SynthDescLib.global[synthDefName].def ?? {
			Error("SynthDef '%' not found".format(synthDefName)).throw
		};

		~nodeProxy = NodeProxy.audio(s, numChannels);
		~nodeProxy.prime(~synthDef);//.set(*initArgs);

		// GUI Definitions
		~rangeParams = IdentityDictionary.new();
		~singleParams = IdentityDictionary.new();

		~singleParamViews = IdentityDictionary.new();
		~rangeParamViews = IdentityDictionary.new();

		// GUI methods

		// TO DO: set up dependencies
		~setUpDependencies = { |self|

		};

		// TO DO: add nodeProxyChanged
		~nodeProxyChanged = { |self, what, args|

		};

		// TO DO: add parameterChanged
		~parameterChanged = { |self, key, val|

		};

		~makeRangeParamSection = { |self|

			//self.rangeParams.do{ |spec| spec.removeDependant(specChangedFunc) };
			self.rangeParams.clear;

			self.nodeProxy
			.controlKeysValues(except: self.nodeProxy.internalKeys ++ singleParams)
			.pairsDo{ |key, val|
				var spec;

				spec = (self.nodeProxy.specs.at(key) ?? { Spec.specs.at(key) }).asSpec;

				//"Spec for paramname %: %".format(key, spec).postln;
				//spec.addDependant(specChangedFunc); ????
				self.rangeParams.put(key, spec);
			};
		};

		~makeSingleParamSection = { |self|

			//self.singleParams.do{ |spec| spec.removeDependant(specChangedFunc) };
			self.singleParams.clear;

			self.nodeProxy
			.controlKeysValues(except: self.nodeProxy.internalKeys ++ rangeParams)
			.pairsDo{ |key, val|
				var spec;

				spec = (self.nodeProxy.specs.at(key) ?? { Spec.specs.at(key) }).asSpec;

				//"Spec for paramname %: %".format(key, spec).postln;
				//spec.addDependant(specChangedFunc); ????
				self.singleParams.put(key, spec);
			};
		};

		~makeSingleParamViews = { |self|
			var sliderView;

			sliderView = View.new().layout_(VLayout.new());

			self.makeSingleParamSection.();

			self.singleParams.sortedKeysValuesDo{ |key, spec|
				var layout, paramVal;
				var slider, valueBox;

				layout = HLayout.new(
					[StaticText.new().string_(key), s: 1],
				);

				paramVal = self.nodeProxy.get(key);

				slider = Slider.new()
				.orientation_(\horizontal)
				.value_(spec.unmap(paramVal))
				.action_({ |obj|
					var val = spec.map(obj.value);
					valueBox.value = val;
					self.nodeProxy.set(key, val);
				});

				valueBox = NumberBox.new()
				.action_({ |obj|
					var val = spec.constrain(obj.value);
					slider.value_(spec.unmap(val));
					self.nodeProxy.set(key, val);
				})
				.decimals_(3)
				.value_(spec.constrain(paramVal));

				//used to be able to fetch the sliders later when they need to be updated
				//singleParamViews.put(key, (type: \number, slider: slider, numBox: valueBox));

				layout.add(valueBox, 1);
				layout.add(slider, 4);

				sliderView.layout.add(layout);
			};

			sliderView;

		};

		~makeRangeParamViews = { |self|
			var rangeSliderView;

			rangeSliderView = View.new().layout_(VLayout.new());

			self.makeRangeParamSection.();

			self.rangeParams.sortedKeysValuesDo{ |key, spec|
				var layout, paramLoVal, paramHiVal;
				var rangeSlider, lowValueBox, highValueBox;

				layout = HLayout.new(
					[StaticText.new().string_(key), s: 1],
				);

				paramLoVal = self.nodeProxy.get(key)[0];
				paramHiVal = self.nodeProxy.get(key)[1];

				rangeSlider = RangeSlider.new()
				.orientation_(\horizontal)
				.lo_(spec.unmap(paramLoVal))
				.hi_(spec.unmap(paramHiVal))
				.action_{ |obj|
					var loVal = spec.map(obj.lo);
					var hiVal = spec.map(obj.hi);
					lowValueBox.value = loVal;
					highValueBox.value = hiVal;
					self.nodeProxy.set(key, [loVal, hiVal]);
				};

				lowValueBox = NumberBox.new()
				.action_({ |obj|
					var val = spec.constrain(obj.value);
					rangeSlider.lo_(spec.unmap(val));
					self.nodeProxy.set(key, val);
				})
				.decimals_(3)
				.value_(spec.constrain(paramLoVal));

				highValueBox = NumberBox.new()
				.action_({ |obj|
					var val = spec.constrain(obj.value);
					rangeSlider.hi_(spec.unmap(val));
					self.nodeProxy.set(key, val);
				})
				.decimals_(3)
				.value_(spec.constrain(paramHiVal));

				//used to be able to fetch the sliders later when they need to be updated
				//rangeParamViews.put(key, (type: \number, slider: slider, numBox: valueBox));

				layout.add(lowValueBox, 1);
				layout.add(rangeSlider, 4);
				layout.add(highValueBox, 1);

				rangeSliderView.layout.add(layout);

			};

			rangeSliderView;

		};

		~makeGui = { |self|
			var window, sliderView, rangeSliderView;

			window = Window(self.synthDef.name, Rect(10, 500, 440, 320)).front;
			window.layout = VLayout.new();

			sliderView = self.makeSingleParamViews.();
			rangeSliderView = self.makeRangeParamViews.();

			window.layout.add(sliderView, 1);
			window.layout.add(rangeSliderView, 1);

			window;
		};

	}.know_(true);

};

x = ~makeNodeProxy.(\test, [\dens, \amp], [\freqRange, \ringRange, \ampRange]);
x.makeGui;
)

x.nodeProxy.play;

i have been building it looking into the source code of NodeProxyGui2, which is awesome but Im missing two functionalities i need:

  • not every parameter should end up in the GUI, there are some i would like to control by using .set
  • i would like to have RangeSliders for some of the parameters

this results in a slimmer GUI then one filled with parameters i dont want there or parameters which need two Sliders for low and high value instead of one RangeSlider.

The first question is: other then specifying an array NamedControl in the SynthDef and using [0] and [1] to map them to low and high values, would it also be possible to specify two NamedControls one for \low.kr and one for \high.kr and their specs and map them to the RangeSlider? I have some code build on top of that which has a problem with using arrays which is not easily fixed i think.

The second question is: i have doubled alot of code ~makeRangeParamSection and ~makeSingleParamViews are nearly identical with ~makeRangeParamSection and ~makeRangeParamViews is there a better way to go about that without retyping most of the stuff?

The third question is: in ~makeRangeParamSection and ~makeSingleParamSection im using .controlKeysValues(except: self.nodeProxy.internalKeys ++ singleParams or rangeParams) to filter out the params which should either end up as Sliders or RangeSliders. Is this a good way of doing things? Or should i better search for specific key names?

There is some functionality in NodeProxyGUi2 for example using .set on the Nodeproxy and the Sliders will update automatically which i wasnt able to figure out yet.
But i already learned alot looking into the code and writing some on my own :slight_smile:

hey, thanks for sharing your approach :slight_smile: Im not sure right now how the code you have shared is helping me out with one of my questions though.

Sorry about this. To be honest, I didn’t fully understand your questions. If they are related to NodeProxyGui2 specifically, I don’t use it, can’t help you much.

It was mostly about the first question, where starting from a IdentityDictionary, I can get enough flexibility to avoid being constrained by ready made implementations, but I’m recreating every view myself instead of having a Class doing it for me. This was also my answer to the problem that every parameter should not be displayed, and also might or might not be displayed depending on certain conditions.

This is also how I dealt with the third question, where I didn’t want to chose between two widget, but to be able to provide both depending on my needs.

In your implementation, you pass single params in one dict, and rangeParams in an other one.
In mine, I pass every param in a single dict, but each one has a key saying how it behaves. For example, if the param is \continuous, it will be displayed as a slider by default. But, if the user chose so, he can switch to multiple buttons instead, referencing scale frequencies for example.

So I don’t think this helps you at all and I’m sorry about this.

Concerning your second question, you can avoid retyping things by creating a function which you call in both function for everything they need that is exactly the same. Since it’s only internal to your class, not accessible from the outside, it’s a private method.

hey thanks alot :slight_smile: , the questions havent been directly about NodeProxyGui2, i was just looking into the source code to get some inspiration for my needs.
I think a good starting point would then be to collect the params and rangeParams in a single dictionary.
I will try to write a function for beeing used for paramSection / paramViews and rangeParamSection / rangeParamViews. lets see.