Dsp::oversampling gain issue

Hi,

Doing upsampling/downsampling (without anything in the middle) changes the gain of the signal.
Is that expected?

The gain change is highly dependant on the signal you pass through.
The worst case seems to be with white noise, giving peaks at +7db when using filterHalfBandPolyphaseIIR, and about +6dB with filterHalfBandFIREquiripple.

as a test I just run white noise through upsampling/downsampling (full PIP below).

    for (int i = 0; i < buffer.getNumChannels(); ++i)
    {
        for (int j = 0; j < buffer.getNumSamples(); ++j)
        {
            float noise = Random::getSystemRandom().nextFloat() * 2.f - 1.f;
            buffer.setSample (i, j, noise);
        }
    }

    dsp::AudioBlock<float> block (buffer);
    oversampling.processSamplesUp (block);
    oversampling.processSamplesDown (block);
> Full PIP plugin
/*******************************************************************************
 The block below describes the properties of this PIP. A PIP is a short snippet
 of code that can be read by the Projucer and used to generate a JUCE project.

 BEGIN_JUCE_PIP_METADATA

  name:             OversamplingTest

  dependencies:     juce_audio_basics, juce_audio_devices, juce_audio_formats, juce_audio_plugin_client, juce_audio_processors, juce_audio_utils, juce_core,
                    juce_data_structures, juce_dsp, juce_events, juce_graphics, juce_gui_basics, 
                    juce_gui_extra
  exporters:        XCODE_MAC

  moduleFlags:      JUCE_STRICT_REFCOUNTEDPOINTER=1

  type:             AudioProcessor
  mainClass:        MyPlugin

 END_JUCE_PIP_METADATA

*******************************************************************************/

#pragma once


//==============================================================================
class MyPlugin  : public juce::AudioProcessor
{
public:
    //==============================================================================
    MyPlugin()
    : AudioProcessor (BusesProperties().withInput  ("Input",  juce::AudioChannelSet::stereo())
                                       .withOutput ("Output", juce::AudioChannelSet::stereo()))
    {
    }

    void prepareToPlay (double sampleRate, int samplesPerBlock) override
    {
        oversampling.initProcessing ((size_t) samplesPerBlock);
        oversampling.reset();
    }

    void processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer&) override
    {
        juce::ScopedNoDenormals noDenormals;

        // override the input buffer with noise
        for (int i = 0; i < buffer.getNumChannels(); ++i)
        {
            for (int j = 0; j < buffer.getNumSamples(); ++j)
            {
                float noise = Random::getSystemRandom().nextFloat() * 2.f - 1.f;
                buffer.setSample (i, j, noise);
            }
        }

        // oversampling
        dsp::AudioBlock<float> block (buffer);
        oversampling.processSamplesUp (block);
        oversampling.processSamplesDown (block);
    }

    void releaseResources() override {}

    //==============================================================================
    juce::AudioProcessorEditor* createEditor() override          { return nullptr; }
    bool hasEditor() const override                              { return false;   }

    const juce::String getName() const override                  { return "OversamplingTest"; }
    bool acceptsMidi() const override                            { return false; }
    bool producesMidi() const override                           { return false; }
    double getTailLengthSeconds() const override                 { return 0; }

    int getNumPrograms() override                                { return 1; }
    int getCurrentProgram() override                             { return 0; }
    void setCurrentProgram (int) override                        {}
    const juce::String getProgramName (int) override             { return {}; }
    void changeProgramName (int, const juce::String&) override   {}
    void getStateInformation (juce::MemoryBlock& destData) override {}
    void setStateInformation (const void* data, int sizeInBytes) override {}

    //==============================================================================
    bool isBusesLayoutSupported (const BusesLayout& layouts) const override
    {
        if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::mono()
            && layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo())
            return false;

        if (layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet())
            return false;

        return true;
    }

private:
    //==============================================================================
    const size_t OversamplingFactor = 2;
    dsp::Oversampling<float> oversampling { 2, OversamplingFactor, dsp::Oversampling<float>::filterHalfBandPolyphaseIIR, true, true };

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MyPlugin)
};

I hit this same observation early on, too. It’s a bit weird but totally expected. The short version is this;

When you upsample, you produce new sample which get closer to ‘true peak’. This will be more evident at higher frequencies. Think about what happens near nyquist, you might only have one sample at each sign of the waveform. The chances of that sample being at the peak? Slim. So as you upsample and generate more ‘in between’ samples, those samples are going to be closer to the peak in the analog equivalent waveform, so louder.

When you downsample, depending on the filter types used, you will either get phase shift, ringing, or both. The result is the peak energy from one of the new sample places while oversampled ‘bleeds’ into the sample position that you end up with at the end of the process.

A listening test should reveal that there isn’t actually any perceived volume change, as it is the same waveform, just with a slightly different energy distribution. It also makes you think about how inaccurate a basic digital compressor could be given they are operating on samples for level, which are kind of arbitrary.

Hope this helps!

3 Likes