libkeynotation 0.2.0

A (musical) key notation parser and transposer
Documentation
use crate::KeyParser;

macro_rules! generate_key {
    ($(($variant:tt, $traditional_num: tt, $lancelot_num: tt, $open_key_num: tt, $is_major: tt, $traditional_str: tt),)*) => {
        #[derive(Debug, PartialEq, Clone, Copy)]
        pub enum Key {
            $(
                $variant
            ),*
        }

        impl Key {
            /// Tries parsing the [`Key`] from a string. Just a wrapper around [`KeyParser::try_from_string()`].
            pub fn from_string(s: &str) -> Result<Self, ()> {
                Ok(KeyParser::try_from_string(s).map_err(|_| ())?.key())
            }

            /// Get the key from a numeric representation. Mainly used internally.
            /// Number needs to be from 1-12 (1=C,2=D,3=E...) and is_major defining
            /// if you want the major or minor version of the note.
            /// Returns an error if number is `<1` or `>12`.
            pub fn from_numeric(number: i32, is_major: bool) -> Result<Self, ()> {
                Ok(match (number, is_major) {
                    $(
                        ($traditional_num, $is_major) => Self::$variant,
                    )*
                    _ => return Err(())
                })
            }
            /// Same as [`Key::from_numeric()`] but number is getting wrapped.
            /// So 0 would be wrapped to 12, -1 to 11, 13 to 1...
            pub fn from_numeric_wrapping(number: i32, is_major: bool) -> Self {
                Self::from_numeric(wrap(number), is_major).expect("unreachable")
            }

            /// Returns the numeric representation of this Key variant.
            pub fn numeric(&self) -> (i32, bool) {
                match self {
                    $(
                        Self::$variant => ($traditional_num, $is_major)
                    ),*
                }
            }

            /// Returns true if this key is a major key, false if it is minor.
            pub fn is_major(&self) -> bool {
                match self {
                    $(
                        Self::$variant => $is_major
                    ),*
                }
            }

            pub fn from_open_key_numeric(number: i32, is_major: bool) -> Result<Self, ()> {
                Ok(match (number, is_major) {
                    $(
                       ($open_key_num, $is_major) => Self::$variant,
                    )*
                    _ => return Err(())
                })

            }

            pub fn from_open_key_numeric_wrapping(number: i32, is_major: bool) -> Result<Self, ()> {
                let number = wrap(number);
                Self::from_open_key_numeric(number, is_major)
            }

            pub fn open_key_numeric(&self) -> (i32, bool) {
                match self {
                    $(
                        Self::$variant => ($open_key_num, $is_major)
                    ),*
                }
            }

            pub fn from_lancelot_numeric(number: i32, is_major: bool) -> Result<Self, ()> {
                Ok(match (number, is_major) {
                    $(
                       ($lancelot_num, $is_major) => Self::$variant,
                    )*
                    _ => return Err(())
                })

            }

            pub fn from_lancelot_numeric_wrapping(number: i32, is_major: bool) -> Result<Self, ()> {
                Self::from_lancelot_numeric(wrap(number), is_major)
            }

            pub fn lancelot_numeric(&self) -> (i32, bool) {
                match self {
                    $(
                        Self::$variant => ($lancelot_num, $is_major)
                    ),*
                }
            }


            /// Returns the traditional string representation. Eg Abm, F#m, F...
            pub fn traditional(&self) -> String {
                match self {
                    $(
                        Self::$variant => $traditional_str
                    ),*
                }.to_string()
            }

            /// Returns the open key string representation. Eg 1m, 1d, 2m... 12m,12d
            pub fn open_key(&self) -> String {
                match self {
                    $(
                        Self::$variant => format!("{}{}", $open_key_num, if $is_major { "d" } else { "m" })
                    ),*
                }.to_string()

            }

            /// Returns the lancelot string representation. Eg 1A, 1B, 2A... 12A, 12B
            pub fn lancelot(&self) -> String {
                match self {
                    $(
                        Self::$variant => format!("{}{}", $lancelot_num, if $is_major { "B" } else { "A" })
                    ),*
                }.to_string()

            }
        }

    }
}

