Published on

How to implement oversampling in JUCE?

Authors

Introduction

Oversampling is one of those things you don’t skip once you start writing nonlinear audio effects — like distortion, saturation, or waveshaping. These kinds of processors generate harmonics above the Nyquist frequency (half the sample rate), which fold back into the audible range as aliasing.

To fight that, we oversample — meaning we temporarily increase the sample rate, process at that higher rate, and then downsample back to the original rate.

Check this article for more in depth information about oversampling: What is Oversampling?

When to implement oversampling in our plugin?

You don’t need oversampling everywhere — only where nonlinearities exist.

Here are a few situations where you should use it:

  • Distortion / saturation (anything that clips or folds the waveform)

  • Waveshapers

  • Bitcrushers (sometimes)

  • Compressors or limiters with fast attack/release that distort under extreme conditions

And you usually don’t need it for:

  • EQs

  • Delays

  • Reverbs

  • Filters that stay linear

Basically, if your plugin changes the shape of the waveform in a nonlinear way, think oversampling.

JUCE's oversampling class

JUCE makes oversampling now easier with the dsp::Oversampling class, from JUCE's dsp module, which handles both the upsampling and downsampling filters internally

Here's the constructor of the Oversampling class:

dsp::Oversampling(
    size_t numChannels,
    size_t factor,
    dsp::Oversampling<float>::FilterType type,
    bool isMaxQuality = true,
    bool shouldUseIntegerLatency = false
);

Parameters breakdown:

  • numChannels: Number of channels (mono = 1, stereo = 2)

  • factor: how much to oversample (2x, 4x, 8x, etc.)

  • FilterType: Either filterHalfBandFIREquiripple or filterHalfBandPolyphaseIIR

  • isMaxQuality: Whether to use higher-quality filters (slightly more CPU)

  • shouldUseIntegerLatency: Whether to round latency to an integer number of samples (useful for plugin delay compensation)

Step by step implementation

Let's assume we have a distortion plugin to which we want to implement oversampling.

First we need to declare an oversampling object in our PluginProcessor.h

PluginProcessor.h

juce::dsp::Oversampling<float> oversampling {
    2, // number of channels
    4, // oversampling factor (4x), we can make it a variable later so the user be able to choose the factor from UI
    juce::dsp::Oversampling<float>::filterHalfBandFIREquiripple, // when we write :: we will see the proposed filters
    true, // max quality
    false // integer latency off
};

Then natually we will need to prepare it in PrepareToPlay function in PluginProcessor.cpp :

PluginProcessor.cpp/PrepareToPlay

     oversampling.reset();
    oversampling.initProcessing(static_cast<juce::uint32>(samplesPerBlock));
  • note: we do static cast to unsigned: (size_t)/(juce::uint32) for samplesPerBlock just to explicitly say it's positive and never negative

Audio in digital systems it's not literally processed sample by sample, but via a pack of samples by pack of samples. This pack is called the buffer size. When we process audio via the dsp module we wrap each buffer in blocks.

    juce::dsp::AudioBlock<float> block (buffer);

Once we have wrapped our buffers in blocks, we can start our oversampling process.

First we do upsampling and save the upsampled block into new variable.

PluginProcessor.cpp/processBlock

// Upsample
    auto oversampledBlock = oversampling.processSamplesUp(block);

Then let's assume we have a simple distortion, from the juce::dsp::WaveShaper<Type, Function> template.

juce::dsp::WaveShaper<float, std::function<float(float)>> distortion;

This struct template have few functions that will be useful now:

  • prepare (const ProcessSpec &) - to use in prepareToPlay
PluginProcessor.cpp/prepareToPlay

  distortion.prepare(spec);
    distortion.functionToUse = [this](float x)
    {
        return saturationFunction(x);
    };

  • process (const ProcessContext & context) -to process the oversampledBlock supplied in the processing context.
PluginProcessor.cpp/processBlock

  juce::dsp::ProcessContextReplacing<float> distortionContext(oversampledBlock);
    distortion.process(distortionContext);

  • note: is a good idea to process antialias filter in the oversampled block, because removing the aliases at higher sample rate, avoid bad digital artifacts.

After we can do our downsampling and return to our original sample rate.

PluginProcessor.cpp/processBlock

 // Downsample
    oversampling.processSamplesDown(block);
	```

# Summary

Oversampling is essential when fighting aliasing in nonlinear plugins.
With dsp::Oversampling, JUCE takes care of the main work like filtering and up/down sampling.

Rule of thumb:

- Use 2x or 4x oversampling for subtle nonlinear effects

- Go 8x or higher for heavy distortion

- Always A/B test CPU usage vs quality