Universal MLPRegressor for preset interpolation

hey, after attending a FluCoMa workshop at Standford in 2023, i have spent some time writing a universal Prototype for the MLPRegressor, so you could plugin your abitrary SynthDef and navigate a 2D space for preset interpolation.
The Prototype is based on @elgiano awesome ProtoDef class. You additionally need the awesome NodeProxyGui2. The implementation works for single and array NamedControls and additionally has the possibility to use a MIDI Controller or an LFO to navigate the 2D space.

Currently the additional LFO to navigate the 2D space needs an additional Bus on top of the already existing Buffer which is necessary for the MLPRegressor and is a bit unfortunate imo.
Additional things i would like to implement and im not really sure how to do that are:

  • instead of placing individual points on the 2D grid i would like to use a bunch of Nodeproxy presets and organize them on a grid with FluidGrid
  • im not sure if the current LFO is the best way to navigate the 2D space. There are additional ways for surface scanning which could be interesting and i havent implemented them yet. But maybe instead of using an LFO it would be possible to create a language side attempt with different functions via SkipJack to get rid of the additional Bus. Or maybe rethink the implementation of the MLPRegressor and its needs for Buffers, couldnt the MLPregressor be implemented with Busses instead?
  • currently every single param inside ~startMLP needs its own Changed Ugen (e.g. the more params you have the more ugens you need). I was wondering if FluidBufToKr is the best option here.

After spending an unimaginable amount of hours on this, I really hope this could be a community effort :slight_smile:

