[DSP module discussion] New Oversampling class

You’re right, I do get an assertion, turns out I was “debugging” a release build rather than a debug build. Still, if I just comment out that assertion, then everything works fine with the IIR, and fine up to 64x on the FIR, is there any particular reason I shouldn’t be doing this?

Also, it seems your oversampling class might not be suitable for ISP detection, or it least it seems to generate different peak magnitudes than the intersample peaks detected with K-Meter.

I am already using a custom version of your class, but I’m not sure how (if it’s even possible) to embed detection while also accounting for the new peaks introduced during the multiple stages of downsampling, I’m not sure it’s possible to detect them without upsampling again after the downsampling.

Well, the thing is I didn’t really tried to optimize further the way the filter coefficients are generated when the oversampling factor is high. And the most important reason I didn’t want people to use the oversampling that way is because for me, more than 16x oversampling is always overkill for solving any specific aliasing issue. Oversampling is not just about filtering like hell aliasing, it’s also something that multiplies by the oversampling factor the CPU load of the processing embedded inside !

So if you still have too much aliasing in any specific context even with 16 times, maybe what you really need to do is to code a custom class heriting from Oversample so you can customize the filter design functions in the constructor and make them more powerful.

About the ISP detection, your concern is more than relevant, and after some additonal thought I think I can say that embedding it inside processing oversampling might not be the best thing to do in general, since multi-stage oversampling is the thing widely used for oversampling 4 times and more for obvious reasons.

The ideal situation there would have been to have only one stage oversampling algorithms, and a detection made after the downsampling filtering before the downsampling itself, to take into account the implication of the filtering on the ISP.

But in general, I think most of audio developers have multi-stages oversampling for obvious reasons, so the only pratical and general solution is additional oversampling after to do the detection. That’s what happens for example if your meter is in a dedicated plug-in. Then, the question is how to perform that oversampling without getting something not relevant because of the filtering… I guess I could find some bibliography on this specific issue, right now unfortunately I don’t have the answer…

1 Like

I actually agree that >16x oversampling is mostly snake oil and a huge cpu hog, but some consumers (mastering engineers) insist on it so I feel like I should at least give it as an option. Still for what it’s worth, your oversampling class even at very high factors is more cpu efficient than other solutions I’ve tried.

What I’ve done for now is use a second instance of your class with 4x oversampling using FIR. At normal levels it seems to catch ISPs fine, and when pushed hard it only seems to get it “wrong” (relative to K-meter) by about 0.1db over or under.

1 Like

I have just read this article : https://techblog.izotope.com/2015/08/24/true-peak-detection/

Apparently, in the BS.1770 specification, the coefficients of a filter for 4 times upsampling are provided, but the resulting ISP detection isn’t that good. I really wonder how better detectors work, and I’ll probably have a look soon :wink:

