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, x2: f32,
28    y1: f32, y2: f32,
29}
30
31impl BandpassFilter {
32    fn new() -> Self { Self { x1: 0.0, x2: 0.0, y1: 0.0, y2: 0.0 } }
33
34    #[inline(always)]
35    fn process(&mut self, input: f32, freq: f32, bw: f32, sr: f32) -> f32 {
36        let r = 1.0 - std::f32::consts::PI * bw / sr;
37        let cos_w = 2.0 * r * (2.0 * std::f32::consts::PI * freq / sr).cos();
38        let a0 = 1.0 - r * r;
39        let y = a0 * input + cos_w * self.y1 - r * r * self.y2;
40        self.y2 = self.y1;
41        self.y1 = y;
42        self.x2 = self.x1;
43        self.x1 = input;
44        y
45    }
46}
47
48pub struct FormantFilter {
49    bp: [[BandpassFilter; 3]; 2], // two sets for morphing
50}
51
52impl FormantFilter {
53    pub fn new() -> Self {
54        Self {
55            bp: [
56                [BandpassFilter::new(), BandpassFilter::new(), BandpassFilter::new()],
57                [BandpassFilter::new(), BandpassFilter::new(), BandpassFilter::new()],
58            ],
59        }
60    }
61
62    fn get_formants(vowel_idx: usize, shift_semitones: f32) -> [f32; 3] {
63        let v = vowel_idx.min(4);
64        let shift_ratio = 2.0f32.powf(shift_semitones / 12.0);
65        [
66            VOWEL_FORMANTS[v][0] * shift_ratio,
67            VOWEL_FORMANTS[v][1] * shift_ratio,
68            VOWEL_FORMANTS[v][2] * shift_ratio,
69        ]
70    }
71}
72
73impl Default for FormantFilter {
74    fn default() -> Self { Self::new() }
75}
76
77impl DspNode for FormantFilter {
78    fn process(
79        &mut self,
80        inputs: &[Option<&[f32; BUFFER_SIZE]>; MAX_INPUTS],
81        output: &mut [f32; BUFFER_SIZE],
82        params: &mut ParamBlock,
83        sample_rate: f32,
84    ) {
85        let silence = [0.0f32; BUFFER_SIZE];
86        let input = inputs[0].unwrap_or(&silence);
87
88        for (i, out) in output.iter_mut().enumerate() {
89            let vowel_f = params.get(0).current.clamp(0.0, 4.0);
90            let shift   = params.get(1).current.clamp(-12.0, 12.0);
91            let wet     = params.get(2).current.clamp(0.0, 1.0);
92
93            let v0 = vowel_f as usize;
94            let v1 = (v0 + 1).min(4);
95            let frac = vowel_f.fract();
96
97            let f0 = Self::get_formants(v0, shift);
98            let f1 = Self::get_formants(v1, shift);
99
100            // Process through two sets of formant filters and interpolate
101            let mut wet_signal = 0.0f32;
102            for k in 0..3 {
103                let freq0 = f0[k] * (1.0 - frac) + f1[k] * frac;
104                wet_signal += self.bp[0][k].process(input[i], freq0, FORMANT_BW[k], sample_rate);
105            }
106            wet_signal *= 0.333; // normalize 3 filters
107
108            *out = input[i] * (1.0 - wet) + wet_signal * wet;
109            params.tick_all();
110        }
111    }
112
113    fn type_name(&self) -> &'static str { "FormantFilter" }
114}