Advice needed on when and where to dynamically change the oversampling factor, is this code OK..?

This is my first time implementing oversampling. I am dynamically changing the oversampling factor at the beginning of the processBlock().
I’m just looking for confirmation that I am doing things right.

PluginProcessor.h

private:
    std::unique_ptr<juce::dsp::Oversampling<float>> oversamplingmodule;

preparetoplay()

    if (oversamplingmodule) {  
        oversamplingmodule->initProcessing(samplesPerBlock); }

processBlock()

    juce::dsp::AudioBlock<float> block(buffer);
//if user has changed oversampling factor then
    oversamplingmodule.reset(new juce::dsp::Oversampling<float>(2, newOversamplingFactorValue, juce::dsp::Oversampling<float>::filterHalfBandPolyphaseIIR));
    oversamplingmodule->processSamplesUp(block);
//rest of processing
    oversamplingmodule->processSamplesDown(block);

That’s an allocation in process block which is a no-no. The way I do it currently is call suspendProcessing(), then create the object, then resume processing. This also gives you an opportunity to set the latency to the new value.

@Fandusss So is this something close to what you do in the processBlock?

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

    suspendProcessing(true);

    // Re-initialize the oversampling module
    oversamplingmodule.reset(new juce::dsp::Oversampling<float>(2, newOversamplingValue, juce::dsp::Oversampling<float>::filterHalfBandPolyphaseIIR));

    // Update latency
    auto newLatency = oversamplingmodule->getLatencyInSamples();
    setLatencySamples(newLatency);

    suspendProcessing(false);

i personally prefer to let processor inherit from timer and check for oversampling parameter changes in timerCallback. if it changed this method, which is on the message thread, suspends the processing on the audio thread, manually calls prepareToPlay and then resumes processing. in prepareToPlay you simply give the oversampler its intended factor. in processBlock you just use the oversampler. this disrupts the audio stream but it’s safe

2 Likes

It is correct (at least to me) if you put this block of code within a listener callback (or something similar) instead of processBlock() (of course remove the first line).

This is what I do too. As you say, you get a potential dropout, but I think it’s acceptable for something like oversampling, which changes infrequently and is not automated.

If you wanted to make it seamless while maintaining correct delay compensation, things get a bit complicated.

@Mrugalla I’ve gone for the timercallback() approach.
Here’s my code below. I’ve got a couple of questions:

  • Do I need to suspendProcessing() in the timercallback() or not?
  • I have my timer set to 200 milliseconds, I assume this is an OK period of time.
  • I’ve not called preparetoplay(), because I’ve got a lot of other DSP initialization going on there, but in preparetoplay() I’ve called the initializeOversampling() method, and in timercallback() too. I trust this is fine.
void myplugin::timerCallback()
{
    if (hasOversamplingChanged() == true)
    {
        suspendProcessing(true); 

        oversamplingmodule.reset(new juce::dsp::Oversampling<float>(2, newOversamplingFactor, juce::dsp::Oversampling<float>::filterHalfBandPolyphaseIIR));

        initializeOversampling(getBlockSize());

        //set hasOversamplingChanged flag to false

        suspendProcessing(false); 
    }
}
void myplugin::initializeOversampling(int samplesPerBlock)
{
    if (oversamplingmodule) {  
        oversamplingmodule->initProcessing(samplesPerBlock);
    }
}
void myplugin::prepareToPlay (double sampleRate, int samplesPerBlock)
{

    //other dsp init code

    initializeOversampling(samplesPerBlock);

}

yeah. seems pretty much the same. just make sure that all dsp objects that are in the oversampled part of processBlock also get prepared for the changed samplerate and blocksize and it should be alright

1 Like

ok, so you suspendProcessing too… . Thanks for the tip about other DSP needing to know about the changes.

I’m trying to confirm the range of values that oversamplingmodule.reset() takes.

Are they 0=off, 1=2x, 2=4x, 3=8x and 4=16x for newOversamplingFactor ??

oversamplingmodule.reset(new juce::dsp::Oversampling<float>(2, newOversamplingFactor, juce::dsp::Oversampling<float>::filterHalfBandPolyphaseIIR));

I looked here, but it wasn’t defined as far as I can see:
https://docs.juce.com/master/classdsp_1_1Oversampling.html

edit: and I find when I engage oversampling, I find my filters and processing, the audio seems muffled and slightly lower volume even when I comment out the filters. I think I am updating the spec in preparetoplay() when oversampling is engaged:

    juce::dsp::ProcessSpec spec;
    spec.numChannels = getNumOutputChannels();  
    spec.maximumBlockSize = samplesPerBlock * realOversamplingRate;    // 1,2,4,8 or 16
    spec.sampleRate = sampleRate * realOversamplingRate; 

// my filter prep here...

I looked here, but it wasn’t defined as far as I can see

IIRC, reset() on a unique_ptr is similar to re-assign by make_unique. Therefore the factor is defined in the constructor:

factor: the processing will perform 2 ^ factor times oversampling


