dsp::Convolution changing level depending on the host sample rate

Hi There,

I’m having trouble using the juce::dsp::Convolution class, hopefully, someone here could help me.

The project is a plugin, and I encountered a problem when the host sample rate is changed. I created a test project to see if I still had the problem and yes.

The test project only has a convolver, I load a neutral (it doesn’t do anything) impulse response and process the buffer with it.

The impulse response is 44.1 kHz, and when I use the plugin using that sample rate, everything works perfectly. I have no gain either attenuation.

The problem starts when I change the sample rate from the host, then I have more gain as the sample rate increases: 44.1 kHz doesn’t add any gain, 48 kHz adds 0.7 dBs, 88.2 kHz adds 4 dBs, 96k kHz adds 4.7 dBs, 176.4 kHz adds 8.9 dBs, and 192 kHz adds 9.6 dBs.

As long as I understand the problem is in the resampling function that loadImpulseResponse uses. It calls resampleImpulseResponse that uses a ResamplingAudioSource to resample the impulse response.

The discrete-time signal theory says that the resampling process does change the amplitude of our signal, but the factor can be calculated and compensated to have the same signal. Maybe that step is missing in the JUCE implementation? I wasn’t able to see that. But probably I’m missing something or doing something wrong.

The test project I did is:

in the pluginProcessor.h

private:
juce::dsp::Convolution convolver;

in the pluginProcessor.cpp

void test::prepareToPlay (double sampleRate, int samplesPerBlock){
DBG(“---------- prepare to play --------------\n”);

juce::dsp::ProcessSpec spec;
spec.sampleRate = sampleRate;
spec.maximumBlockSize = (juce::uint32)samplesPerBlock;
spec.numChannels = (juce::uint32) juce::jmax (getTotalNumInputChannels(), getTotalNumOutputChannels());

convolver.reset();
convolver.prepare(spec);

convolver.loadImpulseResponse(BinaryData::neutral_wav,
                              BinaryData::neutral_wavSize,
                              juce::dsp::Convolution::Stereo::no,
                              juce::dsp::Convolution::Trim::no,0,
                              juce::dsp::Convolution::Normalise::no);

}

void test::processBlock (juce::AudioBuffer& buffer, juce::MidiBuffer& midiMessages)
{
juce::ScopedNoDenormals noDenormals;
auto totalNumInputChannels = getTotalNumInputChannels();
auto totalNumOutputChannels = getTotalNumOutputChannels();

for (auto i = totalNumInputChannels; i < totalNumOutputChannels; ++i)
    buffer.clear (i, 0, buffer.getNumSamples());

const auto numChannels = juce::jmax (totalNumInputChannels, totalNumOutputChannels);
auto inoutBlock = juce::dsp::AudioBlock<float> (buffer).getSubsetChannelBlock (0, (size_t) numChannels);

convolver.process(juce::dsp::ProcessContextReplacing<float> (inoutBlock));

}

Thanks for your time! And sorry in advance If my English is not the clearest one :slight_smile:

2 Likes

Hi Joaquin,

I have not used JUCE’s convolution, but there is a signature that includes sample rate of the file being loaded. Perhaps using this will help (it may just do the conversion for you)?

void loadImpulseResponse (AudioBuffer< float > &&buffer, double bufferSampleRate, Stereo isStereo, TrimrequiresTrimming, Normalise requiresNormalisation)

Hi! Thanks for taking the time. Already tried that, but the problem is still there.

I’ll give it another try tomorrow just in case but I believe the signature I’m using has the sample rate implicit.

I have the same issue. Can the bug be confirmed? Any chance to get a fix or a workaround shortly?
@IvanC perhaps? :slight_smile:

There’s a fix for this on develop now:

Please let us know if you run into any issues with this change.

4 Likes

There is also a gain variation when using Normalise::yes. About +2dB when switching from 48k to 96k.
That can be reproduced with the DSPModulePluginDemo. Anything can be done regarding that?

Thanks, @reuk for the fix added.

I’ve modified the gain similarly and there are still differences in gain for each sample rate.

Tried your implementation and it is an undeniable improvement to the convolution engine since we had 12 dB of gain when working at 192 kHz with 44.1 kHz IRs, but I still hear differences in gain.

I know that the theory says that in the ideal case, the compensation factor should be originalSampleRate / newSampleRate, but I’m afraid the resampling algorithm isnt ideal and doesn’t compensate as it should.

