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
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
178#[non_exhaustive]
179pub enum NoteExpressionType {
180 Volume,
182 Pan,
184 Tuning,
186 Vibrato,
188 Expression,
190 Brightness,
192 Custom(u32),
194}
195
196impl NoteExpressionType {
197 pub(crate) fn type_id(self) -> u32 {
199 match self {
200 NoteExpressionType::Volume => 0,
201 NoteExpressionType::Pan => 1,
202 NoteExpressionType::Tuning => 2,
203 NoteExpressionType::Vibrato => 3,
204 NoteExpressionType::Expression => 4,
205 NoteExpressionType::Brightness => 5,
206 NoteExpressionType::Custom(id) => id,
207 }
208 }
209
210 pub(crate) fn from_type_id(id: u32) -> Self {
212 match id {
213 0 => NoteExpressionType::Volume,
214 1 => NoteExpressionType::Pan,
215 2 => NoteExpressionType::Tuning,
216 3 => NoteExpressionType::Vibrato,
217 4 => NoteExpressionType::Expression,
218 5 => NoteExpressionType::Brightness,
219 other => NoteExpressionType::Custom(other),
220 }
221 }
222}
223
224#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
227pub struct NoteExpressionInfo {
228 pub kind: NoteExpressionType,
230 pub title: String,
232 pub short_title: String,
234 pub units: String,
236 pub default_value: f64,
238 pub min: f64,
240 pub max: f64,
242 pub step_count: i32,
244 pub is_bipolar: bool,
246 pub is_one_shot: bool,
248 pub is_absolute: bool,
250}
251
252impl MidiEvent {
253 pub fn from_midi_bytes(bytes: &[u8]) -> Option<MidiEvent> {
261 let status = *bytes.first()?;
262 if !(0x80..0xF0).contains(&status) {
265 return None;
266 }
267 let channel = MidiChannel::from_index(status & 0x0F)?;
268 let d1 = || bytes.get(1).map(|b| b & 0x7F);
269 let d2 = || bytes.get(2).map(|b| b & 0x7F);
270 match status & 0xF0 {
271 0x90 => {
272 let note = d1()?;
273 let velocity = d2()?;
274 Some(if velocity == 0 {
275 MidiEvent::NoteOff {
276 channel,
277 note,
278 velocity: 0,
279 }
280 } else {
281 MidiEvent::NoteOn {
282 channel,
283 note,
284 velocity,
285 }
286 })
287 }
288 0x80 => Some(MidiEvent::NoteOff {
289 channel,
290 note: d1()?,
291 velocity: d2()?,
292 }),
293 0xB0 => Some(MidiEvent::ControlChange {
294 channel,
295 controller: d1()?,
296 value: d2()?,
297 }),
298 0xA0 => Some(MidiEvent::PolyAftertouch {
299 channel,
300 note: d1()?,
301 pressure: d2()?,
302 }),
303 0xD0 => Some(MidiEvent::ChannelAftertouch {
304 channel,
305 pressure: d1()?,
306 }),
307 0xE0 => {
308 let value = (d2()? as u16) << 7 | d1()? as u16;
309 Some(MidiEvent::PitchBend { channel, value })
310 }
311 _ => None,
313 }
314 }
315}
316
317pub mod cc {
319 pub const BANK_SELECT_MSB: u8 = 0;
321 pub const MODULATION: u8 = 1;
323 pub const BREATH: u8 = 2;
325 pub const FOOT: u8 = 4;
327 pub const PORTAMENTO_TIME: u8 = 5;
329 pub const DATA_ENTRY_MSB: u8 = 6;
331 pub const VOLUME: u8 = 7;
333 pub const BALANCE: u8 = 8;
335 pub const PAN: u8 = 10;
337 pub const EXPRESSION: u8 = 11;
339 pub const SUSTAIN: u8 = 64;
341 pub const PORTAMENTO: u8 = 65;
343 pub const SOSTENUTO: u8 = 66;
345 pub const SOFT_PEDAL: u8 = 67;
347 pub const LEGATO: u8 = 68;
349 pub const HOLD_2: u8 = 69;
351 pub const SOUND_CONTROLLER_1: u8 = 70;
353 pub const SOUND_CONTROLLER_2: u8 = 71;
355 pub const SOUND_CONTROLLER_3: u8 = 72;
357 pub const SOUND_CONTROLLER_4: u8 = 73;
359 pub const SOUND_CONTROLLER_5: u8 = 74;
361 pub const SOUND_CONTROLLER_6: u8 = 75;
363 pub const SOUND_CONTROLLER_7: u8 = 76;
365 pub const SOUND_CONTROLLER_8: u8 = 77;
367 pub const SOUND_CONTROLLER_9: u8 = 78;
369 pub const SOUND_CONTROLLER_10: u8 = 79;
371 pub const GENERAL_PURPOSE_1: u8 = 80;
373 pub const GENERAL_PURPOSE_2: u8 = 81;
375 pub const GENERAL_PURPOSE_3: u8 = 82;
377 pub const GENERAL_PURPOSE_4: u8 = 83;
379 pub const PORTAMENTO_CONTROL: u8 = 84;
381 pub const REVERB_DEPTH: u8 = 91;
383 pub const TREMOLO_DEPTH: u8 = 92;
385 pub const CHORUS_DEPTH: u8 = 93;
387 pub const CELESTE_DEPTH: u8 = 94;
389 pub const PHASER_DEPTH: u8 = 95;
391 pub const DATA_INCREMENT: u8 = 96;
393 pub const DATA_DECREMENT: u8 = 97;
395 pub const NRPN_LSB: u8 = 98;
397 pub const NRPN_MSB: u8 = 99;
399 pub const RPN_LSB: u8 = 100;
401 pub const RPN_MSB: u8 = 101;
403 pub const ALL_SOUNDS_OFF: u8 = 120;
405 pub const RESET_ALL_CONTROLLERS: u8 = 121;
407 pub const LOCAL_CONTROL: u8 = 122;
409 pub const ALL_NOTES_OFF: u8 = 123;
411 pub const OMNI_MODE_OFF: u8 = 124;
413 pub const OMNI_MODE_ON: u8 = 125;
415 pub const MONO_MODE_ON: u8 = 126;
417 pub const POLY_MODE_ON: u8 = 127;
419}
420
421pub fn note_to_name(note: u8) -> String {
424 let note_names = [
425 "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
426 ];
427 let octave = (note as i32 / 12) - 2;
428 let note_in_octave = note % 12;
429 format!("{}{}", note_names[note_in_octave as usize], octave)
430}
431
432pub fn name_to_note(name: &str) -> Option<u8> {
436 let name = name.trim().to_uppercase();
437
438 let (note_part, octave_str) = if name.contains('#') {
440 let parts: Vec<&str> = name.split('#').collect();
441 if parts.len() != 2 {
442 return None;
443 }
444 (format!("{}#", parts[0]), parts[1])
445 } else if name.contains('B') && name.len() > 2 && &name[1..2] == "B" {
446 (format!("{}B", &name[0..1]), &name[2..])
448 } else {
449 let mut chars = name.chars();
451 let note = chars.next()?.to_string();
452 let octave = chars.as_str();
453 (note, octave)
454 };
455
456 let octave: i32 = octave_str.parse().ok()?;
458
459 let semitone = match note_part.as_str() {
461 "C" => 0,
462 "C#" | "DB" => 1,
463 "D" => 2,
464 "D#" | "EB" => 3,
465 "E" => 4,
466 "F" => 5,
467 "F#" | "GB" => 6,
468 "G" => 7,
469 "G#" | "AB" => 8,
470 "A" => 9,
471 "A#" | "BB" => 10,
472 "B" => 11,
473 _ => return None,
474 };
475
476 let midi_note = (octave + 2) * 12 + semitone;
479
480 if (0..=127).contains(&midi_note) {
481 Some(midi_note as u8)
482 } else {
483 None
484 }
485}
486
487#[cfg(test)]
488mod tests {
489 use super::*;
490
491 #[test]
492 fn note_expression_type_ids_round_trip() {
493 for kind in [
494 NoteExpressionType::Volume,
495 NoteExpressionType::Pan,
496 NoteExpressionType::Tuning,
497 NoteExpressionType::Vibrato,
498 NoteExpressionType::Expression,
499 NoteExpressionType::Brightness,
500 NoteExpressionType::Custom(100_001),
501 ] {
502 assert_eq!(NoteExpressionType::from_type_id(kind.type_id()), kind);
503 }
504 assert_eq!(NoteExpressionType::Tuning.type_id(), 2);
506 assert_eq!(
507 NoteExpressionType::from_type_id(5),
508 NoteExpressionType::Brightness
509 );
510 }
511
512 #[test]
513 fn from_midi_bytes_maps_channel_voice_messages() {
514 assert_eq!(
516 MidiEvent::from_midi_bytes(&[0x90, 60, 100]),
517 Some(MidiEvent::NoteOn {
518 channel: MidiChannel::Ch1,
519 note: 60,
520 velocity: 100
521 })
522 );
523 assert_eq!(
525 MidiEvent::from_midi_bytes(&[0x90, 60, 0]),
526 Some(MidiEvent::NoteOff {
527 channel: MidiChannel::Ch1,
528 note: 60,
529 velocity: 0
530 })
531 );
532 assert_eq!(
534 MidiEvent::from_midi_bytes(&[0x89, 64, 40]),
535 Some(MidiEvent::NoteOff {
536 channel: MidiChannel::Ch10,
537 note: 64,
538 velocity: 40
539 })
540 );
541 assert_eq!(
543 MidiEvent::from_midi_bytes(&[0xB0, 1, 64]),
544 Some(MidiEvent::ControlChange {
545 channel: MidiChannel::Ch1,
546 controller: 1,
547 value: 64
548 })
549 );
550 assert_eq!(
552 MidiEvent::from_midi_bytes(&[0xD0, 90]),
553 Some(MidiEvent::ChannelAftertouch {
554 channel: MidiChannel::Ch1,
555 pressure: 90
556 })
557 );
558 assert_eq!(
559 MidiEvent::from_midi_bytes(&[0xA0, 60, 70]),
560 Some(MidiEvent::PolyAftertouch {
561 channel: MidiChannel::Ch1,
562 note: 60,
563 pressure: 70
564 })
565 );
566 }
567
568 #[test]
569 fn from_midi_bytes_pitch_bend_is_14_bit() {
570 assert_eq!(
572 MidiEvent::from_midi_bytes(&[0xE0, 0, 64]),
573 Some(MidiEvent::PitchBend {
574 channel: MidiChannel::Ch1,
575 value: 8192
576 })
577 );
578 assert_eq!(
580 MidiEvent::from_midi_bytes(&[0xE0, 127, 127]),
581 Some(MidiEvent::PitchBend {
582 channel: MidiChannel::Ch1,
583 value: 16383
584 })
585 );
586 }
587
588 #[test]
589 fn from_midi_bytes_rejects_unsupported_and_junk() {
590 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); }
597
598 #[test]
599 fn test_midi_conversions() {
600 assert_eq!(name_to_note("C3"), Some(60));
602 assert_eq!(name_to_note("C2"), Some(48));
603 assert_eq!(name_to_note("A3"), Some(69)); assert_eq!(name_to_note("C-2"), Some(0));
605 assert_eq!(name_to_note("G8"), Some(127));
606
607 assert_eq!(note_to_name(60), "C3");
609 assert_eq!(note_to_name(48), "C2");
610 assert_eq!(note_to_name(69), "A3");
611 assert_eq!(note_to_name(0), "C-2");
612 assert_eq!(note_to_name(127), "G8");
613
614 assert_eq!(name_to_note("C#3"), Some(61));
616 assert_eq!(name_to_note("Db3"), Some(61));
617 assert_eq!(name_to_note("F#3"), Some(66));
618 }
619
620 #[test]
621 fn test_midi_channel() {
622 assert_eq!(MidiChannel::Ch1.as_index(), 0);
623 assert_eq!(MidiChannel::Ch16.as_index(), 15);
624 assert_eq!(MidiChannel::from_index(0), Some(MidiChannel::Ch1));
625 assert_eq!(MidiChannel::from_index(15), Some(MidiChannel::Ch16));
626 assert_eq!(MidiChannel::from_index(16), None);
627 }
628}