Instantiating and controlling Synths with GUI - best practices?

Hi! I want to build a GUI that will allow to instantiate and remove synths (from one single synthdef) and to control all paramterers of each synth independently. I am imagining EZgui boxes for each synth that stack on top of each other. What would be the easiest and cleanest way to implement this?

Thanks!

hi. maybe TabbedView / TabbedView2? each separate tab can represent a synth instance with its parameters

I think EZ classes are still somewhat broken (can’t be used with eg. HLayout), UNLESS it was fixed in the latest updates, I am still on 3.13

Thanks! Right now I can’t figure out how to make the gui control the instances of the synth that are live in the moment. For example, if instance 2 disappears, how do I handle instance 3? How do I link the controls to the parameters, and change the controls when synth parameters are changed from elsewhere?

Hi,

I cannot say what’s best practice but you could have a look at VarGui from miSCellaneous_lib. BTW, it uses EZ classes and works here on OSX / SC 3.14.

It doesn’t allow the dynamical adding of synths but you can, e.g., generate individual GUI controls for n synths of the default synthdef like that:

n = 10;
(\default ! n).sVarGui.gui;

The tutorial “VarGui shortcut builds” might be a better starting point than the VarGui class help.

That would need a dedicated MVC (model-view-controller) design and is more demanding to implement. See SimpleController help for more info. VarGui is not MVC but that has never been a practical restriction for me.

Here is a quick and dirty way which scales nicely. Not a full MVC model, but the most important bits of it. This method needs to be expanded if you want to be able to close and open your ui without loosing values. The code uses two basic tricks

Events can have setters:

(
p = (); // empty event
p.add(\amp_ -> {|self, val|
	self = val;
	('amp set to ' ++ val).postln
});
p.amp = 5; // -> 5. watch post window
)

// you are not actually setting the amp directly with p.amp = 5

(
// we can do anything inside the setter function
p = (); // empty event
p.add(\amp_ -> {|self, val|
	val = val * 300;
	self = val;
	('amp set to ' ++ val).postln
});
p.amp = 5; // -> 1500
)

// notice the important difference betwen p.amp = 10 and p[\amp] = 10

p[\amp] = 10 
p.amp -> 10 // sets value but does not call function so now assigning is normal
p.amp = 10 // now function is called and val is multiplied by 300 -> 1500

Knobs, sliders, buttons etc all store values in the range [0, 1]. We can map and unmap values back and forth between ui elements and real values:

~spec = [20, 100, \lin].asSpec; // simple default linear mapping, range[20, 100]
// map some value to ui range [0, 1]
~spec.unmap(30); //-> 0.125
// if value is 0.125 in ui:
~spec.map(0.125) // -> ui val maps back to real range: 30

~spec2 = [20, 20000, \exp].asSpec; // for freq
~spec2.unmap(730) // -> 0.52076428815216
~spec2.map(0.52076428815216) // -> 730

~spec3 = [0, 1, \lin, 1].asSpec // one step = either 0 or 1
~spec3.unmap(0.2) // we feed it a float, rounds to 0
~spec3.unmap(0.7) // rounds to 1

Here is a full example, change to your chosen synth arg names and implement the synth call where stated. This structure scales well, easy to add parameters and controls.

(
// match names to your actual controls
var seq = [\lpCut, \amp, \state];
var defaults = (
	lpCut: 2000,
	amp: 0.2,
	state: 0
);

var spec = (
	lpCut: [60, 10000, \exp].asSpec, // exp mapping for freq
	amp: [0, 1].asSpec, // amp linear mapping [0, 1]
	state: [0, 1, \lin, 1].asSpec // 1 step = val is either 0 or 1
	
);
var ui = (
	lpCut: Knob(),
	amp: Knob(),
	state: Button().states_([[\Off], [\On]]);
);
var vals = ();
var view = View().layout_(HLayout(*seq.collect{|key|
	VLayout(StaticText().string_(key.toUpper).align_(\center), ui[key])
})).front.alwaysOnTop_(true);


seq.do{|key|
	vals.add((key ++ \_).asSymbol -> { |self, val|
		self = val;
		{ ui[key].value = spec[key].unmap(val) }.defer; // unmaps the real val to the range [0, 1]
		(key ++ ' set to: ' ++ val).asSymbol.postln;
		// ADD SYNTH CALL HERE, eg. mySynth.set(key, val)
	});
	vals.perform(key.asSetter, defaults[key])
};

seq.do{|key|
	ui[key].action = {|n|
		var val = spec[key].map(n.());
		vals.perform(key.asSetter, val);
	};
};
a = vals // assign to oneletter variable for ease
)

/// now setting from ui updates vals and set synth
/// Also the other way around, when set by code ui updates:

a.amp = 0.67
a.lpCut = 600
a.state = 1
a.state = 0
2 Likes

Did you try this approach @Ivan or did you find another way?

Hi Ivan,

