mseq_core/midi.rs
1use crate::note::Note;
2use serde::{Deserialize, Serialize};
3
4const CLOCK: u8 = 0xf8;
5const START: u8 = 0xfa;
6const CONTINUE: u8 = 0xfb;
7const STOP: u8 = 0xfc;
8const NOTE_ON: u8 = 0x90;
9const NOTE_OFF: u8 = 0x80;
10const CC: u8 = 0xB0;
11const PC: u8 = 0xC0;
12
13pub(crate) fn is_valid_channel(channel: u8) -> bool {
14 (1..=16).contains(&channel)
15}
16
17/// Note that can be sent through a MIDI message.
18#[derive(Default, Clone, Copy, PartialEq, Eq, Debug, Deserialize, Serialize, Hash)]
19pub struct MidiNote {
20 /// The chromatic note (A to G)
21 pub note: Note,
22 /// The octave of the note
23 pub octave: u8,
24 /// The velocity of the note (0 to 127)
25 pub vel: u8,
26}
27
28impl MidiNote {
29 /// Construct a new [`MidiNote`]
30 pub fn new(note: Note, octave: u8, vel: u8) -> Self {
31 Self { note, octave, vel }
32 }
33
34 /// Convert a MIDI note value into a [`MidiNote`].
35 pub fn from_midi_value(midi_value: u8, vel: u8) -> Self {
36 let octave = midi_value / 12;
37 let note = Note::from(midi_value % 12);
38 Self::new(note, octave, vel)
39 }
40
41 /// Transpose the [`MidiNote`].
42 /// The `transpose` parameter corresponds to the number of semitones to add to the note.
43 pub fn transpose(&self, transpose: i8) -> Self {
44 let (note, octave) = self.note.add_semitone(self.octave, transpose);
45 Self {
46 note,
47 octave,
48 vel: self.vel,
49 }
50 }
51
52 /// Retrieve the MIDI value of the MidiNote, which can be sent through a MIDI message.
53 pub fn midi_value(&self) -> u8 {
54 u8::from(self.note) + 12 * self.octave
55 }
56}
57
58/// Represents a parsed MIDI instruction.
59///
60/// This enum defines all supported MIDI messages used for input handling.
61#[derive(PartialEq, Debug, Clone, Copy)]
62pub enum MidiMessage {
63 /// Note Off event. This message is sent when a note is released.
64 NoteOff {
65 /// MIDI channel (1-16).
66 channel: u8,
67 /// The MIDI note.
68 note: MidiNote,
69 },
70 /// Note On event. This message is sent when a note is pressed.
71 NoteOn {
72 /// MIDI channel (1-16).
73 channel: u8,
74 /// The MIDI note.
75 note: MidiNote,
76 },
77 /// A MIDI Control Change (CC) message.
78 CC {
79 /// MIDI channel (1-16).
80 channel: u8,
81 /// The controller number (0–127).
82 controller: u8,
83 /// The controller value (0–127).
84 value: u8,
85 },
86 /// A MIDI Program Change (PC) message.
87 PC {
88 /// MIDI channel (1-16).
89 channel: u8,
90 /// The controller value (0–127).
91 value: u8,
92 },
93 /// Timing Clock. Sent 24 times per quarter note when synchronisation is required.
94 ///
95 /// Intercepted internally for transport synchronization.
96 Clock,
97 /// Start. Start the current sequence playing.
98 ///
99 /// Intercepted internally for transport synchronization.
100 Start,
101 /// Continue. Continue at the point the sequence was Stopped.
102 ///
103 /// Intercepted internally for transport synchronization.
104 Continue,
105 /// Stop. Stop the current sequence.
106 ///
107 /// Intercepted internally for transport synchronization.
108 Stop,
109}
110
111impl MidiMessage {
112 /// Parses a byte slice into a `MidiMessage` struct.
113 ///
114 /// This function is not intended to be called directly by end users.
115 /// It is used internally to ensure consistent MIDI message parsing logic across platforms.
116 ///
117 /// Returns `Some(MidiMessage)` if the byte slice represents a known and valid MIDI message,
118 /// or `None` if the data does not match any recognized MIDI message format.
119 pub fn parse(bytes: &[u8]) -> Option<MidiMessage> {
120 if bytes.len() == 1 {
121 match bytes[0] {
122 CLOCK => Some(MidiMessage::Clock),
123 START => Some(MidiMessage::Start),
124 CONTINUE => Some(MidiMessage::Continue),
125 STOP => Some(MidiMessage::Stop),
126 _ => None,
127 }
128 } else if bytes.len() == 2 && bytes[0] & 0xF0 == PC {
129 let channel = (bytes[0] & 0x0F) + 1;
130 if is_valid_channel(channel) {
131 Some(MidiMessage::PC {
132 channel,
133 value: bytes[1],
134 })
135 } else {
136 None
137 }
138 } else if bytes.len() == 3 {
139 let channel = (bytes[0] & 0x0F) + 1;
140 if is_valid_channel(channel) {
141 match bytes[0] & 0xF0 {
142 NOTE_OFF => Some(MidiMessage::NoteOff {
143 channel,
144 note: MidiNote::from_midi_value(bytes[1], bytes[2]),
145 }),
146 NOTE_ON => Some(MidiMessage::NoteOn {
147 channel,
148 note: MidiNote::from_midi_value(bytes[1], bytes[2]),
149 }),
150 CC => Some(MidiMessage::CC {
151 channel,
152 controller: bytes[1],
153 value: bytes[2],
154 }),
155 _ => None,
156 }
157 } else {
158 None
159 }
160 } else {
161 None
162 }
163 }
164}
165
166/// Performs a linear conversion from `[0.0, 1.0]` to [0, 127]. If `v` is smaller than `0.0` return
167/// 0. If `v` is greater than `1.0` return 127. The main purpose of this function is to be used with
168/// MIDI control changes (CC).
169pub fn param_value(v: f32) -> u8 {
170 if v < -1.0 {
171 return 0;
172 }
173 if v > 1.0 {
174 return 127;
175 }
176 63 + (v * 63.0) as u8
177}