ModMatrix with NodeProxies

hey,

i have set up a ProtoDef for a modulation matrix, which dynamically creates a number of in and output busses and corresponding knobs, so you could set the amplitudes of each modulator and they are mixed and sent to a specific bus, which could be mapped to a specific param of the source. The routing is based on InFeedback. The source and modulation are using NodeProxyGui2. I have been thinking alot about how to get that right for some months now, i hope you have some ideas :slight_smile:

There are at least two things here:

1.) Im wondering if InFeeback is capable of audio rate modulation, or how could i make sure the nodes are in the right order? I would actually dont have a problem to integrate the modulator in the ProtoDef itself without the input busses.

2.) I first evaluate the modMatrix Prototype, then i evaluate .play on the modulator and after that .play on the source. The modulator is mapped to a specific param of the source via \fmod, NodeProxy.for(topEnvironment[\modMatrix].outputBusses[0]) or \fmod, topEnvironment[\modMatrix].matrixProxy.bus.subBus(0, 0). When i then hit .stop on the modulator the source is still playing with the same modulation, i would like to disable the modulation when i hit .stop for the modulator. How can i do that?

ProtoDef for the modMatrix

(
ProtoDef(\modMatrix) {

	~init = { |self|

		self.numOfMappings = self.numOfMappings;
		self.inputBusses = Order.new();

		self.numOfModulators = self.numOfModulators;
		self.outputBusses = Order.new();

		self.getMatrix;

		self.params = IdentityDictionary.new();
		self.paramViews = IdentityDictionary.new();

		self.window = Window.new(self.defName, Rect(10, 780, 320, 260)).front;
		self.window.layout = VLayout.new(
			self.makeTransportSection;
		);

		self.initFont;
		self.window.view.children.do{ |c| c.font = self.font };

		self.setUpDependencies;

		self.makeGui;

		self.midiController = MKtl(\faderfox, "faderfox_ec4");

	};

	~initFont = { |self|
		var fontSize = 14;
		self.font = Font.monospace(fontSize, bold: false, italic: false);
	};

	~setInputBusses = { |self|
		self.numOfMappings.do{ |numInput|
			var inputBus = Bus.audio(s, 1);
			self.inputBusses.put(numInput, inputBus);
		};
	};

	~setOutputBusses = { |self|
		self.numOfModulators.do{ |numOutput|
			var outputBus = Bus.audio(s, 1);
			self.outputBusses.put(numOutput, outputBus);
		};
	};

	~getMatrix = { |self|

		self.setOutputBusses;
		self.setInputBusses;

		self.matrixProxy ?? {
			self.matrixProxy = NodeProxy.audio(s, self.numOfModulators);
		};

		self.matrixProxy.source = {

			self.outputBusses.collect{ |outputBus, numOutput|

				var mods, amps, norm;

				mods = self.inputBusses.collect{ |inputBus, numInput|
					InFeedback.ar(inputBus);
				};

				amps = self.inputBusses.collect{ |inputBus, numInput|

					var key, spec;

					key = "mod%_%".format(numOutput, numInput).asSymbol;
					spec = [0.0, 1.0, \lin].asSpec;

					NamedControl.kr(key, spec: spec);

				};

				norm = (1 / amps).sum;
				mods = (mods * amps).sum * min(norm, 1);

				Out.ar(outputBus, mods);
			};

		};

		self.matrixProxy;

	};

	~nodeProxyChanged = { |self|

		self.matrixProxy.controlKeysValues.pairsDo{ |key, val|

			if(self.params[key].notNil, {

				var spec = self.params[key];
				self.paramViews[key].value_(spec.unmap(val));

			});

		};
	};

	~setUpDependencies = { |self|

		var nodeProxyChangedFunc = { self.nodeProxyChanged }.defer;

		self.matrixProxy.addDependant(nodeProxyChangedFunc);

		self.window.onClose_{

			self.matrixProxy.removeDependant(nodeProxyChangedFunc);
			self.matrixProxy.clear;

			self.outputBusses.do{ |outputBus| outputBus.free };
			self.inputBusses.do{ |inputBus| inputBus.free };

			self.outputBusses.clear;
			self.inputBusses.clear;

			self.params.clear;
			self.paramViews.clear;

		};

	};

	~makeParamSection = { |self|

		self.params.clear;

		self.matrixProxy.controlKeysValues.pairsDo{ |key, val|

			var spec;

			spec = (self.matrixProxy.specs.at(key) ?? { Spec.specs.at(key) }).asSpec;

			self.params.put(key, spec);

		};

	};

	~makeParamViews = { |self|

		self.paramViews.clear;
		self.makeParamSection;

		self.params.sortedKeysValuesDo{ |key, spec|

			var paramVal, knob;

			paramVal = self.matrixProxy.get(key);

			knob = Knob.new()
			.value_(spec.unmap(paramVal))
			.action_{ |el|
				self.matrixProxy.set(key, spec.map(el.value));
			};

			self.paramViews.put(key, knob);

		};

		self.paramViews;

	};

	~makeParamLayout = { |self|

		var view, layout, rowsOfKnobs, modTexts, paramTexts;

		view = View.new().layout_(VLayout.new());

		self.makeParamViews;

		paramTexts = self.numOfMappings.collect{ |numOutput|
			StaticText.new().string_("param_%".format(numOutput));
		};

		modTexts = self.numOfModulators.collect{ |numInput|
			StaticText.new().string_("mod_%".format(numInput));
		};

		rowsOfKnobs = self.paramViews.atAll(self.paramViews.order).clump(self.numOfModulators);

		layout = GridLayout.rows(*
			[[nil] ++ modTexts] ++
			self.numOfMappings.collect{ |i| [paramTexts[i]] ++ rowsOfKnobs[i]};
		);

		view.layout.add(layout);

		view.children.do{ |c| c.font = self.font };

		view;

	};

	~paramsDo = { |self, func|

		self.params.keysValuesDo{ |key, spec|

			var paramVal;

			paramVal = self.matrixProxy.get(key);

			self.matrixProxy.set(key, func.(paramVal, spec));
		};

	};

	~randomize = { |self, randmin = 0.0, randmax = 1.0|
		self.paramsDo{ |val, spec|
			spec.map(rrand(randmin, randmax));
		};
	};

	~vary = { |self, deviation = 0.1|
		self.paramsDo{ |val, spec|
			spec.map((spec.unmap(val) + 0.0.gauss(deviation)).clip(0, 1));
		};
	};

	~defaults = { |self|
		self.paramsDo{ |val, spec|
			spec.default;
		};
	};

	~makeTransportSection = { |self|

		var popup;

		popup = PopUpMenu.new()
		.allowsReselection_(true)
		.items_(#[
			"defaults",
			"randomize parameters",
			"vary parameters",
			"document",
			"post",
		])
		.action_({ |obj|
			switch(obj.value,
				0, { self.defaults },
				1, { self.randomize },
				2, { self.vary },
				3, { self.matrixProxy.document },
				4, { self.matrixProxy.asCode.postln },
			)
		})
		.keyDownAction_({ |obj, char|
			if(char == Char.ret, {
				obj.doAction
			})
		})
		.canFocus_(true)
		.fixedWidth_(25);

		HLayout.new(
			[popup, align: \right]
		);

	};

	~makeGui = { |self|

		if(self.paramLayout.notNil, { self.paramLayout.remove });
		self.paramLayout = self.makeParamLayout;
		self.window.layout.add(self.paramLayout);

		self.window;

	};

	~mapMidi = { |self|

		self.unmapMidi;

		self.params.sortedKeysValuesDo{ |key, spec, i|

			var paramVal;

			paramVal = self.matrixProxy.get(key);

			self.midiController.elAt(\GR01, \kn, i)
			.value_(spec.unmap(paramVal))
			.action_{ |el|
				{ self.matrixProxy.set(key, spec.map(el.value)) }.defer
			};

		};

	};

	~unmapMidi = { |self|
		self.midiController.resetActions;
	};

}
)

