Creating a MIDI Bank

Ok, this is going to be a rather long post. When dealing with many controllers, especially if you want to use GUI elements in conjunction with midi controllers, the data structure is key. The structure below may be a tad bit more involved than needed if you don’t use additional SC created GUI(s) but as soon as you want to create a GUI and have both the gui and the cc controllers update values, this structure makes it very easy. For now I will focus on the cc controllers and save the GUI part for another day.

// I will use the single letter variable 'p' to keep track of all events, first bank = index of 0;
p = ();
~bank = 0;

/// 2 SynthDefs for demonstration, here I keep doneaction = 0, so synths can be retriggered
(
SynthDef.new(\pulse, {
	var env, sig;
	env = Env.asr([\atk.kr(0.3), 1, \rel.kr(1)]).kr(0, \gate.kr(0));
	sig = Pulse.ar(\freq.kr(110), \width.kr(0.1), 0.2);
	Out.ar(0, sig!2 * env * \amp.kr(1));
}).add;

SynthDef.new(\saw, {
	var env, sig;
	env = Env.asr([\atk.kr(0.3), 1, \rel.kr(1)]).kr(0, \gate.kr(0));
	sig = Saw.ar(\freq.kr(110), 0.1); 
	sig = LPF.ar(sig, \cutOff.kr(440).lag(0.1));
	Out.ar(0, sig!2 * env  * \amp.kr(1));
}).add;
)

/// Each Synth has a corresponding event keeping track of parameters, values and mappings

p.add(\pulse -> (
	sequence: [\amp, \atk, \rel, \width], // in case you want to create a GUI with controls in a specific order
	vals: (amp: 1, atk: 0.3, rel: 1, width: 0.1, gate: 0), // initial values, these will be dynamically changed by the cc controllers
	maps: (amp: [0, 1], atk: [0.005, 3], rel: [0.05, 6], width: [0, 1], gate: [0, 1, \lin, 1])
));

p.add(\saw -> (
	sequence: [\amp, \atk, \rel, \width], // in case you want to create a GUI with controls in a specific order
	vals: (amp: 1, atk: 0.3, rel: 1, cutOff: 440, gate: 0), // initial values, these will be dynamically changed by the cc controllers
	maps: (amp: [0, 1], atk: [0.005, 3], rel: [0.05, 6], cutOff: [50, 2000, \exp], gate: [0, 1, \lin, 1])
))

// instantiate both synths and add them to the synth-events:

p.pulse.add(\syn -> Synth(\pulse)); // initial gate = 0, so no sound yet
p.pulse.syn.set(\gate, 1) // trigger the env to hear sound
p.pulse.syn.set(\gate, 0) // no sound, but synth is still on the server

p.saw.add(\syn -> Synth(\saw));
p.saw.syn.set(\gate, 1) // trigger the env to hear sound
p.saw.syn.set(\gate, 0) // no sound, but synth is still on the server

// create mappings for you buttons, since they only send value = 127, we need to toggle it:

~buttonStates = 10.collect{|i| (\17: 0, \18: 0, \19: 0, 20: \0) }; // 4 buttons per bank, initially set to 0;

// simulation af button press from button with ccNum = 17

( // evaluate several times to see the button toggle for \17
var ccNum = 17; // incoming via MIDIdef;
~buttonStates[~bank][ccNum.asSymbol] = 1 - ~buttonStates[~bank][ccNum.asSymbol]; // toggle button
)

// create master event for mappings for cc controllers

(
~ccValues = ()!10; // 10 empty events
)

// create assignments for the controls or bank 0
(
~ccValues[0] = (
	\1: [\pulse, \amp], \2: [\pulse, \atk], \3: [\pulse, \rel], \4: [\pulse, \width],
	\5: [\saw, \amp], \6: [\saw, \atk], \7: [\saw, \rel], \8: [\saw, \cutOff],
)
)

// create master event for button presses

(
~buttonFuncs = ()!10;
)

// create botton funcs for bank 0
(
~buttonFuncs[0] = (
	\17: {|val| 
		p.saw.vals.gate = val; 
		p.saw.syn.set(\gate, p.saw.vals.gate);
		p.pulse.vals.gate = 1 - val;
		p.pulse.syn.set(\gate, p.pulse.vals.gate);
	},// toggles the 2 synths
	\18: {|val|
	// put something here
	}
	// etc.
)
)

( // emulate the pressing of button, to toggle the 2 synths, evaluate several times
~buttonStates[~bank][\17] = 1 - ~buttonStates[~bank][\17];
~buttonFuncs[0][\17].(~buttonStates[~bank][\17]);
)

// Function for settings parameters
(
~setParameter = {| val = 0, target = \pulse, parameter = \amp| // val is normalized in the range [0, 1]
	var mappedVal = p[target][\maps][parameter].asSpec.map(val);
	p[target][\vals][parameter] = mappedVal;
	p[target][\vals][parameter].debug(target++" "++parameter);
	p[target][\syn].set(parameter, mappedVal);
};
)

// test it ///

~setParameter.(1, \saw, \gate);
~setParameter.(0.3, \saw, \cutOff);
~setParameter.(1, \pulse, \gate);
~setParameter.(0.3, \pulse, \width);

~setParameter.(0, \saw, \gate);
~setParameter.(0, \pulse, \gate);

// MIDIdef
(
MIDIdef.cc(\cc, {|value, ccNum|
	[value, ccNum].postln;
	case 
	{ ccNum < 17 } 
	{ 
		var normVal = value.linlin(0, 127, 0, 1);
		var assign = ~ccValues[~bank][ccNum.asSymbol];
		if (assign.notNil) { ~setParameter.(normVal, assign[0], assign[1]) } 
	}
	
	{ (ccNum >= 17) && (ccNum <= 20) } // button,
	{
		// ignore the incoming value, the but value toggles bewteen 0 and 1
		~buttonStates[~bank][ccNum.asSymbol] = 1 - ~buttonStates[~bank][ccNum.asSymbol]; 
		if (~buttonFuncs[~bank][ccNum.asSymbol].notNil)
		{ ~buttonFuncs[~bank][ccNum.asSymbol].(~buttonStates[~bank][ccNum.asSymbol]) }
	}
});
)

/// Testing, I don't have a controller hooked up, so sending values with midiout

m = MIDIOut(1); // 1 = IAC bus on my mac
m.control(0, 17) // simulate pressing button with ccNum = 17
m.control(0, 18) // nothing happens, \18 is not defined

m.control(0, 1, rrand(20, 90)) // sending amp messages to the pulse synth
m.control(0, 8, rrand(20, 127)) // sending cuttoff messages to the saw synth, make sure gate is = 1