Here is the Prototype:

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

	~init = { |self|

		self.getNodeProxy;

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

		self.dsXY = FluidDataSet(s);
		self.dsParams = FluidDataSet(s);

		self.bufXY = Buffer.alloc(s, 2);
		self.bufParams = Buffer.alloc(s, self.getNumParamVals);

		self.busXY = Bus.control(s, 2);

		self.mlp = FluidMLPRegressor(
			server: s,
			hiddenLayers: [7],
			activation: FluidMLPRegressor.sigmoid,
			outputActivation: FluidMLPRegressor.sigmoid,
			maxIter: 1000,
			learnRate: 0.1,
			batchSize: 1,
			validation: 0,
		);

		self.window = Window.new(self.synthDefName, Rect(10, 50, 440, 990)).front;
		self.window.layout = VLayout.new();

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

		self.setUpDependencies;
		self.makeParamSection;

	};

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

	~getNodeProxy = { |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)
	};

	~getNumParamVals = { |self|

		var paramVals;

		paramVals = self.nodeProxy
		.getKeysValues(except: self.excludeParams ++ self.ignoreParams)
		.flop[1];

		paramVals.collect{ |val, i|

			case
			{ val.isNumber } { 1 }
			{ val.isArray} { val.size };

		}.sum;

	};

	~setUpDependencies = { |self|

		var dataChangedFunc = { |obj ...args| self.dataChanged(*args) };

		self.addDependant(dataChangedFunc);

		self.window.onClose_{

			self.stopLFO;

			self.nodeProxy.stop;
			self.nodeProxy.free;

			self.unmapMidi;

			self.removeDependant(dataChangedFunc);
			self.bufXY.free;
			self.bufParams.free;

			self.busXY.free;

			self.params.clear;
			self.paramValsUni.clear;

			self.stopMLP;
			self.mlp.clear;

			self.dsXY.clear;
			self.dsParams.clear;

		};

	};

	// notify change
	~setData = { |self, values|

		self.changed(\slider, *values);
		self.changed(\buffer, *values);

	};

	// dataChanged is called when values change
	~dataChanged = { |self, what ...args|

		case
		{ what == \slider } {

			self.bufXY.setn(0, args);

		}
		{ what == \buffer } {

			self.xySlider.setXY(*args);

		}
		{ what == \controller } {

			var updateSliderFunc, index, value;

			index = args[0];
			value = args[1];

			self.bufXY.set(index, value);

			updateSliderFunc = {
				switch(index,
					0, { self.xySlider.x_(value) },
					1, { self.xySlider.y_(value) }
				);
			};

			{ updateSliderFunc.() }.defer;

		};

	};

	~startLFO = { |self|

		var limitUpdateRate = 0.01;

		self.stopLFO;

		self.lfoSynth = {
			var reset = \reset.tr(0);
			var rate = \rate.kr(0.1) * (1 - \freeze.kr(0));
			var phase = Phasor.kr(reset, rate * ControlDur.ir);
			var sig = SinOsc.kr(DC.kr(0), phase + [0.0, 0.25] * 2pi);
			sig = (sig * \radius.kr(1)).linlin(-1, 1, 0, 1);
			BufWr.kr(sig[0], self.bufXY, 0);
			BufWr.kr(sig[1], self.bufXY, 1);
			sig;
		}.play(outbus: self.busXY);

		SkipJack.new(
			updateFunc: { self.changed(\buffer, *self.busXY.getnSynchronous(2)) },
			dt: limitUpdateRate,
			stopTest: { self.window.isClosed or: self.lfoSynth.isNil },
			clock: AppClock
		);

	};

	~stopLFO = { |self|
		self.lfoSynth !? { self.lfoSynth.free; self.lfoSynth = nil; };
	};

	~resetLFO = { |self|
		self.lfoSynth !? { self.lfoSynth.set(\reset, 1); };
	};

	~pauseLFO = { |self|
		self.lfoSynth !? { self.lfoSynth.set(\freeze, 1); };
	};

	~resumeLFO = { |self|
		self.lfoSynth !? { self.lfoSynth.set(\freeze, 0); };
	};

	~trainMLP = { |self, train|

		self.trainTask = self.trainTask ?? {

			Task {

				loop {

					var condition = CondVar.new;
					var done = false;

					self.mlp.fit(self.dsXY, self.dsParams) { |loss|
						done = true;
						condition.signalOne;
						"> Training done. Loss: %".format(loss).postln;
					};

					condition.wait { done };

					0.1.wait;
				};

			};

		};

		if (train) {
			self.trainTask.play;
		} {
			self.trainTask.pause;
		};

	};

	~startMLP = { |self|

		self.stopMLP;

		self.mlpSynth = {

			var xyData, xyTrig, paramValUni, paramTrig;

			// when data in xy buffer has changed, ...
			xyData = FluidBufToKr.kr(self.bufXY);
			xyTrig = Changed.kr(xyData.round(0.01)).sum;

			// ... trigger MLP ...
			self.mlp.kr(xyTrig, self.bufXY, self.bufParams);

			// ... to get associated param value from parameter buffer ...
			paramValUni = FluidBufToKr.kr(self.bufParams);
			paramTrig = Changed.kr(paramValUni.round(0.01)).sum;

			// ... and send OSC message to update NodeProxy!
			SendReply.kr(paramTrig, "/paramsChanged", paramValUni);

			Silent.ar;
		}.play;

		self.responder = OSCFunc({ |msg|
			var paramValsUniOSC = msg.drop(3);
			self.setParamVals(paramValsUniOSC);
		}, "/paramsChanged", argTemplate:[self.mlpSynth.nodeID]);

	};

	~stopMLP = { |self|

		self.mlpSynth !? { self.mlpSynth.free; self.mlpSynth = nil };
		self.responder !? { self.responder.free; self.responder = nil };

	};

	~makeParamSection = { |self|

		self.params.clear;

		self.nodeProxy
		.controlKeysValues(except: self.nodeProxy.internalKeys ++ self.excludeParams ++ self.ignoreParams)
		.pairsDo{ |key, val|

			var spec;

			spec = case

			{ val.isNumber } {
				(self.nodeProxy.specs.at(key) ?? { Spec.specs.at(key) }).asSpec;
			}
			{ val.isArray } {
				(self.nodeProxy.specs.at(key) ?? { Spec.specs.at(key) }).asSpec.dup(val.size);
			};

			self.params.put(key, spec);
		};

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

	};

	~getParamValsUni = { |self|

		self.paramValsUni.clear;

		self.params.sortedKeysValuesDo{ |key, spec|

			var paramVal;

			paramVal = self.nodeProxy.get(key);

			case

			{ paramVal.isNumber } {
				var paramValUni;
				paramValUni = spec.unmap(paramVal);
				self.paramValsUni.put(key, paramValUni);
			}
			{ paramVal.isArray } {
				var paramValsUni;
				paramValsUni = paramVal.collect{ |val, n|
					spec.wrapAt(n).unmap(val);
				};
				self.paramValsUni.put(key, paramValsUni);
			};

		};

		self.paramValsUni;

	};

	~addPoint = { |self, id|

		var paramValsUniSerialised;

		self.getParamValsUni;

		paramValsUniSerialised = self.paramValsUni.asSortedArray.flop[1].flat;

		self.bufParams.loadCollection(paramValsUniSerialised, 0) {
			self.dsParams.addPoint(id, self.bufParams);
		};

		self.dsXY.addPoint(id, self.bufXY);

	};

	~setParamVals = { |self, paramValsUniOSC|

		var paramValsUni;

		paramValsUni = paramValsUniOSC.reshapeLike(self.paramValsUni.asSortedArray.flop[1]);

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

			var paramVal;

			paramVal = self.nodeProxy.get(key);

			case

			{ paramVal.isNumber } {
				var value = spec.map(paramValsUni[i]);
				self.nodeProxy.set(key, value);
			}
			{ paramVal.isArray } {
				paramVal.collect{ |val, n|
					var value = spec.wrapAt(n).map(paramValsUni[i].wrapAt(n));
					self.nodeProxy.seti(key, n, value);
				};
			};

		};

	};

	~makeParamLayout = { |self|

		var view, layout, buttons;
		var graphView, pointView, plotDataSetXY;
		var counter = 0;

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

		plotDataSetXY = {
			self.dsXY.dump { |v|
				if (v["cols"] == 0) {
					v = Dictionary["cols" -> 2, "data" -> Dictionary[]]
				};
				pointView.dict = v
			};
		};

		pointView = FluidPlotter(standalone: false).pointSizeScale_(20/6);
		plotDataSetXY.();

		self.xySlider = Slider2D()
		.background_(Color.white.alpha_(0))
		.action_{ |obj|
			self.changed(\slider, obj.x, obj.y);
		};

		graphView = StackLayout(self.xySlider, View.new().layout_(
			VLayout(pointView).margins_(10)
		)).mode_(\stackAll);

		buttons = VLayout(

			Button(bounds: 100@20).states_([["Add Point"]])
			.action_{
				var id = "point-%".format(counter);
				self.addPoint(id);
				counter = counter + 1;
				plotDataSetXY.();
			},

			Button(bounds: 100@20).states_([["Save Data"]])
			.action_{
				FileDialog({ |folder|
					self.dsXY.write(folder +/+ "xydata.json");
					self.dsParams.write(folder +/+ "paramsdata.json");
				}, {}, 2, 0, true);
			},

			Button(bounds: 100@20).states_([["Load Data"]])
			.action_{
				FileDialog({ |folder|
					self.dsXY.read(folder +/+ "xydata.json");
					self.dsParams.read(folder +/+ "paramsdata.json");
				}, fileMode: 2, acceptMode: 0, stripResult: true);
				plotDataSetXY.();
			},

			Button(bounds: 100@20).states_([
				["Train"], ["Train", Color.white, Color.yellow(0.5)]])
			.action_{ |obj|
				self.trainMLP(obj.value > 0)
			},

			Button(bounds: 100@20).states_([["Save MLP"]])
			.action_{
				Dialog.savePanel({ |path|
					if(PathName(path).extension != "json"){
						path = "%.json".format(path);
					};
					self.mlp.write(path);
				});
			},

			Button(bounds: 100@20).states_([["Load MLP"]])
			.action_{
				Dialog.openPanel({ |path|
					self.mlp.read(path, action: {
						{ plotDataSetXY.() }.defer
					});
				});
			},

			Button(bounds: 100@20).states_([["Start MLP"],
				["Stop MLP", Color.white, Color.red(0.8)]])
			.action_{ |obj|
				if (obj.value > 0) { self.startMLP } { self.stopMLP };
			}.value_(self.mlpSynth.notNil),

			Button(bounds: 100@20).states_([["Clear"]])
			.action_{|state|
				self.mlp.clear;
				self.dsXY.clear;
				self.dsParams.clear;
				plotDataSetXY.();
			},

			Button(bounds: 100@20).states_([["Start LFO"],
				["Stop LFO", Color.white, Color.red(0.8)]])
			.action_{ |obj|
				if (obj.value > 0) { self.startLFO } { self.stopLFO };
			}.value_(self.lfoSynth.notNil)

		);

		layout = HLayout([graphView, s: 1], buttons);

		view.layout.add(layout);

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

		view;
	};

	~mapMidi = { |self, ccs|

		self.midiResponders = self.midiResponders ?? { Order[] };
		self.unmapMidi;

		ccs.do { |cc, n|
			self.midiResponders[cc] = MIDIFunc.cc({ |v, c|
				var val = v / 127;
				self.changed(\controller, n, val);
			}, cc).fix
		};

	};

	~unmapMidi = { |self|
		self.midiResponders.do(_.free);
	};

};
)