ProtoDef for getting NodeProxies from SynthDefs


(
ProtoDef(\makeNodeProxy) { |synthDefName, initArgs=#[], bounds, excludeParams=#[], ignoreParams=#[], numChannels = 2|

	~init = { |self|

		self.setNodeProxy;
		self.makeGui;

	};

	~setNodeProxy = { |self|

		self.synthDef = try{ SynthDescLib.global[self.synthDefName].def } ?? {
			Error("SynthDef '%' not found".format(self.synthDefName)).throw
		};

		self.nodeProxy = NodeProxy.audio(s, self.numChannels);
		self.nodeProxy.prime(self.synthDef).set(*self.initArgs);

	};

	~nodeProxyGui = { |self|
		NodeProxyGui2(self.nodeProxy, show: false)
		.excludeParams_(self.excludeParams)
		.ignoreParams_(self.ignoreParams)
	};

	~makeGui = { |self|

		if(self.window.notNil) { self.window.close };
		self.window = Window.new(self.synthDef.name, self.bounds).front;
		self.window.layout = VLayout(self.nodeProxyGui);

		self.window;

	};
};
)

test SynthDefs:

(
SynthDef(\source, {
	var tFreq, trig, env, freq, fmod, sig;
	tFreq = \tFreq.kr(4, spec: ControlSpec(1, 12));
	trig = Impulse.ar(tFreq);
	env = Env.perc(0.1, 0.2).ar(0, trig);
	freq = \freq.kr(440, spec: ControlSpec(20, 1000));
	sig = SinOsc.ar(freq + (freq * \fmod.ar(0).poll * \fmIndex.kr(2, spec: ControlSpec(0, 5))));
	sig = sig * env;
	sig = Pan2.ar(sig, 0);
	sig = sig * \amp.kr(-35, spec: ControlSpec(-35, -15, \lin, 1)).dbamp;
	Out.ar(\out.kr(0), sig);
}).add;

SynthDef(\modulation, {

	var flux, rate, phase;
	var phaseDivA, phaseDivB, phaseDivC;
	var modA, modB, modC;

	flux = LFDNoise3.ar(\fluxMF.kr(1, spec: ControlSpec(0.125, 50)));
	flux = 2 ** (flux * \fluxMD.kr(0, spec: ControlSpec(0, 3)));

	rate = \rate.kr(0.5, spec: ControlSpec(0.1, 5));
	rate = rate * flux;

	phase = Phasor.ar(DC.ar(0), rate * SampleDur.ir);

	phaseDivA = (phase * \modDivA.kr(1, spec: ControlSpec(1, 16))).wrap(0, 1);
	phaseDivB = (phase * \modDivB.kr(3, spec: ControlSpec(1, 16))).wrap(0, 1);
	phaseDivC = (phase * \modDivC.kr(5, spec: ControlSpec(1, 16))).wrap(0, 1);

	modA = sin(phaseDivA * 2pi);
	modB = sin(phaseDivB * 2pi);
	modC = sin(phaseDivC * 2pi);

	Out.ar(\modOutA.kr(0), modA);
	Out.ar(\modOutB.kr(0), modB);
	Out.ar(\modOutC.kr(0), modC);
}).add;
)

