Seeking help with free-ratio downsampler plugin DSP


#1

Hello all,

Part of a plug-in I’m working on involves having a downsampler effect- an effect that introduces the aliasing/loss of data of a lower sample rate.

For my purpose, will ResamplingAudioSource be usable?

My main concerns are:
A. I’m uncertain if the decimation in ResamplingAudioSource includes an anti-aliasing filter- I’d like to preserve aliasing for the effect- and,
B. I’m uncertain as to how to make a buffer into an Audio Source, and
C. I’m uncertain if ResamplingAudioSource will perform properly when going block-by-block (for ‘real-time’ resampling)

Thanks for any input!


#2

If the whole point of this effect is to generate these aliasing effects, then surely you need to be writing the resampling code yourself in whatever way is necessary to do what you intend (?)


#3

Yep. I was curious how ResamplingAudioSource behaved and if it would suit my needs, but a while after posting this thread I looked through how it’s implemented and determined that for my deliberately ‘bad’ resampling it’s not suitable. I’d only been curious about using it so that I wouldn’t reinvent the wheel.


#4

What you really want I think is downsampling then upsampling without removing the resampling artefacts / aliasing.

  • To downsample with an integer ratio N, you can return one sample every (N-1) samples. This way you get your downsampled “aliased” signal, because for it to be less aliased, you have to filter it before the operation with a cutoff frequency at your sampling rate / (N-1) / 2 and a very stiff curve. Since you have not done it, it’s aliased.
  • Then, you don’t want your signal to be at a lower sample rate, but at the current one. So, you need to upsample it after that operation. To do so, you can add N-1 zeroes between your samples at the lower sample rate. You obtain your signal upsampled, back at the original sample rate, with “imaging artefacts”.
  • There, you need to filter the signal to remove some of the upsampling artefacts. But you can also return directly these samples with zeroes. Or, if you want something a little cleaner, but still harsh, you can do linear interpolation, or “drop sample” interpolation for the upsampling part. Linear interpolation is just going linearly from sample k to sample k+1 in the intermediate values, and “drop sample” is repeating N-1 times the sample k.

Example : you have a buffer of 9 samples to process, say [1 -1 2 -2 3 -3 4 -4 5].

  • You downsample it with an integer ratio of 4. You get [1 3 5]
  • You upsample it back to the original sample rate (4 times). You get [1 0 0 0 3 0 0 0 5]. You can return that depending on your tastes.
  • Or you can choose additional “drop sample” interpolation. The final result is [1 1 1 1 3 3 3 3 5]
  • Or you can choose additional linear Interpolation. The final result is [1 1.5 2 2.5 3 3.5 4 4.5 5]

#5

I think my logic here is correct (though certainly not the most efficient), but I’m stuck on the DSP for interpolation.

void Downsampler::processBlock(AudioSampleBuffer& buffer, int startSample, int numSamples)
{
    resample(buffer, inputSampleRate, targetSampleRate); //introduce distortion
    resample(buffer, targetSampleRate, inputSampleRate); //go back to host Fs
}
void Downsampler::resample(AudioSampleBuffer& sourceBuffer, double fromSampleRate, double toSampleRate)
{
    if(fromSampleRate == toSampleRate)
    {
        return;
    }
    
    int L, M; //upsample factor, downsample factor
    
    int gcd = calcGCD(toSampleRate, fromSampleRate);
    int scalar1 = toSampleRate / gcd;
    int scalar2 = fromSampleRate / gcd;
    L = std::max(scalar1, scalar2);
    M = std::min(scalar1, scalar2);
    
    upsampleBuffer.setSize(sourceBuffer.getNumChannels(), sourceBuffer.getNumSamples() * L);
    upsampleBuffer.clear();

    //place every sourceBuffer sample into upsampleBuffer, displaced every L
    for(int i=0; i < sourceBuffer.getNumSamples(); i++)
    {
        for(int channel=0; channel < sourceBuffer.getNumChannels(); channel++)
        {
            upsampleBuffer.setSample(channel, i*(L-1), sourceBuffer.getSample(channel, i));
        }
    }
    
    //interpolate (connect the dots)
    interpolate(upsampleBuffer);

    //grab every Mth sample from upsampleBuffer (downsample)
    for(int i=0; i < sourceBuffer.getNumSamples(); i++)
    {
        for(int channel=0; channel < sourceBuffer.getNumChannels(); channel++)
        {
            sourceBuffer.setSample(channel, i, upsampleBuffer.getSample(channel, i*(M-1)));
        }
    }
}

First of all, is the logic for grabbing upsample/downsample samples within the resample method correct?

Second of all, I’m clueless as to the proper way to implement the interpolate method. I know an IIRFilter isn’t the most efficient (as a FIR filter won’t depend on all the past values / zeroes that were padded), but I tried making an IIRFilter per channel and processing each channel of the upsampleBuffer with it.

For coefficients, I used IIRCoefficients::makeLowPass(L*fromSampleRate, fromSampleRate/2.0); as I’ve read that Nyquist’s of the original SR is used for the interpolation. This resulted in silence. Using instead IIRCoefficients::makeLowPass(fromSampleRate, fromSampleRate/2.0); results in the output being just buzzing… leading me to believe no interpolation occurred.

