Convert a class to MVC

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;
1 Like

I made something with SimpleController.
I think it’s respect the MVC design pattern.
It’s working so far, but I still asking me if it’s the good way of doing things.

my new Class:

MixerLLmvc { // 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
	controller,
	<>model;

	// 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;
			controller.remove;
		};

		this.prCreateChannelsView;
		this.prCreateMasterView;
		this.prInitController;
	}

	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;
					{
						model[channelName][controlName] = midiControl;
						model.changed(\update, [channelName, controlName]);
					}.defer;
				}
			);

		}, midiCc, mixerMidiChan);
	}

	prInitController {
		controller = SimpleController(model);
		controller.put(\update, { | theChanger, what, args |
			var channelName, controlName, value;
			channelName = args[0];
			controlName = args[1];
			value = theChanger[channelName][controlName];

			channel[channelName][\Synth].set(controlName, value);
			channel[channelName][\Control][controlName].value = value;
		});
		controller.put(\setSynth, { | theChanger, what, args |
			var channelName, controlName, value;
			channelName = args[0];
			controlName = args[1];
			value = theChanger[channelName][controlName];

			channel[channelName][\Synth].set(controlName, value);
		});
	}

	prCreateMasterView {
		model[\master] = IdentityDictionary.new;
		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;
			model[\master][\lpfreq] = lpfreq.value;
			model.changed(\setSynth, [\master, \lpfreq]);
		}, initAction: true, 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;
			model[\master][\hpfreq] = hpfreq.value;
			model.changed(\setSynth, [\master, \hpfreq]);
		}, initAction: true, 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;
			model[\master][\level] = level.value;
			model.changed(\setSynth, [\master, \level]);
		}, initAction: true, layout: 'vert', margin: 39@0).setColors(stringColor: Color.white);
	}

	prCreateChannelsView {
		view = IdentityDictionary.new;
		model = IdentityDictionary.new;

		channelNumber.do({ arg i;
			var channelName;
			channelName = (\channel ++ i).asSymbol;
			model[channelName] = IdentityDictionary.new;
			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][\lpfreq] = EZKnob(view[channelName], 124@70, "lpfreq", ControlSpec(20, 20000, step: 1, default: 20000), { arg lpfreq;
				model[channelName][\lpfreq] = lpfreq.value;
				model.changed(\setSynth, [channelName, \lpfreq]);
				channel[channelName][\Control][\activeLpf] = false;
			}, initAction: true, 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][(\band ++ j ++ \freq).asSymbol] = EZKnob(view[channelName], 35@70, "freq" ++ j, ControlSpec(20, 20000, step: 1, default: default), { arg freq;
					model[channelName][(\band ++ j ++ \freq).asSymbol] = freq.value;
					model.changed(\setSynth, [channelName, (\band ++ j ++ \freq).asSymbol]);
					channel[channelName][\Control][(\activeBand ++ j ++ \Freq).asSymbol] = false;
				}, initAction: true);

				// rq
				channel[channelName][\Control][(\band ++ j ++ \rq).asSymbol] = EZKnob(view[channelName], 35@70, "rq" ++ j, ControlSpec(0, 1, step: 0.01, default: 1), { arg rq;
					model[channelName][(\band ++ j ++ \rq).asSymbol] = rq.value;
					model.changed(\setSynth, [channelName, (\band ++ j ++ \rq).asSymbol]);
					channel[channelName][\Control][(\activeBand ++ j ++ \Rq).asSymbol] = false;
				}, initAction: true);

				// db
				channel[channelName][\Control][(\band ++ j ++ \db).asSymbol] = EZKnob(view[channelName], 35@70, "db" ++ j, ControlSpec(-60, 60, step: 1, default: 0), { arg db;
					model[channelName][(\band ++ j ++ \db).asSymbol] = db.value;
					model.changed(\setSynth, [channelName, (\band ++ j ++ \db).asSymbol]);
					channel[channelName][\Control][(\activeBand ++ j ++ \Db).asSymbol] = false;
				}, initAction: true);

				view[channelName].decorator.nextLine;
			});

			// hpf
			channel[channelName][\Control][\hpfreq] = EZKnob(view[channelName], 120@70, "hpfreq", ControlSpec(20, 20000, step: 1, default: 20), { arg hpfreq;
				model[channelName][\hpfreq] = hpfreq.value;
				model.changed(\setSynth, [channelName, \hpfreq]);
				channel[channelName][\Control][\activeHpf] = false;
			}, initAction: true, 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][(\aux ++ j).asSymbol] = EZKnob(view[channelName], 35@70, "aux" ++ j, ControlSpec(0, 1, step: 0.01, default: 0), { arg aux;
					model[channelName][(\aux ++ j).asSymbol] = aux.value;
					model.changed(\setSynth, [channelName, (\aux ++ j).asSymbol]);
					channel[channelName][\Control][(\activeAux ++ j).asSymbol] = false;
				}, initAction: true)
				.setColors(background: color);

				if (j == 1, {
					StaticText(view[channelName], 35@65)
					.string_("Send")
					.align_(\center);
				});
			});

			// mute
			model[channelName][\mute] = 1; // init
			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;
				model[channelName][\mute] = mute.value.linlin(0, 1, 1, 0);
				model.changed(\setSynth, [channelName, \mute]);
			};

			// pan
			channel[channelName][\Control][\pan] = EZSlider(view[channelName], 112@60, "pan", ControlSpec(-1, 1, step: 0.01, default: 0), { arg pan;
				model[channelName][\pan] = pan.value;
				model.changed(\setSynth, [channelName, \pan]);
				channel[channelName][\Control][\activePan] = false;
			}, initAction: true, layout: 'vert');

			view[channelName].decorator.nextLine;

			// level
			channel[channelName][\Control][\level] = EZSlider(view[channelName], 110@160, "level", ControlSpec(0, 1, step: 0.01, default: 0.5), { arg level;
				model[channelName][\level] = level.value;
				model.changed(\setSynth, [channelName, \level]);
				channel[channelName][\Control][\activeLevel] = false;
			}, initAction: true, 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;
				model[channelName][\delaytime] = delay.value;
				model.changed(\setSynth, [channelName, \delaytime]);
			}, initAction: true);
		});
	}

}

