GUI interactivity limitations or the problem of the code itself?

Hello,

While testing the following code:

The link in the hidden text now contains an updated code. See the code block below which contains the previous code to understand the question:

Overtone Explorer

(
s.options.numWireBufs = 128; // 256;

~numSliders = 64; // Tested at up to 120
~mul = 0.1;
~freq = 200; // 393.44

s.waitForBoot {
	var numSliders, indexThumbSize, indexWidth, centre, upperHeight, lowerHeight, width, win;
	var freqBox, multiSliderFreq, multiSliderPhase, phases, phaseButtons, ampNum, ampSlider, freqScope;
	var lable, lables, waveformView, waveform;
	var lablesHeight, interactiveLableFreq, interactiveLablePhase, interactiveLableUpdate, antiAliasing;

	numSliders = ~numSliders;
	indexThumbSize = 16;
	indexWidth = indexThumbSize + 1;
	centre = [Window.screenBounds.width, Window.screenBounds.height] / 2;

	width = if (numSliders <= 36) {
		36 * indexWidth + 6 + 255;
	} {
		numSliders * indexWidth + 6
	};

	win = Window("overtone explorer", Rect(centre[0] - (width / 2), centre[1] - 325, width, 650));
	win.front.onClose_{
		x.free;
		freqScope.kill;
	}
	.acceptsMouseOver_(true);

	lowerHeight = 250;
	upperHeight = win.bounds.height - lowerHeight ;

	lable = { |parent, left, top, width, height, align, fontSize, string, colour, background|
		StaticText(parent, Rect(left, top, width, height))
		.align_(align)
		.font_(Font("Arial", fontSize))
		.stringColor_(colour)
		.string_(string)
		.background_(background)
	};

	lables = numSliders.collect { |i|
		var string, left;
		string = (i + 1).asString.padLeft(3, "0");
		left = i * indexWidth + 7;
		[
			lable.(win, left, 2,  10, 10, \center, 11, string[0], Color.black, Color.clear),
			lable.(win, left, 14, 10, 10, \center, 11, string[1], Color.black, Color.clear),
			lable.(win, left, 26, 10, 10, \center, 11, string[2], Color.black, Color.clear)
		]
	};

	lablesHeight = 36;

	antiAliasing = { |freqBase|
		numSliders.do { |index|
			var thisFreq;
			thisFreq = index + 1 * freqBase;
			interactiveLableFreq.string_("");
			if (thisFreq < (s.sampleRate / 2)) {
				lables[index].size.do { |i|
					lables[index][i].stringColor_(Color.black)
				}
			}
			{
				multiSliderFreq.value_(multiSliderFreq.value.put(index, 0));
				lables[index].size.do { |i|
					lables[index][i].stringColor_(Color.grey(0.6))
				}
			}
		}
	};

	multiSliderFreq = if (numSliders < 36) {
		MultiSliderView(win, Rect(1, lablesHeight, numSliders * indexWidth + 6, upperHeight - 50 / 2));
	} {
		MultiSliderView(win, Rect(1, lablesHeight, width, upperHeight - 50 / 2));
	};

	multiSliderFreq.value_((0 ! numSliders))
	.showIndex_(true)
	.indexThumbSize_(indexThumbSize)
	.action_{ |sliders|
		var numNotZero = ~numSliders - sliders.value.occurrencesOf(0);
		x.set(
			\numNotZero, numNotZero,
			\overtoneLast, ~numSliders,
			\amplitudes, sliders.value
		)
	}
	.mouseOverAction_{ |view, x, y|
		var mouseOverIndex;
		mouseOverIndex = (x / indexWidth).round;
		interactiveLableUpdate.(x, y, multiSliderFreq, interactiveLableFreq, "Hz;\n", 100, "%",
			"\n0 % to prevent aliasing");
		interactiveLableFreq.stringColor_(if(mouseOverIndex * freqBox.value < (s.sampleRate / 2)) {
			Color.blue(1, 0.5)
		} {
			Color.grey(0.5)
		});
	}
	.mouseMoveAction_{ |view, x, y|
		var mouseMoveIndex;
		mouseMoveIndex = (x / indexWidth).round;
		interactiveLableUpdate.(x, y, multiSliderFreq, interactiveLableFreq, "Hz;\n", 100, "%",
			"\n0 % to prevent aliasing");
		interactiveLableFreq.stringColor_(if(mouseMoveIndex * freqBox.value < (s.sampleRate / 2)) {
			Color.blue(1, 0.5)
		} {
			Color.grey(0.5)
		});
	}
	.mouseUpAction_{ |view, x, y|
		antiAliasing.(freqBox.value);
		interactiveLableUpdate.(x, y, multiSliderFreq, interactiveLableFreq, "Hz;\n", 100, "%",
			"\n0 % to prevent aliasing")
	}
	.mouseLeaveAction_{
		interactiveLableFreq.string_("Amplitudes")
		.bounds_(Rect(
			multiSliderFreq.bounds.extent.x - 120 / 2,
			multiSliderFreq.bounds.extent.y - 20 / 2,
			120,
			25));
		multiSliderFreq.showIndex_(false)
	}
	.mouseEnterAction_{
		multiSliderFreq.showIndex_(true)
	}
	.background_(Color.grey(0.9, 0.1))
	.colors_(Color.grey(0.3, 0.5), Color.grey(0.3, 0.5));

	interactiveLableFreq = lable.(multiSliderFreq,
		multiSliderFreq.bounds.extent.x - 120 / 2,
		multiSliderFreq.bounds.extent.y - 20 / 2,
		120,
		25,
		\left, 24, "Amplitudes", Color.blue(1, 0.5), Color.grey(0.9, 0.7));


	phases = 0 ! ~numSliders;

	phaseButtons = ~numSliders.collect { |i|
		Button(win, Rect(
			indexThumbSize + 1 * i + 4, upperHeight / 2 - 15 + 26, indexThumbSize, indexThumbSize
		))
		.states_([["O", Color.black], ["Ø", Color.green(0.6)]])
		.action = { |view|
			var phase = if(view.value == 0) {
				0
			} {
				pi
			};
			if (phaseButtons[i].states.size == 1) {
				phaseButtons[i]
				.states_([["O", Color.black], ["Ø", Color.green(0.6)]])
			};
			multiSliderPhase.value = multiSliderPhase.value.put(i, phase.linlin(0, 2pi, 0, 1));
			phases.put(i, phase);
			x.set(\phases, phases)
		}
	};

	multiSliderPhase = if (numSliders < 36) {
		MultiSliderView(win, Rect(
			1, upperHeight - 50 / 2 + lablesHeight + indexThumbSize, numSliders * indexWidth + 6, upperHeight - 50 / 2
		));
	} {
		MultiSliderView(win, Rect(
			1, upperHeight - 50 / 2 + lablesHeight + indexThumbSize, width, upperHeight - 50 / 2
		));
	};

	multiSliderPhase.value_(((0 ! numSliders)))
	.showIndex_(true)
	.indexThumbSize_(indexThumbSize)
	.action_{ |sliders|
		var phases = sliders.value.linlin(0, 1, 0, 2pi);
		if(phases[sliders.index] == 0.0 || (phases[sliders.index] == 180.0)) {
			phaseButtons[sliders.index]
			.states_([["O", Color.black], ["Ø", Color.green(0.6)]]);
			phaseButtons[sliders.index].value = (phases[sliders.index] % 180)
		} {
			phaseButtons[sliders.index]
			.states_([["↓", Color.blue(1, 0.5)]])
		};
		x.set(\phases, phases)
	}
	.mouseOverAction_{ |view, x, y|
		var mouseOverIndex;
		mouseOverIndex = (x / indexWidth).round;
		interactiveLableUpdate.(x, y, multiSliderPhase, interactiveLablePhase, "Hz;\n", 360, "°", "");
		//multiSliderPhase.stringColor_(if(mouseOverIndex * freqBox.value < (s.sampleRate / 2)) { Color.blue(1, 0.5) } { Color.grey(0.5) });
	}
	.mouseMoveAction_{ |view, x, y|
		var mouseMoveIndex;
		mouseMoveIndex = (x / indexWidth).round;
		interactiveLableUpdate.(x, y, multiSliderPhase, interactiveLablePhase, "Hz;\n", 360, "°", "");
	}
	.mouseUpAction_{ |view, x, y|
		interactiveLableUpdate.(x, y, multiSliderPhase, interactiveLablePhase, "Hz;\n", 360, "°", "")
	}
	.mouseLeaveAction_{
		interactiveLablePhase.string_("Phases" +
			"(If the mouse is moved too quickly, some phase buttons may not be updated.)")
		.bounds_(Rect(
			multiSliderPhase.bounds.extent.x - 920 / 2,
			multiSliderPhase.bounds.extent.y - 20 / 2,
			920,
			25));
		multiSliderPhase.showIndex_(false)
	}
	.mouseEnterAction_{
		multiSliderPhase.showIndex_(true)
	}
	.background_(Color.grey(0.9, 0.1))
	.colors_(Color.grey(0.3, 0.5), Color.grey(0.3, 0.5));

	interactiveLablePhase = lable.(multiSliderPhase,
		multiSliderPhase.bounds.extent.x - 920 / 2,
		multiSliderPhase.bounds.extent.y - 20 / 2,
		920,
		25,
		\left, 24, "Phases" +
		"(If the mouse is moved too quickly, some phase buttons may not be updated.)", Color.blue(1, 0.5), Color.grey(0.9, 0.7));

	interactiveLableUpdate = { |x, y, where, which, hzORphase, how, unit, antialiasing|
		var index, indexLable, indexLableSize, value, cps;
		index =(x / indexWidth).ceil.clip(1, ~numSliders);
		indexLable = index.asInteger.asString;
		indexLableSize = indexLable.size;
		value = y.linlin(0, where.bounds.extent.y - 1, how, 0).asInteger;
		cps = index * freqBox.value;
		which.string_(
			indexLable ++ ":" + cps ++ hzORphase + (" " ! (indexLableSize + 2)).join + value ++ unit +
			(if(s.sampleRate / 2 <= cps) { antialiasing } { "" })
		);

		which.bounds = which.bounds.size_(which.sizeHint);

		which.bounds_(
			x = if(x + which.bounds.width < where.bounds.extent.x) {
				x
			} {
				x - which.bounds.width
			};
			y = where.bounds.extent.y - which.bounds.height / 2;
			Rect(x, y, which.bounds.width, which.bounds.height);
		)
	};

	lable.(win, 10, upperHeight + 3, 300, 20, \left, 12,
		"Base frequency (Hz):                  (test with 393.44)",
		Color.black, Color.clear);

	freqBox = NumberBox(win, Rect(130, upperHeight + 3, 50, 20))
	.font_(Font("Arial", 12));

	freqBox.value_(~freq)
	.action_{ |numb|
		var freqBase = numb.value;
		x.set(\freq, numb.value);
		waveform.cycle_(s.sampleRate / numb.value);
		antiAliasing.(freqBase)
	};

	freqScope = FreqScopeView(win, Rect(2, upperHeight + 3 + 22, 511, lowerHeight - 28));
	freqScope.active_(true);

	ListView(win, Rect(2 + 511 - 30, upperHeight + 3 + 22, 30, 34))
	.items_(["lin", "log"])
	.action_{ |menu|
		freqScope.freqMode_(menu.value);
	};

	ampNum = NumberBox(win, Rect(3 + 511, upperHeight + 3, 29, 20))
	.font_(Font("Arial", 12))
	.value_(0.1)
	.action_{ |numb|
		ampSlider.value_(numb.value);
		x.set(\mul, numb.value);
	};

	ampSlider = Slider(win, Rect(2 + 511, upperHeight + 2 + 22, 31, lowerHeight - 26))
	.value_(~mul)
	.action_{
		x.set(\mul, ampSlider.value);
		ampNum.value_(ampSlider.value);
	};

	ServerMeterView(s, win, (2 + 511 + 30) @ (upperHeight + 18), 0, 2);


	waveformView = View(win, Rect(
		2 + 511 + 30 + ServerMeterView.getWidth(0, 1, s),
		upperHeight + 1,
		255,
		255
	));
	waveform = Stethoscope(s, 1, view: waveformView);
	// waveform = Plotter(parent: waveformView)
	// value =


	SynthDef(\overtone_additive, { |overtoneFirst = 1, overtoneLast = 1, numNotZero = nil|
		var freq, phases, mul, numOvertones, maxLayers, amplitudes, temp, signalArray, sum;
		mul = \mul.kr(~mul);
		freq = \freq.kr(~freq);
		phases = \phases.kr(0 ! ~numSliders);
		maxLayers = ~numSliders;
		amplitudes = \amplitudes.kr(0 ! maxLayers);
		numOvertones = if(numNotZero == nil) { 
			overtoneLast - overtoneFirst + 1 
			} {			
			numNotZero - overtoneFirst + 1 
		}.clip(1, ~numSliders);
		signalArray = maxLayers.collect { |i|
			var nthOvertone, minDetect, maxDetect, aliasingDetect, switch;
			nthOvertone = i + 1 * freq;
			minDetect = overtoneFirst <= (i + 1);
			maxDetect = overtoneLast >= (i + 1);
			aliasingDetect = nthOvertone < (SampleRate.ir / 2);
			switch = (minDetect * maxDetect * aliasingDetect).lag(0.2);
			SinOsc.ar(nthOvertone, phases[i].lag(0.2)) * switch * numOvertones.reciprocal.sqrt.lag(0.2)
		};
		signalArray = signalArray * amplitudes.lag(0.2);
		sum = signalArray.sum * mul.lag(0.2);
		Out.ar(0, sum ! 2);
	}).add;

	s.sync;

	waveform.cycle_(s.sampleRate / ~freq);
	x = Synth(\overtone_additive);
}
)

