Aliased routed output + click-free BufRd

Hello. I’ve got two primary questions. I am pulling my non-existent hair out over this.

I’m currently working on a simple live sampler. Input from the audio interface is continuously recorded into a buffer and then played back at various speeds and ways. Right now I am routing the output of SC out via the Blackhole app into Reaper for recording. I am on a Mac laptop by the way (running 10.13.6).
My issues are, right now:

  1. I’m getting heavily aliased output when playing the SynthDefs. Sometimes it starts right away, sometimes it starts after a few seconds. This does not happen when I had a version of the sampler using RecordBuf and PlayBuf. The current version uses BufWr and BufRd (see question 2).

I have tried all manner of sample rate and buffer size adjustments, settings, tests, but to no avail. Sometimes rebooting the server will help, running no plugins in Reaper while monitoring the output will help a lot, and sometimes, just sometimes, changing the sample rate in Reaper will alleviate the problem for a while. Interestingly it is only on the Supercollider channel. So it has to be coming from sc itself.
I read something about BufWr recording over the allocated buffer length, which could explain it, but then trying to allocate a buffer size twice the length of what I need and then only playing back a portion of it still didn’t help.
I am going nuts over here, I’ve never tried anything like this. I have to assume I’m going about it the wrong way. I am using a large buffersize (512, 1024+), since I’m running all this on an older laptop.

  1. This whole thing is a product of trying to get a click-free live sampler going. I realized after many tries yesterday that I couldn’t do what I wanted to with RecordBuf and PlayBuf, since I would, to avoid clicks with recording to and playing from a continuously filled buffer, would have to do some logic business by offsetting the playback “head” from the current position of the recording “head”, and I couldn’t figure out how to get the current position of RecordBuf, but using BufWr and then reading from the recording Phasor via a bus seems more do-able, but I can’t continue with my testing with this aliased output.

If anyone has any specific ideas or tips for how to achieve the click-free sampling, please do tell me.

The code, without any bus-reading at this point:

(
var bufLength = 5;

~b1 = Buffer.alloc(s, 48000 * bufLength,1,bufnum:0);

SynthDef.new(\liverecorder,{
	arg buf=0, trigger=1, loop=1, channel_in=2;
	var start = 0;
	var end	= BufFrames.kr(buf) - 1;
	var rate = 1;
	var input = SoundIn.ar(channel_in);
	var phasor = Phasor.ar(trigger, rate, start, end);
	var rec = BufWr.ar(input, bufnum: 0, phase: phasor, loop: loop);
}).add;

SynthDef.new(\liveplayer, {
	arg buf=0, note=60, trigger=1, loop=1, pan=0, amp=1, out=0, atk=0.1, rel=1;
	var rate = BufRateScale.kr(buf) * (note/60);
	var start = BufFrames.kr(buf)-1;
	var end	= BufDur.kr(buf);
	var env = EnvGen.kr(Env.perc(attackTime:atk, releaseTime:rel), doneAction:2);
	var ptr = Phasor.ar(trigger,rate,start, end);
	var play = BufRd.ar(1,buf, ptr, loop, 4);
	Out.ar(out, Pan2.ar(play,pan,amp * env));
}).add;
)

All the best,
MH.

Take a look here:

This kind of does what you want. The thing is, you need to pass the phasor location from the record synth to the play synth. So what I am doing is continuously recording a 30 second buffer and passing the phase to the player when it is triggered. I can trigger as many of the play synths as I want, because they just latch on to the current phasor location and play from there. They never go faster than 1, so the play head will never pass the record head.

I hope this helps.

Sam

Maybe someone else has a different suggestion, but as I understand it:

Click-free playback requires that the recording head and the playback head don’t cross. If they cross, then there will be a moment where you were playing old data and are suddenly playing new data = click.

If it’s long-running playback – to guarantee that the record and play heads don’t cross, they must be moving at the same speed. var rate = BufRateScale.kr(buf) * (note/60); breaks that condition.

If you’re playing back short segments, then you can use algebra to figure out which parts of the buffer are legit to play from. The recording head moves in a straight line with slope = 1. The playback head also moves in a straight line, with slope = rate. These will intersect at some point. If the intersection occurs within the time range of the playback synth, then click.

I’m afraid it’s late in the day here and my brain is fried – I started to try to work it out, but, ain’t gonna happen right now. But it is possible, based on the intersection of two straight lines, to predict whether a given rate will or will not cause a click – and then you can reject the bad ones.

Sam’s idea also works: If you start from the moment that is currently recording, and always go slower than the recording, then the intersection is always “behind” you in time.

hjh

Thank you for the responses so far. I got rid of the aliasing distortion somehow. At least for now.

I will mainly be playing back short segments from a buffer. Mostly overlapping ones I think. Some for more granular stuff, some for drones.