use it this way:

(
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);
)

// MVC Style
v = MixerLLmvc(s, 5, ~aux1, ~aux2, 0, ~mixGroup);

v.createGui;
v.midiMapping;

(
v.model[\channel0][\lpfreq] = rrand(20, 20000);
v.model.changed(\update, [\channel0, \lpfreq]);
)

(
s.bind { ~flanging = Synth(\flanging, [\in, ~aux1, \out, v.channelBus[3]]); };
v.view[\channel3].background_(~aux1Color);
s.bind { ~greyhole = Synth(\greyHole, [\in, ~aux2, \out, v.channelBus[4]]); };
v.view[\channel4].background_(~aux2Color);
)

(
Pdef(\LL,
    Pbind(
        \instrument, \snapkick,
        \dur, Pseq([0.5, 0.75], inf),
        // \out, m.channelBus[0]
        \out, v.channelBus[0]
    )
).play;
)

Pdef(\LL).stop;

I haven’t quite traced through all of the code, but it doesn’t look to me like classical MVC.

The first thing I would do is reduce the scope of each MVC chain. The point of MVC is to maintain simple relationships. One complex model for an entire mixing console → a complex controller → a complex view is quite different from typical MVC.

The individual controls of a mixer channel would be a good candidate for MVC.

Let’s say you represent the volume control with an object that holds the value, syncs it to the server (say, by a control bus), and broadcasts updates through the changed mechanism. This is a model. (Note – due to lack of time, I’m not testing this code extensively.)

MixerControlValue {
	var <value, <spec, <bus;

	*new { |value, spec, busOrServer|
		if(busOrServer.isKindOf(Bus).not) {
			busOrServer = Bus.control(busOrServer ?? { Server.default }, 1);
		};
		^super.newCopyArgs(value, spec.asSpec, busOrServer)
	}

	// don't forget a destructor, to clean up resources
	free {
		bus.free;
		this.changed(\didFree);
	}

	value_ { |newValue|
		value = newValue;
		bus.set(value);
		this.changed(\value, value)
	}

	// for GUI
	value01 { ^spec.unmap(value) }
}

And in the GUI, you want to represent this with a slider. That’s the View. Let’s use EZSlider to get the number box for free. (Since the class already exists, there’s no need to reproduce it here.)

Assuming, then, that the View is a general GUI class, you need an object to link the model to the view. That’s the controller. SimpleController is often fine, though if you run into trouble with it, you might create a bespoke controller class for this specific situation.

