Why rate re-triggers PlayBuf

I am confused by the behavior of the rate argument in PlayBuf: in the following code the rate’s LFNoise re-triggers the player. My intuition was that any thing which goes into the rate argument, only changes the playback speed, but wouldn’t touch the trigger knob of the player. Obviously wrong! Is this just a design decision in PlayBuf or is there a central concept I am missing?
My intuitive expectation was that the UGen in rate would constantly (or randomly) change the speed, which I only would be able to hear, when I re-trigger the player

b = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav");
(
f = {
	arg t_trig=1;
	PlayBuf.ar(b.numChannels, b,
		trigger: t_trig,
		rate: LFNoise0.kr(2).range(-4, 4.0).poll
	)
}.play
)

It does only change the playrate. In your example the playback rate can go as extreme as +/- 4 times the original playback rate and since LFNoise is outputting random values you will hit the boundaries of the buffer quite often causing silence. Let’s say LFNoise0 is outputting 0.2 at some moment in time - the playback direction will be forward and the rate is 1/5 of normal speed, so very slowly forward. LFNoise changes its value every half second. If the next values is say -3.5, the playback rate will change to 3.5 x normal speed playing backwards. If at that time the playhead is 1 second into the buffer, then it will take 1/3.5 = 0.28 seconds to reach the beginning of the buffer. Here, the playhead of Playbuf will sit until the rate changes to a positive number, so if the next value out of LFNoise is negative, you won’t hear a change.

This is a case where you might want to use BufRd instead, since Phasor loops, so that if you reach the beginning of the buffer going backward (like in the example above), the playhead head will loop around to end of the buffer and keep reverse-looping the buffer without any silence.

(
f = {
	arg t_trig = 1;
	 BufRd.ar(1, b.bufnum, Phasor.ar(0, BufRateScale.kr(b.bufnum) * LFNoise0.kr(2).range(-4, 4.0), 0, BufFrames.kr(b.bufnum)))
}.play
)
1 Like

A little further clarification:

In your example, the loop arg was not set, so no looping as it defaults to 0. If you set PlayBuf to loop AND only feed it positive values OR install some mechanism to insure forward playback when the playhead is at zero, you can still use PlayBuf:

(
// Only positive values, so no reverse playback, but continuous looping.
f = {
	arg t_trig=1;
	PlayBuf.ar(b.numChannels, b,
		0,
		rate: LFNoise0.kr(0.5).range(0.3, 4), 
		loop: 1
	)
}.play
)

(
// This is just for demonstration, I think using BufRd is a better option here
// continuous forward and backwards looping, if the playhead reaches 0, the rate will always be positive
f = {
	var buf = b;
	var rate = LFNoise0.kr(0.5).range(-4, 4);
	var phasor = Phasor.ar(0, rate, 0, BufFrames.kr(b)).poll;
	rate = rate * ( (phasor > 0) * 2 - 1 );
	
	PlayBuf.ar(b.numChannels, b,
		0,
		rate: rate, 
		loop: 1
	)
}.play
)

How can a boolean be multiplied by a number? What is this line meant to do with rate?

On the server everything are numbers, so boolean expression evaluates to either 0 (false) or 1 (true). In my example Phasor tracks the playhead counting in samples rather than in seconds so if Phasor is at 44100 and the sampleRate is 44100, the playhead is at 1 second. The expression

( (phasor > 0) * 2 - 1 )

Evaluates to -1 if (phasor <= 0) and 1 if (phasor > 0), so if rate is negative causing phasor to go to zero (heading to negative values), rate will be multiplied by -1 and thus become positive, in all other cases rate is multiplied by 1 causing no change to either negative or positive rates.

Ok, all your clarifications sounds logical. I still face a situation where my observations is that the rate is somehow controlling the triggering!
I have loaded a very short sound file into a buffer, and run the following:

(
f = {
	PlayBuf.ar(b.numChannels, b,
		rate: LFNoise0.kr(1).range(-1,10.0).poll,
	)
}.play
)

from the start to the end, it can read quite fast (max 30X), but backwards not too slowly (max -1X). And I even hear the sound being read often to the very end, then there are big silences and it comes again! This is not the case if I set doneAction: 2. But without, it just continues triggering the sound file again and again. I can not explain it to myself, other than the rate ugen is triggering the playbuf. And the triggers happen exactly at points, where negative values pass to positive or vice versa. If you don’t believe me please test it yourself: load a very short sound file into a variable b, and run the code.

Here is another simulation of the same thing:

(
f = {
	arg rate = 1;
	PlayBuf.ar(b.numChannels, b,
		rate: rate,
	)
}.play
)

Now doing f.set(\rate, 1) doesn’t make any triggering, but a negative value e.g. f.set(\rate, -1) triggers the player! After that recalling the -1 rate doesn’t do anything again, until you call a positive value f.set(\rate, 1) where you hear the sound again.
So from my observations, a rate ugen can re-trigger, as long as the synth has not been killed with doneAction!

It does not, it can make the playhead jump back to the beginning (see my other comment above)

An idea for testing: fill a buffer with sample indices.

b = Buffer.alloc(s, s.sampleRate * 5);

(
{
    var frames = BufFrames.ir(b);
    var phase = Phasor.ar(0, 1, 0, frames + 1000);
    BufWr.ar(phase, b, phase);
    FreeSelf.kr(A2K.kr(phase) >= frames);
    Silent.ar(1);
}.play;
)

