onedrop_engine/
beat_detection.rs

1//! Beat detection for automatic preset changing.
2
3use std::time::{Duration, Instant};
4
5/// Beat detection mode (inspired by MilkDrop3).
6#[derive(Debug, Clone, PartialEq)]
7pub enum BeatDetectionMode {
8    /// No beat detection
9    Off,
10
11    /// HardCut1: Load new preset if bass > 1.5 with minimum delay of 0.2s
12    HardCut1,
13
14    /// HardCut2: Load new preset if treb > 2.9 with minimum delay of 0.5s
15    HardCut2,
16
17    /// HardCut3: Load new preset if treb > 2.9 with minimum delay of 1s
18    HardCut3,
19
20    /// HardCut4: Load new preset if treb > 2.9 with minimum delay of 3s,
21    /// or immediately if treb > 8
22    HardCut4,
23
24    /// HardCut5: Load new preset if treb > 2.9 with minimum delay of 5s
25    HardCut5,
26
27    /// HardCut6: Load new preset if bass > 1.5,
28    /// and load special preset if bass > 4.90
29    HardCut6 { special_preset: String },
30}
31
32impl BeatDetectionMode {
33    /// Get the mode name.
34    pub fn name(&self) -> &'static str {
35        match self {
36            Self::Off => "Off",
37            Self::HardCut1 => "HardCut1",
38            Self::HardCut2 => "HardCut2",
39            Self::HardCut3 => "HardCut3",
40            Self::HardCut4 => "HardCut4",
41            Self::HardCut5 => "HardCut5",
42            Self::HardCut6 { .. } => "HardCut6",
43        }
44    }
45
46    /// Get the next mode (for cycling through modes).
47    pub fn next(&self) -> Self {
48        match self {
49            Self::Off => Self::HardCut1,
50            Self::HardCut1 => Self::HardCut2,
51            Self::HardCut2 => Self::HardCut3,
52            Self::HardCut3 => Self::HardCut4,
53            Self::HardCut4 => Self::HardCut5,
54            Self::HardCut5 => Self::HardCut6 {
55                special_preset: "Bass/WHITE.milk".to_string(),
56            },
57            Self::HardCut6 { .. } => Self::Off,
58        }
59    }
60}
61
62/// Type of preset change to trigger.
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum PresetChange {
65    /// Load a random preset
66    Random,
67
68    /// Load a specific preset
69    Specific(String),
70}
71
72/// Beat detector for automatic preset changing.
73#[derive(Debug, Clone)]
74pub struct BeatDetector {
75    /// Current detection mode
76    mode: BeatDetectionMode,
77
78    /// Last time a preset change was triggered
79    last_trigger: Option<Instant>,
80
81    /// Enable/disable detection
82    enabled: bool,
83}
84
85impl BeatDetector {
86    /// Create a new beat detector.
87    pub fn new() -> Self {
88        Self {
89            mode: BeatDetectionMode::Off,
90            last_trigger: None,
91            enabled: false,
92        }
93    }
94
95    /// Create a beat detector with specific mode.
96    pub fn with_mode(mode: BeatDetectionMode) -> Self {
97        let enabled = mode != BeatDetectionMode::Off;
98        Self {
99            mode,
100            last_trigger: None,
101            enabled,
102        }
103    }
104
105    /// Set the detection mode.
106    pub fn set_mode(&mut self, mode: BeatDetectionMode) {
107        self.enabled = mode != BeatDetectionMode::Off;
108        self.mode = mode;
109    }
110
111    /// Get the current mode.
112    pub fn mode(&self) -> &BeatDetectionMode {
113        &self.mode
114    }
115
116    /// Cycle to the next mode.
117    pub fn next_mode(&mut self) {
118        self.mode = self.mode.next();
119        self.enabled = self.mode != BeatDetectionMode::Off;
120    }
121
122    /// Enable detection.
123    pub fn enable(&mut self) {
124        self.enabled = true;
125    }
126
127    /// Disable detection.
128    pub fn disable(&mut self) {
129        self.enabled = false;
130    }
131
132    /// Check if detection is enabled.
133    pub fn is_enabled(&self) -> bool {
134        self.enabled
135    }
136
137    /// Check if a preset change should be triggered based on audio levels.
138    pub fn should_change_preset(
139        &mut self,
140        bass: f32,
141        _mid: f32,
142        treb: f32,
143    ) -> Option<PresetChange> {
144        if !self.enabled || self.mode == BeatDetectionMode::Off {
145            return None;
146        }
147
148        let now = Instant::now();
149
150        // Check if minimum delay has passed
151        let can_trigger = match self.last_trigger {
152            None => true,
153            Some(last) => {
154                let min_delay = self.get_min_delay();
155                now.duration_since(last) >= min_delay
156            }
157        };
158
159        // Check conditions based on mode
160        let change = match self.mode {
161            BeatDetectionMode::Off => None,
162
163            BeatDetectionMode::HardCut1 => {
164                if bass > 1.5 && can_trigger {
165                    Some(PresetChange::Random)
166                } else {
167                    None
168                }
169            }
170
171            BeatDetectionMode::HardCut2 => {
172                if treb > 2.9 && can_trigger {
173                    Some(PresetChange::Random)
174                } else {
175                    None
176                }
177            }
178
179            BeatDetectionMode::HardCut3 => {
180                if treb > 2.9 && can_trigger {
181                    Some(PresetChange::Random)
182                } else {
183                    None
184                }
185            }
186
187            BeatDetectionMode::HardCut4 => {
188                if treb > 8.0 {
189                    // Immediate trigger on very high treble
190                    Some(PresetChange::Random)
191                } else if treb > 2.9 && can_trigger {
192                    Some(PresetChange::Random)
193                } else {
194                    None
195                }
196            }
197
198            BeatDetectionMode::HardCut5 => {
199                if treb > 2.9 && can_trigger {
200                    Some(PresetChange::Random)
201                } else {
202                    None
203                }
204            }
205
206            BeatDetectionMode::HardCut6 { ref special_preset } => {
207                if bass > 4.90 {
208                    // Load special preset on very high bass
209                    Some(PresetChange::Specific(special_preset.clone()))
210                } else if bass > 1.5 && can_trigger {
211                    Some(PresetChange::Random)
212                } else {
213                    None
214                }
215            }
216        };
217
218        // Update last trigger time if change was triggered
219        if change.is_some() {
220            self.last_trigger = Some(now);
221        }
222
223        change
224    }
225
226    /// Get the minimum delay for the current mode.
227    fn get_min_delay(&self) -> Duration {
228        match self.mode {
229            BeatDetectionMode::Off => Duration::from_secs(0),
230            BeatDetectionMode::HardCut1 => Duration::from_millis(200),
231            BeatDetectionMode::HardCut2 => Duration::from_millis(500),
232            BeatDetectionMode::HardCut3 => Duration::from_secs(1),
233            BeatDetectionMode::HardCut4 => Duration::from_secs(3),
234            BeatDetectionMode::HardCut5 => Duration::from_secs(5),
235            BeatDetectionMode::HardCut6 { .. } => Duration::from_millis(200),
236        }
237    }
238}
239
240impl Default for BeatDetector {
241    fn default() -> Self {
242        Self::new()
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249    use std::thread;
250
251    #[test]
252    fn test_beat_detector_off() {
253        let mut detector = BeatDetector::new();
254
255        assert_eq!(*detector.mode(), BeatDetectionMode::Off);
256        assert!(!detector.is_enabled());
257
258        let change = detector.should_change_preset(2.0, 1.0, 3.0);
259        assert_eq!(change, None);
260    }
261
262    #[test]
263    fn test_hardcut1_bass_threshold() {
264        let mut detector = BeatDetector::with_mode(BeatDetectionMode::HardCut1);
265
266        // Below threshold
267        let change = detector.should_change_preset(1.0, 0.5, 0.5);
268        assert_eq!(change, None);
269
270        // Above threshold
271        let change = detector.should_change_preset(2.0, 0.5, 0.5);
272        assert_eq!(change, Some(PresetChange::Random));
273    }
274
275    #[test]
276    fn test_hardcut2_treb_threshold() {
277        let mut detector = BeatDetector::with_mode(BeatDetectionMode::HardCut2);
278
279        // Below threshold
280        let change = detector.should_change_preset(0.5, 0.5, 2.0);
281        assert_eq!(change, None);
282
283        // Above threshold
284        let change = detector.should_change_preset(0.5, 0.5, 3.5);
285        assert_eq!(change, Some(PresetChange::Random));
286    }
287
288    #[test]
289    fn test_hardcut4_immediate_trigger() {
290        let mut detector = BeatDetector::with_mode(BeatDetectionMode::HardCut4);
291
292        // Trigger once
293        let change = detector.should_change_preset(0.5, 0.5, 3.0);
294        assert_eq!(change, Some(PresetChange::Random));
295
296        // Should not trigger again immediately (min delay not passed)
297        let change = detector.should_change_preset(0.5, 0.5, 3.0);
298        assert_eq!(change, None);
299
300        // But should trigger immediately on very high treble
301        let change = detector.should_change_preset(0.5, 0.5, 9.0);
302        assert_eq!(change, Some(PresetChange::Random));
303    }
304
305    #[test]
306    fn test_hardcut6_special_preset() {
307        let mut detector = BeatDetector::with_mode(BeatDetectionMode::HardCut6 {
308            special_preset: "Bass/WHITE.milk".to_string(),
309        });
310
311        // Normal bass
312        let change = detector.should_change_preset(2.0, 0.5, 0.5);
313        assert_eq!(change, Some(PresetChange::Random));
314
315        // Very high bass - should load special preset
316        let change = detector.should_change_preset(5.0, 0.5, 0.5);
317        assert_eq!(
318            change,
319            Some(PresetChange::Specific("Bass/WHITE.milk".to_string()))
320        );
321    }
322
323    #[test]
324    fn test_min_delay() {
325        let mut detector = BeatDetector::with_mode(BeatDetectionMode::HardCut1);
326
327        // First trigger
328        let change = detector.should_change_preset(2.0, 0.5, 0.5);
329        assert_eq!(change, Some(PresetChange::Random));
330
331        // Immediate second trigger - should be blocked
332        let change = detector.should_change_preset(2.0, 0.5, 0.5);
333        assert_eq!(change, None);
334
335        // Wait for min delay
336        thread::sleep(Duration::from_millis(250));
337
338        // Should trigger again
339        let change = detector.should_change_preset(2.0, 0.5, 0.5);
340        assert_eq!(change, Some(PresetChange::Random));
341    }
342
343    #[test]
344    fn test_mode_cycling() {
345        let mut detector = BeatDetector::new();
346
347        assert_eq!(*detector.mode(), BeatDetectionMode::Off);
348
349        detector.next_mode();
350        assert_eq!(*detector.mode(), BeatDetectionMode::HardCut1);
351
352        detector.next_mode();
353        assert_eq!(*detector.mode(), BeatDetectionMode::HardCut2);
354
355        detector.next_mode();
356        assert_eq!(*detector.mode(), BeatDetectionMode::HardCut3);
357
358        detector.next_mode();
359        assert_eq!(*detector.mode(), BeatDetectionMode::HardCut4);
360
361        detector.next_mode();
362        assert_eq!(*detector.mode(), BeatDetectionMode::HardCut5);
363
364        detector.next_mode();
365        assert!(matches!(
366            detector.mode(),
367            BeatDetectionMode::HardCut6 { .. }
368        ));
369
370        detector.next_mode();
371        assert_eq!(*detector.mode(), BeatDetectionMode::Off);
372    }
373}