Display playhead position in GUI in a synth that uses PlayBuf

Hi

I have a very simple synth that plays a buffer using PlayBuf and I need to get the “playhead” position. I though of different options but none works as I expect. I find difficult to think “server side”.

Any suggestions? Thanks

enrike

SendReply on the server side and OSCFunc or similar in the language should work.

I don’t think there is a way to easily do this with PlayBuf but with BufRd it is pretty straight forward. The Phasor, which reads through the buffer sends it current value to the language through the OSC responder at a rate of updateRate (20 by default in my example). The EZslider is updated from the OSC responder.

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

(
SynthDef(\buf, {|buf, rate = 1, updateRate = 20|
	var phasor = Phasor.ar(0, BufRateScale.kr(buf) * rate, BufFrames.kr(buf));
	var sig = BufRd.ar(1, buf, phasor, 1);
	SendReply.ar(Impulse.ar(updateRate), "/bufPos", phasor);
	Out.ar(0, sig!2)
}).add;
);

(    
w = Window(\, Rect(200, 200, 400, 50)).front;
g = EZSlider(w, 400@16);
g.setColors(Color.grey,Color.white);
);

(
Synth(\buf, [\buf, b]);

o = OSCdef(\x, {|msg|
	var pos = (msg[3]/b.numFrames);
	{ g.value_(pos) }.fork(AppClock)
}, "/bufPos");
)
2 Likes

Hi thanks for the answers. But my problem is how to find out the current play position in a SynthDef that uses PlayBuf to read a Buffer. I know about SendReplay and using a BufRd+Phasor technique to play a Buffer but I cannot use it here, sorry if I wasn’t clear enough.

I have tried to create a Phasor that runs “in parallel”, so to say, to the PlayBuf and reports the position but that did not work and it feels to me like a weird idea. I thought maybe someone has a better understanding of server-side programming and might have a nice solution to this.

thanks

Unfortunately there is no way to retrieve the current play position from a PlayBuf. Why do you say you can’t use BufRd + Phasor? If the problem is that you are playing too long a buffer, you can try GitHub - esluyter/super-bufrd: UGens for accessing long buffers with subsample accuracy which is a hacky but well functioning solution - the documentation is incomplete but I can give some pointers for your situation, and it comes with a PlayBuf equivalent that does give you a playhead position.

But if your buffer is shorter than 6 minutes or so I would think some version of BufRd + Phasor would be an easier solution - if you like you could even wrap them together into a pseudo-PlayBuf class

@enrike
Should the server-side feedback be included for displaying the playback position?
If you think throughout client-side only, you might get the code like the following, and it properly works:

(
2.do{ |i|
	(
		SynthDef(\bufferPlayer_ch ++ (i + 1), { | buf, rate = 1, out, trig, startPos |
			Out.ar(out,
				PlayBuf.ar(i+1, buf, rate * BufRateScale.kr(buf), trig, startPos, doneAction: Done.freeSelf)
			)
		}).add
)};

~playBuffer = { |buf = 0, rate = 1, out = 0, trig = 1, startPos = 0 |
	var duration, win, slider, started, timeElasped, display;
	
	duration = buf.duration / rate;
	
	win = Window(\, Rect(200, 200, 400, 50)).front;
	slider = EZSlider(win, 400@16, "status", ControlSpec(0, duration));
	slider.setColors(Color.grey,Color.white);
	started = Main.elapsedTime;
	timeElasped = { Main.elapsedTime - started };
	
	Synth(\bufferPlayer_ch++(buf.numChannels), [
		\buf, buf, \rate, rate, \out, 0, \trig, trig, \startPos, startPos
	]);
	
	display = { loop {
		var currentTime = timeElasped.();
		slider.value_(currentTime);
		currentTime.postln;
		0.05.wait;
		if(currentTime > duration) {win.close; display.stop }
	}
	}.fork(AppClock)
}
)

b = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav");
c = Buffer.readChannel(s, Platform.resourceDir +/+ "sounds/a11wlk01-44_1.aiff",0, -1, 0!2);

b.numChannels
~playBuffer.(b)
~playBuffer.(b, rate: 2)
~playBuffer.(b, rate: 0.5)
c.numChannels
~playBuffer.(c)

Sorry I forgot to say it has to be PlayBuf because the sounds are often close to 8 mins

mm… That’s actually quite an interesting idea, I think it might work well for me. I guess I was fixed with the idea that I needed to have it done server-side. Many thanks for the tip!

@enrike
Happy to hear it!
When using SinOsc or Noise for rate, there would be a formula per Ugen or formulas which can generally be applicable. I am not good at math, but there should be ways.

You can also pose the starting position in GUI if you need. It is not difficult to do

If you mix server side and client side clocks you will almost certainly experience clock drift, the server side clock and language side clock are never really in sync and for long buffers it might be a problem. You are saying it dit not work to run a Phasor in parallel reporting the position, what about if did not work? I still think this sounds like you best option, using Phasor or Sweep running in parallel with PlayBuf to report the position back to the language trough OSC messages or via a control bus.

1 Like

well… I am not really good at server side stuff and I am not sure what I am doing wrong but the Phasor send some random-ish values. This is my Synth

SynthDef(\splayerS, {|buffer, amp=1, out=0, rate=1, dir=1, loop=0, index=0, trigger=0|
	var signal, phasor;
	signal = PlayBuf.ar(2, buffer, BufRateScale.kr(buffer)*rate*dir, loop: loop);//, doneAction:2);
	phasor = Phasor.ar(trigger, rate*BufRateScale.kr(buffer), 0, 1);
	SendReply.kr( LFPulse.kr(12, 0), '/pos', phasor, index);
	Out.ar(out, signal *  amp);
}).load;


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

p = Synth(\splayerS, [bufnum:b.bufnum, trigger: 1, loops:2, rate:0.7, start: 0.2, end:0.21, index: 9999]);

h = OSCdef(\playhead9999, {|msg, time, addr, recvPort|
	msg[3].postln;
}, '/pos');

OK… quick one. I have to preface this by saying that I’m under some extra pressures lately, so I can’t promise to support this block of code… you’re on your own. It’s just that there’s a way, and seemingly nobody else tried this approach(?), so maybe it’s worth dropping a code block in.

But:

a/ “you can try GitHub - esluyter/super-bufrd: UGens for accessing long buffers with subsample accuracy” – that is, he posted a solution for long buffers and you went and ignored it…

and b/ you’re assuming that the only way to load the file is into one buffer. You could also chunk the file into multiple buffers where each one is within reach of BufRd.

Here the file is short and the chunks are short too, but you could raise ~chunkSize to, say, three minutes = 3 * 60 * 44100 = 7938000 and not be at any risk of losing resolution. EDIT: I forgot that the chunk size should be a multiple of the control block size, so, (3 * 60 * 44100).round(64) = 7937984.

I tried to label everything, hope this helps, and sorry that I am unlikely to be able to follow up on this thread further.

p = Platform.resourceDir +/+ "sounds/a11wlk01.wav";

f = SoundFile.openRead(p);
~frames = f.numFrames;  // 188893
f.close;

// for arguments' sake, let's partition into 32768-frame chunks

~chunkSize = 32768;  // make sure this is a multiple of the control block size!
~numChunks = (~frames / ~chunkSize).roundUp.asInteger;

(
// ContiguousBlockAllocator should not be returning floats, w-t-h
// No time to figure that out now... jeez... so I'll manually `asInteger` it
var buf1 = s.bufferAllocator.alloc(~numChunks).asInteger;

b = Array.fill(~numChunks, { |i|
	var size = if(i + 1 == ~numChunks) {
		~frames % ~chunkSize
	} {
		~chunkSize
	};
	Buffer.alloc(s, size, 1, completionMessage: { |buf|
		buf.readMsg(p, ~chunkSize * i, size, 0)
	}, bufnum: buf1 + i)
});
)