I’m using it and now it’s overcompensaating. With a 44.1 kHz IR, if I use it in 44.1 kHz the level is correct, but if I use it with higher sample rates, the out level gets quieter.

I would love to contribute and help future people who will encounter this kind of issues using the convolution class, but I’m not sure what is the problem.

Just in case is part of the problem, trying with some IRs I encountered more serious problems. The IR is basically a high shelf, and it works fine in 44.1 kHz but, also in 48 kHz, but with higher sample rates, the frequency response is totally changed, I believe (not 100% sure) because there is aliasing in the resampling process or somewhere else.

Maybe I’m doing or interpreting something wrong, if it is the case please let me know.

After discussing with @IvanC, we think it’s likely the remaining volume discrepencies are due to the resampling algorithm which is being used. Fixing these issues would require replacing the resampling algorithm. This would be a significant piece of work, which is not currently scheduled.

I have the same issue. My target audience are mostly “audiophiles” using FIR filters for digital room correction. While I have a way to adjust for gain loss, unfortunately, the frequency response also changes as @joaquinsaavedra mentions. This is a no go for digital room correction filters.

If replacing the resampling algo is work currently not scheduled, then what alternatives can be recommended? Thanks.

Are you generating the FIR filters programmatically? If so, I’d recommend taking the host sampling rate into account when generating the FIR kernel, and recomputing the kernel every time the host sample rate changes. This way, there will be no need to rely on the Convolution’s internal resampling.

Thanks @reuk Yes, the FIR filters are generated programmatically from 44.1 to 384 kHz. The convolution plugin automatically switches the FIR filter to match the host sample rate. All good. However, with the convolution plugin in standalone mode the FIR filters don’t switch based on source sample rate. Not sure how to handle those cases…

In standalone mode, the AudioProcessor should still receive a prepareToPlay call each time the sample rate changes. Are you saying that this is not the case?

Totally understandable. I would like to thanks @reuk and @ivanC for taking the time to discuss and trying to solve it.

Just in case it helps someone, I share how I handle with it.

As a rapid solution, I got it working using the dsp::Oversampling class. As it only resamples to x2 or x4 I had to also use the ResamplingAudioSource.

My IRs are at 44.1 kHz. If the DAW sample rate is 88.2 kHz or 176.4 kHz I just oversample the IR correspondingly. And if it’s 48 kHz, 96 kHz, or 192 kHz firstly I resample it to 48 kHz using ResamplingAudioSource and then oversample it using dsp::Oversampling.

I know it isn’t efficient and also introduces latency, but it was more important to maintain the frequency response unchanged. This way there are no aliasing nor frequency response changes.

Also for the little output level differences, I manually compensated depending on the DAW sample rate. I do it using the sample rate variable in prepareToPlay, and apply the gain in processBlock @Mitchco

Please if it is possible schedule the big work for future improvements of the framework, it is really useful to have a convolution engine that does its work on all the sample rates.

4 Likes

@reuk Sorry, I should have mentioned that in plugin standalone mode, I am using loopback via VB-Cable and Blackhole to route the output of the music player into the convolution and then out to the DAC. The FIR filter does switch based on the sample rate set in the standalone plugin audio settings. But if the source sample rate in the music player changes, that is not reflected in the strandalone plugin. I understand this is by design. My misunderstanding. Thanks again for looking at this.

Convolution calculations do not need to care about what they are used for.

Any filter resampling needs to be handled on a higher level, where both input/out streams and the filter contents have a meaning.

A convolution engine must not be bothered by this, its purpose is to implement the y = h * x function, in which the sample rate is not a factor.

1 Like

I think it’s fair to have a re-sample function built into a convolution class, with an option as to whether the data gets scaled to compensate for any resampling that it may do.

A matter of tech design preferences, I guess.

I like to distribute responsabilities. What should such a class do, in case a user switches from 44.1 / 48 to 192 K? Convolution at such high rates is simply not going to work.

I prefer to make sure that my convolvers get work they can handle and prepare that work anyhow outside of my convolution classes.

I do not use the Juce convolver, so pardon me for jumping in with my design comments :wink:

FWIW: resampling of impulse responses is different from resampling of audio. May sound counter intuitive, but as you’ve found out, if you use audio resampling techniques on impulse responses, you can expect the level of the audio convolved with the resampled impulse response to change by approximately the ratio of the resample factor.

1 Like