Drawing sample playback progress with Pen efficiently?

I have some code in a larger project that is visualizing sample playback progress with Pen in UserViews. I suspect that it was the cause of some issues I was having with seemingly random crashes. A lot of UserViews can be active at once and are rapidly started and stopped by midi input.

Here is a simplified example that will run on other machines:

(
~sf= Platform.resourceDir++"/sounds/a11wlk01.wav";
b= Buffer.read(s, ~sf);

w= Window().front;
~uView= UserView(w, Rect(50, 50, 130, 20));

t= Task({
	var elapsed, mul= 10, time;
	time= b.duration;
	(time*mul).do({|i|
		elapsed= (i+1)/(time*mul);
		~uView.drawFunc = {
			Pen.fillColor = Color.red.alpha_(0.6);
			Pen.addRect(
				Rect(0, 0, (~uView.bounds.width*elapsed), ~uView.bounds.height)
			);
			Pen.fill;
		};
		~uView.refresh;
		(1/mul).wait;
	});
}, AppClock);

Button(w, Rect(50, 50, 130, 20))
.states_([["play", Color.black, Color.green], ["stop", Color.black, Color.white.alpha_(0.3)]])
.action_({|v|
	if(v.value == 1, { a= Synth(\sf, [\bufnum, b]); t.play; }, {
		a.free; t.stop.reset;
		~uView.drawFunc= {
			Pen.fillColor = Color.red.alpha_(0.0);
				Pen.addRect( Rect(0, 0, ~uView.bounds.width, ~uView.bounds.height) );
				Pen.fill;
		};
	});
});

SynthDef(\sf, { |bufnum = 0|
    Out.ar(0,PlayBuf.ar(1, bufnum, BufRateScale.kr(bufnum), doneAction: Done.freeSelf));
}).add;

w.onClose= {b.free};

)

In the old code I used Routine as {}.fork instead of Task and refreshed the whole window (w.refresh) instead of just the UserView, so I guess this now optimizes it somewhat. But it got me wondering, as I’m not too familiar with Pen and UserView - is this here an efficient/good way of doing this or are there other better approaches?

I’m no expert, but a couple of strategies that I would try are:

  • First (obviously!) try reducing the refresh rate of the UserViews to see if this stops the random crashes. If it doesn’t, maybe the error is caused by something else?
  • Try using slightly different refresh rates for all UserViews so they aren’t all refreshing at the same time.
  • Try using UserView methods animate and frameRate to schedule refreshes, instead of using a Task, to see if that makes a difference.
  • Since the red box in the UserView is always growing as the sample is played, you could turn off the UserView clearOnRefresh and then call clearDrawing only at the start of the sample playback. This might save a bit of CPU work.

Hope that helps.
Paul

I’m not too familiar with Pen and UserView - is this here an efficient/good way of doing this or are there other better approaches?

Assuming you’re not doing anything too unusual or customized, SoundFileView would be my go-to for basic visualization of playback progress through an audio sample.

And, for consistency with your current design, you could attach a mouseDownAction to a SoundFileView to make it behave like a button, so that it plays when clicked.

Thank you Paul,

Reducing the refresh rate is a good start yes!

I was having doubts if defining the drawFunc again every time I need to update the UserView is a good thing. Would it make much difference perfomance-wise to define the drawFunc once and change the variables and refresh from a separate Task?

Do you know if the animate feature saves on resources or if it just refreshes the UserView at a set rate in the background?

Since I’m layering Views and relying on alpha values of Color for it to shine through it would become too opaque by turning off clearOnRefresh. But I didn’t know about the clearDrawing function - that cleaned up some code :slight_smile:

Thanks Eli, I’m afraid I’m doing something quite customized (I need to read the text and the buttons are small and a plenty) but others searching the keywords in this post later will surely find this useful.

I’m not really sure about either of these - I don’t have deep knowledge, particularly because Qt is used underneath for the GUI Views. So for example, ‘animate’ is defined in QUserView.sc as:

animate_ { arg bool; this.invokeMethod( \animate, bool ); animate = bool; }

where ‘invokeMethod’ is a method of QObject. So it’s hard to know if it’s more efficient. But people say SCLang is slow, especially compared to C++, so it might make a difference?
[maybe someone with deeper Qt knowledge would chip in here?]

Since you’re using lots of views, I would maybe try these tweaks anyway to see if they improve performance even a little?.
Hope that helps,
Paul

I had a go at stress testing this and found that I was wrong, I got a worse framerate with animate (2-3 fps) vs. task (8-9 fps) in this test:

// Version 1 - using animate

// Setup
(
~sf= Platform.resourceDir++"/sounds/a11wlk01.wav";
b = Buffer.read(s, ~sf);
w = Window("test", Rect(50, 370, 900, 300)).front;
)

