diff --git a/src/dsp/helpers.rs b/src/dsp/helpers.rs index ff9d454..cbb6cb2 100644 --- a/src/dsp/helpers.rs +++ b/src/dsp/helpers.rs @@ -763,6 +763,63 @@ pub fn process_1pole_tpt_highpass(input: f64, freq: f64, israte: f64, z: &mut f6 input - v2 } +const FILTER_OVERSAMPLE_HAL_CHAMBERLIN : usize = 2; +// Hal Chamberlin's State Variable (12dB/oct) filter +// https://www.earlevel.com/main/2003/03/02/the-digital-state-variable-filter/ +// Inspired by SynthV1 by Rui Nuno Capela, under the terms of +// GPLv2 or any later: +/// Process a HAL Chamberlin filter with two delays/state variables. +/// The filter does internal oversampling with very simple decimation to +/// rise the stability for cutoff frequency up to 16kHz. +/// +/// * `input` - Input sample. +/// * `freq` - Frequency in Hz. Please keep it inside 0.0 to 16000.0 Hz! +/// otherwise the filter becomes unstable. +/// * `res` - Resonance from 0.0 to 0.99. Resonance of 1.0 is not recommended, +/// as the filter will then oscillate itself out of control. +/// * `band` - First state variable, containing the band pass result +/// after processing. +/// * `low` - Second state variable, containing the low pass result +/// after processing. +/// +/// Returned are the results of the high and notch filter. +/// +///``` +/// let mut band = 0.0; +/// let mut low = 0.0; +/// let mut freq = 1000.0; +/// +/// for s in samples.iter() { +/// let (high, notch) = +/// process_hal_chamberlin_svf( +/// s, freq, 0.5, &mut band, &mut low); +/// // ... do something with the result here. +/// } +///``` +#[inline] +pub fn process_hal_chamberlin_svf( + input: f64, freq: f64, res: f64, israte: f64, band: &mut f64, low: &mut f64) + -> (f64, f64) +{ + let q = 1.0 - res; + let cutoff = 2.0 * (std::f64::consts::PI * freq * 0.5 * israte).sin(); + + let mut high = 0.0; + let mut notch = 0.0; + + for _ in 0..FILTER_OVERSAMPLE_HAL_CHAMBERLIN { + *low += cutoff * *band; + high = input - *low - q * *band; + *band += cutoff * high; + notch = high + *low; + } + + //d// println!("q={:4.2} cut={:8.3} freq={:8.1} LP={:8.3} HP={:8.3} BP={:8.3} N={:8.3}", + //d// q, cutoff, freq, *low, high, *band, notch); + + (high, notch) +} + // translated from Odin 2 Synthesizer Plugin // Copyright (C) 2020 TheWaveWarden // under GPLv3 or any later diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index 26cc353..885d350 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -582,7 +582,8 @@ macro_rules! node_list { sfilter => SFilter UIType::Generic UICategory::Signal (0 inp n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) (1 freq n_pit d_pit r_fq f_freq stp_d -1.0, 0.5647131, 1000.0) - {2 0 ftype setting(0) fa_sfilter_type 0 3} + (2 res n_id d_id r_id f_def stp_d 0.0, 0.99, 0.5) + {3 0 ftype setting(0) fa_sfilter_type 0 7} [0 sig], test => Test UIType::Generic UICategory::IOUtil (0 f n_id d_id r_id f_def stp_d 0.0, 1.0, 0.5) diff --git a/src/dsp/node_sfilter.rs b/src/dsp/node_sfilter.rs index a81ed7f..1bae370 100644 --- a/src/dsp/node_sfilter.rs +++ b/src/dsp/node_sfilter.rs @@ -9,16 +9,21 @@ use crate::dsp::helpers::{ process_1pole_highpass, process_1pole_tpt_lowpass, process_1pole_tpt_highpass, + process_hal_chamberlin_svf, }; #[macro_export] macro_rules! fa_sfilter_type { ($formatter: expr, $v: expr, $denorm_v: expr) => { { let s = match ($v.round() as usize) { - 0 => "LP(1p)", - 1 => "LP(1pt)", - 2 => "HP(1p)", - 3 => "HP(1pt)", + 0 => "LP 1p", + 1 => "LP 1pt", + 2 => "HP 1p", + 3 => "HP 1pt", + 4 => "LP 12s", + 5 => "HP 12s", + 6 => "BP 12s", + 7 => "NO 12s", _ => "?", }; write!($formatter, "{}", s) @@ -30,6 +35,7 @@ pub struct SFilter { israte: f64, z: f64, y: f64, + otype: i8, } impl SFilter { @@ -38,12 +44,15 @@ impl SFilter { israte: 1.0 / 44100.0, z: 0.0, y: 0.0, + otype: -1, } } pub const inp : &'static str = "SFilter inp\nSignal input\nRange: (-1..1)\n"; pub const freq : &'static str = "SFilter freq\nFilter cutoff frequency.\nRange: (-1..1)\n"; + pub const res : &'static str = + "SFilter res\nFilter resonance.\nRange: (0..1)\n"; pub const ftype : &'static str = "SFilter ftype\nThe filter type, there are varying types of \ filters available. Please consult the node documentation for \ @@ -64,10 +73,14 @@ of varying types. There are only few parameters for you to change: 'freq' and 'res'onance. You can switch between the types with the 'ftype'. There are currently following filters available: - HP(1p) - One pole low-pass filter (6db) - HP(1pt) - One pole low-pass filter (6db) (TPT form) - LP(1p) - One pole high-pass filter (6db) - LP(1pt) - One pole high-pass filter (6db) (TPT form) + HP 1p - One pole low-pass filter (6db) + HP 1pt - One pole low-pass filter (6db) (TPT form) + LP 1p - One pole high-pass filter (6db) + LP 1pt - One pole high-pass filter (6db) (TPT form) + LP 12s - Low-pass Hal Chamberlin state variable filter (12dB) + HP 12s - High-pass Hal Chamberlin state variable filter (12dB) + BP 12s - Band-pass Hal Chamberlin state variable filter (12dB) + NO 12s - Notch Hal Chamberlin state variable filter (12dB) "#; } @@ -78,8 +91,9 @@ impl DspNode for SFilter { self.israte = 1.0 / (srate as f64); } fn reset(&mut self) { - self.z = 0.0; - self.y = 0.0; + self.z = 0.0; + self.y = 0.0; + self.otype = -1; } #[inline] @@ -93,11 +107,20 @@ impl DspNode for SFilter { let inp = inp::SFilter::inp(inputs); let freq = inp::SFilter::freq(inputs); + let res = inp::SFilter::res(inputs); let ftype = at::SFilter::ftype(atoms); let out = out::SFilter::sig(outputs); - match ftype.i() { - 0 => { + let ftype = ftype.i() as i8; + + if ftype != self.otype { + self.y = 0.0; + self.z = 0.0; + self.otype = ftype; + } + + match ftype { + 0 => { // Lowpass for frame in 0..ctx.nframes() { let input = inp.read(frame) as f64; let freq = denorm::SFilter::freq(freq, frame) as f64; @@ -108,7 +131,7 @@ impl DspNode for SFilter { as f32); } }, - 1 => { + 1 => { // Lowpass TPT for frame in 0..ctx.nframes() { let input = inp.read(frame) as f64; let freq = denorm::SFilter::freq(freq, frame) as f64; @@ -119,7 +142,7 @@ impl DspNode for SFilter { as f32); } }, - 2 => { + 2 => { // Highpass for frame in 0..ctx.nframes() { let input = inp.read(frame) as f64; let freq = denorm::SFilter::freq(freq, frame) as f64; @@ -130,7 +153,7 @@ impl DspNode for SFilter { as f32); } }, - 3 => { + 3 => { // Highpass TPT for frame in 0..ctx.nframes() { let input = inp.read(frame) as f64; let freq = denorm::SFilter::freq(freq, frame) as f64; @@ -141,6 +164,70 @@ impl DspNode for SFilter { as f32); } }, + 4 => { // Low Pass Hal Chamberlin SVF + for frame in 0..ctx.nframes() { + let input = inp.read(frame) as f64; + let freq = denorm::SFilter::freq(freq, frame) as f64; + let freq = freq.clamp(1.0, 22000.0); + let res = denorm::SFilter::res(res, frame) as f64; + let res = res.clamp(0.0, 0.99); + + let (_high, _notch) = + process_hal_chamberlin_svf( + input, freq, res, self.israte, + &mut self.z, &mut self.y); + + out.write(frame, self.y as f32); + } + }, + 5 => { // High Pass Hal Chamberlin SVF + for frame in 0..ctx.nframes() { + let input = inp.read(frame) as f64; + let freq = denorm::SFilter::freq(freq, frame) as f64; + let freq = freq.clamp(1.0, 22000.0); + let res = denorm::SFilter::res(res, frame) as f64; + let res = res.clamp(0.0, 0.99); + + let (high, _notch) = + process_hal_chamberlin_svf( + input, freq, res, self.israte, + &mut self.z, &mut self.y); + + out.write(frame, high as f32); + } + }, + 6 => { // Band Pass Hal Chamberlin SVF + for frame in 0..ctx.nframes() { + let input = inp.read(frame) as f64; + let freq = denorm::SFilter::freq(freq, frame) as f64; + let freq = freq.clamp(1.0, 22000.0); + let res = denorm::SFilter::res(res, frame) as f64; + let res = res.clamp(0.0, 0.99); + + let (_high, _notch) = + process_hal_chamberlin_svf( + input, freq, res, self.israte, + &mut self.z, &mut self.y); + + out.write(frame, self.z as f32); + } + }, + 7 => { // Notch Hal Chamberlin SVF + for frame in 0..ctx.nframes() { + let input = inp.read(frame) as f64; + let freq = denorm::SFilter::freq(freq, frame) as f64; + let freq = freq.clamp(1.0, 22000.0); + let res = denorm::SFilter::res(res, frame) as f64; + let res = res.clamp(0.0, 0.99); + + let (_high, notch) = + process_hal_chamberlin_svf( + input, freq, res, self.israte, + &mut self.z, &mut self.y); + + out.write(frame, notch as f32); + } + }, _ => {}, } diff --git a/tests/node_sfilter.rs b/tests/node_sfilter.rs index f67b84a..df727c6 100644 --- a/tests/node_sfilter.rs +++ b/tests/node_sfilter.rs @@ -27,11 +27,11 @@ fn setup_sfilter_matrix() -> (Matrix, NodeExecutor) { fn fft_with_freq_res_type( matrix: &mut Matrix, node_exec: &mut NodeExecutor, - ftype: i64, freq: f32, _res: f32) -> Vec<(u16, u32)> + ftype: i64, freq: f32, res: f32) -> Vec<(u16, u32)> { let sf = NodeId::SFilter(0); - pset_d_wait(matrix, node_exec, sf, "freq", freq); -// pset_d_wait(&mut matrix, &mut node_exec, sf, "freq", freq); + pset_d(matrix, sf, "freq", freq); + pset_d_wait(matrix, node_exec, sf, "res", res); pset_s(matrix, sf, "ftype", ftype); run_and_get_fft4096(node_exec, 0, 1000.0) } @@ -201,3 +201,63 @@ fn check_node_sfilter_highpass_tpt() { (0, 24), (100, 16), (1000, 16), (4000, 16), (12000, 16), ]); } + + +#[test] +fn check_node_sfilter_halsvf_lowpass() { + let (mut matrix, mut node_exec) = setup_sfilter_matrix(); + + // Low Pass Hal Chamberlin SVF @ 1000Hz RES=1.0 + let fft = fft_with_freq_res_type(&mut matrix, &mut node_exec, 4, 1000.0, 1.0); + assert_eq!( + avg_fft_freqs(10.0, &[ + 500, 700, 900, 1000, 1500, 2000, 3000, 4000, 12000 + ], &fft[..]), vec![ + (0, 20), (500, 20), (700, 50), (900, 240), (1000, 60), + (1500, 10), (2000, 0), (3000, 0), (4000, 0) + ]); + + // Low Pass Hal Chamberlin SVF @ 1000Hz RES=0.5 + let fft = fft_with_freq_res_type(&mut matrix, &mut node_exec, 4, 1000.0, 0.5); + assert_eq!( + avg_fft_freqs(10.0, &[ + 500, 700, 900, 1000, 1500, 2000, 3000, 4000, 12000 + ], &fft[..]), vec![ + (0, 20), (500, 20), (700, 30), (900, 40), (1000, 20), + (1500, 0), (2000, 0), (3000, 0), (4000, 0) + ]); + + // Low Pass Hal Chamberlin SVF @ 1000Hz RES=0.0 + let fft = fft_with_freq_res_type(&mut matrix, &mut node_exec, 4, 1000.0, 0.0); + assert_eq!( + avg_fft_freqs(10.0, &[ + 500, 700, 900, 1000, 1500, 2000, 3000, 4000, 12000 + ], &fft[..]), vec![ + (0, 10), (500, 20), (700, 20), (900, 10), (1000, 10), + (1500, 0), (2000, 0), (3000, 0), (4000, 0) + ]); + +// // High Pass TPT @ 4000Hz +// let fft = fft_with_freq_res_type(&mut matrix, &mut node_exec, 3, 4000.0, 0.0); +// assert_eq!( +// avg_fft_freqs(4.0, &[ +// 100, 250, 500, 750, 1000, 1500, 2000, 3000, 8000, 12000 +// ], &fft[..]), vec![ +// (0, 0), (100, 0), (250, 0), (500, 0), (750, 4), (1000, 4), +// (1500, 4), (2000, 8), (3000, 12), (8000, 16), +// ]); +// +// // High Pass TPT @ 22050Hz +// let fft = fft_with_freq_res_type(&mut matrix, &mut node_exec, 3, 22050.0, 0.0); +// assert_eq!( +// avg_fft_freqs(4.0, &[100, 1000, 4000, 12000, 22050], &fft[..]), vec![ +// (0, 0), (100, 0), (1000, 0), (4000, 0), (12000, 0), +// ]); +// +// // High Pass TPT @ 0Hz +// let fft = fft_with_freq_res_type(&mut matrix, &mut node_exec, 3, 0.0, 0.0); +// assert_eq!( +// avg_fft_freqs(4.0, &[100, 1000, 4000, 12000, 22050], &fft[..]), vec![ +// (0, 24), (100, 16), (1000, 16), (4000, 16), (12000, 16), +// ]); +}