Hello,
I try to structure my code in Class, and I’d like to embrace good coding practice.
I’m new in writing classes in SuperCollider so forgive me if I still make mistakes and don’t hesitate to correct me when I’m wrong.
Any advice to improve this code example is really welcome.
AFAICS this example works, but I’d like to improve it by converting it to MVC.
I’m a bit familiar with this design pattern but I’m struggling at applying it to this case.
I looked at SimpleController and ObjectGui Help files, I searched on this forum but I can’t wrap my head around this.
Any help/pointer to a good example or tutorial is welcome; I thank you in advance
The Class:
MixerLL { // 2 Aux Mixer (accept feedback)
var <server, <channelNumber,
<aux1Bus, <aux2Bus, <masterOut,
<group,
<bgColor, <aux1Color, <aux2Color, <masterBgColor,
<ccStart, <ccEnd, <mixerMidiChan,
<window, // global window
<view, // composite view
<channel, // gui element view
<channelBus; // input bus
// Class Method
*new { | server, channelNumber = 5, aux1Bus, aux2Bus, masterOut, group |
if (server.serverRunning.not, {
^warn("Server is not running");
}, {
channelNumber = channelNumber.max(1).min(16);
^super.newCopyArgs(server, channelNumber, aux1Bus, aux2Bus, masterOut, group).prMake;
});
}
// instance Methods
createGui { | backgroundColor, aux1BackgroundColor, aux2BackgroundColor, masterBackgroundColor |
bgColor = backgroundColor ? Color.white;
aux1Color = aux1BackgroundColor ? Color.new255(205, 205, 193);
aux2Color = aux2BackgroundColor ? Color.new255(238, 238, 209);
masterBgColor = masterBackgroundColor ? Color.grey;
window = Window("Mixer", Rect(30, 0, ((170 * (channelNumber + 1)) + 595).min(1840), 900), scroll: true);
window.front;
window.view.decorator = FlowLayout(window.view.bounds, 3@3, 3@3);
window.onClose = {
channelNumber.do({ arg i;
channel[(\channel ++ i).asSymbol][\Synth].free;
});
channel[\master][\Synth].free;
};
this.prCreateChannelsView;
this.prCreateMasterView;
}
midiMapping { | midiCcStart = 21, midiChannel = 0 |
if (window.isNil, {
this.createGui;
});
midiCcStart = midiCcStart.max(1).min(127);
ccStart = midiCcStart;
midiChannel = midiChannel.max(0).min(15);
mixerMidiChan = midiChannel;
format("mixer midi channel: %\n", midiChannel).post;
channelNumber.do({ arg i;
var channelName;
channelName = (\channel ++ i).asSymbol;
// lpf midi map
this.prCreateMidiFunc(midiCcStart, channelName, \lpfreq, \activeLpf, 20, 20000, true, 500);
format("channel % lpfreq cc number: %\n", i, midiCcStart).post;
midiCcStart = midiCcStart + 1;
// Eq
3.do({ arg j;
j = j + 1;
// freq midi map
this.prCreateMidiFunc(midiCcStart, channelName, (\band ++ j ++ \freq).asSymbol, (\activeBand ++ j ++ \Freq).asSymbol, 20, 20000, true, 500);
format("channel % band%freq cc number: %\n", i, j, midiCcStart).post;
midiCcStart = midiCcStart + 1;
// rq midi map
this.prCreateMidiFunc(midiCcStart, channelName, (\band ++ j ++ \rq).asSymbol, (\activeBand ++ j ++ \Rq).asSymbol, 0, 1, false, 0.1);
format("channel % band%rq cc number: %\n", i, j, midiCcStart).post;
midiCcStart = midiCcStart + 1;
// db midi map
this.prCreateMidiFunc(midiCcStart, channelName, (\band ++ j ++ \db).asSymbol, (\activeBand ++ j ++ \Db).asSymbol, -60, 60, false, 10);
format("channel % band%db cc number: %\n", i, j, midiCcStart).post;
midiCcStart = midiCcStart + 1;
});
// hpf midi map
this.prCreateMidiFunc(midiCcStart, channelName, \hpfreq, \activeHpf, 20, 20000, true, 500);
format("channel % hpfreq cc number: %\n", i, midiCcStart).post;
midiCcStart = midiCcStart + 1;
// aux
2.do({ arg j;
j = j + 1;
// aux midi map
this.prCreateMidiFunc(midiCcStart, channelName, (\aux ++ j).asSymbol, (\activeAux ++ j).asSymbol, 0, 1, false, 0.1);
format("channel % aux% cc number: %\n", i, j, midiCcStart).post;
midiCcStart = midiCcStart + 1;
});
// mute midi map
MIDIFunc.cc({ arg val, num, chan, src;
var midiControl;
midiControl = val.linlin(0, 127, 0, 1);
{ channel[channelName][\Control][\mute].valueAction_(midiControl); }.defer;
}, midiCcStart, midiChannel);
format("channel % mute cc number: %\n", i, midiCcStart).post;
midiCcStart = midiCcStart + 1;
// pan midi map
this.prCreateMidiFunc(midiCcStart, channelName, \pan, \activePan, -1, 1, false, 0.1);
format("channel % pan cc number: %\n", i, midiCcStart).post;
midiCcStart = midiCcStart + 1;
// level midi map
this.prCreateMidiFunc(midiCcStart, channelName, \level, \activeLevel, 0, 1, false, 0.1);
format("channel % level cc number: %\n", i, midiCcStart).post;
midiCcStart = midiCcStart + 1;
});
ccEnd = midiCcStart - 1;
}
// Private Methods
prMake {
var masterBus;
channel = IdentityDictionary.new;
channelBus = Array.newClear(channelNumber);
{
this.prInitSynth;
server.sync;
masterBus = Bus.audio(server, 2);
channelNumber.do { arg i;
var channelName = (\channel ++ i).asSymbol;
channelBus[i] = Bus.audio(server, 2);
channel[channelName] = IdentityDictionary.new;
channel[channelName][\Synth] = Synth(\ChannelStrip, [\in, channelBus[i], \outAux1, aux1Bus, \outAux2, aux2Bus, \out, masterBus], group);
server.sync;
};
channel[\master] = IdentityDictionary.new;
channel[\master][\Synth] = Synth(\MasterStrip, [\in, masterBus, \out, masterOut], group, addAction: 'addToTail');
server.sync;
}.fork;
}
prInitSynth {
if(SynthDescLib.global[\ChannelStrip].isNil, {
SynthDef(\ChannelStrip, { arg in = 0, out = 0, outAux1 = 3, outAux2 = 4, pan = 0, hpfreq = 20, band1freq = 8000, band1rq = 1, band1db = 0, band2freq = 1200, band2rq = 1, band2db = 0, band3freq = 80, band3rq = 1, band3db = 0, lpfreq = 20000, mute = 1, auxsend1 = 0, auxsend2 = 0, level = 0.5, delaytime = 0;
var input, sig, bad, lagTime;
lagTime = 0.2
input = InFeedback.ar(in, 2);
sig = BHiPass4.ar(input, Lag2.kr(hpfreq.max(20).min(20000), lagTime)); // HPF
sig = BPeakEQ.ar(sig, Lag2.kr(band1freq.max(20).min(20000), lagTime), band1rq, band1db); // Band 1
sig = BPeakEQ.ar(sig, Lag2.kr(band2freq.max(20).min(20000), lagTime), band2rq, band2db); // Band 2
sig = BPeakEQ.ar(sig, Lag2.kr(band3freq.max(20).min(20000), lagTime), band3rq, band3db); // Band 3
sig = BLowPass4.ar(sig, Lag2.kr(lpfreq.max(20).min(20000)), lagTime); // LPF
sig = Select.ar(CheckBadValues.ar(sig, post: 2) > 0, [sig, DC.ar(0)]); // test for infinity, NaN and denormals before sending to Aux and MasterStrip
Out.ar(outAux1, sig * auxsend1.curvelin(0, 1, 0, 1, log(10))); // Aux 1 pre fader / pre mute / post eq
Out.ar(outAux2, sig * auxsend2.curvelin(0, 1, 0, 1, log(10))); // Aux 2 pre fader / pre mute / post eq
sig = sig * Lag2.kr(mute, lagTime); // Mute
sig = DelayN.ar(sig, 0.01, delaytime / 100); // Delay
Out.ar(out, Balance2.ar(sig[0], sig[1], pan, level.curvelin(0, 1, 0, 1, log(10)))); // curvelin to have a logarithmic scale
}).add;
});
if(SynthDescLib.global[\MasterStrip].isNil, {
SynthDef(\MasterStrip, { arg in = 0, out = 0, hpfreq = 20, lpfreq = 20000, level = 0.5;
var input, sig, lagTime;
lagTime = 0.2;
input = In.ar(in, 2);
sig = BHiPass4.ar(input, Lag2.kr(hpfreq.max(20).min(20000), lagTime)); // HPF
sig = BLowPass4.ar(sig, Lag2.kr(lpfreq.max(20).min(20000), lagTime)); // LPF
sig = sig * level.curvelin(0, 1, 0, 1, log(10));
sig = Limiter.ar(sig);
Out.ar(out, sig);
// Out.ar(out+2, sig); // use it to feed external gear with signal
}).add;
});
}
prCreateMidiFunc { | midiCc, channelName, controlName, activeName, valMin = 0, valMax = 1, exp = false, threshold = 0.1 |
^MIDIFunc.cc({ arg val, num, chan, src;
var guiControl, midiControl;
if (exp, {
midiControl = val.linexp(0, 127, valMin, valMax);
}, {
midiControl = val.linlin(0, 127, valMin, valMax);
});
guiControl = channel[channelName][\Control][controlName].value;
if ( // soft takeover
(channel[channelName][\Control][activeName] or: ((midiControl > (guiControl - threshold)) and: (midiControl < (guiControl + threshold)))),
{
channel[channelName][\Control][activeName] = true;
{
channel[channelName][\Synth].set(controlName, midiControl);
channel[channelName][\Control][controlName].value_(midiControl);
}.defer;
}
);
}, midiCc, mixerMidiChan);
}
prCreateMasterView {
channel[\master][\Control] = IdentityDictionary.new;
view[\master] = CompositeView(window, 170@780).background_(masterBgColor);
view[\master].decorator_(FlowLayout(view[\master].bounds, 3@3, 3@3));
// Channel name
StaticText(view[\master], 118@10)
.string_("Master")
.stringColor_(Color.white)
.align_(\center);
view[\master].decorator.nextLine;
// lpf
channel[\master][\Control][\lpfreq] = EZKnob(view[\master], 124@70, "lpfreq", ControlSpec(20, 20000, step: 1, default: 20000), { arg lpfreq;
channel[\master][\Synth].set(\lpfreq, lpfreq.value);
}, margin: 39@0).setColors(stringColor: Color.white);
view[\master].decorator.nextLine;
// hpf
channel[\master][\Control][\hpfreq] = EZKnob(view[\master], 120@70, "hpfreq", ControlSpec(20, 20000, step: 1, default: 20), { arg hpfreq;
channel[\master][\Synth].set(\hpfreq, hpfreq.value);
}, margin: 39@0).setColors(stringColor: Color.white);
view[\master].decorator.nextLine;
// level
channel[\master][\Control][\level] = EZSlider(view[\master], 110@160, "level", ControlSpec(0, 1, step: 0.01, default: 0.5), { arg level;
channel[\master][\Synth].set(\level, level.value);
}, layout: 'vert', margin: 39@0).setColors(stringColor: Color.white);
}
prCreateChannelsView {
view = IdentityDictionary.new;
channelNumber.do({ arg i;
var channelName;
channelName = (\channel ++ i).asSymbol;
channel[channelName][\Control] = IdentityDictionary.new;
view[channelName] = CompositeView(window, 170@780).background_(bgColor);
view[channelName].decorator_(FlowLayout(view[channelName].bounds, 3@3, 3@3));
// Channel name
StaticText(view[channelName], 190@30)
.string_(channelName.asString)
.stringColor_(Color.black)
.align_(\center);
view[channelName].decorator.nextLine;
// lpf
channel[channelName][\Control][\activeLpf] = false;
channel[channelName][\Control][\lpfreq] = EZKnob(view[channelName], 124@70, "lpfreq", ControlSpec(20, 20000, step: 1, default: 20000), { arg lpfreq;
channel[channelName][\Synth].set(\lpfreq, lpfreq.value);
channel[channelName][\Control][\activeLpf] = false;
}, margin: 39@0);
view[channelName].decorator.nextLine;
// Eq
3.do({ arg j;
var default;
switch(j,
0, { default = 8000; },
1, { default = 1200; },
2, { default = 80; },
{ default = 1200; }
);
j = j + 1;
// freq
channel[channelName][\Control][(\activeBand ++ j ++ \Freq).asSymbol] = false;
channel[channelName][\Control][(\band ++ j ++ \freq).asSymbol] = EZKnob(view[channelName], 35@70, "freq" ++ j, ControlSpec(20, 20000, step: 1, default: default), { arg freq;
channel[channelName][\Synth].set((\band ++ j ++ \freq).asSymbol, freq.value);
channel[channelName][\Control][(\activeBand ++ j ++ \Freq).asSymbol] = false;
});
// rq
channel[channelName][\Control][(\activeBand ++ j ++ \Rq).asSymbol] = false;
channel[channelName][\Control][(\band ++ j ++ \rq).asSymbol] = EZKnob(view[channelName], 35@70, "rq" ++ j, ControlSpec(0, 1, step: 0.01, default: 1), { arg rq;
channel[channelName][\Synth].set((\band ++ j ++ \rq).asSymbol, rq.value);
channel[channelName][\Control][(\activeBand ++ j ++ \Rq).asSymbol] = false;
});
// db
channel[channelName][\Control][(\activeBand ++ j ++ \Db).asSymbol] = false;
channel[channelName][\Control][(\band ++ j ++ \db).asSymbol] = EZKnob(view[channelName], 35@70, "db" ++ j, ControlSpec(-60, 60, step: 1, default: 0), { arg db;
channel[channelName][\Synth].set((\band ++ j ++ \db).asSymbol, db.value);
channel[channelName][\Control][(\activeBand ++ j ++ \Db).asSymbol] = false;
});
view[channelName].decorator.nextLine;
});
// hpf
channel[channelName][\Control][\activeHpf] = false;
channel[channelName][\Control][\hpfreq] = EZKnob(view[channelName], 120@70, "hpfreq", ControlSpec(20, 20000, step: 1, default: 20), { arg hpfreq;
channel[channelName][\Synth].set(\hpfreq, hpfreq.value);
channel[channelName][\Control][\activeHpf] = false;
}, margin: 39@0);
view[channelName].decorator.nextLine;
// aux
2.do({ arg j;
var color;
switch(j,
0, { color = aux1Color; },
1, { color = aux2Color; },
{ color = Color.white; }
);
j = j + 1;
channel[channelName][\Control][(\activeAux ++ j).asSymbol] = false;
channel[channelName][\Control][(\aux ++ j).asSymbol] = EZKnob(view[channelName], 35@70, "aux" ++ j, ControlSpec(0, 1, step: 0.01, default: 0), { arg aux;
channel[channelName][\Synth].set((\auxsend ++ j).asSymbol, aux.value);
channel[channelName][\Control][(\activeAux ++ j).asSymbol] = false;
})
.setColors(background: color);
if (j == 1, {
StaticText(view[channelName], 35@65)
.string_("Send")
.align_(\center);
});
});
// mute
channel[channelName][\Control][\mute] = Button(view[channelName], Rect(10,110,112,35)).states_([["Mute", Color.black, Color.white],["Active", Color.white, Color.grey]]).action = { arg mute;
channel[channelName][\Synth].set(\mute, mute.value.linlin(0, 1, 1, 0));
};
// pan
channel[channelName][\Control][\activePan] = false;
channel[channelName][\Control][\pan] = EZSlider(view[channelName], 112@60, "pan", ControlSpec(-1, 1, step: 0.01, default: 0), { arg pan;
channel[channelName][\Synth].set(\pan, pan.value);
channel[channelName][\Control][\activePan] = false;
}, layout: 'vert');
view[channelName].decorator.nextLine;
// level
channel[channelName][\Control][\activeLevel] = false;
channel[channelName][\Control][\level] = EZSlider(view[channelName], 110@160, "level", ControlSpec(0, 1, step: 0.01, default: 0.5), { arg level;
channel[channelName][\Synth].set(\level, level.value);
channel[channelName][\Control][\activeLevel] = false;
}, layout: 'vert', margin: 39@0);
view[channelName].decorator.nextLine;
// delay
channel[channelName][\Control][\delay] = EZNumber(view[channelName], 95@20, "delay", ControlSpec(0, 1, step: 0.01, default: 0), { arg delay;
channel[channelName][\Synth].set(\delaytime, delay.value);
});
});
}
}
The dimension of the GUI elements is not set yet sorry for this.
How to use it
(
MIDIClient.init;
if (MIDIClient.sources[3].notNil, {
MIDIIn.connect(0, MIDIClient.sources[3]);
});
s = Server.default;
s.boot;
)
(
SynthDef(\flanging, {arg in = 0, out = 0, drywet = 0.5, fgfreq = 200, fdback = 0.99;
var input, sig, effect;
input = InFeedback.ar(in, 2);
effect = input + LocalIn.ar(2); // add some feedback
effect = DelayN.ar(effect, 0.02, SinOsc.kr(fgfreq, 0, 0.005, 0.005)); // max delay of 20msec
LocalOut.ar(fdback * effect);
sig = XFade2.ar(input, effect, drywet);
ReplaceOut.ar(out, sig);
//XOut.ar(out, drywet, effect);
}).add;
SynthDef(\greyHole, { arg in = 0, out = 0, drywet = 0.5, amp = 1, delayTime = 2.1, damp = 0.62, size = 1.33, diff = 0.08, feedback = 0, modDepth = 0.5, modFreq = 3;
var sig, input;
input = InFeedback.ar(in, 2);
sig = Greyhole.ar(input, delayTime, damp, size, diff, feedback, modDepth, modFreq);
// sig = SelectX.ar(drywet, [input, sig]);
sig = XFade2.ar(input, sig, drywet, Lag2.kr(amp));
ReplaceOut.ar(out, sig);
}).add;
SynthDef(\snapkick, { |out = 0, amp = 0.3, pan = 0, bdFrqL1 = 261, bdFrqL2 = 120, bdFrqL3 = 51, bdFrqT1 = 0.035, bdFrqT2 = 0.08, bdFrqC = \exp, bdAmpAtt = 0.005, bdAmpSus = 0.1, bdAmpRel = 0.3, bdAmpLev = 1, bdAmpCurve = \linear, popFrqSt = 750, popFrqEnd = 261, popFrqDur = 0.02, popAmpAtt = 0.001, popAmpSus = 0.02, popAmpRel = 0.001, popAmpLev = 0.15, clkAmpAtt = 0.001, clkAmpRel = 0.01, clkAmpLev = 0.15, clkAmpCurve = (-4), clkfFundFreq = 910, clkfFormFreq = 4760, clkfBwFreq = 2110, clkLpfFreq = 3140, doneAction = 2|
var body, bodyFreq, bodyAmp;
var pop, popFreq, popAmp;
var click, clickAmp;
var snd;
// body starts midrange, quickly drops down to low freqs, and trails off
bodyFreq = EnvGen.ar(Env([bdFrqL1, bdFrqL2, bdFrqL3], [bdFrqT1, bdFrqT2], bdFrqC));
bodyAmp = EnvGen.ar(Env.linen(bdAmpAtt, bdAmpSus, bdAmpRel, bdAmpLev, bdAmpCurve), doneAction: doneAction);
body = SinOsc.ar(bodyFreq) * bodyAmp;
// pop sweeps over the midrange
popFreq = XLine.kr(popFrqSt, popFrqEnd, popFrqDur);
popAmp = EnvGen.ar(Env.linen(popAmpAtt, popAmpSus, popAmpRel, popAmpLev));
pop = SinOsc.ar(popFreq) * popAmp;
// click is spectrally rich, covering the high-freq range
// you can use Formant, FM, noise, whatever
clickAmp = EnvGen.ar(Env.perc(clkAmpAtt, clkAmpRel, clkAmpLev, clkAmpCurve));
click = LPF.ar(Formant.ar(clkfFundFreq, clkfFormFreq, clkfBwFreq), clkLpfFreq) * clickAmp;
snd = body + pop + click;
snd = snd.tanh;
OffsetOut.ar(out, Pan2.ar(snd, pan, amp));
}).add;
~mixGroup = Group.new(s, \addToTail);
~aux1 = Bus.audio(s, 2);
~aux2 = Bus.audio(s, 2);
~aux1Color = Color.new255(205, 205, 193);
~aux2Color = Color.new255(238, 238, 209);
)
m = MixerLL(s, 5, ~aux1, ~aux2, 0, ~mixGroup);
m.channel[\channel1];
m.channel[\master][\Synth].set(\out, 0);
m.channelBus;
m.createGui;
m.midiMapping;
m.ccStart;
m.ccEnd;
m.channel[\channel0][\Control][\band1rq].valueAction_(0.76);
m.channel[\channel0][\Control][\band1rq].value;
(
s.bind { ~flanging = Synth(\flanging, [\in, ~aux1, \out, m.channelBus[3]]); };
m.view[\channel3].background_(~aux1Color);
s.bind { ~greyhole = Synth(\greyHole, [\in, ~aux2, \out, m.channelBus[4]]); };
m.view[\channel4].background_(~aux2Color);
)
(
Pdef(\LL,
Pbind(
\instrument, \snapkick,
\dur, Pseq([0.5, 0.75], inf),
\out, m.channelBus[0] // v.channelBus[0]
)
).play;
)
Pdef(\LL).stop;