evaluate the ProtoDefs and test the setup:

// setup Busses
~modMatrix = Prototype(\modMatrix) { ~numOfMappings = 4; ~numOfModulators = 3 };

// set param value
~modMatrix.matrixProxy.set(\mod0_0, 1);
~modMatrix.matrixProxy.set(\mod0_1, 1);
~modMatrix.matrixProxy.set(\mod0_2, 1);

// map MIDI
~modMatrix.mapMidi;

// unmap MIDI
~modMatrix.unmapMidi;

/////////////////////////////////////////////////////

// make nodeProxy and map source to outputbusses of modMatrix

(
~source = Prototype(\makeNodeProxy) {

	~synthDefName = \source;

	~initArgs = [
		\fmod, NodeProxy.for(topEnvironment[\modMatrix].outputBusses[0])
		//\fmod, topEnvironment[\modMatrix].matrixProxy.bus.subBus(0, 0)
	];

	~bounds = Rect(10, 430, 440, 300);

	~excludeParams = [];

	~ignoreParams = [\amp];
};
)

/////////////////////////////////////////////////////

// make nodeProxy and map modulators to inputBusses of modMatrix

(
~modulation = Prototype(\makeNodeProxy) {

	~synthDefName = \modulation;

	~initArgs = [
		\modOutA, topEnvironment[\modMatrix].inputBusses[0],
		\modOutB, topEnvironment[\modMatrix].inputBusses[1],
		\modOutC, topEnvironment[\modMatrix].inputBusses[2]
	];

	~bounds = Rect(10, 70, 440, 320);

	~excludeParams = [\modOutA, \modOutB, \modOutC];

	~ignoreParams = [];
};
)

3 Likes

I think the second bullet point boils down to this:

(
~source = NodeProxy.audio(s, 2).source_({
	SinOsc.ar(200) !2 * \amp.ar(0.5);
});
)

(
~mod = NodeProxy.audio(s, 1).source_({
    SinOsc.ar(5) * 0.5 + 0.5;
});
)

~source.set(\amp, ~mod);
~source.play;

// cant stop the modulator
~mod.stop;

After setting the modulation, you cant stop it.
Whats different from the mod matrix is that you dont have to hit play on the modulator if you connect the nodeproxies directly. I think i could live with that.
But having to hit play and then not being able to stop doesnt make any sense.

I actually dont know how this could be implemented, when wanting to access the different output busses of the nodeproxy. This seems to work without having to .play the modulator explicitily, but dont know why thats currently not the case for my implementation, which follows the same model:

