Buffer fades: seamless playback of continuous recording

Dear list,

i’m trying to figure out how to seamlessly playback continuously recording buffers at variable rates/reading positions (delays). To avoid rec and playback points coinciding im setting up two buffers of which one is recording input and the other playing back and fading between them so as only to hear the already recorded buffer.
in this example there are still clicks due to the sudden muting of one of the recording buffer (lagging the demand ugen it makes worse). Any advice how to optimize this or other ideas to make it possible to have seamless variable rate playback with live input? (I also tried a setup with RecordBuf but to no avail…seemed easier than it turns out to be!)

(
b = Buffer.alloc(s,s.sampleRate*2);
c = Buffer.alloc(s,s.sampleRate*2);
)
(
SynthDef(\buftest,{|freq|
	
	var sig=SinOsc.ar(200*LFDNoise3.kr(1).range(1,4));
	
	var bdur=BufDur.kr(b);
	var trig=Impulse.ar(1/bdur);
	var delay=((0.1*(1..8)/8)*BufFrames.kr(b));
	
	var playbackrate=MouseY.kr(0.5,2,1);
	
	var switch=Demand.ar(trig,0,Dseq([-1,1],inf)).lag(0.05);
	
	var mute1=Demand.ar(trig,0,Dseq([1,0],inf)).lag(0); //rec switch
	var mute2=Demand.ar(trig,0,Dseq([0,1],inf)).lag(0);
	
	var wphasor=Phasor.ar(0,BufRateScale.kr(b),0,BufFrames.kr(b));
	var rphasor=Phasor.ar(0,BufRateScale.kr(b)*playbackrate,0,BufFrames.kr(b));
	
	var rec1= BufWr.ar(sig*mute1,b,wphasor*mute1);    //muting buffer recording alternatively
	var rec2= BufWr.ar(sig*mute1,c,wphasor*mute1);
	
	var snd1=(BufRd.ar(1,b,rphasor-delay,1,4));
	var snd2=BufRd.ar(1,c,rphasor-delay,1,4);
	
	var mix= LinXFade2.ar(snd2,snd1,switch); //hear buffer that is currently not recording
	Out.ar(0, Pan2.ar(sum(mix)));}
).add
)
a = Synth(\buftest);

Thank you!
Jan

1 Like

Would you mind to edit your post to use code tags, instead of quote tags? If someone copy/pastes the code as shown, it won’t run.

I’m not at the computer now, but the core of the technique is that the record head is one linear equation, and the playback head is a second linear equation. The record head’s slope is 1. If the play rate is not 1, then the lines will intersect. This is where you’d have a click. Knowing when it will click (the x coordinate of the intersection) makes it possible to calculate a start time for a segment to guarantee no click.

More later…

hjh

1 Like

Ok… This should work for a single buffer.

The record head is always “now”:

Yr = x

For each playback segment, first choose a rate (a) and a duration (d). Now the play position can be expressed as:

Yp = ax + b

We want to choose b so that these two lines do not intersect within x and x+d. That is, in x < play_x < x+d, ax + b should not = x.

One bound of the bad range will be at x.

x = ax + b1
x(1 - a) = b1

The other bound is at x+d.

x+d = a(x+d) + b2
(x+d)(1-a) = b2

If a > 1 (faster playback), then b2 < b1. So choose (randomly) b < b2 or b > b1 (i.e. outside the bad range).

If a < 1 (slower playback), then b1 < b2. So choose b < b1 or b > b2.

Then the starting position for the segment is ax + b, where x is the current recording position. I think this should work whether x is expressed in seconds or in sample frames btw, as long as it’s consistent.

hjh

Thank you James, and sorry about misquoting the code, i just reedited!
While i do understand the single steps of your suggestion im still unsure about a few points and how to translate it to code.
You’re proposing a way to to adapt duration of the reading according to playback rate. When you say “choose” a duration (randomly?) you mean choosing a reading segment position that will be enveloped according to that duration, (so basically a granular approach)?
This would eventually limit the playback options to certain positions/durations, hence the original idea to separate/alternate the buffers and in which one is allowed more freedom and eventually even using total duration of the buffer that is currently not recording. Downside would be the additional latency surely…
But maybe im missing something in your suggestion?
Greetings,
Jan

