A good many of my modules are sample players, and most of them have some way of modulating the playback position. This would typically cause clicks and pops, so I wrote a really simple algorithm to smooth things out. I was hoping that I could get feedback on my approach.
I’ll start by posting the code, then walk through it how it works:
#pragma once
struct DeclickFilter
{
double smooth_ramp = 1;
// The HIGHER the smooth_constant, the faster the smoothing will take effect
// 2048 seems to work well and doesn't have a noticeable affect on the
// snappiness of drum sounds. Low values, such as 256 or lower, will have
// a dramatic affect on the punch of drums.
double smooth_constant = 2048.0;
double smooth_rate = (smooth_constant / rack::settings::sampleRate);
float previous_left_audio = 0.0;
float previous_right_audio = 0.0;
// Trigger the declick filter before a large jump in voltages due to
// sample position, or waveform selection, or other types of events that
// would normally create clicks and pops in the output.
void trigger()
{
smooth_ramp = 0;
}
// This method should be called by the module when onSampleRateChange is called
void updateSampleRate()
{
smooth_rate = (smooth_constant / rack::settings::sampleRate);
}
void process(float *left_audio_ptr, float *right_audio_ptr)
{
if(smooth_ramp < 1)
{
// smooth_ramp counts up at smooth_rate until it reaches 1.0
smooth_ramp += smooth_rate;
// Start at the last known audio value and "morph" into the up-to-date
// audio signal over a small amount of time.
*left_audio_ptr = (previous_left_audio * (1 - smooth_ramp)) + ((*left_audio_ptr) * smooth_ramp);
*right_audio_ptr = (previous_right_audio * (1 - smooth_ramp)) + ((*right_audio_ptr) * smooth_ramp);
}
previous_left_audio = *left_audio_ptr;
previous_right_audio = *right_audio_ptr;
}
};
Here’s some pseudocode to illustrate its usage:
struct MyModule : Module
{
DeclickFilter declick_filter;
float left;
float right;
void process(const ProcessArgs &args) override
{
getAudio(&left, &right); // fills in the two variables with some sample audio
incrementSamplePlaybackPosition();
if("something would cause a click or pop") declick_filter.trigger();
// Run the filter
declick_filter(&left, &right);
// Output the voltages
outputs[LEFT_OUTPUT].setVoltage(left);
outputs[RIGHT_OUTPUT].setVoltage(right);
}
}
This, unfortunately, doesn’t work very well. Here’s a transition with a very fast smoothing applied:
Not as simple or clean but for static sample playback you could make the ramp be a crossfade between the previous (still running) playhead’s value and the next playhead’s. This is essentially what you want to happen instead of “freezing” the previous sample value at the point of change.
It gets messy when triggering a change while the ramp is still going but you can generalize that to be “polyphonic” instead of just 2 playheads.
Might also work better with an exponential ramp instead of linear but that’s just a guess.
Running two playheads is a really interesting idea. I’ll see if that’s something I can implement. I wonder if that algorithm will run into issues when de-clicking looping samples. Hmmmm…
If the loop region is within a larger sample it can all work the same way and if the loop ends at the end of the sample you would effectively use your original filter, since it is the same as the multiple playheads implementation with the previous playhead simply not moving when reaching the end.
Or treat it as an issue with the sample itself left to be fixed by the creator of the loop.
Hi Bill, that document is brutal! It seems to be talking about Karplus-Strong, which I understand in theory, but that paper is so thick with terminology that it makes my head spin. I’m not trying to implement a delay line. Can you summarize the basics for me, and how it might apply to creating a de-clicking solution?
Hi Bret, the paper describes a playback strategy where 2 playheads move along a delay line (could apply to any sample stream) with an algorithm to crossfade between the playheads to smooth the output. The paper is written as a way to implement a modulating tap on a delay line, but the principles should apply to any sample playback scheme.
Modulating a tap on a delay line (or any sample buffer playback point) causes discontinuities which result in clicks. Crossfading between dual read heads smooths out those discontinuities. The dual read heads are a fixed distance apart, usually a small number of samples, and can be thought of as a single tap: they move together always staying a fixed distance apart. Output is produced by reading samples from each read head and interpolating (linear, equal power, etc.) As output samples are produced the crossfade point is updated. This results in an alternating behavior every N samples where one of the read heads gets 100% of the crossfade and the other gets 0%. Selecting an appropriate distance between the read heads and determining the crossfade rate (that depends on the rate of motion of the dual read heads) are the primary tunable parameters.
Modulating the playback point on a sample buffer is identical to modulating a tap on a delay line. Any non-1:1 playback motion would need to be de-clicked. It took me a few reads to get the concepts and some work/hacking/experimenting to get it working in my delay lines, but it does work and sounds pretty good.
PS. Recommend always reading the buffer through the dual read head tap. I.e., the ’ ```
if(“something would cause a click or pop”) …’ in your OP would not be required. When modulating, every single sample read could potentially cause a click.
The only way I know of, and perhaps this is out of context for the task here, is a very fast crossfade (with a slew on the crossfade control signal). For a mute button for example, it comes down to a crossfade between the audio signal and 0V.
Well, hmmmmm. I’ve run into a bit of a conundrum. My cross-fading anti-click code does an amazing job of removing clicks. However, it’s also affecting the “punch” of kick drums.
I apply the cross fade when a sample is triggered while it is already playing. A kick drum with a long fall-off can be interrupted by the next kick drum, which then triggers a cross fade, and in turn, the second kick sounds muted.
I played with various cross-fade speeds. Unfortunately, values that are fast enough to avoid muting the kick drums are too fast for being effective at de-clicking other situations, such as when a sample’s start position is being modulated.
Perhaps another approach might be to make each track multi-timbral. That way, playing a kickdrum wouldn’t cut off the previous kick drum.
On a related note, I was able to apply slew limiters to the pitch, volume, and pan parameters so that they don’t cause clicks and pops. At least that’s some good news!
Thanks for the idea. Wouldn’t that delay the playback of the second sample? (I’m talking about the case where you trigger the sample playback. Not just looping a sample.)
Let’s take a look at the example where you have a kick drum with a long release. It would go something like:
The kick drum sample is playing
You trigger kick start playing again, which will interrupt the old playback
You would need to complete the fade from the first playback position before starting to output the second position. That would delay the playback of the second kick drum by a tiny amount.
With the 2 playheads technique what you could try is making the fade in window on the destination playhead adapt to the start position.
If the dest position is zero, dont fade in the dest audio (playhead B), just play it, but fade out the source audio (playhead A). But as playhead B moves into the sample, adjust the fade in window accordingly, from zero up to the fade-in max time.
That way you maintain the transients at the start of samples but avoid clicks when modulating inside the sample time.
I imagine you’ll have to experiment with how you map playhead B position to the fade in time and it probably won’t be a ‘one solution fits all scenarios’ situation. Maybe an option to choose crossfade strategy when playhead position is near the start of a sample would be useful to suit both heavily transient material like kicks and other sounds like pads and ambiences…
This seems like a pretty good solution. I had implemented something similar previously, but I didn’t fade out playhead A when playhead B started at the beginning of a sample. That might work.
The one thing I didn’t account for is the sample phase. The cleanest transition will be when you fade a rising signal into a rising signal & vice versa.
As for timing you have to anticipate the transition. To do that you introduce a delay window. Your module has to play back a little late relative to the Incoming trigger. In other words a look ahead buffer. Lots of modules do this, but if you keep it under 2 millesecond it will be perceived as still in sync.