Skip to main content

mlua_pulse/
rhythm.rs

1use crate::error::{PulseError, PulseResult};
2
3const MAX_RHYTHM_STEPS: usize = 1_000_000;
4
5/// Generates Euclidean rhythm step indices.
6pub fn euclidean(pulses: usize, steps: usize) -> Vec<usize> {
7    tunes::sequences::euclidean::generate(pulses, steps)
8}
9
10/// Generates Euclidean rhythm step indices after validating allocation size.
11///
12/// # Errors
13///
14/// Returns an error when `steps` is too large to allocate safely.
15pub fn try_euclidean(pulses: usize, steps: usize) -> PulseResult<Vec<usize>> {
16    validate_rhythm_steps(steps)?;
17    Ok(euclidean(pulses, steps))
18}
19
20/// Generates a binary Euclidean rhythm pattern.
21pub fn euclidean_pattern(pulses: usize, steps: usize) -> Vec<u32> {
22    tunes::sequences::euclidean::pattern(pulses, steps)
23}
24
25/// Generates a binary Euclidean rhythm pattern after validating allocation size.
26///
27/// # Errors
28///
29/// Returns an error when `steps` is too large to allocate safely.
30pub fn try_euclidean_pattern(pulses: usize, steps: usize) -> PulseResult<Vec<u32>> {
31    validate_rhythm_steps(steps)?;
32    Ok(euclidean_pattern(pulses, steps))
33}
34
35/// Resolves stable named rhythm patterns.
36pub fn named_pattern(name: &str) -> PulseResult<Vec<usize>> {
37    match normalize_name(name).as_str() {
38        "fouronfloor" | "kickfourfloor" => Ok(tunes::sequences::euclidean::kick_four_floor()),
39        "tresillo" => Ok(tunes::sequences::euclidean::tresillo()),
40        "cinquillo" => Ok(tunes::sequences::euclidean::cinquillo()),
41        _ => Err(PulseError::InvalidRhythmPattern {
42            value: name.to_string(),
43        }),
44    }
45}
46
47/// Converts a drum pattern string into zero-based step indices.
48pub fn steps_from_pattern_string(pattern: &str) -> Vec<usize> {
49    pattern
50        .chars()
51        .enumerate()
52        .filter_map(|(index, character)| {
53            matches!(character, 'x' | 'X' | '1' | '*').then_some(index)
54        })
55        .collect()
56}
57
58fn normalize_name(value: &str) -> String {
59    value
60        .trim()
61        .to_ascii_lowercase()
62        .replace(['_', '-', ' '], "")
63}
64
65fn validate_rhythm_steps(steps: usize) -> PulseResult<()> {
66    if steps <= MAX_RHYTHM_STEPS {
67        Ok(())
68    } else {
69        Err(PulseError::InvalidDrumGridSteps { steps })
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn wraps_euclidean_generation() {
79        assert_eq!(euclidean(4, 16), vec![0, 4, 8, 12]);
80        assert_eq!(euclidean_pattern(4, 8).len(), 8);
81    }
82
83    #[test]
84    fn try_euclidean_rejects_unrenderable_step_counts() {
85        let error = try_euclidean(4, usize::MAX)
86            .expect_err("oversized Euclidean rhythm should fail before allocation");
87
88        assert!(matches!(error, PulseError::InvalidDrumGridSteps { .. }));
89    }
90
91    #[test]
92    fn resolves_named_patterns() {
93        assert_eq!(named_pattern("four_on_floor").unwrap(), vec![0, 4, 8, 12]);
94        assert_eq!(named_pattern("tresillo").unwrap(), vec![0, 2, 5]);
95        assert_eq!(named_pattern("cinquillo").unwrap(), vec![0, 2, 3, 5, 7]);
96    }
97
98    #[test]
99    fn parses_pattern_strings_to_zero_based_steps() {
100        assert_eq!(steps_from_pattern_string("x---x---"), vec![0, 4]);
101        assert_eq!(steps_from_pattern_string("xX1*----"), vec![0, 1, 2, 3]);
102        assert!(steps_from_pattern_string("--------").is_empty());
103    }
104}