Skip to main content

midi_io/midi/
pitch_bend.rs

1use crate::midi::data_byte::DataByte;
2use crate::ValueError;
3
4pub(crate) const PITCH_BEND_CENTER: i16 = 8192;
5
6#[repr(transparent)]
7#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
8pub struct PitchBend(u16);
9
10impl PitchBend {
11    pub(crate) fn from_msb_lsb(msb: DataByte, lsb: DataByte) -> Self {
12        Self(((msb.get() as u16) << 7) | lsb.get() as u16)
13    }
14
15    pub fn from_signed(value: i16) -> Result<Self, ValueError> {
16        if !(-PITCH_BEND_CENTER..PITCH_BEND_CENTER).contains(&value) {
17            Err(ValueError::PitchBend(value as i32))
18        } else {
19            Ok(Self((value as i32 + PITCH_BEND_CENTER as i32) as u16))
20        }
21    }
22
23    pub const fn raw(self) -> u16 {
24        self.0
25    }
26
27    pub const fn signed(self) -> i16 {
28        self.0 as i16 - PITCH_BEND_CENTER
29    }
30
31    pub const fn lsb(self) -> u8 {
32        (self.0 & 0x7F) as u8
33    }
34
35    pub const fn msb(self) -> u8 {
36        ((self.0 >> 7) & 0x7F) as u8
37    }
38}
39
40impl TryFrom<u16> for PitchBend {
41    type Error = ValueError;
42
43    fn try_from(value: u16) -> Result<Self, Self::Error> {
44        if value > 0x3FFF {
45            Err(ValueError::PitchBend(value as i32))
46        } else {
47            Ok(Self(value))
48        }
49    }
50}
51
52impl std::fmt::Display for PitchBend {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        write!(f, "{}", self.signed())
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn pitch_bend_center_is_zero_signed() {
64        let center = PitchBend::from_signed(0).unwrap();
65        assert_eq!(center.raw(), 8192);
66        assert_eq!(center.signed(), 0);
67        assert_eq!(center.lsb(), 0);
68        assert_eq!(center.msb(), 64);
69    }
70
71    #[test]
72    fn pitch_bend_signed_roundtrip() {
73        for value in [-8192i16, -4096, -1, 0, 1, 4096, 8191] {
74            assert_eq!(PitchBend::from_signed(value).unwrap().signed(), value);
75        }
76    }
77
78    #[test]
79    fn pitch_bend_from_signed_rejects_out_of_range() {
80        assert_eq!(
81            PitchBend::from_signed(-8192).map(PitchBend::signed),
82            Ok(-8192)
83        );
84        assert_eq!(
85            PitchBend::from_signed(8191).map(PitchBend::signed),
86            Ok(8191)
87        );
88        assert_eq!(
89            PitchBend::from_signed(8192),
90            Err(ValueError::PitchBend(8192))
91        );
92        assert_eq!(
93            PitchBend::from_signed(-8193),
94            Err(ValueError::PitchBend(-8193))
95        );
96        assert_eq!(
97            PitchBend::from_signed(i16::MAX),
98            Err(ValueError::PitchBend(32767))
99        );
100        assert_eq!(
101            PitchBend::from_signed(i16::MIN),
102            Err(ValueError::PitchBend(-32768))
103        );
104    }
105
106    #[test]
107    fn pitch_bend_try_from_rejects_out_of_range() {
108        assert_eq!(PitchBend::try_from(0).map(PitchBend::raw), Ok(0));
109        assert_eq!(PitchBend::try_from(0x3FFF).map(PitchBend::raw), Ok(0x3FFF));
110        assert_eq!(
111            PitchBend::try_from(0x4000),
112            Err(ValueError::PitchBend(0x4000))
113        );
114        assert_eq!(
115            PitchBend::try_from(0xFFFF),
116            Err(ValueError::PitchBend(0xFFFF))
117        );
118    }
119
120    #[test]
121    fn pitch_bend_is_two_bytes() {
122        assert_eq!(std::mem::size_of::<PitchBend>(), 2);
123    }
124}