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), channel/poly aftertouch, and Program Change. Returns `None` for
267    /// empty/truncated input, running-status messages (no leading status byte), and
268    /// system/realtime/SysEx messages.
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 => Some(MidiEvent::ProgramChange {
321                channel,
322                program: d1()?,
323            }),
324            _ => None,
325        }
326    }
327}
328
329/// Common MIDI control change numbers
330pub mod cc {
331    /// Bank Select MSB
332    pub const BANK_SELECT_MSB: u8 = 0;
333    /// Modulation Wheel
334    pub const MODULATION: u8 = 1;
335    /// Breath Controller
336    pub const BREATH: u8 = 2;
337    /// Foot Controller
338    pub const FOOT: u8 = 4;
339    /// Portamento Time
340    pub const PORTAMENTO_TIME: u8 = 5;
341    /// Data Entry MSB
342    pub const DATA_ENTRY_MSB: u8 = 6;
343    /// Channel Volume
344    pub const VOLUME: u8 = 7;
345    /// Balance
346    pub const BALANCE: u8 = 8;
347    /// Pan
348    pub const PAN: u8 = 10;
349    /// Expression
350    pub const EXPRESSION: u8 = 11;
351    /// Sustain Pedal
352    pub const SUSTAIN: u8 = 64;
353    /// Portamento On/Off
354    pub const PORTAMENTO: u8 = 65;
355    /// Sostenuto
356    pub const SOSTENUTO: u8 = 66;
357    /// Soft Pedal
358    pub const SOFT_PEDAL: u8 = 67;
359    /// Legato Footswitch
360    pub const LEGATO: u8 = 68;
361    /// Hold 2
362    pub const HOLD_2: u8 = 69;
363    /// Sound Controller 1 (default: Sound Variation)
364    pub const SOUND_CONTROLLER_1: u8 = 70;
365    /// Sound Controller 2 (default: Timbre/Harmonic Content)
366    pub const SOUND_CONTROLLER_2: u8 = 71;
367    /// Sound Controller 3 (default: Release Time)
368    pub const SOUND_CONTROLLER_3: u8 = 72;
369    /// Sound Controller 4 (default: Attack Time)
370    pub const SOUND_CONTROLLER_4: u8 = 73;
371    /// Sound Controller 5 (default: Brightness)
372    pub const SOUND_CONTROLLER_5: u8 = 74;
373    /// Sound Controller 6-10
374    pub const SOUND_CONTROLLER_6: u8 = 75;
375    /// Sound controller 7
376    pub const SOUND_CONTROLLER_7: u8 = 76;
377    /// Sound controller 8
378    pub const SOUND_CONTROLLER_8: u8 = 77;
379    /// Sound controller 9
380    pub const SOUND_CONTROLLER_9: u8 = 78;
381    /// Sound controller 10
382    pub const SOUND_CONTROLLER_10: u8 = 79;
383    /// General Purpose Controllers
384    pub const GENERAL_PURPOSE_1: u8 = 80;
385    /// General purpose controller 2
386    pub const GENERAL_PURPOSE_2: u8 = 81;
387    /// General purpose controller 3
388    pub const GENERAL_PURPOSE_3: u8 = 82;
389    /// General purpose controller 4
390    pub const GENERAL_PURPOSE_4: u8 = 83;
391    /// Portamento Control
392    pub const PORTAMENTO_CONTROL: u8 = 84;
393    /// Effects Depth
394    pub const REVERB_DEPTH: u8 = 91;
395    /// Tremolo depth
396    pub const TREMOLO_DEPTH: u8 = 92;
397    /// Chorus depth
398    pub const CHORUS_DEPTH: u8 = 93;
399    /// Celeste depth
400    pub const CELESTE_DEPTH: u8 = 94;
401    /// Phaser depth
402    pub const PHASER_DEPTH: u8 = 95;
403    /// Data Increment
404    pub const DATA_INCREMENT: u8 = 96;
405    /// Data Decrement
406    pub const DATA_DECREMENT: u8 = 97;
407    /// NRPN LSB
408    pub const NRPN_LSB: u8 = 98;
409    /// NRPN MSB
410    pub const NRPN_MSB: u8 = 99;
411    /// RPN LSB
412    pub const RPN_LSB: u8 = 100;
413    /// RPN MSB
414    pub const RPN_MSB: u8 = 101;
415    /// All Sounds Off
416    pub const ALL_SOUNDS_OFF: u8 = 120;
417    /// Reset All Controllers
418    pub const RESET_ALL_CONTROLLERS: u8 = 121;
419    /// Local Control On/Off
420    pub const LOCAL_CONTROL: u8 = 122;
421    /// All Notes Off
422    pub const ALL_NOTES_OFF: u8 = 123;
423    /// Omni Mode Off
424    pub const OMNI_MODE_OFF: u8 = 124;
425    /// Omni Mode On
426    pub const OMNI_MODE_ON: u8 = 125;
427    /// Mono Mode On
428    pub const MONO_MODE_ON: u8 = 126;
429    /// Poly Mode On
430    pub const POLY_MODE_ON: u8 = 127;
431}
432
433/// Convert MIDI note number to note name
434/// Using the convention where C3 = MIDI 60
435pub fn note_to_name(note: u8) -> String {
436    let note_names = [
437        "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
438    ];
439    let octave = (note as i32 / 12) - 2;
440    let note_in_octave = note % 12;
441    format!("{}{}", note_names[note_in_octave as usize], octave)
442}
443
444/// Convert note name to MIDI note number
445/// Accepts formats like "C3", "C#4", "Db3", etc.
446/// Using the convention where C3 = MIDI 60
447pub fn name_to_note(name: &str) -> Option<u8> {
448    let name = name.trim().to_uppercase();
449
450    // Extract the note letter and accidental
451    let (note_part, octave_str) = if name.contains('#') {
452        let parts: Vec<&str> = name.split('#').collect();
453        if parts.len() != 2 {
454            return None;
455        }
456        (format!("{}#", parts[0]), parts[1])
457    } else if name.contains('B') && name.len() > 2 && &name[1..2] == "B" {
458        // Handle Bb notation
459        (format!("{}B", &name[0..1]), &name[2..])
460    } else {
461        // Natural note
462        let mut chars = name.chars();
463        let note = chars.next()?.to_string();
464        let octave = chars.as_str();
465        (note, octave)
466    };
467
468    // Parse octave
469    let octave: i32 = octave_str.parse().ok()?;
470
471    // Convert note to semitone offset within octave
472    let semitone = match note_part.as_str() {
473        "C" => 0,
474        "C#" | "DB" => 1,
475        "D" => 2,
476        "D#" | "EB" => 3,
477        "E" => 4,
478        "F" => 5,
479        "F#" | "GB" => 6,
480        "G" => 7,
481        "G#" | "AB" => 8,
482        "A" => 9,
483        "A#" | "BB" => 10,
484        "B" => 11,
485        _ => return None,
486    };
487
488    // Calculate MIDI note number
489    // Using the convention where C3 = MIDI 60
490    let midi_note = (octave + 2) * 12 + semitone;
491
492    if (0..=127).contains(&midi_note) {
493        Some(midi_note as u8)
494    } else {
495        None
496    }
497}
498
499#[cfg(test)]
500mod tests {
501    use super::*;
502
503    #[test]
504    fn note_expression_type_ids_round_trip() {
505        for kind in [
506            NoteExpressionType::Volume,
507            NoteExpressionType::Pan,
508            NoteExpressionType::Tuning,
509            NoteExpressionType::Vibrato,
510            NoteExpressionType::Expression,
511            NoteExpressionType::Brightness,
512            NoteExpressionType::Custom(100_001),
513        ] {
514            assert_eq!(NoteExpressionType::from_type_id(kind.type_id()), kind);
515        }
516        // The well-known VST3 type ids.
517        assert_eq!(NoteExpressionType::Tuning.type_id(), 2);
518        assert_eq!(
519            NoteExpressionType::from_type_id(5),
520            NoteExpressionType::Brightness
521        );
522    }
523
524    #[test]
525    fn from_midi_bytes_maps_channel_voice_messages() {
526        // Note on (ch 1, note 60, vel 100).
527        assert_eq!(
528            MidiEvent::from_midi_bytes(&[0x90, 60, 100]),
529            Some(MidiEvent::NoteOn {
530                channel: MidiChannel::Ch1,
531                note: 60,
532                velocity: 100
533            })
534        );
535        // Note on velocity 0 => note off.
536        assert_eq!(
537            MidiEvent::from_midi_bytes(&[0x90, 60, 0]),
538            Some(MidiEvent::NoteOff {
539                channel: MidiChannel::Ch1,
540                note: 60,
541                velocity: 0
542            })
543        );
544        // Note off on channel 10.
545        assert_eq!(
546            MidiEvent::from_midi_bytes(&[0x89, 64, 40]),
547            Some(MidiEvent::NoteOff {
548                channel: MidiChannel::Ch10,
549                note: 64,
550                velocity: 40
551            })
552        );
553        // CC.
554        assert_eq!(
555            MidiEvent::from_midi_bytes(&[0xB0, 1, 64]),
556            Some(MidiEvent::ControlChange {
557                channel: MidiChannel::Ch1,
558                controller: 1,
559                value: 64
560            })
561        );
562        // Channel + poly aftertouch.
563        assert_eq!(
564            MidiEvent::from_midi_bytes(&[0xD0, 90]),
565            Some(MidiEvent::ChannelAftertouch {
566                channel: MidiChannel::Ch1,
567                pressure: 90
568            })
569        );
570        assert_eq!(
571            MidiEvent::from_midi_bytes(&[0xA0, 60, 70]),
572            Some(MidiEvent::PolyAftertouch {
573                channel: MidiChannel::Ch1,
574                note: 60,
575                pressure: 70
576            })
577        );
578    }
579
580    #[test]
581    fn from_midi_bytes_pitch_bend_is_14_bit() {
582        // Center: LSB 0, MSB 64 -> 8192.
583        assert_eq!(
584            MidiEvent::from_midi_bytes(&[0xE0, 0, 64]),
585            Some(MidiEvent::PitchBend {
586                channel: MidiChannel::Ch1,
587                value: 8192
588            })
589        );
590        // Max: LSB 127, MSB 127 -> 16383.
591        assert_eq!(
592            MidiEvent::from_midi_bytes(&[0xE0, 127, 127]),
593            Some(MidiEvent::PitchBend {
594                channel: MidiChannel::Ch1,
595                value: 16383
596            })
597        );
598    }
599
600    #[test]
601    fn from_midi_bytes_rejects_unsupported_and_junk() {
602        assert_eq!(MidiEvent::from_midi_bytes(&[]), None); // empty
603        assert_eq!(MidiEvent::from_midi_bytes(&[0x60]), None); // data byte, not status
604        assert_eq!(MidiEvent::from_midi_bytes(&[0xF8]), None); // realtime clock
605        assert_eq!(MidiEvent::from_midi_bytes(&[0xF0, 1, 2]), None); // sysex
606        assert_eq!(MidiEvent::from_midi_bytes(&[0x90, 60]), None); // truncated note on
607    }
608
609    #[test]
610    fn from_midi_bytes_maps_program_change() {
611        assert_eq!(
612            MidiEvent::from_midi_bytes(&[0xC0, 5]),
613            Some(MidiEvent::ProgramChange {
614                channel: MidiChannel::Ch1,
615                program: 5
616            })
617        );
618        // Channel is taken from the low nibble; the program byte is masked to 7 bits.
619        assert_eq!(
620            MidiEvent::from_midi_bytes(&[0xC9, 0xFF]),
621            Some(MidiEvent::ProgramChange {
622                channel: MidiChannel::Ch10,
623                program: 127
624            })
625        );
626        // Truncated (no program byte) is rejected.
627        assert_eq!(MidiEvent::from_midi_bytes(&[0xC0]), None);
628    }
629
630    #[test]
631    fn test_midi_conversions() {
632        // Test some known values using C3=60 convention
633        assert_eq!(name_to_note("C3"), Some(60));
634        assert_eq!(name_to_note("C2"), Some(48));
635        assert_eq!(name_to_note("A3"), Some(69)); // Concert A
636        assert_eq!(name_to_note("C-2"), Some(0));
637        assert_eq!(name_to_note("G8"), Some(127));
638
639        // Test reverse conversion
640        assert_eq!(note_to_name(60), "C3");
641        assert_eq!(note_to_name(48), "C2");
642        assert_eq!(note_to_name(69), "A3");
643        assert_eq!(note_to_name(0), "C-2");
644        assert_eq!(note_to_name(127), "G8");
645
646        // Test accidentals
647        assert_eq!(name_to_note("C#3"), Some(61));
648        assert_eq!(name_to_note("Db3"), Some(61));
649        assert_eq!(name_to_note("F#3"), Some(66));
650    }
651
652    #[test]
653    fn test_midi_channel() {
654        assert_eq!(MidiChannel::Ch1.as_index(), 0);
655        assert_eq!(MidiChannel::Ch16.as_index(), 15);
656        assert_eq!(MidiChannel::from_index(0), Some(MidiChannel::Ch1));
657        assert_eq!(MidiChannel::from_index(15), Some(MidiChannel::Ch16));
658        assert_eq!(MidiChannel::from_index(16), None);
659    }
660}