Skip to main content

aether_nodes/
formant.rs

1//! Formant filter — shapes audio to sound like a vowel.
2//!
3//! Essential for Ney, Bansuri, and any wind instrument synthesis.
4//! Uses three parallel bandpass filters tuned to vowel formant frequencies.
5//!
6//! Param layout:
7//!   0 = vowel  (0=A, 1=E, 2=I, 3=O, 4=U, fractional = morph between vowels)
8//!   1 = shift  (semitones, -12 to +12, transposes all formants)
9//!   2 = wet    (0.0 – 1.0)
10
11use aether_core::{node::DspNode, param::ParamBlock, BUFFER_SIZE, MAX_INPUTS};
12
13/// Formant frequencies (Hz) for each vowel: [F1, F2, F3]
14/// Based on average male voice formants.
15const VOWEL_FORMANTS: [[f32; 3]; 5] = [
16    [800.0, 1200.0, 2500.0], // A
17    [400.0, 2000.0, 2800.0], // E
18    [350.0, 2800.0, 3300.0], // I
19    [450.0, 800.0, 2830.0],  // O
20    [325.0, 700.0, 2700.0],  // U
21];
22
23/// Bandwidths (Hz) for each formant
24const FORMANT_BW: [f32; 3] = [80.0, 90.0, 120.0];
25
26struct BandpassFilter {
27    x1: f32,
28    x2: f32,
29    y1: f32,
30    y2: f32,
31}
32
33impl BandpassFilter {
34    fn new() -> Self {
35        Self {
36            x1: 0.0,
37            x2: 0.0,
38            y1: 0.0,
39            y2: 0.0,
40        }
41    }
42
43    #[inline(always)]
44    fn process(&mut self, input: f32, freq: f32, bw: f32, sr: f32) -> f32 {
45        let r = 1.0 - std::f32::consts::PI * bw / sr;
46        let cos_w = 2.0 * r * (2.0 * std::f32::consts::PI * freq / sr).cos();
47        let a0 = 1.0 - r * r;
48        let y = a0 * input + cos_w * self.y1 - r * r * self.y2;
49        self.y2 = self.y1;
50        self.y1 = y;
51        self.x2 = self.x1;
52        self.x1 = input;
53        y
54    }
55}
56
57pub struct FormantFilter {
58    bp: [[BandpassFilter; 3]; 2], // two sets for morphing
59}
60
61impl FormantFilter {
62    pub fn new() -> Self {
63        Self {
64            bp: [
65                [
66                    BandpassFilter::new(),
67                    BandpassFilter::new(),
68                    BandpassFilter::new(),
69                ],
70                [
71                    BandpassFilter::new(),
72                    BandpassFilter::new(),
73                    BandpassFilter::new(),
74                ],
75            ],
76        }
77    }
78
79    fn get_formants(vowel_idx: usize, shift_semitones: f32) -> [f32; 3] {
80        let v = vowel_idx.min(4);
81        let shift_ratio = 2.0f32.powf(shift_semitones / 12.0);
82        [
83            VOWEL_FORMANTS[v][0] * shift_ratio,
84            VOWEL_FORMANTS[v][1] * shift_ratio,
85            VOWEL_FORMANTS[v][2] * shift_ratio,
86        ]
87    }
88}
89
90impl Default for FormantFilter {
91    fn default() -> Self {
92        Self::new()
93    }
94}
95
96impl DspNode for FormantFilter {
97    fn process(
98        &mut self,
99        inputs: &[Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS],
100        output: &mut [f32; BUFFER_SIZE],
101        params: &mut ParamBlock,
102        sample_rate: f32,
103    ) {
104        let silence = [0.0f32; BUFFER_SIZE];
105        let input = inputs[0].unwrap_or(&silence);
106
107        for (i, out) in output.iter_mut().enumerate() {
108            let vowel_f = params.get(0).current.clamp(0.0, 4.0);
109            let shift = params.get(1).current.clamp(-12.0, 12.0);
110            let wet = params.get(2).current.clamp(0.0, 1.0);
111
112            let v0 = vowel_f as usize;
113            let v1 = (v0 + 1).min(4);
114            let frac = vowel_f.fract();
115
116            let f0 = Self::get_formants(v0, shift);
117            let f1 = Self::get_formants(v1, shift);
118
119            // Process through two sets of formant filters and interpolate
120            let mut wet_signal = 0.0f32;
121            for k in 0..3 {
122                let freq0 = f0[k] * (1.0 - frac) + f1[k] * frac;
123                wet_signal += self.bp[0][k].process(input[i], freq0, FORMANT_BW[k], sample_rate);
124            }
125            wet_signal *= 0.333; // normalize 3 filters
126
127            *out = input[i] * (1.0 - wet) + wet_signal * wet;
128            params.tick_all();
129        }
130    }
131
132    fn type_name(&self) -> &'static str {
133        "FormantFilter"
134    }
135}