Help me improve my de-clicking sample playback strategy.

Hello everyone!

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. :slight_smile:

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:

Here’s an example with a very slow smooth applied:

Any recommendations? Thanks!

1 Like

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. :stuck_out_tongue:

This is my go to approach for situations like that:

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?

@marc_boule - might you be able to help Bret out here by any chance please? I know we use a lot of de-clickers!

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.

1 Like

Ah, ok! It will take me some time to digest this, but a HUGE thank-you for taking the time to break it down for me!

1 Like

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.

2 Likes

Thanks Marc. It sounds like multiple playheads is the way to go, so I’ll start experimenting with that.

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!

2 Likes

Another way to attack it would be to look at the sample at the new play position. fade the old sample to the first sample at the new position.

If the last sample under the old playback head is the same as the first sample at the new position, just start playing at the new position.

If the last sample is less than the new first sample, back up in the old sample and ramp up to the new sample value.

If the last sample is greater than the new first sample, back up in the old sample and ramp down to the new sample value.

Just spitballing here, I’m NOT an audio DSP programmer by any means.

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:

  1. The kick drum sample is playing
  2. You trigger kick start playing again, which will interrupt the old playback
  3. 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.

My brain is easily confused, so I might be wrong.

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…

1 Like

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. :+1:

1 Like

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.