I found that very fast mouse movements were not fully reflected in the button value. When you move the mouse pointer in MultiSliderView, the value of the corresponding slider button should change. However, some of them do not change. Here is a video recording of this:
link to video

Is this due to the wrong code structure or due to the GUI response limit?

The relevant part of the code is as follows:

(
s.options.numWireBufs = 128; // 256;

~numSliders = 64; // Tested at up to 120
~mul = 0.1;
~freq = 200; // 393.44

s.waitForBoot {
	var numSliders, indexThumbSize, indexWidth, centre, upperHeight, lowerHeight, width, win;
	var freqBox, multiSliderFreq, multiSliderPhase, phases, phaseButtons, ampNum, ampSlider, freqScope;
	var lable, lables, waveformView, waveform;
	var lablesHeight, interactiveLableFreq, interactiveLablePhase, interactiveLableUpdate, antiAliasing;

	numSliders = ~numSliders;
	indexThumbSize = 16;
	indexWidth = indexThumbSize + 1;
	centre = [Window.screenBounds.width, Window.screenBounds.height] / 2;

	width = if (numSliders <= 36) {
		36 * indexWidth + 6 + 255;
	} {
		numSliders * indexWidth + 6
	};

	win = Window("overtone explorer", Rect(centre[0] - (width / 2), centre[1] - 325, width, 650));
	win.front.onClose_{
		x.free;
		freqScope.kill;
	}
	.acceptsMouseOver_(true);

	lowerHeight = 250;
	upperHeight = win.bounds.height - lowerHeight ;

	lable = { |parent, left, top, width, height, align, fontSize, string, colour, background|
		StaticText(parent, Rect(left, top, width, height))
		.align_(align)
		.font_(Font("Arial", fontSize))
		.stringColor_(colour)
		.string_(string)
		.background_(background)
	};

	lablesHeight = 36;


	phases = 0 ! ~numSliders;

	phaseButtons = ~numSliders.collect { |i|
		Button(win, Rect(
			indexThumbSize + 1 * i + 4, upperHeight / 2 - 15 + 26, indexThumbSize, indexThumbSize
		))
		.states_([["O", Color.black], ["Ø", Color.green(0.6)]])
		.action = { |view|
			var phase = if(view.value == 0) {
				0
			} {
				pi
			};
			if (phaseButtons[i].states.size == 1) {
				phaseButtons[i]
				.states_([["O", Color.black], ["Ø", Color.green(0.6)]])
			};
			multiSliderPhase.value = multiSliderPhase.value.put(i, phase.linlin(0, 2pi, 0, 1));
			phases.put(i, phase);
			//x.set(\phases, phases)
		}
	};

	multiSliderPhase = if (numSliders < 36) {
		MultiSliderView(win, Rect(
			1, upperHeight - 50 / 2 + lablesHeight + indexThumbSize, numSliders * indexWidth + 6, upperHeight - 50 / 2
		));
	} {
		MultiSliderView(win, Rect(
			1, upperHeight - 50 / 2 + lablesHeight + indexThumbSize, width, upperHeight - 50 / 2
		));
	};

	multiSliderPhase.value_(((0 ! numSliders)))
	.showIndex_(true)
	.indexThumbSize_(indexThumbSize)
	.action_{ |sliders|
		var phases = sliders.value.linlin(0, 1, 0, 2pi);
		if(phases[sliders.index] == 0.0 || (phases[sliders.index] == 180.0)) {
			phaseButtons[sliders.index]
			.states_([["O", Color.black], ["Ø", Color.green(0.6)]]);
			phaseButtons[sliders.index].value = (phases[sliders.index] % 180)
		} {
			phaseButtons[sliders.index]
			.states_([["↓", Color.blue(1, 0.5)]])
		};
	//	x.set(\phases, phases)
	}
	.mouseOverAction_{ |view, x, y|
		var mouseOverIndex;
		mouseOverIndex = (x / indexWidth).round;
		interactiveLableUpdate.(x, y, multiSliderPhase, interactiveLablePhase, "Hz;\n", 360, "°", "");
	}
	.mouseMoveAction_{ |view, x, y|
		var mouseMoveIndex;
		mouseMoveIndex = (x / indexWidth).round;
		interactiveLableUpdate.(x, y, multiSliderPhase, interactiveLablePhase, "Hz;\n", 360, "°", "");
	}
	.mouseUpAction_{ |view, x, y|
		interactiveLableUpdate.(x, y, multiSliderPhase, interactiveLablePhase, "Hz;\n", 360, "°", "")
	}
	.mouseLeaveAction_{
		interactiveLablePhase.string_("Phases" +
			"(If the mouse is moved too quickly, some phase buttons may not be updated.)")
		.bounds_(Rect(
			multiSliderPhase.bounds.extent.x - 920 / 2,
			multiSliderPhase.bounds.extent.y - 20 / 2,
			920,
			25));
		multiSliderPhase.showIndex_(false)
	}
	.mouseEnterAction_{
		multiSliderPhase.showIndex_(true)
	}
	.background_(Color.grey(0.9, 0.1))
	.colors_(Color.grey(0.3, 0.5), Color.grey(0.3, 0.5));

	interactiveLablePhase = lable.(multiSliderPhase,
		multiSliderPhase.bounds.extent.x - 920 / 2,
		multiSliderPhase.bounds.extent.y - 20 / 2,
		920,
		25,
		\left, 24, "Phases" +
		"(If the mouse is moved too quickly, some phase buttons may not be updated.)", Color.blue(1, 0.5), Color.grey(0.9, 0.7));

	interactiveLableUpdate = { |x, y, where, which, hzORphase, how, unit, antialiasing|
		var index, indexLable, indexLableSize, value, cps;
		index =(x / indexWidth).ceil.clip(1, ~numSliders);
		indexLable = index.asInteger.asString;
		indexLableSize = indexLable.size;
		value = y.linlin(0, where.bounds.extent.y - 1, how, 0).asInteger;
		/*cps = index * freqBox.value;
		which.string_(
			indexLable ++ ":" + cps ++ hzORphase + (" " ! (indexLableSize + 2)).join + value ++ unit +
			(if(s.sampleRate / 2 <= cps) { antialiasing } { "" })
		);*/

		which.bounds = which.bounds.size_(which.sizeHint);

		which.bounds_(
			x = if(x + which.bounds.width < where.bounds.extent.x) {
				x
			} {
				x - which.bounds.width
			};
			y = where.bounds.extent.y - which.bounds.height / 2;
			Rect(x, y, which.bounds.width, which.bounds.height);
		)
	};
}
)

