paracletics-hypercube 0.1.0

General-purpose paracletic hyper cube compression toolkit.
Documentation
use std::error::Error;
use std::fmt;

const NOTE_NAMES: [&str; 12] = [
    "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
];

const SYLLABLES: [&str; 16] = [
    "ka", "zu", "mi", "te", "ra", "lo", "fi", "na", "se", "di", "vo", "pa", "qu", "xe", "yo", "tu",
];

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CrossModalError {
    InvalidToken,
    UnknownSyllable,
    InvalidNote,
}

impl fmt::Display for CrossModalError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            CrossModalError::InvalidToken => write!(f, "invalid word token"),
            CrossModalError::UnknownSyllable => write!(f, "unknown syllable"),
            CrossModalError::InvalidNote => write!(f, "invalid note descriptor"),
        }
    }
}

impl Error for CrossModalError {}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ModalFrame {
    pub byte: u8,
    pub note_name: &'static str,
    pub register: u8,
    pub hue_index: u8,
    pub rgb: [u8; 3],
    pub token: String,
}

pub fn encode_byte(byte: u8) -> ModalFrame {
    let pitch_class = byte % 12;
    let register = byte / 12;
    let hue_index = pitch_class;
    let rgb = hue_to_rgb(hue_index);

    ModalFrame {
        byte,
        note_name: NOTE_NAMES[pitch_class as usize],
        register,
        hue_index,
        rgb,
        token: token_for_byte(byte),
    }
}

pub fn decode_from_note(note_name: &str, register: u8) -> Result<u8, CrossModalError> {
    let pitch_class = NOTE_NAMES
        .iter()
        .position(|n| *n == note_name)
        .ok_or(CrossModalError::InvalidNote)? as u8;
    Ok(register.saturating_mul(12).saturating_add(pitch_class))
}

pub fn decode_from_hue(hue_index: u8, register: u8) -> Result<u8, CrossModalError> {
    if hue_index >= 12 {
        return Err(CrossModalError::InvalidNote);
    }
    Ok(register.saturating_mul(12).saturating_add(hue_index))
}

pub fn token_for_byte(byte: u8) -> String {
    let hi = (byte >> 4) as usize;
    let lo = (byte & 0x0f) as usize;
    format!("{}-{}", SYLLABLES[hi], SYLLABLES[lo])
}

pub fn byte_from_token(token: &str) -> Result<u8, CrossModalError> {
    let mut parts = token.split('-');
    let hi = parts.next().ok_or(CrossModalError::InvalidToken)?;
    let lo = parts.next().ok_or(CrossModalError::InvalidToken)?;
    if parts.next().is_some() {
        return Err(CrossModalError::InvalidToken);
    }
    let hi_idx = syllable_index(hi)?;
    let lo_idx = syllable_index(lo)?;
    Ok((hi_idx << 4) | lo_idx)
}

fn syllable_index(s: &str) -> Result<u8, CrossModalError> {
    SYLLABLES
        .iter()
        .position(|x| *x == s)
        .map(|i| i as u8)
        .ok_or(CrossModalError::UnknownSyllable)
}

fn hue_to_rgb(hue_index: u8) -> [u8; 3] {
    const HUE: [[u8; 3]; 12] = [
        [255, 0, 0],
        [255, 128, 0],
        [255, 220, 0],
        [170, 255, 0],
        [50, 255, 0],
        [0, 255, 80],
        [0, 255, 220],
        [0, 170, 255],
        [0, 60, 255],
        [120, 0, 255],
        [220, 0, 255],
        [255, 0, 140],
    ];
    HUE[hue_index as usize]
}