It’s possible that I misunderstood what you’re trying to do, but (AFAIK) truly seamless variable rate playback of live audio recording, for an unlimited duration, is impossible.

If you’re playing back faster than you’re recording, then eventually the play head will cross over the record head into the future (which would mean playing back audio that hasn’t been recorded yet).

If you’re playing back slower than you’re recording, then the playback head will drift further and further behind the record head. For long durations, this would require a very long buffer.

Eventually, there is always a limit – there is always a point where you will have to switch to a different delay time. (The only way to proceed without a limit is to match recording and play rate – or to ensure that the integral over time of the playback rate matches the recording rate.) A two-second buffer means you will hit that limit fairly quickly.

I suggested an essentially granular approach because I believe it’s the most likely way.

You could record into a longer buffer and use longer segments, and it wouldn’t feel like granular synthesis. (If the buffer is 30 seconds, you’d be pretty safe with durations up to 20 seconds perhaps.)

You’re proposing a way to to adapt duration of the reading according to playback rate.

No, in the suggestion, the playback rate and duration of one segment are chosen in advance. The start position in the buffer is adapted to avoid a collision. Rate and duration are givens; start position is the unknown for which to solve the equations.

hjh

Hi James,

Yes, precisely because of this eventuality I thought of having two separate buffers, between which I fade instead of adapting different start positions to avoid collision (I’m aware this implies eventually looping through the whole buffer here, but that’s ok for this case). The advantage in this is not having this parameter dependence one’d have to deal with using one buffer and hence more flexibility in playback.

The example I posted just fails in the alternate muting of the recording buffers as it produces a click. The transition is not as seamless as intended, so I thought maybe there’s a smarter way to do that.

Id still like to try the single buffer approach you suggest to see how/if it works out practically, unfortunately i’m not sure about the implementation of the code. I’d be especially confused about where to place this part of the bound for the playback segment:

x = ax + b1

x(1 - a) = b1

The other bound is at x+d.

x+d = a(x+d) + b2
(x+d)(1-a) = b2

Thanks for helping out!

Jan

This may be the source of the clicks.

hjh

Yes i hoped so too at first but this modified example with an envelope for the buffer recorded signal doesn’t change this:


(
b = Buffer.alloc(s,s.sampleRate*2);
c = Buffer.alloc(s,s.sampleRate*2);
)
(
SynthDef(\buftest,{|freq|
	
	var sig=SinOsc.ar(200*LFDNoise3.kr(1).range(1,4));
	
	var bdur=BufDur.kr(b);
	var trig=Impulse.ar(1/bdur);
	var delay=((0.1*(1..8)/8)*BufFrames.kr(b));
	var env=EnvGen.ar(Env.sine,trig,8,timeScale:bdur).tanh;
	var playbackrate=MouseY.kr(0.5,2,1);
	
	var switch=Demand.ar(trig,0,Dseq([-1,1],inf)).lag(0.05);
	
	var mute1=Demand.ar(trig,0,Dseq([1,0],inf)).lag(0); //rec switch
	var mute2=Demand.ar(trig,0,Dseq([0,1],inf)).lag(0);
	
	var wphasor=Phasor.ar(0,BufRateScale.kr(b),0,BufFrames.kr(b));
	var rphasor=Phasor.ar(0,BufRateScale.kr(b)*playbackrate,0,BufFrames.kr(b));
	
	var rec1= BufWr.ar(sig*env,b,wphasor*mute1);    //muting buffer recording alternatively
	var rec2= BufWr.ar(sig*env,c,wphasor*mute1);
	
	var snd1=(BufRd.ar(1,b,rphasor-delay,1,4));
	var snd2=BufRd.ar(1,c,rphasor-delay,1,4);
	
	var mix= LinXFade2.ar(snd2,snd1,switch); //hear buffer that is currently not recording
	Out.ar(0, Pan2.ar(sum(mix)));}
).add
)
a = Synth(\buftest);

What I meant by “looping through the whole buffer may be the cause of the clicks” is:

Let’s assume 2x 2 second buffers. Let’s also assume best case for now, where playbackrate = 1.

After 2 seconds, buffer A contains audio range 0 … 2 seconds, and buffer B starts recording.

