Karplus Strong - Buffer as Delay Line

Hi Everyone. Sorry that I posted this before and deleted it. I hope that doesn´t cause confusion. I´m new to the forums but already learning alot!

I´ve created a Karplus Strong String model using a Delay UGen. Now I would like to translate that same code, but this time to using a Buffer as a delay line. My “translation” is not working - Maybe someone can spot what is going wrong with my BufRd and BufWr usage.

First the Karplus Strong model with DelayC. I´ve also set it up so that I can tune the “string” but it doesn´t work in higher frequencies (this is a whole other question - feel free to help me with this as well):

(
SynthDef(\karplus_tuneable, { arg impulse = 0.5, amp = 0.5, del = 440, gain = 0.99, lpf = 2000;
	var in = Impulse.ar(impulse);
	var infeed = LocalIn.ar(1);
	var delay = DelayC.ar(in + infeed, 3, (1/del) - (1/s.sampleRate * s.options.blockSize));
	var lpdelay = LPF.ar(delay * gain, lpf);
	LocalOut.ar(lpdelay);
	Out.ar(0, lpdelay*amp!2);
}).add
)

x = Synth(\karplus_tuneable, [\del, 440]);

And here, my “translation” to using a Buffer. (Doesn´t work, although a cool sound results).

~buffer = Buffer.alloc(s, s.sampleRate * 8.0, 1); 

(
SynthDef(\karplus_buffer, { arg impulse = 0.5, length = 1; 
	
	var in = Impulse.ar(impulse);
	var infeed = LocalIn.ar(1);
	
	var frames = BufFrames.kr(~buffer.bufnum) * length;
	var phasor = Phasor.ar(0, BufRateScale.kr(~buffer.bufnum), 0, frames); 
	
	var bufferIn = BufWr.ar(impulse + infeed, ~buffer, phasor, 0, frames);
	var bufferOut = BufRd.ar(1, ~buffer.bufnum, phasor * frames) * 0.9; 
	
	var outfeed = LocalOut.ar(bufferOut);
	
	DetectSilence.ar(infeed, doneAction: 2);
	Out.ar(0, Limiter.ar(bufferOut, 0.2));
}).add
)

x = Synth(\karplus_buffer, [\length, 1]);

What have I done wrong? Why isn´t buffer behaving? What am I missing?

Thank you!

A delay is basically:

  • Write into a buffer at position x (phasor in your code).
  • Read from the buffer at an earlier time point “x - (delay_sec * sample_rate)” modulo buffer size (BufRd can do the mod for you if the loop input is nonzero).

It looks like you’re referring to buffer size instead of sample rate, and multiplying instead of subtracting. So it’s something other than a delay.

Let’s imagine a 10 sample buffer. So frames = 10. Frame indices are 0 to 9.

When phasor = 0, frames * phasor = 0. Ok. First buffer sample.

When phasor = 1, frames * phasor = 10. That’s outside the buffer range. If loop is 0, then you get silence. If loop is nonzero, then it will wrap back to… the first sample.

Same for all other integer values of phasor.

So it’s definitely not a delay.

Block calculation means that the shortest possible feedback loop is s.options.blockSize / s.sampleRate seconds (or ControlDur.ir) in the server. The highest frequency is the reciprocal. With default settings, it means that the maximum frequency of a feedback loop is 44100/64 = 687.something Hz.

To go higher currently you can do one of three things:

  • Set blockSize to a smaller power of 2 (e.g. 16 would give you 2 more octaves) and reboot the server. The server would run a little less efficiently but it may be worth it.

  • Or look at the Feedback quark. (Almost forgot this one!)

  • Or find/write a C++ plugin to do the feedback loop and filtering internally. There’s Pluck in the core library, or sc3-plugins has a number of excellent digital waveguide UGens.

hjh

@jamshark70
Thank you so much for taking the time for a detailed answer! Your explanation for a delay makes sense. I wish I had a better understanding of delay. I wish I were better at implementing certain processes like that into SuperCollider.

So, I took the equation and altered my BufRd, but still only get a click. Any thoughts? (I want to do this myself instead of using Pluck because I want to be able to explore altering the various building blocks of this function. My original mistake after all did produce an interesting sound. However, I need to understand the basics nonetheless ).

(
~bus = Bus.audio(s, 1);
s = Server.local;
~buffer = Buffer.alloc(s, s.sampleRate * 8.0, 1);

SynthDef(\impulse, {arg impFreq, bus;
	var impulse;
	impulse = Impulse.ar(impFreq);
	impulse = FreeSelf.kr(impulse);
	Out.ar(bus, impulse);

}).add;

SynthDef(\karplus, {arg  bus, delay_sec;
	var preRead, impulse, env, infeed, output, bufferIn, bufferOut, frames, phasor;

	impulse = In.ar(bus, 1);
	infeed = LocalIn.ar(1);
	

	frames = BufFrames.kr(~buffer.bufnum);
	phasor = Phasor.ar(0, BufRateScale.kr(~buffer.bufnum), 0, frames);
	
	bufferIn = BufWr.ar(impulse + infeed, ~buffer.bufnum, phasor * frames, 0, frames);
	bufferOut = BufRd.ar(1, ~buffer.bufnum, phasor - (delay_sec*s.sampleRate))  * 0.999;
	


	LocalOut.ar(bufferOut);
	DetectSilence.ar(infeed, doneAction: 2);

	output = Out.ar(0, bufferOut);
}).add;
)

(
x = Synth(\karplus, [\bus, ~bus, \delay_sec, 0.01]);
y = Synth(\impulse, [\bus, ~bus]);
)



Follow up question - when do I need to write .bufnum? I am using ~buffer, but also writing ~buffer.bufnum just to be sure. I´m not certain what the difference is.

Thanks a million!

