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/"
);