Files
UnrealEngine/Engine/Source/Runtime/SignalProcessing/Private/LinkwitzRileyBandSplitter.cpp
2025-05-18 13:04:45 +08:00

273 lines
8.7 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "DSP/LinkwitzRileyBandSplitter.h"
#include "DSP/FloatArrayMath.h"
#ifndef TWO_PI
#define TWO_PI (6.28318530718)
#endif
namespace Audio
{
void FLinkwitzRileyBandSplitter::Init(const int32 InChannels, const float InSampleRate, const EFilterOrder InFilterOrder, const bool bInPhaseCompensate, const TArray<float>& InCrossovers)
{
NumBands = InCrossovers.Num() + 1;
NumChannels = InChannels;
if (NumBands <= 1)
{
return;
}
FilterOrder = InFilterOrder;
SampleRate = InSampleRate;
BandFilters.Reset(NumBands);
BandFilters.AddDefaulted(NumBands);
Crossovers.Reset(InCrossovers.Num());
for (float Crossover : InCrossovers)
{
Crossovers.Add({ Crossover, GetBandwidthFromQ(GetQ(InFilterOrder)) });
}
// initalize each filter
// each band gets an AP filter for each subsquent band after its HP/LP filters
// 0 L LA LAA LAAA etc
// H HL HLA HLAA
// H HL HLA
// H HL
// H
BandFilters[0].Filters.AddDefaulted(NumBands - 1);
for (int32 BandId = 1; BandId < NumBands; BandId++)
{
const int32 NumFilters = NumBands - BandId;
BandFilters[BandId].Filters.AddDefaulted( bInPhaseCompensate ? NumFilters : FMath::Clamp(NumFilters, 1, 2) );
}
// band 0 special case
BandFilters[0][0].Init(FilterOrder, SampleRate, NumChannels, Crossovers[0].Frequency, EBiquadFilter::ButterworthLowPass, Crossovers[0].Bandwidth);
for (int32 FilterId = 1; FilterId < BandFilters[0].Filters.Num(); FilterId++)
{
BandFilters[0][FilterId].Init(FilterOrder, SampleRate, NumChannels, Crossovers[FilterId].Frequency, EBiquadFilter::AllPass, Crossovers[FilterId].Bandwidth);
}
// final band special case
BandFilters[NumBands - 1][0].Init(FilterOrder, SampleRate, NumChannels, Crossovers[Crossovers.Num() - 1].Frequency, EBiquadFilter::ButterworthHighPass, Crossovers[Crossovers.Num() - 1].Bandwidth);
if (NumBands <= 2)
{
return;
}
for (int32 BandId = 1; BandId < NumBands - 1; BandId++)
{
BandFilters[BandId][0].Init(FilterOrder, SampleRate, NumChannels, Crossovers[BandId - 1].Frequency, EBiquadFilter::ButterworthHighPass, Crossovers[BandId - 1].Bandwidth);
BandFilters[BandId][1].Init(FilterOrder, SampleRate, NumChannels, Crossovers[BandId].Frequency, EBiquadFilter::ButterworthLowPass, Crossovers[BandId].Bandwidth);
for (int32 FilterId = 2; FilterId < BandFilters[BandId].Filters.Num(); FilterId++)
{
const int32 CrossoverId = BandId + FilterId - 1; //band 1 crossovers are 0 - 1 - 2 - 4 etc, band 2 crossovers are 1 - 2 - 3 etc
BandFilters[BandId][FilterId].Init(FilterOrder, SampleRate, NumChannels, Crossovers[CrossoverId].Frequency, EBiquadFilter::AllPass, Crossovers[CrossoverId].Bandwidth);
}
}
}
// copy in -> shared
//
// if not band 0 - process first filter pair (hp) in place to reuse for next buffer
//
// copy result to new work buffer to not affect next bands
//
// process filters after 0
//
// invert if Band % 2 && fourpole
//
// output
void FLinkwitzRileyBandSplitter::ProcessAudioFrame(const float* InBuffer, FMultibandBuffer& OutBuffer)
{
if (NumBands <= 1)
{
// passthrough
CopyToBuffer(OutBuffer[0], InBuffer, NumChannels);
return;
}
SharedBuffer.Reset(NumChannels);
SharedBuffer.AddZeroed(NumChannels);
float* SharedBufferPtr = SharedBuffer.GetData();
CopyToBuffer(SharedBufferPtr, InBuffer, NumChannels);
BandWorkBuffer.Reset(NumChannels);
BandWorkBuffer.AddZeroed(NumChannels);
float* BandBufferPtr = BandWorkBuffer.GetData();
for (int32 BandId = 0; BandId < NumBands; BandId++)
{
int32 FilterId = 0;
// apply first filter before copying on bands > 0 so the filtered signal can be reused
if (BandId > 0)
{
BandFilters[BandId][0].ProcessAudioFrame(SharedBufferPtr, SharedBufferPtr);
FilterId++;
}
CopyToBuffer(BandBufferPtr, SharedBufferPtr, NumChannels);
for (; FilterId < BandFilters[BandId].Filters.Num(); FilterId++)
{
BandFilters[BandId][FilterId].ProcessAudioFrame(BandBufferPtr, BandBufferPtr);
}
constexpr int32 IsOddBitMask = 0x00000001;
if ((static_cast<int32>(FilterOrder) & IsOddBitMask) && (BandId & IsOddBitMask))
{
InvertBuffer(BandBufferPtr, NumChannels);
}
CopyToBuffer(OutBuffer[BandId], BandBufferPtr, NumChannels);
}
}
void FLinkwitzRileyBandSplitter::ProcessAudioBuffer(const float* InBuffer, FMultibandBuffer& OutBuffer, const int32 NumFrames)
{
check(OutBuffer.NumBands > 0);
check(OutBuffer.NumSamples >= NumFrames * NumChannels);
const int32 NumSamples = NumChannels * NumFrames;
if (NumBands <= 1)
{
// passthrough
CopyToBuffer(OutBuffer[0], InBuffer, NumSamples);
return;
}
SharedAlignedBuffer.SetNumZeroed(NumSamples, EAllowShrinking::No);
BandAlignedBuffer.SetNumUninitialized(NumSamples, EAllowShrinking::No);
float* const SharedBufferPtr = SharedAlignedBuffer.GetData();
float* const BandBufferPtr = BandAlignedBuffer.GetData();
CopyToBuffer(SharedBufferPtr, InBuffer, NumSamples);
for (int32 BandId = 0; BandId < NumBands; BandId++)
{
int32 FilterId = 0;
// apply first filter before copying on bands > 0 so the filtered signal can be reused
if (BandId > 0)
{
BandFilters[BandId][0].ProcessAudioBuffer(SharedBufferPtr, SharedBufferPtr, NumFrames);
FilterId++;
}
CopyToBuffer(BandBufferPtr, SharedBufferPtr, NumSamples);
for (; FilterId < BandFilters[BandId].Filters.Num(); FilterId++)
{
BandFilters[BandId][FilterId].ProcessAudioBuffer(BandBufferPtr, BandBufferPtr, NumFrames);
}
constexpr int32 IsOddBitMask = 0x00000001;
if ((static_cast<int32>(FilterOrder) & IsOddBitMask) && (BandId & IsOddBitMask))
{
TArrayView<float> BandBufferView(BandBufferPtr, NumSamples);
ArrayMultiplyByConstantInPlace(BandBufferView, -1.f);
}
CopyToBuffer(OutBuffer[BandId], BandBufferPtr, NumSamples);
}
}
void FLinkwitzRileyBandSplitter::SetCrossovers(const TArray<float>& InCrossoverFrequencies)
{
if (InCrossoverFrequencies.Num() != Crossovers.Num())
{
return;
}
Crossovers.Reset(InCrossoverFrequencies.Num());
for (float Crossover : InCrossoverFrequencies)
{
Crossovers.Add({ Crossover, GetBandwidthFromQ(GetQ(FilterOrder)) });
}
// perform same loop over band filters as in init, but only call SetFrequency instead of initializing it
// band 0
BandFilters[0][0].SetParams(EBiquadFilter::ButterworthLowPass, Crossovers[0].Frequency, Crossovers[0].Bandwidth);
for (int32 FilterId = 1; FilterId < BandFilters[0].Filters.Num(); FilterId++)
{
BandFilters[0][FilterId].SetParams(EBiquadFilter::AllPass, Crossovers[FilterId].Frequency, Crossovers[FilterId].Bandwidth);
}
// final band
BandFilters[NumBands - 1][0].SetParams(EBiquadFilter::ButterworthHighPass, Crossovers[Crossovers.Num() - 1].Frequency, Crossovers[Crossovers.Num() - 1].Bandwidth);
if (NumBands <= 2)
{
return;
}
// intermediate bands, if any
for (int32 BandId = 1; BandId < NumBands - 1; BandId++)
{
BandFilters[BandId][0].SetParams(EBiquadFilter::ButterworthHighPass, Crossovers[BandId - 1].Frequency, Crossovers[BandId - 1].Bandwidth);
BandFilters[BandId][1].SetParams(EBiquadFilter::ButterworthLowPass, Crossovers[BandId].Frequency, Crossovers[BandId].Bandwidth);
for (int32 FilterId = 2; FilterId < BandFilters[BandId].Filters.Num(); FilterId++)
{
const int32 CrossoverId = BandId + FilterId - 1; //band 1 crossovers are 0 - 1 - 2 - 4 etc, band 2 crossovers are 1 - 2 - 3 etc
BandFilters[BandId][FilterId].SetParams(EBiquadFilter::AllPass, Crossovers[CrossoverId].Frequency, Crossovers[CrossoverId].Bandwidth);
}
}
}
void FLinkwitzRileyBandSplitter::CopyToBuffer(float* Destination, const float* Origin, const int32 NumSamples)
{
FMemory::Memcpy(Destination, Origin, NumSamples * sizeof(float));
}
void FLinkwitzRileyBandSplitter::InvertBuffer(float* Buffer, const int32 NumSamples)
{
for (int32 Sample = 0; Sample < NumSamples; Sample++)
{
Buffer[Sample] *= -1.f;
}
}
float FLinkwitzRileyBandSplitter::GetQ(EFilterOrder InFilterOrder)
{
/*
* a two-pole filter naturally crosses its corner frequency at -3db with a Q of 1.
* with a Q of -3db, that makes it -6db, which is our target for LR crossover filters
*
* with stacked filters, that -3db point adds per filter, but the goal is still -6db,
* so four-pole filters automatically reach the correct level at the crossover
*
* passed that, the Q needs to raise the crossover point back to -6, but now it must be split over several stacked filters, so:
*
* Q(db) = (3db * NumFilters - 6db) / NumFilters
*/
const int32 NumFilters = static_cast<int32>(InFilterOrder);
if (ensure(NumFilters > 0) == false)
{
return 1.f;
}
const float Qdb = (3.f * NumFilters - 6.f) / NumFilters;
return Audio::ConvertToLinear(Qdb);
}
}