Thank you very much @jordan and @jamshark70 for your advices.
I added a callback method to the class (action argument) and I use forkIfNeeded everywhere I used to fork I don’t use CondVar inside the class cause I thaught that you can use it outside:
you make an instance, put a cond.wait just after (you have to be in a routine) and do a cond.signalOne inside your action method. This way you know that your object is ready.
Does that seem acceptable to you?
this is the new class:
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
<action, // A callback Function will be passed this as an argument when evaluated
<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,
isNdef;
// Class Method
*new { | server, synth, paramScale, paramExclude, defaultName, defaultPath, xCCNum, yCCNum, midiChan, group, bgColor, action |
if (server.isKindOf(Server).not, {
^warn("Server required");
});
if (server.serverRunning.not, {
^warn("Server is not running");
});
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; };
action = action ?? { { arg reg; format("% is ready\n", reg).post; } };
^super.newCopyArgs(server, synth, paramScale, paramExclude, defaultName, defaultPath, xCCNum, yCCNum, midiChan, group, bgColor, action).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 (isNdef, {
preset = "Ndef(\\" ++ synth.key.asSymbol ++ ").xset(%);".format(argString);
}, {
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;
}.forkIfNeeded;
}
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;
});
}.forkIfNeeded;
}
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 (isNdef, {
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;
if (synth.source.isNil, {
^warn("Ndef function isNil");
});
isNdef = true;
defaultName = defaultName ?? { synth.key; };
defaultName = defaultName.asString;
controlNames = synth.controlKeys;
paramArr = this.prRejectParam(controlNames);
}, {
if (synth.isKindOf(Synth), {
var synthName, controlNames;
isNdef = false;
synthName = synth.defName.asSymbol;
defaultName = defaultName ?? { synthName; };
defaultName = defaultName.asString;
controlNames = SynthDescLib.global[synthName].controlNames;
paramArr = this.prRejectParam(controlNames);
}, {
^warn("Synth or Ndef required");
});
});
outputNumber = paramArr.size;
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});
{
controlBus = Bus.control(server, outputNumber);
xybuf = Buffer.alloc(server, 2);
paramsbuf = Buffer.alloc(server, outputNumber);
server.sync;
this.prCreateSynth;
server.sync;
this.prMapSynth;
server.sync;
this.prInitController;
{
this.prCreateView;
this.liveMode(true);
this.prCallBack;
}.defer;
}.forkIfNeeded;
}
prCallBack {
action.value(this);
}
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 (isNdef, {
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;
}, {
synth.get(parameter, { arg currentVal; // asynchronous
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 (isNdef, {
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); };
}, {
synth.get(parameter, { arg currentVal; // asynchronous
if (currentVal.isNumber, {
argValDict[parameter] = currentVal;
});
});
});
});
if (argValDict.size > 0, {
if (isNdef, {
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);
}.forkIfNeeded;
}
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;
}); // asynchronous
cond.wait;
while { ids.includes(id) } { id = "point-%".format(UniqueID.next) };
xydata.addPoint(id, xybuf);
paramsdata.addPoint(id, paramsbuf);
}.forkIfNeeded;
}
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;
^window;
}
}
I haven’t been able to thoroughly test it yet after making these changes