create a test SynthDef and use the Prototype for preset interpolation:

(
SynthDef(\test, {

	var tFreq, trig, env, freqRange, freq, fmod, sig;

	tFreq = \tFreq.kr(4, spec: ControlSpec(1, 12));
	trig = Impulse.ar(tFreq);
	env = Env.perc(\atk.kr(0.1, spec: ControlSpec(0.005, 1)), \rel.kr(1, spec: ControlSpec(0.05, 1))).ar(0, trig);

	//freqRange = \freqRange.kr([440, 440], spec: ControlSpec(100, 8000));
	//freq = exprand(freqRange[0], freqRange[1]);
	freq = \freq.kr(440, spec: ControlSpec(20, 1000));

	fmod = SinOsc.ar(\fmFreq.kr(3, spec: ControlSpec(1, 100)));

	sig = SinOsc.ar(freq + (freq * fmod * \fmIndex.kr(2, spec: ControlSpec(0, 5))));

	sig = sig * env;

	sig = Pan2.ar(sig, \pan.kr(0, spec: ControlSpec(-1, 1)));

	sig = sig * \amp.kr(-25, spec: ControlSpec(-35, -15, \lin, 1)).dbamp;

	Out.ar(\out.kr(0), sig);
}).add;
)


(
~testMLP = Prototype(\makeMLP) {

	~synthDefName = \test;

	~initArgs = [

	];

	~excludeParams = [

	];

	~ignoreParams = [

		\amp,

	];

};
)


