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/// An opaque per-voice handle returned by [`Plugin::note_on`](crate::Plugin::note_on), used to
163/// target note-expression events (and the note-off) at a specific sounding note — the basis for
164/// MPE-style per-note control.
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
166pub struct NoteId(pub(crate) i32);
167
168impl NoteId {
169    /// The raw VST3 note id.
170    pub fn raw(self) -> i32 {
171        self.0
172    }
173}
174
175/// A VST3 per-note expression dimension. Values are normalized `0.0..=1.0`; the bipolar
176/// dimensions (Pan, Tuning) center at `0.5`.
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
178#[non_exhaustive]
179pub enum NoteExpressionType {
180    /// Per-note volume (`kVolumeTypeID`).
181    Volume,
182    /// Per-note pan, bipolar (`kPanTypeID`).
183    Pan,
184    /// Per-note tuning / pitch, bipolar (`kTuningTypeID`).
185    Tuning,
186    /// Per-note vibrato (`kVibratoTypeID`).
187    Vibrato,
188    /// Per-note expression (`kExpressionTypeID`).
189    Expression,
190    /// Per-note brightness / timbre (`kBrightnessTypeID`).
191    Brightness,
192    /// A plugin-defined custom expression type id (`kCustomStart..kCustomEnd`).
193    Custom(u32),
194}
195
196impl NoteExpressionType {
197    /// The VST3 `NoteExpressionTypeID` for this dimension.
198    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    /// Map a VST3 `NoteExpressionTypeID` back to a type (unknown ids become `Custom`).
211    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/// A note-expression dimension a plugin advertises via `INoteExpressionController`
225/// (from [`Plugin::note_expressions`](crate::Plugin::note_expressions)).
226#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
227pub struct NoteExpressionInfo {
228    /// Which expression dimension this is.
229    pub kind: NoteExpressionType,
230    /// Display title (e.g. "Tuning").
231    pub title: String,
232    /// Short title.
233    pub short_title: String,
234    /// Units string (may be empty).
235    pub units: String,
236    /// Default normalized value.
237    pub default_value: f64,
238    /// Minimum normalized value.
239    pub min: f64,
240    /// Maximum normalized value.
241    pub max: f64,
242    /// Discrete step count (0 = continuous).
243    pub step_count: i32,
244    /// Whether the dimension is bipolar (centered at 0.5).
245    pub is_bipolar: bool,
246    /// Whether it's a one-shot (applied once at note start).
247    pub is_one_shot: bool,
248    /// Whether the value is absolute (vs relative to the note's base).
249    pub is_absolute: bool,
250}
251
252impl MidiEvent {
253    /// Parse a single channel-voice MIDI message from raw bytes (status + data), as delivered
254    /// by a MIDI input device.
255    ///
256    /// Maps Note On/Off (a Note On with velocity 0 becomes a Note Off), Control Change,
257    /// Pitch Bend (14-bit), and channel/poly aftertouch. Returns `None` for empty/truncated
258    /// input, running-status messages (no leading status byte), system/realtime/SysEx
259    /// messages, and Program Change (which the library doesn't forward).
260    pub fn from_midi_bytes(bytes: &[u8]) -> Option<MidiEvent> {
261        let status = *bytes.first()?;
262        // Require a channel-voice status byte (0x80..=0xEF); reject data bytes (running status)
263        // and system/realtime messages (0xF0..=0xFF).
264        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            // 0xC0 ProgramChange is intentionally not forwarded.
312            _ => None,
313        }
314    }
315}
316
317/// Common MIDI control change numbers
318pub mod cc {
319    /// Bank Select MSB
320    pub const BANK_SELECT_MSB: u8 = 0;
321    /// Modulation Wheel
322    pub const MODULATION: u8 = 1;
323    /// Breath Controller
324    pub const BREATH: u8 = 2;
325    /// Foot Controller
326    pub const FOOT: u8 = 4;
327    /// Portamento Time
328    pub const PORTAMENTO_TIME: u8 = 5;
329    /// Data Entry MSB
330    pub const DATA_ENTRY_MSB: u8 = 6;
331    /// Channel Volume
332    pub const VOLUME: u8 = 7;
333    /// Balance
334    pub const BALANCE: u8 = 8;
335    /// Pan
336    pub const PAN: u8 = 10;
337    /// Expression
338    pub const EXPRESSION: u8 = 11;
339    /// Sustain Pedal
340    pub const SUSTAIN: u8 = 64;
341    /// Portamento On/Off
342    pub const PORTAMENTO: u8 = 65;
343    /// Sostenuto
344    pub const SOSTENUTO: u8 = 66;
345    /// Soft Pedal
346    pub const SOFT_PEDAL: u8 = 67;
347    /// Legato Footswitch
348    pub const LEGATO: u8 = 68;
349    /// Hold 2
350    pub const HOLD_2: u8 = 69;
351    /// Sound Controller 1 (default: Sound Variation)
352    pub const SOUND_CONTROLLER_1: u8 = 70;
353    /// Sound Controller 2 (default: Timbre/Harmonic Content)
354    pub const SOUND_CONTROLLER_2: u8 = 71;
355    /// Sound Controller 3 (default: Release Time)
356    pub const SOUND_CONTROLLER_3: u8 = 72;
357    /// Sound Controller 4 (default: Attack Time)
358    pub const SOUND_CONTROLLER_4: u8 = 73;
359    /// Sound Controller 5 (default: Brightness)
360    pub const SOUND_CONTROLLER_5: u8 = 74;
361    /// Sound Controller 6-10
362    pub const SOUND_CONTROLLER_6: u8 = 75;
363    /// Sound controller 7
364    pub const SOUND_CONTROLLER_7: u8 = 76;
365    /// Sound controller 8
366    pub const SOUND_CONTROLLER_8: u8 = 77;
367    /// Sound controller 9
368    pub const SOUND_CONTROLLER_9: u8 = 78;
369    /// Sound controller 10
370    pub const SOUND_CONTROLLER_10: u8 = 79;
371    /// General Purpose Controllers
372    pub const GENERAL_PURPOSE_1: u8 = 80;
373    /// General purpose controller 2
374    pub const GENERAL_PURPOSE_2: u8 = 81;
375    /// General purpose controller 3
376    pub const GENERAL_PURPOSE_3: u8 = 82;
377    /// General purpose controller 4
378    pub const GENERAL_PURPOSE_4: u8 = 83;
379    /// Portamento Control
380    pub const PORTAMENTO_CONTROL: u8 = 84;
381    /// Effects Depth
382    pub const REVERB_DEPTH: u8 = 91;
383    /// Tremolo depth
384    pub const TREMOLO_DEPTH: u8 = 92;
385    /// Chorus depth
386    pub const CHORUS_DEPTH: u8 = 93;
387    /// Celeste depth
388    pub const CELESTE_DEPTH: u8 = 94;
389    /// Phaser depth
390    pub const PHASER_DEPTH: u8 = 95;
391    /// Data Increment
392    pub const DATA_INCREMENT: u8 = 96;
393    /// Data Decrement
394    pub const DATA_DECREMENT: u8 = 97;
395    /// NRPN LSB
396    pub const NRPN_LSB: u8 = 98;
397    /// NRPN MSB
398    pub const NRPN_MSB: u8 = 99;
399    /// RPN LSB
400    pub const RPN_LSB: u8 = 100;
401    /// RPN MSB
402    pub const RPN_MSB: u8 = 101;
403    /// All Sounds Off
404    pub const ALL_SOUNDS_OFF: u8 = 120;
405    /// Reset All Controllers
406    pub const RESET_ALL_CONTROLLERS: u8 = 121;
407    /// Local Control On/Off
408    pub const LOCAL_CONTROL: u8 = 122;
409    /// All Notes Off
410    pub const ALL_NOTES_OFF: u8 = 123;
411    /// Omni Mode Off
412    pub const OMNI_MODE_OFF: u8 = 124;
413    /// Omni Mode On
414    pub const OMNI_MODE_ON: u8 = 125;
415    /// Mono Mode On
416    pub const MONO_MODE_ON: u8 = 126;
417    /// Poly Mode On
418    pub const POLY_MODE_ON: u8 = 127;
419}
420
421/// Convert MIDI note number to note name
422/// Using the convention where C3 = MIDI 60
423pub 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
432/// Convert note name to MIDI note number
433/// Accepts formats like "C3", "C#4", "Db3", etc.
434/// Using the convention where C3 = MIDI 60
435pub fn name_to_note(name: &str) -> Option<u8> {
436    let name = name.trim().to_uppercase();
437
438    // Extract the note letter and accidental
439    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        // Handle Bb notation
447        (format!("{}B", &name[0..1]), &name[2..])
448    } else {
449        // Natural note
450        let mut chars = name.chars();
451        let note = chars.next()?.to_string();
452        let octave = chars.as_str();
453        (note, octave)
454    };
455
456    // Parse octave
457    let octave: i32 = octave_str.parse().ok()?;
458
459    // Convert note to semitone offset within octave
460    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    // Calculate MIDI note number
477    // Using the convention where C3 = MIDI 60
478    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        // The well-known VST3 type ids.
505        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        // Note on (ch 1, note 60, vel 100).
515        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        // Note on velocity 0 => note off.
524        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        // Note off on channel 10.
533        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        // CC.
542        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        // Channel + poly aftertouch.
551        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        // Center: LSB 0, MSB 64 -> 8192.
571        assert_eq!(
572            MidiEvent::from_midi_bytes(&[0xE0, 0, 64]),
573            Some(MidiEvent::PitchBend {
574                channel: MidiChannel::Ch1,
575                value: 8192
576            })
577        );
578        // Max: LSB 127, MSB 127 -> 16383.
579        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); // empty
591        assert_eq!(MidiEvent::from_midi_bytes(&[0x60]), None); // data byte, not status
592        assert_eq!(MidiEvent::from_midi_bytes(&[0xF8]), None); // realtime clock
593        assert_eq!(MidiEvent::from_midi_bytes(&[0xF0, 1, 2]), None); // sysex
594        assert_eq!(MidiEvent::from_midi_bytes(&[0xC0, 5]), None); // program change (not forwarded)
595        assert_eq!(MidiEvent::from_midi_bytes(&[0x90, 60]), None); // truncated note on
596    }
597
598    #[test]
599    fn test_midi_conversions() {
600        // Test some known values using C3=60 convention
601        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)); // Concert A
604        assert_eq!(name_to_note("C-2"), Some(0));
605        assert_eq!(name_to_note("G8"), Some(127));
606
607        // Test reverse conversion
608        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        // Test accidentals
615        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}