I’m trying to work out logic conditions for the BufRd playback phasor. For now I’m thinking something along the lines of this: I’d want the playback phasor to be at least the duration of the segment playback value behind the recording phasor. If possible.

My questions are now:

  1. How would I go about rejecting the playback of the segment if the conditions aren’t met? Set up a trigger system with a trig input for the playback phasor and send it a 0 if conditions aren’t met? Right now they’re just going between 0 and BufFrames.kr(bufnum).

2) I would like the duration to be determined by the Pbind that is playing back the Synth. Say I have \dur that is something like Pwhite(0.2,1,inf), how do I go about sending that pattern data to the Synthdefs? I’d like to make sure that is the duration that the player phasor is offset by. If that makes sense.

EDIT: I think the last part was just be thinking about bussing without need.

After tying my head into knots, and a couple of silly algebra mistakes, the answer that I come up with is:

  • If rate > 1, then the starting buffer position of the segment should be at least (rate - 1) * segmentDur before the current recording position. That is, if the record head is now at 1.2 seconds in the buffer, and rate = 2, and you plan to play a 0.2 sec grain, then the starting position must be < 1.2 - ((2 - 1) * 0.2) or < 1.0 in the buffer. (Then, at segment start, time now = 1.2, rechead = 1.2, playhead = 1.0. And at segment end, time = 1.4, rechead = 1.4, playhead = 1.4 – no problem.)

  • If rate < 1, then all recent audio data should be fine. The problem would be if you go back far enough in the past (but you probably won’t do that).

I had started to write up the derivation, but that’s going to take too much time…

So:

  • Offset from record head = (1 - rate) * dur. (I said "(rate - 1) * segmentDur before recHead" above, but “before” inverts the term. If we want to apply the offset by addition, then it has to be the 1-rate version.)
  • Start pos = rechead + offset (less than this, if rate > 1).
  • Then modulo buffer size.

hjh

I appreciate your help a lot. Bear with me here, I’m not well-versed in Supercollider. Though this problem is forcing me to learn a lot more than I already knew.

I’m still having issues getting it to work. Could you explain where you’d put the modulo buffer size?
As far as I understand your comment, it should be something along the lines of (bits of my code as of now):
offset = (1 - rate) * dur (I assume dur needs to be scaled here?)
and
phasorStart = Latch.ar((In.ar(phasorRecBus.index,1)),1) + offset;
but then something like phasorStart = (Latch.ar((In.ar(phasorRecBus.index,1)),1) + offset) % bufLength;

I’m not even sure I’m reading the values from the record phasor correctly to be honest.

This happens somewhat often – “That shouldn’t be too hard, right?” and then you find unanticipated gotchas. One problem at a time…

Yes, you got it right, as the last thing when calculating the start position.

You have a recording phasor that you know runs between 0 and bufLength - 1.

And we know that the “bad” range of playback start positions may be offset away from the record position.

If the record position is very close to the beginning of the buffer, and (1 - rate) * dur is negative, then it’s possible that some start positions at the very end of the buffer may be invalid.

You could try to write complex logic to handle that, but I think it’s easier in that case just to calculate the start position as a negative number, and let % bufLength wrap the negative number around to where it should be.

(recordpos + offset) % bufLength

Note that, for rate > 1, recordPos + offset is the latest click-free starting position. You could calculate a random offset randomOffset = offset + (aPositiveNumber.rand * offset.sign) and that would be valid too.

All of my reasoning was in seconds – so yes, before using any of these values, you would need to multiply by the buffer sample rate.

I can’t tell, because your initial example is “without any bus-reading at this point” and I don’t see any other examples from you.

In case this helps anyone else – I thought some more about how to explain the principle. (I find it hard to remember formulas, but if I know the principle, then I can get back to the formula.)

Let’s call the current time ‘t’ (real-time).

Let’s assume that recording always proceeds in real time: record head R = t.

A grain for playback starts at time t0, playing back from buffer position s, with duration d and rate r. So: for this grain, t is between t0 and t0 + d, and the buffer position for t is (t - t0) * r + s. (All in seconds for now.)

If r = 1, then the recording and playback lines are either congruent or parallel – so it’s impossible for them to cross at a specific point, no click, no problem. So we can disregard that case.

If r != 1, then there will be a specific intersection between the lines, at real-time ‘ti’. We’re interested in a start position that will control the intersection.

Intersection means R = P, means ti = (ti - t0) * r + s.

Test 1: Let’s assume that the intersection time is at t0, exactly. For that to be the case, intuitively, playback would need to start exactly at the record head (think about it). The math works too: if ti = t0, then t0 = (t0 - t0) * r + ss = t0.

Test 2: Let’s assume it’s at the other endpoint: ti = t0 + d (playback ends at the intersection point). Then:

// first solve for s
ti = (ti - t0) * r + s
s = ti - (r * (ti - t0))
s = ti - r*ti + r*t0
s = (1 - r)*ti + r*t0

