Skip to main content

maolan_engine/
modulator.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
4pub enum ModulatorController {
5    Volume,
6    Balance,
7}
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
10pub enum ModulatorShape {
11    #[default]
12    Sine,
13    Triangle,
14    Saw,
15    Square,
16    SampleHold,
17}
18
19impl std::fmt::Display for ModulatorShape {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        match self {
22            Self::Sine => write!(f, "Sine"),
23            Self::Triangle => write!(f, "Triangle"),
24            Self::Saw => write!(f, "Saw"),
25            Self::Square => write!(f, "Square"),
26            Self::SampleHold => write!(f, "Sample & Hold"),
27        }
28    }
29}
30
31#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
32pub enum ModulatorTarget {
33    TrackVolume {
34        track_name: String,
35        min: f32,
36        max: f32,
37    },
38    TrackBalance {
39        track_name: String,
40        min: f32,
41        max: f32,
42    },
43    HwOutVolume {
44        min: f32,
45        max: f32,
46    },
47    HwOutBalance {
48        min: f32,
49        max: f32,
50    },
51    ClapParameter {
52        track_name: String,
53        instance_id: usize,
54        param_id: u32,
55        min: f64,
56        max: f64,
57    },
58    Vst3Parameter {
59        track_name: String,
60        instance_id: usize,
61        param_id: u32,
62        min: f32,
63        max: f32,
64    },
65    #[cfg(all(unix, not(target_os = "macos")))]
66    Lv2Parameter {
67        track_name: String,
68        instance_id: usize,
69        index: u32,
70        min: f32,
71        max: f32,
72    },
73    MidiCc {
74        track_name: String,
75        channel: u8,
76        cc: u8,
77    },
78}
79
80impl ModulatorTarget {
81    pub fn track_name(&self) -> Option<&str> {
82        match self {
83            Self::TrackVolume { track_name, .. }
84            | Self::TrackBalance { track_name, .. }
85            | Self::ClapParameter { track_name, .. }
86            | Self::Vst3Parameter { track_name, .. }
87            | Self::MidiCc { track_name, .. } => Some(track_name),
88            #[cfg(all(unix, not(target_os = "macos")))]
89            Self::Lv2Parameter { track_name, .. } => Some(track_name),
90            Self::HwOutVolume { .. } | Self::HwOutBalance { .. } => None,
91        }
92    }
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct Modulator {
97    pub id: usize,
98    pub name: String,
99    pub shape: ModulatorShape,
100    pub rate_hz: f32,
101    pub phase: f32,
102    pub enabled: bool,
103    pub targets: Vec<ModulatorTarget>,
104}
105
106/// Map a normalized modulator value in `[0, 1]` to a target range.
107/// When `min <= max` the output increases with the modulator value.
108/// When `min > max` the output is reversed (decreases as the modulator value increases).
109pub fn map_value(value: f32, min: f32, max: f32) -> f32 {
110    if min <= max {
111        (min + value * (max - min)).clamp(min, max)
112    } else {
113        (max + (1.0 - value) * (min - max)).clamp(max, min)
114    }
115}
116
117/// `map_value` for `f64` target ranges.
118pub fn map_value_f64(value: f32, min: f64, max: f64) -> f64 {
119    let value = f64::from(value);
120    if min <= max {
121        (min + value * (max - min)).clamp(min, max)
122    } else {
123        (max + (1.0 - value) * (min - max)).clamp(max, min)
124    }
125}
126
127impl Modulator {
128    pub fn new(id: usize) -> Self {
129        Self {
130            id,
131            name: format!("Modulator {id}"),
132            shape: ModulatorShape::default(),
133            rate_hz: 1.0,
134            phase: 0.0,
135            enabled: true,
136            targets: Vec::new(),
137        }
138    }
139
140    /// Evaluate the modulator at a given transport sample and sample rate.
141    /// Returns a normalized value in `[0, 1]`.
142    pub fn value_at(&self, sample: usize, sample_rate: f64) -> f32 {
143        let cycles = sample as f64 / sample_rate * self.rate_hz as f64 + self.phase as f64;
144        let phase = cycles.rem_euclid(1.0) as f32;
145        let raw = match self.shape {
146            ModulatorShape::Sine => (phase * 2.0 * std::f32::consts::PI).sin(),
147            ModulatorShape::Triangle => {
148                if phase < 0.5 {
149                    4.0 * phase - 1.0
150                } else {
151                    3.0 - 4.0 * phase
152                }
153            }
154            ModulatorShape::Saw => 2.0 * phase - 1.0,
155            ModulatorShape::Square => {
156                if phase < 0.5 {
157                    1.0
158                } else {
159                    -1.0
160                }
161            }
162            ModulatorShape::SampleHold => {
163                let step = (phase * 16.0).floor() as i32;
164                let mut hasher = std::collections::hash_map::DefaultHasher::new();
165                use std::hash::{Hash, Hasher};
166                step.hash(&mut hasher);
167                let h = hasher.finish();
168                ((h as f32 / u64::MAX as f32) * 2.0) - 1.0
169            }
170        };
171        ((raw + 1.0) / 2.0).clamp(0.0, 1.0)
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn map_value_maps_forward_range() {
181        assert!((map_value(0.0, 0.0, 100.0) - 0.0).abs() < f32::EPSILON);
182        assert!((map_value(1.0, 0.0, 100.0) - 100.0).abs() < f32::EPSILON);
183        assert!((map_value(0.5, 0.0, 100.0) - 50.0).abs() < f32::EPSILON);
184    }
185
186    #[test]
187    fn map_value_reverses_when_min_greater_than_max() {
188        assert!((map_value(0.0, 100.0, 0.0) - 100.0).abs() < f32::EPSILON);
189        assert!((map_value(1.0, 100.0, 0.0) - 0.0).abs() < f32::EPSILON);
190        assert!((map_value(0.5, 100.0, 0.0) - 50.0).abs() < f32::EPSILON);
191    }
192
193    #[test]
194    fn map_value_clamps_out_of_range() {
195        assert!((map_value(-0.5, 0.0, 100.0) - 0.0).abs() < f32::EPSILON);
196        assert!((map_value(1.5, 0.0, 100.0) - 100.0).abs() < f32::EPSILON);
197        assert!((map_value(-0.5, 100.0, 0.0) - 100.0).abs() < f32::EPSILON);
198        assert!((map_value(1.5, 100.0, 0.0) - 0.0).abs() < f32::EPSILON);
199    }
200
201    #[test]
202    fn map_value_f64_reverses_when_min_greater_than_max() {
203        assert!((map_value_f64(0.0, 1.0, 0.0) - 1.0).abs() < f64::EPSILON);
204        assert!((map_value_f64(1.0, 1.0, 0.0) - 0.0).abs() < f64::EPSILON);
205        assert!((map_value_f64(0.5, 1.0, 0.0) - 0.5).abs() < f64::EPSILON);
206    }
207}