Parameterize FirCoefficients over the kernel size
This commit is contained in:
parent
b32cd27e8c
commit
7e3dfe904d
|
@ -200,7 +200,7 @@ impl FirCrossover {
|
||||||
for coef in fir_hp_coefs.0.iter_mut() {
|
for coef in fir_hp_coefs.0.iter_mut() {
|
||||||
*coef = -*coef;
|
*coef = -*coef;
|
||||||
}
|
}
|
||||||
fir_hp_coefs.0[FILTER_SIZE / 2] += f32x2::splat(1.0);
|
fir_hp_coefs.0[FILTER_SIZE / 2] += 1.0;
|
||||||
|
|
||||||
self.band_filters[num_bands - 1].coefficients = fir_hp_coefs;
|
self.band_filters[num_bands - 1].coefficients = fir_hp_coefs;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,10 +26,7 @@ use crate::crossover::iir::biquad::{Biquad, BiquadCoefficients};
|
||||||
pub struct FirFilter {
|
pub struct FirFilter {
|
||||||
/// The coefficients for this filter. The filters for both channels should be equivalent, this
|
/// The coefficients for this filter. The filters for both channels should be equivalent, this
|
||||||
/// just avoids broadcasts in the filter process.
|
/// just avoids broadcasts in the filter process.
|
||||||
///
|
pub coefficients: FirCoefficients<FILTER_SIZE>,
|
||||||
/// TODO: Profile to see if storing this as f32x2 rather than f32s plus splatting makes any
|
|
||||||
/// difference in performance at all
|
|
||||||
pub coefficients: FirCoefficients,
|
|
||||||
|
|
||||||
/// A ring buffer storing the last `FILTER_SIZE - 1` samples. The capacity is `FILTER_SIZE`
|
/// A ring buffer storing the last `FILTER_SIZE - 1` samples. The capacity is `FILTER_SIZE`
|
||||||
/// rounded up to the next power of two.
|
/// rounded up to the next power of two.
|
||||||
|
@ -40,11 +37,11 @@ pub struct FirFilter {
|
||||||
delay_buffer_next_idx: usize,
|
delay_buffer_next_idx: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Coefficients for an FIR filter. This struct includes ways to design the filter. Parameterized
|
/// Coefficients for a (linear-phase) FIR filter. This struct includes ways to design the filter.
|
||||||
/// over `f32x2` only for the time being since that's what we need here.
|
/// `T` is the sample type and `N` is the number of taps/coefficients and should be odd for linear-phase filters.
|
||||||
#[repr(transparent)]
|
#[repr(transparent)]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct FirCoefficients(pub [f32x2; FILTER_SIZE]);
|
pub struct FirCoefficients<const N: usize>(pub [f32; N]);
|
||||||
|
|
||||||
impl Default for FirFilter {
|
impl Default for FirFilter {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
|
@ -56,12 +53,12 @@ impl Default for FirFilter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FirCoefficients {
|
impl<const N: usize> Default for FirCoefficients<N> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
// Initialize this to a delay with the same amount of latency as we'd introduce with our
|
// Initialize this to a delay with the same amount of latency as we'd introduce with our
|
||||||
// linear-phase filters
|
// linear-phase filters
|
||||||
let mut coefficients = [f32x2::default(); FILTER_SIZE];
|
let mut coefficients = [0.0; N];
|
||||||
coefficients[FILTER_SIZE / 2] = f32x2::splat(1.0);
|
coefficients[N / 2] = 1.0;
|
||||||
|
|
||||||
Self(coefficients)
|
Self(coefficients)
|
||||||
}
|
}
|
||||||
|
@ -73,7 +70,7 @@ impl FirFilter {
|
||||||
// TODO: Replace direct convolution with FFT convolution, would make the implementation much
|
// TODO: Replace direct convolution with FFT convolution, would make the implementation much
|
||||||
// more complex though because of the multi output part
|
// more complex though because of the multi output part
|
||||||
let coefficients = &self.coefficients.0;
|
let coefficients = &self.coefficients.0;
|
||||||
let mut result = coefficients[0] * samples;
|
let mut result = f32x2::splat(coefficients[0]) * samples;
|
||||||
|
|
||||||
// Now multiply `self.coefficients[1..]` with the delay buffer starting at
|
// Now multiply `self.coefficients[1..]` with the delay buffer starting at
|
||||||
// `self.delay_buffer_next_idx - 1`, wrapping around to the end when that is reached
|
// `self.delay_buffer_next_idx - 1`, wrapping around to the end when that is reached
|
||||||
|
@ -89,7 +86,7 @@ impl FirFilter {
|
||||||
.rev(),
|
.rev(),
|
||||||
) {
|
) {
|
||||||
// `result += coefficient * sample`, but with explicit FMA
|
// `result += coefficient * sample`, but with explicit FMA
|
||||||
result = coefficient.mul_add(*delayed_sample, result);
|
result = f32x2::splat(*coefficient).mul_add(*delayed_sample, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
let after_wraparound_begin_idx =
|
let after_wraparound_begin_idx =
|
||||||
|
@ -100,7 +97,7 @@ impl FirFilter {
|
||||||
.iter()
|
.iter()
|
||||||
.rev(),
|
.rev(),
|
||||||
) {
|
) {
|
||||||
result = coefficient.mul_add(*delayed_sample, result);
|
result = f32x2::splat(*coefficient).mul_add(*delayed_sample, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
// And finally write the samples to the delay buffer for the enxt sample
|
// And finally write the samples to the delay buffer for the enxt sample
|
||||||
|
@ -117,7 +114,7 @@ impl FirFilter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FirCoefficients {
|
impl<const N: usize> FirCoefficients<N> {
|
||||||
/// A somewhat crude but very functional and relatively fast way create linear phase FIR
|
/// A somewhat crude but very functional and relatively fast way create linear phase FIR
|
||||||
/// **low-pass** filter that matches the frequency response of a fourth order biquad low-pass
|
/// **low-pass** filter that matches the frequency response of a fourth order biquad low-pass
|
||||||
/// filter. As in, this matches the frequency response magnitudes of applying those biquads to a
|
/// filter. As in, this matches the frequency response magnitudes of applying those biquads to a
|
||||||
|
@ -150,23 +147,24 @@ impl FirCoefficients {
|
||||||
///
|
///
|
||||||
/// The corresponding high-pass filter can be computed through spectral inversion.
|
/// The corresponding high-pass filter can be computed through spectral inversion.
|
||||||
pub fn design_fourth_order_linear_phase_low_pass_from_biquad(
|
pub fn design_fourth_order_linear_phase_low_pass_from_biquad(
|
||||||
biquad_coefs: BiquadCoefficients<f32x2>,
|
biquad_coefs: BiquadCoefficients<f32>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
const CENTER_IDX: usize = FILTER_SIZE / 2;
|
// Ruest doesn't allow you to define this as a constant
|
||||||
|
let center_idx = N / 2;
|
||||||
|
|
||||||
// We'll start with an impulse (at exactly half of this odd sized buffer)...
|
// We'll start with an impulse (at exactly half of this odd sized buffer)...
|
||||||
let mut impulse_response = [f32x2::default(); FILTER_SIZE];
|
let mut impulse_response = [0.0; N];
|
||||||
impulse_response[CENTER_IDX] = f32x2::splat(1.0);
|
impulse_response[center_idx] = 1.0;
|
||||||
|
|
||||||
// ...and filter that in both directions
|
// ...and filter that in both directions
|
||||||
let mut biquad = Biquad::default();
|
let mut biquad = Biquad::default();
|
||||||
biquad.coefficients = biquad_coefs;
|
biquad.coefficients = biquad_coefs;
|
||||||
for sample in impulse_response.iter_mut().skip(CENTER_IDX - 1) {
|
for sample in impulse_response.iter_mut().skip(center_idx - 1) {
|
||||||
*sample = biquad.process(*sample);
|
*sample = biquad.process(*sample);
|
||||||
}
|
}
|
||||||
|
|
||||||
biquad.reset();
|
biquad.reset();
|
||||||
for sample in impulse_response.iter_mut().skip(CENTER_IDX - 1).rev() {
|
for sample in impulse_response.iter_mut().skip(center_idx - 1).rev() {
|
||||||
*sample = biquad.process(*sample);
|
*sample = biquad.process(*sample);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,19 +174,19 @@ impl FirCoefficients {
|
||||||
|
|
||||||
// Adopted from `nih_plug::util::window`. We only end up applying the right half of the
|
// Adopted from `nih_plug::util::window`. We only end up applying the right half of the
|
||||||
// window, starting at the top of the window.
|
// window, starting at the top of the window.
|
||||||
let blackman_scale_1 = (2.0 * f32::consts::PI) / (impulse_response.len() - 1) as f32;
|
let blackman_scale_1 = (2.0 * f32::consts::PI) / (N - 1) as f32;
|
||||||
let blackman_scale_2 = blackman_scale_1 * 2.0;
|
let blackman_scale_2 = blackman_scale_1 * 2.0;
|
||||||
for (sample_idx, sample) in impulse_response.iter_mut().enumerate().skip(CENTER_IDX - 1) {
|
for (sample_idx, sample) in impulse_response.iter_mut().enumerate().skip(center_idx - 1) {
|
||||||
let cos_1 = (blackman_scale_1 * sample_idx as f32).cos();
|
let cos_1 = (blackman_scale_1 * sample_idx as f32).cos();
|
||||||
let cos_2 = (blackman_scale_2 * sample_idx as f32).cos();
|
let cos_2 = (blackman_scale_2 * sample_idx as f32).cos();
|
||||||
*sample *= f32x2::splat(0.42 - (0.5 * cos_1) + (0.08 * cos_2));
|
*sample *= 0.42 - (0.5 * cos_1) + (0.08 * cos_2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since this final filter will be symmetrical around `impulse_response[CENTER_IDX]`, we
|
// Since this final filter will be symmetrical around `impulse_response[CENTER_IDX]`, we
|
||||||
// can simply normalize based on that fact:
|
// can simply normalize based on that fact:
|
||||||
let would_be_impulse_response_sum =
|
let would_be_impulse_response_sum = (impulse_response.iter().skip(center_idx).sum::<f32>()
|
||||||
(impulse_response.iter().skip(CENTER_IDX).sum::<f32x2>() * f32x2::splat(2.0))
|
* 2.0)
|
||||||
- impulse_response[CENTER_IDX];
|
- impulse_response[center_idx];
|
||||||
let would_be_impulse_response_recip = would_be_impulse_response_sum.recip();
|
let would_be_impulse_response_recip = would_be_impulse_response_sum.recip();
|
||||||
for sample in &mut impulse_response {
|
for sample in &mut impulse_response {
|
||||||
*sample *= would_be_impulse_response_recip;
|
*sample *= would_be_impulse_response_recip;
|
||||||
|
@ -196,8 +194,8 @@ impl FirCoefficients {
|
||||||
|
|
||||||
// And finally we can simply copy the right half of the filter kernel to the left half
|
// And finally we can simply copy the right half of the filter kernel to the left half
|
||||||
// around the `CENTER_IDX`.
|
// around the `CENTER_IDX`.
|
||||||
for source_idx in CENTER_IDX + 1..impulse_response.len() {
|
for source_idx in center_idx + 1..N {
|
||||||
let target_idx = CENTER_IDX - (source_idx - CENTER_IDX);
|
let target_idx = center_idx - (source_idx - center_idx);
|
||||||
impulse_response[target_idx] = impulse_response[source_idx];
|
impulse_response[target_idx] = impulse_response[source_idx];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue