When is BufRd "done?"


I can’t quite seem to find the right pieces that will allow me to play through part of a buffer once with a modulating rate. PlayBuf’s rate is modulatable but doesn’t accept an end-point. So using BufRd… I can get a single play-through using Line but it isn’t modulatable. A Phasor is modulatable but loops with no way to make it one-shot. I tried using an Envelope (for both PlayBuf and as the phase for BufRd) but it’s fixed-length and I have no idea how long to make it. I stumbled across https://doc.sccode.org/Classes/Done.html and it indicates that BufRd triggers a DoneAction which I thought I might be able to use to kill the looping Phasor but I’m pretty sure (empirically and reading source code) that BufRd doesn’t trigger a DoneAction.

Any thoughts are appreciated.

If it’s one shot, the solution is to use an envelope. There is two kind of envelope:

  • a gated one where you have to release yourself the synth
  • a non gated one where you specify the duration at creation

Here is the synthdef i use often to play samples. Maybe it’s not what you want to do but starting from this, it will be easier to clarify your goal.
In this case, the envelope is a gated one (Env.adsr) and the pattern take care of releasing the synth after 2 beats (\dur = 2 * \legato = 1)

SynthDef(\playersec, { arg out=0, amp=0.1, gate=1, pan=0, freq=200, bufnum, speed=1, pos=0, doneAction=2, loop=0, trigger=1, velamp=1;
	// pos in seconds
	var sig;
	var sig1;
	//speed = speed * ( SinOsc.kr(1.3) * 1 + 1 ); // modulate rate ?
	sig = PlayBuf.ar(2, bufnum, BufRateScale.kr(bufnum) * speed, trigger, startPos: (pos*BufSampleRate.kr(bufnum)), doneAction:doneAction, loop: loop);
	sig = sig * EnvGen.ar(\adsr.kr(Env.adsr(0.001,0,1,0.01)),gate,doneAction:doneAction);
	Out.ar(out, sig * \gain.kr(1) * velamp);

~mybuffer = Buffer.readChannel(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav", channels:[0,0]); 

		\instrument, \playersec,
		\bufnum, ~mybuffer,
		\legato, 1,
		\dur, 2,
		\gain, 1,

Thanks, but I since I want to play with a variable rate, think of a slowly accelerating fast-forward or rewind, I can neither use a fixed-duration envelope nor know when to close the gate of a sustaining envelope. That seems to rule out using PlayBuf since, yea, an Envelope is the only way I know to get it to play a section of a buffer. That leaves me with BufRd but I can’t figure out how to make a one-shot-variable-rate-line-like ugen.

If I understand correctly, you want to play a buffer with a variable rate, but once the read head hit a specific position in the buffer, you want the synth to stop automatically ?

If the rate is constant, this is a lot simpler, so do you want a dynamic rate (for example a rate modulated by a SinOSc) ?

Like this?

	var rate = LFNoise2.kr(50).exprange(0.5, 2);
	var line = Integrator.kr(rate);
	var done = line >= 200;
}.plot(duration: 0.5);  // too long, proves it's really stopping


1 Like

Yes, perfect.

Here’s an accelerating fast-forward / rewind using it. It still needs to incorporate BufRateScale but works great when the system and buffer’s sample rate match. Then add Done so I can send an OSC message back to the client letting it know too.

SynthDef.new(\ffrw, {
	|bufn, startPos=0, endPos=0, pan=0, amp=0.5, out=0|
	var dir, rate, phase, done, sig;

	// +1 forward, -1 backward
	dir = ( ( endPos - startPos ) > 0 ) * 2 - 1;
	rate = EnvGen.ar(Env([1,8],[20],[6])) * dir;
	phase = Integrator.ar(rate, add: startPos).poll(2);
	done = FreeSelf.kr( ( ( phase - endPos ) * dir ) >= 0);

	sig = BufRd.ar(1, bufn, phase);
	sig = Pan2.ar(sig, pan, amp);
	Out.ar(0, sig);

Synth(\ffrw, [\bufn, ~buf, \startPos, ~buf.numFrames / 8, \endPos, 2 * ~buf.numFrames / 8])
Synth(\ffrw, [\bufn, ~buf, \startPos, 2 * ~buf.numFrames / 8, \endPos, ~buf.numFrames / 8])

Thank you

Simply multiply rate by BufRateScale before integrating.


This isn’t actually correct. If its loop argument is set to 0 (which not the default), then BufRd actually does flip the done bit when it reaches the last frame in the buffer, and this can be seen with Done. I’ve checked this with a simple trace on a simple UGen.

The reason why you can’t easily find where this is done in the C++ code is that code is shared with PlayBuf via several macros and functions. The entry is BufRd_next_1 that does

    double loopMax = (double)(loop ? bufFrames : bufFrames - 1);

before calling sc_loop via the LOOP_BODY_1 macro as

    phase = sc_loop((Unit*)unit, phase, loopMax, loop);

The latter function renames loopMax to hi in its arguments and does this test and set:

inline double sc_loop(Unit* unit, double in, double hi, int loop) {
    if (in >= hi) {
        if (!loop) {
            unit->mDone = true;
            return hi;
   // ...

This is why Done gets signaled from RdBuf only when loop is false.

There is however a bit of a conceptual problem with the combo of Phasor and BufRd plus Done. Unlike for PlayBuf, in which the trigger goes directly into it, so it resets the “done” status, there’s no way to reset the “done” status for BufRd because the trigger goes into the Phasor. So while you can detect BufRd “done” status once… there is no way to reset it. So you might as well kill the whole thing with an envelope.

Here’s a parred down version of what I use. The infinite-end Phasor is basically the same as using an Integrator, but re-triggerable.

p = NodeProxy.audio(s, 2);

(p.prime({ arg rate = 1, amp = 0.3, bufnum = 0, t_trig = 1, phafps = 4, interp = 2;
	var framidx = Phasor.ar(t_trig, rate * BufRateScale.kr(bufnum), 0, inf); 
	var playgen = BufRd.ar(2, bufnum, framidx, 0, interp);
	var done = framidx >= BufFrames.kr(bufnum);
	var env = 1 - done; // could add nicer fade out	
	SendTrig.ar(env * Impulse.ar(phafps, 0.5), 66, framidx);
	SendTrig.ar(done, 77);
	env * playgen * amp}))

(o = OSCdef(\track, { arg msg, time;
	switch (msg[2])
	{66} {
		{ ("OSC now playing frame" + msg[3]).postln;}.defer }
	{77} {
		{ ("OSC buffer play ended.").postln }.defer;
		{ p.set(\t_trig, 1); ("OSC buffer play restarted").postln }.defer(3) }
}, '/tr', s.addr))


Works quite nicely for updating a timeCursorPosition in a SoundFileView on the 66 message (at phafps frames per second), changing the buffer when it’s “faded out” by the env on the 77 message etc. You can vary rate continuously while playing as well.

1 Like

It turns out there is actually a bug in the way BufRd checks for end, even with loop set to zero. If the play rate is >= 4 times the normal, it will jump over the end without detecting it. Test:

x = Signal.fill(2**18) { |x| sin(x.linlin(0, 44100.0, 0, 2pi * 500, nil)) };
b = Buffer.alloc(s, 2**18); 
b.loadCollection(x); // 500 Hz sine, assuming 44.1kHz sampling

(SynthDef(\yobra, { arg out = 0, bufn = 0, rbeg = 1, rend = 4;
	var rate = XLine.ar(rbeg, rend, BufDur.kr(bufn));
	var phase = Phasor.ar(0, rate * BufRateScale.kr(bufn), 0, BufFrames.kr(bufn));
	var brsig = BufRd.ar(1, bufn, phase, 0); // 0 -> no loop, flags "done"
	FreeSelfWhenDone.kr(brsig); // no doneAction arg in BufRd, unlike PlayBuf
	Out.ar(0, brsig);

q = Synth(\yobra, [\bufn, b, \rend, 1]) // ends ok
q = Synth(\yobra, [\bufn, b, \rend, 2]) // ends ok
q = Synth(\yobra, [\bufn, b, \rend, 3]) // ends but rather odd
q = Synth(\yobra, [\bufn, b, \rend, 4]) // doesn't end!

Since delaytimer is using faster rates in their example, I suspect they ran into this issue, even with loop set to zero.

Phasor jumps back to the beginning.

I’m surprised that done is triggered at all, when the Phasor’s output is constrained to 0 … bufSize - 1.


Quite true. Silly bug on my behalf. I suspect they may have been trying something like that though. With the proper code (inf-end Phasor) it ends of course…

(SynthDef(\yobra, { arg out = 0, bufn = 0, rbeg = 1, rend = 4;
	var rate = XLine.ar(rbeg, rend, BufDur.kr(bufn));
	var phase = Phasor.ar(0, rate * BufRateScale.kr(bufn), 0, inf); // FIX
	var brsig = BufRd.ar(1, bufn, phase, 0);
	Out.ar(0, brsig);

q = Synth(\yobra, [\bufn, b, \rend, 4]) // ends now ok
q = Synth(\yobra, [\bufn, b, \rend, 10]) // also ends

And the Phasor with neither a trigger nor a looping point is rather replaceable with an Integrator. Apparently that also avoids a bug (or at least usage gotcha) in Phasor with -inf end checks.

Also, BufRd with zero loop flags properly for Done overruns at the beginning of the buffer too.

(SynthDef(\yobra, { arg out = 0, bufn = 0, rbeg = 1, rend = 4, spos = 0.0;
	var rate = XLine.ar(rbeg, rend, BufDur.kr(bufn));
	var phase = spos + Integrator.ar(rate * BufRateScale.kr(bufn));
	var brsig = BufRd.ar(1, bufn, phase, 0);
	Out.ar(0, brsig);

(x = Signal.fill(2**18) { |x| sin(x.linlin(0, 44100.0, 0, 2pi * 500, nil)) };
x = x.fade; // so we can tell the diff. back vs forth
b = Buffer.alloc(s, 2**18);

q = Synth(\yobra, [\bufn, b, \rend, 20]);
q = Synth(\yobra, [\bufn, b, \rbeg, -1, \rend, -20, \spos, b.numFrames-1]);

This could be expected from sc_loop which also does check that

  } else if (in < 0.) {
        if (!loop) {
            unit->mDone = true;
            return 0.;