~testMLP.startLFO;
~testMLP.resetLFO;
~testMLP.pauseLFO;
~testMLP.resumeLFO;

~testMLP.lfoSynth.set(\radius, 0.8);
~testMLP.lfoSynth.set(\rate, 0.03);

~testMLP.stopLFO;

~testMLP.setData([0.5, 0.5]);
~testMLP.bufXY.getn(0, 2, { |a| a.postln });

~testMLP.mapMidi([0, 1]);
~testMLP.unmapMidi;
6 Likes

It would mean a world to me if we could advance this attempt with all the ideas i have been sharing. This approach is working fine for basic usage, but i hope you have some ideas how we could advance its nature to be absolutely universal. You wont believe how many hours i have spent on this, it really means something to me :slight_smile:

1 Like

This sounds like something I might use and be interested in helping work on

(I don’t currently use flucoma much because the “barrier to entry” always seems daunting, like I have to learn how it works every time and it never has become intuitive. But I am always impressed by it when I do manage to wrangle it, and am very interested in lowering the barrier for this specific preset space case in particular)

Your use case sounds not quite aligned with how I would personally want to use something like this. (For example, I don’t currently use nodeproxies, I am working on a custom preset system that operates over a whole network of separate synths/nodes sort of like a proxy space)

An easy way to make this more universal would be to make it into a class instead of a ProtoDef so it could be distributed in the standard way as a quark or extension and not require ProtoDef to be installed.

And I think standard best practice would be to separate GUI code from the functionality so that it could e.g. be incorporated into a larger gui system or the gui could be omitted altogether

Maybe more challenging is to find ways to make the functionality more general while still being easy to do the things you describe. e.g. decouple it from the nodeproxy ecosystem so it could play with arbitrary preset collections, and then write some glue to interface with node proxies. Also as you suggest I don’t see why an LFO needs to be built-in on a specific circular path like you have, seems much more powerful to either let the user make their own LFO, or even to draw a path through the 2d space (I remember we talked about something like this here on the forum a while ago…)

If these kinds of ideas are things you would be open to I would be excited about helping work on it (although my time is quite limited for the next few months so can’t promise immediate action)

hey, thanks for your interest :slight_smile:

I think this could be a last step when the main functionality is working as expected.

Yes, thats totally possible. In my opinion NodeProxyGui2 is nice here because its creating a gui for your arbitrary SynthDef and its ignore / exclude methods nicely fit into the mlp attempt and additionally you can use random / vary to create presets.

The circular LFO is just a simple example. This could be adjusted to have more degrees of freedom with some additional functionality or could be excluded and the Environment just sets up the Bus and the SkipJack. Im my opinion the LFO params should also be part of the GUI.
My main point here is that FluidMLPRegressor.kr expects Buffers as its inputs, so you end up with parallel data structures (Buffer and Bus) for this LFO implementation, which i find really unfortunate.

I have to admit im always thinking in terms of signals (e.g. LFOs) and not so much in terms of language side attempts. But in this specific case a language side attempt would let us get rid of the additional Bus.

I also think it would be nice if there could be a way to restructure ~makeMLP, so that more params are not leading to more Ugens (dont know if thats possible) and pass a bunch of param-presets and create the points in the xy-space on a grid automatically instead of placing them manually with ~addPoint (maybe with FluidGrid). But i dont know how to approach that.

For the rest I will have to wait until in front of SC :), but re: when to convert to classes here is my argument for sooner rather than later

In my experience having used other forms of object prototyping, the bigger the prototype gets the more annoying it is to convert to a class (remove all references to self, explicitly return with ^, figure out *new vs -init, sometimes you need to use *initClass, blah blah) and the more likely it is to not work as expected. Also when multiple classes interact with each other, for simple example if we have the main class MLP2D (uninspired name) and then a GUI class MLP2DWindow where

+MLP2D {
  gui { ^MLP2DWindow(this) }
}

I imagine this would be annoying to figure out with ProtoDef and then reimplement with classes?

Or is there a straightforward and automatic way to do all this with ProtoDef?

(I agree it is also annoying to recompile the class library every time you make a change, but to me it is pretty fast and less annoying than doing the same work twice… And so much is idiosyncratic about both classes and object prototyping in SC that they often don’t translate well in my experience)

I have to study the helpfile in more detail. At least I know that you can seperate processing logic from gui by extending the ProtoDef definitions over multiple files.

Would like to hear your additional thoughts :slight_smile:
Quick update: Im currently working on a Klein-Bottle 2D surface scanner based on a Task / Skipjack as a potential blueprint for a language side navigation of the 2D space to get rid of the additional Bus which is needed for the LFO. But for my other questions we possibly need a bit of help from some FluCoMa experts.

Just got to play very briefly with this, and it works great! (I will be away from the computer entirely for the next 3 weeks…)