// substitute ti = t0 + d
s = (1 - r) * (t0 + d) + r*t0
s = t0 - r*t0 + d - r*d + r*t0  -- r*t0 terms cancel
s = t0 + d - r*d
s = t0 + ((1-r) * d)

… which is the grain starting real time (= record head), plus an offset (1 - r) * d. (Note that in the special case r = 1, the offset is 0 :wink: )

If s is between t0 and t0+offset, then you will get a click. If r > 1, the offset is negative, so you would want to go further negative: Any s < t0+offset is OK. If r < 1, the offset is positive, and valid s values would be further positive: s > t0+offset.

hjh

Once again, thanks for the thorough explanation. I think I’ve got a pretty good grasp of the idea now, but lo and behold, the aliasing distortion is back. I’ve tried everything in my power - I think.

With the current code, the aliasing starts, seemingly, after the first length of the buffer has been recorded to. Then it sometimes disappears (adhering to both the pan and envelope of the play synthdef) and then returns. Is something crossing over into the buffer? I’ve tried replacing the play synthdef with a PlayBuf version instead to make sure it wasn’t the playback phasor causing issues. It doesn’t seem like it was that.
I’ve tried to use BPZ2 to limit the frequency range, thinking something might be mirroring back somewhere, but to no avail.

Also, when I leave the synthdef running for some time (looped by a pbind), the aliasing will intensify and be accompanied by clicks and pops, the CPU load slowly increasing. I wouldn’t think I had to call doneAction on anything in the recording synth. That’s just plugging away, recording to the buffer in a loop. And I’m calling doneAction: 2 on the play synthdef via the envelope.

It’s impossible for me to continue my exploration of the sampler when this is happening. Can anyone spot any glaring errors?

(
var bufLength,phasorPlayBus,phasorRecBus,buffer;

buffer.free;
bufLength = 5 * 48000;
buffer = Buffer.alloc(s, bufLength, 1, bufnum:0);

phasorRecBus = Bus.audio(s,1);
phasorPlayBus = Bus.audio(s,1);

SynthDef.new(\liverec, {
	arg channelIn=2, bufnum=0;
	var input, phasorEnd, phasorStart, rate, phasor;

	input = SoundIn.ar(channelIn);
	phasorEnd = BufFrames.kr(bufnum) - 1;
	rate = BufRateScale.kr(bufnum) * 1;
	phasor = Phasor.ar(0, rate, 0, phasorEnd);
	Out.ar(phasorRecBus, phasor);
	BufWr.ar(input, bufnum, phasor, loop:1);
}).add;

SynthDef.new(\liveplay,{
	arg atk=0.1, rel=1, note=60, bufnum=0, outBus=0, pan=0, amp=1, dur=0;
	var phasorStart, env, rate, out, in, phasor, phasorEnd, offset, durScaled, randomOffset;

	env = EnvGen.kr(Env.linen(attackTime: atk, releaseTime: rel), doneAction:2);
	rate = BufRateScale.kr(bufnum) * (note/60);
	durScaled = (dur * BufSampleRate.kr(bufnum));
	offset = ((1 - rate) * durScaled);
	phasorEnd = BufFrames.kr(bufnum) - 1;
	phasorStart = (In.ar(phasorRecBus,1) + offset) % bufLength;
	phasor = Phasor.ar(0, rate: rate, start: phasorStart, end: phasorEnd);
	out = BufRd.ar(1, bufnum, phasor, loop:1);
	Out.ar(phasorPlayBus, phasor);
	Out.ar(outBus, (Pan2.ar(out, pan, amp * env)));
}).add;
)

I still haven’t implemented some of James’ ideas from the last post as I need to figure out this issue first before progressing.

I don’t see what the problem is yet, but a couple of thoughts:

  • I don’t see how you’re starting your synths. There may be a mistake in that, but you haven’t shared that code, so it’s making it harder for people to help you.

  • Second, eliminate unknowns – I can’t tell if it’s a recording or playback problem. One way to find out is to (temporarily) change your recording synth to add an Out.ar(0, BufRd.ar(1, bufnum, phasor).dup) – to be 100% sure you’re hearing what was just written into the buffer using the same phasor. If there’s no problem, then you know for sure it’s in the playback SynthDef. In that case, start with the simplest possible playback synth. If rate == 1, then there can’t be a click, so, first try, start at buffer position 0 with rate == 1. Then add features until it breaks.

This isn’t the cause of the problem, but Phasor counts up to its end position - 1, so you shouldn’t subtract one here.

hjh

I got the source of the aliasing removed; it was me, without know better, calling the \liveRec synthdef in a Pdef. So multiple \liveRecs were banging away at the same buffer and ramping up the CPU.

And ah, thanks! I will get back to the coding later and hopefully find the issue - now that I am able.