A convenient class to use MLP regressor

Hello community,

I’d like to share this class with you.
If you want to try it, copy and paste the code in an .sc file in your extension folder and recompile the class library.
It uses Flucoma FluidMLPRegressor (so you have to install Flucoma first).
Basically you pass a Synth or Ndef instance to it and it automatically map the parameters so you can control them all with a Slider2D (xy).
I realize this probably isn’t perfect, but if it helps even a little, I’ll be happy.
If you have any tips on how to improve this, I’d love to hear from you.

RegAutoMapSrvLL {
	var <server,
	<synth, // Synth or Ndef instance
	<paramScale, // IdentityDictionary of \paramName = [minVal, maxVal, curve] ex: IdentityDictionary.newFrom([\freq, [20, 20000, \exp], \pan, [-1, 1], \level, [0, 10]])
	<paramExclude, // Array of parameter name to exclude
	<defaultName, // String name of the Window and name of the default loaded files
	<defaultPath, // String ex: "/home/fabien/Bureau/"
	<xCCNum, // control change Number
	<yCCNum,
	<midiChan, // midi channel Number
	<group,
	<bgColor, // Color of background
	<window,
	<regScaler, // Synth(\regScaler1001)
	<paramArr, // Array of parameter name without the excluding ones
	<outputNumber, // Number of output
	<controlBus, // get a multi channel Bus
	<subBusDict, // IdentityDictionary of \parameter = Bus
	<xydata,
	<paramsdata,
	<xybuf,
	<paramsbuf,
	<mlp,
	<midiFunc,
	<>active, // Boolean for soft take over on xy midi mapping
	<xyModel, // IdentityDictionary of \x = value and \y = value
	<viewElement, // IdentityDictionary of viewElement[\controlName] = Button (for example)
	<live, // Boolean liveMode state (when live == true you save on defaultPath ++ defaultName file)
	controller,
	oscFunc,
	currentValues;

	// Class Method

	*new { | server, synth, paramScale, paramExclude, defaultName, defaultPath, xCCNum, yCCNum, midiChan, group, bgColor |
		if (server.isKindOf(Server).not, {
			^warn("Server required");
		});

		if (server.serverRunning.not, {
			^warn("Server is not running");
		});

		if ((synth.isKindOf(Synth).not and: synth.isKindOf(Ndef).not), {
			^warn("Synth or Ndef required");
		}, {
			if ((synth.isKindOf(Ndef) and: synth.source.isNil), {
				^warn("Ndef function isNil");
			});
		});

		paramScale = paramScale ? IdentityDictionary.new;
		paramExclude = paramExclude ? Array.newClear;

		paramExclude = paramExclude.collect({ arg paramName;
			paramName.asString.toLower;
		});

		paramExclude = paramExclude ++ ["in", "out", "input", "output", "inbus", "outbus", "bus", "doneaction", "trigger", "trig", "gate", "t_trig", "t_gate", "loop", "mute", "on",  "off"];

		defaultPath = defaultPath ? (Platform.userHomeDir +/+ "Bureau/");
		defaultPath = defaultPath.asString;
		bgColor = bgColor ? Color.white;

		^super.newCopyArgs(server, synth, paramScale, paramExclude, defaultName, defaultPath, xCCNum, yCCNum, midiChan, group, bgColor).prInit;
	}

	// Instance Method

	remove {
		if (window.isClosed.not, { window.close });
	}

	getPreset {
		var preset, argString;
		argString = "";

		paramArr.do({ arg param, i;
			argString = argString ++ "\\" ++ param.asString ++ ", " ++ subBusDict[param].getSynchronous.asString;
			if ((i + 1) < paramArr.size, {
				argString = argString ++ ", ";
			});
		});

		if (synth.isKindOf(Ndef), {
			preset = "Ndef(\\" ++ synth.key.asSymbol ++ ").xset(%);".format(argString);
		}, {
			if (synth.isKindOf(Synth), {
				preset = "Synth(\\" ++ synth.defName.asSymbol ++ ").set(%);".format(argString);
			});
		});

		^(Post << preset);
	}

	getBus { | paramName |
		^subBusDict[paramName.asSymbol];
	}

	mapBus { | paramName |
		^subBusDict[paramName.asSymbol].asMap;
	}

	reMap {
		{
			this.prMapSynth;

			server.sync;

			{ viewElement[\multiSlider].valueAction_(currentValues.asArray); }.defer;
		}.fork;
	}

	liveMode { | active = true |
		var enable;
		enable = active.not;
		{
			viewElement[\loadData].enabled = enable;
			viewElement[\loadMLP].enabled = enable;
		}.defer;
		live = active;
		^live;
	}

	autoTrainMLP { | cyclesNumber = 1 |
		{
			cyclesNumber.asInteger.do({ arg i;
				mlp.fit(xydata, paramsdata, { arg loss;
					if ((i + 1) == cyclesNumber, {
						{ viewElement[\accuracy].string_(loss.round(0.00000001)); }.defer;
					});
				});
				0.05.wait;
			});
		}.fork;
	}

	xyMidiMap { | xCCNum, yCCNum, midiChan |
		var x, y;
		x = 0;
		y = 0;

		midiFunc !? ( _.free; );

		midiFunc = MIDIFunc.cc({ arg val, num, chan, src;
			var scaleValue, threshold, guiX, guiY;
			scaleValue = val.linlin(0, 127, 0, 1);
			if (num == xCCNum, {
				x = scaleValue;
			});

			if (num == yCCNum, {
				y = scaleValue;
			});

			threshold = 0.05;
			{
				guiX = viewElement[\xySlider].x;
				guiY = viewElement[\xySlider].y;

				if ( // Soft Takeover
					(active or: (((x > (guiX - threshold)) and: (x < (guiX + threshold))) and: ((y > (guiY - threshold)) and: (y < (guiY + threshold))))),
					{
						xyModel[\x] = x;
						xyModel[\y] = y;
						active = true;
						xyModel.changed(\update);
					}
				);
			}.defer;
		}, [xCCNum, yCCNum], midiChan);

		format("% regressor x cc number: %, midi channel: %\n", defaultName, xCCNum, midiChan).post;
		format("% regressor y cc number: %, midi channel: %\n", defaultName, yCCNum, midiChan).post;

		^midiFunc;
	}

	setSynth { // for SynthDef
		var argValDict;
		argValDict = IdentityDictionary.new;

		paramArr.do({ arg param;
			argValDict[param] = subBusDict[param].getSynchronous;
		});

		if (synth.isKindOf(Ndef), {
			synth.unmap(*paramArr);
		});

		synth.set(*argValDict.asKeyValuePairs);
	}

	// Private Methods

	prRejectParam { | controlNames |
		var cleanArr;

		cleanArr = controlNames.reject({ arg param;
			paramExclude.includesEqual(param.asString.toLower);
		});
		^cleanArr;
	}

	prInit {
		if (synth.isKindOf(Ndef), {
			var controlNames;
			defaultName = defaultName ? synth.key;
			defaultName = defaultName.asString;
			controlNames = synth.controlKeys;
			paramArr = this.prRejectParam(controlNames);
		}, {
			if (synth.isKindOf(Synth), {
				var synthName, controlNames;
				synthName = synth.defName.asSymbol;
				defaultName = defaultName ? synthName;
				defaultName = defaultName.asString;
				controlNames = SynthDescLib.global[synthName].controlNames;
				paramArr = this.prRejectParam(controlNames);
			});
		});

		outputNumber = paramArr.size;

		controlBus = Bus.control(server, outputNumber);
		subBusDict = IdentityDictionary.new;
		xyModel = IdentityDictionary.newFrom([\x, 0.0, \y, 0.0]);
		active = false;
		viewElement = IdentityDictionary.new;
		xydata = FluidDataSet(server);
		paramsdata = FluidDataSet(server);
		mlp = FluidMLPRegressor(
			server,
			[7],
			activation: FluidMLPRegressor.sigmoid,
			outputActivation: FluidMLPRegressor.sigmoid,
			maxIter: 1000,
			learnRate: 0.1,
			batchSize: 1,
			validation: 0
		);
		currentValues = Array.fill(outputNumber, {0.0});

		{
			xybuf = Buffer.alloc(server, 2);
			paramsbuf = Buffer.alloc(server, outputNumber);

			server.sync;

			this.prCreateSynth;

			server.sync;

			this.prMapSynth; // asynchrone

			server.sync;

			this.prInitController;
			{ this.prCreateView; }.defer;
		}.fork;
	}

	prInitController {
		controller = SimpleController(xyModel);
		controller.put(\update, { | theChanger, what, args |
			var x, y;
			x = theChanger[\x];
			y = theChanger[\y];
			xybuf.setn(0, [x, y]);
			regScaler.set(\t_trig_xy, 1);
			{
				viewElement[\xySlider].x = x;
				viewElement[\xySlider].y = y;
			}.defer;
		});
		controller.put(\set, { | theChanger, what, args |
			var x, y;
			x = theChanger[\x];
			y = theChanger[\y];
			xybuf.setn(0, [x, y]);
			regScaler.set(\t_trig_xy, 1);
		});
		^controller;
	}

	prMapSynth {
		paramArr.do({ arg param, i;
			var parameter, bus;
			parameter = param.asSymbol;
			subBusDict[parameter] = controlBus.subBus(i);

			// get current parameters values
			if (synth.isKindOf(Ndef), {
				var currentVal;
				currentVal = synth.get(parameter);

				if (currentVal.isNumber, {
					subBusDict[parameter].set(currentVal);
					currentValues.put(i, this.prReverseScale(parameter, currentVal)); // set MultiSlider values
				});

				bus = subBusDict[parameter].asMap;
			}, {
				if (synth.isKindOf(Synth), {
					synth.get(parameter, { arg currentVal; // asynchrone
						if (currentVal.isNumber, {
							subBusDict[parameter].set(currentVal);
							currentValues.put(i, this.prReverseScale(parameter, currentVal));
						});
					});

					bus = subBusDict[parameter];
				});
			});

			synth.map(parameter, bus);
		});
	}

	prUnMapSynth {
		var argValDict;
		argValDict = IdentityDictionary.new;

		paramArr.do({ arg param, i;
			var parameter;
			parameter = param.asSymbol;

			// get current parameters values
			if (synth.isKindOf(Ndef), {
				var currentVal;
				currentVal = synth.get(parameter);

				case
				{ currentVal.isSymbol; } { argValDict[parameter] = subBusDict[param].getSynchronous; }
				// { currentVal.isSymbol; } { argValDict[parameter] = synth.getDefaultVal(parameter); }
				{ currentVal.isNumber; } {}
				{ currentVal.isKindOf(Bus); } { argValDict[parameter] = currentVal.getSynchronous; }
				{ argValDict[parameter] = synth.getDefaultVal(parameter); };
			}, {
				if (synth.isKindOf(Synth), {
					synth.get(parameter, { arg currentVal; // asynchrone
						if (currentVal.isNumber, {
							argValDict[parameter] = currentVal;
						});
					});
				});
			});
		});

		if (argValDict.size > 0, {
			if (synth.isKindOf(Ndef), {
				synth.unmap(*paramArr);
			});

			synth.set(*argValDict.asKeyValuePairs);
		});
	}

	prCleanUp {
		var synthDefName;
		synthDefName = regScaler.defName;

		synth.group !? { this.prUnMapSynth; };

		regScaler !? ( _.free; );

		controlBus !? ( _.free; );

		subBusDict.do({ arg bus;
			bus !? ( _.free; );
		});

		SynthDef.removeAt(synthDefName);

		controller !? ( _.remove; );

		mlp !? ( _.free; );
		xybuf !? ( _.free; );
		paramsbuf !? ( _.free; );

		oscFunc !? ( _.free; );
		midiFunc !? ( _.free; );

		xydata !? ( _.free; );
		paramsdata !? ( _.free; );
	}

	prReverseScale { |paramName, value|
		if (paramScale[paramName].notNil, {
			var scaleArr;
			scaleArr = paramScale[paramName];

			if (["exp", "exponential"].includesEqual(scaleArr[2].asString.toLower), {
				value = value.explin(scaleArr[0], scaleArr[1], 0, 1);
			}, {
				if (scaleArr[2].isNumber, {
					value = value.curvelin(scaleArr[0], scaleArr[1], 0, 1, scaleArr[2]);
				}, {
					value = value.linlin(scaleArr[0], scaleArr[1], 0, 1);
				});
			});
		});
		^value;
	}

	prScaleOutput { | outputArr |
		paramScale.keysValuesDo({ arg key, scaleArr;
			var index;
			index = paramArr.indexOf(key.asSymbol);

			if (index.notNil, {
				if (["exp", "exponential"].includesEqual(scaleArr[2].asString.toLower), {
					outputArr[index] = outputArr[index].linexp(0, 1, scaleArr[0], scaleArr[1]);
				}, {
					if (scaleArr[2].isNumber, {
						outputArr[index] = outputArr[index].lincurve(0, 1, scaleArr[0], scaleArr[1], scaleArr[2]);
					}, {
						outputArr[index] = outputArr[index].linlin(0, 1, scaleArr[0], scaleArr[1]);
					});
				});
			});
		});
		^outputArr;
	}

	prCreateSynth {
		{
			var scalerName, replyString;
			scalerName = ("regScaler" ++ UniqueID.next).asSymbol;
			replyString = "/" ++ scalerName.asString;
			SynthDef(scalerName, { arg bus, t_trig_xy = 0, gate = 1, predicting = 0;
				var values, trig, env/*, xy*/;

				env = EnvGen.kr(Env.asr, gate, doneAction: 2);

				trig = t_trig_xy * predicting;
				/*xy = FluidBufToKr.kr(xybuf);
				trig = Mix(Changed.kr(xy)) * predicting;*/
				mlp.kr(trig, xybuf, paramsbuf);
				values = FluidBufToKr.kr(paramsbuf);
				SendReply.kr(trig, replyString, values);

				values = this.prScaleOutput(values);
				Out.kr(bus, values);
			}).add;

			server.sync;

			regScaler = Synth(scalerName, [\bus, controlBus.index, \predicting, 0], group);

			oscFunc = OSCFunc({ arg msg;
				{ viewElement[\multiSlider].value_(msg[3..]); }.defer;
			}, replyString);
		}.fork;
	}

	prAddPoint {
		{ // allow to keep adding points after loading data
			var ids, id, cond;
			id = "point-%".format(UniqueID.next);
			cond = Condition(false);
			xydata.dump({ arg dict;
				ids = dict.keys;
				cond.test = true;
				cond.signal;
			}); // asynchrone

			cond.wait;

			while { ids.includes(id) } { id = "point-%".format(UniqueID.next) };

			xydata.addPoint(id, xybuf);
			paramsdata.addPoint(id, paramsbuf);
		}.fork;
	}

	prLoadDataDefault {
		var path, xyfile, paramsfile, filesExists;
		path = defaultPath ++ defaultName;
		xyfile = path ++ "-xydata.json";
		paramsfile = path ++ "-paramsdata.json";
		filesExists = (File.exists(xyfile) and: File.exists(paramsfile));

		if (filesExists, {
			xydata.read(xyfile);
			paramsdata.read(paramsfile);
		});

		^filesExists;
	}

	prLoadMlpDefault {
		var path, mlpfile, fileExists;
		path = defaultPath ++ defaultName;
		mlpfile = path ++ "-mlp.json";
		fileExists = File.exists(mlpfile);

		if (fileExists, {
			mlp.read(mlpfile);
		});

		^fileExists;
	}

	prCreateView {
		window = Window(defaultName, Rect(10, outputNumber, 840, 320));
		window.background = bgColor;
		window.onClose = { this.prCleanUp; };

		viewElement[\paramName] = StaticText().string_("parameter name").maxHeight_(15);
		viewElement[\paramValue] = StaticText().string_("value").maxHeight_(15);

		viewElement[\multiSlider] = MultiSliderView()
		.size_(outputNumber)
		.elasticMode_(1)
		.isFilled_(1)
		.action_({ arg ms;
			var sliderValues, index, scaleValues;
			sliderValues = ms.value;
			index = ms.index;

			paramsbuf.setn(0, sliderValues);

			scaleValues = this.prScaleOutput(sliderValues);

			{
				viewElement[\paramName].string_(paramArr[index]);
				viewElement[\paramValue].string_(scaleValues[index].round(0.0001));
			}.defer;
		})
		.valueAction_(currentValues.asArray);

		viewElement[\xySlider] = Slider2D()
		.action_({ arg view;
			xyModel[\x] = view.x;
			xyModel[\y] = view.y;
			xyModel.changed(\set);
			active = false;
		})
		.setXYActive(xyModel[\x], xyModel[\y]);

		if ((xCCNum.isInteger and: yCCNum.isInteger), {
			this.xyMidiMap(xCCNum, yCCNum, midiChan);
		});

		viewElement[\addData] = Button()
		.states_([["Add Data"]])
		.action_{
			this.prAddPoint;
		};

		viewElement[\clearData] = Button()
		.states_([["Clear Data"]])
		.action_{
			xydata.clear;
			paramsdata.clear;
		};

		viewElement[\saveData] = Button()
		.states_([["Save data"]])
		.action_{
			if (live.not, {
				Dialog.savePanel({ arg path;
					xydata.write(path ++ "-xydata.json");
					paramsdata.write(path ++ "-paramsdata.json");
				}, path: defaultPath);
			}, {
				var name;
				name = defaultPath ++ defaultName;
				xydata.write(name ++ "-xydata.json");
				paramsdata.write(name ++ "-paramsdata.json");
			});
		};

		viewElement[\loadData] = Button()
		.states_([["Load Data"]])
		.action_{
			Dialog.openPanel({ arg paths;
				var xypath, paramspath;
				xypath = paths.select({ arg path; path.contains("xydata"); });
				paramspath = paths.select({ arg path; path.contains("paramsdata"); });

				if(xypath.notNil, {
					xydata.read(xypath[0]);
				});

				if(paramspath.notNil, {
					paramsdata.read(paramspath[0]);
				});
			}, multipleSelection: true, path: defaultPath);
		};

		this.prLoadDataDefault;

		viewElement[\trainMLP] = Button()
		.states_([["Train MLP"]])
		.action_{
			mlp.fit(xydata, paramsdata, { arg loss;
				{ viewElement[\accuracy].string_(loss.round(0.00000001)); }.defer;
			});
		};

		viewElement[\accuracy] = StaticText().string_("Accuracy").maxHeight_(15);

		viewElement[\clearMLP] = Button()
		.states_([["Clear MLP"]])
		.action_{
			mlp.clear;
			{ viewElement[\accuracy].string_("Accuracy"); }.defer;
		};

		viewElement[\saveMLP] = Button()
		.states_([["Save MLP"]])
		.action_{
			if (live.not, {
				Dialog.savePanel({ arg path;
					mlp.write(path ++ "-mlp.json");
				}, path: defaultPath);
			}, {
				mlp.write(defaultPath ++ defaultName ++ "-mlp.json");
			});
		};

		viewElement[\loadMLP] = Button()
		.states_([["Load MLP"]])
		.action_{
			Dialog.openPanel({ arg path;
				mlp.read(path);
			}, path: defaultPath);
		};

		this.prLoadMlpDefault;

		viewElement[\prediction] = Button()
		.states_([["Not Predicting"], ["Predicting"]])
		.action_{ arg but;
			regScaler.set(\predicting, but.value);
		};

		window.layout = HLayout(
			VLayout(
				HLayout(
					viewElement[\paramName],
					viewElement[\paramValue]
				),
				viewElement[\multiSlider]
			),
			viewElement[\xySlider],
			VLayout(
				viewElement[\addData],
				viewElement[\clearData],
				viewElement[\saveData],
				viewElement[\loadData],
				viewElement[\trainMLP],
				viewElement[\accuracy],
				viewElement[\clearMLP],
				viewElement[\saveMLP],
				viewElement[\loadMLP],
				viewElement[\prediction]
			)
		);
		window.front;
		this.liveMode(true);
		^window;
	}

}