the audio seems muffled and slightly lower volume

That sounds like a filter effect. Could you please confirm that you process the oversampled audio block instead of the original block? The code should be something like this:

auto oversampled_block = overSamplers[idxSampler]->processSamplesUp(context.getInputBlock());
// do something on the oversampled_block
overSamplers[idxSampler]->processSamplesDown(context.getOutputBlock());

I think my processBlock() logic is OK and I am processing on a sample-by-sample basis the upsampled audio block.



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

        if (//oversampling is on)             
        {
            oversamplingmodule->processSamplesUp(block);     
            numSamplesToProcess = block.getNumSamples();     
        }

        for (int channel = 0; channel < totalNumInputChannels; ++channel)
        {
            auto* channelData = block.getChannelPointer(channel);      

            for (auto sample = 0; sample < numSamplesToProcess; sample++)
            {

                 channelData[sample] = my_highpass_filter.processSample(channel, channelData[sample]);

		//more processing
            }
        }
        if (//oversampling is on) {                               
            oversamplingmodule->processSamplesDown(block);
        }

preparetoplay()

    juce::dsp::ProcessSpec spec;
    spec.numChannels = getNumOutputChannels();
    spec.maximumBlockSize = samplesPerBlock * realOversamplingRate;    // 1,2,4,8 or 16
    spec.sampleRate = sampleRate * realOversamplingRate;

    my_highpass_filter.prepare(spec);
    my_highpass_filter.reset();  
    my_highpass_filter.setType(juce::dsp::StateVariableTPTFilterType::highpass);
    my_highpass_filter.setResonance(1.0);     //default values, changed in processBlock()
    my_highpass_filter.setCutoffFrequency(20.0);
oversamplingmodule->processSamplesUp(block);

You didn’t store the returned reference to the up-sampled block. You are processing the original block.

I inserted block = oversamplingmodule->processSamplesUp(block);
and it crashed on me. Am I getting the types right, … what am I missing ?

I think I’m getting closer to the cause of the crash.
Its when I come to downsample at the end of my processblock(). I checked numsamples after I upsampled, and its reporting the correct amount. e.g. 256 samples becomes 512 when I am on 2x etc.

it asserts here: juce_oversampling.cpp, line 374

jassert (outputBlock.getNumSamples() * ParentType::factor <= static_cast<size_t> (ParentType::buffer.getNumSamples()));

in context

void processSamplesDown (AudioBlock<SampleType>& outputBlock) override
    {
        jassert (outputBlock.getNumChannels() <= static_cast<size_t> (ParentType::buffer.getNumChannels()));
        jassert (outputBlock.getNumSamples() * ParentType::factor <= static_cast<size_t> (ParentType::buffer.getNumSamples()));

        // Initialization

processblock()

        if (//oversampling is on) {   
            oversamplingmodule->processSamplesDown(block);
        }

processSamplesDown (AudioBlock< SampleType > &outputBlock) noexcept

Could you please confirm that you pass the original block (or the outputblock of the original context) to the function above? It seems that you pass the up-sampled block instead (after inserting the code you provided).

This is the block I am passing to downsample. Its pointing to the upsampled buffer, not the original buffer.
processblock()

juce::dsp::AudioBlock<float> block(buffer);   //block assigned to original non-upsampled buffer at start of processblock()
...
        if (//oversampling is on)   
        {
            block = oversamplingmodule->processSamplesUp(block);     //enlarge block of samples to process by the oversampling factor, and assign `block` to point to that upsampled block
        }
        int numSamplesToProcess = block.getNumSamples();
...
        for (int channel = 0; channel < totalNumInputChannels; ++channel)
        {
            auto* channelData = block.getChannelPointer(channel);     

            for (auto sample = 0; sample < numSamplesToProcess; sample++)
            {
...          //processing
...
        if (//oversampling is on) {                                //downsample if we've upsampled
            oversamplingmodule->processSamplesDown(block);
        }
...

That’s the problem. You should pass the reference to the original buffer to processSamplesDown().

So is this now correct logic? (It doesn’t crash now).
I think I can make it more elegant than this but here’s how I altered it so far.

    juce::dsp::AudioBlock<float> originalBlock(buffer);     //assigned to original non-upsampled buffer
    juce::dsp::AudioBlock<float> block(buffer);   //block assigned to original non-upsampled buffer at start of processblock()
...
        if (//oversampling is on)   
        {
            block = oversamplingmodule->processSamplesUp(block);     //enlarge block of samples to process by the oversampling factor, and assign `block` to point to that upsampled block
        }
        int numSamplesToProcess = block.getNumSamples();
...
        for (int channel = 0; channel < totalNumInputChannels; ++channel)
        {
            auto* channelData = block.getChannelPointer(channel);     

            for (auto sample = 0; sample < numSamplesToProcess; sample++)
            {
...          //processing
...
        if (//oversampling is on) {                                //downsample if we've upsampled
            oversamplingmodule->processSamplesDown(originalBlock);     //block changed to originalblock
        }
...

It looks correct.

1 Like