v = SoundFileView(nil, Rect(800, 200, 500, 400)).front;
v.readFile(f);
v.timeCursorOn = true;

(
a = { |startBuf, chunkSize, numBufs, remainderFrames|
	// localin/out are for looping
	// if you're not looping, you can delete this and not use a 'trig'
	var reachedEnd = LocalIn.kr(1);
	var phasor = Phasor.ar(trig: reachedEnd, start: 0, end: chunkSize);
	var nextBufTrig = HPZ1.ar(phasor) < 0;
	var bufIncrement = PulseCount.ar(nextBufTrig) % numBufs;
	var onLastBuf = (bufIncrement + 1) >= numBufs;
	var checkEnd = onLastBuf * (phasor >= remainderFrames);
	var sig = BufRd.ar(1, startBuf + bufIncrement, phasor, loop: 0);
	SendReply.ar(Impulse.ar(10), '/playpos', [phasor, bufIncrement]);
	LocalOut.kr(checkEnd);  // to not loop: FreeSelf.kr(A2K.kr(checkEnd)) I think
	(sig * 0.5).dup
}.play(args: [
	startBuf: b[0], chunkSize: ~chunkSize,
	numBufs: ~numChunks, remainderFrames: ~frames % ~chunkSize
]);

o = OSCFunc({ |msg|
	defer {
		v.timeCursorPosition = (msg[3] + (~chunkSize * msg[4])).asInteger;
	};
}, '/playpos', s.addr, argTemplate: [a.nodeID]);

a.onFree { o.free };
)

a.free;

b.do(_.free);

hjh

Sorry I forgot to answer to this in a previous reply. I know about this but I don’t want to use any extra stuff that needs to be installed.

As for your suggestion, It is an interesting idea. I will have a look at it. Thanks.

The reason you are getting weird readings is the lack of conversion from time (PlayBuf) to samples (Phasor). Try this SynthDef. The post window now shows values bewteen 0 and 1, i.e fractions of the whole sample.

SynthDef(\splayerS, {|buffer, amp=1, out=0, rate=1, dir=1, loop=0, index=0, trigger=0|
	var signal, phasor;
	signal = PlayBuf.ar(1, buffer, BufRateScale.kr(buffer)*rate*dir, loop: loop);//, doneAction:2);
	phasor = Phasor.ar(trigger, rate*BufRateScale.kr(buffer), 0, BufFrames.ir(buffer));
	SendReply.kr( LFPulse.kr(12, 0), '/pos', phasor/BufFrames.ir(buffer), index);
	Out.ar(out, signal *  amp);
}).add;

of course! thanks! I guess I go kind of blind after going over and over again the same code…

Hey Eric, I took a look at your SuperPlay/SuperBuf classes, looks really useful. I tried installing and everything appears to be in order but when I run the examples from the SuperPlayBuf I get

this error message: exception in GraphDef_Recv: UGen 'SuperBufFrames' not installed.

*** ERROR: SynthDef temp__0 not found
FAILURE IN SERVER /s_new SynthDef not found
WARNING: keyword arg 'cuePos' not found in call to Meta_SuperPlayBuf:ar
WARNING: keyword arg 'cueTrig' not found in call to Meta_SuperPlayBuf:ar
-> Synth('temp__1' : 1001)
exception in GraphDef_Recv: UGen 'SuperBufFrames' not installed.
*** ERROR: SynthDef temp__1 not found
FAILURE IN SERVER /s_new SynthDef not found

