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