Issue with WindowedSincInterpolator?

Just experimenting with JUCE’s interpolators.

My idea was to create a class that takes an AudioBuffer and up-samples or down-samples it to another AudioBuffer. Input sample rate and output sample rate are specified.

As a very first step sanity check I’m generating a sine wave at 440Hz, with an overall sample rate of 96000Hz. I convert it to the host DAW sample rate, in this case 44100Hz. I’m using Span spectrum analyser with the floor set at -180dB so I can see everything that’s going on.

This is what I’m seeing:

If the input and output sample rates are the same, I see a perfect frequency spectrum. It also works for other combos. But not for 96000 → 44100. I suspect it maybe be due to fractional buffer size calculation.

Here’s my quick knock-up class for reference:

class AudioResampler
{
public:
    AudioResampler(double inputSampleRate, double outputSampleRate)
        : inputRate(inputSampleRate), outputRate(outputSampleRate), ratio(inputSampleRate / outputSampleRate)
    {
        // Initialize one interpolator per channel
        for (int i = 0; i < 2; ++i)
            interpolators.add(new juce::WindowedSincInterpolator());
    }

    int getNumInputSamplesNeeded(const int numOutputSamples) const
    {
        const int baseNeeded = static_cast<int>(std::ceil(numOutputSamples * ratio));
        return baseNeeded;
    }

    // Get the fixed latency
    int getLatencyInSamples() const
    {
        return 16; // Fixed latency of WindowedSincInterpolator
    }

    // Process input buffer to output buffer
    void process(AudioBuffer<float> &inputBuffer,
                 AudioBuffer<float> &outputBuffer)
    {
        const int numChannels = jmin(inputBuffer.getNumChannels(),
                                     outputBuffer.getNumChannels());

        auto numOutputSamples = outputBuffer.getNumSamples();
        
        // Process each channel
        for (int channel = 0; channel < numChannels; ++channel)
        {
            float *outData = outputBuffer.getWritePointer(channel);
            const float *inData = inputBuffer.getReadPointer(channel);

            // Let the interpolator handle the resampling
            interpolators[channel]->process(ratio,
                                            inData,
                                            outData,
                                            numOutputSamples);
        }
    }

    // Reset the interpolators' internal state
    void reset()
    {
        for (auto *interp : interpolators)
            interp->reset();
    }

    // Get current sample rates
    double getInputSampleRate() const { return inputRate; }
    double getOutputSampleRate() const { return outputRate; }
    double getRatio() const { return ratio; }

private:
    const double inputRate;
    const double outputRate;
    const double ratio;
    OwnedArray<WindowedSincInterpolator> interpolators;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AudioResampler)
};

I use getNumInputSamplesNeeded() to figure out how many samples to generate in the input buffer - in this case for the 440Hz sine wave.

The documentation for WindowedSincInterpolator says the number of input samples “…must contain at least (speedRatio * numOutputSamplesToProduce) samples.”

I’m using std::ceil in getNumInputSamplesNeeded(), but I’ve tried floor and round too - no luck.

Aliasing isn’t a factor - it’s a 440Hz sine wave.

Anyone here with experience of using this class that can see what I’m missing?

if numOutputSamples is less than the outputBuffer samples I think your gonna get clicking

numOutSamples = 64 for instances
outputBufferSize = 128
you have a bunch of 0’s

you have to process until the end of the buffer

if you have some samples left over you need to add a delay like so

look at stenzels comment on how to implement a little delay

Thanks for the response. Having said that, I’ve got this:

So, in theory, the interpolator has the right buffer size that it needs to fill. I set input buffer size to be a ratio of output buffer size using the getNumInputSamplesNeeded method. That’s calculated using the input sample rate and the output sample rate.

Otherwise, I think you’re right. There is a discrepancy resulting in a discontinuity/click which is the disturbance I’m seeing in the spectrum analyser.

You don’t seem to be taking into account that the number of input samples consumed (returned by the process method) by the interpolator may not be exactly what you expected. This can make it somewhat difficult to orchestrate using the interpolator in a realtime stream context. If you can have all the data available beforehand (like a file you are playing), then it’s not as problematic since you could simply move the data read position based on the number of samples actually consumed by the interpolator.

oh got you also you also maybe be running out of input samples because you are compressing time from 96khz to 44.1khz. if this is a live effect.

so if you want to downsamples by 2 you get 256 input samples but only produce 128 you need 256 more samples to downsample to matchup the output back to 256 if that makes sense

1 Like

Here’s some code that demonstrates the varying number of input samples consumed by the interpolator :

juce::WindowedSincInterpolator interp;
    double insamplerate = 96000.0;
    double outsamplerate = 44100.0;
    double ratio = insamplerate / outsamplerate;
    int bufsize = 512;
    int numinputsamplesexpected = ratio * bufsize;
    std::cout << std::format("expecting num input samples to used to be {}\n", numinputsamplesexpected);
    std::vector<float> insamples;
    insamples.resize(1024 * 1024);
    std::vector<float> outsamples;
    outsamples.resize(1024 * 1024);
    for (int i = 0; i < 16; ++i)
    {
        auto consumed = interp.process(ratio, insamples.data(), outsamples.data(), bufsize);
        std::cout << std::format("processing round {} consumed {} samples\n", i, consumed);
    }

Which produces as output :

1 Like

Yeah, that would account for it. That’s the discrepancy. I suspected it’d be something to do with fractional sampling given particular input and output sample rates.

Not sure if there’s a workaround for that. Expecting 1114 samples on the output but sometimes getting more (or less!). Maybe a delay line? Although in this case as I’d always be taking only 1114 samples, the extra sample of 1115 would eventually build up and overflow the delay line. That would work for a fixed length of sample data, but not continuous streaming. Blergh.

TBF, I’m slightly off track with this. I’m researching the idea of running my code at a fixed internal sample rate and resampling to the host sample rate - whatever that is - in AudioProcessor processBlock.

Thought this might have been part of the solution, even though it’s the interpolation part not the band-limiting. But guess not.