BufRd crackles after playing a while

I’m trying to simulate the sound of an LFSR a la Gameboy using a buffer which is filled with noise when triggered. It is mostly working, but I noticed when swapping the noise for a sine, the buffer starts crackling after about 20 seconds. I watched JACK and it didn’t show any xruns.

Here’s the SynthDef and the pattern I’m testing with.

(SynthDef(\fauxLfsr, {|
    outbus=0, freq=55, 
	decay=1, ampCurve=(-4),
	t_sync=0, quality=1, 
	t_rewrite=0, 
	t_trig=1,
	pan=0|
	var bufferSeconds = 1;
	var lastIndex = s.sampleRate;
	var buf = Buffer.alloc(s, lastIndex * bufferSeconds, bufferSeconds);
	
	// Reader - rate based on quality, not (note) freq
	var readRate = quality.max(0.00001).min(1);
	var startIndex = 0;
	var loopIndex = (lastIndex / freq * readRate).min(lastIndex);
	var readIndex = Phasor.ar(t_sync, readRate, startIndex, loopIndex);
	
	// Writer - one shot
	// Empty until t_rewrite triggered
	var writeIndex = EnvGen.ar(Env.new([0, lastIndex], [1]), t_rewrite);
	
	var ampEnv = EnvGen.ar(Env.perc(0, decay, 1, ampCurve), t_trig, /* doneAction: Done.freeSelf */);
	
	// Writer - one shot
	// .sign to make "1 bit" signal
	// BufWr.ar([WhiteNoise.ar.sign], buf.bufnum, writeIndex);
	BufWr.ar([SinOsc.ar], buf.bufnum, writeIndex);
	
	// Reader
	Out.ar(outbus, Pan2.ar(
		BufRd.ar(1, buf.bufnum, readIndex, 0, 4), 
		pan
	) * ampEnv);
}).add;);

(
var makeLine = {|pan| 
	Pmono(\fauxLfsr,
		// \rewrite, Pseq([1]) ++ Pwrand([0, 1], [0.6, 0.4], inf),
		\rewrite, Pseq([1]) ++ Pn(0), // necessary
		\sync, 1,
		\degree, Pdefn(\degree),
		\scale, [0, 1, 3, 4, 6, 7, 9, 10],
		\ctranspose, Pdefn(\tran),
		\dur, Prand((1..4)/10, inf) + Pwhite(-0.0001, 0.0001),
		\decay, Pdefn(\decay),
		\quality, Pdefn(\quality),
		\ampCurve, Pdefn(\ampCurve),
		\pan, pan,
	);
};
Pdefn(\degree, Pbrown(0, 12, 3));
Pdefn(\quality, 1/8);
Pdefn(\tran, -24);
Pdefn(\decay, 0.2);
Pdefn(\ampCurve, -4);

Ppar([-0.3, 0.3].collect(makeLine)).trace.play;
);

I initially used a non-interpolated BufRd, but the problem still reproduces with both linear and cubic interpolation.

This is a separate issue obviously, but I also noticed the BufWr doesn’t seem to re-read when I send it a \rewrite trigger (see commented-out parameter in the Pmono)

Subtle: BufWr does not actually stop writing at the end of the buffer when loop is 0 – it continually overwrites the last sample. That’s a guess (based on reading the source code) – I’m on my phone, haven’t tried your example.

I’d not recommend mixing the read and write in the same SynthDef. Have one SynthDef to prepare the buffer data (and this synth frees itself upon completion) and a second one that only plays.

Also not recommended to do Buffer.alloc in the SynthDef. Allocating a buffer is not a synth operation – it should be done externally to the synth, and you pass the buffer number in as an argument.

An envelope segment is not (as you’re probably thinking) start value, end value, duration. An envelope segment is always “start from the current value, go to the target value in dur seconds.” When you retrigger, then, the envelope has already reached lastIndex, and the trigger asks it to go from its current value lastIndex to the target value lastIndex. The Env needs a second segment to reset: Env([0, 0, lastIndex], [0, 1]).

