sim-lib-music-notation 0.1.0-rc.1

SIM workspace package for sim lib music notation.
Documentation
use sim_lib_pitch_core::{Letter, Pitch, SpelledPitch};

use crate::model::NotationError;

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) enum KeyFlavor {
    Sharps,
    Flats,
    Neutral,
}

pub(crate) fn lily_key_spec(
    key: Option<&str>,
) -> Result<Option<(String, &'static str, KeyFlavor)>, NotationError> {
    let Some(key) = key else {
        return Ok(None);
    };
    let trimmed = key.trim();
    if trimmed.is_empty() {
        return Ok(None);
    }
    let (tonic_raw, mode) = if let Some(value) = trimmed.strip_suffix(" major") {
        (value.trim(), "major")
    } else if let Some(value) = trimmed.strip_suffix(" minor") {
        (value.trim(), "minor")
    } else if let Some(value) = trimmed.strip_suffix('m') {
        (value.trim(), "minor")
    } else {
        (trimmed, "major")
    };
    let tonic = tonic_raw.to_owned();
    let flavor = key_flavor(&tonic, mode);
    Ok(Some((to_lily_tonic(&tonic)?, mode, flavor)))
}

fn key_flavor(tonic: &str, mode: &str) -> KeyFlavor {
    let tonic = tonic.to_ascii_lowercase();
    match (tonic.as_str(), mode) {
        ("f", "major")
        | ("bb", "major")
        | ("eb", "major")
        | ("ab", "major")
        | ("db", "major")
        | ("gb", "major")
        | ("cb", "major")
        | ("d", "minor")
        | ("g", "minor")
        | ("c", "minor")
        | ("f", "minor")
        | ("bb", "minor")
        | ("eb", "minor")
        | ("ab", "minor") => KeyFlavor::Flats,
        ("c", "major") | ("a", "minor") => KeyFlavor::Neutral,
        _ => KeyFlavor::Sharps,
    }
}

fn to_lily_tonic(value: &str) -> Result<String, NotationError> {
    let normalized = value.to_ascii_lowercase();
    let lily = match normalized.as_str() {
        "c" => "c",
        "c#" => "cis",
        "db" => "des",
        "d" => "d",
        "d#" => "dis",
        "eb" => "ees",
        "e" => "e",
        "f" => "f",
        "f#" => "fis",
        "gb" => "ges",
        "g" => "g",
        "g#" => "gis",
        "ab" => "aes",
        "a" => "a",
        "a#" => "ais",
        "bb" => "bes",
        "b" => "b",
        _ => return Err(NotationError::InvalidKey(value.to_owned())),
    };
    Ok(lily.to_owned())
}

pub(crate) fn spell_pitch_in_key(
    pitch: Pitch,
    key: Option<&str>,
) -> Result<SpelledPitch, NotationError> {
    let flavor = lily_key_spec(key)?
        .map(|(_, _, flavor)| flavor)
        .unwrap_or(KeyFlavor::Neutral);
    let table = match flavor {
        KeyFlavor::Sharps | KeyFlavor::Neutral => SHARP_SPELLINGS,
        KeyFlavor::Flats => FLAT_SPELLINGS,
    };
    let (letter, accidental) = table[pitch.class.0 as usize];
    Ok(SpelledPitch {
        letter,
        accidental,
        octave: pitch.octave,
    })
}

pub(crate) fn encode_lily_pitch(spelled: SpelledPitch) -> String {
    let mut out = match spelled.letter {
        Letter::C => "c",
        Letter::D => "d",
        Letter::E => "e",
        Letter::F => "f",
        Letter::G => "g",
        Letter::A => "a",
        Letter::B => "b",
    }
    .to_owned();
    out.push_str(match spelled.accidental {
        -2 => "eses",
        -1 => "es",
        0 => "",
        1 => "is",
        2 => "isis",
        _ => "",
    });
    match spelled.octave.cmp(&3) {
        std::cmp::Ordering::Greater => {
            for _ in 0..(spelled.octave - 3) {
                out.push('\'');
            }
        }
        std::cmp::Ordering::Less => {
            for _ in 0..(3 - spelled.octave) {
                out.push(',');
            }
        }
        std::cmp::Ordering::Equal => {}
    }
    out
}

pub(crate) fn decode_lily_pitch(token: &str) -> Option<SpelledPitch> {
    let mut chars = token.chars();
    let letter = match chars.next()? {
        'c' => Letter::C,
        'd' => Letter::D,
        'e' => Letter::E,
        'f' => Letter::F,
        'g' => Letter::G,
        'a' => Letter::A,
        'b' => Letter::B,
        _ => return None,
    };
    let mut rest = chars.as_str();
    let mut accidental = 0i8;
    while let Some(slice) = rest.strip_prefix("isis") {
        accidental += 2;
        rest = slice;
    }
    while let Some(slice) = rest.strip_prefix("eses") {
        accidental -= 2;
        rest = slice;
    }
    while let Some(slice) = rest.strip_prefix("is") {
        accidental += 1;
        rest = slice;
    }
    while let Some(slice) = rest.strip_prefix("es") {
        accidental -= 1;
        rest = slice;
    }
    let mut octave = 3i16;
    for ch in rest.chars() {
        match ch {
            '\'' => octave += 1,
            ',' => octave -= 1,
            _ => return None,
        }
    }
    Some(SpelledPitch {
        letter,
        accidental,
        octave,
    })
}

pub(crate) fn key_from_lily(tonic: &str, mode: &str) -> Option<String> {
    let tonic = match tonic {
        "c" => "C",
        "cis" => "C#",
        "des" => "Db",
        "d" => "D",
        "dis" => "D#",
        "ees" => "Eb",
        "e" => "E",
        "f" => "F",
        "fis" => "F#",
        "ges" => "Gb",
        "g" => "G",
        "gis" => "G#",
        "aes" => "Ab",
        "a" => "A",
        "ais" => "A#",
        "bes" => "Bb",
        "b" => "B",
        _ => return None,
    };
    Some(format!("{tonic} {mode}"))
}

const SHARP_SPELLINGS: [(Letter, i8); 12] = [
    (Letter::C, 0),
    (Letter::C, 1),
    (Letter::D, 0),
    (Letter::D, 1),
    (Letter::E, 0),
    (Letter::F, 0),
    (Letter::F, 1),
    (Letter::G, 0),
    (Letter::G, 1),
    (Letter::A, 0),
    (Letter::A, 1),
    (Letter::B, 0),
];

const FLAT_SPELLINGS: [(Letter, i8); 12] = [
    (Letter::C, 0),
    (Letter::D, -1),
    (Letter::D, 0),
    (Letter::E, -1),
    (Letter::E, 0),
    (Letter::F, 0),
    (Letter::G, -1),
    (Letter::G, 0),
    (Letter::A, -1),
    (Letter::A, 0),
    (Letter::B, -1),
    (Letter::B, 0),
];