Some GUI questions for PV_BinDelay

hey, ive started to work on a gui for PV_BinDelay based on some code from an older thread here on the forum, and have some questions:

(
ProtoDef(\spectralDelay, {

	~init = { |self|

		self.fftSize = 256;
		self.windowSize = self.fftSize / 2;
		self.maxDelay = 0.5;

		self.buffers = IdentityDictionary.new();
		self.sliders = IdentityDictionary.new();

		self.getBuffers;
		self.getSynthDef;

		self.window = Window(\spectralDelay, Rect(10, 530, 250, 500)).front;
		self.window.layout = VLayout.new();

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

		self.cleanUp;
		self.makeGui;

	};

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

	~cleanUp = { |self|

		self.window.onClose_{

			self.stopDelay;

			self.buffers.keysValuesDo{ |key, value|
				if(value.isKindOf(Buffer)) {
					value.free;
				};
				self.buffers.removeAt(key);
			};

		};

	};

	~getBuffers = { |self|

		[\delay, \feedback].do{ |key|
			var buffer = Buffer.alloc(s, self.windowSize);
			self.buffers.put(key, buffer);
		};

	};

	~getSynthDef = { |self|

		SynthDef(\spectralDelay, { |delay, feedback|

			var inSig, sig;

			inSig = In.ar(\in.kr(0), 2);
			sig = inSig.collect{ |localSig|
				var localChain;
				localChain = FFT(LocalBuf(self.fftSize), localSig, 0.25);
				localChain = PV_BinDelay(localChain, self.maxDelay, delay, feedback, 0.25);
				IFFT(localChain)
			};

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

	};

	~startDelay = { |self|

		self.stopDelay;

		self.delaySynth = Synth(\spectralDelay, [
			\in, ~busses[\fx][0],
			\delay, self.buffers[\delay],
			\feedback, self.buffers[\feedback],
			\out, 0,
		], target: ~groups[\fx]);

	};

	~stopDelay = { |self|

		self.delaySynth !? { self.delaySynth.free; self.delaySynth = nil };

	};

	~makeParamLayout = { |self|

		var view;

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

		self.buffers.sortedKeysValuesDo{ |key, buffer|

			var staticText, multiSlider;

			staticText = StaticText.new()
			.string_(key);

			multiSlider = MultiSliderView.new()
			.value_(0 ! self.windowSize)
			.elasticMode_(1);

			self.sliders.put(key, multiSlider);

			view.layout.add(staticText);
			view.layout.add(multiSlider);

		};

		self.sliders[\delay].action_({ |obj|
			self.buffers[\delay].set(obj.index, obj.currentvalue * self.maxDelay)
		});

		self.sliders[\feedback].action_({ |obj|
			self.buffers[\feedback].set(obj.index, obj.currentvalue)
		});

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

		view;

	};

	~makeGui = { |self|

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

		self.window;

	};

});
)

x = Prototype(\spectralDelay);
x.startDelay;
x.stopDelay;

// set buffers and update sliders
x.buffers[\delay].setn(0, { exprand(0.1, 0.5) } ! x.windowSize);
x.buffers[\feedback].setn(0, { exprand(0.1, 0.9) } ! x.windowSize);

1.) Im creating an IdentityDictionary and put two Buffers in there (delay and feedback) and iterate over the IdentityDictionary via self.buffers.sortedKeysValuesDo but have excluded the .action because one is scaling the obj.currentvalue by self.maxDelay and the other doesnt. Ideally i would like to include the .action in the iteration. Can the scaling be done elsewhere, maybe in the SynthDef?

2.) When setting values for the buffers via .setn the MultiSliders are currently not changing their position. Is there a way to set up a dependency for the Buffers, so that when the values in the Buffers have been changed the MultiSliders are updated?

3.) Would somebody know how to shift the phases by 90° in one channel for a nice stereo effect?

Sorry to be dumb, but where does the Prototype class come from?

  1. Use an if statement in the function to assign the action based on key?

  2. I would just set the multislider with a valuaction instead of the buffer. This relies on 1.

  3. Does PV_PhaseShift not work?

thanks, you can find the class here: GitHub - elgiano/ProtoDef: Prototyping classes for SuperCollider

