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    /// Reconstruct a [`NoteId`] from a raw VST3 note id.
175    ///
176    /// A `NoteId` is normally minted by [`Plugin::note_on`](crate::Plugin::note_on); this is
177    /// the inverse of [`raw`](Self::raw), used to carry an id across the process-isolation
178    /// boundary (the helper owns the plugin and allocates the id; the host re-wraps it).
179    pub fn from_raw(raw: i32) -> Self {
180        NoteId(raw)
181    }
182}
183
184/// A VST3 per-note expression dimension. Values are normalized `0.0..=1.0`; the bipolar
185/// dimensions (Pan, Tuning) center at `0.5`.
186#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
187#[non_exhaustive]
188pub enum NoteExpressionType {
189    /// Per-note volume (`kVolumeTypeID`).
190    Volume,
191    /// Per-note pan, bipolar (`kPanTypeID`).
192    Pan,
193    /// Per-note tuning / pitch, bipolar (`kTuningTypeID`).
194    Tuning,
195    /// Per-note vibrato (`kVibratoTypeID`).
196    Vibrato,
197    /// Per-note expression (`kExpressionTypeID`).
198    Expression,
199    /// Per-note brightness / timbre (`kBrightnessTypeID`).
200    Brightness,
201    /// A plugin-defined custom expression type id (`kCustomStart..kCustomEnd`).
202    Custom(u32),
203}
204
205impl NoteExpressionType {
206    /// The VST3 `NoteExpressionTypeID` for this dimension.
207    pub(crate) fn type_id(self) -> u32 {
208        match self {
209            NoteExpressionType::Volume => 0,
210            NoteExpressionType::Pan => 1,
211            NoteExpressionType::Tuning => 2,
212            NoteExpressionType::Vibrato => 3,
213            NoteExpressionType::Expression => 4,
214            NoteExpressionType::Brightness => 5,
215            NoteExpressionType::Custom(id) => id,
216        }
217    }
218
219    /// Map a VST3 `NoteExpressionTypeID` back to a type (unknown ids become `Custom`).
220    pub(crate) fn from_type_id(id: u32) -> Self {
221        match id {
222            0 => NoteExpressionType::Volume,
223            1 => NoteExpressionType::Pan,
224            2 => NoteExpressionType::Tuning,
225            3 => NoteExpressionType::Vibrato,
226            4 => NoteExpressionType::Expression,
227            5 => NoteExpressionType::Brightness,
228            other => NoteExpressionType::Custom(other),
229        }
230    }
231}
232
233/// A note-expression dimension a plugin advertises via `INoteExpressionController`
234/// (from [`Plugin::note_expressions`](crate::Plugin::note_expressions)).
235#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
236pub struct NoteExpressionInfo {
237    /// Which expression dimension this is.
238    pub kind: NoteExpressionType,
239    /// Display title (e.g. "Tuning").
240    pub title: String,
241    /// Short title.
242    pub short_title: String,
243    /// Units string (may be empty).
244    pub units: String,
245    /// Default normalized value.
246    pub default_value: f64,
247    /// Minimum normalized value.
248    pub min: f64,
249    /// Maximum normalized value.
250    pub max: f64,
251    /// Discrete step count (0 = continuous).
252    pub step_count: i32,
253    /// Whether the dimension is bipolar (centered at 0.5).
254    pub is_bipolar: bool,
255    /// Whether it's a one-shot (applied once at note start).
256    pub is_one_shot: bool,
257    /// Whether the value is absolute (vs relative to the note's base).
258    pub is_absolute: bool,
259}
260
261impl MidiEvent {
262    /// Parse a single channel-voice MIDI message from raw bytes (status + data), as delivered
263    /// by a MIDI input device.
264    ///
265    /// Maps Note On/Off (a Note On with velocity 0 becomes a Note Off), Control Change,
266    /// Pitch Bend (14-bit), and channel/poly aftertouch. Returns `None` for empty/truncated
267    /// input, running-status messages (no leading status byte), system/realtime/SysEx
268    /// messages, and Program Change (which the library doesn't forward).
269    pub fn from_midi_bytes(bytes: &[u8]) -> Option<MidiEvent> {
270        let status = *bytes.first()?;
271        // Require a channel-voice status byte (0x80..=0xEF); reject data bytes (running status)
272        // and system/realtime messages (0xF0..=0xFF).
273        if !(0x80..0xF0).contains(&status) {
274            return None;
275        }
276        let channel = MidiChannel::from_index(status & 0x0F)?;
277        let d1 = || bytes.get(1).map(|b| b & 0x7F);
278        let d2 = || bytes.get(2).map(|b| b & 0x7F);
279        match status & 0xF0 {
280            0x90 => {
281                let note = d1()?;
282                let velocity = d2()?;
283                Some(if velocity == 0 {
284                    MidiEvent::NoteOff {
285                        channel,
286                        note,
287                        velocity: 0,
288                    }
289                } else {
290                    MidiEvent::NoteOn {
291                        channel,
292                        note,
293                        velocity,
294                    }
295                })
296            }
297            0x80 => Some(MidiEvent::NoteOff {
298                channel,
299                note: d1()?,
300                velocity: d2()?,
301            }),
302            0xB0 => Some(MidiEvent::ControlChange {
303                channel,
304                controller: d1()?,
305                value: d2()?,
306            }),
307            0xA0 => Some(MidiEvent::PolyAftertouch {
308                channel,
309                note: d1()?,
310                pressure: d2()?,
311            }),
312            0xD0 => Some(MidiEvent::ChannelAftertouch {
313                channel,
314                pressure: d1()?,
315            }),
316            0xE0 => {
317                let value = (d2()? as u16) << 7 | d1()? as u16;
318                Some(MidiEvent::PitchBend { channel, value })
319            }
320            // 0xC0 ProgramChange is intentionally not forwarded.
321            _ => None,
322        }
323    }
324}
325
326/// Common MIDI control change numbers
327pub mod cc {
328    /// Bank Select MSB
329    pub const BANK_SELECT_MSB: u8 = 0;
330    /// Modulation Wheel
331    pub const MODULATION: u8 = 1;
332    /// Breath Controller
333    pub const BREATH: u8 = 2;
334    /// Foot Controller
335    pub const FOOT: u8 = 4;
336    /// Portamento Time
337    pub const PORTAMENTO_TIME: u8 = 5;
338    /// Data Entry MSB
339    pub const DATA_ENTRY_MSB: u8 = 6;
340    /// Channel Volume
341    pub const VOLUME: u8 = 7;
342    /// Balance
343    pub const BALANCE: u8 = 8;
344    /// Pan
345    pub const PAN: u8 = 10;
346    /// Expression
347    pub const EXPRESSION: u8 = 11;
348    /// Sustain Pedal
349    pub const SUSTAIN: u8 = 64;
350    /// Portamento On/Off
351    pub const PORTAMENTO: u8 = 65;
352    /// Sostenuto
353    pub const SOSTENUTO: u8 = 66;
354    /// Soft Pedal
355    pub const SOFT_PEDAL: u8 = 67;
356    /// Legato Footswitch
357    pub const LEGATO: u8 = 68;
358    /// Hold 2
359    pub const HOLD_2: u8 = 69;
360    /// Sound Controller 1 (default: Sound Variation)
361    pub const SOUND_CONTROLLER_1: u8 = 70;
362    /// Sound Controller 2 (default: Timbre/Harmonic Content)
363    pub const SOUND_CONTROLLER_2: u8 = 71;
364    /// Sound Controller 3 (default: Release Time)
365    pub const SOUND_CONTROLLER_3: u8 = 72;
366    /// Sound Controller 4 (default: Attack Time)
367    pub const SOUND_CONTROLLER_4: u8 = 73;
368    /// Sound Controller 5 (default: Brightness)
369    pub const SOUND_CONTROLLER_5: u8 = 74;
370    /// Sound Controller 6-10
371    pub const SOUND_CONTROLLER_6: u8 = 75;
372    /// Sound controller 7
373    pub const SOUND_CONTROLLER_7: u8 = 76;
374    /// Sound controller 8
375    pub const SOUND_CONTROLLER_8: u8 = 77;
376    /// Sound controller 9
377    pub const SOUND_CONTROLLER_9: u8 = 78;
378    /// Sound controller 10
379    pub const SOUND_CONTROLLER_10: u8 = 79;
380    /// General Purpose Controllers
381    pub const GENERAL_PURPOSE_1: u8 = 80;
382    /// General purpose controller 2
383    pub const GENERAL_PURPOSE_2: u8 = 81;
384    /// General purpose controller 3
385    pub const GENERAL_PURPOSE_3: u8 = 82;
386    /// General purpose controller 4
387    pub const GENERAL_PURPOSE_4: u8 = 83;
388    /// Portamento Control
389    pub const PORTAMENTO_CONTROL: u8 = 84;
390    /// Effects Depth
391    pub const REVERB_DEPTH: u8 = 91;
392    /// Tremolo depth
393    pub const TREMOLO_DEPTH: u8 = 92;
394    /// Chorus depth
395    pub const CHORUS_DEPTH: u8 = 93;
396    /// Celeste depth
397    pub const CELESTE_DEPTH: u8 = 94;
398    /// Phaser depth
399    pub const PHASER_DEPTH: u8 = 95;
400    /// Data Increment
401    pub const DATA_INCREMENT: u8 = 96;
402    /// Data Decrement
403    pub const DATA_DECREMENT: u8 = 97;
404    /// NRPN LSB
405    pub const NRPN_LSB: u8 = 98;
406    /// NRPN MSB
407    pub const NRPN_MSB: u8 = 99;
408    /// RPN LSB
409    pub const RPN_LSB: u8 = 100;
410    /// RPN MSB
411    pub const RPN_MSB: u8 = 101;
412    /// All Sounds Off
413    pub const ALL_SOUNDS_OFF: u8 = 120;
414    /// Reset All Controllers
415    pub const RESET_ALL_CONTROLLERS: u8 = 121;
416    /// Local Control On/Off
417    pub const LOCAL_CONTROL: u8 = 122;
418    /// All Notes Off
419    pub const ALL_NOTES_OFF: u8 = 123;
420    /// Omni Mode Off
421    pub const OMNI_MODE_OFF: u8 = 124;
422    /// Omni Mode On
423    pub const OMNI_MODE_ON: u8 = 125;
424    /// Mono Mode On
425    pub const MONO_MODE_ON: u8 = 126;
426    /// Poly Mode On
427    pub const POLY_MODE_ON: u8 = 127;
428}
429
430/// Convert MIDI note number to note name
431/// Using the convention where C3 = MIDI 60
432pub fn note_to_name(note: u8) -> String {
433    let note_names = [
434        "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
435    ];
436    let octave = (note as i32 / 12) - 2;
437    let note_in_octave = note % 12;
438    format!("{}{}", note_names[note_in_octave as usize], octave)
439}
440
441/// Convert note name to MIDI note number
442/// Accepts formats like "C3", "C#4", "Db3", etc.
443/// Using the convention where C3 = MIDI 60
444pub fn name_to_note(name: &str) -> Option<u8> {
445    let name = name.trim().to_uppercase();
446
447    // Extract the note letter and accidental
448    let (note_part, octave_str) = if name.contains('#') {
449        let parts: Vec<&str> = name.split('#').collect();
450        if parts.len() != 2 {
451            return None;
452        }
453        (format!("{}#", parts[0]), parts[1])
454    } else if name.contains('B') && name.len() > 2 && &name[1..2] == "B" {
455        // Handle Bb notation
456        (format!("{}B", &name[0..1]), &name[2..])
457    } else {
458        // Natural note
459        let mut chars = name.chars();
460        let note = chars.next()?.to_string();
461        let octave = chars.as_str();
462        (note, octave)
463    };
464
465    // Parse octave
466    let octave: i32 = octave_str.parse().ok()?;
467
468    // Convert note to semitone offset within octave
469    let semitone = match note_part.as_str() {
470        "C" => 0,
471        "C#" | "DB" => 1,
472        "D" => 2,
473        "D#" | "EB" => 3,
474        "E" => 4,
475        "F" => 5,
476        "F#" | "GB" => 6,
477        "G" => 7,
478        "G#" | "AB" => 8,
479        "A" => 9,
480        "A#" | "BB" => 10,
481        "B" => 11,
482        _ => return None,
483    };
484
485    // Calculate MIDI note number
486    // Using the convention where C3 = MIDI 60
487    let midi_note = (octave + 2) * 12 + semitone;
488
489    if (0..=127).contains(&midi_note) {
490        Some(midi_note as u8)
491    } else {
492        None
493    }
494}
495
496#[cfg(test)]
497mod tests {
498    use super::*;
499
500    #[test]
501    fn note_expression_type_ids_round_trip() {
502        for kind in [
503            NoteExpressionType::Volume,
504            NoteExpressionType::Pan,
505            NoteExpressionType::Tuning,
506            NoteExpressionType::Vibrato,
507            NoteExpressionType::Expression,
508            NoteExpressionType::Brightness,
509            NoteExpressionType::Custom(100_001),
510        ] {
511            assert_eq!(NoteExpressionType::from_type_id(kind.type_id()), kind);
512        }
513        // The well-known VST3 type ids.
514        assert_eq!(NoteExpressionType::Tuning.type_id(), 2);
515        assert_eq!(
516            NoteExpressionType::from_type_id(5),
517            NoteExpressionType::Brightness
518        );
519    }
520
521    #[test]
522    fn from_midi_bytes_maps_channel_voice_messages() {
523        // Note on (ch 1, note 60, vel 100).
524        assert_eq!(
525            MidiEvent::from_midi_bytes(&[0x90, 60, 100]),
526            Some(MidiEvent::NoteOn {
527                channel: MidiChannel::Ch1,
528                note: 60,
529                velocity: 100
530            })
531        );
532        // Note on velocity 0 => note off.
533        assert_eq!(
534            MidiEvent::from_midi_bytes(&[0x90, 60, 0]),
535            Some(MidiEvent::NoteOff {
536                channel: MidiChannel::Ch1,
537                note: 60,
538                velocity: 0
539            })
540        );
541        // Note off on channel 10.
542        assert_eq!(
543            MidiEvent::from_midi_bytes(&[0x89, 64, 40]),
544            Some(MidiEvent::NoteOff {
545                channel: MidiChannel::Ch10,
546                note: 64,
547                velocity: 40
548            })
549        );
550        // CC.
551        assert_eq!(
552            MidiEvent::from_midi_bytes(&[0xB0, 1, 64]),
553            Some(MidiEvent::ControlChange {
554                channel: MidiChannel::Ch1,
555                controller: 1,
556                value: 64
557            })
558        );
559        // Channel + poly aftertouch.
560        assert_eq!(
561            MidiEvent::from_midi_bytes(&[0xD0, 90]),
562            Some(MidiEvent::ChannelAftertouch {
563                channel: MidiChannel::Ch1,
564                pressure: 90
565            })
566        );
567        assert_eq!(
568            MidiEvent::from_midi_bytes(&[0xA0, 60, 70]),
569            Some(MidiEvent::PolyAftertouch {
570                channel: MidiChannel::Ch1,
571                note: 60,
572                pressure: 70
573            })
574        );
575    }
576
577    #[test]
578    fn from_midi_bytes_pitch_bend_is_14_bit() {
579        // Center: LSB 0, MSB 64 -> 8192.
580        assert_eq!(
581            MidiEvent::from_midi_bytes(&[0xE0, 0, 64]),
582            Some(MidiEvent::PitchBend {
583                channel: MidiChannel::Ch1,
584                value: 8192
585            })
586        );
587        // Max: LSB 127, MSB 127 -> 16383.
588        assert_eq!(
589            MidiEvent::from_midi_bytes(&[0xE0, 127, 127]),
590            Some(MidiEvent::PitchBend {
591                channel: MidiChannel::Ch1,
592                value: 16383
593            })
594        );
595    }
596
597    #[test]
598    fn from_midi_bytes_rejects_unsupported_and_junk() {
599        assert_eq!(MidiEvent::from_midi_bytes(&[]), None); // empty
600        assert_eq!(MidiEvent::from_midi_bytes(&[0x60]), None); // data byte, not status
601        assert_eq!(MidiEvent::from_midi_bytes(&[0xF8]), None); // realtime clock
602        assert_eq!(MidiEvent::from_midi_bytes(&[0xF0, 1, 2]), None); // sysex
603        assert_eq!(MidiEvent::from_midi_bytes(&[0xC0, 5]), None); // program change (not forwarded)
604        assert_eq!(MidiEvent::from_midi_bytes(&[0x90, 60]), None); // truncated note on
605    }
606
607    #[test]
608    fn test_midi_conversions() {
609        // Test some known values using C3=60 convention
610        assert_eq!(name_to_note("C3"), Some(60));
611        assert_eq!(name_to_note("C2"), Some(48));
612        assert_eq!(name_to_note("A3"), Some(69)); // Concert A
613        assert_eq!(name_to_note("C-2"), Some(0));
614        assert_eq!(name_to_note("G8"), Some(127));
615
616        // Test reverse conversion
617        assert_eq!(note_to_name(60), "C3");
618        assert_eq!(note_to_name(48), "C2");
619        assert_eq!(note_to_name(69), "A3");
620        assert_eq!(note_to_name(0), "C-2");
621        assert_eq!(note_to_name(127), "G8");
622
623        // Test accidentals
624        assert_eq!(name_to_note("C#3"), Some(61));
625        assert_eq!(name_to_note("Db3"), Some(61));
626        assert_eq!(name_to_note("F#3"), Some(66));
627    }
628
629    #[test]
630    fn test_midi_channel() {
631        assert_eq!(MidiChannel::Ch1.as_index(), 0);
632        assert_eq!(MidiChannel::Ch16.as_index(), 15);
633        assert_eq!(MidiChannel::from_index(0), Some(MidiChannel::Ch1));
634        assert_eq!(MidiChannel::from_index(15), Some(MidiChannel::Ch16));
635        assert_eq!(MidiChannel::from_index(16), None);
636    }
637}