At this exact moment, the rphasor jumps back to 0. Buffer A will continue to play for 50 ms because of the Lag serving as an envelope. This will of course loop back to the beginning of the buffer – meaning, jumping from source audio 2 sec to 0 sec = click, and the envelope is open at this time, so you would hear the click.

If playbackrate != 1, then you would hear more clicks because playback switching is tied to recording duration – but playback is desynchronized from recording, so the playback readers will not be enveloped according to continuous audio segments.

Perhaps the thing to do now is for you to describe the result that you want. I have no idea what you mean by “seamless” so it’s hard to propose an alternate design.

hjh

I actually now seem to have found a passable solution writing into one longer buffer and fading between its first and second half.
The aim of this synth was to have the possibility to control the rate of delayed or even synced playback of a live input using buffers…
Thanks for your inputs so far James, as always!

I know this is an old thread but just wanted to chip in. In my recent code I am recording the same signal into two different buffers, both Buffer1 and Buffer2 are 30 seconds long and loop recording. Buffer2 is offset by 15 seconds so it starts recording in the middle of its cycle. This way I can always access the last 15 second of recorded audio by choosing the right buffer, ie. the buffer that does not loop back in the middle of the chunk. I can play audio from either buffer while it is recording as long as I don’t try to pass ‘now’, ie. ‘look into the future’…or to be more specific, if I do so I get audio from the last loop, ie. 30 seconds old audio. It is a little hard to explain, I hope you understand what I mean.

Thanks @Thor_Madsen, sounds like a good solution too! Are you fading the buffers with Select?

@mousaique I use a function in the language to determine which of the buffers to use, then index into the chosen buffer using a synth based on BufRd and Phasor.

Another, incredibly naive solution, but will not click under any setting.
It records into two buffers of equal size and when the reading and playback phases get close it will blend between them. It will introduce echos and time travelling when the phases cross, or if the dangerWindow is too large, but does so quite nicely in most settings.


s.boot

(
~pingpong = {
	|in, raw, pb, pbPhase, dangerWindow=0.5|
	// in: sound
	// raw & pb: buffers, must be same channel count as in, and same numFrames as each other
	// pbPhase: a signal (0, pb.numFrames]
	// dangerWindow: 
	//      in seconds, determines when the two phases are considered dangerously close.
	//      can be raring if too small, and will produce artifacts if too large
	var rawPhase = Phasor.ar(Impulse.kr(0), BufRateScale.kr(raw), 0, BufFrames.kr(raw), BufFrames.kr(raw) * 0.1);
	
	var distanceSecondsUnwrapped = ((rawPhase - pbPhase) / BufSampleRate.kr(raw)).abs;
	var distanceSeconds = distanceSecondsUnwrapped.fold(0, (BufFrames.kr(raw)/ BufSampleRate.kr(raw))/2);
	var outsideDangerWindow = distanceSeconds.clip(0, dangerWindow).linlin(0, dangerWindow, 0, 1);
	
	var initalRecord = BufWr.ar(in, raw, rawPhase);
	
	var whatHappenedLastIter = BufRd.ar([in].flat.size, pb, rawPhase);
	
	var safeRecord = BufWr.ar(whatHappenedLastIter.blend(in, outsideDangerWindow), pb, rawPhase);
	
	BufRd.ar([in].flat.size, pb, pbPhase);
};
)



// must be equal length, but can be anything
~bSize = 10 * s.sampleRate;
~b1 = Buffer.alloc(s, ~bSize);
~b2 = Buffer.alloc(s, ~bSize);

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

(

try {~x.free}{};
try {~b1.zero; ~b2.zero}{};

~x = { 
	var sig = PlayBuf.ar(1, ~a11, loop:1);
	
	var pbPhase = Phasor.ar(0, BufRateScale.kr(~b2) * \pbRate.kr(1), 0, BufFrames.kr(~b2));
	~pingpong.(
		in: sig, 
		raw: ~b1, 
		pb: ~b2, 
		pbPhase: pbPhase, 
		dangerWindow: \dangerWindow.kr(0.1)
	);
}.play

)

// setting this too larger will make a weird echo when the two phases collide.
// should probably be less than a second.
~x.set(\dangerWindow, 0.01); 

// 1 = normal
~x.set(\pbRate, 0.5); 


~b1.plot
~b2.plot