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/// Musical note division used for tempo-synced modulator rates.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
33pub enum MusicalDivision {
34    Bar,
35    Half,
36    Beat,
37    Eighth,
38    Sixteenth,
39    ThirtySecond,
40    SixtyFourth,
41}
42
43impl MusicalDivision {
44    /// Convert this division to a frequency in Hz for the given tempo and time signature.
45    /// Bar length is computed from the time signature; beat refers to a quarter-note beat.
46    pub fn to_hz(self, bpm: f64, tsig_num: u16, tsig_denom: u16) -> f64 {
47        let beat_hz = bpm / 60.0;
48        let bar_hz = beat_hz / (tsig_num as f64 * 4.0 / tsig_denom.max(1) as f64);
49        match self {
50            Self::Bar => bar_hz,
51            Self::Half => beat_hz / 2.0,
52            Self::Beat => beat_hz,
53            Self::Eighth => beat_hz * 2.0,
54            Self::Sixteenth => beat_hz * 4.0,
55            Self::ThirtySecond => beat_hz * 8.0,
56            Self::SixtyFourth => beat_hz * 16.0,
57        }
58    }
59}
60
61impl std::fmt::Display for MusicalDivision {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        match self {
64            Self::Bar => write!(f, "1/1"),
65            Self::Half => write!(f, "1/2"),
66            Self::Beat => write!(f, "1/4"),
67            Self::Eighth => write!(f, "1/8"),
68            Self::Sixteenth => write!(f, "1/16"),
69            Self::ThirtySecond => write!(f, "1/32"),
70            Self::SixtyFourth => write!(f, "1/64"),
71        }
72    }
73}
74
75/// Modulator rate specified either as a fixed frequency or as a tempo-synced division.
76#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
77pub enum ModulatorRate {
78    Hz(f32),
79    Musical(MusicalDivision),
80}
81
82impl Default for ModulatorRate {
83    fn default() -> Self {
84        Self::Hz(1.0)
85    }
86}
87
88impl ModulatorRate {
89    /// Return the effective frequency in Hz for the given transport timing.
90    pub fn effective_hz(&self, bpm: f64, tsig_num: u16, tsig_denom: u16) -> f64 {
91        match *self {
92            Self::Hz(hz) => f64::from(hz),
93            Self::Musical(div) => div.to_hz(bpm, tsig_num, tsig_denom),
94        }
95    }
96}
97
98#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
99pub enum ModulatorTarget {
100    TrackVolume {
101        track_name: String,
102        min: f32,
103        max: f32,
104    },
105    TrackBalance {
106        track_name: String,
107        min: f32,
108        max: f32,
109    },
110    HwOutVolume {
111        min: f32,
112        max: f32,
113    },
114    HwOutBalance {
115        min: f32,
116        max: f32,
117    },
118    ClapParameter {
119        track_name: String,
120        instance_id: usize,
121        param_id: u32,
122        min: f64,
123        max: f64,
124    },
125    Vst3Parameter {
126        track_name: String,
127        instance_id: usize,
128        param_id: u32,
129        min: f32,
130        max: f32,
131    },
132    #[cfg(all(unix, not(target_os = "macos")))]
133    Lv2Parameter {
134        track_name: String,
135        instance_id: usize,
136        index: u32,
137        min: f32,
138        max: f32,
139    },
140    MidiCc {
141        track_name: String,
142        channel: u8,
143        cc: u8,
144    },
145}
146
147impl ModulatorTarget {
148    pub fn track_name(&self) -> Option<&str> {
149        match self {
150            Self::TrackVolume { track_name, .. }
151            | Self::TrackBalance { track_name, .. }
152            | Self::ClapParameter { track_name, .. }
153            | Self::Vst3Parameter { track_name, .. }
154            | Self::MidiCc { track_name, .. } => Some(track_name),
155            #[cfg(all(unix, not(target_os = "macos")))]
156            Self::Lv2Parameter { track_name, .. } => Some(track_name),
157            Self::HwOutVolume { .. } | Self::HwOutBalance { .. } => None,
158        }
159    }
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct Modulator {
164    pub id: usize,
165    pub name: String,
166    pub shape: ModulatorShape,
167    pub rate: ModulatorRate,
168    pub phase: f32,
169    pub enabled: bool,
170    pub targets: Vec<ModulatorTarget>,
171}
172
173/// Map a normalized modulator value in `[0, 1]` to a target range.
174/// When `min <= max` the output increases with the modulator value.
175/// When `min > max` the output is reversed (decreases as the modulator value increases).
176pub fn map_value(value: f32, min: f32, max: f32) -> f32 {
177    if min <= max {
178        (min + value * (max - min)).clamp(min, max)
179    } else {
180        (max + (1.0 - value) * (min - max)).clamp(max, min)
181    }
182}
183
184/// `map_value` for `f64` target ranges.
185pub fn map_value_f64(value: f32, min: f64, max: f64) -> f64 {
186    let value = f64::from(value);
187    if min <= max {
188        (min + value * (max - min)).clamp(min, max)
189    } else {
190        (max + (1.0 - value) * (min - max)).clamp(max, min)
191    }
192}
193
194impl Modulator {
195    pub fn new(id: usize) -> Self {
196        Self {
197            id,
198            name: format!("Modulator {id}"),
199            shape: ModulatorShape::default(),
200            rate: ModulatorRate::default(),
201            phase: 0.0,
202            enabled: true,
203            targets: Vec::new(),
204        }
205    }
206
207    /// Evaluate the modulator at a given transport sample, sample rate, and timing.
208    /// Returns a normalized value in `[0, 1]`.
209    pub fn value_at(
210        &self,
211        sample: usize,
212        sample_rate: f64,
213        bpm: f64,
214        tsig_num: u16,
215        tsig_denom: u16,
216    ) -> f32 {
217        let rate_hz = self.rate.effective_hz(bpm, tsig_num, tsig_denom);
218        let cycles = sample as f64 / sample_rate * rate_hz + self.phase as f64;
219        let phase = cycles.rem_euclid(1.0) as f32;
220        let raw = match self.shape {
221            ModulatorShape::Sine => (phase * 2.0 * std::f32::consts::PI).sin(),
222            ModulatorShape::Triangle => {
223                if phase < 0.5 {
224                    4.0 * phase - 1.0
225                } else {
226                    3.0 - 4.0 * phase
227                }
228            }
229            ModulatorShape::Saw => 2.0 * phase - 1.0,
230            ModulatorShape::Square => {
231                if phase < 0.5 {
232                    1.0
233                } else {
234                    -1.0
235                }
236            }
237            ModulatorShape::SampleHold => {
238                let step = (phase * 16.0).floor() as i32;
239                let mut hasher = std::collections::hash_map::DefaultHasher::new();
240                use std::hash::{Hash, Hasher};
241                step.hash(&mut hasher);
242                let h = hasher.finish();
243                ((h as f32 / u64::MAX as f32) * 2.0) - 1.0
244            }
245        };
246        ((raw + 1.0) / 2.0).clamp(0.0, 1.0)
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    #[test]
255    fn map_value_maps_forward_range() {
256        assert!((map_value(0.0, 0.0, 100.0) - 0.0).abs() < f32::EPSILON);
257        assert!((map_value(1.0, 0.0, 100.0) - 100.0).abs() < f32::EPSILON);
258        assert!((map_value(0.5, 0.0, 100.0) - 50.0).abs() < f32::EPSILON);
259    }
260
261    #[test]
262    fn map_value_reverses_when_min_greater_than_max() {
263        assert!((map_value(0.0, 100.0, 0.0) - 100.0).abs() < f32::EPSILON);
264        assert!((map_value(1.0, 100.0, 0.0) - 0.0).abs() < f32::EPSILON);
265        assert!((map_value(0.5, 100.0, 0.0) - 50.0).abs() < f32::EPSILON);
266    }
267
268    #[test]
269    fn map_value_clamps_out_of_range() {
270        assert!((map_value(-0.5, 0.0, 100.0) - 0.0).abs() < f32::EPSILON);
271        assert!((map_value(1.5, 0.0, 100.0) - 100.0).abs() < f32::EPSILON);
272        assert!((map_value(-0.5, 100.0, 0.0) - 100.0).abs() < f32::EPSILON);
273        assert!((map_value(1.5, 100.0, 0.0) - 0.0).abs() < f32::EPSILON);
274    }
275
276    #[test]
277    fn map_value_f64_reverses_when_min_greater_than_max() {
278        assert!((map_value_f64(0.0, 1.0, 0.0) - 1.0).abs() < f64::EPSILON);
279        assert!((map_value_f64(1.0, 1.0, 0.0) - 0.0).abs() < f64::EPSILON);
280        assert!((map_value_f64(0.5, 1.0, 0.0) - 0.5).abs() < f64::EPSILON);
281    }
282}