BufRd with multiple playheads frequently crashes

I’m having issues with some of my looper / bufPlayer synths. I have written a few variants but here is the simplest one: playback of a soundfile read simultaneously by different playback heads which is then processed by delay and filter effects. A variant of this is to record into a buffer and loop the playback at different rates. The inspiration is the Destroy FX Transverb AU plugin and also the classic Max/MSP stutter patch.

{ |rate=#[1,1,1,1],buf,rq=0.25,t_trig=1|
  var playbackEnv,sig,phasor,delayedSig;

	var lfo = SinOsc.ar(4.collect({0.1.rand})).range(100,10000);

  // Playback Head
  phasor =      Phasor.ar(0, (BufRateScale.ir(buf) * rate), TRand.kr(0,BufSamples.ir(buf),t_trig), BufSamples.ir(buf));
  sig =         BufRd.ar(2, buf, phasor);

  // Spatialization
  sig = RLPF.ar(sig, lfo, rq);
  sig = Splay.ar(sig);
	delayedSig = CombL.ar(sig, 0.2,LFSaw.ar(0.001.rand,1.0.rand), 10, \wet.kr(0.7));
	sig = Mix.ar([sig, delayedSig]);
	sig = sig*EnvGen.kr(Env.asr(releaseTime: \rel.kr(6)), \gate.kr(1),doneAction:Done.freeSelf)*\amp.kr(0);
  Out.ar(\out.kr(0),sig);
}

I can get this fairly reliably to cause audio processing to completely stop on my machine. It doesn’t crash the server or client, just a loud click and no more sound. In the past, all audio processing would cease however, recently I observed this only occuring only after the point on the node graph where the synth was running.

How do I debug this type of problem if there is no crash, heap log or error message? What causes scsynth to stop processing audio entirely?

Is this a node order thing? An i/o issue? Is the buffer not properly allocated in memory (I am using Buffer.read)? Is there some kind of race condition? Is it simply too CPU intensive. Does it matter in this context if I am using .ir vs. .kr for the BufRateScale UGens?

Any help would be appreciated.

Hi,

I have no troubles at all running this code on my old iMac (SC 3.12.2). I only had to adapt the BufRd numChannels arg. This gave a warning but yet it works. Maybe the number of channels has to be correct on windows ? I vaguely remember some issues with Buffers on Windows, but that was years ago.

Here my test – also had to adapt the amp which defaults to 0, which was a bit confusing.


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

x = { |rate=#[1,1,1,1], rq=0.25,t_trig=1|
	var playbackEnv,sig,phasor,delayedSig;
	var buf = b;
	var lfo = SinOsc.ar(4.collect({0.1.rand})).range(100,10000);

	// Playback Head
	phasor =      Phasor.ar(0, (BufRateScale.ir(buf) * rate), TRand.kr(0, BufSamples.ir(buf),t_trig), BufSamples.ir(buf)).poll;
	sig =         BufRd.ar(1, buf, phasor);

	// Spatialization
	sig = RLPF.ar(sig, lfo, rq);
	sig = Splay.ar(sig);
	delayedSig = CombL.ar(sig, 0.2,LFSaw.ar(0.001.rand,1.0.rand), 10, \wet.kr(0.7));
	sig = Mix.ar([sig, delayedSig]);
	sig = sig*EnvGen.kr(Env.asr(releaseTime: \rel.kr(6)), \gate.kr(1),doneAction:Done.freeSelf)*\amp.kr(0.1);
	Out.ar(\out.kr(0), sig);
}.play;


x.set(\t_trig, 1);

BTW I also got some surprisingly nice effects with the rather unimpressive SinedPink file:

p = Platform.resourceDir +/+ "sounds/SinedPink.aiff";
b = Buffer.read(s, p);

...
	sig = BufRd.ar(2, buf, phasor);
...

I’m a big fan of such buffer manipulation techniques and have used them quite often recently.

Segmenting: e.g., here drop the spatialization, lfo, array args etc and start with the easiest constellation possible. Then reintroduce the other stuff again. Not very exciting – debugging is not a strength of SC – but it usually clarifies the source of error.

1 Like

Not at all, these techniques are usually very CPU-cheap, except you use masses of buffers or other extra-processings.

In my experience these silent non-crashes usually have to do with indexing outside the buffer. Just to ask a stupid question, you are not changing the buffer while this is running? Cause if the new buffer is shorter than the initial one using .ir over .kr could cause a silent non-crash.

@Thor_Madsen : I had that in the past but not in the most recent crash.

@dkmayer : what happens if you play with the \rate param? Try setting it to weird random values (including playing through the buffer in reverse). I was able to get your code example to crash by doing that.

x.set(\rate, [1.0.rand, 2.0.rand, -0.5, 1.0 ]);

The SinedPink file sounds really cool by the way.

Works smoothly here. What OS and SC version do you use ?

Hm… Are you sure about this? For multichannel buffers, BufSamples will be too large.

I think BufFrames is what you wanted here.

Edit: Going back to Thor’s message:

