Skip to main content

embedded_audio/synth/
fm.rs

1use crate::fixed::{Phase, hz_to_phase_inc, lerp_i8, phase_index};
2use crate::synth::wavetable::SINE_TABLE;
3
4/// Tier A two-operator FM (modulator → carrier index).
5#[derive(Debug, Clone, Copy)]
6pub struct FmVoice {
7    carrier_phase: Phase,
8    carrier_inc: u32,
9    mod_phase: Phase,
10    mod_inc: u32,
11    mod_depth_q8: u8,
12    active: bool,
13}
14
15impl Default for FmVoice {
16    fn default() -> Self {
17        Self::new()
18    }
19}
20
21impl FmVoice {
22    pub const fn new() -> Self {
23        Self {
24            carrier_phase: 0,
25            carrier_inc: 0,
26            mod_phase: 0,
27            mod_inc: 0,
28            mod_depth_q8: 64,
29            active: false,
30        }
31    }
32
33    /// `mod_ratio_cent` is mod/carrier ratio × 100 (e.g. 200 = 2.00).
34    pub fn start(
35        &mut self,
36        carrier_hz: u32,
37        mod_ratio_cent: u16,
38        mod_depth_q8: u8,
39        sample_rate_hz: u32,
40    ) {
41        self.carrier_phase = 0;
42        self.mod_phase = 0;
43        self.carrier_inc = hz_to_phase_inc(carrier_hz, sample_rate_hz);
44        let mod_hz = (carrier_hz as u64 * mod_ratio_cent as u64) / 100;
45        self.mod_inc = hz_to_phase_inc(mod_hz as u32, sample_rate_hz);
46        self.mod_depth_q8 = mod_depth_q8;
47        self.active = true;
48    }
49
50    pub fn stop(&mut self) {
51        self.active = false;
52    }
53
54    pub fn is_active(&self) -> bool {
55        self.active
56    }
57
58    pub fn next_sample(&mut self) -> Option<i8> {
59        if !self.active {
60            return None;
61        }
62        let mod_idx = phase_index(self.mod_phase);
63        let mod_frac = (self.mod_phase >> 16) as u8;
64        let ma = SINE_TABLE[mod_idx as usize] as i8;
65        let mb = SINE_TABLE[mod_idx.wrapping_add(1) as usize] as i8;
66        let mod_sample = lerp_i8(ma, mb, mod_frac);
67
68        let mod_offset = ((mod_sample as i32 * self.mod_depth_q8 as i32) >> 8) as u8;
69        let car_idx = phase_index(self.carrier_phase.wrapping_add((mod_offset as u32) << 24));
70        let car_frac = (self.carrier_phase >> 16) as u8;
71        let ca = SINE_TABLE[car_idx as usize] as i8;
72        let cb = SINE_TABLE[car_idx.wrapping_add(1) as usize] as i8;
73        let sample = lerp_i8(ca, cb, car_frac);
74
75        self.carrier_phase = self.carrier_phase.wrapping_add(self.carrier_inc);
76        self.mod_phase = self.mod_phase.wrapping_add(self.mod_inc);
77        Some(sample)
78    }
79}