euphony-units 0.1.1

Core types and traits for music composition
Documentation
use crate::pitch::{
    interval::Interval,
    mode::{intervals::RoundingStrategy, position::ModePosition, system::ModeSystem},
};
use core::fmt;

#[macro_use]
pub mod intervals;
#[macro_use]
pub mod system;
pub mod position;

#[macro_export]
macro_rules! named_mode {
    ($name:ident($position:expr, $system:ident)) => {
        pub const $name: $crate::pitch::mode::Mode =
            $crate::pitch::mode::Mode::new_named($position, $system, stringify!($name));
    };
}

pub mod chromatic;
pub use chromatic as dodecatonic;
pub mod ditonic;
pub mod heptatonic;
pub mod pentatonic;

pub mod western {
    pub use super::{chromatic::*, heptatonic::*};
}

#[derive(Clone, Copy, PartialOrd, Eq, Ord)]
pub struct Mode {
    pub ascending: ModePosition,
    pub descending: ModePosition,
    pub name: Option<&'static str>,
}

impl PartialEq for Mode {
    fn eq(&self, other: &Self) -> bool {
        self.ascending.eq(&other.ascending) && self.descending.eq(&other.descending)
    }
}

impl Default for Mode {
    fn default() -> Self {
        heptatonic::MAJOR
    }
}

impl fmt::Debug for Mode {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        if let Some(name) = self.name {
            return f.debug_tuple("Mode").field(&name).finish();
        }
        if self.ascending == self.descending {
            return f.debug_tuple("Mode").field(&self.ascending).finish();
        }
        f.debug_struct("Mode")
            .field("ascending", &self.ascending)
            .field("descending", &self.descending)
            .finish()
    }
}

impl Mode {
    pub const fn new(position: usize, system: ModeSystem) -> Self {
        Self {
            ascending: ModePosition(position, system),
            descending: ModePosition(position, system),
            name: None,
        }
    }

    pub const fn new_named(position: usize, system: ModeSystem, name: &'static str) -> Self {
        Self {
            ascending: ModePosition(position, system),
            descending: ModePosition(position, system),
            name: Some(name),
        }
    }

    pub const fn len(&self) -> usize {
        self.ascending.intervals().len()
    }

    pub const fn is_empty(&self) -> bool {
        self.ascending.intervals().is_empty()
    }

    pub fn collapse(&self, interval: Interval, rounding_strategy: RoundingStrategy) -> Interval {
        self.checked_collapse(interval, rounding_strategy)
            .expect("Interval could not be collapsed")
    }

    pub fn checked_collapse(
        &self,
        interval: Interval,
        rounding_strategy: RoundingStrategy,
    ) -> Option<Interval> {
        if interval < 0 {
            self.descending
                .checked_collapse(interval, rounding_strategy)
        } else {
            self.ascending.checked_collapse(interval, rounding_strategy)
        }
    }

    pub fn expand(&self, interval: Interval, rounding_strategy: RoundingStrategy) -> Interval {
        self.checked_expand(interval, rounding_strategy)
            .expect("Interval could not be expanded")
    }

    pub fn checked_expand(
        &self,
        interval: Interval,
        rounding_strategy: RoundingStrategy,
    ) -> Option<Interval> {
        if interval < 0 {
            self.descending.checked_expand(interval, rounding_strategy)
        } else {
            self.ascending.checked_expand(interval, rounding_strategy)
        }
    }
}

impl core::ops::Shr<usize> for Mode {
    type Output = Mode;

    fn shr(self, rhs: usize) -> Self::Output {
        Self {
            ascending: self.ascending.shr(rhs),
            descending: self.descending.shr(rhs),
            name: None,
        }
    }
}

impl core::ops::ShrAssign<usize> for Mode {
    fn shr_assign(&mut self, rhs: usize) {
        *self = core::ops::Shr::shr(*self, rhs)
    }
}

impl core::ops::Shl<usize> for Mode {
    type Output = Mode;

    fn shl(self, rhs: usize) -> Self::Output {
        Self {
            ascending: self.ascending.shl(rhs),
            descending: self.descending.shl(rhs),
            name: None,
        }
    }
}