@jamshark70

I think I got it. I forgot to reroute the bufRd. :slight_smile:

(
~bus = Bus.audio(s, 1);
s = Server.local;
~buffer = Buffer.alloc(s, s.sampleRate * 8.0, 1);

SynthDef(\impulse, {arg impFreq, bus;
	var impulse;
	impulse = Impulse.ar(0);
	//impulse = FreeSelf.kr(impulse);
	Out.ar(bus, impulse);

}).add;

SynthDef(\karplus, {arg  bus, delay_sec;
	var preRead, impulse, delay, env, infeed, output, bufferIn, bufferOut, bufferOutDelay, frames, phasor;

	impulse = In.ar(bus, 1);
	infeed = LocalIn.ar(1);
	delay = delay_sec * s.sampleRate;
	

	frames = BufFrames.kr(~buffer.bufnum);
	phasor = Phasor.ar(0, BufRateScale.kr(~buffer.bufnum), 0, frames);
	
	bufferIn = BufWr.ar(impulse + infeed, ~buffer.bufnum, phasor , 0, frames);
	
	bufferOut = BufRd.ar(1, ~buffer.bufnum, phasor) * 0.9;
	bufferOutDelay = BufRd.ar(1, ~buffer.bufnum, phasor - delay)   * 0.99;
	


	LocalOut.ar(bufferOutDelay);
	DetectSilence.ar(infeed, doneAction: 2);

	output = Out.ar(0, bufferOutDelay + bufferOut);
}).add;
)

(
x = Synth(\karplus, [\bus, ~bus, \delay_sec, 1/600]);
y = Synth(\impulse, [\bus, ~bus]);
)

`

Great! Also this version deletes the * frames, which was causing the circular buffer to be not a circle but rather a point.

One thing you could make more efficient – currently you have:

  • Make a signal impulse + infeed.
  • Write it to the buffer.
  • Read it back from the buffer and * 0.9.

But the “write to” and “read from” are the same here! So bufferOut is not providing any new information – so this BufRd is redundant.

sig = impulse + infeed;
bufferIn = BufWr.ar(impulse + infeed, ~buffer.bufnum, phasor , 0, frames);
bufferOut = sig * 0.9;
bufferOutDelay = BufRd.ar(1, ~buffer.bufnum, phasor - delay)   * 0.99;

Edit: Tablet posted prematurely.

It can be difficult to be certain.

Unit generator inputs, Synth argument lists and Event parameters generally convert bus and buffer objects for you.

If you’re adding an offset (aBuffer + 1) you may need bufnum explicitly.

But in general, it’s not an ideal practice to hardcode buffer indices into a SynthDef. It’s better to use SynthDef arguments to pass the buffer index in.

hjh

@jamshark70 thank you! You are a supercollider angel.

Any ideas why the current code doesn´t get the pitch correct? It seems to be about a third off. I mean, when I put delay_sec = 100, I´m looking to get 100 Hz.

(
~bus = Bus.audio(s, 1);
s = Server.local;
~buffer = Buffer.alloc(s, s.sampleRate * 8.0, 1);

SynthDef(\impulse, {arg impFreq, bus;
	var impulse;
	impulse = Impulse.ar(0);
	//impulse = FreeSelf.kr(impulse);
	Out.ar(bus, impulse);

}).add;

SynthDef(\karplus, {arg  bus, delay_sec;
	var preRead, impulse, delay, env, infeed, output, bufferIn, bufferOut, bufferOutDelay, frames, phasor, lpf;

	impulse = In.ar(bus, 1);
	infeed = LocalIn.ar(1);

	delay = 1/delay_sec * s.sampleRate;

	frames = BufFrames.kr(~buffer.bufnum);
	phasor = Phasor.ar(0, BufRateScale.kr(~buffer.bufnum), 0, frames);

	bufferIn = BufWr.ar(impulse + infeed, ~buffer.bufnum, phasor, 0, frames);


	bufferOut = impulse + infeed;
	bufferOutDelay = BufRd.ar(1, ~buffer.bufnum, (phasor - delay))   * 0.9;

	LocalOut.ar(bufferOutDelay);
	DetectSilence.ar(infeed, doneAction: 2);

	lpf = LPF.ar(bufferOut + bufferOutDelay, 1000);

	output = Out.ar(0, lpf);
}).add;
)

(
x = Synth(\karplus, [\bus, ~bus, \delay_sec, 100]);
y = Synth(\impulse, [\bus, ~bus]);
)

Ah, forgot about that. (Side note, if the input is 100 for 100 Hz, freq is a more apt name for it.)

LocalIn / LocalOut add one block’s worth of delay, so you should subtract that from the delay time: delay = SampleRate.ir / freq - ControlDur.ir;. (Note also that it’s better in a SynthDef to use the info-UGens to get server details like sample rate and block size. Also 1 / x * y is less efficient than y / x.)

Edit: I forgot to convert ControlDur into samples rather than seconds, so it’s really sr / f - (sr * cd) – then you have two terms with a factor of the sample rate, so simplify algebraically: sr * (1/f) - (sr * cd) = sr * (1/f - cd):

SampleRate.ir * (f.reciprocal - ControlDur.ir)

At 44100 Hz, a 100 Hz wave occupies 441 samples. If the block size is the default 64, that bumps it up to 505 samples. Then the frequency interval between them will be 505/441 = 1.14something. Then, to know the equal tempered interval, we need the logarithm base 2 of that ratio – that’s the proportion of an octave occupied by that interval – and then multiply by 12 for semitones: log2(505 / 441) * 12 gives about 2.346 or almost 2 1/2 semitones, “about a third” indeed. So 1/ your ears are fine :grin: and 2/ the math shows that a 64 sample addition to the delay fully explains what you’re hearing.

hjh