1use serde::{Deserialize, Serialize};
4use std::fmt;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub enum MidiChannel {
9 Ch1,
11 Ch2,
13 Ch3,
15 Ch4,
17 Ch5,
19 Ch6,
21 Ch7,
23 Ch8,
25 Ch9,
27 Ch10,
29 Ch11,
31 Ch12,
33 Ch13,
35 Ch14,
37 Ch15,
39 Ch16,
41}
42
43impl MidiChannel {
44 pub fn as_index(&self) -> u8 {
46 match self {
47 MidiChannel::Ch1 => 0,
48 MidiChannel::Ch2 => 1,
49 MidiChannel::Ch3 => 2,
50 MidiChannel::Ch4 => 3,
51 MidiChannel::Ch5 => 4,
52 MidiChannel::Ch6 => 5,
53 MidiChannel::Ch7 => 6,
54 MidiChannel::Ch8 => 7,
55 MidiChannel::Ch9 => 8,
56 MidiChannel::Ch10 => 9,
57 MidiChannel::Ch11 => 10,
58 MidiChannel::Ch12 => 11,
59 MidiChannel::Ch13 => 12,
60 MidiChannel::Ch14 => 13,
61 MidiChannel::Ch15 => 14,
62 MidiChannel::Ch16 => 15,
63 }
64 }
65
66 pub fn from_index(index: u8) -> Option<Self> {
68 match index {
69 0 => Some(MidiChannel::Ch1),
70 1 => Some(MidiChannel::Ch2),
71 2 => Some(MidiChannel::Ch3),
72 3 => Some(MidiChannel::Ch4),
73 4 => Some(MidiChannel::Ch5),
74 5 => Some(MidiChannel::Ch6),
75 6 => Some(MidiChannel::Ch7),
76 7 => Some(MidiChannel::Ch8),
77 8 => Some(MidiChannel::Ch9),
78 9 => Some(MidiChannel::Ch10),
79 10 => Some(MidiChannel::Ch11),
80 11 => Some(MidiChannel::Ch12),
81 12 => Some(MidiChannel::Ch13),
82 13 => Some(MidiChannel::Ch14),
83 14 => Some(MidiChannel::Ch15),
84 15 => Some(MidiChannel::Ch16),
85 _ => None,
86 }
87 }
88}
89
90impl fmt::Display for MidiChannel {
91 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92 write!(f, "Ch{}", self.as_index() + 1)
93 }
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
101#[non_exhaustive]
102pub enum MidiEvent {
103 NoteOn {
105 channel: MidiChannel,
107 note: u8,
109 velocity: u8,
111 },
112 NoteOff {
114 channel: MidiChannel,
116 note: u8,
118 velocity: u8,
120 },
121 ControlChange {
123 channel: MidiChannel,
125 controller: u8,
127 value: u8,
129 },
130 ProgramChange {
132 channel: MidiChannel,
134 program: u8,
136 },
137 PitchBend {
139 channel: MidiChannel,
141 value: u16,
143 },
144 ChannelAftertouch {
146 channel: MidiChannel,
148 pressure: u8,
150 },
151 PolyAftertouch {
153 channel: MidiChannel,
155 note: u8,
157 pressure: u8,
159 },
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
166pub struct NoteId(pub(crate) i32);
167
168impl NoteId {
169 pub fn raw(self) -> i32 {
171 self.0
172 }
173
174 pub fn from_raw(raw: i32) -> Self {
180 NoteId(raw)
181 }
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
187#[non_exhaustive]
188pub enum NoteExpressionType {
189 Volume,
191 Pan,
193 Tuning,
195 Vibrato,
197 Expression,
199 Brightness,
201 Custom(u32),
203}
204
205impl NoteExpressionType {
206 pub(crate) fn type_id(self) -> u32 {
208 match self {
209 NoteExpressionType::Volume => 0,
210 NoteExpressionType::Pan => 1,
211 NoteExpressionType::Tuning => 2,
212 NoteExpressionType::Vibrato => 3,
213 NoteExpressionType::Expression => 4,
214 NoteExpressionType::Brightness => 5,
215 NoteExpressionType::Custom(id) => id,
216 }
217 }
218
219 pub(crate) fn from_type_id(id: u32) -> Self {
221 match id {
222 0 => NoteExpressionType::Volume,
223 1 => NoteExpressionType::Pan,
224 2 => NoteExpressionType::Tuning,
225 3 => NoteExpressionType::Vibrato,
226 4 => NoteExpressionType::Expression,
227 5 => NoteExpressionType::Brightness,
228 other => NoteExpressionType::Custom(other),
229 }
230 }
231}
232
233#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
236pub struct NoteExpressionInfo {
237 pub kind: NoteExpressionType,
239 pub title: String,
241 pub short_title: String,
243 pub units: String,
245 pub default_value: f64,
247 pub min: f64,
249 pub max: f64,
251 pub step_count: i32,
253 pub is_bipolar: bool,
255 pub is_one_shot: bool,
257 pub is_absolute: bool,
259}
260
261impl MidiEvent {
262 pub fn from_midi_bytes(bytes: &[u8]) -> Option<MidiEvent> {
270 let status = *bytes.first()?;
271 if !(0x80..0xF0).contains(&status) {
274 return None;
275 }
276 let channel = MidiChannel::from_index(status & 0x0F)?;
277 let d1 = || bytes.get(1).map(|b| b & 0x7F);
278 let d2 = || bytes.get(2).map(|b| b & 0x7F);
279 match status & 0xF0 {
280 0x90 => {
281 let note = d1()?;
282 let velocity = d2()?;
283 Some(if velocity == 0 {
284 MidiEvent::NoteOff {
285 channel,
286 note,
287 velocity: 0,
288 }
289 } else {
290 MidiEvent::NoteOn {
291 channel,
292 note,
293 velocity,
294 }
295 })
296 }
297 0x80 => Some(MidiEvent::NoteOff {
298 channel,
299 note: d1()?,
300 velocity: d2()?,
301 }),
302 0xB0 => Some(MidiEvent::ControlChange {
303 channel,
304 controller: d1()?,
305 value: d2()?,
306 }),
307 0xA0 => Some(MidiEvent::PolyAftertouch {
308 channel,
309 note: d1()?,
310 pressure: d2()?,
311 }),
312 0xD0 => Some(MidiEvent::ChannelAftertouch {
313 channel,
314 pressure: d1()?,
315 }),
316 0xE0 => {
317 let value = (d2()? as u16) << 7 | d1()? as u16;
318 Some(MidiEvent::PitchBend { channel, value })
319 }
320 _ => None,
322 }
323 }
324}
325
326pub mod cc {
328 pub const BANK_SELECT_MSB: u8 = 0;
330 pub const MODULATION: u8 = 1;
332 pub const BREATH: u8 = 2;
334 pub const FOOT: u8 = 4;
336 pub const PORTAMENTO_TIME: u8 = 5;
338 pub const DATA_ENTRY_MSB: u8 = 6;
340 pub const VOLUME: u8 = 7;
342 pub const BALANCE: u8 = 8;
344 pub const PAN: u8 = 10;
346 pub const EXPRESSION: u8 = 11;
348 pub const SUSTAIN: u8 = 64;
350 pub const PORTAMENTO: u8 = 65;
352 pub const SOSTENUTO: u8 = 66;
354 pub const SOFT_PEDAL: u8 = 67;
356 pub const LEGATO: u8 = 68;
358 pub const HOLD_2: u8 = 69;
360 pub const SOUND_CONTROLLER_1: u8 = 70;
362 pub const SOUND_CONTROLLER_2: u8 = 71;
364 pub const SOUND_CONTROLLER_3: u8 = 72;
366 pub const SOUND_CONTROLLER_4: u8 = 73;
368 pub const SOUND_CONTROLLER_5: u8 = 74;
370 pub const SOUND_CONTROLLER_6: u8 = 75;
372 pub const SOUND_CONTROLLER_7: u8 = 76;
374 pub const SOUND_CONTROLLER_8: u8 = 77;
376 pub const SOUND_CONTROLLER_9: u8 = 78;
378 pub const SOUND_CONTROLLER_10: u8 = 79;
380 pub const GENERAL_PURPOSE_1: u8 = 80;
382 pub const GENERAL_PURPOSE_2: u8 = 81;
384 pub const GENERAL_PURPOSE_3: u8 = 82;
386 pub const GENERAL_PURPOSE_4: u8 = 83;
388 pub const PORTAMENTO_CONTROL: u8 = 84;
390 pub const REVERB_DEPTH: u8 = 91;
392 pub const TREMOLO_DEPTH: u8 = 92;
394 pub const CHORUS_DEPTH: u8 = 93;
396 pub const CELESTE_DEPTH: u8 = 94;
398 pub const PHASER_DEPTH: u8 = 95;
400 pub const DATA_INCREMENT: u8 = 96;
402 pub const DATA_DECREMENT: u8 = 97;
404 pub const NRPN_LSB: u8 = 98;
406 pub const NRPN_MSB: u8 = 99;
408 pub const RPN_LSB: u8 = 100;
410 pub const RPN_MSB: u8 = 101;
412 pub const ALL_SOUNDS_OFF: u8 = 120;
414 pub const RESET_ALL_CONTROLLERS: u8 = 121;
416 pub const LOCAL_CONTROL: u8 = 122;
418 pub const ALL_NOTES_OFF: u8 = 123;
420 pub const OMNI_MODE_OFF: u8 = 124;
422 pub const OMNI_MODE_ON: u8 = 125;
424 pub const MONO_MODE_ON: u8 = 126;
426 pub const POLY_MODE_ON: u8 = 127;
428}
429
430pub fn note_to_name(note: u8) -> String {
433 let note_names = [
434 "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
435 ];
436 let octave = (note as i32 / 12) - 2;
437 let note_in_octave = note % 12;
438 format!("{}{}", note_names[note_in_octave as usize], octave)
439}
440
441pub fn name_to_note(name: &str) -> Option<u8> {
445 let name = name.trim().to_uppercase();
446
447 let (note_part, octave_str) = if name.contains('#') {
449 let parts: Vec<&str> = name.split('#').collect();
450 if parts.len() != 2 {
451 return None;
452 }
453 (format!("{}#", parts[0]), parts[1])
454 } else if name.contains('B') && name.len() > 2 && &name[1..2] == "B" {
455 (format!("{}B", &name[0..1]), &name[2..])
457 } else {
458 let mut chars = name.chars();
460 let note = chars.next()?.to_string();
461 let octave = chars.as_str();
462 (note, octave)
463 };
464
465 let octave: i32 = octave_str.parse().ok()?;
467
468 let semitone = match note_part.as_str() {
470 "C" => 0,
471 "C#" | "DB" => 1,
472 "D" => 2,
473 "D#" | "EB" => 3,
474 "E" => 4,
475 "F" => 5,
476 "F#" | "GB" => 6,
477 "G" => 7,
478 "G#" | "AB" => 8,
479 "A" => 9,
480 "A#" | "BB" => 10,
481 "B" => 11,
482 _ => return None,
483 };
484
485 let midi_note = (octave + 2) * 12 + semitone;
488
489 if (0..=127).contains(&midi_note) {
490 Some(midi_note as u8)
491 } else {
492 None
493 }
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499
500 #[test]
501 fn note_expression_type_ids_round_trip() {
502 for kind in [
503 NoteExpressionType::Volume,
504 NoteExpressionType::Pan,
505 NoteExpressionType::Tuning,
506 NoteExpressionType::Vibrato,
507 NoteExpressionType::Expression,
508 NoteExpressionType::Brightness,
509 NoteExpressionType::Custom(100_001),
510 ] {
511 assert_eq!(NoteExpressionType::from_type_id(kind.type_id()), kind);
512 }
513 assert_eq!(NoteExpressionType::Tuning.type_id(), 2);
515 assert_eq!(
516 NoteExpressionType::from_type_id(5),
517 NoteExpressionType::Brightness
518 );
519 }
520
521 #[test]
522 fn from_midi_bytes_maps_channel_voice_messages() {
523 assert_eq!(
525 MidiEvent::from_midi_bytes(&[0x90, 60, 100]),
526 Some(MidiEvent::NoteOn {
527 channel: MidiChannel::Ch1,
528 note: 60,
529 velocity: 100
530 })
531 );
532 assert_eq!(
534 MidiEvent::from_midi_bytes(&[0x90, 60, 0]),
535 Some(MidiEvent::NoteOff {
536 channel: MidiChannel::Ch1,
537 note: 60,
538 velocity: 0
539 })
540 );
541 assert_eq!(
543 MidiEvent::from_midi_bytes(&[0x89, 64, 40]),
544 Some(MidiEvent::NoteOff {
545 channel: MidiChannel::Ch10,
546 note: 64,
547 velocity: 40
548 })
549 );
550 assert_eq!(
552 MidiEvent::from_midi_bytes(&[0xB0, 1, 64]),
553 Some(MidiEvent::ControlChange {
554 channel: MidiChannel::Ch1,
555 controller: 1,
556 value: 64
557 })
558 );
559 assert_eq!(
561 MidiEvent::from_midi_bytes(&[0xD0, 90]),
562 Some(MidiEvent::ChannelAftertouch {
563 channel: MidiChannel::Ch1,
564 pressure: 90
565 })
566 );
567 assert_eq!(
568 MidiEvent::from_midi_bytes(&[0xA0, 60, 70]),
569 Some(MidiEvent::PolyAftertouch {
570 channel: MidiChannel::Ch1,
571 note: 60,
572 pressure: 70
573 })
574 );
575 }
576
577 #[test]
578 fn from_midi_bytes_pitch_bend_is_14_bit() {
579 assert_eq!(
581 MidiEvent::from_midi_bytes(&[0xE0, 0, 64]),
582 Some(MidiEvent::PitchBend {
583 channel: MidiChannel::Ch1,
584 value: 8192
585 })
586 );
587 assert_eq!(
589 MidiEvent::from_midi_bytes(&[0xE0, 127, 127]),
590 Some(MidiEvent::PitchBend {
591 channel: MidiChannel::Ch1,
592 value: 16383
593 })
594 );
595 }
596
597 #[test]
598 fn from_midi_bytes_rejects_unsupported_and_junk() {
599 assert_eq!(MidiEvent::from_midi_bytes(&[]), None); assert_eq!(MidiEvent::from_midi_bytes(&[0x60]), None); assert_eq!(MidiEvent::from_midi_bytes(&[0xF8]), None); assert_eq!(MidiEvent::from_midi_bytes(&[0xF0, 1, 2]), None); assert_eq!(MidiEvent::from_midi_bytes(&[0xC0, 5]), None); assert_eq!(MidiEvent::from_midi_bytes(&[0x90, 60]), None); }
606
607 #[test]
608 fn test_midi_conversions() {
609 assert_eq!(name_to_note("C3"), Some(60));
611 assert_eq!(name_to_note("C2"), Some(48));
612 assert_eq!(name_to_note("A3"), Some(69)); assert_eq!(name_to_note("C-2"), Some(0));
614 assert_eq!(name_to_note("G8"), Some(127));
615
616 assert_eq!(note_to_name(60), "C3");
618 assert_eq!(note_to_name(48), "C2");
619 assert_eq!(note_to_name(69), "A3");
620 assert_eq!(note_to_name(0), "C-2");
621 assert_eq!(note_to_name(127), "G8");
622
623 assert_eq!(name_to_note("C#3"), Some(61));
625 assert_eq!(name_to_note("Db3"), Some(61));
626 assert_eq!(name_to_note("F#3"), Some(66));
627 }
628
629 #[test]
630 fn test_midi_channel() {
631 assert_eq!(MidiChannel::Ch1.as_index(), 0);
632 assert_eq!(MidiChannel::Ch16.as_index(), 15);
633 assert_eq!(MidiChannel::from_index(0), Some(MidiChannel::Ch1));
634 assert_eq!(MidiChannel::from_index(15), Some(MidiChannel::Ch16));
635 assert_eq!(MidiChannel::from_index(16), None);
636 }
637}