Yes seems like there’s a lot of inefficiency going on here, turning a buffer into a kr signal just to see if it’s changed (scztt previously suggested there is an easy way to make a BufChanged ugen, but nobody’s done it yet that I know of New class VisualBuffer - #3 by scztt )

then turning the result buffer into a kr in order to detect if any parameters have changed in order to send the reply back via OSC… I wonder if for starters it’s a reasonable assumption that if the XY position has changed, the parameters will also have changed?

…

Now, I would suggest to completely separate the interface to the regressor from the destination SynthDef / Ndef / nodeproxy presets / etc., the midi mapping, the LFO, and the GUI.

Then perhaps we wind up with (each of these would be a separate class)

  • a robust MLP object that handles training and manages a Synth which maps from some inputs to some outputs. (keeping this closer in spirit to FluidMLPRegressor but more sc-idiomatic and user friendly, e.g. conveniences for training and arranging presets in space, accepting both set messages for setting inputs and bus mapping to connect arbitrary LFOs, providing both language-side output via OSC as you have as well as write to buses for direct server-side mapping – would be super cool if it could run at audio rate?! – we could work on brainstorming a full spec while I’m away)
  • a generic GUI for this object, which could e.g. use a 2Dslider for 2D input and a multislider for higher dimensional input
  • an interface to SynthDef / nodeproxy which would be straightforward to plug into this object
  • a gui for this interface (already done – NodeProxyGui2 :slight_smile: )
  • either classes or simple templates for midi and LFO mapping

so all together this could plug together to create exactly what you have already shared, but opens up much more flexibility for different applications as well…

what do you think?

hey, thanks for your ideas :slight_smile: I think we should start with the BufChanged Ugen. Do you have an idea how to approach that?

I would look at

For a pattern for how to access the SndBuf

For a pattern for accessing a member variable of a SndBuf

And scztt suggests looking at ScopeOut implementation for specific inspiration on m_bufupdates

I have previously used the cookie cutter project to begin work on a new plugin but not sure if that’s still the best way

hey, thanks :slight_smile:

Im currently learning about the differences between using Tasks with .json files in VSCode and CMakeLists with Kits and Presets for the compilers i have now successfully installed (GCC and MSVC). I think i need some more time with CMake before i understand what the cookie cutter is exactly solving (yet more things to install).

hey, i have modularized the MLP approach. Now you could decide between a MIDI controller, a 2D LFO (Orbit) writing to a bus or a drawable trajectory to navigate the 2D space (inspired by an idea @bgola has shared with me).

3 Likes

We are still trying to figure out how a BufChanged Ugen could be implemented, which would make the number of Ugens needed for signaling a change in the buffer independent of the number of params one uses.

1 Like

sorry to be late to this thread. 2 ideas pop in where I could probably help:

I’d be very interested to hear where it fails for your learning - always important to me and the project to make as many entrypoints as possible…

now, I can think of a dirty way which could be a simple KR giving a 1 when it happened. The thing is that object will need 2 buffers since there is no flag as far as I know in between UGens if one changes the buffer…

so maybe a BufCompare.kr object is what you need. you have a running buffer and a reference buffer and it compares them and sends a trig when one diff is spotted. that is going to be ‘expensive’

another option would be for me to recode FluidKrToBuf as a real UGen that sends a trigger when written to. @tedmoore and @Sam_Pluta might have strong opinions about this. It is not a very hard thing to do for me at all if that is seen as a real improvement.

another option is what I do now: I use the trigger that fills the buffer to know that it has changed.

1 Like

What about something like:

(
var currentHash, lastHash;

fork{
    var b = Buffer.loadCollection(s, (0..3));
    s.sync;
    b.getn(0, 4,{ |i| currentHash = i.hash });
    s.sync;

    inf.do {
        0.5.coin.if { b.setn(0, { 4.0.rand } ! 4); "changed".postln; };
        lastHash = currentHash;
        b.getn(0, 4,{ |i| currentHash = i.hash });
        s.sync;

        (currentHash != lastHash).postln;
    }
}
)
1 Like

hey, thanks for your reply :slight_smile:
Im not sure what makes the most sense here.
I could imagine rewriting FluidBufToKr could be an option but i guess a general BufChanged ugen could also be useful beyond the Flucoma Toolkit.
Either way i dont really understand some of the details necessary for such an implementation.
I had a brief chat with @Eric_Sluyter about that and they made a first draft.
There seem to be some tricky details which are beyond the scope of what i do understand, so im not very helpful here. Currently its not giving the expected results when using .set messages on a buffer.

this is because I am not certain buffers know in-between ugens if they have been changed. Hence my first dirty proposal (BufChanged) which would have to compare with a state somewhere, which would be expensive… hence my 2nd proposal that if it is about our object not behaving like an optimal SC citizen then I can bite that bullet.

@Mike_McCormick idea of a hash is a clever way maybe to go about BufChange… still have to hash the input at each kr, so I’m tempted by recoding FluidKrToBuf. @tedmoore didn’t yell “noooooooooooo” and even loved the post, so I might do a little weekend project

sounds cool :slight_smile: Ive actually never used FluidKrToBuf my current implementation uses FluidBufToKr into Changed.

My opinion is actually that a BufChanged Ugen is a better idea than recoding FluidKrToBuf or FluidKrToBuf. I think keeping the “Kr ↔ Buf” interfacing and “buf changed” functionalities separate is more flexible. “Sneaking” a “buf changed” functionality into KrToBuf is a bit too sneaky for my taste as a coder.

Frankly, I actually think, as @dietcv says, just using the Changed.kr UGen that already exists is a perfectly good solution and concocting a new solution is not really necessary.

1 Like