Perfect Crossover Filters

Hello,

With a little help from this forum and this post here:

https://www.modernmetalproduction.com/linkwitz-riley-crossovers-digital-multiband-processing/

I’ve been attempting to build a perfect Multiband crossover network, however I keep seeing issues in the signal when it’s reconstructed. Although audibly it sounds okay, I see distortions in the signal, or perhaps the “perfect crossover” isn’t perfect in this manner?

My method is the one described by @danielrudrich in this post:

I’m using the makeLowPass, makeHighPass, and makeAllPass functions of the IIR filter classes to construct the filters

The block looks like:

                                | ---> HP ---> |
        | ---> HP ------------->|              + ---> |
        |                       | ---> LP ---> |
     -->|                                      + ---> |
        |
        | ---> LP ---> AP -------------------- + ---> | 

HP & LP refer to two of those lowpass & highpass filters in a chain. From my understanding of these forum posts, chaining the filters together in this manner creates a Linkwitz Riley response.

The AP filter has the cutoff set to that of the second crossover. From my understanding this should realign the phase between the bands for a perfect response. I’ve tried using two Allpasses in chain, and one, but neither generated a perfect response. So, that is the first thing I’m unclear about. When using these chained butterworths, do I need to also chain two AP filters for the correct response?

I’m getting a sort of notch sounding phase cancellation in the signal I can’t track down. This is actually improved by removing the Allpass filter, but visually I can see the waveform is getting distorted although audibly the notch is lessened.

Would any DSP wizards be willing to chime in and tell me if the is the correct approach here? The article from modern metal says the mid band should have its phase inverted, but this only worsens the notching sound in the signal.

I was able to get a perfect reconstruction by doing the approach of generating the low & high band, and subtracting them to generate the mid band. although this method works well, I need to refilter the bands after processing, which that doesn’t support. I also found the bands to sound less tight than desired.

Appreciate any input or feedback!

Thanks,

J

3 Likes

I don’t think that makeAllPass will give you the needed allpass-filter to correct the phase shift by the LR-crossover.

Take a look at these lines:

That’s the code how we calculate the coefficients for LP, HP and AP of the LR-crossover. The allpass coefficients are set in the last line.

I think the logic here is good, but the order of the filters might be off. If you have LP and HP filters at the 2Nth order, you need an all-pass filter at the Nth order to compensate their phase response.

If you want to create your third bands separator using only JUCE classes, each LP and HP block has to be the combination of TWO juce::IIRFilter, to get a 4th order LP/HP filter, all with the same cutoff frequency and the resonance at 1/sqrt(2). Then you can use on juce::IIRFilter for the all-pass filter with the right cutoff frequency, and you get a separation with 4th order Linkwitz Riley filters. The maths is quite different for 2nd order, you can’t just use a regular LP or HP filter at the 2nd order, and you need 1st order all-pass filter so it would be more complicated to do.

Hope that helps

2 Likes

Hey guys, thanks for your response : )

Thanks for the code example Daniel. I feel kind of uncomfortable copying your coefficient code. Do you have any references I can read into on this perhaps?

I actually did check out the compressor code process block where you posted it in the other channel, but it was quite complex! I’ve been coding things in a quite obtuse way as I work through this.

Thanks for this Ivan, so that clarifies I only need a single All Pass

So, from what I can tell, in order to pull this off in the way described, here are all the filters I’d need:

    enum Filters {
            
    //---------\
    //          \
    //           \-------------
    
    Stage_1_Band_1_Lowpass = 0,
    Stage_2_Band_1_Lowpass,
    Stage_1_Band_1_Allpass,
    
    //           /------------
    //          /
    //---------/
    
    Stage_1_Band_2_Highpass,
    Stage_2_Band_2_Highpass,
            
    //         /--------\
    //        /          \
    //-------/            \---------
    
    Stage_1_Band_2_Lowpass,
    Stage_2_Band_2_Lowpass,
    
    //                    /--------
    //                   /
    //------------------/
    
    Stage_1_Band_3_Highpass,
    Stage_2_Band_3_Highpass,
    
    TotalFilterAmount
};