Any advice on what I might be doing wrong? Thanks!


#6

If I’ve understood you correctly you want to:

a. Quantize the audio signal’s amplitude to emulate bit reduction
b. Quantize the signal in time (ie. sample and hold) to emulate sample rate reduction

If the point of the effect is to introduce dirty dirty aliasing then life is easy. Here’s one example:

http://www.musicdsp.org/showArchiveComment.php?ArchiveID=139

… looks like Pascal or some such craziness but it’s only a few lines.


#7

Amplitude quantization (bitcrushing) isn’t being implemented at the moment, just emulated sample rate reduction.

I’ll give that algorithm a look, thanks.


#8
void Downsampler::processBlock(AudioSampleBuffer& buffer, int startSample, int numSamples)
{
    processBuffer.makeCopyOf(buffer);
    
    resample(processBuffer, inputSampleRate, targetSampleRate); //introduce distortion
    resample(processBuffer, targetSampleRate, inputSampleRate); //go back to host Fs
    
    buffer.clear();
    for(int channel = 0; channel < buffer.getNumChannels(); channel++)
    {
        buffer.copyFrom(channel, 0, processBuffer, channel, 0, processBuffer.getNumSamples());
    }
}
void Downsampler::resample(AudioSampleBuffer& buffer, double fromSampleRate, double toSampleRate)
{
    if(fromSampleRate == toSampleRate)
    {
        return;
    }
    
    int upsampleFactor, downsampleFactor; //upsample factor, downsample factor
    
    int gcd = calcGCD(toSampleRate, fromSampleRate);
    upsampleFactor = toSampleRate / gcd;
    downsampleFactor = fromSampleRate / gcd;
    
    upsample(buffer, upsampleFactor);
    interpolate(upsampledBuffer, fromSampleRate, fromSampleRate/2.0);
    downsample(buffer, downsampleFactor);
}

void Downsampler::upsample(AudioSampleBuffer& bufferToUpsampleFrom, int upsampleFactor)
{
    upsampledBuffer.setSize(bufferToUpsampleFrom.getNumChannels(), upsampleFactor*bufferToUpsampleFrom.getNumSamples());
    upsampledBuffer.clear();
    
    for(int srcBuffIndex=0; srcBuffIndex < bufferToUpsampleFrom.getNumSamples(); srcBuffIndex++)
    {
        int upsamBuffIndex = srcBuffIndex * upsampleFactor;
        for(int channels = 0; channels < upsampledBuffer.getNumChannels(); channels++)
        {
            upsampledBuffer.setSample(channels, upsamBuffIndex, bufferToUpsampleFrom.getSample(channels, srcBuffIndex));
        }
    }
}
void Downsampler::interpolate(AudioSampleBuffer& buffer, double sampleRate, double cutoffFreq)
{
    IIRCoefficients interpolationCoefficients = IIRCoefficients::makeLowPass(sampleRate, cutoffFreq);
    int numSamples = buffer.getNumSamples();
    
    leftFilter.setCoefficients(interpolationCoefficients);
    float* bufferPointer = buffer.getWritePointer(0);
    leftFilter.processSamples(bufferPointer, numSamples);
    
    rightFilter.setCoefficients(interpolationCoefficients);
    bufferPointer = buffer.getWritePointer(1);
    rightFilter.processSamples(bufferPointer, numSamples);
}
void Downsampler::downsample(AudioSampleBuffer& bufferToDownsampleTo, int downsampleFactor)
{
    int newBufferSize = upsampledBuffer.getNumSamples()/downsampleFactor;
    bufferToDownsampleTo.setSize(upsampledBuffer.getNumChannels(), newBufferSize);
    for(int i = 0; i < newBufferSize; i++)
    {
        int retrieveIndex = i*downsampleFactor;
        for(int channel = 0; channel < bufferToDownsampleTo.getNumChannels(); channel++)
        {
            bufferToDownsampleTo.setSample(channel, i, upsampledBuffer.getSample(channel, retrieveIndex));
        }
    }
}

int Downsampler::calcGCD(int x, int y)
{
    return y == 0 ? x : calcGCD(y, x % y);
}

Above is my updated code for the Downsampler effect. This code audibly seems to work for a targetSampleRate that’s an integer ratio of the inputSampleRate, but produces crackly garbage with non-ratio targetSampleRates.

Also, there’s another problem: the second time through, newBufferSize in downsample sometimes comes out a few samples below the size of the buffer originally passed to processBlock. It’s for this reason that I’m using copyFrom rather than makeCopyOf, but the extra zeros from not fully filling the block introduces unwanted frequencies to the signal.

Any ideas on what needs to be fixed? Also, any recommendations for some spaces specifically focused on audio/music DSP where I might find further help? Thanks!


#9

I see you posted on KVRAudio as well, which is probably your other bet (beyond dsp.stackexchange). How bad do you want it? You seem to want to emulate some effect, but not really elaborating on what.


#10

The aim is simply to emulate older digital gear’s lower sample rates. It’s an effect within a broader plug-in.