So K-meter (this one ? http://www.meterplugs.com/kmeter ) is considered as a reference in this area ?

I’m not sure if K-meter is considered a reference, it just happens to be what I’ve been using for a long time.

Let us know if you find if you find a better solution! :slight_smile:

There resulting intersample peaks will depend on which algorithm for oversampling is used. That’s why there is a specification.

The question is whether the most important thing is to have an algorithm following a specification for true peak detection, or an algorithm efficient for detecting these peaks… That’s the question the izotope blog article is asking, and I really don’t know this area enough to have an opinion there !

I’m assuming that the JUCE resampling option here is the older ResamplingAudioSource class: http://src.infinitewave.ca/ ? How do you think the new Oversampling class with the high quality option would compare in that list? (perhaps you could get it updated on that site :wink: )

1 Like

Note that the comparison is for 96000Hz to 44100Hz while the Oversampling’s underlying down/up-sampling are for ratios 2, 4, 8, 16…

I have questions regarding quality and differences between FIR-Equiripple and Polyphase-IIR - ran this very simple test of oversampling an input of zeros with a very simple process that just adds a sine wave at the oversampled frequency’s nyquist, and looks at the signal level of the last block.
Basically an ideal downsampling should make this high-freq sine disappear.

Here’s the test:

dsp::Oversampling<double> oversampling (1, 4, dsp::Oversampling<double>::filterHalfBandFIREquiripple);
AudioBuffer<double> buffer (1, 256);
dsp::AudioBlock<double> block (buffer);
oversampling.initProcessing (buffer.getNumSamples());
for (int block_i = 0; block_i < 1000; ++block_i)
{
    block.clear();
    dsp::AudioBlock<double> highSampleRateBlock = oversampling.processSamplesUp (block);
    double* samples = highSampleRateBlock.getChannelPointer (0);
    for (int i = 0; i < highSampleRateBlock.getNumSamples(); ++i)
        samples[i] += (i % 2 == 0) ? 1.0 : -1.0;
    oversampling.processSamplesDown (block);
}
for (int i = 0; i < 10; ++i)
    cout << block.getSample (0, i) << " ";
cout << endl;
cout << Decibels::gainToDecibels (fabs (buffer.getRMSLevel (0, 0, buffer.getNumSamples())), -10000.0) << endl;

Using the filterHalfBandPolyphaseIIR option, the signal level / difference from the ideal (which is a zero signal) was under -300 dB at all ratios, which seems very reasonable.
But when using filterHalfBandFIREquiripple the results were far from as good: -50 dB for the x16 ratio, -64 dB for the x4 ratio.

Am I doing something wrong in my test? Is polyphase-iir expected to have much better qualitity than fir-equiripple?

Using the filterHalfBandPolyphaseIIR option, the signal level / difference from the ideal (which is a zero signal) was under -300 dB at all ratios, which seems very reasonable.

It seems that this is actually the symptom of a bug in Oversampling2TimesPolyphaseIIR. It was fixed on the develop branch (DSP: Fixed a bug when oversampling multiple channels · juce-framework/JUCE@1ff97d3 · GitHub), but not on master nor in the JUCE 5.2.0 release.

  • The Polyphase-IIR option was the one behaving fine, so that’s probably not it.
  • Just pulled latest develop and I get the exact same results.

Yair - that’s the absolute worst case scenario you’re testing isn’t it? I wouldn’t draw too many conclusions from it - maybe try setting up a test bed where you sweep a few harmonics through it instead.

I’ve spent a lot of time experimenting with half band filters and generating higher order harmonics and have learnt that selection of filter characteristics really is an exercise in finding the best compromise for your particular application. And that brings me to some feedback for Ivan…

Ivan, I’ve been taking a look at this class over the last couple of days and comparing it to my own wrapper for Laurent de Soras’ HIIR. I must say your solution for handling multiple stages is quite elegant! I have however got a few constructive comments:

  • As noted above, I think it’s important for users to be able to design their own filters
    • Your code comments say to inherit from Oversampling to do this
    • However this is not practical unless you change most of the private members to protected
  • I think your choice of transition bandwidths could be improved a little
    • First stage of upsampling and last stage of downsampling could be tighter (I use 0.01 for high quality)
    • Other stages could be a lot looser (0.255 should be fine for all of them)
  • I find it a bit confusing where you refer to the factor when I think you’re referring to the order (i.e. 16X is a factor while 4 is the order)
  • I think there are important use cases where factors much higher than 16X are useful (e.g. guitar cabinet modelling)
  • Performance notes
    • I adapted your code to match the filter design I use in my own library and found that on some systems it was 45% slower than the SSE optimised HIIR routines by Laurent de Soras. On other systems the performance was approximately the same.
    • If I unroll the for loops for the direct and delayed paths then performance more or less matches (at block sizes of 256)
      • This of course makes the code quite verbose if you want to cater for say 4…13 coefficients (but I’d be happy to provide this if you like!)
    • For mastering quality filters it might be worth considering using SIMD instructions to process the channels in parallel
      • I haven’t thought this through, but other than exposing those private members, I don’t think you’d need to make any other changes to your interface
  • I haven’t yet validated the outputs of your filters against my own library, but I’ll get to it eventually :slight_smile:
3 Likes

Hello @yairadix !

I think your test is wrong, since your assumption of the oversampling filtering which should act on the half sampling frequency is wrong.

A better test would be to use a sine with a frequency around 10 kHz, then applying some heavy waveshaping to it, and oversampling that waveshaping. With a spectrum analyzer, you might see how much the aliased components are attenuated with the different options. Even better would be to use a sine sweep and run the tests like in the SRC website.

@Andrew_J : thanks for the extensive and constructive feedback ! I have to confess I have not spent that much time exploring the best options for the different stages filtering options, because I have not been able to get enough information on this prior to the development of the DSP module. Which was the reason I thought it might a good thing to let users create their derived class from Oversampling to set the filters depending on their needs. And of course some of the members should be protected instead of private to allow this !

The other reason I didn’t spend much time on this is because I wanted to have my PlotComponent class fully developed before working again on this, so users (and me) might be able to see the influence of the filters parameters thanks to some curves in a demo app, to be able to choose the best options. Anyway, I might change at some point the default values for something more relevant, I’ll do it when I have some time (and I’ll update the comments to prevent any confusion between “order” and “factor” or “number of stages”).

Regarding performance, I have not done yet a lot of benchmarking there, I’ll have a look in the code at the same time to see if I can improve it there as well. SIMD is something I usually don’t do when it’s not an absolute necessity and that I have a lot of parallel processing. But since Laurent de Soras did it that means there is something relevant to do on the SIMD side in my class as well.

(Going back to finishing the PlotComponent class, + the delay line class in my own personal module, + debugging some stuff in the Convolution class + working on the next DSP module discussions topics for the JUCE forum + finishing work for upcoming deadlines… :joy: :rofl: )

By the way, I have created this topic on KVR : http://www.kvraudio.com/forum/viewtopic.php?f=33&t=497984

Don’t hesitate to participate, I’ll include all the new intel from there in the next iterations of the Oversampling class :wink:

1 Like

Hi Ivan, I’m currently working on code that uses your Oversampling class and I have a few comments/questions.

Due to 10.6.8 compatibility, I so far used the hiir oversampling class (http://ldesoras.free.fr/prod.html) which is similar to your IIR implementation. I think it uses the exact same math for the filters, but the recommendations for bandwidth and attenuation are different. In hiir, Laurent de Soras writes:

For example, let's suppose one wants 16x downsampling, with 96 dB of stopband
attenuation and a 0.49*Fs passband. You'll need the following specifications
for each stage:

 2x -> 1x: TBW = 0.01
 4x -> 2x: TBW = 0.01/2 + 1/4 = 0.255
 8x -> 4x: TBW = 0.01/4 + 1/8 + 1/4 = 0.3775
16x -> 8x: TBW = 0.01/8 + 1/16 + 1/8 + 1/4 = 0.43865

The reason is that you do not need to preserve spectrum parts that will be
wiped out by subsequent stages. Only the spectrum part present after the
final stage has to be preserved.

More generally:

TBW[stage] = (TBW[stage-1] + 0.5) / 2
or
TBW[stage] = TBW[0] * (0.5^stage) + 0.5 * (1 - 0.5^stage)

Your code has this logic:

    numStages = newFactor;

    for (size_t n = 0; n < numStages; ++n)
    {
        auto twUp   = (isMaximumQuality ? 0.10f : 0.12f) * (n == 0 ? 0.5f : 1.f);
        auto twDown = (isMaximumQuality ? 0.12f : 0.15f) * (n == 0 ? 0.5f : 1.f);

        auto gaindBStartUp    = (isMaximumQuality ? -75.f : -65.f);
        auto gaindBStartDown  = (isMaximumQuality ? -70.f : -60.f);
        auto gaindBFactorUp   = (isMaximumQuality ? 10.f  : 8.f);
        auto gaindBFactorDown = (isMaximumQuality ? 10.f  : 8.f);

        engines.add (new Oversampling2TimesPolyphaseIIR<SampleType> (numChannels,
                                                                     twUp, gaindBStartUp + gaindBFactorUp * n,
                                                                     twDown, gaindBStartDown + gaindBFactorDown * n));
    }

The issue is I got better quality with hiir and it looks like your code uses narrower bandwidth than necessary on higher stages and therefore wastes CPU. For me, it would be very nice if the attenuation in dB could be a constructor parameter instead of that isMaximumQuality flag. In my humble opinion, your maximum quality isn’t maximum at all. I am creating a plugin where suppressing aliasing is the most important aspect and -75 dB (+ n*10dB) is not enough for my taste.

If I read your logic right you are assuming that the incoming signal has a maximum frequency slope of 10 dB/oct (gaindBFactorUp/Down) and thus later stages can use less attenuation. IMHO that is a faulty assumption, there are signals that have more high-frequency content, especially in cases where oversampling is needed the most. I don’t understand why gaindBFactorUp is higher for maximum quality? I think it would be better to just keep attenuation constant for all stages and increase bandwidth more.

One more question here: Why are the quality constants different for the IIR and the FIR oversamplers?

I will now create my own constructor to replace that logic and thank you for hinting at that in the description, but I also think this could lead to an interesting discussion and maybe some improvement or more flexibility for the non-subclassed oversampler.

Lastly… is it possible to use the juce dsp oversampler with SSE instructions? And I also mean the one-channel case. The mentioned hiir library uses SSE to calculate both polyphase filters at the same time and that seems a great use of SSE, but as far as I can tell this is not possible with the JUCE class. It looks like it might be able to do multi-channel oversampling using SIMDRegister, but that would mean interleaving the data, correct?

1 Like

I tried subclassing the Oversampling class as stated in the description

    Note: you might want to create a class inheriting from Oversampling with a
    different constructor if you need more control over what happens in the process.

However… as all members are private it is impossible. Would it be possible to change these to protected? Or did you mean something else by that comment?

Anyway even that wouldn’t help as all the engines are inaccessible due to being in the .cpp file. To be honest rather than subclassing I’d like to get a more flexible constructor which would allow configuring the individual stages.

1 Like

Hello and thanks for your message !

I have been working on some improvements of the Oversampling class over the last months, but I have not been able to finish them yet, and some of them can’t be pushed until there is a Delay class in JUCE :slight_smile:

All your comments are very relevant, and to be fair I didn’t put that much thought into the constructor default logic, since I assumed that most of the people would just copy and paste the class to fit their needs (which is wrong, since it could be more efficient to change the constructor, which is not possible yet by mistake). I guess that’s something I can improve quickly already. I’m in the middle of something for one of my clients right now, when it’s done I’ll have a look into this (I have to change the way the Convolution class does the smoothing as well by the way). I’ll probably add a new constructor there as well.

For the SSE optimization, I didn’t try to do it here yet (sorry about this).

1 Like

I would be very interested on a Oversampling class with SIMD optimisation as well.
I haven’t analysed the Oversampling class deeply enough yet, but I would propose: why not making the Convolution class SIMD optimised (I’m not sure if FloatVectorOperation always uses SIMD inst) and use it for the Oversampling class?

FloatVectorOperations always uses SIMD (or whatever the platform provides, like VDSP if the app is configured to use it where possible). The only difference is that its copy function uses memcpy, which the compiler is likely to optimise better under the hood. I think Jules and co. will have a better/more correct explanation as they’re the ones that dug into it when designing the class.

Note that juce::Oversampling uses AudioBlock under the hood, which wraps functionality over FloatVectorOperations:

1 Like