bbx_midi/
message.rs

1//! MIDI message types and parsing.
2
3use std::{
4    fmt::{Display, Formatter},
5    time::SystemTime,
6};
7
8const NOTES: [&str; 12] = ["C", "C#", "D", "Eb", "E", "F", "F#", "G", "Ab", "A", "Bb", "B"];
9
10/// A parsed MIDI message with channel, status, and data bytes.
11///
12/// Uses `#[repr(C)]` for C-compatible memory layout, enabling FFI usage.
13#[repr(C)]
14#[derive(Debug, Clone, Copy)]
15pub struct MidiMessage {
16    channel: u8,
17    status: MidiMessageStatus,
18    data_1: u8,
19    data_2: u8,
20}
21
22/// MIDI message status byte types.
23///
24/// Uses `#[repr(C)]` for C-compatible memory layout, enabling FFI usage.
25#[repr(C)]
26#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
27pub enum MidiMessageStatus {
28    /// Unrecognized or system message.
29    Unknown = 0,
30    /// Note released (0x80-0x8F).
31    NoteOff = 1,
32    /// Note pressed (0x90-0x9F).
33    NoteOn = 2,
34    /// Per-note pressure change (0xA0-0xAF).
35    PolyphonicAftertouch = 3,
36    /// Controller value change (0xB0-0xBF).
37    ControlChange = 4,
38    /// Instrument/patch change (0xC0-0xCF).
39    ProgramChange = 5,
40    /// Channel-wide pressure (0xD0-0xDF).
41    ChannelAftertouch = 6,
42    /// Pitch bend wheel (0xE0-0xEF).
43    PitchWheel = 7,
44}
45
46/// A MIDI event with sample-accurate timing for audio buffer processing.
47///
48/// Combines a MIDI message with a sample offset indicating when the event
49/// should be processed within the current audio buffer.
50///
51/// Uses `#[repr(C)]` for C-compatible memory layout, enabling FFI usage.
52#[repr(C)]
53#[derive(Debug, Clone, Copy)]
54pub struct MidiEvent {
55    /// The MIDI message data.
56    pub message: MidiMessage,
57    /// Sample offset within the current buffer (0 to buffer_size - 1).
58    pub sample_offset: u32,
59}
60
61impl MidiEvent {
62    /// Create a new MIDI event with the given message and sample offset.
63    pub fn new(message: MidiMessage, sample_offset: u32) -> Self {
64        Self { message, sample_offset }
65    }
66}
67
68impl From<u8> for MidiMessageStatus {
69    fn from(byte: u8) -> Self {
70        match byte {
71            // 128 - 144
72            0x80..0x90 => MidiMessageStatus::NoteOff,
73            // 144 - 160
74            0x90..0xA0 => MidiMessageStatus::NoteOn,
75            // 160 - 176
76            0xA0..0xB0 => MidiMessageStatus::PolyphonicAftertouch,
77            // 176 - 192
78            0xB0..0xC0 => MidiMessageStatus::ControlChange,
79            // 192 - 208
80            0xC0..0xD0 => MidiMessageStatus::ProgramChange,
81            // 208 - 224
82            0xD0..0xE0 => MidiMessageStatus::ChannelAftertouch,
83            // 224 - 255
84            0xE0..=0xFF => MidiMessageStatus::PitchWheel,
85            _ => MidiMessageStatus::Unknown,
86        }
87    }
88}
89
90impl MidiMessage {
91    /// Create a new MIDI message from raw bytes.
92    pub fn new(bytes: [u8; 3]) -> Self {
93        MidiMessage {
94            channel: (bytes[0] & 0x0F) + 1,
95            status: MidiMessageStatus::from(bytes[0]),
96            data_1: bytes[1],
97            data_2: bytes[2],
98        }
99    }
100}
101
102impl MidiMessage {
103    /// Get the message status type.
104    pub fn get_status(&self) -> MidiMessageStatus {
105        self.status
106    }
107
108    /// Get the MIDI channel (1-16).
109    pub fn get_channel(&self) -> u8 {
110        self.channel
111    }
112
113    fn get_data(&self, data_field: usize, statuses: &[MidiMessageStatus]) -> Option<u8> {
114        if statuses.contains(&self.status) {
115            match data_field {
116                1 => Some(self.data_1),
117                2 => Some(self.data_2),
118                _ => None,
119            }
120        } else {
121            None
122        }
123    }
124
125    /// Get the note name (e.g., "C4", "F#3") for note messages.
126    pub fn get_note(&self) -> Option<String> {
127        let note_number = self.get_note_number()?;
128        // Determine the note name (C, C#, D, etc.)
129        let note_index = (note_number % 12) as usize;
130        let note_name = NOTES[note_index];
131
132        // Determine the octave (MIDI note 60 is C4, middle C)
133        let octave = (note_number / 12) as i8 - 1;
134
135        // Return the formatted string (e.g., "C4", "D#3")
136        Some(format!("{note_name}{octave}"))
137    }
138
139    /// Get the note frequency in Hz (A4 = 440 Hz) for note messages.
140    pub fn get_note_frequency(&self) -> Option<f32> {
141        let note_number = self.get_note_number()?;
142        Some(440.0 * 2.0f32.powf((note_number as f32 - 69.0) / 12.0))
143    }
144
145    /// Get the MIDI note number (0-127) for note messages.
146    pub fn get_note_number(&self) -> Option<u8> {
147        self.get_data(1, &[MidiMessageStatus::NoteOn, MidiMessageStatus::NoteOff])
148    }
149
150    /// Get the velocity (0-127) for note messages.
151    pub fn get_velocity(&self) -> Option<u8> {
152        self.get_data(2, &[MidiMessageStatus::NoteOn, MidiMessageStatus::NoteOff])
153    }
154
155    /// Get the pressure value (0-127) for polyphonic aftertouch.
156    pub fn get_pressure(&self) -> Option<u8> {
157        self.get_data(2, &[MidiMessageStatus::PolyphonicAftertouch])
158    }
159
160    /// Get the control value (0-127) for control change messages.
161    pub fn get_control_change_data(&self) -> Option<u8> {
162        self.get_data(2, &[MidiMessageStatus::ControlChange])
163    }
164
165    /// Get the pitch wheel data (LSB, MSB) for pitch bend messages.
166    pub fn get_pitch_wheel_data(&self) -> Option<(u8, u8)> {
167        let least_significant_byte = self.get_data(1, &[MidiMessageStatus::PitchWheel])?;
168        let most_significant_byte = self.get_data(2, &[MidiMessageStatus::PitchWheel])?;
169        Some((least_significant_byte, most_significant_byte))
170    }
171}
172
173impl Display for MidiMessage {
174    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
175        let now = SystemTime::now()
176            .duration_since(SystemTime::UNIX_EPOCH)
177            .unwrap()
178            .as_secs();
179        match self.status {
180            MidiMessageStatus::NoteOff | MidiMessageStatus::NoteOn => {
181                write!(
182                    f,
183                    "[{}] Ch {} {:?}\t Note = {} ({}Hz)\t Velocity = {}",
184                    now,
185                    self.channel,
186                    self.status,
187                    self.get_note().unwrap(),
188                    self.get_note_frequency().unwrap(),
189                    self.get_velocity().unwrap()
190                )
191            }
192            _ => {
193                write!(
194                    f,
195                    "[{}] Ch {} {:?}\t Data 1 = {}\t Data 2 = {}",
196                    now, self.channel, self.status, self.data_1, self.data_2
197                )
198            }
199        }
200    }
201}
202
203impl From<&[u8]> for MidiMessage {
204    fn from(bytes: &[u8]) -> Self {
205        match bytes.len() {
206            1 => MidiMessage {
207                channel: (bytes[0] & 0x0F) + 1,
208                status: MidiMessageStatus::from(bytes[0]),
209                data_1: 0,
210                data_2: 0,
211            },
212            2 => MidiMessage {
213                channel: (bytes[0] & 0x0F) + 1,
214                status: MidiMessageStatus::from(bytes[0]),
215                data_1: bytes[1],
216                data_2: 0,
217            },
218            3 => MidiMessage {
219                channel: (bytes[0] & 0x0F) + 1,
220                status: MidiMessageStatus::from(bytes[0]),
221                data_1: bytes[1],
222                data_2: bytes[2],
223            },
224            _ => MidiMessage {
225                channel: (bytes[0] & 0x0F) + 1,
226                status: MidiMessageStatus::Unknown,
227                data_1: 0,
228                data_2: 0,
229            },
230        }
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn test_note_on_parsing() {
240        // Note On, channel 1, note 60 (C4), velocity 100
241        let msg = MidiMessage::new([0x90, 60, 100]);
242        assert_eq!(msg.get_status(), MidiMessageStatus::NoteOn);
243        assert_eq!(msg.get_channel(), 1);
244        assert_eq!(msg.get_note_number(), Some(60));
245        assert_eq!(msg.get_velocity(), Some(100));
246        assert_eq!(msg.get_note(), Some("C4".to_string()));
247    }
248
249    #[test]
250    fn test_note_off_parsing() {
251        // Note Off, channel 3, note 64 (E4), velocity 64
252        let msg = MidiMessage::new([0x82, 64, 64]);
253        assert_eq!(msg.get_status(), MidiMessageStatus::NoteOff);
254        assert_eq!(msg.get_channel(), 3);
255        assert_eq!(msg.get_note_number(), Some(64));
256        assert_eq!(msg.get_velocity(), Some(64));
257        assert_eq!(msg.get_note(), Some("E4".to_string()));
258    }
259
260    #[test]
261    fn test_control_change_parsing() {
262        // Control Change, channel 1, controller 7 (volume), value 127
263        let msg = MidiMessage::new([0xB0, 7, 127]);
264        assert_eq!(msg.get_status(), MidiMessageStatus::ControlChange);
265        assert_eq!(msg.get_channel(), 1);
266        assert_eq!(msg.get_control_change_data(), Some(127));
267        assert_eq!(msg.get_note_number(), None);
268    }
269
270    #[test]
271    fn test_channel_extraction() {
272        // Channels are 1-16 in user-facing API (internal byte is 0-15)
273        for ch in 0..16u8 {
274            let msg = MidiMessage::new([0x90 | ch, 60, 100]);
275            assert_eq!(msg.get_channel(), ch + 1);
276        }
277    }
278
279    #[test]
280    fn test_note_frequency() {
281        // A4 (note 69) should be 440 Hz
282        let msg = MidiMessage::new([0x90, 69, 100]);
283        let freq = msg.get_note_frequency().unwrap();
284        assert!((freq - 440.0).abs() < 0.01);
285    }
286
287    #[test]
288    fn test_from_slice() {
289        // Test parsing from byte slice
290        let bytes: &[u8] = &[0x90, 60, 100];
291        let msg = MidiMessage::from(bytes);
292        assert_eq!(msg.get_status(), MidiMessageStatus::NoteOn);
293        assert_eq!(msg.get_note_number(), Some(60));
294    }
295
296    #[test]
297    fn test_pitch_wheel() {
298        // Pitch wheel message
299        let msg = MidiMessage::new([0xE0, 0x00, 0x40]);
300        assert_eq!(msg.get_status(), MidiMessageStatus::PitchWheel);
301        assert_eq!(msg.get_pitch_wheel_data(), Some((0x00, 0x40)));
302    }
303}