And the coefficients as such:

    //---------\
//          \
//           \-------------

IIRCoeff low_mid_lowpass_coeff = dsp::IIR::Coefficients<float>::makeLowPass(mSampleRate, inCrossover1Freq);
IIRCoeff low_allpass_coeff = dsp::IIR::Coefficients<float>::makeAllPass(mSampleRate, inCrossover2Freq);

mBSF.LeftFilters[BandSplittingFilters::Stage_1_Band_1_Lowpass].coefficients = low_mid_lowpass_coeff;

mBSF.LeftFilters[BandSplittingFilters::Stage_2_Band_1_Lowpass].coefficients = low_mid_lowpass_coeff;

mBSF.LeftFilters[BandSplittingFilters::Stage_1_Band_1_Allpass].coefficients = low_allpass_coeff;


//           /------------
//          /
//---------/

IIRCoeff low_mid_highpass_coeff = dsp::IIR::Coefficients<float>::makeHighPass(mSampleRate, inCrossover1Freq);

mBSF.LeftFilters[BandSplittingFilters::Stage_1_Band_2_Highpass].coefficients = low_mid_highpass_coeff;

mBSF.LeftFilters[BandSplittingFilters::Stage_2_Band_2_Highpass].coefficients = low_mid_highpass_coeff;

//         /--------\
//        /          \
//-------/            \-------------

IIRCoeff mid_high_lowpass_coeff = dsp::IIR::Coefficients<float>::makeLowPass(mSampleRate, inCrossover2Freq);

mBSF.LeftFilters[BandSplittingFilters::Stage_1_Band_2_Lowpass].coefficients = mid_high_lowpass_coeff;

mBSF.LeftFilters[BandSplittingFilters::Stage_2_Band_2_Lowpass].coefficients = mid_high_lowpass_coeff;

//                     /--------
//                    /
//-------------------/

IIRCoeff mid_high_highpass_coeff = dsp::IIR::Coefficients<float>::makeHighPass(mSampleRate, inCrossover2Freq);

mBSF.LeftFilters[BandSplittingFilters::Stage_1_Band_3_Highpass].coefficients = mid_high_highpass_coeff;

mBSF.LeftFilters[BandSplittingFilters::Stage_2_Band_3_Highpass].coefficients = mid_high_highpass_coeff;

So, it’s two LPF on the dry input to create the low band a cross over 1, then an Allpass filter.

two HPF on dry input at crossover 1 to create the high band, now we have two bands, then pass the high band to a LPF at crossover 2 to generate the mid band, and a HPF at crossover2 to create the high band.

So, if this is all correct, then perhaps it really is the APF isn’t working for this like you say @danielrudrich? Are there any good papers on where these coeffs came from? I don’t wanna just copy for coefficient code : / although I’ll give it a test and see if that fixes it. Thank you both!

1 Like
                             | ---> HP1
   | ---> HP0 ---> AP2 ----->|             
   |                         | ---> LP1 
-->|                                      
   |                         | ---> HP2 
   | ---> LP0 ---> AP1 ----->| 
                             | ---> LP2 

Did you do something like this? AP1 has the frequency response of HP1+LP1 and AP2 that of HP2+LP2 in order to compensate both paths.

1 Like

With that layout you’ll need an additional AP2 in the lower lane.
AP1 only compensates for HP1+HP2, however the lanes after HP1 and LP1 get an additional Allpass-characteristic (AP2 which is HP2+LP2).

@danielrudrich Thanks very much! I seem to have a flatter response now. There still seems to be a drop off of a few dB across the whole frequency range but I can live with that.

Hey Jake, wondering if you figured this out or found some good references on how/why to place the All-Pass in the right place? Going through this process trying to create a multi-band processor right now