1.) thanks, thats working fine:

			multiSlider = MultiSliderView.new()
			.value_(0 ! self.windowSize)
			.elasticMode_(1)
			.action_({ |obj|
				var val = if(key == \delay) {
					obj.currentvalue * self.maxDelay;
				} {
					obj.currentvalue;
				};
				self.buffers[key].set(obj.index, val);
			});

2.) Isnt there a way to setup up a dependency like you would do for Synth.set and Sliders, stored in a dictionary, which would be updated if a param has changed? Isnt setting the Buffer data and updating the Sliders more aligned with a Model-View-Controller approach, then setting the Sliders and update the Buffer data? Maybe store the buffer data in an Order or IdentityDicitionary. Shouldnt the multisliders be initialised with .value_(self.bufferData[key]) from an IdentitiyDictionary anyway? Maybe as a first step, store the data and the buffer in the same IdentityDictionary?:

	~getDelayData = { |self|

		[\delay, \feedback].do{ |key|
			var buffer = Buffer.alloc(s, self.windowSize);
			var initData = 0 ! self.windowSize;
			self.delayData.put(key, (buffer: buffer, data: initData));
		};

	};

	~makeParamLayout = { |self|

		var view;

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

		self.delayData.keysDo{ |key|

			var staticText, multiSlider;

			staticText = StaticText.new()
			.string_(key);

			multiSlider = MultiSliderView.new()
			.value_(self.delayData[key][\data])
			.elasticMode_(1)
			.action_({ |obj|
				var val = if(key == \delay) {
					obj.currentvalue * self.maxDelay;
				} {
					obj.currentvalue;
				};
				self.delayData[key][\buffer].set(obj.index, val);
			});

			self.sliders.put(key, multiSlider);

			view.layout.add(staticText);
			view.layout.add(multiSlider);

		};

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

		view;

	};

3.) will try that thanks :slight_smile:

there is addDependants, which allows you to connect things so they update on changes. There is not too much about it in the Help files unfortunately.

1 Like

Thanks, I have used that for another gui based on nodeproxies After extensively Studying NodeProxyGui2 in the last year. Does anyone has an example on how to handle buffers with the addDependant attempt ? I guess the data in a Dictionary could have a dependency but Not the buffer itself, because .setn doesnt notify dependencies, might be wrong. But storing parallel data is also not optimal when you already have the buffer, which stores the data. But when my general attempt is to set a Synth or NodeProxy and notify the sliders to update as well, then i dont want to differ from the model here.

Since Buffer doesn’t notify dependents of changes, you would not be able to use the buffer methods directly to perform the change and get the notification.

But you could create your own alternate interface that wraps both.

The easiest is just to make a function:

~bufferSetn = { |buffer ... pairs|
	buffer.setn(*pairs);
	buffer.changed(\setn, *pairs);
};

This is a type of question that comes up fairly often, where a class library method does part of what a user requires, but not all of it. Writing abstractions on top of given methods is the essence of programming, and those abstractions don’t always have to be highly automated. Often, a function really is enough.

hjh

Thanks, I will try to use that for different methods to set the Buffer content, notify a Change and Update the sliders.

hey,

i have done some further investigations:

In this specific context its probably enough to directly set the sliders:

~setData = { |self, key, n, values|

	var normalizedVals = if(key == \delay) {
		values * self.maxDelay;
	} {
		values;
	};

	self.buffers[key].setn(n, normalizedVals);

	{
		if(self.sliders[key].notNil) {
			self.sliders[key].value_(values);
		};

	}.defer;
};

But i wanted to learn more about the MVC approach, and tried to set everything up with .addDependant, .changed and .update

for a simple case this could look like this:

~setUpDependencies = { |self|
	
	// and UI is updated...
	var dataChangedFunc = { |obj ...args| self.dataChanged(*args) }.defer;
	
	self.buffers.keysValuesDo{ |key, buffer|
		buffer.addDependant(dataChangedFunc);
	};
	
	self.window.onClose_{
		
		[...]
		
		self.buffers.keysValuesDo{ |key, value|
			if(value.isKindOf(Buffer)) {
				value.removeDependant(dataChangedFunc);
				value.free;
			};
			self.buffers.removeAt(key);
		};
		
		[...]
		
	};
	
};