generate_key! {
    (CMajor       ,  1,  8,  1,  true, "C"),
    (DFlatMajor   ,  2,  3,  8,  true, "Db"),
    (DMajor       ,  3, 10,  3,  true, "D"),
    (EFlatMajor   ,  4,  5, 10,  true, "Eb"),
    (EMajor       ,  5, 12,  5,  true, "E"),
    (FMajor       ,  6,  7, 12,  true, "F"),
    (FSharpMajor  ,  7,  2,  7,  true, "F#"),
    (GMajor       ,  8,  9,  2,  true, "G"),
    (AFlatMajor   ,  9,  4,  9,  true, "Ab"),
    (AMajor       , 10, 11,  4,  true, "A"),
    (BFlatMajor   , 11,  6, 11,  true, "Bb"),
    (BMajor       , 12,  1,  6,  true, "B"),
    (CMinor       ,  1,  5, 10, false, "Cm"),
    (CSharpMinor  ,  2, 12,  5, false, "C#m"),
    (DMinor       ,  3,  7, 12, false, "Dm"),
    (EFlatMinor   ,  4,  2,  7, false, "Ebm"),
    (EMinor       ,  5,  9,  2, false, "Em"),
    (FMinor       ,  6,  4,  9, false, "Fm"),
    (FSharpMinor  ,  7, 11,  4, false, "F#m"),
    (GMinor       ,  8,  6, 11, false, "Gm"),
    (GSharpMinor  ,  9,  1,  6, false, "G#m"),
    (AMinor       , 10,  8,  1, false, "Am"),
    (BFlatMinor   , 11,  3,  8, false, "Bbm"),
    (BMinor       , 12, 10,  3, false, "Bm"),
}

impl Key {
    /// Transpose this key by n semitones
    pub fn transpose_semitones(&self, semitones: i32) -> Self {
        let (number, is_major) = self.numeric();
        Self::from_numeric_wrapping(number + semitones, is_major)
    }

    /// Transpose this key by n full tones
    pub fn transpose_tones(&self, tones: i32) -> Self {
        let (number, is_major) = self.numeric();
        Self::from_numeric_wrapping(number + tones * 2, is_major)
    }

    /// Transpose this key by a factor. 0.5 would be half the tempo, 2.0 twice the tempo.
    /// (Imagine a pitch fader on a vinyl recorder)
    pub fn transpose_factor(&self, factor: f32) -> Self {
        self.transpose_semitones(factor_to_semitones(factor).round() as i32)
    }

    /// Transpose this key by an initial bpm and a target bpm.
    /// Imagine a track has the Key 3A at 135.0bpm (initial_bpm) and you want
    /// to know what bpm it is at 140.4bpm (target bpm).
    pub fn transpose_bpm(&self, initial_bpm: f32, bpm: f32) -> Self {
        self.transpose_factor(bpm_to_factor(initial_bpm, bpm))
    }

    /// Return the i32 representation of this Key in the lancelot sorting. The i32 will be between 0 and 23. This is helpful for serialization and sorting.
    pub fn to_lancelot_i32(&self) -> i32 {
        match self.lancelot_numeric() {
            (number, true) => 12 + number - 1,
            (number, false) => number - 1,
        }
    }

    /// Try turning an i32 in lancelot sorting into a Key object. See [`Key::to_lancelot_i32`].
    pub fn try_from_lancelot_i32(number: i32) -> Result<Self, ()> {
        let number = number + 1;
        if number > 24 {
            return Err(());
        }
        if number > 12 {
            Self::from_lancelot_numeric(number - 12, true)
        } else {
            Self::from_lancelot_numeric(number, false)
        }
    }
}

impl std::fmt::Display for Key {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.traditional())
    }
}

/// Helper method to wrap number representations. 1 -> 1, 2 -> 2, 12 -> 12, 0->12, 13->1, -1->11...
pub fn wrap(number: i32) -> i32 {
    ((number - 1).rem_euclid(12) + 1).abs()
}

/// Calculate how many semitones a key would change if played at a certain tempo factor
pub fn factor_to_semitones(factor: f32) -> f32 {
    factor.log2() * 12.0
}