But this problem goes away if you move the write into a separate one-shot synth.

hjh

1 Like

Thank you for your reply. Side note: I’ve been working through your pattern tutorial and patterns are finally clicking for me.

That is an interesting point and I see how this would necessitate freeing the writer SynthDef so that there isn’t a volatile pass-through sample hanging out at the end. For what it’s worth, this does not appear to be the problem since the way I implemented the looping with the Phasor, none of the values in the included pattern should ever reach the end of the buffer. The freq would have to be <1 Hz. Also the clicking is much more prevalent than a single sample jump would be and sounds more like a buffer underrun.

I can see why this would be a best practice, but this seems like it would get in the way of things like live sampling and move complexity into coordinating timing between Synths. I was hoping to be able to re-generate the buffer on-the-fly for potentially interesting effects and it made sense from a sort of ergonomic perspective to include that in the SynthDef. I am still a beginner, though, and perhaps there are some constructs to help with this type of thing that I’m not yet aware of?

I guess pre-allocating the buffer(s) I need and then writing to the same bufnum if I need to resample would help for those kinds of use cases.

Ahh, I see! Thanks for that!

EDIT: Oh… messages crossed. Will look at your other message.

I think this is free of crackling (though I’m not listening on studio monitors right now).

// run this block first
(
// buffer init needs to complete asynchronously
// requires scheduling

fork {
	var bufferSeconds = 1;
	var lastIndex = s.sampleRate;
	
	SynthDef(\fauxLfsr, {|
		bufnum,
		outbus=0, freq=55, 
		decay=1, ampCurve=(-4),
		t_sync=0, quality=1, 
		t_rewrite=0, 
		t_trig=1, gate = 1,
		pan=0|
		
		// Reader - rate based on quality, not (note) freq
		var readRate = quality.max(0.00001).min(1);
		var startIndex = 0;
		var lastIndex = BufFrames.kr(bufnum);
		var loopIndex = (lastIndex / freq * readRate).min(lastIndex);
		var readIndex = Phasor.ar(t_sync, readRate, startIndex, loopIndex);
				
		// var ampEnv = EnvGen.ar(Env.perc(0, decay, 1, ampCurve), t_trig, /* doneAction: Done.freeSelf */);
		var ampEnv = EnvGen.ar(Env.adsr, gate, doneAction: 2);
		
		// Reader
		Out.ar(outbus, Pan2.ar(
			BufRd.ar(1, bufnum, readIndex, 0, 4), 
			pan
		) * ampEnv);
	}).add;
	
	SynthDef(\waveWriter, { |bufnum, freq = 400|
		// Writer - one shot
		var lastIndex = BufFrames.kr(bufnum),
		writeIndex = EnvGen.ar(Env.new([0, lastIndex], [BufDur.kr(bufnum)])),
		// writer = BufWr.ar([WhiteNoise.ar.sign], buf.bufnum, writeIndex);
		writer = BufWr.ar(SinOsc.ar(freq), bufnum, writeIndex, loop: 0);
		FreeSelfWhenDone.kr(writer);  // stop!
	}).add;

	b = Buffer.alloc(s, lastIndex * bufferSeconds, bufferSeconds);
	s.sync;  // wait for all synthdefs and the buffer to be ready

	Synth(\waveWriter, [bufnum: b]);  // removes itself
};
);

// then this
a = Synth(\fauxLfsr, [freq: 1, quality: 1]);

a.release;  // quiet

hjh

1 Like

Wow, I was going to say, that was quite the fast reply!

Thanks again, will pore over this. I wanted to quickly clarify that I see your distinction now between allocating and writing to the buffer.

FWIW I’m trying this simpler pattern with your original synthdef (except allocating the buffer outside), and after a minute, I don’t hear the timbre changing at all. I can’t reproduce crackling :worried:

p = Pmono(\fauxLfsr, \freq, 400, \dur, 1, \decay, 10, \quality, Pdefn(\quality)).play;

p.stop;

hjh