Sorry I didn’t write documentation for it but you can find explicit comments in the class code and you can use it like this:

~reg = RegAutoMapSrvLL(
	server: s, 
	synth: Ndef(\myNdef), 
	paramScale: IdentityDictionary.newFrom(['freq', [20, 20000, \exp], 'pan', [-1, 1, \lin]/*...*/]), 
	paramExclude: [\amp], 
	defaultName: "regressor_default_name", 
	defaultPath: "/home/user_name/Bureau/"
);

thank you very much !
there’s a typo sever: s in your demo.
and IdentityDictionary.newFrom('freq', [20, 20000, \exp], 'pan', [-1, 1, \lin]/*...*/) throws ERROR: Message 'keysValuesDo' not understood..
got it to work with:

(
s.waitForBoot({
	Ndef(\myNdef,{
		Pan2.ar( SinOsc.ar(\freq.kr(200),0,0.5), \pan.kr(0) );
	}).play;
});
)

(
~reg = RegAutoMapSrvLL(
	server: s,
	synth: Ndef(\myNdef),
	paramScale: IdentityDictionary().put('freq', [20, 20000, \exp], 'pan', [-1, 1, \lin]),
	// paramScale: IdentityDictionary.newFrom('freq', [20, 20000, \exp], 'pan', [-1, 1, \lin]/*...*/),
	paramExclude: [\amp],
	defaultName: "regressor_default_name",
	defaultPath: "/home/user/"
	// defaultPath: "/home/user/"
);
)

