Glitches in ResamplingAudioSource with low resampling ratios


#1

I think we've stumbled upon an edge case where ResamplingAudioSource doesn't read enough samples from the input to do its interpolation.

It seems that with certain combinations of block sizes and a small resampling ratio, the resampling algorithm will read beyond the end of the input ring buffer. Here's some code to reproduce:


//==============================================================================
int main (int argc, char* argv[])
{
    double sampleRate = 48000.0;
    int blockSize = 128;
    double resamplingFactor = 0.33181;
    File outputFile ("~/Desktop/knister.wav");
    
    if (outputFile.existsAsFile()) {
        if (!outputFile.deleteFile())
        {
            std::cerr << "Could not delete existing output file " << outputFile.getFullPathName() << newLine;
            return -1;
        }
    }
    ResamplingAudioSource resampler (new ToneGeneratorAudioSource(), true, 1);
    resampler.setResamplingRatio (resamplingFactor);
    
    WavAudioFormat wav;
    ScopedPointer<AudioFormatWriter> writer (wav.createWriterFor (new FileOutputStream (outputFile), sampleRate, 1, 16, StringPairArray(), 0));
    resampler.prepareToPlay (blockSize, sampleRate);
        
    writer->writeFromAudioSource (resampler, 441000, blockSize);
    resampler.releaseResources();
    
    return 0;
}

In the output file you'll hear a lot of clicks, caused by random samples being occasionaly inserted. 

This will only happen if the end of your output block aligns with the end of the last read input block. In that case, when the last output sample(s) of a block are being interpolated, sometimes there is only one sample left in the input ring buffer, and nextPos will point to a random previous sample.

Here's an assertion for ResamplingAudioSource::getNextAudioBlock to catch that situation:

    int nextPos = (bufferPos + 1) % bufferSize;
    for (int m = info.numSamples; --m >= 0;)
    {
        jassert (sampsInBuffer > 1 && nextPos != endOfBufferPos); // don't read beyond the last input block

It only happens with uneven resampling ratios near 1/2, 1/3, 1/4 etc, and certain block sizes. This may seem like a very rare scenario, but we are using the resampling as an effect and therefore allow arbitrary ratios, and the glitches in the output are very obvious when the resampler is used with low ratios.

The only solution I've come up so far, is to further increase the number of samples to read from the input:

const int sampsNeeded = roundToInt (info.numSamples * localRatio) + 3;

Any other idea how to address this? I'm not entirely sure if imply reading one more sample (+3 instead of +2) will work for all combinations of resampling ratio & block size, but it fixed this issue in all scenarios I've tested it with.


#2

Thanks!

Just increasing the samples needed is probably quite sensible, there may be a more elegant fix but I can't immediately spot one, so I'll bump up that number and add an assertion too.