mlua-pulse 0.1.0

Lua-friendly music composition and audio export bindings built on tunes and mlua
Documentation
use crate::error::{PulseError, PulseResult};

const MAX_RHYTHM_STEPS: usize = 1_000_000;

/// Generates Euclidean rhythm step indices.
pub fn euclidean(pulses: usize, steps: usize) -> Vec<usize> {
    tunes::sequences::euclidean::generate(pulses, steps)
}

/// Generates Euclidean rhythm step indices after validating allocation size.
///
/// # Errors
///
/// Returns an error when `steps` is too large to allocate safely.
pub fn try_euclidean(pulses: usize, steps: usize) -> PulseResult<Vec<usize>> {
    validate_rhythm_steps(steps)?;
    Ok(euclidean(pulses, steps))
}

/// Generates a binary Euclidean rhythm pattern.
pub fn euclidean_pattern(pulses: usize, steps: usize) -> Vec<u32> {
    tunes::sequences::euclidean::pattern(pulses, steps)
}

/// Generates a binary Euclidean rhythm pattern after validating allocation size.
///
/// # Errors
///
/// Returns an error when `steps` is too large to allocate safely.
pub fn try_euclidean_pattern(pulses: usize, steps: usize) -> PulseResult<Vec<u32>> {
    validate_rhythm_steps(steps)?;
    Ok(euclidean_pattern(pulses, steps))
}

/// Resolves stable named rhythm patterns.
pub fn named_pattern(name: &str) -> PulseResult<Vec<usize>> {
    match normalize_name(name).as_str() {
        "fouronfloor" | "kickfourfloor" => Ok(tunes::sequences::euclidean::kick_four_floor()),
        "tresillo" => Ok(tunes::sequences::euclidean::tresillo()),
        "cinquillo" => Ok(tunes::sequences::euclidean::cinquillo()),
        _ => Err(PulseError::InvalidRhythmPattern {
            value: name.to_string(),
        }),
    }
}

/// Converts a drum pattern string into zero-based step indices.
pub fn steps_from_pattern_string(pattern: &str) -> Vec<usize> {
    pattern
        .chars()
        .enumerate()
        .filter_map(|(index, character)| {
            matches!(character, 'x' | 'X' | '1' | '*').then_some(index)
        })
        .collect()
}

fn normalize_name(value: &str) -> String {
    value
        .trim()
        .to_ascii_lowercase()
        .replace(['_', '-', ' '], "")
}

fn validate_rhythm_steps(steps: usize) -> PulseResult<()> {
    if steps <= MAX_RHYTHM_STEPS {
        Ok(())
    } else {
        Err(PulseError::InvalidDrumGridSteps { steps })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn wraps_euclidean_generation() {
        assert_eq!(euclidean(4, 16), vec![0, 4, 8, 12]);
        assert_eq!(euclidean_pattern(4, 8).len(), 8);
    }

    #[test]
    fn try_euclidean_rejects_unrenderable_step_counts() {
        let error = try_euclidean(4, usize::MAX)
            .expect_err("oversized Euclidean rhythm should fail before allocation");

        assert!(matches!(error, PulseError::InvalidDrumGridSteps { .. }));
    }

    #[test]
    fn resolves_named_patterns() {
        assert_eq!(named_pattern("four_on_floor").unwrap(), vec![0, 4, 8, 12]);
        assert_eq!(named_pattern("tresillo").unwrap(), vec![0, 2, 5]);
        assert_eq!(named_pattern("cinquillo").unwrap(), vec![0, 2, 3, 5, 7]);
    }

    #[test]
    fn parses_pattern_strings_to_zero_based_steps() {
        assert_eq!(steps_from_pattern_string("x---x---"), vec![0, 4]);
        assert_eq!(steps_from_pattern_string("xX1*----"), vec![0, 1, 2, 3]);
        assert!(steps_from_pattern_string("--------").is_empty());
    }
}