Hey, the filter configuration i showed above is actually correct. If you set it up that way, you should be all good. The issue i was having of phasyness was actually due to extra latency being introduced to a band before summing them back together, something to watch out for!

Jake

I’m currently struggling with odd phase response for my 3 band plugin using the dsp::LinkwitzRiley filters. I get a flat frequency response, but phase weirdness at the cross-over points:

I do the processing like this:

Use one LR filter in LowPass to split the input into Low and Mid-Highs.
Use another LR filter in LowPass to split the Mid-Highs.
Use a 3rd LR filter in AllPass set to the cutoff of the Mid-High splitting filter on the Low signal.
Sum the Mid, High and AllPassed-Low signals.

I thought I understood the theory behind this, but have obviously missed something to get this weird phase response.

Nothing really surprising, it looks continuous in phase space.

So am I barking up the wrong tree with the JUCE Linkwitz-Riley filters if I want a flat frequency and phase response for a multi-band processor?

You can’t have both flat phase and flat frequency. That would be a filter that is just the identity AFAIK.

But something close? A couple of other multi-band processors I have show either perfect or near perfect frequency and phase response (Waves LinMB is flat in phase and shows just a tiny roll off in the top end, UAD Precison Multiband is flat across both), so it’s certainly possible, but how achievable for someone like me is another matter! :joy:

Even if they seem to be flat, they will have an effect on the phase. You may see it as 0 phase, but it may be -10pi, which means that the signal is still very different from the original signal that you had.
You can still add an all-pass filter after that would somehow make the phase flatter, but it’s still an additional process. And you have to remember as well that phase is not something that the ear can hear (phase differences between the ears, yes).

1 Like

I think you’re correct here, I looked at the phase response for the individual bands on the UAD Preicision Multiband and each one had really crazy phase information out of band, I guess that all combines somehow to give this apparently flat phase response.

I think I just over analysed the problem really, if it sounds OK (which it does) then the phase shifting isn’t a problem.

You can of course have flat phase response with linear phase FIR crossover filters. (but the price is induced latency)

1 Like

And that’s probably what is the result in the end when you try to flatten the response again. You can’t beat causality, so the only way to get back to a flat response is added latency.