don’t know if I should explicitly recommend it but I have written a library named CVCenter - already more than 10 years ago:

It won’t let you instantiate any synths from GUI but you should be able to automatically create GUI controls (knobs, 2D sliders, multisliders) for the controls of a running synth. Moreover you can create (= generate) GUI controls for Ndef, NodeProxie or ProxySpace as well as it can be used in Patterns (CVCenter is built around CV, a class from Ronald Kuivila’s Conductor library. CV inherits from Stream, so, a CV instance which CVCenter wraps in a CVWidget can be placed in a Pattern just like another Pattern). Any CVWidget can be connected to, resp. listen to MIDI or OSC. A set of CVWidgets within CVCenter can also be saved to disk including MIDI and OSC connections.

Here’s some overview: CVCenter | SuperCollider 3.14.0-dev Help

CVCenter can be installed as a quark (if you can use the quarks mechanism on your machine):

Quarks.install("https://github.com/nuss/CVCenter.git")

Also you will need to install CV (contained in a stripped down clone of the Conductor library, named CVmin):

Quarks.install("https://github.com/nuss/CVmin.git")

And you will need TabbedView2 and (optionally, I think) wslib (both can be installed from within scIde):

Quarks.install("https://github.com/supercollider-quarks/wslib");
Quarks.install("https://github.com/jmuxfeldt/TabbedView2_QT");

… maybe give it a try. I am still using it in my performance practice. Maybe not exactly what you’re looking for. Let me know if come across problems. I am in fact working on a complete re-write but that’s still far from usable.

Good luck, Stefan

2 Likes

@Thor_Madsen
Do you mean the issue reported in Update EZGui classes to work seamlessly with layouts · Issue #1685 · supercollider/supercollider · GitHub on GitHub?

Each code snippet in the opening example of the issue produces the following error:

ERROR: Message 'asView' not understood.
RECEIVER:
Instance of EZSlider {    (0x11374b228, gc=10, fmt=00, flg=00, set=05)
  instance variables [21]
    labelView : instance of StaticText (0x1410fd388, size=37, set=6)
    widget : nil
    view : instance of CompositeView (0x171289ce8, size=34, set=6)
    gap : instance of Point (0x114598148, size=2, set=2)
    popUp : false
    innerBounds : instance of Rect (0x1137e1118, size=4, set=2)
    action : nil
    layout : Symbol 'horz'
    value : Float 0.500000   00000000 3FE00000
    labelSize : instance of Point (0x113e152e8, size=2, set=2)
    alwaysOnTop : false
    margin : instance of Point (0x1416ea5c8, size=2, set=2)
    scaler : Integer 1
    sliderView : instance of Slider (0x1410eef88, size=39, set=6)
    numberView : instance of NumberBox (0x155a418e8, size=46, set=6)
    unitView : nil
    controlSpec : instance of ControlSpec (0x36807d708, size=9, set=4)
    numSize : instance of Point (0x1716d5298, size=2, set=2)
    numberWidth : Integer 45
    unitWidth : Integer 0
    round : Float 0.001000   D2F1A9FC 3F50624D
}
ARGS:
KEYWORD ARGUMENTS:
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 = <instance of EZSlider>
		arg selector = 'asView'
		arg args = [*0]
		arg kwargs = [*0]
	Meta_LineLayout:parse
		arg this = <instance of Meta_VLayout>
		arg in = <instance of EZSlider>
		var out = [*3]
		var key = nil
		var i = nil
	< FunctionDef in Method Collection:collectAs >
		arg elem = <instance of EZSlider>
		arg i = 0
	ArrayedCollection:do
		arg this = [*5]
		arg function = <instance of Function>
		var i = 0
	Collection:collectAs
		arg this = [*5]
		arg function = <instance of Function>
		arg class = <instance of Meta_Array>
		var res = [*0]
	Meta_LineLayout:new
		arg this = <instance of Meta_VLayout>
		arg items = [*5]
		var serializedItems = nil
	< closed FunctionDef >  (no arguments or variables)
	Interpreter:interpretPrintCmdLine
		arg this = <instance of Interpreter>
		var res = nil
		var func = <instance of Function>
		var code = "// EZSlider \horz
w = Window..."
		var doc = nil
		var ideClass = <instance of Meta_ScIDE>
	Process:interpretPrintCmdLine
		arg this = <instance of Main>

^^ ERROR: Message 'asView' not understood.
RECEIVER: an EZSlider

Messages with a similar name understood by the receiver:
	isView
	view

Many other objects respond to the message 'asView' (found 13 superclasses).

No I wasn’t referring to the bug, just asking if OP had tried what I suggested.

Thank you really much! This clarified things a lot.

Sorry for late answer, didn’t have time to test it properly until now.

I wonder how this approach compares to using SimpleController?

Also wonder how you implement midi cc control in the most elegant way? And how does your project structure look like? Do you implement this with classes?

Sincere thanks,
Ivan