Thank you for your attention, I fixed the typos.

Is it unreasonable to ask to someone with more experience than me to review this class?

I don’t want to share anything that could be dangerous for users, and since I’ve been having this problem (Interpreter has crashed or stopped forcefully. [Exit code: 11] - #52 by kesey) for several months and can’t figure out where it’s coming from, I can’t rule out the possibility that it might be caused by my code.

Some language pointers.

Don’t do a ? expr, instead do a ?? { expr }. This way expr only gets evaluated when needed.

Don’t do { | a = 1 | ... } instead do { | a (1) | ... }. This one’s really confusing, but tldr, a can still be nil in some cases.

Having class initialisation that forks a function is a bad idea as you won’t be able to use the class until it is done and there is no way to enforce that. You could use CondVar but then you can’t make an instance of the class on the main thread.

I fixed the two first points, thanks for the tips.

Is there a scenario where you prefer to use a ? expr over a ?? { expr } ? Can you give me an example ? I don’t understand why it isn’t good to do a ? expr in my case.

but I don’t know how to fix the last one.

How can I use CondVar here ? Can you show me with code ?

What does that mean ? What is the workaround ?

(post deleted by author - post will be hidden for 24 hours and then fully removed)

No.

If it’s a literal (like a number) then it doesn’t really matter. If it involves creating a resource, then ?? { ... } only makes the resource when necessary.

You have to wait on it and let the forked functions mark it as done.

You can’t wait on the main thread.

I don’t know! Never found a good solution myself. I always change the default to always fork so nothing runs on the main thread, then you can just block whenever.

(post deleted by author - post will be hidden for 24 hours and then fully removed)

x = 1;
x = x ? Array.fill(1000000, nil);

Here, the steps for the second line are 1/ push x onto the stack; 2/ create the million element array and push it; do the ? operator. Because x is not nil, the array will never be used, but it is always created, and always allocating memory, and always adding to the garbage collector’s load.

If you use `?? { Array.fill(1000000, nil) }, then the steps are 1/ push x onto the stack; 2/ if x is not nil, jump over the function and just use x; 3/ if you didn’t jump over, then create the array. If x isn’t nil, then the array creation is skipped.

I agree with Jordan here; there’s not really a good reason to use ?.

hjh

Just for precision not that it really matters here…

{ ... } is technically not a Function yet. It’s better to refer to it as a ‘block’, that if it can be inlined, gets turned into bytecode and injected into the parent block, and if it can’t, finally becomes a Function.

But yeah, that level of detail doesn’t really matter for this.


One exception I just thought of is when you want to return a Function. The following both return functions (that return 1).

a ? {1}
a ?? {{1}}

Top is one less set of brackets and easier to read. Although this is quite confusing in many ways due to their similarity.

These should also have minimal performance difference as the Function is created at compile time and only pushed on to the stack. It behaves like a literal in this case.

Of precision: I intentionally did not capitalize “function” because I was aware of referring to a syntactic unit and not an instance of a class. Well, you’ll be able to find some post of mine in the past where I used lowercase-function to refer to a Function object, so you caught me, but in this case it was on purpose :handshake:

hjh

1 Like

Thank you very much, it’s really clear for me now.
I’ll check all my code and replace all the a ? expr by a ?? { expr }

What is not clear for me is this part.
I understand why it’s a bad idea to have class initialisation that forks methods cause you don’t have garanties that all is done when you make an instance.
But I don’t understand how you are supposed to initialize your class if you need to add a synthDef to the SynthDescLib for example or anything asynchronous ?
If I just move the routine part outside of the initialisation, Am I just shifting the problem without solving it ?

If someone could help me with that it would be very welcome.

Thank you @jordan to point me some bad practice of mine.

In this example, the routine is outside but there is a server.sync inside the class initialisation.
Is the

thing is a problem here too ? or is it a kind of workaround ?

SuperDirtMixer {
	var dirt;
	var <>presetPath = "../../presets/";
	var <>prMasterBus;
	var <>switchControlButtonEvent;
	var >switchLiveButtonEvent;
	var >midiInternalOut;
	var reverbVariableName = \room;
	var reverbNativeSize = 0.95;
	var oscMasterLevelSender;
	var defaultParentEvent, defaultParentEventKeys;
	var freeAction, addRemoteControlAction;

	*new { |dirt|
		^super.newCopyArgs(dirt).init
	}

	/* INITIALIZATION */
	init {
		try {
			"---- initialize SuperDirtMixer ----".postln;
			this.prLoadSynthDefs;

			dirt.server.sync;

                        ...

I take this example from here:

Then you instantiate it like this:

s.waitForBoot {
        ~dirt = SuperDirt(2, s);
        ~dirt.start(57120, 0 ! 14);
        // More SuperDirt ...

        // Initialize the SuperDirtMixer
        ~mixer = SuperDirtMixer(~dirt);

        // You can adjust parameters before you use the ui
        ~mixer.setMasterBus(0);

        // When you added your settings in you startup file, then you should be able to use the ui at any time in any differtent SuperCollider file.
        ~mixer.gui;
    }

If you instantiate it in wait for boot you can use things like CondVar and wait for the other threads to finish. There’s no problem there.

The problem comes when you use it in the main thread where you can’t wait.

I don’t know what the solution is, I’ve never found a good one. One option would be to have a Boolean that gets marked when the initialisation has finished, then check it where relevant.

I once made an auto promise type specifically to fix this issue. People didn’t like it because it was too complex… which it was, but it did work well.

We could also make all code evaluations fork - @jamshark70 I think this was your idea originally? I’ve been running it for a while now and haven’t had any issues.

Thank you very much , I think I understand now.

It might simplify this object’s init process to use forkIfNeeded – then, if the user instantiates it within a thread, it will just block the thread rather than forking new ones.

I’m afraid I don’t have bandwidth for a full review. A general comment would be that SC isn’t very modern about asynchronous ops. The only in-built sync mechanism we have is a callback function (action arguments in various methods). CondVar can be used to make these callbacks less awkward when there are long chains of them, but the asynchronous op itself doesn’t know which CondVar to signal upon completion, so we usually end up relying on a callback function anyway, to do the signaling.

That’s a long way of saying that a SC-idiomatic way of finishing object init asynchronously is to put an action argument in, and call that function when init is finished. Then it’s the user’s responsibility to do something useful with the action function if they want (or, maybe they don’t care and they’re content to wait before running next instruction – this is the user’s choice).

Jordan isn’t wrong here – callbacks are outdated compared to Python’s await for instance, so can they be called a “good” solution? But, if the choices are a/ to architect a proper generalized sync mechanism before you can even think about releasing your class or b/ to use an existing mechanism that lacks the “cool” factor of more up-to-date languages but which isn’t worse than syncing after loading a buffer, then… I think there’s a solid defense to be made in favor of not over-engineering.

There’s also scztt’s Deferred quark, which streamlines it a bit (at the cost of pulling in a dependency).

TBH I don’t see a strong rationale not to fork every interactive code execution. Error dumps would look a bit different (the head would be a scheduler, and not intepretPrintCmdLine) but people would get used to that, I think.

hjh

What’s still a bit unclear to me is this concept of a thread.

From what I gather after reading your explanation: when you execute code within a routine, you create a new thread and you are therefore outside the main thread; in all other cases (when you execute code outside a routine), you are in the main thread.

Is that correct?

Almost, routine inherits from thread, when you create it, you create the new thread.

When you call run/next/value the thread is scheduled with the scheduler to run when next possible, but the current thread continues.

It is only when no threads are currently running will the new routine be evaluated.

... some code A ... 
fork { ... some code B ... }
... some code C ....

Order of execution here is: A, create the new thread, C, (maybe some other threads), then B.