impl core::ops::ShlAssign<usize> for Mode {
    fn shl_assign(&mut self, rhs: usize) {
        self.ascending <<= rhs;
        self.descending <<= rhs;
    }
}

impl core::ops::Mul<Interval> for Mode {
    type Output = Interval;

    fn mul(self, interval: Interval) -> Self::Output {
        self.expand(interval, Default::default())
    }
}

impl core::ops::Mul<Mode> for Interval {
    type Output = Interval;

    fn mul(self, mode: Mode) -> Self::Output {
        mode.expand(self, Default::default())
    }
}

impl core::ops::Div<Mode> for Interval {
    type Output = Interval;

    fn div(self, mode: Mode) -> Self::Output {
        mode.collapse(self, Default::default())
    }
}

#[test]
fn shift_test() {
    use crate::pitch::mode::heptatonic::*;
    assert_eq!(MAJOR >> 1, DORIAN);
    assert_eq!(MAJOR << 2, MINOR);
    assert_eq!(MAJOR << 7, MAJOR);
}

#[test]
fn expansion_test() {
    use crate::pitch::mode::western::*;

    assert_eq!(A + MINOR * I, A);
    assert_eq!(A + MINOR * II, B);
    assert_eq!(A + MINOR * III, C);
    assert_eq!(A + MINOR * IV, D);
    assert_eq!(A + MINOR * V, E);
    assert_eq!(A + MINOR * VI, F);
    assert_eq!(A + MINOR * VII, G);

    assert_eq!(C + MAJOR * I, C);
    assert_eq!(C + MAJOR * II, D);
    assert_eq!(C + MAJOR * III, E);
    assert_eq!(C + MAJOR * IV, F);
    assert_eq!(C + MAJOR * V, G);
    assert_eq!(C + MAJOR * VI, A + 1);
    assert_eq!(C + MAJOR * VII, B + 1);

    assert_eq!(C + MINOR * I, C);
    assert_eq!(C + MINOR * II, D);
    assert_eq!(C + MINOR * III, E.flat());
    assert_eq!(C + MINOR * IV, F);
    assert_eq!(C + MINOR * V, G);
    assert_eq!(C + MINOR * VI, A.flat() + 1);
    assert_eq!(C + MINOR * VII, B.flat() + 1);

    assert_eq!(A + MINOR * -I, A);
    assert_eq!(A + MINOR * -II, G - 1);
    assert_eq!(A + MINOR * -III, F - 1);
    assert_eq!(A + MINOR * -IV, E - 1);
    assert_eq!(A + MINOR * -V, D - 1);
    assert_eq!(A + MINOR * -VI, C - 1);
    assert_eq!(A + MINOR * -VII, B - 1);

    assert_eq!(C + MAJOR * -I, C);
    assert_eq!(C + MAJOR * -II, B);
    assert_eq!(C + MAJOR * -III, A);
    assert_eq!(C + MAJOR * -IV, G - 1);
    assert_eq!(C + MAJOR * -V, F - 1);
    assert_eq!(C + MAJOR * -VI, E - 1);
    assert_eq!(C + MAJOR * -VII, D - 1);
}

#[test]
fn collapse_test() {
    use crate::pitch::mode::western::*;

    assert_eq!(A / MINOR, I);
    assert_eq!(A.sharp() / MINOR, I);
    assert_eq!(B / MINOR, II);
    assert_eq!(C / MINOR, III);
    assert_eq!(C.sharp() / MINOR, III);
    assert_eq!(D / MINOR, IV);
    assert_eq!(D.sharp() / MINOR, IV);
    assert_eq!(E / MINOR, V);
    assert_eq!(F / MINOR, VI);
    assert_eq!(F.sharp() / MINOR, VI);
    assert_eq!(G / MINOR, VII);
    assert_eq!(G.sharp() / MINOR, VII);
    assert_eq!((A + 1) / MINOR, Interval(7, 7));
    assert_eq!(A.flat() / MINOR, -Interval(1, 7));
    assert_eq!(A.flat().flat() / MINOR, -Interval(1, 7));
    assert_eq!(A.flat().flat().flat() / MINOR, -Interval(2, 7));
}