// dataChanged is called when values change
~dataChanged = { |self, what ...args|
	
	if(what == \setn) {
		
		var key = args[0];
		var index = args[1]; 
		var value = args[2];
		
		if(self.sliders[key].notNil) {
			self.sliders[key].value_(value);
		};
		
	};
};

// set values and notify change
~setData = { |self, key, index, values|
	
	var normalizedVals;
	
	// normalize values
	normalizedVals = if(key == \delay) {
		values * self.maxDelay;
	} {
		values;
	};
	
	// set values in buffer
	self.buffers[key].setn(index, normalizedVals);
	
	// notify about the change
	self.buffers[key].changed(\setn, key, index, values);
};

While experimenting with this approach i have figured out, you could also rename the custom ~dataChanged method to ~update and can use .addDependant(self). I have no idea how the notification makes its way into var dataChangedFunc = { |obj ...args| self.dataChanged(*args) }.defer;, but it works and i can probably live with that (maybe just learning vocabulary).

I was also wondering if a “true” MVC approach (whatever that means), wouldnt be setup by getting the current state of the Model inside the ~dataChanged method instead of passing the values to the UI, something like:

~dataChanged = { |self, what ...args|

	if(what == \setn) {

		var key = args[0];
		var index = args[1];
		var value = args[2];

		self.buffers[key].getn(index, value, { |values|

			{
				var normalizedVals = if(key == \delay) {
					values / self.maxDelay;
				} {
					values;
				};

				if(self.sliders[key].notNil) {
					self.sliders[key].value_(normalizedVals);
				};

			}.defer;

		});
	};
};

Here is the current state of my implementation, where i tried to implement some cases for all types of combinations of single indices, and array of indices, and single values and array of values, where probably not all of them are needed, maybe you can just throw an error when the input data is invalid.
I have also implemented some functions to randomize, vary and shape the data.
All of this could probably be implemented in a way more sophisticated way (especially for the different cases and the vary method, where ive tried to implement what ive learned about CondVar), maybe you have some ideas for adjustments. Would be happy about that :slight_smile:
I think i will additionally try to implement a function to morph between different shapes, maybe with a task.

delay (rampDown), feedback (flat) with threshold

vary the delay data by adding a gaussian distribution

randomize the feedback data

