Timing issue while playing a buffer in loop

Hi,

I’m trying to make a looper, and I wanted to see if it’s possible to seamlessly loop over a buffer. So I’m playing the following code, while recording the audio output in Audacity in order to analyze it.

b = Buffer.alloc(s, s.sampleRate, 2);

(
b.set(0, 1);
b.set(s.sampleRate * 2 - 1, 1);

SynthDef(\play_buffer,{ arg out = 0, bufnum;
	Out.ar( out,
		PlayBuf.ar(2, bufnum, BufRateScale.kr(bufnum))	
	)
}).send(s);

t = Task({
	loop {
		s.makeBundle(s.latency, {
			Synth(\play_buffer, [\bufnum, b]);
		});
		1.wait;
	}
}).play;
)

This somehow completely saturates the second output. Looks like the last sample of a buffer leaks, is it a bug?

So let’s instead mark the second last sample:

b.set(s.sampleRate * 2 - 3, 1);

Both canals are ticking fine now, but Audacity shows the second output is late by two samples. Just to check:

b.set(s.sampleRate * 2 - 7, 1);

instead, and now both canals match.

So there’s a timing error of 3 samples, about 7e-5 seconds. A float variable with a value around 1 can handle this precision by far. I would have expected that the logical time of the server, coupled with a time tagged OSC bundle, would result in a sample-perfect timing, if the buffer length is not too big.

Where does this issue come from? Is it a bug, or should I take it into account?

It doesn’t. Never has, never will.

There are lots of other threads about this.

hjh

1 Like

To be more specific:

Server audio must follow the soundcard’s sample clock. If it doesn’t, there will be glitches.

Sclang clocks are based on the system clock, not the soundcard. There is no guarantee that this will run in lockstep with the soundcard. (Why not use the soundcard clock in the language? One concrete reason is that you can have multiple Server objects using different soundcards – which I’ve actually seen in a performance in 2006 – enforcing “one sclang, one soundcard” in the same way that a DAW enforces one soundcard would remove some possibilities for ensemble performance that we want to keep).

So you can run your loop within the SynthDef, but eventually it will get out of sync with rhythms played in sclang patterns or routines.

Or you can run the loop by sequencing – it will be rhythmically accurate relative to other rhythms in the same sclang, but not sample accurate (which you can ameliorate by a short envelope – you haven’t done this yet – and arranging the attack/release times to crossfade).

Or you can use supernova and set s.options.useSystemClock = false (http://doc.sccode.org/Classes/ServerOptions.html#-useSystemClock) – but eventually latency will not be accurate and this may or may not be usable over a long time.

hjh

As James said, that’s not possible in realtime in principle, see

Another issue here is that you’d need OffsetOut, but even this doesn’t cause sample-exact timing.

Apart from that, seamless looping (of arbitrary buffers) needs crossfading. You can check PlayBufCF from wslib or DX ugens from miSCellaneous_lib

Thanks a lot! I guess I’ll add a quick crossfade envelope.

This is a valuable info that I haven’t found, perhaps you could as it to a page like https://doc.sccode.org/Guides/ServerTiming.html ?

Besides, can you reproduce this bleeding effect regarding the last sample of a buffer? I’m running SC 3.10.3 on Win 10 x64.

I’m not clear what you meant by “completely saturates the second output.” Since you’re populating both the L (beginning) and R (ending) samples with 1.0 and doing no amplitude scaling, I’d expect both channels to be fully saturated (but not out of range).

I’ll have to try the code later, but just wondering more specifically what you’re seeing.

hjh

OK, I realized what it is. Compare these two:

b = Buffer.alloc(s, s.sampleRate, 2);
b.set(0, 1);
b.set(b.numFrames * 2 - 1, 1);

(
a = {
	PlayBuf.ar(2, b, BufRateScale.ir(b))
}.play;
)

a.free;

(
a = {
	PlayBuf.ar(2, b, BufRateScale.ir(b), doneAction: 2)
}.play;
)

a.free;  // edit: actually this is already done! copy/paste error

In the first case, the synth plays forever. When it reaches the end of the buffer, it keeps outputting the final data frame.

In the second case, the synth stops when it reaches the end of the buffer, and the final sample is output only once.

I’d say it’s a very unusual use case to run a PlayBuf with loop == 0 and without any node or envelope control over start/stop – i.e., probably not a good idea. Make sure to tell SC how to stop playing, in this case.

hjh

Oooh right, I haven’t realized there were no done action. It is expected then. Thanks!