Any idea what I did wrong? I am on OSX M1 and used this code to install from the terminal (the one posted on the GitHub page:

git clone https://github.com/esluyter/super-bufrd.git
cd super-bufrd
mkdir build
cd build
cmake -DSC_PATH=/path/to/sc3source/ -DCMAKE_BUILD_TYPE=Release ..
cmake --build . --config Release

There are already two great solutions by @jamshark70 and @Thor_Madsen!
I also coded based on @Thor_Madsen’s first code and my first code as follows (you can see the time sync issue between server and client in the gui):

(
2.do{ |i|
	(
		SynthDef(\bufferPlayer_ch ++ (i + 1), { | buf, rate = 1, out, trig, startPos |
			var sweep, sig;
			rate = rate * BufRateScale.kr(buf);
			sweep = Sweep.kr;
			sig = PlayBuf.ar(i + 1, buf, rate, trig, startPos, doneAction: Done.freeSelf);
			SendReply.ar(Impulse.ar(20), "/sweep", sweep);
			Out.ar(out, sig)
		}).add
) };

~playBuffer = { |buf = 0, rate = 1, out = 0, trig = 1, startPosRatio = 0 |
	var bufDuration, playbackDuration, startTime, startPos, started, currentTime, win, sliders, display;
	
	bufDuration = buf.duration;
	playbackDuration = bufDuration / rate;
	startTime = startPosRatio * bufDuration;
	startPos = startTime * buf.sampleRate;
	started = Main.elapsedTime;
	currentTime = { Main.elapsedTime - started };	
	win = Window(\, Rect(200, 200, 400, 100)).front;
	sliders = 2.collect { |i| 
		EZSlider(win, Rect(0, 20 * i, 400, 16), "status", ControlSpec(0, playbackDuration)) };
	
	Synth(\bufferPlayer_ch++(buf.numChannels), [
		\buf, buf, \rate, rate, \out, 0, \trig, trig, \startPos, startPos
	])
	.onFree{ 
		"synth freed".postln; 
		{ win.close }.defer; 
		display.stop; 
		(
			"                      rate:" + rate ++ "\n"
			"              started from:" + (startTime.round(0.01) / rate) + "sec. playing rate is considered." ++ "\n"
			"                total time:" + currentTime.().round(0.01) + "sec\n"
			"expected playback duration:" + (playbackDuration - (startTime / rate)).round(0.01) + "sec\n"
			"             buffer length:" + bufDuration.round(0.01) + "sec\n"
		).postln
	};
	display = { 
		OSCdef(\display, { |msg| 
			var pos = msg[3] + (startTime / rate);
			{ sliders[0].value_(pos) }.fork(AppClock) 
		}, "/sweep");
		loop { 
			sliders[1].value_(currentTime.() + (startTime / rate));
			currentTime.().postln;
			0.05.wait;
		}
	}.fork(AppClock)
}
)

b = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav");
c = Buffer.readChannel(s, Platform.resourceDir +/+ "sounds/a11wlk01-44_1.aiff",0, -1, 0!2);

b.numChannels
~playBuffer.(b)
~playBuffer.(b, rate: 2)
~playBuffer.(b, rate: 0.5)
~playBuffer.(b, rate: 2/3)
~playBuffer.(b, startPosRatio: 0.5)
~playBuffer.(b, rate: 2, startPosRatio: 0.5)
~playBuffer.(b, rate: 0.5, startPosRatio: 0.5)
~playBuffer.(b, rate: 0.5, startPosRatio: 2/3)
~playBuffer.(b, startPosRatio: 1)
~playBuffer.(b, rate: 0.5, startPosRatio: 1)
~playBuffer.(b, rate: 2, startPosRatio: 1)

c.numChannels
~playBuffer.(c)

@enrike
I have found an old code of mine on the old mailing list. In the code, there is no PlayBuf but BufRd. It is similar to @jamshark70’s code, but my code is an automatic looper. You can enlarge the part of the sound file on the small upper part of the window. As soon as you select a part of the sound file from the large part of the window, it will be played immediately. You will hear no sound if you click any point of the large part of the window:

https://listarc.cal.bham.ac.uk/lists/sc-users-2016/msg51590.html

I am attaching the code below:

(
w = Window("An Instant Sound File Player (Looper)", Rect(0,Window.screenBounds.height-180,Window.screenBounds.width-100,Window.screenBounds.height-180)).front;
w.view.decorator = FlowLayout( w.view.bounds, 10@10, 20@5 );

q = Bus.control(s, 1);

a = Button(w, Rect(20, 20, 340, 30))
.states_([
	["Load a Sound File into a Buffer", Color.white, Color.black]
])
.action_({
	s.waitForBoot{
		if(y!=nil, {y.free; y=nil});
		b= Buffer.loadDialog(s,
			action:{
				b.loadToFloatArray(
					action: {
						|a|
						{ e.waveColors_(Color.green(1, 0.3)!b.numChannels);
							v.waveColors_(Color.green(1, 0.5)!b.numChannels);
							e.setData(a, channels: b.numChannels);
							v.setData(a, channels: b.numChannels);
							e.setSelection(0, [0,b.numFrames]);
							e.zoomAllOut;
							v.setSelection(0, [0,0])
				}.defer })
			}
		);
		"is loading a sound file".postln }
}
);
m = [10, 10, 30, 30];
n = 100;

e = SoundFileView(w, Rect(m[0], m[1], w.bounds.width-m[2], n));
e.timeCursorOn_(true);
e.setSelectionColor(0, Color.gray(0.8, 0.2));
e.gridOn_(true);
e.gridResolution_(60);
e.mouseUpAction = {|view, char, modifiers, unicode, keycode, key|
	var posData, posLo, posHi;
	posData = [e.selections[0][0], (e.selections[0][0] + e.selections[0][1])] / e.numFrames;
	posData;
		v.zoomToFrac(posData[1] - posData[0]);
		v.scrollTo (posData[0]);
		v.scroll (posData[0]);
};


v = SoundFileView(w, Rect(m[0], m[2], w.bounds.width-m[2], w.bounds.height-(m[3]*2)-n));
v.timeCursorOn_(true).setSelectionColor(0, Color.gray(0.8, 0.5));
v.gridOn_(true);
v.gridResolution_(10);

x = { |lo = 0, hi = 1|
	var phasor = Phasor.ar(0, BufRateScale.kr(b), lo * BufFrames.kr(b), hi *BufFrames.kr(b));
	Out.kr(q.index,A2K.kr(phasor));
	BufRd.ar(b.numChannels, b, phasor) };

v.mouseUpAction = {|view, char, modifiers, unicode, keycode, key|
	var posData, posLo, posHi;
	posData = [v.selections[0][0], (v.selections[0][0] + v.selections[0][1])] / v.numFrames;
	posData;
	v.selections[0][0]; v.selections[0][1];
	if (y==nil, {y = x.play(args: [\lo, posData[0], \hi, posData[1]])}, {y.set(\lo, posData[0], \hi, posData[1]); });
};
w.onClose_({y.free; b=nil; c=nil; e=nil; y=nil; x=nil; v=nil; w=nil; AppClock.clear});

(
AppClock.sched(0.0, {
	q.get({arg val; {v.timeCursorPosition=val; e.timeCursorPosition=val}.defer;});
	0.05;
});
)
)
1 Like

I tried and get the same error when I try to build on an M1… and I think this is because we’re using the universal SC build. Here are the binaries I compiled on an intel mac, I copied them to the M1 and they work!

https://drive.google.com/drive/folders/1FonpjfenVuehBpgKIbc0jwt6a3F8-eKy?usp=sharing

BTW the best helpfile to look at ATM is “SuperUGens Overview” - most of the others are out of date.
updated example from SuperPlayBuf:

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

{ SuperPlayBuf.ar(1, b, MouseX.kr(0.1, 10, \exponential), reset:b.atSec(b.duration / 2), trig: MouseButton.kr(0, 1, 0))!2 }.play;
2 Likes

Thanks for this!

I copied the extracted files into ~/Library/Application Support/SuperCollider/Extensions/super-bufrd, but nothing happend on iMac-2013-late (intel i7, Mojave) and MacBook Pro 2021 (silicon, Monterey).

What should I do?

Thanks!