(
ProtoDef(\spectralDelay, { |input, output, target|

	~init = { |self|

		self.fftSize = 256;
		self.windowSize = self.fftSize / 2;
		self.maxDelay = 0.5;

		self.buffers = IdentityDictionary.new();
		self.sliders = IdentityDictionary.new();

		self.initBuffers;
		self.getSynthDef;

		self.bounds = Rect(10, 530, self.windowSize * 2, self.windowSize * 4);
		self.window = Window(\spectralDelay, self.bounds).front;
		self.window.layout = VLayout.new();

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

        self.setUpDependencies;
		self.makeGui;

	};

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

	~setUpDependencies = { |self|

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

		self.buffers.keysValuesDo{ |key, buffer|
			buffer.addDependant(dataChangedFunc);
        };

		self.window.onClose_{

			self.stopDelay;

			self.buffers.keysValuesDo{ |key, value|
				if(value.isKindOf(Buffer)) {
					value.removeDependant(dataChangedFunc);
					value.free;
				};
				self.buffers.removeAt(key);
			};

			self.buffers.clear;
			self.sliders.clear;

		};

	};

	~initBuffers = { |self|

		[\delay, \feedback].do{ |key|
			var buffer = Buffer.alloc(s, self.windowSize);
			self.buffers.put(key, buffer);
		};

	};

	~getSynthDef = { |self|

		SynthDef(\spectralDelay, {

			var inSig, sig;

			inSig = In.ar(\in.kr(0), 2);
			sig = inSig.collect{ |localSig|
				var localChain;
				localChain = FFT(LocalBuf(self.fftSize), localSig, 0.25);
				localChain = PV_BinDelay(
					buffer: localChain,
					maxdelay: self.maxDelay,
					delaybuf: \delaybuf.kr(0),
					fbbuf: \fbbuf.kr(0),
					hop: 0.25
				);
				IFFT(localChain);
			};

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

	};

	// set values and notify change
	~setData = { |self, key, indices, values|

		var normalizedVals;

		// Normalize values
		normalizedVals = if(key == \delay) {
			values * self.maxDelay;
		} {
			values;
		};

		case
		// Case 1: Single index
		{ indices.isNumber } {
			// Use setn for efficiency (handles both array and single value)
			self.buffers[key].setn(indices, normalizedVals);
		}

		// Case 2: Array of indices
		{ indices.isArray } {
			// Handle array indices with single number value
			if(values.isNumber) {
				normalizedVals = normalizedVals ! indices.size;
			};

			// Update each index individually
			indices.do { |idx, i|
				idx = idx.asInteger;
				if(idx >= 0 && idx < self.windowSize) {
					self.buffers[key].set(idx, normalizedVals[i]);
				};
			};
		};

		// Notify about the change
		self.buffers[key].changed(\setn, key, indices, values);
	};

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

		if(what == \setn) {

			var key = args[0];
			var indices = args[1];
			var values = args[2];

			if(self.sliders[key].notNil) {

				case
				// Case 1: Single index
				{ indices.isNumber } {
					// Setting entire buffer at once
					if(values.isArray) {
						if(indices == 0 && values.size == self.windowSize) {
							self.sliders[key].value_(values);
						} {
							// Setting consecutive range
							var currentVals = self.sliders[key].value;
							values.size.do { |i|
								currentVals[indices + i] = values[i];
							};
							self.sliders[key].value_(currentVals);
						};
					} {
						// Single value at single index
						var currentVals = self.sliders[key].value;
						currentVals[indices] = values;
						self.sliders[key].value_(currentVals);
					};
				}

				// Case 2: Array of indices
				{ indices.isArray } {
					var currentVals = self.sliders[key].value;

					// Handle array indices with single number value
					if(values.isNumber) {
						// Set same value at multiple indices
						indices.do { |idx|
							currentVals[idx] = values;
						};
					} {
						// Update each index with corresponding value
						indices.do { |idx, i|
							currentVals[idx] = values[i];
						};
					};

					// Update the entire slider
					self.sliders[key].value_(currentVals);
				};
			};
		};
	};

	// Clear buffer
	~clearData = { |self, key|
		var values = 0 ! self.windowSize;
		self.setData(key, 0, values);
	};

	// Randomize buffer
	~randomizeData = { |self, key, min = 0.0, max = 1.0|
		var values = { rrand(min, max) } ! self.windowSize;
		self.setData(key, 0, values);
	};

	// Vary buffer
	~varyData = { |self, key, deviation = 0.1|

		Routine({

			var currentVals, normalizedVals, variedVals;
			var condition = CondVar.new;
			var done = false;

			// Step 1: Load buffer values to FloatArray
			self.buffers[key].loadToFloatArray(0, self.windowSize, { |array|
				// Store values and signal condition
				currentVals = array;
				done = true;
				condition.signalAll;
			});

			// Wait for values to be ready
			condition.wait { done };

			// Step 2: Process the values
			// Normalize values
			normalizedVals = if(key == \delay) {
				currentVals / self.maxDelay;
			} {
				currentVals;
			};

			// Apply Gaussian variation
			variedVals = normalizedVals.collect { |val|
				(val + 0.0.gauss(deviation)).clip(0.0, 1.0);
			};

			// Step 3: Update buffer and UI
			self.setData(key, 0, variedVals);

		}).play(AppClock);

	};

	// Apply shape function
	~shapeData = { |self, key, shape = \sine, threshold = 0.5|

		var values;

		values = case
		{ shape == \sine } {
			(0.. self.windowSize - 1).collect { |i|
				sin(i / self.windowSize * 2pi) * 0.5 + 0.5
			}
		}
		{ shape == \triangle } {
			(0.. self.windowSize - 1).collect { |i|
				if(i < (self.windowSize / 2)) {
					i / (self.windowSize / 2)
				} {
					1.0 - ((i - (self.windowSize / 2)) / (self.windowSize / 2))
				}
			}
		}
		{ shape == \rampUp } {
			(0.. self.windowSize - 1).collect { |i|
				i / self.windowSize
			}
		}
		{ shape == \rampDown } {
			(0.. self.windowSize - 1).collect { |i|
				1 - (i / self.windowSize)
			}
		}
		{ shape == \flat } {
			threshold ! self.windowSize
		};

		self.setData(key, 0, values);
	};

	~startDelay = { |self|

		self.stopDelay;

		self.delaySynth = Synth(\spectralDelay, [
			\in, self.input,
			\out, self.output,
			\delaybuf, self.buffers[\delay],
			\fbbuf, self.buffers[\feedback],
		], target: self.target, addAction: \addToTail);

	};

	~stopDelay = { |self|

		self.delaySynth !? { self.delaySynth.free; self.delaySynth = nil };

	};

	~makeParamLayout = { |self|

		var view;

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

		self.buffers.sortedKeysValuesDo{ |key, buffer|

			var staticText, multiSlider;

			staticText = StaticText.new()
			.string_(key);

			multiSlider = MultiSliderView.new()
			.elasticMode_(1)

			.isFilled_(true)
			.background_(Color.white)
			.strokeColor_(Color.black.alpha_(0.3))
			.fillColor_(Color.blue.alpha_(0.7))

			.value_(0 ! self.windowSize)
			.action_({ |obj|
				var val = if(key == \delay) {
					obj.currentvalue * self.maxDelay;
				} {
					obj.currentvalue;
				};
				buffer.set(obj.index, val);
			});

			self.sliders.put(key, multiSlider);

			view.layout.add(staticText);
			view.layout.add(multiSlider);
		};

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

		view;

	};

	~makeGui = { |self|

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

		self.window;

	};

});
)

~bus = Bus.audio(s, 2);
~group = Group.new;

(
x = Prototype(\spectralDelay) {
	~input = topEnvironment[\bus];
	~output = 0;
	~target = topEnvironment[\group];
};
)

x.startDelay;
x.stopDelay;

x.varyData(\delay, 0.03);
x.varyData(\feedback, 0.03);

x.randomizeData(\delay, 0.0, 1.0);
x.randomizeData(\feedback, 0.0, 1.0);

x.shapeData(\delay, \sine);
x.shapeData(\delay, \triangle);
x.shapeData(\delay, \rampUp);
x.shapeData(\delay, \rampDown);
x.shapeData(\delay, \flat, 0.2);

x.shapeData(\feedback, \sine);
x.shapeData(\feedback, \triangle);
x.shapeData(\feedback, \rampUp);
x.shapeData(\feedback, \rampDown);
x.shapeData(\feedback, \flat, 0.8);

x.clearData(\delay);
x.clearData(\feedback);

// Single value at single index:
x.setData(\delay, rrand(0.0, x.windowSize), rrand(0.0, 1.0));
x.setData(\feedback, rrand(0.0, x.windowSize), rrand(0.0, 1.0));

// Multiple values at consecutive indices:
x.setData(\delay, rrand(0.0, x.windowSize), [0.25, 0.5, 0.75]);
x.setData(\feedback, rrand(0.0, x.windowSize), [0.25, 0.5, 0.75]);

// Single value at multiple indices:
x.setData(\delay, { rrand(0, x.windowSize) } ! 10, 0.5);
x.setData(\feedback, { rrand(0, x.windowSize) } ! 10, 0.5);

// Multiple values at multiple indices:
x.setData(\delay, { rrand(0.0, x.windowSize) } !10, { rrand(0.0, 1.0) } !10 );
x.setData(\feedback, { rrand(0.0, x.windowSize) } !10, { rrand(0.0, 1.0) } !10 );

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

// SynthDef for testing

(
SynthDef(\test, {

	var trig, gainEnv, sig;

	trig = Impulse.ar(1);
	gainEnv = Decay.ar(trig, 0.4);

	sig = Saw.ar(\freq.kr(440) * (2 ** TIRand.ar(-1, 1, trig)));
	sig = sig * gainEnv;

	sig = Pan2.ar(sig, \pan.kr(0));

	sig = sig * \amp.kr(-15).dbamp;

	sig = sig * Env.asr(0.001, 1, 0.001).ar(Done.freeSelf, \gate.kr(1));

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

(
Routine {

	var freqs = [57, 60, 64, 65, 70].midicps;

	s.bind {
		freqs.collect{ |freq|

			Synth(\test, [

				\freq, freq,

				\amp, -25,
				\out, ~bus,

			], target: ~group);

		};
	};

}.play;
)
1 Like

After changing the internal Synth to a NodeProxy i have figured out one specific bug when closing the window, which evaluates self.window.onCLose_ and frees the buffers. The error is not happening, when i dont create the NodeProxy via self.getNodeProxy; in ~init or dont free the buffers inside .onClose.

(
ProtoDef(\spectralDelay, {

	~init = { |self|

		self.fftSize = 256;
		self.windowSize = self.fftSize / 2;
		self.maxDelay = 0.5;

		self.buffers = IdentityDictionary.new();
		self.sliders = IdentityDictionary.new();

		self.initBuffers;
		self.getNodeProxy;

		//self.bounds = Rect(10, 340, 384, 690);
		self.bounds = Rect(10, 520, self.windowSize * 2, self.windowSize * 4);
		self.window = Window(self.defName, self.bounds).front;
		self.window.layout = VLayout.new();

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

        self.setUpDependencies;
		self.makeGui;

	};

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

	~setUpDependencies = { |self|

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

		self.buffers.keysValuesDo{ |key, buffer|
			buffer.addDependant(dataChangedFunc);
        };

		self.window.onClose_{
			
			self.nodeProxy.free;
			self.nodeProxy.clear;

			self.buffers.keysValuesDo{ |key, value|
				if(value.isKindOf(Buffer)) {
					value.removeDependant(dataChangedFunc);
					value.free;
				};
				self.buffers.removeAt(key);
			};

			self.sliders.clear;

		};

	};

	~initBuffers = { |self|

		[\delay, \feedback].do{ |key|
			var buffer = Buffer.alloc(s, self.windowSize);
			self.buffers.put(key, buffer);
		};

	};

	~getNodeProxy = { |self|

		self.nodeProxy.clear;

		self.nodeProxy ?? {
			self.nodeProxy = NodeProxy.audio(s, 2);
		};

		self.nodeProxy.source = {

			var inSig, sig;

			inSig = \in.ar(0!2);
			sig = inSig.collect{ |localSig|
				var localChain;
				localChain = FFT(LocalBuf(self.fftSize), localSig, 0.25);
				localChain = PV_BinDelay(
					buffer: localChain,
					maxdelay: self.maxDelay,
					delaybuf: self.buffers[\delay],
					fbbuf: self.buffers[\feedback],
					hop: 0.25
				);
				IFFT(localChain);
			};

		};

		self.nodeProxy;

	};

	// set values and notify change
	~setData = { |self, key, indices, values|

		var normalizedVals;

		// Normalize values
		normalizedVals = if(key == \delay) {
			values * self.maxDelay;
		} {
			values;
		};

		case
		// Case 1: Single index
		{ indices.isNumber } {
			// Use setn for efficiency (handles both array and single value)
			self.buffers[key].setn(indices, normalizedVals);
		}

		// Case 2: Array of indices
		{ indices.isArray } {
			// Handle array indices with single number value
			if(values.isNumber) {
				normalizedVals = normalizedVals ! indices.size;
			};

			// Update each index individually
			indices.do { |idx, i|
				idx = idx.asInteger;
				if(idx >= 0 && idx < self.windowSize) {
					self.buffers[key].set(idx, normalizedVals[i]);
				};
			};
		};

		// Notify about the change
		self.buffers[key].changed(\setn, key, indices, values);

	};

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

		if(what == \setn) {

			var key = args[0];
			var indices = args[1];
			var values = args[2];

			if(self.sliders[key].notNil) {

				case
				// Case 1: Single index
				{ indices.isNumber } {
					// Setting entire buffer at once
					if(values.isArray) {
						if(indices == 0 && values.size == self.windowSize) {
							self.sliders[key].value_(values);
						} {
							// Setting consecutive range
							var currentVals = self.sliders[key].value;
							values.size.do { |i|
								currentVals[indices + i] = values[i];
							};
							self.sliders[key].value_(currentVals);
						};
					} {
						// Single value at single index
						var currentVals = self.sliders[key].value;
						currentVals[indices] = values;
						self.sliders[key].value_(currentVals);
					};
				}

				// Case 2: Array of indices
				{ indices.isArray } {

					var currentVals = self.sliders[key].value;

					// Handle array indices with single number value
					if(values.isNumber) {
						// Set same value at multiple indices
						indices.do { |idx|
							currentVals[idx] = values;
						};
					} {
						// Update each index with corresponding value
						indices.do { |idx, i|
							currentVals[idx] = values[i];
						};
					};
					// Update the entire slider
					self.sliders[key].value_(currentVals);
				};
			};
		};
	};

	// Clear data
	~clearData = { |self, key|
		var values = 0 ! self.windowSize;
		self.setData(key, 0, values);
	};

	// Randomize data
	~randomizeData = { |self, key, min = 0.0, max = 1.0|
		var values = { rrand(min, max) } ! self.windowSize;
		self.setData(key, 0, values);
	};

	// Vary data
	~varyData = { |self, key, deviation = 0.1|

		Routine({

			var currentVals, normalizedVals, variedVals;
			var condition = CondVar.new;
			var done = false;

			// Step 1: Load buffer values to FloatArray
			self.buffers[key].loadToFloatArray(0, self.windowSize, { |array|
				// Store values and signal condition
				currentVals = array;
				done = true;
				condition.signalAll;
			});

			// Wait for values to be ready
			condition.wait { done };

			// Step 2: Process the values
			// Normalize values
			normalizedVals = if(key == \delay) {
				currentVals / self.maxDelay;
			} {
				currentVals;
			};

			// Apply Gaussian variation
			variedVals = normalizedVals.collect { |val|
				(val + 0.0.gauss(deviation)).clip(0, 1);
			};

			// Step 3: Update buffer and UI
			self.setData(key, 0, variedVals);

		}).play(AppClock);

	};

	// Apply shaping function to data
	~shapeData = { |self, key, shape = \sine, threshold = 0.5|

		var values;

		values = case

		{ shape == \sine } {
			(0.. self.windowSize - 1).collect { |i|
				sin(i / self.windowSize * 2pi) * 0.5 + 0.5
			}
		}
		{ shape == \triangle } {
			(0.. self.windowSize - 1).collect { |i|
				if(i < (self.windowSize / 2)) {
					i / (self.windowSize / 2)
				} {
					1.0 - ((i - (self.windowSize / 2)) / (self.windowSize / 2))
				}
			}
		}
		{ shape == \rampUp } {
			(0.. self.windowSize - 1).collect { |i|
				i / self.windowSize
			}
		}
		{ shape == \rampDown } {
			(0.. self.windowSize - 1).collect { |i|
				1 - (i / self.windowSize)
			}
		}
		{ shape == \flat } {
			threshold ! self.windowSize
		};

		self.setData(key, 0, values);

	};

	~makeParamLayout = { |self|

		var view;

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

		self.sliders.clear;

		self.buffers.sortedKeysValuesDo{ |key, buffer|

			var staticText, multiSlider;

			staticText = StaticText.new()
			.string_(key);

			multiSlider = MultiSliderView.new()
			.elasticMode_(1)

			.isFilled_(true)
			.background_(Color.white)
			.strokeColor_(Color.black.alpha_(0.3))
			.fillColor_(Color.blue.alpha_(0.7))

			.value_(0 ! self.windowSize)
			.action_({ |obj|
				var val = if(key == \delay) {
					obj.currentvalue * self.maxDelay;
				} {
					obj.currentvalue;
				};
				buffer.set(obj.index, val);
			});

			self.sliders.put(key, multiSlider);

			view.layout.add(staticText);
			view.layout.add(multiSlider);
		};

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

		view;

	};

	~makeGui = { |self|

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

		self.window;

	};

});
)

creating the Prototype and closing the window:

~spectralDelay = Prototype(\spectralDelay);

i get:

Server 'localhost' exited with exit code -1073741819.
(sclang) SC_UdpInPort: received error - Eine vorhandene Verbindung wurde vom Remotehost geschlossen
server 'localhost' disconnected shared memory interface

I don’t know exactly why, but the Crash happened because of the local fft buffer.

Local FFT buffer – that’s odd – did you observe this in a debugger trace?

I’ve had hiccups before where I freed a buffer, but the synth(s) using it hadn’t been fully removed yet. In your onClose function, you’re freeing the synth first but, if NodeProxy releases the synth rather than brute-force freeing it, then the synth may still be in its release phase when you free the buffers. If there’s a UGen in the synth that accesses the buffer and doesn’t guard against the buffer disappearing, that could cause a crash.

hjh

Hey, ive noticed while testing a more simplified example. If i allocate an FFT Buffer instead of the LocalBuf the server doesnt crash on self.window.onClose_. So we want to make sure the Synths are fully released before we free the buffers?

Weirdly enough i think its the same error i get with some specific buffers and initial buf_loc 1 and OscOS.


(
ProtoDef(\spectralDelay, {

	~init = { |self|

		self.fftSize = 256;
		self.windowSize = self.fftSize / 2;
		self.maxDelay = 0.5;

		self.buffers = IdentityDictionary.new();

		self.initBuffers;
		self.getNodeProxy;

		self.window = Window(self.defName, Rect(10, 340, 384, 690)).front;

        self.setUpDependencies;

	};

	~setUpDependencies = { |self|

		self.window.onClose_{

			self.nodeProxy.free;

			self.buffers.keysValuesDo{ |key, value|
				value.free;
			};

		};

	};

	~initBuffers = { |self|

		[\delay, \feedback].do{ |key|
			var buffer = Buffer.alloc(s, self.windowSize);
			self.buffers.put(key, buffer);
		};

		self.fftBuffer = Buffer.alloc(s, self.fftSize);
		
	};

	~getNodeProxy = { |self|

		self.nodeProxy.clear;

		self.nodeProxy ?? {
			self.nodeProxy = NodeProxy.audio(s, 2);
		};

		self.nodeProxy.source_{

			var inSig, sig;

			inSig = \in.ar(0!2);

			sig = inSig.collect{ |localSig|
				var localChain;

				// allocated Buffer instead of LocalBuf doesnt cause a crash on .onClose_
				localChain = FFT(self.fftBuffer, localSig, 0.25);
				//localChain = FFT(LocalBuf(self.fftSize), localSig, 0.25);

				localChain = PV_BinDelay(
					buffer: localChain,
					maxdelay: self.maxDelay,
					delaybuf: self.buffers[\delay],
					fbbuf: self.buffers[\feedback],
					hop: 0.25
				);
				IFFT(localChain);
			};

		};

		self.nodeProxy;

	};

});
)

~spectralDelay = Prototype(\spectralDelay);

That’s really weird; LocalBufs are supposed to be self-managing.

Maybe the LocalBuf UGen goes through its Dtor before PV_BinDelay does. If the bin delay doesn’t properly guard against a null pointer, that could do it.

Unlikely to be the same cause, though. I reported the OscOS bug awhile back (but it unfortunately isn’t reproducible on all machines).

hjh

I have it fixed. Just have been finishing something else before I have time to test it.

hey, was not sure if its just a bug on my maschine in both cases. I have seen your report and also made a comment and im very happy that its going to be fixed, thanks alot @Sam_Pluta

When using a LocalBuf for the FFT Buffer with PV_BinDelay you can wrap self.nodeProxy.free and self.buffers.keysValuesDo{ |key, buffer| buffer.free }; into s.bind inside self.window.onClose_ to fix the issue or you can allocate an additional FFT Buffer and free them all together, without the need for explicit synchronicity.

~setUpDependencies = { |self|
	
	var dataChangedFunc = { |obj ...args| self.dataChanged(*args) }.defer;
	
	self.buffers.keysValuesDo{ |key, buffer|
		buffer.addDependant(dataChangedFunc);
	};
	
	self.window.onClose_{
		
		s.bind({
			
			self.nodeProxy.free;
			
			self.buffers.keysValuesDo{ |key, buffer|
				buffer.removeDependant(dataChangedFunc);
				buffer.free;
			};
			
		});
		
		self.nodeProxy.clear;
		self.sliders.clear;
		self.buffers.clear;
		
	};
	
};

I would use node notifications:

	~setUpDependencies = { |self|
		self.window.onClose_{
			OSCFunc({
				self.buffers.keysValuesDo{ |key, value|
					value.free;
				};
			}, '/n_end', s.addr, argTemplate: [self.nodeProxy.group.nodeID])
			.oneShot;
			
			self.nodeProxy.free;
		};
	};

I could reproduce your crash with your example as is. With the above onClose logic, no crash.

Great! Thanks –

hjh

thanks, im unfamiliar with those, but i will look them up :slight_smile: