use nih_plug::prelude::Enum;
use serde::{Deserialize, Serialize};
use std::f32::consts::PI;
// Modified implementation from https://www.musicdsp.org/en/latest/Filters/23-state-variable.html and some tweaks
// Adapted to rust by Ardura
#[derive(Enum, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub enum ResonanceType {
// Allegedly the "ideal" response when tying Q to angular sin response
Default,
// Allegedly a Moog Ladder Q approximation further modified
Moog,
// Allegedly an approximation of a TB-303 LP further modified
TB,
// Allegedly an approximation of an Arp 2600 further modified
Arp,
// I made this up - kind of a hyper resonance while still being gentle
Res,
// I made this up - Gentle bump - kind of Arp-ey
Bump,
// I made this up - Curve based on powf behavior
Powf,
}
#[derive(Clone)]
pub struct StateVariableFilter {
sample_rate: f32,
frequency: f32,
q: f32,
low_output: f32,
band_output: f32,
high_output: f32,
res_mode: ResonanceType,
oversample: i32,
}
impl Default for StateVariableFilter {
fn default() -> Self {
Self {
sample_rate: 44100.0,
q: 0.0,
frequency: 20000.0,
low_output: 0.0,
band_output: 0.0,
high_output: 0.0,
res_mode: ResonanceType::Default,
oversample: 4,
}
}
}
impl StateVariableFilter {
pub fn set_oversample(mut self, oversample_amount: i32) -> Self {
self.oversample = oversample_amount;
self
}
pub fn update(
&mut self,
frequency: f32,
q: f32,
sample_rate: f32,
resonance_mode: ResonanceType,
) {
if sample_rate != self.sample_rate {
self.sample_rate = sample_rate;
}
if q != self.q {
// This section tames instability brought from Q changes in different resonance modes
match resonance_mode {
ResonanceType::Default | ResonanceType::Bump => {
self.q = q.clamp(0.13, 1.0);
}
ResonanceType::Moog | ResonanceType::TB | ResonanceType::Arp => {
self.q = q.clamp(0.0, 1.0);
}
ResonanceType::Res | ResonanceType::Powf => {
self.q = q.clamp(0.0, 1.0);
}
}
}
if frequency != self.frequency {
//self.frequency = frequency.clamp(20.0, 16000.0);
self.frequency = frequency.clamp(20.0, 20000.0);
}
if resonance_mode != self.res_mode {
self.res_mode = resonance_mode;
}
}
pub fn process(&mut self, input: f32) -> (f32, f32, f32) {
// Calculate our normalized freq for filtering
let normalized_freq: f32 = match self.res_mode {
ResonanceType::Default => (2.0 * PI * self.frequency) / (self.sample_rate * 4.0),
ResonanceType::Moog => (2.0 * PI * self.frequency) / (self.sample_rate * 0.5),
ResonanceType::TB => (2.0 * PI * self.frequency) / (self.sample_rate * 0.5),
ResonanceType::Arp => (2.0 * PI * self.frequency) / (self.sample_rate * 0.5),
// Actuate v1.0.2 additions
ResonanceType::Res => (2.0 * PI * self.frequency) / (self.sample_rate * 0.5),
ResonanceType::Bump => (2.0 * PI * self.frequency) / (self.sample_rate * 4.0),
ResonanceType::Powf => (2.0 * PI * self.frequency) / (self.sample_rate * 4.0),
};
// Calculate our resonance coefficient
// This is here to save calls during filter sweeps even though a static filter will use more resources this way
let resonance = match self.res_mode {
ResonanceType::Default => (normalized_freq / (2.0 * self.q)).sin(),
// These are all approximations I found then modified - I'm not claiming any accuracy - more like inspiration
ResonanceType::Moog => {
let resonance_exp = 16.0 * PI * self.q - 2.0;
resonance_exp * (2.0 * PI * normalized_freq / self.sample_rate)
}
ResonanceType::TB => {
let resonance_exp = 8.0 * PI * self.q;
resonance_exp * (PI * normalized_freq / self.sample_rate).tan()
}
ResonanceType::Arp => {
let resonance_exp = 2.0 * PI * self.q + 0.3;
resonance_exp * (2.0 * PI * normalized_freq / self.sample_rate)
}
// Actuate v1.0.2 additions
// These ones I have made based off other ideas
ResonanceType::Res => {
let resonance_exp = (2.0 * PI * self.q).powf(0.9);
resonance_exp * (2.0 * PI * normalized_freq / self.sample_rate).tan()
}
ResonanceType::Bump => {
let resonance_exp = self.q * (normalized_freq / (2.0 * self.q)).sin();
resonance_exp
* (normalized_freq / (2.0 * (self.q + 0.001))).asinh()
* (self.q + 0.001).sin()
}
ResonanceType::Powf => {
let resonance_exp = (2.0 * PI * self.q).powf(0.4) + 0.001;
(resonance_exp * (2.0 * PI * normalized_freq / (2.0 * resonance_exp)).sin()).tanh()
}
};
// Oversample by running multiple iterations
for _ in 0..self.oversample {
self.low_output += normalized_freq * self.band_output;
self.high_output = input - self.low_output - self.q * self.band_output;
self.band_output += resonance * self.high_output;
self.low_output += resonance * self.band_output;
}
(self.low_output, self.band_output, self.high_output)
}
}