Hi!
I’m trying to implement a 6 bands crossover following the code you linked but in this case the scheme must be: (?)


    //  filter block diagram
    //                                                    | ---> HP3 -----------------------------------------> |
    //        | ---> HP2 ---> AP1 --->|                                                   |---> LP3 --->
    //        |                                           | ---> LP3 ---> | ---> AP5---> |   
                                                                                                       |---> LP4 --->     
 
    //     -->|                                                                                      | ---> HP2 --->
    //        |                                           | ---> HP1 ---> |---> AP7---> |
    //        | ---> LP2 ---> AP3 --->|                                                   | ---> HP3 --->
    //                                                    | ---> LP1 ----------------------------------------> |


    for (int i = 0; i < numSimdFilters; ++i)
    {
        const filterFloatType* chPtrInterleaved[1] = {interleaved[i]->getChannelPointer (0)};
        juce::dsp::AudioBlock<filterFloatType> abInterleaved (const_cast<filterFloatType**> (chPtrInterleaved), 1, L);

        const filterFloatType* chPtrLow[1] = {freqBands[FrequencyBands::Low][i]->getChannelPointer (0)};
        juce::dsp::AudioBlock<filterFloatType> abLow (const_cast<filterFloatType**> (chPtrLow), 1, L);

        const filterFloatType* chPtrMidLow[1] = {freqBands[FrequencyBands::MidLow][i]->getChannelPointer (0)};
        juce::dsp::AudioBlock<filterFloatType> abMidLow (const_cast<filterFloatType**> (chPtrMidLow), 1, L);

        const filterFloatType* chPtrMid[1] = {freqBands[FrequencyBands::Mid][i]->getChannelPointer (0)};
        juce::dsp::AudioBlock<filterFloatType> abMid(const_cast<filterFloatType**> (chPtrMid), 1, L);

        const filterFloatType* chPtrUpperMid[1] = {freqBands[FrequencyBands::UpperMid][i]->getChannelPointer (0)};
        juce::dsp::AudioBlock<filterFloatType> abUpperMid(const_cast<filterFloatType**> (chPtrUpperMid), 1, L);

        const filterFloatType* chPtrMidHigh[1] = {freqBands[FrequencyBands::MidHigh][i]->getChannelPointer (0)};
        juce::dsp::AudioBlock<filterFloatType> abMidHigh (const_cast<filterFloatType**> (chPtrMidHigh), 1, L);

        const filterFloatType* chPtrHigh[1] = {freqBands[FrequencyBands::High][i]->getChannelPointer (0)};
        juce::dsp::AudioBlock<filterFloatType> abHigh (const_cast<filterFloatType**> (chPtrHigh), 1, L);


        iirLP[1][i]->process (juce::dsp::ProcessContextNonReplacing<filterFloatType> (abInterleaved, abLow));
        iirHP[1][i]->process (juce::dsp::ProcessContextNonReplacing<filterFloatType> (abInterleaved, abHigh));

        iirLP2[1][i]->process (juce::dsp::ProcessContextReplacing<filterFloatType> (abLow));
        iirHP2[1][i]->process (juce::dsp::ProcessContextReplacing<filterFloatType> (abHigh));

        iirAP[2][i]->process (juce::dsp::ProcessContextReplacing<filterFloatType> (abLow));
        iirAP[0][i]->process (juce::dsp::ProcessContextReplacing<filterFloatType> (abHigh));

        iirHP[0][i]->process (juce::dsp::ProcessContextNonReplacing<filterFloatType> (abLow, abMidLow));
        iirHP2[0][i]->process (juce::dsp::ProcessContextReplacing<filterFloatType> (abMidLow));

        iirLP[0][i]->process (juce::dsp::ProcessContextReplacing<filterFloatType> (abLow));
        iirLP2[0][i]->process (juce::dsp::ProcessContextReplacing<filterFloatType> (abLow));

        iirLP[2][i]->process (juce::dsp::ProcessContextNonReplacing<filterFloatType> (abHigh, abMidHigh));
        iirLP2[2][i]->process (juce::dsp::ProcessContextReplacing<filterFloatType> (abMidHigh));

        iirAP[0][i]->process (juce::dsp::ProcessContextReplacing<filterFloatType> (abMidLow));
        iirAP[2][i]->process (juce::dsp::ProcessContextReplacing<filterFloatType> (abMidHigh));

        iirHP[0][i]->process (juce::dsp::ProcessContextNonReplacing<filterFloatType> (abMidLow, abMid));
        iirHP2[0][i]->process (juce::dsp::ProcessContextReplacing<filterFloatType> (abMid));

        iirLP[0][i]->process (juce::dsp::ProcessContextReplacing<filterFloatType> (abMidLow));
        iirLP2[0][i]->process (juce::dsp::ProcessContextReplacing<filterFloatType> (abMidLow));

        iirLP[2][i]->process (juce::dsp::ProcessContextNonReplacing<filterFloatType> (abMidHigh, abUpperMid));
        iirLP2[2][i]->process (juce::dsp::ProcessContextReplacing<filterFloatType> (abMidHigh));

        iirHP[2][i]->process (juce::dsp::ProcessContextReplacing<filterFloatType> (abHigh));
        iirHP2[2][i]->process (juce::dsp::ProcessContextReplacing<filterFloatType> (abHigh));
    }

Am I Wrong?

I’m struggling with this and i want to manage in total tranquility, because It’s an audio thing and not a C++ thing and I want to improve my knowledge…

Thank you in advance!