Why does my synth glitch at higher pitches?

Hi SuperCollider hive mind!

I have just started learning SuperCollider and I’m doing some experimentation with feedback delays, more particularly recreating the Karplus-Strong resonator from scratch.

I realize that the built in Pluck class does exactly that, but by doing it from scratch, I can of course get more fine-grained control over the sound.

It seemed like I succeeded with my experiment until I tried playing some higher notes and glitches started to appear. For some reason, the glitches only seem to appear if the number of samples of the wave period resulted by the input frequency of the synthDef is shorter than the block size of the audio device.

Below follows the source code to a proof-of-concept demonstration of the problem, with comments included to walk you through it:

// 1. Setup the server (my devices of choice are commented out).
(
~opts = Server.default.options;
// ~opts.inDevice = 'Windows WASAPI : Microphone (Realtek(R) Audio)';
// ~opts.outDevice = 'Windows WASAPI : Speakers (Realtek(R) Audio)';
~opts.sampleRate = 48000;
~opts.blockSize = 64;
)

// 2. Boot the server.
s.boot;

// 3. Add the two synthDefs: 'pluckTest', featuring the built-in SC Pluck class for a ready-to-use Karplus-Strong pluck emulation, and my own work-in-progress proof-of-concept 'feedbackTest' that for now aims to do the same thing.
(
SynthDef(\pluckTest,{arg out=0,freq=220,gate=1,amp=1;
	var pluck;
	EnvGen.kr(Env.asr(0, 1, 0.5), gate: gate, doneAction: 2);
	pluck = Pluck.ar(
		in: BrownNoise.ar(0.1*amp),
		trig: gate,
		maxdelaytime: 0.5,
		delaytime: freq.reciprocal,
		decaytime: LinLin.kr(gate, 0, 1, 0.1, 10),
		coef: LinLin.kr(gate, 0, 1, 0.9, 0.2)
	);
	Out.ar(out, pluck.dup);
}).add;

SynthDef(\feedbackTest, {arg out=0,freq=200,gate=1,amp=1;
	var sustainFbk = 0.998, releaseFbk = 0.2, sr, buf, bufLength, splOffset, ptr, noiseEnv, noise, sig;

	sr = s.sampleRate;
	bufLength = (sr * 0.0625).floor;
	buf = LocalBuf(bufLength,1).clear;
	splOffset = sr / freq;
	ptr = Phasor.ar(0, 1, 0, bufLength);
	noiseEnv = EnvGen.kr(Env.perc(0, 0.02, amp), gate);
	noise = BrownNoise.ar(noiseEnv) * 0.1;
	sig = BufRd.ar(1, buf, ptr % bufLength) * LinLin.kr(gate, 0, 1, releaseFbk, sustainFbk) + noise;
	sig = OnePole.ar(sig, 0.2);
	BufWr.ar(sig, buf, ptr + splOffset);
	EnvGen.kr(Env.asr(0, 1, 2), gate: gate, doneAction: 2);
	Out.ar(out, sig.dup);
}).add;
)

// (Steps 4-7 are just a demonstration of the two synthDefs where no problem is anticipated. Feel free to skip these and go directly to step 8.)
// 4. A simple pattern for trying out the synthDefs. First with 'pluckTest'...
(
Pbindef(\demo,
    \instrument, \pluckTest,
	\dur, Pseq([0.25, 0.5, 0.25, 0.25, 0.5], inf),
	\freq, Pseq([[88, 132], 440 / 3, 220, 330, 440], inf),
	\sustain, 1.65,
    \amp, Pseq([0.3, 0.22], inf)
).play;
)

// 5. ...then with 'feedbackTest' (should sound quite similar and without glitches).
Pbindef(\demo, \instrument, \feedbackTest);

// 6. Optionally, compare with 'pluckTest' again.
Pbindef(\demo, \instrument, \pluckTest);

// 7. Stop.
Pbindef(\demo).stop;

