How to get rid of popping sounds when jumping around Playbuf

I’ve got a simple little sound that’s kind of like a grainy jittery scrub through a sample, simply by using a Playbuf and a random Dust impulse signal to re-start playback at startPos based on MouseY. It’s very basic as I’m still a noob, and I’m sure there are more polished alternatives (maybe TGrains? something else?), but I’m wondering what my options are for getting rid of the popping sounds that often occur when it jumps around. I understand the cause (I’m abruptly cutting off the signal at some random value and beginning playback at some other random value), but I’m wondering how to properly think about this or approach problems like these. I guess I’d want some kind of envelope with a fast attack/release at each “edge” (each time the trigger causes a restart), but to do that I feel like I’d have to know in advance how long the random-length grain would be so that I could start fading out before the trigger fires.

Is it a common theme in SuperCollider (and programmatic audio stuff in general) that it’s easy to make crazy noises with math, but hard to polish the “edges”?

~buff = Buffer.read(s, Platform.resourceDir +/+ "sounds/a11wlk01.wav");
(
{
    PlayBuf.ar(
        numChannels: 1, 
        bufnum: ~buff,
        rate: BufRateScale.kr(~buff),
        trigger: Dust.ar(10),
        startPos: ~buff.numFrames * 2 * MouseY.kr,
        loop: 0);
}.scope(1);
)

Yes, crossfading by envelope is the right way to handle this.

The crossfaded segments should overlap during the fade in/out period, so you need two PlayBufs and two envelopes. If you don’t do it that way, you have a gap between the segments, which you probably don’t want.

Not really. The fades should overlap, meaning that you trigger A’s fade out and B’s fade in at the same time. So it’s enough to use gated envelopes, where the gate signals are the inverse:

{
	var trig = Impulse.ar(400),
	gate = ToggleFF.ar(trig),
	inverseGate = 1 - gate,
	envelopes = EnvGen.ar(Env.asr(0.001, 1, 0.001, [-4, 4]), [gate, inverseGate]);
	[gate, inverseGate] ++ envelopes
}.plot;

So:

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

a = {
	// could be Impulse or TDuty or other
	var trig = Dust.kr(3),
	// one segment is on at first trigger, off at second
	gate = ToggleFF.kr(trig),
	// the other is reversed
	gates = [gate, 1 - gate],
	// we need pairs of PlayBuf parameters, so: multichannel expansion
	// TRand.kr(..., gates) --> [TRand.kr(... gates[0]), TRand.kr(... gates[1])]
	// i.e. a new start frame when the segment fires but not otherwise
	startFrames = TRand.kr(0, BufFrames.kr(b) - 40000, gates),
	// similarly, 2 PlayBufs with alternating triggers
	signals = PlayBuf.ar(1, b, trigger: gates, startPos: startFrames),
	// alternating gate signals for 2 envelopes too
	egs = EnvGen.kr(Env.asr(0.01, 1, 0.01, [-4, 4]), gates),
	// apply envelopes to the 2 signals and mix before going further
	mix = (signals * egs).sum;
	// now it's 1 channel: final amp scaling and expand to stereo
	Out.ar(0, (mix * 0.1).dup);
}.play;

Is it a common theme in SuperCollider (and programmatic audio stuff in general) that it’s easy to make crazy noises with math, but hard to polish the “edges”?

It would be more accurate to say that VSTs handle the edge-polishing for you, so when you use VSTs, you are responsible for fewer of the details. (The plug-in developers handled them for you.) In SC, it’s up to you to handle a lot of things by yourself. So when you play sound file segments using Kontakt, Kontakt applies an envelope to every note and you don’t have to think about it. In SC, you do have to be aware that an envelope should be applied.

EDIT: PlayBufCF in wslib (quark) does it for you. But it’s worth taking apart the “how” in the example here.

hjh

5 Likes

Clever ! Thanks James !!

Dang, thank you so much, I’ve been absorbing your examples for the last 2 hours at least :slight_smile:

Two questions right off the bat:

  1. This is the first I’ve seen of ToggleFF so I was playing around with various plots and was noticing that { ToggleFF.ar }.plot produces a mess of random values, but { ToggleFF.ar(DC.ar(0)) }.plot is an expected flat 0. Is there something about ToggleFF that needs to be initialized by a real UGen value for it to properly function? (Also, I guess I’d assumed that passing in a number would auto-convert a number to a UGen, am I wrong about that? Isn’t that how passing in \freq, 1 to a synth works?
  2. How did you find PlayBufCF? Something you searched for or remembered from past experience?

That seems to be a bug. ToggleFF in the server assumes that the trigger input is at the same rate, but the SynthDef builder doesn’t enforce this. There’s an open issue to review all the UGens that need this check, but it’s a big job, may not be done quickly.

There’s no auto-conversion of numbers. If you plug a number into a UGen, then it’s hardcoded forever and it can never change. That’s a 100% rule, no exceptions: the only way the value of an input to a UGen can change is if that input comes from another UGen.

Because the value changes with \freq, x, then you know that this is not plugging the number in directly.

SynthDef(\test, { |freq = 440| freq.postln })
-> an OutputProxy

… not a number at all. This is a channel of a Control UGen.

Prior knowledge. There’s no good way to search for extensions. (Pure Data has the same problem… you can’t search it until you’ve downloaded it, but you don’t know which one to download…)

hjh