(
~source = NodeProxy.audio(s, 2).source_({
	SinOsc.ar(200) !2 * \amp.ar(0.5);
});
)

(
~mod = NodeProxy.audio(s, 3).source_({
    SinOsc.ar(5!3) * 0.5 + 0.5;
});
)

~source.set(\amp, ~mod.bus.subBus(0, 0).asMap);
~source.play;

The problem here is that this would then need a custom gui for the modulation, because the start stop button in nodeproxygui2 is obsolete. Thats really frustrating.

1 Like

Ive also tried to figure out another attempt using .reshaping(\elastic), where you could dynamically change the number of modulators after initializing the Prototype and keep the modulation amounts , but unfortunately the busses are changing as well otherwise this would be really cool. But i think the first approach is better, i wouldnt change the modulators anyway.

(
ProtoDef(\modMatrix) { |numOfMappings|

	~init = { |self|

		self.params = IdentityDictionary.new();
		self.paramViews = IdentityDictionary.new();

		self.window = Window.new(self.defName, Rect(10, 780, 320, 260));
		self.window.layout = VLayout.new(
			self.makeTransportSection;
		);

		self.initFont;
		self.window.view.children.do{ |c| c.font = self.font };

		self.midiController = MKtl(\faderfox, "faderfox_ec4");

	};

	~initFont = { |self|
		var fontSize = 14;
		self.font = Font.monospace(fontSize, bold: false, italic: false);
	};

	~setModulators = { |self ...mods|

		self.modulators.clear;

		self.modulators ?? { self.modulators = List[] };

		self.modulators = mods.collect { |mod|
			case
			{ mod.isKindOf(NodeProxy)} { mod }
			{ mod.isKindOf(Function) } { NodeProxy.audio(s, 1).source_(mod) }
			{ Error("modMatrix: unsupported type %").format(mod)}
		};

		self.numOfModulators = self.modulators.size;

		self.rebuildProxy;
	};

	~rebuildProxy = { |self|

		self.nodeProxy ?? {
			self.nodeProxy = NodeProxy.audio(s, self.numOfModulators).reshaping_(\elastic);
			self.setUpDependencies;
		};

		// Store all current control keys
		if(self.nodeProxy.notNil) {

			var validKeys = Set.new;

			// Mark which keys should be kept
			self.numOfMappings.do { |outputIdx|

				self.numOfModulators.do { |inputIdx|
					var key = "mod%_%".format(outputIdx, inputIdx).asSymbol;
					validKeys.add(key);
				};

			};

			// Unset any keys not in our valid set
			self.nodeProxy.controlKeys.do { |key|
				if(validKeys.includes(key).not) {
					self.nodeProxy.unset(key);
				};
			};
		};

		//self.nodeProxy.clear;

		self.nodeProxy.source = {

			self.numOfModulators.collect{ |numOutput|

				var amps, norm;

				amps = self.modulators.collect{ |mod, numInput|

					var key, spec;

					key = "mod%_%".format(numOutput, numInput).asSymbol;
					spec = [0.0, 1.0, \lin].asSpec;

					NamedControl.kr(key, spec: spec);

				};

				norm = (1 / amps).sum;

				(self.modulators * amps).sum * min(norm, 1);

			};

		};

		self.nodeProxy.changed(\rebuild);

	};

	~nodeProxyChanged = { |self, what, args|

		case

		{ what == \set } {

			args.pairsDo { |paramKey, val|
				if(self.params[paramKey].notNil, {
					var spec = self.params[paramKey];
					self.paramViews[paramKey].value_(spec.unmap(val));
				});
			};

		}

		{ what == \rebuild } {

			self.makeParamSection;
		};


	};

	~setUpDependencies = { |self|

		var nodeProxyChangedFunc = { |obj ...args| self.nodeProxyChanged(*args) }.defer;

		self.nodeProxy.addDependant(nodeProxyChangedFunc);

		self.window.onClose_{

			self.nodeProxy.stop;

			self.nodeProxy.removeDependant(nodeProxyChangedFunc);
			self.nodeProxy.free;

			self.params.clear;
			self.paramViews.clear;

		};

	};

	~makeParamSection = { |self|

		self.params.clear;

		self.nodeProxy.controlKeysValues.pairsDo{ |key, val|

			var spec;

			spec = (self.nodeProxy.specs.at(key) ?? { Spec.specs.at(key) }).asSpec;

			self.params.put(key, spec);

		};

		if(self.paramLayout.notNil) { self.paramLayout.remove };
		self.paramLayout = self.makeParamLayout;
		self.window.layout.add(self.paramLayout);

	};

	~makeParamViews = { |self|

		self.paramViews.clear;

		self.params.sortedKeysValuesDo{ |key, spec|

			var paramVal, knob;

			paramVal = self.nodeProxy.get(key);

			knob = Knob.new()
			.value_(spec.unmap(paramVal))
			.action_{ |el|
				self.nodeProxy.set(key, spec.map(el.value));
			};

			self.paramViews.put(key, knob);

		};

		self.paramViews;

	};

	~makeParamLayout = { |self|

		var view, layout, rowsOfKnobs, modTexts, paramTexts;

		view = View.new().layout_(VLayout.new());

		self.makeParamViews;

		paramTexts = self.numOfMappings.collect{ |numOutput|
			StaticText.new().string_("_param_%".format(numOutput));
		};

		modTexts = self.numOfModulators.collect{ |numInput|
			StaticText.new().string_("_mod_%".format(numInput));
		};
		
		if(self.paramViews.order.notNil) {
			
			rowsOfKnobs = self.paramViews
			.atAll(self.paramViews.order)
			.clump(self.numOfModulators);
			
			layout = GridLayout.rows(*
				[[nil] ++ modTexts] ++
				self.numOfMappings.collect{ |i|
					[ paramTexts[i] ] ++ rowsOfKnobs[i]
				};
			);
			
		};

		view.layout.add(layout);

		view.children.do{ |c| c.font = self.font };

		view;
	};

	~paramsDo = { |self, func|

		self.params.keysValuesDo{ |key, spec|

			var paramVal;

			paramVal = self.nodeProxy.get(key);

			self.nodeProxy.set(key, func.(paramVal, spec));
		};

	};

	~randomize = { |self, randmin = 0.0, randmax = 1.0|
		self.paramsDo{ |val, spec|
			spec.map(rrand(randmin, randmax));
		};
	};

	~vary = { |self, deviation = 0.1|
		self.paramsDo{ |val, spec|
			spec.map((spec.unmap(val) + 0.0.gauss(deviation)).clip(0, 1));
		};
	};

	~defaults = { |self|
		self.paramsDo{ |val, spec|
			spec.default;
		};
	};

	~makeTransportSection = { |self|

		var popup;

		popup = PopUpMenu.new()
		.allowsReselection_(true)
		.items_(#[
			"defaults",
			"randomize parameters",
			"vary parameters",
			"document",
			"post",
		])
		.action_({ |obj|
			switch(obj.value,
				0, { self.defaults },
				1, { self.randomize },
				2, { self.vary },
				3, { self.nodeProxy.document },
				4, { self.nodeProxy.asCode.postln },
			)
		})
		.keyDownAction_({ |obj, char|
			if(char == Char.ret, {
				obj.doAction
			})
		})
		.canFocus_(true)
		.fixedWidth_(25);

		HLayout.new(
			[popup, align: \right]
		);

	};

	~makeGui = { |self|

		self.makeParamSection;
		self.window.front;

	};

	~mapMidi = { |self|

		self.unmapMidi;

		self.params.sortedKeysValuesDo{ |key, spec, i|
			var paramVal;

			paramVal = self.nodeProxy.get(key);

			self.midiController.elAt(\GR01, \kn, i)
			.value_(spec.unmap(paramVal))
			.action_{ |el|
				{ self.nodeProxy.set(key, spec.map(el.value)) }.defer
			};
		};

	};

	~unmapMidi = { |self|
		self.midiController.resetActions;
	};
};
)

Try out here:

// setup matrix
(
~modMatrix = Prototype(\modMatrix) {

	~numOfMappings = 3;

};
)

(
~modMatrix.setModulators
{ SinOsc.ar(10) }
{ SinOsc.ar(50) }
{ SinOsc.ar(100) }
{ SinOsc.ar(200) }
)

// make GUI
~modMatrix.makeGui;

// set param values
~modMatrix.nodeProxy.set(\mod0_0, 1);
~modMatrix.nodeProxy.set(\mod0_1, 1);
~modMatrix.nodeProxy.set(\mod0_2, 1);
~modMatrix.nodeProxy.set(\mod0_3, 1);

// set with a different amount of modulators
(
~modMatrix.setModulators
{ SinOsc.ar(110) }
{ SinOsc.ar(220) }
{ SinOsc.ar(440) }
)

// get param value
~modMatrix.nodeProxy.get(\mod0_0);
~modMatrix.nodeProxy.get(\mod0_1);
~modMatrix.nodeProxy.get(\mod0_2);
~modMatrix.nodeProxy.get(\mod0_3);
1 Like

okay, i think ive made some subtle mistakes. Currently trying put everything together. update soon.