You have a two channel buffer, meaning that you’re indexing twice as many frames as you have. (BufRd indexes in terms of frames, not samples.) So in the synth as written, there’s definitely a risk of indexing outside the buffer – so, does the behavior improve with BufFrames in both places?

hjh

This makes sense to me and I believe it is the source of the issue. I’m experimenting wih BufFrames and it seems to resolve the problem.

Thanks for all your help!

This sounds wonderful , how would I put a live input in? I tried but failed at setting up a synthdef .
Can it be reconfigured to work like a hardware tape delay? Or is it tied to only accessing a wav file? Thanks

It’s tied to reading from the buffer, not a specific wav file. You can always rewrite the same buffer (possibly from a live source) while the Synth is playing, e.g. with RecordBuf or BufWr. You’d have to expect glitches, though.

You would BufWr at a looping phase. This phase position defines “now.”

Then a delay subtracts the amount of time from “now” – if writePhase is in samples, then a n second delay would read from (writePhase - (n * SampleRate.ir)) % BufFrames.kr(bufnum).

If both are running at normal speed, the two phases run in parallel, and will never cross, so they won’t glitch. A modulating delay time where the average delay time is constant should also not glitch, unless the modulation is too extreme (as in a chorus effect).

hjh

Yes, I use this technique a lot with buffers and live input. And just to expand a bit on what @jamshark70 wrote: Let say you want access to the last second of the live input at all times. Now we can set up two pairs of BufWr/BufRd where Buffer A starts recording from the beginning of Buffer A and Buffer B starts recording in the middle (2 seconds in) of Buffer B. If t is time in seconds since the Synth was initiated and we want the slice from t = 3.5 to t = 4.5 we can’t use Buffer A without creating a discontinuity (a ‘glitch’) because Buffer A loops at t = 4. However, because Buffer B is offset by half the buffer duration we can use Buffer B instead, since the slice is located at 1.5 - 2.5 seconds relative to the time of buffer B.

This works great for rate >= 1 but if rate is < 1 we can still get in trouble. Let’s say we want a rate of 0.5 (half speed = 1 octave down). Now it will take 2 seconds to play back the 1 second slice. After two seconds buffer B will be at 3.5 seconds so no problem. But if rate = 0.25 the slice will take 4 seconds to play back and we will get a discontinuity. Had we instead used buffer durations of 8 seconds, then we are good - so the longer the buffers are, the slower you can go.

The pair of buffers isn’t strictly necessary – BufRd with an audio rate phase wraps around seamlessly.

s.boot;

b = Buffer.alloc(s, s.sampleRate, 1);  // 1 sec

a = { |buf, delay = 0.3|
	var sr = SampleRate.ir;
	var sine = SinOsc.ar(
		// non-integer division of sr
		Rand(0.005, 0.015) * sr
	);
	var frames = BufFrames.kr(buf);
	var writePhase = Phasor.ar(0, 1, 0, frames);
	var readPhase = (writePhase - (delay * sr)) % frames;
	
	BufWr.ar(sine, buf, writePhase);
	
	(BufRd.ar(1, buf, readPhase) * 0.1).dup  // clean and smooth
}.play(args: [buf: b]);  // edited here, oops

hjh

I think the args are missing from .play so that buf becomes a reference to b.

Ah that’s right, I forgot that. I’ll edit.

It won’t change the behavior of BufRd, apart from using the correct buffer.

hjh

Yes you are right, I had this thread mixed up with this thread where you want overlapping grains of different durations. I think for this you would need the two-buffer approach, right?

That example was one buffer per channel, so that they could be used with GrainBufJ – I don’t see where it’s using multiple buffers to work around boundary problems.

You’d need to use multiple buffers if you’re using TGrains with a circular buffer, because TGrains doesn’t wrap around AFAIK. The GrainBuf family does wrap around, so these UGens do work transparently with circular buffers.

If the buffer playback UGen can wrap around smoothly from the end to the beginning, then you shouldn’t need an overlapping buffer pair – seems an unnecessary complication.

hjh

Thanks for clarifying this, it means I have a bunch of code which I can simplify. I don’t know how I missed that both sample based and time based buffer ugens wrap around seamlessly. I think maybe my (wrong) intuition was that there could potentially be rounding errors. I just noticed to my surprise that it is possible to allocate a buffer with a non-integer number of frames:

b = Buffer.alloc(s, s.sampleRate * 0.9666)

For sampleRate = 44.1k the number of frames is 42627.06
For sampleRate = 48k the number of frames is 46396.8

Still, I can get seamless playback across the loop boundary with both PlayBuf and BufRd. So what happens internally when the number of frames is a non-integer?

I expect it’s truncated to the next lower integer – floor(). There’s no way to have a fractional number of frames.

hjh

1 Like

Yea I supposed you’re right. This all seems quite confusing as (at 48k) b.duration still reports 0.9666, where as if you truncate the the next lower integer the duration is 46396 / 48000 = 0.96658333333333 which I suspect could lead to unpleasant surprises. So I guess the safest thing is to make sure that when you use syntax as eg. Buffer.alloc(t * s.sampleRate), t * s.sampleRate returns an non-fractional number.