Then run the PlayBuf synth again, but don’t play out to the speakers (definitely don’t slam your speakers with high-amplitude DC). Instead, record into another buffer. Then otherBuffer.write (give a path etc – and use float as the sample format!!). Then you can use SoundFile to read the audio, plot it, inspect it. Set LFNoise0 to a higher frequency (to record more rate changes); then this frequency tells you where to look for jumps. If you find a jump, then it’s possible to quantify exactly where it jumped from and to.

There either is a smoking gun or there isn’t. If there is, then it’s conclusive proof of your observation (and we’d have facts on which to base an analysis). If there isn’t, then it’s likely that the “retriggering” observation may be a misunderstanding.

hjh

I inserted your values in my code suggestion from above and created a short 150ms sample. When I run the code I don’t hear any gaps, do you get gaps on your system? Once in a while you will get rate values close to 0 which will result in almost no sound. You could build a mechanism into the synthdef to avoid that. It is possible I am misunderstanding what you are looking for…

(
s.newBufferAllocators;
s.waitForBoot{
	b = Buffer.alloc(s, s.sampleRate * 0.2);
	p = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav");
	s.sync;
	p.copyData(b, 0, 0.5 * s.sampleRate, s.sampleRate * 0.15)
}
)

b.play;

(
f = {
	var buf = b;
	var rate = LFNoise0.kr(1).range(-1, 10);
	var phasor = Phasor.ar(0, rate, 0, BufFrames.kr(b)).poll;
	rate = rate * ( (phasor > 0) * 2 - 1 );
	
	PlayBuf.ar(b.numChannels, b,
		0,
		rate: rate, 
		loop: 1
	)
}.play
)

If you use PlayBuf without looping and the playhead hits the end of the buffer, then you are right, you will need to give it negative value to hear any sound, otherwise the playhead will be stuck at the last frame of the buffer.

Ok, I think that was the problem I had with understanding the rate parameter. Basically you can move the playhead with only rate (from negative to positive or vice versa). I personally would have find it a better design if rate would have only controlled the playback speed, and trigger every thing else concerning triggering!

That is the design.

Let’s say you give it a negative rate, and the play position reaches the beginning (frame 0).

At this point, there are only two possible designs.

  • It could freeze the play position at 0 – the rate specifies to go negative, but negative isn’t allowed (more precisely, “out of buffer bounds” isn’t allowed). At this time, you don’t hear any sound. If rate later becomes positive, then the playhead is allowed to start moving up from 0 into positive numbers, and at this point you do hear sound.

  • Or it could allow the playhead to go negative, producing silence when it’s out of bounds. Then, if it becomes positive, it will make sound when the playhead crosses 0 at some time in the future which may or may not be known, depending on how simple the rate signal is, and how good your calculus is. (For this reason, this way would be a worse design.)

I guess that your thinking is,“I wasn’t hearing sound, then I reversed the rate, and suddenly I’m hearing sound – therefore the rate input is triggering” but this is not the logic at all. It seems to be rather that the playhead is stopped because of being asked to move past the buffer boundary, and it can move again when it’s asked to move away from the boundary (into the buffer’s contents). The transition from stationary to moving is also a transition from no-signal to signal, but that does not mean it’s the result of a trigger.

hjh

Thinking of it this way is a misconception and these kind of wrong ways of conceptualizing what is going on makes it much harder to write code that does what you want. Also, using randomness in a situation where you are not 100% sure what is going on makes trouble shooting much harder, so a good idea is to first write deterministic code till you understand exactly what is going on.

We can generalize the behaviour of a looping buffer. Below I am assuming we want to loop the whole buffer but the principles are the same if you wanted to loop only a portion of the buffer:

When the playhead reaches the end of the buffer, ie. the upper limit
1a. Wrap around to the beginning of the buffer, play forward
1b. Change direction and play backward (think of this as 'folding);
1c. Playhead is stuck on last frame = no sound until rate becomes negative

When the playhead is going backwards and reaches 0, ie. the lower limit
2a. Wrap around to the end of the buffer, play backward
2b. Change direction and play forward (again, this is 'folding);
2c. Playhead is stuck on first frame = no sound until rate becomes positive

PlayBuf with no looping: 1c and 2c
PlayBuf with looping:1a and 2c
My code example from above: 1a, 2b

BufRd with Phasor or Sweep: 1a, 2a.

(
s.newBufferAllocators;
s.waitForBoot{
	var bufDur = 1; // values in seconds
	b = Buffer.alloc(s, s.sampleRate * bufDur);
	p = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav");
	s.sync;
	p.copyData(b, 0, 0.4 * s.sampleRate, s.sampleRate * bufDur) // carve out a 1 second buffer
}
)

b.numFrames // == 1 * s.sampleRate
b.play

( // 'Wrap' Looping 
f = {
	var loopDur = 1;
	var phasor =  Phasor.ar(0, 1, 0, loopDur * s.sampleRate);
	BufRd.ar(1, b, phasor)
}.play
)

If you want a ‘folding’ looper, ie. a mechanism to change direction when you hit either the upper or lower limit, this is quite simple using BufRd (and as I said a couple of time, using PlayBuf is not the most obvious choice for the behaviour(s) your are after).

( // 'Fold' Looping' - behaviour 1b and 2b
f = {
	var loopDur = 1;
	var phasor =  Phasor.ar(0, 1, 0, inf).fold(0, loopDur * s.sampleRate).poll;
	BufRd.ar(1, b, phasor)
}.play
)