Thank you in advance!

99% sure it’s just a response limit.

If you watch the mouse pointer while you’re moving the mouse quickly, you’ll see that it doesn’t move smoothly, but rather it can jump a large distance in one cycle.

You can test this by collecting an array of x deltas (new x - old x).

(
var x = 20, y = 20;
var color = Color(0.5, 1, 0.5);

a = List.new;

w = Window("test", Window.screenBounds).front;
w.acceptsMouseOver_(true);
w.layout = VLayout(
	u = UserView()
);
u.drawFunc = { |view|
	Pen.color_(color)
	.fillOval(Rect(x - 4, y - 4, 8, 8))
};
u//.acceptsMouseOver_(true)
.mouseOverAction = { |view, mouseX, mouseY|
	a.add(mouseX - x);
	x = mouseX;
	y = mouseY;
	u.refresh;
};
)

Move the mouse back and forth quickly a few times, then:

a

-> List[159, 20, 31, 40, 51, 69, 82, 101, 130, 157, 181, 202, 129, -13, -22, -27, -34, -1042, -199, -28, 26, 42, 59, 83, 111, 134, 160, 188, 204, 200, 146, -29, -41, -52, -69, -86, -117, -144, -168, -195, -206, -199, -34, 41, 68, 98, 129, 165, 198, 202, 203, 211, 34, -20, -28, -38, -51, -67, -78, -93, -109, -124, -130, -125, -127, -129, -122, -114, 0, 42, 69, 98, 124, 156, 190, 207, 199, 209, 52, -26, -39, -57, -83, -104, -126, -152, -167, -177, -172, -164, -81, 33, 52, 75, 98, 122, 144, 161, 167, 162, 153, 13...etc...

Notice that some of these deltas are as high as 200 pixels (even one -1099 but that may be a glitch). If your MultiSliderView has, say, 20 pixels per column, a 200 px jump would skip over nine columns, never touching them.

hjh

1 Like

Thank you! You have enlightened me!

After testing your code, I changed mine accordingly:
I noticed that I had added updating floating text to various mouse actions, but had not added updating-button-status to various mouse actions. I have added updating-mouse-button-status to the .mouseMoveAction, and the problem is solved.

https://sccode.org/1-5hC