midi-io 0.1.1

Stream and send strictly-typed MIDI messages
Documentation
use crate::midi::data_byte::DataByte;
use crate::ValueError;

pub(crate) const PITCH_BEND_CENTER: i16 = 8192;

#[repr(transparent)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct PitchBend(u16);

impl PitchBend {
    pub(crate) fn from_msb_lsb(msb: DataByte, lsb: DataByte) -> Self {
        Self(((msb.get() as u16) << 7) | lsb.get() as u16)
    }

    pub fn from_signed(value: i16) -> Result<Self, ValueError> {
        if !(-PITCH_BEND_CENTER..PITCH_BEND_CENTER).contains(&value) {
            Err(ValueError::PitchBend(value as i32))
        } else {
            Ok(Self((value as i32 + PITCH_BEND_CENTER as i32) as u16))
        }
    }

    pub const fn raw(self) -> u16 {
        self.0
    }

    pub const fn signed(self) -> i16 {
        self.0 as i16 - PITCH_BEND_CENTER
    }

    pub const fn lsb(self) -> u8 {
        (self.0 & 0x7F) as u8
    }

    pub const fn msb(self) -> u8 {
        ((self.0 >> 7) & 0x7F) as u8
    }
}

impl TryFrom<u16> for PitchBend {
    type Error = ValueError;

    fn try_from(value: u16) -> Result<Self, Self::Error> {
        if value > 0x3FFF {
            Err(ValueError::PitchBend(value as i32))
        } else {
            Ok(Self(value))
        }
    }
}

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

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

    #[test]
    fn pitch_bend_center_is_zero_signed() {
        let center = PitchBend::from_signed(0).unwrap();
        assert_eq!(center.raw(), 8192);
        assert_eq!(center.signed(), 0);
        assert_eq!(center.lsb(), 0);
        assert_eq!(center.msb(), 64);
    }

    #[test]
    fn pitch_bend_signed_roundtrip() {
        for value in [-8192i16, -4096, -1, 0, 1, 4096, 8191] {
            assert_eq!(PitchBend::from_signed(value).unwrap().signed(), value);
        }
    }

    #[test]
    fn pitch_bend_from_signed_rejects_out_of_range() {
        assert_eq!(
            PitchBend::from_signed(-8192).map(PitchBend::signed),
            Ok(-8192)
        );
        assert_eq!(
            PitchBend::from_signed(8191).map(PitchBend::signed),
            Ok(8191)
        );
        assert_eq!(
            PitchBend::from_signed(8192),
            Err(ValueError::PitchBend(8192))
        );
        assert_eq!(
            PitchBend::from_signed(-8193),
            Err(ValueError::PitchBend(-8193))
        );
        assert_eq!(
            PitchBend::from_signed(i16::MAX),
            Err(ValueError::PitchBend(32767))
        );
        assert_eq!(
            PitchBend::from_signed(i16::MIN),
            Err(ValueError::PitchBend(-32768))
        );
    }

    #[test]
    fn pitch_bend_try_from_rejects_out_of_range() {
        assert_eq!(PitchBend::try_from(0).map(PitchBend::raw), Ok(0));
        assert_eq!(PitchBend::try_from(0x3FFF).map(PitchBend::raw), Ok(0x3FFF));
        assert_eq!(
            PitchBend::try_from(0x4000),
            Err(ValueError::PitchBend(0x4000))
        );
        assert_eq!(
            PitchBend::try_from(0xFFFF),
            Err(ValueError::PitchBend(0xFFFF))
        );
    }

    #[test]
    fn pitch_bend_is_two_bytes() {
        assert_eq!(std::mem::size_of::<PitchBend>(), 2);
    }
}