1#[derive(Debug, Clone, Copy)]
5pub struct MidiMessage {
6 pub timestamp: Option<u64>, pub message_type: MidiMessageType,
10 pub raw: [u8; 3],
12 pub len: u8,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq)]
18pub enum MidiMessageType {
19 NoteOn {
20 channel: u8,
21 note: u8,
22 velocity: u8,
23 },
24 NoteOff {
25 channel: u8,
26 note: u8,
27 velocity: u8,
28 },
29 ControlChange {
30 channel: u8,
31 controller: u8,
32 value: u8,
33 },
34 PitchBend {
35 channel: u8,
36 value: u16,
38 },
39 ProgramChange {
40 channel: u8,
41 program: u8,
42 },
43 ChannelPressure {
44 channel: u8,
45 pressure: u8,
46 },
47 Other,
49}
50
51impl MidiMessage {
52 pub fn from_bytes(bytes: &[u8], timestamp: u64) -> Option<Self> {
55 if bytes.is_empty() {
56 return None;
57 }
58
59 let mut raw = [0u8; 3];
60 let len = bytes.len().min(3);
61 raw[..len].copy_from_slice(&bytes[..len]);
62
63 let message_type = Self::parse_type(bytes);
64
65 Some(Self {
66 timestamp: Some(timestamp),
67 message_type,
68 raw,
69 len: len as u8,
70 })
71 }
72
73 fn parse_type(bytes: &[u8]) -> MidiMessageType {
74 if bytes.is_empty() {
75 return MidiMessageType::Other;
76 }
77
78 let status = bytes[0];
79 let kind = status & 0xF0;
80 let channel = status & 0x0F;
81
82 match kind {
83 0x90 if bytes.len() >= 3 => {
84 if bytes[2] == 0 {
85 MidiMessageType::NoteOff {
87 channel,
88 note: bytes[1],
89 velocity: 0,
90 }
91 } else {
92 MidiMessageType::NoteOn {
93 channel,
94 note: bytes[1],
95 velocity: bytes[2],
96 }
97 }
98 }
99 0x80 if bytes.len() >= 3 => MidiMessageType::NoteOff {
100 channel,
101 note: bytes[1],
102 velocity: bytes[2],
103 },
104 0xB0 if bytes.len() >= 3 => MidiMessageType::ControlChange {
105 channel,
106 controller: bytes[1],
107 value: bytes[2],
108 },
109 0xE0 if bytes.len() >= 3 => {
110 let value = (bytes[2] as u16) << 7 | (bytes[1] as u16);
111 MidiMessageType::PitchBend { channel, value }
112 }
113 0xC0 if bytes.len() >= 2 => MidiMessageType::ProgramChange {
114 channel,
115 program: bytes[1],
116 },
117 0xD0 if bytes.len() >= 2 => MidiMessageType::ChannelPressure {
118 channel,
119 pressure: bytes[1],
120 },
121 _ => MidiMessageType::Other,
122 }
123 }
124
125 pub fn note_to_freq(note: u8) -> f64 {
127 440.0 * 2.0f64.powf((note as f64 - 69.0) / 12.0)
128 }
129
130 pub fn note_to_name(note: u8) -> String {
132 const NAMES: [&str; 12] = [
133 "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
134 ];
135 let octave = (note as i8 / 12) - 1;
136 let name = NAMES[note as usize % 12];
137 format!("{name}{octave}")
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn parse_note_on() {
147 let msg = MidiMessage::from_bytes(&[0x90, 60, 100], 0).unwrap();
148 assert_eq!(
149 msg.message_type,
150 MidiMessageType::NoteOn {
151 channel: 0,
152 note: 60,
153 velocity: 100,
154 }
155 );
156 }
157
158 #[test]
159 fn parse_note_on_velocity_zero_is_note_off() {
160 let msg = MidiMessage::from_bytes(&[0x90, 60, 0], 0).unwrap();
161 assert_eq!(
162 msg.message_type,
163 MidiMessageType::NoteOff {
164 channel: 0,
165 note: 60,
166 velocity: 0,
167 }
168 );
169 }
170
171 #[test]
172 fn parse_note_off() {
173 let msg = MidiMessage::from_bytes(&[0x80, 60, 64], 0).unwrap();
174 assert_eq!(
175 msg.message_type,
176 MidiMessageType::NoteOff {
177 channel: 0,
178 note: 60,
179 velocity: 64,
180 }
181 );
182 }
183
184 #[test]
185 fn parse_control_change() {
186 let msg = MidiMessage::from_bytes(&[0xB3, 7, 127], 0).unwrap();
187 assert_eq!(
188 msg.message_type,
189 MidiMessageType::ControlChange {
190 channel: 3,
191 controller: 7,
192 value: 127,
193 }
194 );
195 }
196
197 #[test]
198 fn parse_pitch_bend() {
199 let msg = MidiMessage::from_bytes(&[0xE0, 0, 64], 0).unwrap();
201 assert_eq!(
202 msg.message_type,
203 MidiMessageType::PitchBend {
204 channel: 0,
205 value: 8192,
206 }
207 );
208 }
209
210 #[test]
211 fn parse_pitch_bend_extremes() {
212 let msg = MidiMessage::from_bytes(&[0xE0, 0, 0], 0).unwrap();
214 if let MidiMessageType::PitchBend { value, .. } = msg.message_type {
215 assert_eq!(value, 0);
216 }
217 let msg = MidiMessage::from_bytes(&[0xE0, 127, 127], 0).unwrap();
219 if let MidiMessageType::PitchBend { value, .. } = msg.message_type {
220 assert_eq!(value, 16383);
221 }
222 }
223
224 #[test]
225 fn parse_program_change() {
226 let msg = MidiMessage::from_bytes(&[0xC5, 42], 0).unwrap();
227 assert_eq!(
228 msg.message_type,
229 MidiMessageType::ProgramChange {
230 channel: 5,
231 program: 42,
232 }
233 );
234 }
235
236 #[test]
237 fn parse_empty_returns_none() {
238 assert!(MidiMessage::from_bytes(&[], 0).is_none());
239 }
240
241 #[test]
242 fn parse_truncated_note_on_is_other() {
243 let msg = MidiMessage::from_bytes(&[0x90], 0).unwrap();
244 assert_eq!(msg.message_type, MidiMessageType::Other);
245 }
246
247 #[test]
248 fn parse_unknown_status_is_other() {
249 let msg = MidiMessage::from_bytes(&[0xF0, 0x7E], 0).unwrap();
250 assert_eq!(msg.message_type, MidiMessageType::Other);
251 }
252
253 #[test]
254 fn note_to_freq_a4() {
255 let freq = MidiMessage::note_to_freq(69);
256 assert!((freq - 440.0).abs() < 0.01);
257 }
258
259 #[test]
260 fn note_to_freq_middle_c() {
261 let freq = MidiMessage::note_to_freq(60);
262 assert!((freq - 261.63).abs() < 0.1);
263 }
264
265 #[test]
266 fn note_to_name_middle_c() {
267 assert_eq!(MidiMessage::note_to_name(60), "C4");
268 }
269
270 #[test]
271 fn note_to_name_a4() {
272 assert_eq!(MidiMessage::note_to_name(69), "A4");
273 }
274
275 #[test]
276 fn all_channels_parsed_correctly() {
277 for ch in 0..16u8 {
278 let msg = MidiMessage::from_bytes(&[0x90 | ch, 60, 100], 0).unwrap();
279 if let MidiMessageType::NoteOn { channel, .. } = msg.message_type {
280 assert_eq!(channel, ch);
281 } else {
282 panic!("Expected NoteOn for channel {ch}");
283 }
284 }
285 }
286
287 #[test]
288 fn raw_bytes_preserved() {
289 let msg = MidiMessage::from_bytes(&[0x90, 60, 100], 12345).unwrap();
290 assert_eq!(msg.raw[0], 0x90);
291 assert_eq!(msg.raw[1], 60);
292 assert_eq!(msg.raw[2], 100);
293 assert_eq!(msg.len, 3);
294 assert_eq!(msg.timestamp, Some(12345));
295 }
296}