Skip to main content

vst3_host/
midi.rs

1//! MIDI types and utilities for VST3 host
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6/// MIDI channel enumeration (1-16)
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub enum MidiChannel {
9    /// Channel 1
10    Ch1,
11    /// Channel 2
12    Ch2,
13    /// Channel 3
14    Ch3,
15    /// Channel 4
16    Ch4,
17    /// Channel 5
18    Ch5,
19    /// Channel 6
20    Ch6,
21    /// Channel 7
22    Ch7,
23    /// Channel 8
24    Ch8,
25    /// Channel 9
26    Ch9,
27    /// Channel 10 (often drums in GM)
28    Ch10,
29    /// Channel 11
30    Ch11,
31    /// Channel 12
32    Ch12,
33    /// Channel 13
34    Ch13,
35    /// Channel 14
36    Ch14,
37    /// Channel 15
38    Ch15,
39    /// Channel 16
40    Ch16,
41}
42
43impl MidiChannel {
44    /// Get the channel as a 0-based index (0-15)
45    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    /// Create from 0-based index (0-15)
67    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/// High-level MIDI event types.
97///
98/// Marked `#[non_exhaustive]`: match with a wildcard arm, as new event kinds (e.g. SysEx)
99/// may be added in future versions without it being a breaking change.
100#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
101#[non_exhaustive]
102pub enum MidiEvent {
103    /// Note On event
104    NoteOn {
105        /// MIDI channel (1-16)
106        channel: MidiChannel,
107        /// Note number (0-127)
108        note: u8,
109        /// Velocity (0-127)
110        velocity: u8,
111    },
112    /// Note Off event
113    NoteOff {
114        /// MIDI channel (1-16)
115        channel: MidiChannel,
116        /// Note number (0-127)
117        note: u8,
118        /// Velocity (0-127)
119        velocity: u8,
120    },
121    /// Control Change event
122    ControlChange {
123        /// MIDI channel (1-16)
124        channel: MidiChannel,
125        /// Controller number (0-127)
126        controller: u8,
127        /// Value (0-127)
128        value: u8,
129    },
130    /// Program Change event
131    ProgramChange {
132        /// MIDI channel (1-16)
133        channel: MidiChannel,
134        /// Program number (0-127)
135        program: u8,
136    },
137    /// Pitch Bend event
138    PitchBend {
139        /// MIDI channel (1-16)
140        channel: MidiChannel,
141        /// Pitch bend value (0-16383, center is 8192)
142        value: u16,
143    },
144    /// Channel Aftertouch event
145    ChannelAftertouch {
146        /// MIDI channel (1-16)
147        channel: MidiChannel,
148        /// Pressure value (0-127)
149        pressure: u8,
150    },
151    /// Polyphonic Aftertouch event
152    PolyAftertouch {
153        /// MIDI channel (1-16)
154        channel: MidiChannel,
155        /// Note number (0-127)
156        note: u8,
157        /// Pressure value (0-127)
158        pressure: u8,
159    },
160}
161
162/// Maximum pointer-backed payload accepted for one VST3 event.
163///
164/// This bounds both in-process plugin output and process-isolation messages. A SysEx message
165/// larger than this is rejected instead of allowing an untrusted plugin or IPC peer to force an
166/// unbounded allocation in the host.
167pub const MAX_EVENT_PAYLOAD_BYTES: usize = 1024 * 1024;
168
169/// Maximum UTF-16 code units accepted from a pointer-backed VST3 event.
170pub const MAX_EVENT_TEXT_UNITS: usize = 16 * 1024;
171
172/// A fully owned VST3 event.
173///
174/// Unlike the SDK's raw `Event` union, pointer-backed payloads live in this value and remain safe
175/// to queue, move between threads, or serialize across process isolation.
176#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
177pub struct PluginEvent {
178    /// Event-bus index.
179    pub bus_index: i32,
180    /// Sample offset within the next process block.
181    pub sample_offset: i32,
182    /// Musical position in quarter notes, when known.
183    pub ppq_position: f64,
184    /// Raw VST3 event flags.
185    pub flags: u16,
186    /// Event payload.
187    pub data: PluginEventData,
188}
189
190/// An event emitted by a plugin.
191///
192/// Input and output use the same owned representation, so this alias mainly documents direction
193/// at API boundaries such as [`Plugin::take_output_events`](crate::Plugin::take_output_events).
194pub type OutputEvent = PluginEvent;
195
196/// The owned payload of a [`PluginEvent`].
197#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
198#[non_exhaustive]
199#[allow(missing_docs)]
200pub enum PluginEventData {
201    /// VST3 note-on event.
202    NoteOn {
203        channel: i16,
204        pitch: i16,
205        tuning: f32,
206        velocity: f32,
207        length: i32,
208        note_id: i32,
209    },
210    /// VST3 note-off event.
211    NoteOff {
212        channel: i16,
213        pitch: i16,
214        velocity: f32,
215        note_id: i32,
216        tuning: f32,
217    },
218    /// Pointer-backed data event. VST3 currently defines data type `0` as MIDI SysEx.
219    Data { data_type: u32, bytes: Vec<u8> },
220    /// Polyphonic pressure.
221    PolyPressure {
222        channel: i16,
223        pitch: i16,
224        pressure: f32,
225        note_id: i32,
226    },
227    /// Floating-point per-note expression.
228    NoteExpressionValue {
229        type_id: u32,
230        note_id: i32,
231        value: f64,
232    },
233    /// UTF-16 per-note expression text.
234    NoteExpressionText {
235        type_id: u32,
236        note_id: i32,
237        text: Vec<u16>,
238    },
239    /// Integer per-note expression.
240    NoteExpressionIntValue {
241        type_id: u32,
242        note_id: i32,
243        value: u64,
244    },
245    /// Chord event with owned UTF-16 display text.
246    Chord {
247        root: i16,
248        bass_note: i16,
249        mask: i16,
250        text: Vec<u16>,
251    },
252    /// Scale event with owned UTF-16 display text.
253    Scale {
254        root: i16,
255        mask: i16,
256        text: Vec<u16>,
257    },
258    /// Legacy MIDI output event. This is valid only as plugin output.
259    LegacyMidiCcOut {
260        control_number: u8,
261        channel: i8,
262        value: u8,
263        value2: u8,
264    },
265}
266
267impl PluginEvent {
268    /// Construct a MIDI SysEx data event on bus 0 at block start.
269    pub fn sysex(bytes: Vec<u8>) -> Self {
270        Self {
271            bus_index: 0,
272            sample_offset: 0,
273            ppq_position: 0.0,
274            flags: 0,
275            data: PluginEventData::Data {
276                data_type: 0,
277                bytes,
278            },
279        }
280    }
281
282    /// Set the sample offset for this event.
283    pub fn at(mut self, sample_offset: i32) -> Self {
284        self.sample_offset = sample_offset;
285        self
286    }
287
288    /// Convert a channel-voice event into the compatibility [`MidiEvent`] model.
289    ///
290    /// SysEx, note-expression, chord, and scale events intentionally return `None`; callers that
291    /// need those events should use the owned event API.
292    pub fn to_midi(&self) -> Option<MidiEvent> {
293        let byte = |value: f32| (value * 127.0).round().clamp(0.0, 127.0) as u8;
294        match &self.data {
295            PluginEventData::NoteOn {
296                channel,
297                pitch,
298                velocity,
299                ..
300            } => Some(MidiEvent::NoteOn {
301                channel: MidiChannel::from_index(u8::try_from(*channel).ok()?)?,
302                note: u8::try_from(*pitch).ok().filter(|pitch| *pitch <= 127)?,
303                velocity: byte(*velocity),
304            }),
305            PluginEventData::NoteOff {
306                channel,
307                pitch,
308                velocity,
309                ..
310            } => Some(MidiEvent::NoteOff {
311                channel: MidiChannel::from_index(u8::try_from(*channel).ok()?)?,
312                note: u8::try_from(*pitch).ok().filter(|pitch| *pitch <= 127)?,
313                velocity: byte(*velocity),
314            }),
315            PluginEventData::PolyPressure {
316                channel,
317                pitch,
318                pressure,
319                ..
320            } => Some(MidiEvent::PolyAftertouch {
321                channel: MidiChannel::from_index(u8::try_from(*channel).ok()?)?,
322                note: u8::try_from(*pitch).ok().filter(|pitch| *pitch <= 127)?,
323                pressure: byte(*pressure),
324            }),
325            PluginEventData::LegacyMidiCcOut {
326                control_number,
327                channel,
328                value,
329                value2,
330            } => {
331                let channel = MidiChannel::from_index(u8::try_from(*channel).ok()?)?;
332                match u32::from(*control_number) {
333                    129 => Some(MidiEvent::PitchBend {
334                        channel,
335                        value: (u16::from(*value2 & 0x7f) << 7) | u16::from(*value & 0x7f),
336                    }),
337                    128 => Some(MidiEvent::ChannelAftertouch {
338                        channel,
339                        pressure: *value & 0x7f,
340                    }),
341                    130 => Some(MidiEvent::ProgramChange {
342                        channel,
343                        program: *value & 0x7f,
344                    }),
345                    cc if cc < 128 => Some(MidiEvent::ControlChange {
346                        channel,
347                        controller: cc as u8,
348                        value: *value & 0x7f,
349                    }),
350                    _ => None,
351                }
352            }
353            _ => None,
354        }
355    }
356
357    /// Bytes owned by pointer-backed fields in this event.
358    pub(crate) fn payload_bytes(&self) -> usize {
359        match &self.data {
360            PluginEventData::Data { bytes, .. } => bytes.len(),
361            PluginEventData::NoteExpressionText { text, .. }
362            | PluginEventData::Chord { text, .. }
363            | PluginEventData::Scale { text, .. } => text.len().saturating_mul(2),
364            _ => 0,
365        }
366    }
367}
368
369impl From<MidiEvent> for PluginEvent {
370    fn from(event: MidiEvent) -> Self {
371        let data = match event {
372            MidiEvent::NoteOn {
373                channel,
374                note,
375                velocity,
376            } => PluginEventData::NoteOn {
377                channel: i16::from(channel.as_index()),
378                pitch: i16::from(note),
379                tuning: 0.0,
380                velocity: f32::from(velocity) / 127.0,
381                length: 0,
382                note_id: -1,
383            },
384            MidiEvent::NoteOff {
385                channel,
386                note,
387                velocity,
388            } => PluginEventData::NoteOff {
389                channel: i16::from(channel.as_index()),
390                pitch: i16::from(note),
391                velocity: f32::from(velocity) / 127.0,
392                note_id: -1,
393                tuning: 0.0,
394            },
395            MidiEvent::ControlChange {
396                channel,
397                controller,
398                value,
399            } => PluginEventData::LegacyMidiCcOut {
400                control_number: controller,
401                channel: channel.as_index() as i8,
402                value,
403                value2: 0,
404            },
405            MidiEvent::ProgramChange { channel, program } => PluginEventData::LegacyMidiCcOut {
406                control_number: 130,
407                channel: channel.as_index() as i8,
408                value: program,
409                value2: 0,
410            },
411            MidiEvent::PitchBend { channel, value } => PluginEventData::LegacyMidiCcOut {
412                control_number: 129,
413                channel: channel.as_index() as i8,
414                value: (value & 0x7f) as u8,
415                value2: ((value >> 7) & 0x7f) as u8,
416            },
417            MidiEvent::ChannelAftertouch { channel, pressure } => {
418                PluginEventData::LegacyMidiCcOut {
419                    control_number: 128,
420                    channel: channel.as_index() as i8,
421                    value: pressure,
422                    value2: 0,
423                }
424            }
425            MidiEvent::PolyAftertouch {
426                channel,
427                note,
428                pressure,
429            } => PluginEventData::PolyPressure {
430                channel: i16::from(channel.as_index()),
431                pitch: i16::from(note),
432                pressure: f32::from(pressure) / 127.0,
433                note_id: -1,
434            },
435        };
436        Self {
437            bus_index: 0,
438            sample_offset: 0,
439            ppq_position: 0.0,
440            flags: 0,
441            data,
442        }
443    }
444}
445
446/// An opaque per-voice handle returned by [`Plugin::note_on`](crate::Plugin::note_on), used to
447/// target note-expression events (and the note-off) at a specific sounding note — the basis for
448/// MPE-style per-note control.
449#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
450pub struct NoteId(pub(crate) i32);
451
452impl NoteId {
453    /// The raw VST3 note id.
454    pub fn raw(self) -> i32 {
455        self.0
456    }
457
458    /// Reconstruct a [`NoteId`] from a raw VST3 note id.
459    ///
460    /// A `NoteId` is normally minted by [`Plugin::note_on`](crate::Plugin::note_on); this is
461    /// the inverse of [`raw`](Self::raw), used to carry an id across the process-isolation
462    /// boundary (the helper owns the plugin and allocates the id; the host re-wraps it).
463    pub fn from_raw(raw: i32) -> Self {
464        NoteId(raw)
465    }
466}
467
468/// A VST3 per-note expression dimension. Values are normalized `0.0..=1.0`; the bipolar
469/// dimensions (Pan, Tuning) center at `0.5`.
470#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
471#[non_exhaustive]
472pub enum NoteExpressionType {
473    /// Per-note volume (`kVolumeTypeID`).
474    Volume,
475    /// Per-note pan, bipolar (`kPanTypeID`).
476    Pan,
477    /// Per-note tuning / pitch, bipolar (`kTuningTypeID`).
478    Tuning,
479    /// Per-note vibrato (`kVibratoTypeID`).
480    Vibrato,
481    /// Per-note expression (`kExpressionTypeID`).
482    Expression,
483    /// Per-note brightness / timbre (`kBrightnessTypeID`).
484    Brightness,
485    /// A plugin-defined custom expression type id (`kCustomStart..kCustomEnd`).
486    Custom(u32),
487}
488
489impl NoteExpressionType {
490    /// The VST3 `NoteExpressionTypeID` for this dimension.
491    pub(crate) fn type_id(self) -> u32 {
492        match self {
493            NoteExpressionType::Volume => 0,
494            NoteExpressionType::Pan => 1,
495            NoteExpressionType::Tuning => 2,
496            NoteExpressionType::Vibrato => 3,
497            NoteExpressionType::Expression => 4,
498            NoteExpressionType::Brightness => 5,
499            NoteExpressionType::Custom(id) => id,
500        }
501    }
502
503    /// Map a VST3 `NoteExpressionTypeID` back to a type (unknown ids become `Custom`).
504    pub(crate) fn from_type_id(id: u32) -> Self {
505        match id {
506            0 => NoteExpressionType::Volume,
507            1 => NoteExpressionType::Pan,
508            2 => NoteExpressionType::Tuning,
509            3 => NoteExpressionType::Vibrato,
510            4 => NoteExpressionType::Expression,
511            5 => NoteExpressionType::Brightness,
512            other => NoteExpressionType::Custom(other),
513        }
514    }
515}
516
517/// A note-expression dimension a plugin advertises via `INoteExpressionController`
518/// (from [`Plugin::note_expressions`](crate::Plugin::note_expressions)).
519#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
520pub struct NoteExpressionInfo {
521    /// Which expression dimension this is.
522    pub kind: NoteExpressionType,
523    /// Display title (e.g. "Tuning").
524    pub title: String,
525    /// Short title.
526    pub short_title: String,
527    /// Units string (may be empty).
528    pub units: String,
529    /// Default normalized value.
530    pub default_value: f64,
531    /// Minimum normalized value.
532    pub min: f64,
533    /// Maximum normalized value.
534    pub max: f64,
535    /// Discrete step count (0 = continuous).
536    pub step_count: i32,
537    /// Whether the dimension is bipolar (centered at 0.5).
538    pub is_bipolar: bool,
539    /// Whether it's a one-shot (applied once at note start).
540    pub is_one_shot: bool,
541    /// Whether the value is absolute (vs relative to the note's base).
542    pub is_absolute: bool,
543}
544
545impl MidiEvent {
546    /// Parse a single channel-voice MIDI message from raw bytes (status + data), as delivered
547    /// by a MIDI input device.
548    ///
549    /// Maps Note On/Off (a Note On with velocity 0 becomes a Note Off), Control Change,
550    /// Pitch Bend (14-bit), channel/poly aftertouch, and Program Change. Returns `None` for
551    /// empty/truncated input, running-status messages (no leading status byte), and
552    /// system/realtime/SysEx messages.
553    pub fn from_midi_bytes(bytes: &[u8]) -> Option<MidiEvent> {
554        let status = *bytes.first()?;
555        // Require a channel-voice status byte (0x80..=0xEF); reject data bytes (running status)
556        // and system/realtime messages (0xF0..=0xFF).
557        if !(0x80..0xF0).contains(&status) {
558            return None;
559        }
560        let channel = MidiChannel::from_index(status & 0x0F)?;
561        let d1 = || bytes.get(1).map(|b| b & 0x7F);
562        let d2 = || bytes.get(2).map(|b| b & 0x7F);
563        match status & 0xF0 {
564            0x90 => {
565                let note = d1()?;
566                let velocity = d2()?;
567                Some(if velocity == 0 {
568                    MidiEvent::NoteOff {
569                        channel,
570                        note,
571                        velocity: 0,
572                    }
573                } else {
574                    MidiEvent::NoteOn {
575                        channel,
576                        note,
577                        velocity,
578                    }
579                })
580            }
581            0x80 => Some(MidiEvent::NoteOff {
582                channel,
583                note: d1()?,
584                velocity: d2()?,
585            }),
586            0xB0 => Some(MidiEvent::ControlChange {
587                channel,
588                controller: d1()?,
589                value: d2()?,
590            }),
591            0xA0 => Some(MidiEvent::PolyAftertouch {
592                channel,
593                note: d1()?,
594                pressure: d2()?,
595            }),
596            0xD0 => Some(MidiEvent::ChannelAftertouch {
597                channel,
598                pressure: d1()?,
599            }),
600            0xE0 => {
601                let value = (d2()? as u16) << 7 | d1()? as u16;
602                Some(MidiEvent::PitchBend { channel, value })
603            }
604            0xC0 => Some(MidiEvent::ProgramChange {
605                channel,
606                program: d1()?,
607            }),
608            _ => None,
609        }
610    }
611}
612
613/// Common MIDI control change numbers
614pub mod cc {
615    /// Bank Select MSB
616    pub const BANK_SELECT_MSB: u8 = 0;
617    /// Modulation Wheel
618    pub const MODULATION: u8 = 1;
619    /// Breath Controller
620    pub const BREATH: u8 = 2;
621    /// Foot Controller
622    pub const FOOT: u8 = 4;
623    /// Portamento Time
624    pub const PORTAMENTO_TIME: u8 = 5;
625    /// Data Entry MSB
626    pub const DATA_ENTRY_MSB: u8 = 6;
627    /// Channel Volume
628    pub const VOLUME: u8 = 7;
629    /// Balance
630    pub const BALANCE: u8 = 8;
631    /// Pan
632    pub const PAN: u8 = 10;
633    /// Expression
634    pub const EXPRESSION: u8 = 11;
635    /// Sustain Pedal
636    pub const SUSTAIN: u8 = 64;
637    /// Portamento On/Off
638    pub const PORTAMENTO: u8 = 65;
639    /// Sostenuto
640    pub const SOSTENUTO: u8 = 66;
641    /// Soft Pedal
642    pub const SOFT_PEDAL: u8 = 67;
643    /// Legato Footswitch
644    pub const LEGATO: u8 = 68;
645    /// Hold 2
646    pub const HOLD_2: u8 = 69;
647    /// Sound Controller 1 (default: Sound Variation)
648    pub const SOUND_CONTROLLER_1: u8 = 70;
649    /// Sound Controller 2 (default: Timbre/Harmonic Content)
650    pub const SOUND_CONTROLLER_2: u8 = 71;
651    /// Sound Controller 3 (default: Release Time)
652    pub const SOUND_CONTROLLER_3: u8 = 72;
653    /// Sound Controller 4 (default: Attack Time)
654    pub const SOUND_CONTROLLER_4: u8 = 73;
655    /// Sound Controller 5 (default: Brightness)
656    pub const SOUND_CONTROLLER_5: u8 = 74;
657    /// Sound Controller 6-10
658    pub const SOUND_CONTROLLER_6: u8 = 75;
659    /// Sound controller 7
660    pub const SOUND_CONTROLLER_7: u8 = 76;
661    /// Sound controller 8
662    pub const SOUND_CONTROLLER_8: u8 = 77;
663    /// Sound controller 9
664    pub const SOUND_CONTROLLER_9: u8 = 78;
665    /// Sound controller 10
666    pub const SOUND_CONTROLLER_10: u8 = 79;
667    /// General Purpose Controllers
668    pub const GENERAL_PURPOSE_1: u8 = 80;
669    /// General purpose controller 2
670    pub const GENERAL_PURPOSE_2: u8 = 81;
671    /// General purpose controller 3
672    pub const GENERAL_PURPOSE_3: u8 = 82;
673    /// General purpose controller 4
674    pub const GENERAL_PURPOSE_4: u8 = 83;
675    /// Portamento Control
676    pub const PORTAMENTO_CONTROL: u8 = 84;
677    /// Effects Depth
678    pub const REVERB_DEPTH: u8 = 91;
679    /// Tremolo depth
680    pub const TREMOLO_DEPTH: u8 = 92;
681    /// Chorus depth
682    pub const CHORUS_DEPTH: u8 = 93;
683    /// Celeste depth
684    pub const CELESTE_DEPTH: u8 = 94;
685    /// Phaser depth
686    pub const PHASER_DEPTH: u8 = 95;
687    /// Data Increment
688    pub const DATA_INCREMENT: u8 = 96;
689    /// Data Decrement
690    pub const DATA_DECREMENT: u8 = 97;
691    /// NRPN LSB
692    pub const NRPN_LSB: u8 = 98;
693    /// NRPN MSB
694    pub const NRPN_MSB: u8 = 99;
695    /// RPN LSB
696    pub const RPN_LSB: u8 = 100;
697    /// RPN MSB
698    pub const RPN_MSB: u8 = 101;
699    /// All Sounds Off
700    pub const ALL_SOUNDS_OFF: u8 = 120;
701    /// Reset All Controllers
702    pub const RESET_ALL_CONTROLLERS: u8 = 121;
703    /// Local Control On/Off
704    pub const LOCAL_CONTROL: u8 = 122;
705    /// All Notes Off
706    pub const ALL_NOTES_OFF: u8 = 123;
707    /// Omni Mode Off
708    pub const OMNI_MODE_OFF: u8 = 124;
709    /// Omni Mode On
710    pub const OMNI_MODE_ON: u8 = 125;
711    /// Mono Mode On
712    pub const MONO_MODE_ON: u8 = 126;
713    /// Poly Mode On
714    pub const POLY_MODE_ON: u8 = 127;
715}
716
717/// Convert a MIDI note number to its note name, using the convention where C3 = MIDI 60.
718///
719/// The MIDI note domain is `0..=127`; the parameter is a `u8`, so larger values are
720/// representable and are rendered as `"Invalid(<n>)"`. They used to be given a fabricated
721/// name (`"D#19"` for 255) that [`name_to_note`] correctly rejects, silently breaking the
722/// round trip at the boundary where a note number becomes text and back.
723pub fn note_to_name(note: u8) -> String {
724    if note > 127 {
725        return format!("Invalid({note})");
726    }
727    let note_names = [
728        "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
729    ];
730    let octave = (note as i32 / 12) - 2;
731    let note_in_octave = note % 12;
732    format!("{}{}", note_names[note_in_octave as usize], octave)
733}
734
735/// Convert note name to MIDI note number
736/// Accepts formats like "C3", "C#4", "Db3", etc.
737/// Using the convention where C3 = MIDI 60
738pub fn name_to_note(name: &str) -> Option<u8> {
739    let name = name.trim().to_uppercase();
740
741    // Parse by chars, never by byte index: `to_uppercase` can produce multi-byte chars (an
742    // accented letter, say), and slicing those at byte 1 would panic rather than returning None.
743    let mut chars = name.chars();
744    let letter = chars.next()?;
745    if !letter.is_ascii_alphabetic() {
746        return None;
747    }
748
749    // An optional accidental follows the letter: '#' (sharp) or 'B' (flat, as in "Db").
750    // A bare "B..." is the note B, not a flat — only treat 'B' as an accidental when it is the
751    // *second* char.
752    let rest = chars.as_str();
753    let (accidental, octave_str) = match rest.chars().next() {
754        Some('#') => (Some('#'), &rest[1..]),
755        Some('B') => (Some('B'), &rest[1..]),
756        _ => (None, rest),
757    };
758
759    // Parse octave
760    let octave: i32 = octave_str.parse().ok()?;
761
762    // Convert note to semitone offset within octave
763    let semitone = match (letter, accidental) {
764        ('C', None) => 0,
765        ('C', Some('#')) | ('D', Some('B')) => 1,
766        ('D', None) => 2,
767        ('D', Some('#')) | ('E', Some('B')) => 3,
768        ('E', None) => 4,
769        ('F', None) => 5,
770        ('F', Some('#')) | ('G', Some('B')) => 6,
771        ('G', None) => 7,
772        ('G', Some('#')) | ('A', Some('B')) => 8,
773        ('A', None) => 9,
774        ('A', Some('#')) | ('B', Some('B')) => 10,
775        ('B', None) => 11,
776        _ => return None,
777    };
778
779    // Calculate MIDI note number, using the convention where C3 = MIDI 60.
780    // Checked: the octave comes from parsing arbitrary text, and `(octave + 2) * 12` overflows for
781    // extremes like "C2147483647" — a panic in debug, a wrapped (wrong) note in release.
782    let midi_note = octave
783        .checked_add(2)
784        .and_then(|o| o.checked_mul(12))
785        .and_then(|base| base.checked_add(semitone))?;
786
787    if (0..=127).contains(&midi_note) {
788        Some(midi_note as u8)
789    } else {
790        None
791    }
792}
793
794#[cfg(test)]
795mod tests {
796    use super::*;
797
798    #[test]
799    fn note_expression_type_ids_round_trip() {
800        for kind in [
801            NoteExpressionType::Volume,
802            NoteExpressionType::Pan,
803            NoteExpressionType::Tuning,
804            NoteExpressionType::Vibrato,
805            NoteExpressionType::Expression,
806            NoteExpressionType::Brightness,
807            NoteExpressionType::Custom(100_001),
808        ] {
809            assert_eq!(NoteExpressionType::from_type_id(kind.type_id()), kind);
810        }
811        // The well-known VST3 type ids.
812        assert_eq!(NoteExpressionType::Tuning.type_id(), 2);
813        assert_eq!(
814            NoteExpressionType::from_type_id(5),
815            NoteExpressionType::Brightness
816        );
817    }
818
819    #[test]
820    fn from_midi_bytes_maps_channel_voice_messages() {
821        // Note on (ch 1, note 60, vel 100).
822        assert_eq!(
823            MidiEvent::from_midi_bytes(&[0x90, 60, 100]),
824            Some(MidiEvent::NoteOn {
825                channel: MidiChannel::Ch1,
826                note: 60,
827                velocity: 100
828            })
829        );
830        // Note on velocity 0 => note off.
831        assert_eq!(
832            MidiEvent::from_midi_bytes(&[0x90, 60, 0]),
833            Some(MidiEvent::NoteOff {
834                channel: MidiChannel::Ch1,
835                note: 60,
836                velocity: 0
837            })
838        );
839        // Note off on channel 10.
840        assert_eq!(
841            MidiEvent::from_midi_bytes(&[0x89, 64, 40]),
842            Some(MidiEvent::NoteOff {
843                channel: MidiChannel::Ch10,
844                note: 64,
845                velocity: 40
846            })
847        );
848        // CC.
849        assert_eq!(
850            MidiEvent::from_midi_bytes(&[0xB0, 1, 64]),
851            Some(MidiEvent::ControlChange {
852                channel: MidiChannel::Ch1,
853                controller: 1,
854                value: 64
855            })
856        );
857        // Channel + poly aftertouch.
858        assert_eq!(
859            MidiEvent::from_midi_bytes(&[0xD0, 90]),
860            Some(MidiEvent::ChannelAftertouch {
861                channel: MidiChannel::Ch1,
862                pressure: 90
863            })
864        );
865        assert_eq!(
866            MidiEvent::from_midi_bytes(&[0xA0, 60, 70]),
867            Some(MidiEvent::PolyAftertouch {
868                channel: MidiChannel::Ch1,
869                note: 60,
870                pressure: 70
871            })
872        );
873    }
874
875    #[test]
876    fn from_midi_bytes_pitch_bend_is_14_bit() {
877        // Center: LSB 0, MSB 64 -> 8192.
878        assert_eq!(
879            MidiEvent::from_midi_bytes(&[0xE0, 0, 64]),
880            Some(MidiEvent::PitchBend {
881                channel: MidiChannel::Ch1,
882                value: 8192
883            })
884        );
885        // Max: LSB 127, MSB 127 -> 16383.
886        assert_eq!(
887            MidiEvent::from_midi_bytes(&[0xE0, 127, 127]),
888            Some(MidiEvent::PitchBend {
889                channel: MidiChannel::Ch1,
890                value: 16383
891            })
892        );
893    }
894
895    #[test]
896    fn from_midi_bytes_rejects_unsupported_and_junk() {
897        assert_eq!(MidiEvent::from_midi_bytes(&[]), None); // empty
898        assert_eq!(MidiEvent::from_midi_bytes(&[0x60]), None); // data byte, not status
899        assert_eq!(MidiEvent::from_midi_bytes(&[0xF8]), None); // realtime clock
900        assert_eq!(MidiEvent::from_midi_bytes(&[0xF0, 1, 2]), None); // sysex
901        assert_eq!(MidiEvent::from_midi_bytes(&[0x90, 60]), None); // truncated note on
902    }
903
904    #[test]
905    fn from_midi_bytes_maps_program_change() {
906        assert_eq!(
907            MidiEvent::from_midi_bytes(&[0xC0, 5]),
908            Some(MidiEvent::ProgramChange {
909                channel: MidiChannel::Ch1,
910                program: 5
911            })
912        );
913        // Channel is taken from the low nibble; the program byte is masked to 7 bits.
914        assert_eq!(
915            MidiEvent::from_midi_bytes(&[0xC9, 0xFF]),
916            Some(MidiEvent::ProgramChange {
917                channel: MidiChannel::Ch10,
918                program: 127
919            })
920        );
921        // Truncated (no program byte) is rejected.
922        assert_eq!(MidiEvent::from_midi_bytes(&[0xC0]), None);
923    }
924
925    #[test]
926    fn test_midi_conversions() {
927        // Test some known values using C3=60 convention
928        assert_eq!(name_to_note("C3"), Some(60));
929        assert_eq!(name_to_note("C2"), Some(48));
930        assert_eq!(name_to_note("A3"), Some(69)); // Concert A
931        assert_eq!(name_to_note("C-2"), Some(0));
932        assert_eq!(name_to_note("G8"), Some(127));
933
934        // Test reverse conversion
935        assert_eq!(note_to_name(60), "C3");
936        assert_eq!(note_to_name(48), "C2");
937        assert_eq!(note_to_name(69), "A3");
938        assert_eq!(note_to_name(0), "C-2");
939        assert_eq!(note_to_name(127), "G8");
940
941        // Test accidentals
942        assert_eq!(name_to_note("C#3"), Some(61));
943        assert_eq!(name_to_note("Db3"), Some(61));
944        assert_eq!(name_to_note("F#3"), Some(66));
945    }
946
947    /// `name_to_note` is a safe `Option`-returning parser fed by UI text fields and config files,
948    /// so every input has to come back as `None` rather than a panic. It used to slice at byte
949    /// index 1 to detect a flat, which panics whenever the uppercased first char is multi-byte.
950    #[test]
951    fn name_to_note_rejects_junk_without_panicking() {
952        for junk in [
953            "éB3",    // multi-byte first char — used to panic on a non-char-boundary slice
954            "ÉB3",    //
955            "日本語", // no ASCII at all
956            "",       // empty
957            "3",      // no note letter
958            "H3",     // not a note name
959            "C",      // no octave
960            "C#",     // accidental, no octave
961            "Cb3",    // Cb is not one of the accidentals we map
962            "C##3",   // double sharp
963            "CB3",    // uppercase input is normalised, but Cb still isn't mapped
964            "C99",    // octave out of MIDI range
965            "C-99",   //
966            "#3",     // accidental with no letter
967            // Octave arithmetic has to be checked: `(octave + 2) * 12` overflows on these, which
968            // panics in a debug build and silently returns a wrong note in a release one.
969            "C2147483647",
970            "C-2147483648",
971            "Bb2147483647",
972            "C#2147483647",
973        ] {
974            assert_eq!(name_to_note(junk), None, "expected None for {junk:?}");
975        }
976    }
977
978    /// A bare "B" is the note B, not a flat — the flat form only applies when 'B' is the *second*
979    /// character. Both readings go through the same branch, so pin them together.
980    #[test]
981    fn name_to_note_distinguishes_b_natural_from_flats() {
982        assert_eq!(name_to_note("B3"), Some(71));
983        assert_eq!(name_to_note("Bb3"), Some(70));
984        assert_eq!(name_to_note("bb3"), Some(70)); // case-insensitive
985        assert_eq!(name_to_note("Eb3"), Some(63));
986        assert_eq!(name_to_note("Ab3"), Some(68));
987        // Round-trip every note through its own name.
988        for n in 0..=127u8 {
989            assert_eq!(name_to_note(&note_to_name(n)), Some(n), "round-trip {n}");
990        }
991    }
992
993    /// `note_to_name` takes a `u8`, so 128..=255 are representable but outside the MIDI note
994    /// domain. They used to be given a fabricated name ("D#19") that `name_to_note` rejects,
995    /// so the two functions disagreed about what a valid note name is.
996    #[test]
997    fn note_to_name_marks_out_of_domain_notes_instead_of_fabricating_one() {
998        for n in 128..=255u8 {
999            let name = note_to_name(n);
1000            assert_eq!(name, format!("Invalid({n})"));
1001            assert_eq!(
1002                name_to_note(&name),
1003                None,
1004                "{name} must not parse back as a note"
1005            );
1006        }
1007        // And the domain that does round-trip is unchanged, in both directions.
1008        for n in 0..=127u8 {
1009            let name = note_to_name(n);
1010            assert!(!name.starts_with("Invalid"), "note {n} rendered as {name}");
1011            assert_eq!(name_to_note(&name), Some(n), "round-trip {n}");
1012        }
1013    }
1014
1015    #[test]
1016    fn test_midi_channel() {
1017        assert_eq!(MidiChannel::Ch1.as_index(), 0);
1018        assert_eq!(MidiChannel::Ch16.as_index(), 15);
1019        assert_eq!(MidiChannel::from_index(0), Some(MidiChannel::Ch1));
1020        assert_eq!(MidiChannel::from_index(15), Some(MidiChannel::Ch16));
1021        assert_eq!(MidiChannel::from_index(16), None);
1022    }
1023}