// 8. Another pattern featuring higher pitches to demonstrate the issue. First with 'pluckTest'...
(
Pbindef(\jaws,
	\instrument, \pluckTest,
	\dur, 0.3,
	\midinote, Pseq([78, 79], inf),
	\amp, 0.5
).play;
)

// 9. ...and then with my 'feedbackTest' (glitches are anticipated at the higher note).
Pbindef(\jaws, \instrument, \feedbackTest);
// Since the higher note's frequency results in a period of fewer samples than the block size, glitches appear (I don't know why).

// 10. Optionally, compare with 'pluckTest' again.
Pbindef(\jaws, \instrument, \pluckTest);

// 11. Stop.
Pbindef(\jaws).stop;

// 12. A hacky proof-of-concept solution is to reboot the server with a lower block size:
(
~opts.blockSize = 32;
s.reboot;
)

// 13. Now, after the reboot is finished, try the second pattern with 'feedbackTest' again. The glitches should be gone.
Pbindef(\jaws, \instrument, \feedbackTest);

Since I am new to SuperCollider, I highly suspect that I have misunderstood some aspect of some core concept of how the software works, so I am therefore reaching out to you, hoping to gain some clarity and also hopefully learn what went wrong with my experiment.

Thanks in advance!

The glitches appear because of this. You cannot have a delay shorter than ControlDur.ir unless you write it in C++ (which is what Pluck is written in). In other words, you simply cannot build a Karplus-Strong resonator that works at high frequencies in supercollider (but you can use one made in C++ as a server plugin). In Max you can use gen~ to do this as it jit compiles it, but you cannot do it in normal max for this same reason.

2 Likes

I’m not really a Max user, but I think you can tell the [poly~] to reblock a patch to smaller (or higher) vector size, see poly~ Reference. (Resampling is also supported.)

In Pd you can use the [block~] object to reblock and/or resample any subpatch or abstraction. With [block~ 1], for example, you can have single-sample feedback.

Local resampling/reblocking is a really powerful feature that I sorely miss in SuperCollider.

2 Likes

Thanks for your replies! So in other words, use some other program? :face_with_diagonal_mouth:
This raises another question: if you want to do some DSP programming involving delays shorter than the block size and integrate it with SuperCollider, preferably without having to compile C++ code but possibly by using some quark or other extension or by integrating third party software, like Max or Pure Data, what would be the best approach?

Supercollider isn’t really a DSP library, its a `A PLATFORM FOR AUDIO SYNTHESIS AND ALGORITHMIC COMPOSITION, USED BY MUSICIANS, ARTISTS AND RESEARCHERS WORKING WITH SOUND’ (from the website).

I’m pretty sure you have to compile something, but there are many options; perhaps Faust would work for you? Getting started with Faust for SuperCollider

1 Like

Thanks for the tip! I had a look at Faust yesterday, but I did not know about the faust2sc utility. This might definitely come in handy! :slight_smile:

This may be obvious but you could just set your block size lower, if your computer can handle it… (I also miss local resampling / reblocking)

Also there is Fb1 from miSCellaneous_lib for single sample feedback with higher block sizes

1 Like

Thanks for your input! Setting the block size lower has indeed occurred to me. (In fact, my code snippet actually contains a command where exactly that is accomplished, even with a comment above explaining it in clear text. :smirk:)

Thanks for the tip about the miSCellaneous_lib though! I just took quick look at it, and it might come in handy in all sorts of contexts.

Hey @Frigolito there are some workarounds with demand rate but ultimately, as other people mentioned, you have to write your own ugen.
Here you can find my experiments with it, 4 ways of doing it in sc and then ugens with a slightly different implementation ( in the ‘compiled extensions’ folder, also on my main page you can find the 3 ugens i wrote for KS)

2 Likes

Thanks for your comment! I have had a look at it, and I think it can come in handy as a reference point if I some day desire to write my own UGen from scratch.

1 Like