/// Calculate the factor between an intial and target bpm
pub fn bpm_to_factor(initial_bpm: f32, bpm: f32) -> f32 {
    bpm / initial_bpm
}

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

    #[test]
    fn test_wrap() {
        assert_eq!(wrap(1), 1);
        assert_eq!(wrap(12), 12);
        assert_eq!(wrap(0), 12);
        assert_eq!(wrap(-1), 11);
    }
    #[test]
    fn test_from_numeric_wrapping() {
        assert_eq!(Key::from_numeric_wrapping(1, true), Key::CMajor);
        assert_eq!(Key::from_numeric_wrapping(2, true), Key::DFlatMajor);
        assert_eq!(Key::from_numeric_wrapping(3, true), Key::DMajor);
        assert_eq!(Key::from_numeric_wrapping(11, false), Key::BFlatMinor);
        assert_eq!(Key::from_numeric_wrapping(12, false), Key::BMinor);
        assert_eq!(Key::from_numeric_wrapping(13, false), Key::CMinor);
        assert_eq!(Key::from_numeric_wrapping(24, false), Key::BMinor);
    }

    #[test]
    fn test_transpose_semitones() {
        macro_rules! test_case {
            ($lancelot_init: tt, $tones: expr, $lancelot_transposed: tt) => {
                let key_init = Key::from_string($lancelot_init).expect("Failed parsing key init");
                assert_eq!(
                    key_init.transpose_semitones($tones).lancelot(),
                    $lancelot_transposed
                );
            };
        }
        test_case!("8A", 12, "8A");
        test_case!("8A", -1, "1A");
        test_case!("8A", -2, "6A");
        test_case!("8A", -2, "6A");
        test_case!("8A", 1, "3A");
        test_case!("8A", 2, "10A");
        test_case!("8A", 3, "5A");
    }

    #[test]
    fn test_transpose_tones() {
        macro_rules! test_case {
            ($lancelot_init: tt, $tones: expr, $lancelot_transposed: tt) => {
                let key_init = Key::from_string($lancelot_init).expect("Failed parsing key init");
                assert_eq!(
                    key_init.transpose_tones($tones).lancelot(),
                    $lancelot_transposed
                );
            };
        }

        test_case!("3A", -1, "1A");
    }

    #[test]
    fn test_transpose_factor() {
        let root = Key::CMajor;
        println!("root={root:?}");

        assert_eq!(root.transpose_factor(2.0), root);
    }

    #[test]
    fn test_factor_to_semitones() {
        assert_eq!(factor_to_semitones(1.0), 0.0);
        assert_eq!(factor_to_semitones(2.0), 12.0);
        assert_eq!(factor_to_semitones(1.0595).round(), 1.0);
        assert_eq!(factor_to_semitones(0.9659259).round(), -1.0)
    }

    #[test]
    fn test_bpm_to_factor() {
        assert_eq!(bpm_to_factor(120.0, 120.0), 1.0);
        assert_eq!(bpm_to_factor(120.0, 240.0), 2.0);
        assert_eq!(bpm_to_factor(240.0, 120.0), 0.5);

        assert_eq!(bpm_to_factor(135.0, 130.4), 0.9659259);
    }

    #[test]
    fn test_transpose_bpm() {
        let root = Key::CMajor;
        println!("root={root:?}");

        assert_eq!(root.transpose_bpm(90.0, 180.0), root);

        macro_rules! test_case_lancelot {
            ($lancelot_init:tt, $bpm_init: tt, $lancelot_transposed: tt, $bpm_transposed: tt) => {
                let key_init = Key::from_string($lancelot_init).expect("Failed parsing key init");
                assert_eq!(
                    key_init
                        .transpose_bpm($bpm_init, $bpm_transposed)
                        .lancelot(),
                    $lancelot_transposed
                );
            };
        }

        test_case_lancelot!("3A", 135.0, "8A", 130.4);
        test_case_lancelot!("3A", 135.0, "10A", 140.4);
        test_case_lancelot!("3A", 135.0, "5A", 148.9);
        test_case_lancelot!("3A", 135.0, "11A", 13.5);
    }

    #[test]
    fn test_lancelot_i32_representation() {
        for i in 0..24 {
            let key = Key::try_from_lancelot_i32(i).expect("Failed turning i32 test case into Key");
            assert_eq!(key.to_lancelot_i32(), i);
        }
        assert_eq!(Key::try_from_lancelot_i32(-1), Err(()));
        assert_eq!(Key::try_from_lancelot_i32(24), Err(()));

        macro_rules! test_case_lancelot {
            ($lancelot:tt, $i32:tt) => {
                let key = Key::from_string($lancelot).expect("Failed parsing key init");
                //assert_eq!(Key::try_from_i32($i32), Ok(key));
                assert_eq!(key.to_lancelot_i32(), $i32);
            };
        }

        test_case_lancelot!("1A", 0);
        test_case_lancelot!("3A", 2);
        test_case_lancelot!("12A", 11);
        test_case_lancelot!("1B", 12);
        test_case_lancelot!("12B", 23);
    }
}