(
s.waitForBoot {
	var levelControl = MixerControlValue(-12, [-90, 6]);
	
	var levelSlider = EZSlider(nil, Rect(800, 200, 70, 200),
		"demo", [-90, 6], { |view|
			levelControl.value = view.value
		},
		levelControl.value,
		layout: \vert
	);
	
	var levelCtl = SimpleController(levelControl)
	.put(\value, { |obj, what, value|
		levelSlider.value = value
	})
	.put(\didFree, { levelCtl.remove; levelSlider.view.close; });
	
	levelSlider.view.front;
	
	c = levelControl;  // just for demo purposes
}
)

// we can use the control-value bus as we like:
// probably should lag it too, but whatever
a = { |level| (SinOsc.ar(220) * level.dbamp).dup }.play(args: [level: c.bus.asMap]);

// change value programmatically, GUI updates
c.value = -10;

a.free;

c.free;
c.dependants;  // empty -- garbage is cleaned up

Then you would build a class for a single mixer channel, which maintains multiple control-values, and another class for a single channel’s GUI.

Out of these, you can build an entire mixing console.

Designing the objects in a modular way will help you later. Making one gigantic class to represent all of the behavior of the entire mixing console seems easier now, but it will cause you problems down the road (the “god-object” anti-pattern). It’s much better to split off a single channel into its own class, and individual controls of that channel into a class.

hjh

1 Like

A design principle that you might keep in mind - from your code, I see you very close to this but maybe muddying the waters as well too…

It can sometimes be really helpful to start first by designing your data structure - that is, a representation of ALL of the state of your mixer - completely independent of Sliders, UI, server processes, etc - only the data. Keep it simple and easy to understand - IdentityDictionary and Event are good for this:

~state = (
  channels: [
    (  
      mute: false,
      solo: false,
      volume: 1.0
      pan: 0.0
    )
  ]
)

This is now the one truth about the state of your mixer - no need to wrap this in a class, or make this more sophisticated than it needs to be initially. You can of course make your state more modular, make sophisticated abstractions on top of it, etc. - but these kinds of things should be considered refactoring or clean-up tasks to do only once you’ve (a) gotten the basic functionality working, and (b) identified that you have a real problem to solve. As long as your data structure is clear and readable, it’s very easy to make these changes later on if needed.

I would caution against over-engineering here - probably your mixer data structure is something you could sit down for an hour and sketch out on a piece of paper. If you make a mistake or need to re-design in the future, it cheap enough that you could easily just re-make it from scratch.

Keeping this in mind, you basically need one or more functions that map:
(1) some value in ~state to…
(2) some method in your UI or server structure (e.g. a Synth or Bus).

As an example:

{ ~ui.channels[2].volumeSlider.value = ~state.channels[2].volume }

These would be the kinds of functions you attach to a SimpleController to update UI values. There’s a lot of flexibility in how you structure these functions. You can write one function that sets the state of an entire mixer channel UI all at once - you can write one function per individual UI slider or component - or you can have a single function that updates ALL the state in your UI. Again, don’t over-complicate it - you probably don’t need the ability to dispatch granular updates for every single data item and every single UI widget.

There are good design reasons for each of these choices, but you probably won’t know the right design until you’ve mostly finished building your mixer :slight_smile: - and since these functions map between (a) your data structure and (b) your UI (BOTH of which have a high potential for change as you build your mixer), it’s important that these be very low-complexity, easy to write, and isolated from other code.

With this in mind, a good rule of thumb would be - if you re-built your ~state from scratch with a different structure, how easy would it be to re-write all these mapping functions? Is it fifteen minutes of work, an afternoon project or a week long slog? Are they in one place or spread out across your code-base? Would you be able to immediately find all “update” functions, delete them, and start re-writing - or are they tangled up with other code in intricate ways? This, for example, can be a very good reason to use a single “update everything” function. As a chunk of code, it’s less maintainable and less modular - but, it’s centralized and readable, and you can always break it down into smaller pieces later.

A final note - it can be pretty critical to treat Server objects like Buses and Synths as if they were UI: that is, as optional things that may or may not exist, and connect to your ~state. Keeping state on the server, in apart from a few cases, is a bad idea. Your code should work without a UI showing, and without a server running.

2 Likes

Thanks to both of you for taking the time to answer me and thank you for your advices.