// Build Views
(
d =();
d.numCopies = 900;
d.targetFrameRate = 10;
d.trans = 0.2;  // view transparency

w.view.removeAll;  // remove old
d.arrElapsed =  0 ! d.numCopies;
d.arrViews = nil ! d.numCopies;
d.arrTasks = nil ! d.numCopies;
d.frameCount = 0;
d.timeSinceStart = 0;
d.numCopies.do({arg i;
	d.arrViews[i] = UserView(w, Rect(800.rand, 280.rand, 130, 20))
		.background_(Color.black.alpha_(d.trans));
	d.arrViews[i].drawFunc = {arg view;
		var framerate, elapsed;
		// print framerate of view 0 every second
		if (i == 0, {
			if (d.frameCount == 0, {
				d.startTime = Main.elapsedTime;
				d.printSecs = 0;
			}, {
				d.timeSinceStart =  Main.elapsedTime - d.startTime;
				if (d.printSecs != d.timeSinceStart.asInteger, {
					d.printSecs = d.timeSinceStart.asInteger;
					framerate = view.frameRate;
					("Average frame rate =" + framerate.round(0.01)).postln;
				});
			});
			d.frameCount = d.frameCount + 1;
		});
		Pen.fillColor = Color.red.alpha_(d.trans);
		elapsed = (d.timeSinceStart / b.duration).min(1);
		Pen.addRect(
			Rect(0, 0, (view.bounds.width * elapsed), view.bounds.height)
		);
		Pen.fill;
	};

});

Button(w, Rect(50, 80, 130, 20))
.states_([["play", Color.black, Color.green], ["stop", Color.black, Color.white.alpha_(0.3)]])
.action_({|v|
	if(v.value == 1, {
		d.frameCount = 0;
		d.timeSinceStart = 0;
		a = Synth(\sf, [\bufnum, b]);
		d.numCopies.do({arg i;
			d.arrViews[i].frameRate_(d.targetFrameRate);
			d.arrViews[i].animate_(true);
		});
	}, {
		a.free;
		d.numCopies.do({arg i;
			d.arrViews[i].animate_(false);
		});
	});
});

SynthDef(\sf, { |bufnum = 0|
    Out.ar(0,PlayBuf.ar(1, bufnum, BufRateScale.kr(bufnum), doneAction: Done.freeSelf));
}).add;

w.onClose= {b.free};

)

w.close;


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

// Version 2 - using Task

// setup
(
~sf= Platform.resourceDir++"/sounds/a11wlk01.wav";
b = Buffer.read(s, ~sf);
w = Window("test", Rect(50, 370, 900, 300)).front;
)

// Build Views
(
d =();
d.numCopies = 900;
d.targetFrameRate = 10;
// d.trans = 1;  // view transparency
d.trans = 0.2;  //

w.view.removeAll;
d.arrElapsed =  0 ! d.numCopies;
d.arrViews = nil ! d.numCopies;
d.arrTasks = nil ! d.numCopies;
d.frameCount = 0;
d.timeSinceStart = 0;
d.numCopies.do({arg i;
	d.arrViews[i] = UserView(w, Rect(800.rand, 280.rand, 130, 20))
		.background_(Color.black.alpha_(d.trans));
	d.arrViews[i].drawFunc = {arg view;
		var framerate;
		// print framerate of view 0 every second
		if (i == 0, {
			if (d.frameCount == 0, {
				d.startTime = Main.elapsedTime;
				d.printSecs = 0;
			}, {
				d.timeSinceStart =  Main.elapsedTime - d.startTime;
				if (d.printSecs != d.timeSinceStart.asInteger, {
					d.printSecs = d.timeSinceStart.asInteger;
					framerate = d.frameCount / d.timeSinceStart;
					("Average frame rate =" + framerate.round(0.01)).postln;
				});
			});
			d.frameCount = d.frameCount + 1;
		});
		Pen.fillColor = Color.red.alpha_(d.trans);
		Pen.addRect(
			Rect(0, 0, (view.bounds.width * d.arrElapsed[i]), view.bounds.height)
		);
		Pen.fill;
	};

	d.arrTasks[i] = Task({
		var  mul = d.targetFrameRate, time;
		time = b.duration;
		(time*mul).do({|slice|
			d.arrElapsed[i] = (slice + 1) / (time * mul);
			d.arrViews[i].refresh;
			(1 / mul).wait;
		});
	}, AppClock);

});

Button(w, Rect(50, 80, 130, 20))
.states_([["play", Color.black, Color.green], ["stop", Color.black, Color.white.alpha_(0.3)]])
.action_({|v|
	if(v.value == 1, {
		d.frameCount = 0;
		d.timeSinceStart = 0;
		a = Synth(\sf, [\bufnum, b]);
		d.numCopies.do({arg i; d.arrTasks[i].play});
	}, {
		a.free;
		d.numCopies.do({arg i; d.arrTasks[i].stop.reset});
	});
});

SynthDef(\sf, { |bufnum = 0|
    Out.ar(0,PlayBuf.ar(1, bufnum, BufRateScale.kr(bufnum), doneAction: Done.freeSelf));
}).add;

w.onClose= {b.free};

)

Sorry for the noise.
Paul

1 Like

Wow, this was very insightful, and thorough! I was looking for a way to use .bench to benchmark the difference but this is just on point.

I’m getting the same results here on an M1 mac. That is a pretty substantial difference.

And the way you separate Task from the drawFunc here seems like the proper way to do it, as opposed to the code in my first post.

(Also I’m reminded to use Event as a Dictionary more when sketching code, it looks so clean and makes so much more sense)

Thanks again Paul!

1 Like