1use crate::error::{PulseError, PulseResult};
2
3const MAX_RHYTHM_STEPS: usize = 1_000_000;
4
5pub fn euclidean(pulses: usize, steps: usize) -> Vec<usize> {
7 tunes::sequences::euclidean::generate(pulses, steps)
8}
9
10pub fn try_euclidean(pulses: usize, steps: usize) -> PulseResult<Vec<usize>> {
16 validate_rhythm_steps(steps)?;
17 Ok(euclidean(pulses, steps))
18}
19
20pub fn euclidean_pattern(pulses: usize, steps: usize) -> Vec<u32> {
22 tunes::sequences::euclidean::pattern(pulses, steps)
23}
24
25pub fn try_euclidean_pattern(pulses: usize, steps: usize) -> PulseResult<Vec<u32>> {
31 validate_rhythm_steps(steps)?;
32 Ok(euclidean_pattern(pulses, steps))
33}
34
35pub 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
47pub 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}