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}