Weird, I’m doing vice versa (your improved SynthDefs and my original pattern) and I can’t reproduce it either… maybe some edge/corner case bug?

EDIT: I think the fact that you didn’t use the \rewrite parameter in your pattern to re-write the buffer may be significant.

I took a similar route to what you did, moving the writer to its own SynthDef, and the crackles are gone. Interestingly, I’m still continuously writing to the buffer as part of a pattern, and now I’m not having any problems other than the expected click when the playback and record “heads” cross. The big difference is that the writers are “polyphonic” and now free themselves when reaching the range’s end, as you suggested. There seems to be an issue re-writing to a buffer with a monophonic SynthDef.

// Run first - buffer alloc, SynthDefs
(
fork {
    var bufferSeconds = 1;
    var lastIndex = s.sampleRate;
    var buf = Buffer.alloc(s, lastIndex * bufferSeconds, bufferSeconds);
    b = buf;

    // Writer - empty until t_rewrite triggered
    SynthDef(\fillBuffer, {|bufnum, freq=440, t_rewrite=1|
        var writeIndex = EnvGen.ar(Env.new([0, 0, lastIndex], [0, bufferSeconds]), t_rewrite, doneAction: 2);
        BufWr.ar([SinOsc.ar(freq)], bufnum, writeIndex);
    }).add;

    // Reader - rate based on quality, not (note) freq
    SynthDef(\fauxLfsr, {|
        bufnum,
        outbus=0, freq=55,
        decay=1, ampCurve=(-4),
        t_sync=0, quality=1,
        t_rewrite=0,
        t_trig=1,
        pan=0|

        var readRate = quality.max(0.00001).min(1);
        var startIndex = 0;
        var loopIndex = (lastIndex / freq * readRate).min(lastIndex);
        var readIndex = Phasor.ar(t_sync, readRate, startIndex, loopIndex);

        var ampEnv = EnvGen.ar(Env.perc(0, decay, 1, ampCurve), t_trig, /* doneAction: Done.freeSelf */);

        Out.ar(outbus, Pan2.ar(
            BufRd.ar(1, buf.bufnum, readIndex, 0, 1),
            pan
        ) * ampEnv / 2);
    }).add;
    s.sync;
    "all done".postln;
}
);

// Run second - patterns
(
var makeLine = {|pan|
	Pmono(\fauxLfsr,
		\bufnum, b,
		\sync, 1,
		\degree, Pdefn(\degree),
		\scale, [0, 1, 3, 4, 6, 7, 9, 10],
		\ctranspose, Pdefn(\tran),
		\dur, Prand((1..8)/4, inf),
		\decay, Pdefn(\decay),
		\quality, Pdefn(\quality),
		\ampCurve, Pdefn(\ampCurve),
		\pan, pan,
	);
};

Pdefn(\degree, Pbrown(0, 18, Prand((1..5), inf)));
Pdefn(\quality, (1/(2.pow(Prand((0..4), inf)))) + Pwhite(-0.01, 0.01));
Pdefn(\tran, -24);
Pdefn(\decay, 5);
Pdefn(\ampCurve, -4);

Pdefn(\bufferFiller,
	Pbind(
		\instrument, \fillBuffer,
		\bufnum, b,
		\freq, Pseq(((1..10) ++ (9..2)) * 55, inf),
		\rewrite, 1,
		\dur, Prand((1..8)/8, inf),
	)
);

Ppar([-0.5, 0, 0.5].collect(makeLine) ++ Pdefn(\bufferFiller)).trace.play;
);

Side note: are you using scide, and if so, how do you indent a block? I have to indent a line at a time in scide, which is pretty tedious.

This is one of my favorite scide features: just select a whole block of code and press tab, every nested block will be indented correctly for you.

1 Like

I just tried that and it worked. I don’t know why yesterday I thought it didn’t. Thanks!

Have to admit, I’m not sure why that would be. But glad it’s working now!

hjh

1 Like

I think the trick is something with a writer that frees itself rather than being persistent (as you suggested), but beyond that I’m at a loss.