Skip to content

Commit

Permalink
More work on moving filter inside FM synth
Browse files Browse the repository at this point in the history
 * Basic filtering does work when playing the synth
 * TODO: change handling, state management, output channel changes, and audio graph cleanup
  • Loading branch information
Ameobea committed Jan 14, 2024
1 parent 9062c3c commit 76bebe5
Show file tree
Hide file tree
Showing 21 changed files with 360 additions and 84 deletions.
1 change: 0 additions & 1 deletion cypress/integration/basic.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ context('Entrypoint', () => {

cy.get('.control-panel-setting-label').should('contain.text', 'volume');
cy.get('.control-panel-setting-label').should('contain.text', 'wavetable fm');
cy.get('.control-panel-setting-label').should('contain.text', 'detune');
cy.get('.control-panel-setting-label').should('contain.text', 'order 2 wtable fm');
cy.get('.control-panel-setting-label').should('contain.text', 'lfo len samples');
cy.get('.control-panel-setting-label').should('contain.text', 'reverb room size');
Expand Down
20 changes: 10 additions & 10 deletions engine/dsp/src/band_splitter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,44 +17,44 @@ pub struct BandSplitter {
pub high_band_filter_chain: [BiquadFilter; BAND_SPLITTER_FILTER_CHAIN_LENGTH],
}

// computed using `compute_higher_order_biquad_q_factors`
const Q_FACTORS: [f32; 8] = [
-5.9786735, -5.638297, -4.929196, -3.7843077, -2.067771, 0.5116703, 4.7229195, 14.153371,
];

impl BandSplitter {
pub fn new() -> Self {
// computed using `compute_higher_order_biquad_q_factors`
let q_factors = [
-5.9786735, -5.638297, -4.929196, -3.7843077, -2.067771, 0.5116703, 4.7229195, 14.153371,
];

let mut low_band_filter_chain = [BiquadFilter::default(); BAND_SPLITTER_FILTER_CHAIN_LENGTH];
let mut mid_band_bottom_filter_chain =
[BiquadFilter::default(); BAND_SPLITTER_FILTER_CHAIN_LENGTH];
let mut mid_band_top_filter_chain =
[BiquadFilter::default(); BAND_SPLITTER_FILTER_CHAIN_LENGTH];
let mut high_band_filter_chain = [BiquadFilter::default(); BAND_SPLITTER_FILTER_CHAIN_LENGTH];
for i in 0..q_factors.len() {
for i in 0..Q_FACTORS.len() {
low_band_filter_chain[i].set_coefficients(
FilterMode::Lowpass,
q_factors[i],
Q_FACTORS[i],
0.,
LOW_BAND_CUTOFF,
0.,
);
mid_band_bottom_filter_chain[i].set_coefficients(
FilterMode::Highpass,
q_factors[i],
Q_FACTORS[i],
0.,
LOW_BAND_CUTOFF + 7.5,
0.,
);
mid_band_top_filter_chain[i].set_coefficients(
FilterMode::Lowpass,
q_factors[i],
Q_FACTORS[i],
0.,
MID_BAND_CUTOFF - 184.8,
0.,
);
high_band_filter_chain[i].set_coefficients(
FilterMode::Highpass,
q_factors[i],
Q_FACTORS[i],
0.,
MID_BAND_CUTOFF,
0.,
Expand Down
11 changes: 11 additions & 0 deletions engine/dsp/src/filters/biquad.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub enum FilterMode {
Peak,
Lowshelf,
Highshelf,
Allpass,
}

impl FilterMode {
Expand All @@ -35,6 +36,7 @@ impl FilterMode {
FilterMode::Peak => true,
FilterMode::Lowshelf => true,
FilterMode::Highshelf => true,
FilterMode::Allpass => false,
}
}

Expand All @@ -47,6 +49,7 @@ impl FilterMode {
FilterMode::Peak => true,
FilterMode::Lowshelf => false,
FilterMode::Highshelf => false,
FilterMode::Allpass => true,
}
}
}
Expand Down Expand Up @@ -136,6 +139,14 @@ impl BiquadFilter {
a1 = 2. * ((A - 1.) - (A + 1.) * w0_cos);
a2 = (A + 1.) - (A - 1.) * w0_cos - 2. * a_s * A_sqrt;
},
FilterMode::Allpass => {
b0 = 1. - aq;
b1 = -2. * w0_cos;
b2 = 1. + aq;
a0 = 1. + aq;
a1 = -2. * w0_cos;
a2 = 1. - aq;
},
}

(b0 / a0, b1 / a0, b2 / a0, a1 / a0, a2 / a0)
Expand Down
37 changes: 34 additions & 3 deletions engine/dsp/src/filters/filter_chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,48 @@ pub fn apply_filter_chain_and_compute_coefficients<const N: usize>(
chain: &mut [BiquadFilter; N],
frame: &mut [f32; FRAME_SIZE],
filter_mode: FilterMode,
precomputed_base_qs: &[f32; N],
q: &[f32; FRAME_SIZE],
cutoff_freq: &[f32; FRAME_SIZE],
gain: &[f32; FRAME_SIZE],
) {
for i in 0..frame.len() {
let coeffs = BiquadFilter::compute_coefficients(filter_mode, q[i], 0., cutoff_freq[i], gain[i]);

let mut val = frame[i];
for filter in chain.into_iter() {
for (filter_ix, filter) in chain.into_iter().enumerate() {
let coeffs = BiquadFilter::compute_coefficients(
filter_mode,
precomputed_base_qs[filter_ix] + q[i],
0.,
cutoff_freq[i],
gain[i],
);
val = filter.apply_with_coefficients(val, coeffs.0, coeffs.1, coeffs.2, coeffs.3, coeffs.4);
}
frame[i] = val;
}
}

#[inline]
pub fn apply_filter_chain_and_compute_coefficients_minimal<const N: usize>(
chain: &mut [BiquadFilter; N],
frame: &mut [f32; FRAME_SIZE],
filter_mode: FilterMode,
precomputed_base_qs: &[f32; N],
cutoff_freq: &[f32; FRAME_SIZE],
) {
for (filter_ix, filter) in chain.into_iter().enumerate() {
for i in 0..frame.len() {
let coeffs = BiquadFilter::compute_coefficients(
filter_mode,
precomputed_base_qs[filter_ix],
0.,
cutoff_freq[i],
0.,
);

let mut val = frame[i];
val = filter.apply_with_coefficients(val, coeffs.0, coeffs.1, coeffs.2, coeffs.3, coeffs.4);
frame[i] = val;
}
}
}
95 changes: 95 additions & 0 deletions engine/wavetable/src/fm/filter/dynabandpass.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
use dsp::{
filters::{
biquad::{BiquadFilter, FilterMode},
filter_chain::apply_filter_chain_and_compute_coefficients_minimal,
},
FRAME_SIZE, NYQUIST,
};

/// Frequency is a log scale, so we need to increase the bandwidth as the base frequency increases.
///
/// Given the frequency of the center of a band and its width when the band's center is 10 Hz,
/// this function returns the width of the band when the base frequency is the given frequency.
///
/// # Arguments
///
/// * `base_frequency` - The base frequency.
/// * `base_bandwidth` - The base bandwidth.
/// * `frequency` - The frequency for which to compute the modified bandwidth.
///
/// # Returns
///
/// * The computed modified bandwidth.
#[inline]
fn compute_modified_dynabandpass_filter_bandwidth(
base_frequency: f32,
base_bandwidth: f32,
frequency: f32,
) -> f32 {
let log_base_frequency = (base_frequency + base_bandwidth / 2.0).log10();
let log_frequency = frequency.log10();
let log_base_bandwidth = base_bandwidth.log10();

10f32.powf(log_base_bandwidth + (log_frequency - log_base_frequency))
}

/// Computes the cutoff frequencies for the high-order lowpass and highpass filters that make up the
/// dynabandpass filter.
#[inline]
fn compute_filter_cutoff_frequencies(center_frequency: f32, base_bandwidth: f32) -> (f32, f32) {
let bandwidth =
compute_modified_dynabandpass_filter_bandwidth(10., base_bandwidth, center_frequency);
let highpass_freq = dsp::clamp(10., NYQUIST, center_frequency - bandwidth / 2.);
let lowpass_freq = dsp::clamp(10., NYQUIST, center_frequency + bandwidth / 2.);
(lowpass_freq, highpass_freq)
}

const DYNABANDPASS_FILTER_ORDER: usize = 8;

// computed using `compute_higher_order_biquad_q_factors`
const PRECOMPUTED_BASE_Q_FACTORS: [f32; 4] = [-5.852078, -4.417527, -0.91537845, 8.174685];

#[derive(Clone)]
pub(crate) struct DynabandpassFilter {
lowpass_filter_chain: [BiquadFilter; DYNABANDPASS_FILTER_ORDER / 2],
highpass_filter_chain: [BiquadFilter; DYNABANDPASS_FILTER_ORDER / 2],
bandwidth: f32,
lowpass_cutoff_freqs: [f32; FRAME_SIZE],
highpass_cutoff_freqs: [f32; FRAME_SIZE],
}

impl DynabandpassFilter {
pub fn new(bandwidth: f32) -> Self {
Self {
lowpass_filter_chain: [BiquadFilter::default(); DYNABANDPASS_FILTER_ORDER / 2],
highpass_filter_chain: [BiquadFilter::default(); DYNABANDPASS_FILTER_ORDER / 2],
bandwidth,
lowpass_cutoff_freqs: [0.; FRAME_SIZE],
highpass_cutoff_freqs: [0.; FRAME_SIZE],
}
}

pub fn apply_frame(&mut self, frame: &mut [f32; FRAME_SIZE], cutoff_freqs: &[f32; FRAME_SIZE]) {
for i in 0..FRAME_SIZE {
let (lowpass_freq, highpass_freq) =
compute_filter_cutoff_frequencies(cutoff_freqs[i], self.bandwidth);
self.lowpass_cutoff_freqs[i] = lowpass_freq;
self.highpass_cutoff_freqs[i] = highpass_freq;
}

apply_filter_chain_and_compute_coefficients_minimal(
&mut self.lowpass_filter_chain,
frame,
FilterMode::Lowpass,
&PRECOMPUTED_BASE_Q_FACTORS,
&self.lowpass_cutoff_freqs,
);
apply_filter_chain_and_compute_coefficients_minimal(
&mut self.highpass_filter_chain,
frame,
FilterMode::Highpass,
&PRECOMPUTED_BASE_Q_FACTORS,
&self.highpass_cutoff_freqs,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,18 @@ use dsp::{
FRAME_SIZE,
};

use self::dynabandpass::DynabandpassFilter;

use super::{ParamSource, RenderRawParams, FILTER_PARAM_BUFFER_COUNT};

pub mod dynabandpass;

const ZERO_FRAME: [f32; FRAME_SIZE] = [0.; FRAME_SIZE];

#[derive(Clone, Copy)]
#[repr(usize)]
enum FilterType {
#[allow(dead_code)]
pub(crate) enum FilterType {
Lowpass = 0,
LP4 = 1,
LP8 = 2,
Expand All @@ -36,14 +43,30 @@ enum FilterType {
Allpass = 21,
}

impl FilterType {
pub fn from_usize(val: usize) -> Self {
if val > Self::Allpass as usize {
panic!("Invalid FilterType value: {}", val);
}
unsafe { std::mem::transmute(val) }
}
}

#[derive(Clone)]
enum FilterState {
pub(crate) enum FilterState {
SimpleBiquad(FilterMode, BiquadFilter),
Order4Biquad(FilterMode, [BiquadFilter; 2]),
Order8Biquad(FilterMode, [BiquadFilter; 4]),
Order16Biquad(FilterMode, [BiquadFilter; 8]),
DynaBandpass(DynabandpassFilter),
}

const PRECOMPUTED_ORDER_4_BASE_Q_FACTORS: [f32; 2] = [-5.3329067, 2.322607];
const PRECOMPUTED_ORDER_8_BASE_Q_FACTORS: [f32; 4] = [-5.852078, -4.417527, -0.91537845, 8.174685];
const PRECOMPUTED_ORDER_16_BASE_Q_FACTORS: [f32; 8] = [
-5.9786735, -5.638297, -4.929196, -3.7843077, -2.067771, 0.5116703, 4.7229195, 14.153371,
];

impl FilterState {
pub fn new_simple_biquad(filter_mode: FilterMode) -> Self {
Self::SimpleBiquad(filter_mode, BiquadFilter::default())
Expand All @@ -67,6 +90,7 @@ impl FilterState {
FilterState::Order4Biquad(filter_mode, _) => *filter_mode,
FilterState::Order8Biquad(filter_mode, _) => *filter_mode,
FilterState::Order16Biquad(filter_mode, _) => *filter_mode,
FilterState::DynaBandpass(_) => panic!("DynaBandpass should be special-cased"),
}
}

Expand Down Expand Up @@ -95,6 +119,7 @@ impl FilterState {
chain,
frame,
*filter_mode,
&PRECOMPUTED_ORDER_4_BASE_Q_FACTORS,
q,
cutoff_freq,
gain,
Expand All @@ -103,6 +128,7 @@ impl FilterState {
chain,
frame,
*filter_mode,
&PRECOMPUTED_ORDER_8_BASE_Q_FACTORS,
q,
cutoff_freq,
gain,
Expand All @@ -112,10 +138,12 @@ impl FilterState {
chain,
frame,
*filter_mode,
&PRECOMPUTED_ORDER_16_BASE_Q_FACTORS,
q,
cutoff_freq,
gain,
),
FilterState::DynaBandpass(filter) => filter.apply_frame(frame, cutoff_freq),
}
}
}
Expand Down Expand Up @@ -149,8 +177,6 @@ impl Default for FilterModule {
}
}

const ZERO_FRAME: [f32; FRAME_SIZE] = [0.; FRAME_SIZE];

impl FilterModule {
pub fn set_filter_type(&mut self, new_filter_type: FilterType) {
self.filter_type = new_filter_type;
Expand All @@ -167,20 +193,26 @@ impl FilterModule {
FilterType::BP4 => FilterState::new_order4_biquad(FilterMode::Bandpass),
FilterType::BP8 => FilterState::new_order8_biquad(FilterMode::Bandpass),
FilterType::BP16 => FilterState::new_order16_biquad(FilterMode::Bandpass),
FilterType::DynaBp50 => FilterState::new_simple_biquad(FilterMode::Bandpass),
FilterType::DynaBp100 => FilterState::new_simple_biquad(FilterMode::Bandpass),
FilterType::DynaBp200 => FilterState::new_simple_biquad(FilterMode::Bandpass),
FilterType::DynaBp400 => FilterState::new_simple_biquad(FilterMode::Bandpass),
FilterType::DynaBp800 => FilterState::new_simple_biquad(FilterMode::Bandpass),
FilterType::DynaBp50 => FilterState::DynaBandpass(DynabandpassFilter::new(50.)),
FilterType::DynaBp100 => FilterState::DynaBandpass(DynabandpassFilter::new(100.)),
FilterType::DynaBp200 => FilterState::DynaBandpass(DynabandpassFilter::new(200.)),
FilterType::DynaBp400 => FilterState::DynaBandpass(DynabandpassFilter::new(400.)),
FilterType::DynaBp800 => FilterState::DynaBandpass(DynabandpassFilter::new(800.)),
FilterType::Lowshelf => FilterState::new_simple_biquad(FilterMode::Lowshelf),
FilterType::Highshelf => FilterState::new_simple_biquad(FilterMode::Highshelf),
FilterType::Peaking => FilterState::new_simple_biquad(FilterMode::Peak),
FilterType::Notch => FilterState::new_simple_biquad(FilterMode::Notch),
FilterType::Allpass => unimplemented!(),
FilterType::Allpass => FilterState::new_simple_biquad(FilterMode::Allpass),
};
}

// TODO: Setters for param sources
pub fn set_q(&mut self, new_q: ParamSource) { self.q.replace(new_q); }

pub fn set_cutoff_freq(&mut self, new_cutoff_freq: ParamSource) {
self.cutoff_freq.replace(new_cutoff_freq);
}

pub fn set_gain(&mut self, new_gain: ParamSource) { self.gain.replace(new_gain); }

pub fn apply_frame(
&mut self,
Expand Down
Loading

0 comments on commit 76bebe5

Please sign in to comment.