beamer_core/
midi.rs

1//! MIDI event types for audio plugins.
2//!
3//! This module provides format-agnostic MIDI event types designed for
4//! real-time audio processing. Most basic types (notes, CC, pitch bend)
5//! are `Copy` and can be passed without heap allocation.
6//!
7//! ## SysEx Handling
8//!
9//! The [`MidiEventKind::SysEx`] variant uses `Box<SysEx>` to avoid stack
10//! overflow from the large 512-byte SysEx buffer. As a result, [`MidiEvent`]
11//! and [`MidiEventKind`] are `Clone` but not `Copy`.
12//!
13//! **Note:** Cloning a SysEx event allocates. For pass-through of SysEx in
14//! `process_midi()`, consider whether allocation is acceptable for your use case.
15//!
16//! ## Buffer Sizes
17//!
18//! SysEx buffer size can be configured via Cargo features:
19//! - Default: 512 bytes (a common default for audio plugins)
20//! - `sysex-256`: 256 bytes (smaller memory footprint)
21//! - `sysex-1024`: 1024 bytes
22//! - `sysex-2048`: 2048 bytes
23
24// =============================================================================
25// Buffer Size Configuration
26// =============================================================================
27
28/// Maximum SysEx payload size in bytes.
29///
30/// Configurable via Cargo features: `sysex-256`, `sysex-1024`, `sysex-2048`.
31/// Default is 512 bytes (a common default for audio plugins).
32#[cfg(feature = "sysex-2048")]
33pub const MAX_SYSEX_SIZE: usize = 2048;
34
35/// Maximum SysEx payload size in bytes.
36#[cfg(all(feature = "sysex-1024", not(feature = "sysex-2048")))]
37pub const MAX_SYSEX_SIZE: usize = 1024;
38
39/// Maximum SysEx payload size in bytes.
40#[cfg(all(feature = "sysex-256", not(feature = "sysex-1024"), not(feature = "sysex-2048")))]
41pub const MAX_SYSEX_SIZE: usize = 256;
42
43/// Maximum SysEx payload size in bytes.
44#[cfg(not(any(feature = "sysex-256", feature = "sysex-1024", feature = "sysex-2048")))]
45pub const MAX_SYSEX_SIZE: usize = 512;
46
47/// Maximum text size for Note Expression text events.
48pub const MAX_EXPRESSION_TEXT_SIZE: usize = 64;
49
50/// Maximum chord name size in bytes.
51pub const MAX_CHORD_NAME_SIZE: usize = 32;
52
53/// Maximum scale name size in bytes.
54pub const MAX_SCALE_NAME_SIZE: usize = 32;
55
56// =============================================================================
57// Basic MIDI Types
58// =============================================================================
59
60/// MIDI channel (0-15).
61pub type MidiChannel = u8;
62
63/// MIDI note number (0-127, where 60 = middle C).
64pub type MidiNote = u8;
65
66/// Unique identifier for tracking note on/off pairs.
67/// Use -1 when note ID is not available.
68pub type NoteId = i32;
69
70/// A MIDI note-on event.
71#[derive(Debug, Clone, Copy, PartialEq)]
72pub struct NoteOn {
73    /// MIDI channel (0-15).
74    pub channel: MidiChannel,
75    /// Note number (0-127).
76    pub pitch: MidiNote,
77    /// Velocity (0.0 to 1.0, where 0.0 is silent).
78    pub velocity: f32,
79    /// Unique note ID for tracking this note instance.
80    pub note_id: NoteId,
81    /// Pitch offset in cents (-120.0 to +120.0) for microtonal/MPE support.
82    pub tuning: f32,
83    /// Note length in samples (0 = unknown/not provided).
84    pub length: i32,
85}
86
87/// A MIDI note-off event.
88#[derive(Debug, Clone, Copy, PartialEq)]
89pub struct NoteOff {
90    /// MIDI channel (0-15).
91    pub channel: MidiChannel,
92    /// Note number (0-127).
93    pub pitch: MidiNote,
94    /// Release velocity (0.0 to 1.0).
95    pub velocity: f32,
96    /// Unique note ID matching the original note-on.
97    pub note_id: NoteId,
98    /// Pitch offset in cents (-120.0 to +120.0) for microtonal/MPE support.
99    pub tuning: f32,
100}
101
102/// Polyphonic key pressure (aftertouch per note).
103#[derive(Debug, Clone, Copy, PartialEq)]
104pub struct PolyPressure {
105    /// MIDI channel (0-15).
106    pub channel: MidiChannel,
107    /// Note number (0-127).
108    pub pitch: MidiNote,
109    /// Pressure amount (0.0 to 1.0).
110    pub pressure: f32,
111    /// Unique note ID for tracking this note instance.
112    pub note_id: NoteId,
113}
114
115/// Control Change (CC) message.
116#[derive(Debug, Clone, Copy, PartialEq)]
117pub struct ControlChange {
118    /// MIDI channel (0-15).
119    pub channel: MidiChannel,
120    /// Controller number (0-127).
121    pub controller: u8,
122    /// Controller value (0.0 to 1.0, normalized from 0-127).
123    pub value: f32,
124}
125
126impl ControlChange {
127    /// Check if this is a modulation wheel CC (CC1).
128    #[inline]
129    pub const fn is_mod_wheel(&self) -> bool {
130        self.controller == cc::MOD_WHEEL
131    }
132
133    /// Check if this is a sustain pedal CC (CC64).
134    #[inline]
135    pub const fn is_sustain_pedal(&self) -> bool {
136        self.controller == cc::SUSTAIN_PEDAL
137    }
138
139    /// Check if this is an expression pedal CC (CC11).
140    #[inline]
141    pub const fn is_expression(&self) -> bool {
142        self.controller == cc::EXPRESSION
143    }
144
145    /// Check if this is a volume CC (CC7).
146    #[inline]
147    pub const fn is_volume(&self) -> bool {
148        self.controller == cc::VOLUME
149    }
150
151    /// Check if sustain is pressed (value >= 0.5).
152    #[inline]
153    pub fn is_sustain_on(&self) -> bool {
154        self.is_sustain_pedal() && self.value >= 0.5
155    }
156
157    // =========================================================================
158    // Bank Select Helpers
159    // =========================================================================
160
161    /// Check if this is a Bank Select MSB (CC0).
162    #[inline]
163    pub const fn is_bank_select_msb(&self) -> bool {
164        self.controller == cc::BANK_SELECT_MSB
165    }
166
167    /// Check if this is a Bank Select LSB (CC32).
168    #[inline]
169    pub const fn is_bank_select_lsb(&self) -> bool {
170        self.controller == cc::BANK_SELECT_LSB
171    }
172
173    /// Check if this is any Bank Select message (CC0 or CC32).
174    #[inline]
175    pub const fn is_bank_select(&self) -> bool {
176        self.is_bank_select_msb() || self.is_bank_select_lsb()
177    }
178
179    // =========================================================================
180    // 14-bit Controller Helpers
181    // =========================================================================
182
183    /// Check if this controller is an MSB (CC 0-31) that has a corresponding LSB.
184    ///
185    /// MIDI defines CC 0-31 as MSB controllers with CC 32-63 as their LSB pairs.
186    #[inline]
187    pub const fn is_14bit_msb(&self) -> bool {
188        self.controller < 32
189    }
190
191    /// Check if this controller is an LSB (CC 32-63) that pairs with an MSB.
192    ///
193    /// MIDI defines CC 32-63 as LSB controllers that pair with CC 0-31.
194    #[inline]
195    pub const fn is_14bit_lsb(&self) -> bool {
196        self.controller >= 32 && self.controller < 64
197    }
198
199    /// Returns the LSB controller number for this MSB (CC 0-31 → CC 32-63).
200    ///
201    /// Returns `None` if this isn't an MSB controller.
202    #[inline]
203    pub const fn lsb_pair(&self) -> Option<u8> {
204        if self.controller < 32 {
205            Some(self.controller + 32)
206        } else {
207            None
208        }
209    }
210
211    /// Returns the MSB controller number for this LSB (CC 32-63 → CC 0-31).
212    ///
213    /// Returns `None` if this isn't an LSB controller.
214    #[inline]
215    pub const fn msb_pair(&self) -> Option<u8> {
216        if self.controller >= 32 && self.controller < 64 {
217            Some(self.controller - 32)
218        } else {
219            None
220        }
221    }
222
223    // =========================================================================
224    // RPN/NRPN Detection Helpers
225    // =========================================================================
226
227    /// Check if this is an RPN MSB (CC 101).
228    #[inline]
229    pub const fn is_rpn_msb(&self) -> bool {
230        self.controller == cc::RPN_MSB
231    }
232
233    /// Check if this is an RPN LSB (CC 100).
234    #[inline]
235    pub const fn is_rpn_lsb(&self) -> bool {
236        self.controller == cc::RPN_LSB
237    }
238
239    /// Check if this is any RPN selection message (CC 100 or 101).
240    #[inline]
241    pub const fn is_rpn_select(&self) -> bool {
242        self.controller == cc::RPN_MSB || self.controller == cc::RPN_LSB
243    }
244
245    /// Check if this is an NRPN MSB (CC 99).
246    #[inline]
247    pub const fn is_nrpn_msb(&self) -> bool {
248        self.controller == cc::NRPN_MSB
249    }
250
251    /// Check if this is an NRPN LSB (CC 98).
252    #[inline]
253    pub const fn is_nrpn_lsb(&self) -> bool {
254        self.controller == cc::NRPN_LSB
255    }
256
257    /// Check if this is any NRPN selection message (CC 98 or 99).
258    #[inline]
259    pub const fn is_nrpn_select(&self) -> bool {
260        self.controller == cc::NRPN_MSB || self.controller == cc::NRPN_LSB
261    }
262
263    /// Check if this is a Data Entry MSB (CC 6).
264    #[inline]
265    pub const fn is_data_entry_msb(&self) -> bool {
266        self.controller == cc::DATA_ENTRY_MSB
267    }
268
269    /// Check if this is a Data Entry LSB (CC 38).
270    #[inline]
271    pub const fn is_data_entry_lsb(&self) -> bool {
272        self.controller == cc::DATA_ENTRY_LSB
273    }
274
275    /// Check if this is any Data Entry message (CC 6 or 38).
276    #[inline]
277    pub const fn is_data_entry(&self) -> bool {
278        self.controller == cc::DATA_ENTRY_MSB || self.controller == cc::DATA_ENTRY_LSB
279    }
280
281    /// Check if this is a Data Increment (CC 96).
282    #[inline]
283    pub const fn is_data_increment(&self) -> bool {
284        self.controller == cc::DATA_INCREMENT
285    }
286
287    /// Check if this is a Data Decrement (CC 97).
288    #[inline]
289    pub const fn is_data_decrement(&self) -> bool {
290        self.controller == cc::DATA_DECREMENT
291    }
292
293    /// Check if this CC is part of an RPN/NRPN sequence.
294    ///
295    /// Returns true for CC 6, 38, 96-101.
296    #[inline]
297    pub const fn is_rpn_nrpn_related(&self) -> bool {
298        matches!(
299            self.controller,
300            cc::DATA_ENTRY_MSB
301                | cc::DATA_ENTRY_LSB
302                | cc::DATA_INCREMENT
303                | cc::DATA_DECREMENT
304                | cc::NRPN_LSB
305                | cc::NRPN_MSB
306                | cc::RPN_LSB
307                | cc::RPN_MSB
308        )
309    }
310}
311
312/// Pitch bend message.
313#[derive(Debug, Clone, Copy, PartialEq)]
314pub struct PitchBend {
315    /// MIDI channel (0-15).
316    pub channel: MidiChannel,
317    /// Pitch bend value (-1.0 to 1.0, where 0.0 is center).
318    pub value: f32,
319}
320
321/// Channel pressure (channel aftertouch).
322#[derive(Debug, Clone, Copy, PartialEq)]
323pub struct ChannelPressure {
324    /// MIDI channel (0-15).
325    pub channel: MidiChannel,
326    /// Pressure amount (0.0 to 1.0).
327    pub pressure: f32,
328}
329
330/// Program change message.
331#[derive(Debug, Clone, Copy, PartialEq)]
332pub struct ProgramChange {
333    /// MIDI channel (0-15).
334    pub channel: MidiChannel,
335    /// Program number (0-127).
336    pub program: u8,
337}
338
339// =============================================================================
340// Advanced VST3 Events
341// =============================================================================
342
343/// System Exclusive (SysEx) message.
344///
345/// Uses a fixed-size buffer for efficient storage. When used in [`MidiEventKind`],
346/// it is boxed (`Box<SysEx>`) to prevent the large buffer from bloating the enum
347/// size and causing stack overflow.
348///
349/// The buffer size is configurable via Cargo features (default 512 bytes).
350#[derive(Clone, Copy)]
351pub struct SysEx {
352    /// Raw SysEx data (excluding F0/F7 framing bytes).
353    pub data: [u8; MAX_SYSEX_SIZE],
354    /// Actual length of valid data in the buffer.
355    pub len: u16,
356}
357
358impl SysEx {
359    /// Create a new empty SysEx message.
360    pub const fn new() -> Self {
361        Self {
362            data: [0u8; MAX_SYSEX_SIZE],
363            len: 0,
364        }
365    }
366
367    /// Get the valid data slice.
368    #[inline]
369    pub fn as_slice(&self) -> &[u8] {
370        &self.data[..self.len as usize]
371    }
372}
373
374impl Default for SysEx {
375    fn default() -> Self {
376        Self::new()
377    }
378}
379
380impl core::fmt::Debug for SysEx {
381    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
382        f.debug_struct("SysEx")
383            .field("len", &self.len)
384            .field("data", &self.as_slice())
385            .finish()
386    }
387}
388
389impl PartialEq for SysEx {
390    fn eq(&self, other: &Self) -> bool {
391        self.len == other.len && self.as_slice() == other.as_slice()
392    }
393}
394
395/// Note Expression value event (f64 precision).
396///
397/// Used for MPE-style per-note modulation. Each playing note can have
398/// independent expression values for volume, pan, tuning, etc.
399#[derive(Debug, Clone, Copy, PartialEq)]
400pub struct NoteExpressionValue {
401    /// Note ID this expression applies to.
402    pub note_id: NoteId,
403    /// Expression type (see [`note_expression`] module for constants).
404    pub expression_type: u32,
405    /// Normalized value. Range depends on expression type:
406    /// - Most types: 0.0 to 1.0
407    /// - Tuning: -0.5 to 0.5 (semitones, can exceed for wider range)
408    pub value: f64,
409}
410
411/// Note Expression integer value event.
412///
413/// Used for discrete expression values.
414#[derive(Debug, Clone, Copy, PartialEq)]
415pub struct NoteExpressionInt {
416    /// Note ID this expression applies to.
417    pub note_id: NoteId,
418    /// Expression type.
419    pub expression_type: u32,
420    /// Integer value.
421    pub value: u64,
422}
423
424/// Note Expression text event.
425///
426/// Used for text-based expression like phonemes for vocal synthesis.
427#[derive(Clone, Copy)]
428pub struct NoteExpressionText {
429    /// Note ID this expression applies to.
430    pub note_id: NoteId,
431    /// Expression type (typically TEXT or PHONEME).
432    pub expression_type: u32,
433    /// UTF-8 text data.
434    pub text: [u8; MAX_EXPRESSION_TEXT_SIZE],
435    /// Actual length of text.
436    pub text_len: u8,
437}
438
439impl NoteExpressionText {
440    /// Get the text as a string slice.
441    #[inline]
442    pub fn as_str(&self) -> &str {
443        core::str::from_utf8(&self.text[..self.text_len as usize]).unwrap_or("")
444    }
445}
446
447impl core::fmt::Debug for NoteExpressionText {
448    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
449        f.debug_struct("NoteExpressionText")
450            .field("note_id", &self.note_id)
451            .field("expression_type", &self.expression_type)
452            .field("text", &self.as_str())
453            .finish()
454    }
455}
456
457impl PartialEq for NoteExpressionText {
458    fn eq(&self, other: &Self) -> bool {
459        self.note_id == other.note_id
460            && self.expression_type == other.expression_type
461            && self.text_len == other.text_len
462            && self.text[..self.text_len as usize] == other.text[..other.text_len as usize]
463    }
464}
465
466/// Chord information from DAW chord track.
467///
468/// Provides harmonic context that plugins can use for intelligent processing.
469#[derive(Clone, Copy)]
470pub struct ChordInfo {
471    /// Root note pitch class (0=C, 1=C#, ..., 11=B), -1 = invalid/unknown.
472    pub root: i8,
473    /// Bass note pitch class (for slash chords like C/G), -1 = same as root.
474    pub bass_note: i8,
475    /// Bitmask of chord tones relative to root.
476    /// Bit 0 = root, bit 1 = minor 2nd, bit 2 = major 2nd, etc.
477    pub mask: u16,
478    /// Chord name as UTF-8 (e.g., "Cmaj7", "Dm").
479    pub name: [u8; MAX_CHORD_NAME_SIZE],
480    /// Actual length of name.
481    pub name_len: u8,
482}
483
484impl ChordInfo {
485    /// Get the chord name as a string slice.
486    #[inline]
487    pub fn name_str(&self) -> &str {
488        core::str::from_utf8(&self.name[..self.name_len as usize]).unwrap_or("")
489    }
490
491    /// Check if the chord info is valid.
492    #[inline]
493    pub fn is_valid(&self) -> bool {
494        self.root >= 0 && self.root < 12
495    }
496}
497
498impl core::fmt::Debug for ChordInfo {
499    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
500        f.debug_struct("ChordInfo")
501            .field("root", &self.root)
502            .field("bass_note", &self.bass_note)
503            .field("mask", &format_args!("{:#06x}", self.mask))
504            .field("name", &self.name_str())
505            .finish()
506    }
507}
508
509impl PartialEq for ChordInfo {
510    fn eq(&self, other: &Self) -> bool {
511        self.root == other.root
512            && self.bass_note == other.bass_note
513            && self.mask == other.mask
514            && self.name_len == other.name_len
515            && self.name[..self.name_len as usize] == other.name[..other.name_len as usize]
516    }
517}
518
519/// Scale/key information from DAW.
520///
521/// Provides tonal context that plugins can use for scale-aware processing.
522#[derive(Clone, Copy)]
523pub struct ScaleInfo {
524    /// Root note pitch class (0=C, 1=C#, ..., 11=B), -1 = invalid/unknown.
525    pub root: i8,
526    /// Bitmask of scale degrees (12 bits for chromatic scale).
527    /// Bit 0 = root, bit 1 = minor 2nd, bit 2 = major 2nd, etc.
528    pub mask: u16,
529    /// Scale name as UTF-8 (e.g., "Major", "Dorian", "Pentatonic").
530    pub name: [u8; MAX_SCALE_NAME_SIZE],
531    /// Actual length of name.
532    pub name_len: u8,
533}
534
535impl ScaleInfo {
536    /// Get the scale name as a string slice.
537    #[inline]
538    pub fn name_str(&self) -> &str {
539        core::str::from_utf8(&self.name[..self.name_len as usize]).unwrap_or("")
540    }
541
542    /// Check if the scale info is valid.
543    #[inline]
544    pub fn is_valid(&self) -> bool {
545        self.root >= 0 && self.root < 12
546    }
547
548    /// Check if a pitch class (0-11) is in the scale.
549    #[inline]
550    pub fn contains(&self, pitch_class: u8) -> bool {
551        if pitch_class >= 12 {
552            return false;
553        }
554        // Rotate mask so root is at bit 0
555        let rotated = if self.root >= 0 {
556            let shift = self.root as u32;
557            (self.mask >> shift) | (self.mask << (12 - shift))
558        } else {
559            self.mask
560        };
561        (rotated & (1 << pitch_class)) != 0
562    }
563}
564
565impl core::fmt::Debug for ScaleInfo {
566    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
567        f.debug_struct("ScaleInfo")
568            .field("root", &self.root)
569            .field("mask", &format_args!("{:#06x}", self.mask))
570            .field("name", &self.name_str())
571            .finish()
572    }
573}
574
575impl PartialEq for ScaleInfo {
576    fn eq(&self, other: &Self) -> bool {
577        self.root == other.root
578            && self.mask == other.mask
579            && self.name_len == other.name_len
580            && self.name[..self.name_len as usize] == other.name[..other.name_len as usize]
581    }
582}
583
584// =============================================================================
585// MIDI 2.0 Types
586// =============================================================================
587
588/// MIDI 2.0 Controller identifier.
589///
590/// Represents a MIDI 2.0 controller which can be either a Registered Parameter
591/// or an Assignable Controller, identified by bank and index.
592#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
593pub struct Midi2Controller {
594    /// Controller bank (0-127).
595    pub bank: u8,
596    /// True for Registered Parameter, false for Assignable Controller.
597    pub registered: bool,
598    /// Controller index within the bank.
599    pub index: u8,
600}
601
602impl Midi2Controller {
603    /// Create a new MIDI 2.0 controller.
604    pub const fn new(bank: u8, registered: bool, index: u8) -> Self {
605        Self {
606            bank,
607            registered,
608            index,
609        }
610    }
611
612    /// Create a Registered Parameter controller.
613    pub const fn registered(bank: u8, index: u8) -> Self {
614        Self::new(bank, true, index)
615    }
616
617    /// Create an Assignable Controller.
618    pub const fn assignable(bank: u8, index: u8) -> Self {
619        Self::new(bank, false, index)
620    }
621}
622
623// =============================================================================
624// MIDI Control Change Constants
625// =============================================================================
626
627/// Common MIDI Control Change (CC) numbers.
628pub mod cc {
629    /// Bank Select MSB (CC0).
630    pub const BANK_SELECT: u8 = 0;
631    /// Bank Select MSB (CC0) - explicit name.
632    pub const BANK_SELECT_MSB: u8 = 0;
633    /// Bank Select LSB (CC32).
634    pub const BANK_SELECT_LSB: u8 = 32;
635    /// Modulation Wheel (CC1).
636    pub const MOD_WHEEL: u8 = 1;
637    /// Breath Controller (CC2).
638    pub const BREATH: u8 = 2;
639    /// Volume (CC7).
640    pub const VOLUME: u8 = 7;
641    /// Pan (CC10).
642    pub const PAN: u8 = 10;
643    /// Expression (CC11).
644    pub const EXPRESSION: u8 = 11;
645    /// Sustain Pedal (CC64).
646    pub const SUSTAIN_PEDAL: u8 = 64;
647    /// Portamento (CC65).
648    pub const PORTAMENTO: u8 = 65;
649    /// Sostenuto Pedal (CC66).
650    pub const SOSTENUTO: u8 = 66;
651    /// Soft Pedal (CC67).
652    pub const SOFT_PEDAL: u8 = 67;
653    /// All Sound Off (CC120).
654    pub const ALL_SOUND_OFF: u8 = 120;
655    /// Reset All Controllers (CC121).
656    pub const RESET_ALL_CONTROLLERS: u8 = 121;
657    /// All Notes Off (CC123).
658    pub const ALL_NOTES_OFF: u8 = 123;
659
660    // =========================================================================
661    // System Messages (VST3 SDK 3.8.0)
662    // =========================================================================
663
664    /// Poly Pressure (virtual CC 131) - per-note aftertouch via LegacyMIDICCOut.
665    pub const POLY_PRESSURE: u8 = 131;
666    /// MTC Quarter Frame (virtual CC 132).
667    pub const QUARTER_FRAME: u8 = 132;
668    /// Song Select (virtual CC 133).
669    pub const SONG_SELECT: u8 = 133;
670    /// Song Position Pointer (virtual CC 134).
671    pub const SONG_POSITION: u8 = 134;
672    /// Cable Select (virtual CC 135).
673    pub const CABLE_SELECT: u8 = 135;
674    /// Tune Request (virtual CC 136).
675    pub const TUNE_REQUEST: u8 = 136;
676    /// MIDI Clock Start (virtual CC 137).
677    pub const CLOCK_START: u8 = 137;
678    /// MIDI Clock Continue (virtual CC 138).
679    pub const CLOCK_CONTINUE: u8 = 138;
680    /// MIDI Clock Stop (virtual CC 139).
681    pub const CLOCK_STOP: u8 = 139;
682    /// Active Sensing (virtual CC 140).
683    pub const ACTIVE_SENSING: u8 = 140;
684
685    // =========================================================================
686    // RPN/NRPN Controllers
687    // =========================================================================
688
689    /// Data Entry MSB (CC6) - Value for RPN/NRPN.
690    pub const DATA_ENTRY_MSB: u8 = 6;
691    /// Data Entry LSB (CC38) - Fine value for RPN/NRPN.
692    pub const DATA_ENTRY_LSB: u8 = 38;
693    /// Data Increment (CC96) - Increment RPN/NRPN value.
694    pub const DATA_INCREMENT: u8 = 96;
695    /// Data Decrement (CC97) - Decrement RPN/NRPN value.
696    pub const DATA_DECREMENT: u8 = 97;
697    /// NRPN LSB (CC98) - Non-Registered Parameter Number LSB.
698    pub const NRPN_LSB: u8 = 98;
699    /// NRPN MSB (CC99) - Non-Registered Parameter Number MSB.
700    pub const NRPN_MSB: u8 = 99;
701    /// RPN LSB (CC100) - Registered Parameter Number LSB.
702    pub const RPN_LSB: u8 = 100;
703    /// RPN MSB (CC101) - Registered Parameter Number MSB.
704    pub const RPN_MSB: u8 = 101;
705}
706
707// =============================================================================
708// Registered Parameter Numbers (RPNs)
709// =============================================================================
710
711/// Well-known Registered Parameter Numbers (RPNs).
712///
713/// These are standard MIDI parameters with defined meanings across all devices.
714/// RPN messages are sent using CC 101 (MSB) and CC 100 (LSB) to select the
715/// parameter, followed by CC 6 (Data Entry MSB) and optionally CC 38 (LSB)
716/// to set the value.
717///
718/// # Example
719///
720/// To set Pitch Bend Sensitivity to 12 semitones:
721/// ```text
722/// CC 101 = 0   (RPN MSB = 0)
723/// CC 100 = 0   (RPN LSB = 0)  → Selects Pitch Bend Sensitivity
724/// CC 6   = 12  (Data Entry = 12 semitones)
725/// CC 101 = 127 (RPN Null)
726/// CC 100 = 127 (RPN Null)     → Deselect to prevent accidental changes
727/// ```
728pub mod rpn {
729    /// Pitch Bend Sensitivity (semitones + cents).
730    ///
731    /// Data Entry MSB = semitones (0-127, typically 0-24).
732    /// Data Entry LSB = cents (0-127, typically 0).
733    /// Default is usually 2 semitones.
734    pub const PITCH_BEND_SENSITIVITY: u16 = 0x0000;
735
736    /// Channel Fine Tuning (cents, 14-bit).
737    ///
738    /// Value 0x2000 (8192) = A440 (no change).
739    /// Range: +/- 100 cents (approximately 1 semitone).
740    pub const FINE_TUNING: u16 = 0x0001;
741
742    /// Channel Coarse Tuning (semitones).
743    ///
744    /// Data Entry MSB = semitones offset from A440.
745    /// Value 64 = A440 (no change).
746    /// Range: +/- 64 semitones.
747    pub const COARSE_TUNING: u16 = 0x0002;
748
749    /// Tuning Program Change.
750    ///
751    /// Selects a tuning program (0-127) from the currently selected bank.
752    pub const TUNING_PROGRAM: u16 = 0x0003;
753
754    /// Tuning Bank Select.
755    ///
756    /// Selects a tuning bank (0-127).
757    pub const TUNING_BANK: u16 = 0x0004;
758
759    /// Modulation Depth Range (MPE).
760    ///
761    /// Sets the range for per-note pitch bend in MPE mode.
762    /// Data Entry MSB = semitones, LSB = cents.
763    pub const MODULATION_DEPTH: u16 = 0x0005;
764
765    /// MPE Configuration Message.
766    ///
767    /// Used to configure MPE zones. Sent on the Manager Channel.
768    /// Data Entry MSB = number of Member Channels (0 = disable MPE).
769    pub const MPE_CONFIGURATION: u16 = 0x0006;
770
771    /// RPN Null - Deselects RPN/NRPN (no parameter selected).
772    ///
773    /// Send after setting an RPN/NRPN value to prevent accidental
774    /// data entry changes from affecting parameters.
775    pub const NULL: u16 = 0x7F7F;
776
777    /// Check if a parameter number represents RPN Null.
778    #[inline]
779    pub const fn is_null(parameter: u16) -> bool {
780        parameter == NULL
781    }
782}
783
784// =============================================================================
785// RPN/NRPN Message Types
786// =============================================================================
787
788/// Type of parameter number (RPN vs NRPN).
789#[derive(Debug, Clone, Copy, PartialEq, Eq)]
790pub enum ParameterNumberKind {
791    /// Registered Parameter Number (CC 100/101).
792    /// Standard MIDI parameters with defined meanings.
793    Rpn,
794    /// Non-Registered Parameter Number (CC 98/99).
795    /// Manufacturer/device-specific parameters.
796    Nrpn,
797}
798
799/// A complete RPN or NRPN message with its 14-bit value.
800///
801/// This represents a fully-decoded RPN/NRPN sequence after the [`RpnTracker`]
802/// has assembled all the CC messages.
803///
804/// # Example
805///
806/// ```ignore
807/// use beamer_core::{RpnTracker, ControlChange, cc, rpn};
808///
809/// let mut tracker = RpnTracker::new();
810///
811/// // Simulate receiving CC sequence for Pitch Bend Sensitivity = 12 semitones
812/// tracker.process_cc(&ControlChange { channel: 0, controller: cc::RPN_MSB, value: 0.0 });
813/// tracker.process_cc(&ControlChange { channel: 0, controller: cc::RPN_LSB, value: 0.0 });
814/// let msg = tracker.process_cc(&ControlChange { channel: 0, controller: cc::DATA_ENTRY_MSB, value: 12.0/127.0 });
815///
816/// if let Some(msg) = msg {
817///     assert!(msg.is_pitch_bend_sensitivity());
818///     let (semitones, cents) = msg.pitch_bend_sensitivity();
819///     assert_eq!(semitones, 12);
820/// }
821/// ```
822#[derive(Debug, Clone, Copy, PartialEq)]
823pub struct ParameterNumberMessage {
824    /// MIDI channel (0-15).
825    pub channel: MidiChannel,
826    /// RPN or NRPN.
827    pub kind: ParameterNumberKind,
828    /// 14-bit parameter number (MSB << 7 | LSB).
829    pub parameter: u16,
830    /// 14-bit data value, normalized to 0.0-1.0.
831    pub value: f32,
832    /// Whether this was a data increment (+1 to current value).
833    pub is_increment: bool,
834    /// Whether this was a data decrement (-1 from current value).
835    pub is_decrement: bool,
836}
837
838impl ParameterNumberMessage {
839    /// Create a new RPN message.
840    pub const fn rpn(channel: MidiChannel, parameter: u16, value: f32) -> Self {
841        Self {
842            channel,
843            kind: ParameterNumberKind::Rpn,
844            parameter,
845            value,
846            is_increment: false,
847            is_decrement: false,
848        }
849    }
850
851    /// Create a new NRPN message.
852    pub const fn nrpn(channel: MidiChannel, parameter: u16, value: f32) -> Self {
853        Self {
854            channel,
855            kind: ParameterNumberKind::Nrpn,
856            parameter,
857            value,
858            is_increment: false,
859            is_decrement: false,
860        }
861    }
862
863    /// Check if this is an RPN.
864    #[inline]
865    pub const fn is_rpn(&self) -> bool {
866        matches!(self.kind, ParameterNumberKind::Rpn)
867    }
868
869    /// Check if this is an NRPN.
870    #[inline]
871    pub const fn is_nrpn(&self) -> bool {
872        matches!(self.kind, ParameterNumberKind::Nrpn)
873    }
874
875    /// Check if this is the Pitch Bend Sensitivity RPN.
876    #[inline]
877    pub fn is_pitch_bend_sensitivity(&self) -> bool {
878        self.is_rpn() && self.parameter == rpn::PITCH_BEND_SENSITIVITY
879    }
880
881    /// Check if this is the RPN Null message.
882    #[inline]
883    pub fn is_null(&self) -> bool {
884        self.is_rpn() && rpn::is_null(self.parameter)
885    }
886
887    /// Get the raw 14-bit value (0-16383).
888    #[inline]
889    pub fn raw_value(&self) -> u16 {
890        (self.value.clamp(0.0, 1.0) * 16383.0) as u16
891    }
892
893    /// For Pitch Bend Sensitivity: get semitones and cents.
894    ///
895    /// Returns (semitones, cents) where MSB = semitones (0-127)
896    /// and LSB = cents (0-127).
897    #[inline]
898    pub fn pitch_bend_sensitivity(&self) -> (u8, u8) {
899        let raw = self.raw_value();
900        let msb = ((raw >> 7) & 0x7F) as u8;
901        let lsb = (raw & 0x7F) as u8;
902        (msb, lsb)
903    }
904}
905
906// =============================================================================
907// RPN/NRPN Tracker
908// =============================================================================
909
910/// Per-channel RPN/NRPN state for tracking multi-CC sequences.
911#[derive(Debug, Clone, Copy, Default)]
912struct RpnChannelState {
913    /// Currently selected parameter MSB (CC 99/101).
914    parameter_msb: Option<u8>,
915    /// Currently selected parameter LSB (CC 98/100).
916    parameter_lsb: Option<u8>,
917    /// Current data entry MSB (CC 6).
918    data_msb: Option<u8>,
919    /// Current data entry LSB (CC 38).
920    data_lsb: Option<u8>,
921    /// Whether the current selection is RPN (true) or NRPN (false).
922    is_rpn: bool,
923}
924
925impl RpnChannelState {
926    /// Reset all state (e.g., after RPN Null).
927    fn reset(&mut self) {
928        *self = Self::default();
929    }
930
931    /// Check if we have a complete parameter selection.
932    fn has_parameter(&self) -> bool {
933        self.parameter_msb.is_some() && self.parameter_lsb.is_some()
934    }
935
936    /// Get the 14-bit parameter number if both MSB and LSB are set.
937    fn parameter(&self) -> Option<u16> {
938        match (self.parameter_msb, self.parameter_lsb) {
939            (Some(msb), Some(lsb)) => Some(combine_14bit_raw(msb, lsb)),
940            _ => None,
941        }
942    }
943
944    /// Get the 14-bit data value if MSB is set (LSB defaults to 0).
945    fn data_value(&self) -> Option<u16> {
946        self.data_msb.map(|msb| {
947            let lsb = self.data_lsb.unwrap_or(0);
948            combine_14bit_raw(msb, lsb)
949        })
950    }
951}
952
953/// Tracks RPN/NRPN state across all 16 MIDI channels.
954///
955/// This struct is designed for real-time safety:
956/// - Fixed-size array (no heap allocation)
957/// - All operations are O(1)
958/// - Implements `Copy` for simple value semantics
959///
960/// # Usage
961///
962/// Plugins that need to receive RPN/NRPN messages should store an instance
963/// of this tracker in their state and call `process_cc` for each incoming
964/// Control Change event.
965///
966/// ```ignore
967/// struct MyPlugin {
968///     rpn_tracker: RpnTracker,
969/// }
970///
971/// impl AudioProcessor for MyPlugin {
972///     fn process_midi(&mut self, input: &[MidiEvent], output: &mut MidiBuffer) {
973///         for event in input {
974///             if let MidiEventKind::ControlChange(cc) = &event.event {
975///                 if let Some(msg) = self.rpn_tracker.process_cc(cc) {
976///                     // Handle complete RPN/NRPN message
977///                     if msg.is_pitch_bend_sensitivity() {
978///                         let (semitones, cents) = msg.pitch_bend_sensitivity();
979///                         self.pitch_bend_range = semitones as f32 + cents as f32 / 100.0;
980///                     }
981///                 }
982///             }
983///         }
984///     }
985/// }
986/// ```
987#[derive(Debug, Clone, Copy)]
988pub struct RpnTracker {
989    /// Per-channel state for all 16 MIDI channels.
990    channels: [RpnChannelState; 16],
991}
992
993impl Default for RpnTracker {
994    fn default() -> Self {
995        Self::new()
996    }
997}
998
999impl RpnTracker {
1000    /// Create a new RPN tracker with all channels in their default state.
1001    pub const fn new() -> Self {
1002        Self {
1003            channels: [RpnChannelState {
1004                parameter_msb: None,
1005                parameter_lsb: None,
1006                data_msb: None,
1007                data_lsb: None,
1008                is_rpn: false,
1009            }; 16],
1010        }
1011    }
1012
1013    /// Reset all channel states.
1014    pub fn reset(&mut self) {
1015        for channel in &mut self.channels {
1016            channel.reset();
1017        }
1018    }
1019
1020    /// Reset a specific channel's state.
1021    pub fn reset_channel(&mut self, channel: MidiChannel) {
1022        if (channel as usize) < 16 {
1023            self.channels[channel as usize].reset();
1024        }
1025    }
1026
1027    /// Process a Control Change event.
1028    ///
1029    /// Returns `Some(ParameterNumberMessage)` when a complete RPN/NRPN
1030    /// message has been assembled from the CC sequence.
1031    ///
1032    /// # Arguments
1033    /// * `cc` - The Control Change event to process
1034    ///
1035    /// # Returns
1036    /// - `None` for non-RPN/NRPN CCs or incomplete sequences
1037    /// - `Some(message)` when a complete RPN/NRPN is ready
1038    pub fn process_cc(&mut self, cc: &ControlChange) -> Option<ParameterNumberMessage> {
1039        let channel_idx = (cc.channel as usize) & 0x0F;
1040        let state = &mut self.channels[channel_idx];
1041
1042        // Convert normalized value back to 7-bit
1043        let value_7bit = (cc.value.clamp(0.0, 1.0) * 127.0) as u8;
1044
1045        match cc.controller {
1046            // RPN parameter selection
1047            cc::RPN_MSB => {
1048                state.parameter_msb = Some(value_7bit);
1049                state.is_rpn = true;
1050                // Clear data values on new parameter selection
1051                state.data_msb = None;
1052                state.data_lsb = None;
1053                None
1054            }
1055            cc::RPN_LSB => {
1056                state.parameter_lsb = Some(value_7bit);
1057                state.is_rpn = true;
1058                // Check for RPN Null
1059                if let Some(parameter) = state.parameter() {
1060                    if rpn::is_null(parameter) {
1061                        state.reset();
1062                    }
1063                }
1064                None
1065            }
1066
1067            // NRPN parameter selection
1068            cc::NRPN_MSB => {
1069                state.parameter_msb = Some(value_7bit);
1070                state.is_rpn = false;
1071                state.data_msb = None;
1072                state.data_lsb = None;
1073                None
1074            }
1075            cc::NRPN_LSB => {
1076                state.parameter_lsb = Some(value_7bit);
1077                state.is_rpn = false;
1078                None
1079            }
1080
1081            // Data Entry MSB - may complete the message
1082            cc::DATA_ENTRY_MSB => {
1083                state.data_msb = Some(value_7bit);
1084                self.try_emit_message(channel_idx, false, false)
1085            }
1086
1087            // Data Entry LSB - may complete the message
1088            cc::DATA_ENTRY_LSB => {
1089                state.data_lsb = Some(value_7bit);
1090                // Only emit if we already have MSB
1091                if self.channels[channel_idx].data_msb.is_some() {
1092                    self.try_emit_message(channel_idx, false, false)
1093                } else {
1094                    None
1095                }
1096            }
1097
1098            // Data Increment
1099            cc::DATA_INCREMENT => {
1100                if self.channels[channel_idx].has_parameter() {
1101                    self.try_emit_message(channel_idx, true, false)
1102                } else {
1103                    None
1104                }
1105            }
1106
1107            // Data Decrement
1108            cc::DATA_DECREMENT => {
1109                if self.channels[channel_idx].has_parameter() {
1110                    self.try_emit_message(channel_idx, false, true)
1111                } else {
1112                    None
1113                }
1114            }
1115
1116            _ => None,
1117        }
1118    }
1119
1120    /// Try to emit a complete RPN/NRPN message.
1121    fn try_emit_message(
1122        &self,
1123        channel_idx: usize,
1124        is_increment: bool,
1125        is_decrement: bool,
1126    ) -> Option<ParameterNumberMessage> {
1127        let state = &self.channels[channel_idx];
1128
1129        let parameter = state.parameter()?;
1130
1131        // For increment/decrement, we don't need a data value
1132        let value = if is_increment || is_decrement {
1133            0.0 // Value is relative, not absolute
1134        } else {
1135            let raw = state.data_value()?;
1136            raw as f32 / 16383.0
1137        };
1138
1139        Some(ParameterNumberMessage {
1140            channel: channel_idx as u8,
1141            kind: if state.is_rpn {
1142                ParameterNumberKind::Rpn
1143            } else {
1144                ParameterNumberKind::Nrpn
1145            },
1146            parameter,
1147            value,
1148            is_increment,
1149            is_decrement,
1150        })
1151    }
1152
1153    /// Get the currently selected parameter for a channel, if any.
1154    pub fn current_parameter(&self, channel: MidiChannel) -> Option<(ParameterNumberKind, u16)> {
1155        let state = &self.channels[(channel as usize) & 0x0F];
1156        state.parameter().map(|p| {
1157            let kind = if state.is_rpn {
1158                ParameterNumberKind::Rpn
1159            } else {
1160                ParameterNumberKind::Nrpn
1161            };
1162            (kind, p)
1163        })
1164    }
1165}
1166
1167// =============================================================================
1168// 14-bit Controller Utilities
1169// =============================================================================
1170
1171/// Combines MSB and LSB controller values into a single 14-bit normalized value.
1172///
1173/// MIDI CC 0-31 are MSB controllers, and CC 32-63 are their corresponding LSB pairs.
1174/// Together they provide 14-bit resolution (0-16383) instead of 7-bit (0-127).
1175///
1176/// # Arguments
1177/// * `msb_value` - MSB controller value (0.0 to 1.0, normalized from CC 0-31)
1178/// * `lsb_value` - LSB controller value (0.0 to 1.0, normalized from CC 32-63)
1179///
1180/// # Returns
1181/// Combined 14-bit value normalized to 0.0-1.0
1182///
1183/// # Example
1184/// ```
1185/// use beamer_core::midi::combine_14bit_cc;
1186///
1187/// // Full resolution: MSB=127, LSB=127 → 16383 → 1.0
1188/// assert!((combine_14bit_cc(1.0, 1.0) - 1.0).abs() < 0.001);
1189///
1190/// // Center value: MSB=64, LSB=0 → 8192 → ~0.5
1191/// assert!((combine_14bit_cc(0.504, 0.0) - 0.5).abs() < 0.01);
1192/// ```
1193#[inline]
1194pub fn combine_14bit_cc(msb_value: f32, lsb_value: f32) -> f32 {
1195    let msb = (msb_value.clamp(0.0, 1.0) * 127.0) as u16;
1196    let lsb = (lsb_value.clamp(0.0, 1.0) * 127.0) as u16;
1197    let combined = (msb << 7) | (lsb & 0x7F);
1198    combined as f32 / 16383.0
1199}
1200
1201/// Splits a 14-bit normalized value into MSB and LSB controller values.
1202///
1203/// This is the inverse of [`combine_14bit_cc`].
1204///
1205/// # Arguments
1206/// * `value` - Combined 14-bit value (0.0 to 1.0)
1207///
1208/// # Returns
1209/// Tuple of (msb_value, lsb_value), both normalized to 0.0-1.0
1210///
1211/// # Example
1212/// ```
1213/// use beamer_core::midi::{split_14bit_cc, combine_14bit_cc};
1214///
1215/// // Round-trip test: split then combine should give same value
1216/// let original = 0.75;
1217/// let (msb, lsb) = split_14bit_cc(original);
1218/// let reconstructed = combine_14bit_cc(msb, lsb);
1219/// assert!((original - reconstructed).abs() < 0.001);
1220///
1221/// // Full value splits to (1.0, 1.0)
1222/// let (msb, lsb) = split_14bit_cc(1.0);
1223/// assert!((msb - 1.0).abs() < 0.01);
1224/// assert!((lsb - 1.0).abs() < 0.01);
1225/// ```
1226#[inline]
1227pub fn split_14bit_cc(value: f32) -> (f32, f32) {
1228    let raw = (value.clamp(0.0, 1.0) * 16383.0) as u16;
1229    let msb = ((raw >> 7) & 0x7F) as f32 / 127.0;
1230    let lsb = (raw & 0x7F) as f32 / 127.0;
1231    (msb, lsb)
1232}
1233
1234/// Combines two raw 7-bit values into a 14-bit value.
1235///
1236/// # Arguments
1237/// * `msb` - MSB value (0-127)
1238/// * `lsb` - LSB value (0-127)
1239///
1240/// # Returns
1241/// Combined 14-bit value (0-16383)
1242#[inline]
1243pub const fn combine_14bit_raw(msb: u8, lsb: u8) -> u16 {
1244    ((msb as u16) << 7) | ((lsb as u16) & 0x7F)
1245}
1246
1247/// Splits a 14-bit value into MSB and LSB components.
1248///
1249/// # Arguments
1250/// * `value` - 14-bit value (0-16383)
1251///
1252/// # Returns
1253/// Tuple of (msb, lsb), both 0-127
1254#[inline]
1255pub const fn split_14bit_raw(value: u16) -> (u8, u8) {
1256    let msb = ((value >> 7) & 0x7F) as u8;
1257    let lsb = (value & 0x7F) as u8;
1258    (msb, lsb)
1259}
1260
1261// =============================================================================
1262// Note Expression Constants
1263// =============================================================================
1264
1265/// VST3 Note Expression type IDs.
1266///
1267/// These constants identify the type of per-note expression in
1268/// [`NoteExpressionValue`], [`NoteExpressionInt`], and [`NoteExpressionText`] events.
1269pub mod note_expression {
1270    /// Per-note volume (0.0 = silent, 1.0 = full).
1271    pub const VOLUME: u32 = 0;
1272    /// Per-note pan (-1.0 = left, 0.0 = center, 1.0 = right).
1273    pub const PAN: u32 = 1;
1274    /// Per-note tuning in semitones. Critical for MPE pitch bend.
1275    /// Typically -0.5 to 0.5 for standard pitch bend range.
1276    pub const TUNING: u32 = 2;
1277    /// Per-note vibrato depth (0.0 to 1.0).
1278    pub const VIBRATO: u32 = 3;
1279    /// Per-note expression (general purpose, 0.0 to 1.0).
1280    pub const EXPRESSION: u32 = 4;
1281    /// Per-note brightness/timbre (0.0 to 1.0).
1282    pub const BRIGHTNESS: u32 = 5;
1283    /// Text expression type.
1284    pub const TEXT: u32 = 6;
1285    /// Phoneme expression type (for vocal synthesis).
1286    pub const PHONEME: u32 = 7;
1287    /// Start of custom expression type range.
1288    pub const CUSTOM_START: u32 = 100000;
1289    /// End of custom expression type range.
1290    pub const CUSTOM_END: u32 = 200000;
1291    /// Invalid type ID.
1292    pub const INVALID: u32 = u32::MAX;
1293}
1294
1295// =============================================================================
1296// MIDI Event Enum
1297// =============================================================================
1298
1299/// MIDI event types.
1300///
1301/// Most variants are small (8-32 bytes). The `SysEx` variant uses `Box<SysEx>`
1302/// to avoid bloating the enum size and prevent stack overflow.
1303#[derive(Debug, Clone, PartialEq)]
1304pub enum MidiEventKind {
1305    // =========================================================================
1306    // Note-related events (have note_id for tracking)
1307    // =========================================================================
1308
1309    /// Note on event.
1310    NoteOn(NoteOn),
1311    /// Note off event.
1312    NoteOff(NoteOff),
1313    /// Polyphonic key pressure (per-note aftertouch).
1314    PolyPressure(PolyPressure),
1315
1316    // =========================================================================
1317    // Channel-wide events
1318    // =========================================================================
1319
1320    /// Control change (CC).
1321    ControlChange(ControlChange),
1322    /// Pitch bend.
1323    PitchBend(PitchBend),
1324    /// Channel pressure (channel aftertouch).
1325    ChannelPressure(ChannelPressure),
1326    /// Program change.
1327    ProgramChange(ProgramChange),
1328
1329    // =========================================================================
1330    // Advanced VST3 events
1331    // =========================================================================
1332
1333    /// System Exclusive (SysEx) message.
1334    ///
1335    /// Uses `Box<SysEx>` to avoid bloating the enum size. SysEx messages are
1336    /// relatively rare compared to notes and CCs, so the heap allocation is acceptable.
1337    SysEx(Box<SysEx>),
1338    /// Per-note expression value (MPE, f64 precision).
1339    NoteExpressionValue(NoteExpressionValue),
1340    /// Per-note expression integer value.
1341    NoteExpressionInt(NoteExpressionInt),
1342    /// Per-note expression text.
1343    NoteExpressionText(NoteExpressionText),
1344    /// Chord information from DAW chord track.
1345    ChordInfo(ChordInfo),
1346    /// Scale/key information from DAW.
1347    ScaleInfo(ScaleInfo),
1348}
1349
1350/// A sample-accurate MIDI event.
1351///
1352/// The `sample_offset` field specifies when within the current audio buffer
1353/// this event should be processed, enabling sample-accurate MIDI timing.
1354#[derive(Debug, Clone, PartialEq)]
1355pub struct MidiEvent {
1356    /// Sample offset within the current buffer (0 = start of buffer).
1357    pub sample_offset: u32,
1358    /// The MIDI event data.
1359    pub event: MidiEventKind,
1360}
1361
1362impl Default for MidiEvent {
1363    /// Creates a default MidiEvent (NoteOff with all fields zeroed).
1364    ///
1365    /// Used for buffer initialization. Does not allocate.
1366    fn default() -> Self {
1367        Self {
1368            sample_offset: 0,
1369            event: MidiEventKind::NoteOff(NoteOff {
1370                channel: 0,
1371                pitch: 0,
1372                velocity: 0.0,
1373                note_id: -1,
1374                tuning: 0.0,
1375            }),
1376        }
1377    }
1378}
1379
1380impl MidiEvent {
1381    /// Create a new note-on event.
1382    pub const fn note_on(
1383        sample_offset: u32,
1384        channel: MidiChannel,
1385        pitch: MidiNote,
1386        velocity: f32,
1387        note_id: NoteId,
1388        tuning: f32,
1389        length: i32,
1390    ) -> Self {
1391        Self {
1392            sample_offset,
1393            event: MidiEventKind::NoteOn(NoteOn {
1394                channel,
1395                pitch,
1396                velocity,
1397                note_id,
1398                tuning,
1399                length,
1400            }),
1401        }
1402    }
1403
1404    /// Create a new note-off event.
1405    pub const fn note_off(
1406        sample_offset: u32,
1407        channel: MidiChannel,
1408        pitch: MidiNote,
1409        velocity: f32,
1410        note_id: NoteId,
1411        tuning: f32,
1412    ) -> Self {
1413        Self {
1414            sample_offset,
1415            event: MidiEventKind::NoteOff(NoteOff {
1416                channel,
1417                pitch,
1418                velocity,
1419                note_id,
1420                tuning,
1421            }),
1422        }
1423    }
1424
1425    /// Create a polyphonic pressure event.
1426    pub const fn poly_pressure(
1427        sample_offset: u32,
1428        channel: MidiChannel,
1429        pitch: MidiNote,
1430        pressure: f32,
1431        note_id: NoteId,
1432    ) -> Self {
1433        Self {
1434            sample_offset,
1435            event: MidiEventKind::PolyPressure(PolyPressure {
1436                channel,
1437                pitch,
1438                pressure,
1439                note_id,
1440            }),
1441        }
1442    }
1443
1444    /// Create a control change event.
1445    pub const fn control_change(
1446        sample_offset: u32,
1447        channel: MidiChannel,
1448        controller: u8,
1449        value: f32,
1450    ) -> Self {
1451        Self {
1452            sample_offset,
1453            event: MidiEventKind::ControlChange(ControlChange {
1454                channel,
1455                controller,
1456                value,
1457            }),
1458        }
1459    }
1460
1461    /// Create a pitch bend event.
1462    pub const fn pitch_bend(sample_offset: u32, channel: MidiChannel, value: f32) -> Self {
1463        Self {
1464            sample_offset,
1465            event: MidiEventKind::PitchBend(PitchBend { channel, value }),
1466        }
1467    }
1468
1469    /// Create a channel pressure event.
1470    pub const fn channel_pressure(
1471        sample_offset: u32,
1472        channel: MidiChannel,
1473        pressure: f32,
1474    ) -> Self {
1475        Self {
1476            sample_offset,
1477            event: MidiEventKind::ChannelPressure(ChannelPressure { channel, pressure }),
1478        }
1479    }
1480
1481    /// Create a program change event.
1482    pub const fn program_change(sample_offset: u32, channel: MidiChannel, program: u8) -> Self {
1483        Self {
1484            sample_offset,
1485            event: MidiEventKind::ProgramChange(ProgramChange { channel, program }),
1486        }
1487    }
1488
1489    // =========================================================================
1490    // Advanced VST3 event constructors
1491    // =========================================================================
1492
1493    /// Create a SysEx event.
1494    ///
1495    /// Note: This allocates the SysEx data on the heap. SysEx messages are
1496    /// relatively rare, so the allocation is acceptable.
1497    pub fn sysex(sample_offset: u32, data: &[u8]) -> Self {
1498        let mut sysex = SysEx::new();
1499        let copy_len = data.len().min(MAX_SYSEX_SIZE);
1500        sysex.data[..copy_len].copy_from_slice(&data[..copy_len]);
1501        sysex.len = copy_len as u16;
1502        Self {
1503            sample_offset,
1504            event: MidiEventKind::SysEx(Box::new(sysex)),
1505        }
1506    }
1507
1508    /// Create a Note Expression value event.
1509    pub const fn note_expression_value(
1510        sample_offset: u32,
1511        note_id: NoteId,
1512        expression_type: u32,
1513        value: f64,
1514    ) -> Self {
1515        Self {
1516            sample_offset,
1517            event: MidiEventKind::NoteExpressionValue(NoteExpressionValue {
1518                note_id,
1519                expression_type,
1520                value,
1521            }),
1522        }
1523    }
1524
1525    /// Create a Note Expression integer event.
1526    pub const fn note_expression_int(
1527        sample_offset: u32,
1528        note_id: NoteId,
1529        expression_type: u32,
1530        value: u64,
1531    ) -> Self {
1532        Self {
1533            sample_offset,
1534            event: MidiEventKind::NoteExpressionInt(NoteExpressionInt {
1535                note_id,
1536                expression_type,
1537                value,
1538            }),
1539        }
1540    }
1541
1542    /// Create a Note Expression text event.
1543    ///
1544    /// Note: This is not `const` because it initializes the fixed-size buffer.
1545    pub fn note_expression_text(
1546        sample_offset: u32,
1547        note_id: NoteId,
1548        expression_type: u32,
1549        text: &str,
1550    ) -> Self {
1551        let mut expr = NoteExpressionText {
1552            note_id,
1553            expression_type,
1554            text: [0u8; MAX_EXPRESSION_TEXT_SIZE],
1555            text_len: 0,
1556        };
1557        let bytes = text.as_bytes();
1558        let copy_len = bytes.len().min(MAX_EXPRESSION_TEXT_SIZE);
1559        expr.text[..copy_len].copy_from_slice(&bytes[..copy_len]);
1560        expr.text_len = copy_len as u8;
1561        Self {
1562            sample_offset,
1563            event: MidiEventKind::NoteExpressionText(expr),
1564        }
1565    }
1566
1567    /// Create a Chord info event.
1568    ///
1569    /// Note: This is not `const` because it initializes the fixed-size buffer.
1570    pub fn chord_info(
1571        sample_offset: u32,
1572        root: i8,
1573        bass_note: i8,
1574        mask: u16,
1575        name: &str,
1576    ) -> Self {
1577        let mut info = ChordInfo {
1578            root,
1579            bass_note,
1580            mask,
1581            name: [0u8; MAX_CHORD_NAME_SIZE],
1582            name_len: 0,
1583        };
1584        let bytes = name.as_bytes();
1585        let copy_len = bytes.len().min(MAX_CHORD_NAME_SIZE);
1586        info.name[..copy_len].copy_from_slice(&bytes[..copy_len]);
1587        info.name_len = copy_len as u8;
1588        Self {
1589            sample_offset,
1590            event: MidiEventKind::ChordInfo(info),
1591        }
1592    }
1593
1594    /// Create a Scale info event.
1595    ///
1596    /// Note: This is not `const` because it initializes the fixed-size buffer.
1597    pub fn scale_info(sample_offset: u32, root: i8, mask: u16, name: &str) -> Self {
1598        let mut info = ScaleInfo {
1599            root,
1600            mask,
1601            name: [0u8; MAX_SCALE_NAME_SIZE],
1602            name_len: 0,
1603        };
1604        let bytes = name.as_bytes();
1605        let copy_len = bytes.len().min(MAX_SCALE_NAME_SIZE);
1606        info.name[..copy_len].copy_from_slice(&bytes[..copy_len]);
1607        info.name_len = copy_len as u8;
1608        Self {
1609            sample_offset,
1610            event: MidiEventKind::ScaleInfo(info),
1611        }
1612    }
1613
1614    // =========================================================================
1615    // Event transformation
1616    // =========================================================================
1617
1618    /// Create a new event with the same timing but different event data.
1619    ///
1620    /// This preserves the `sample_offset` while replacing the `MidiEventKind`.
1621    /// Useful when transforming MIDI events where you've already matched on
1622    /// the event type and want to create a modified version.
1623    ///
1624    /// # Arguments
1625    /// * `kind` - The new event data
1626    ///
1627    /// # Returns
1628    /// A new `MidiEvent` with the same `sample_offset` but new event data.
1629    ///
1630    /// # Example
1631    /// ```ignore
1632    /// MidiEventKind::NoteOn(note_on) => {
1633    ///     output.push(event.clone().with(MidiEventKind::NoteOn(NoteOn {
1634    ///         pitch: new_pitch,
1635    ///         velocity: new_velocity,
1636    ///         ..*note_on  // Copy channel, note_id, tuning, length
1637    ///     })));
1638    /// }
1639    /// ```
1640    pub fn with(self, kind: MidiEventKind) -> Self {
1641        MidiEvent {
1642            sample_offset: self.sample_offset,
1643            event: kind,
1644        }
1645    }
1646}
1647
1648/// Maximum number of MIDI events per buffer.
1649/// This is a reasonable limit for real-time processing.
1650pub const MAX_MIDI_EVENTS: usize = 1024;
1651
1652/// A buffer for collecting MIDI events during processing.
1653///
1654/// Uses a fixed-size array to avoid heap allocation during processing.
1655/// Events should be added in chronological order (by sample_offset).
1656#[derive(Debug)]
1657pub struct MidiBuffer {
1658    events: [MidiEvent; MAX_MIDI_EVENTS],
1659    len: usize,
1660    /// Set to true when a push fails due to buffer exhaustion
1661    overflowed: bool,
1662}
1663
1664impl MidiBuffer {
1665    /// Create a new empty MIDI buffer.
1666    ///
1667    /// Uses `std::array::from_fn` with `MidiEvent::default()` since
1668    /// `MidiEvent` is no longer `Copy` (due to `Box<SysEx>`).
1669    pub fn new() -> Self {
1670        Self {
1671            events: std::array::from_fn(|_| MidiEvent::default()),
1672            len: 0,
1673            overflowed: false,
1674        }
1675    }
1676
1677    /// Clear all events from the buffer.
1678    #[inline]
1679    pub fn clear(&mut self) {
1680        self.len = 0;
1681        self.overflowed = false;
1682    }
1683
1684    /// Returns the number of events in the buffer.
1685    #[inline]
1686    pub fn len(&self) -> usize {
1687        self.len
1688    }
1689
1690    /// Returns true if the buffer is empty.
1691    #[inline]
1692    pub fn is_empty(&self) -> bool {
1693        self.len == 0
1694    }
1695
1696    /// Returns true if any push failed since the last clear.
1697    #[inline]
1698    pub fn has_overflowed(&self) -> bool {
1699        self.overflowed
1700    }
1701
1702    /// Push an event to the buffer.
1703    ///
1704    /// Returns `true` if the event was added, `false` if the buffer is full.
1705    /// Sets the overflow flag when the buffer is exhausted.
1706    #[inline]
1707    pub fn push(&mut self, event: MidiEvent) -> bool {
1708        if self.len < MAX_MIDI_EVENTS {
1709            self.events[self.len] = event;
1710            self.len += 1;
1711            true
1712        } else {
1713            self.overflowed = true;
1714            false
1715        }
1716    }
1717
1718    /// Iterate over events in the buffer.
1719    #[inline]
1720    pub fn iter(&self) -> impl Iterator<Item = &MidiEvent> {
1721        self.events[..self.len].iter()
1722    }
1723
1724    /// Get the events as a slice.
1725    ///
1726    /// This is useful for passing to functions that expect `&[MidiEvent]`.
1727    #[inline]
1728    pub fn as_slice(&self) -> &[MidiEvent] {
1729        &self.events[..self.len]
1730    }
1731}
1732
1733impl Default for MidiBuffer {
1734    fn default() -> Self {
1735        Self::new()
1736    }
1737}
1738
1739// =============================================================================
1740// Note Expression Controller Types (VST3 SDK 3.5.0)
1741// =============================================================================
1742
1743/// Flags for Note Expression type configuration.
1744#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1745pub struct NoteExpressionTypeFlags(pub i32);
1746
1747impl NoteExpressionTypeFlags {
1748    /// No special flags.
1749    pub const NONE: Self = Self(0);
1750    /// Event is bipolar (centered around 0), otherwise unipolar (0 to 1).
1751    pub const IS_BIPOLAR: Self = Self(1 << 0);
1752    /// Event occurs only once at the start of the note.
1753    pub const IS_ONE_SHOT: Self = Self(1 << 1);
1754    /// Expression applies absolute change (not relative/offset).
1755    pub const IS_ABSOLUTE: Self = Self(1 << 2);
1756    /// The associated_parameter_id field is valid.
1757    pub const ASSOCIATED_PARAMETER_ID_VALID: Self = Self(1 << 3);
1758
1759    /// Check if a flag is set.
1760    pub const fn contains(&self, flag: Self) -> bool {
1761        (self.0 & flag.0) != 0
1762    }
1763
1764    /// Combine flags.
1765    pub const fn or(self, other: Self) -> Self {
1766        Self(self.0 | other.0)
1767    }
1768}
1769
1770/// Value description for a Note Expression type.
1771#[derive(Debug, Clone, Copy, PartialEq, Default)]
1772pub struct NoteExpressionValueDesc {
1773    /// Minimum value (usually 0.0).
1774    pub minimum: f64,
1775    /// Maximum value (usually 1.0).
1776    pub maximum: f64,
1777    /// Default/center value.
1778    pub default_value: f64,
1779    /// Number of discrete steps (0 = continuous).
1780    pub step_count: i32,
1781}
1782
1783impl NoteExpressionValueDesc {
1784    /// Create a continuous unipolar value description (0.0 to 1.0).
1785    pub const fn unipolar() -> Self {
1786        Self {
1787            minimum: 0.0,
1788            maximum: 1.0,
1789            default_value: 0.0,
1790            step_count: 0,
1791        }
1792    }
1793
1794    /// Create a continuous bipolar value description (-1.0 to 1.0, center at 0.0).
1795    pub const fn bipolar() -> Self {
1796        Self {
1797            minimum: -1.0,
1798            maximum: 1.0,
1799            default_value: 0.0,
1800            step_count: 0,
1801        }
1802    }
1803
1804    /// Create a tuning value description (in semitones).
1805    pub const fn tuning(range_semitones: f64) -> Self {
1806        Self {
1807            minimum: -range_semitones,
1808            maximum: range_semitones,
1809            default_value: 0.0,
1810            step_count: 0,
1811        }
1812    }
1813}
1814
1815/// Maximum length for note expression title strings.
1816pub const MAX_NOTE_EXPRESSION_TITLE_SIZE: usize = 64;
1817
1818/// Information about a Note Expression type.
1819///
1820/// Used to advertise which note expressions the plugin supports.
1821#[derive(Clone, Copy)]
1822pub struct NoteExpressionTypeInfo {
1823    /// Unique identifier for this expression type.
1824    /// Use constants from [`note_expression`] module or custom IDs.
1825    pub type_id: u32,
1826    /// Display title (e.g., "Volume", "Tuning").
1827    pub title: [u8; MAX_NOTE_EXPRESSION_TITLE_SIZE],
1828    /// Title length.
1829    pub title_len: u8,
1830    /// Short title (e.g., "Vol", "Tun").
1831    pub short_title: [u8; MAX_NOTE_EXPRESSION_TITLE_SIZE],
1832    /// Short title length.
1833    pub short_title_len: u8,
1834    /// Unit label (e.g., "dB", "semitones").
1835    pub units: [u8; MAX_NOTE_EXPRESSION_TITLE_SIZE],
1836    /// Units length.
1837    pub units_len: u8,
1838    /// Unit ID for grouping (-1 for none).
1839    pub unit_id: i32,
1840    /// Value range description.
1841    pub value_desc: NoteExpressionValueDesc,
1842    /// Associated parameter ID for automation mapping (-1 for none).
1843    pub associated_parameter_id: i32,
1844    /// Configuration flags.
1845    pub flags: NoteExpressionTypeFlags,
1846}
1847
1848impl NoteExpressionTypeInfo {
1849    /// Create a new Note Expression type info.
1850    pub fn new(type_id: u32, title: &str, short_title: &str) -> Self {
1851        let mut info = Self {
1852            type_id,
1853            title: [0u8; MAX_NOTE_EXPRESSION_TITLE_SIZE],
1854            title_len: 0,
1855            short_title: [0u8; MAX_NOTE_EXPRESSION_TITLE_SIZE],
1856            short_title_len: 0,
1857            units: [0u8; MAX_NOTE_EXPRESSION_TITLE_SIZE],
1858            units_len: 0,
1859            unit_id: -1,
1860            value_desc: NoteExpressionValueDesc::unipolar(),
1861            associated_parameter_id: -1,
1862            flags: NoteExpressionTypeFlags::NONE,
1863        };
1864        info.set_title(title);
1865        info.set_short_title(short_title);
1866        info
1867    }
1868
1869    /// Set the title.
1870    pub fn set_title(&mut self, title: &str) {
1871        let bytes = title.as_bytes();
1872        let len = bytes.len().min(MAX_NOTE_EXPRESSION_TITLE_SIZE);
1873        self.title[..len].copy_from_slice(&bytes[..len]);
1874        self.title_len = len as u8;
1875    }
1876
1877    /// Set the short title.
1878    pub fn set_short_title(&mut self, short_title: &str) {
1879        let bytes = short_title.as_bytes();
1880        let len = bytes.len().min(MAX_NOTE_EXPRESSION_TITLE_SIZE);
1881        self.short_title[..len].copy_from_slice(&bytes[..len]);
1882        self.short_title_len = len as u8;
1883    }
1884
1885    /// Set the units label.
1886    pub fn set_units(&mut self, units: &str) {
1887        let bytes = units.as_bytes();
1888        let len = bytes.len().min(MAX_NOTE_EXPRESSION_TITLE_SIZE);
1889        self.units[..len].copy_from_slice(&bytes[..len]);
1890        self.units_len = len as u8;
1891    }
1892
1893    /// Get the title as a string slice.
1894    pub fn title_str(&self) -> &str {
1895        core::str::from_utf8(&self.title[..self.title_len as usize]).unwrap_or("")
1896    }
1897
1898    /// Get the short title as a string slice.
1899    pub fn short_title_str(&self) -> &str {
1900        core::str::from_utf8(&self.short_title[..self.short_title_len as usize]).unwrap_or("")
1901    }
1902
1903    /// Get the units as a string slice.
1904    pub fn units_str(&self) -> &str {
1905        core::str::from_utf8(&self.units[..self.units_len as usize]).unwrap_or("")
1906    }
1907
1908    /// Builder: set value description.
1909    pub fn with_value_desc(mut self, desc: NoteExpressionValueDesc) -> Self {
1910        self.value_desc = desc;
1911        self
1912    }
1913
1914    /// Builder: set flags.
1915    pub fn with_flags(mut self, flags: NoteExpressionTypeFlags) -> Self {
1916        self.flags = flags;
1917        self
1918    }
1919
1920    /// Builder: set units.
1921    pub fn with_units(mut self, units: &str) -> Self {
1922        self.set_units(units);
1923        self
1924    }
1925
1926    /// Builder: set associated parameter.
1927    pub fn with_associated_parameter(mut self, parameter_id: i32) -> Self {
1928        self.associated_parameter_id = parameter_id;
1929        self.flags = self.flags.or(NoteExpressionTypeFlags::ASSOCIATED_PARAMETER_ID_VALID);
1930        self
1931    }
1932}
1933
1934impl core::fmt::Debug for NoteExpressionTypeInfo {
1935    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1936        f.debug_struct("NoteExpressionTypeInfo")
1937            .field("type_id", &self.type_id)
1938            .field("title", &self.title_str())
1939            .field("short_title", &self.short_title_str())
1940            .field("units", &self.units_str())
1941            .field("value_desc", &self.value_desc)
1942            .field("flags", &self.flags)
1943            .finish()
1944    }
1945}
1946
1947impl Default for NoteExpressionTypeInfo {
1948    fn default() -> Self {
1949        Self::new(note_expression::INVALID, "", "")
1950    }
1951}
1952
1953// =============================================================================
1954// Keyswitch Controller Types (VST3 SDK 3.5.0)
1955// =============================================================================
1956
1957/// Keyswitch type identifiers.
1958pub mod keyswitch_type {
1959    /// Keyswitch triggered by a single note on/off.
1960    pub const NOTE_ON_KEY: u32 = 0;
1961    /// Keyswitch that must be held (pressed while playing).
1962    pub const ON_THE_FLY: u32 = 1;
1963    /// Keyswitch that toggles on/off with repeated presses.
1964    pub const ON_RELEASE: u32 = 2;
1965    /// Keyswitch triggered by a range of keys.
1966    pub const KEY_RANGE: u32 = 3;
1967}
1968
1969/// Maximum length for keyswitch title strings.
1970pub const MAX_KEYSWITCH_TITLE_SIZE: usize = 64;
1971
1972/// Information about a keyswitch (articulation).
1973///
1974/// Used by sample libraries and orchestral instruments to describe
1975/// available articulation switches.
1976#[derive(Clone, Copy)]
1977pub struct KeyswitchInfo {
1978    /// Keyswitch type (see [`keyswitch_type`] module).
1979    pub type_id: u32,
1980    /// Display title (e.g., "Staccato", "Legato").
1981    pub title: [u8; MAX_KEYSWITCH_TITLE_SIZE],
1982    /// Title length.
1983    pub title_len: u8,
1984    /// Short title (e.g., "Stac", "Leg").
1985    pub short_title: [u8; MAX_KEYSWITCH_TITLE_SIZE],
1986    /// Short title length.
1987    pub short_title_len: u8,
1988    /// Minimum key in the keyswitch range (MIDI note 0-127).
1989    pub keyswitch_min: i32,
1990    /// Maximum key in the keyswitch range (MIDI note 0-127).
1991    pub keyswitch_max: i32,
1992    /// Remapped key (-1 if not remapped).
1993    pub key_remapped: i32,
1994    /// Unit ID for grouping (-1 for none).
1995    pub unit_id: i32,
1996    /// Flags (reserved for future use).
1997    pub flags: i32,
1998}
1999
2000impl KeyswitchInfo {
2001    /// Create a new keyswitch info for a single key.
2002    pub fn new(type_id: u32, title: &str, key: i32) -> Self {
2003        let mut info = Self {
2004            type_id,
2005            title: [0u8; MAX_KEYSWITCH_TITLE_SIZE],
2006            title_len: 0,
2007            short_title: [0u8; MAX_KEYSWITCH_TITLE_SIZE],
2008            short_title_len: 0,
2009            keyswitch_min: key,
2010            keyswitch_max: key,
2011            key_remapped: -1,
2012            unit_id: -1,
2013            flags: 0,
2014        };
2015        info.set_title(title);
2016        info
2017    }
2018
2019    /// Create a keyswitch for a range of keys.
2020    pub fn key_range(type_id: u32, title: &str, min_key: i32, max_key: i32) -> Self {
2021        let mut info = Self::new(type_id, title, min_key);
2022        info.keyswitch_max = max_key;
2023        info
2024    }
2025
2026    /// Set the title.
2027    pub fn set_title(&mut self, title: &str) {
2028        let bytes = title.as_bytes();
2029        let len = bytes.len().min(MAX_KEYSWITCH_TITLE_SIZE);
2030        self.title[..len].copy_from_slice(&bytes[..len]);
2031        self.title_len = len as u8;
2032    }
2033
2034    /// Set the short title.
2035    pub fn set_short_title(&mut self, short_title: &str) {
2036        let bytes = short_title.as_bytes();
2037        let len = bytes.len().min(MAX_KEYSWITCH_TITLE_SIZE);
2038        self.short_title[..len].copy_from_slice(&bytes[..len]);
2039        self.short_title_len = len as u8;
2040    }
2041
2042    /// Get the title as a string slice.
2043    pub fn title_str(&self) -> &str {
2044        core::str::from_utf8(&self.title[..self.title_len as usize]).unwrap_or("")
2045    }
2046
2047    /// Get the short title as a string slice.
2048    pub fn short_title_str(&self) -> &str {
2049        core::str::from_utf8(&self.short_title[..self.short_title_len as usize]).unwrap_or("")
2050    }
2051
2052    /// Builder: set short title.
2053    pub fn with_short_title(mut self, short_title: &str) -> Self {
2054        self.set_short_title(short_title);
2055        self
2056    }
2057
2058    /// Builder: set remapped key.
2059    pub fn with_key_remapped(mut self, key: i32) -> Self {
2060        self.key_remapped = key;
2061        self
2062    }
2063}
2064
2065impl core::fmt::Debug for KeyswitchInfo {
2066    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
2067        f.debug_struct("KeyswitchInfo")
2068            .field("type_id", &self.type_id)
2069            .field("title", &self.title_str())
2070            .field("keyswitch_min", &self.keyswitch_min)
2071            .field("keyswitch_max", &self.keyswitch_max)
2072            .finish()
2073    }
2074}
2075
2076impl Default for KeyswitchInfo {
2077    fn default() -> Self {
2078        Self::new(keyswitch_type::NOTE_ON_KEY, "", 0)
2079    }
2080}
2081
2082// =============================================================================
2083// Physical UI Mapping Types (VST3 SDK 3.6.11)
2084// =============================================================================
2085
2086/// Physical UI type identifiers for MPE and physical controllers.
2087pub mod physical_ui {
2088    /// X-axis movement (horizontal slide on MPE controllers).
2089    pub const X_MOVEMENT: u32 = 0;
2090    /// Y-axis movement (vertical slide / "Slide" on MPE controllers).
2091    pub const Y_MOVEMENT: u32 = 1;
2092    /// Pressure (aftertouch on MPE controllers).
2093    pub const PRESSURE: u32 = 2;
2094    /// Type face (for instruments with multiple playing styles).
2095    pub const TYPE_FACE: u32 = 3;
2096    /// Reserved value for unassigned/unknown.
2097    pub const INVALID: u32 = u32::MAX;
2098}
2099
2100/// Maps a physical UI input to a Note Expression output.
2101///
2102/// Used to define how MPE controllers map to note expression types.
2103#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2104pub struct PhysicalUIMap {
2105    /// Physical UI type (see [`physical_ui`] module).
2106    pub physical_ui_type_id: u32,
2107    /// Note expression type to map to (see [`note_expression`] module).
2108    pub note_expression_type_id: u32,
2109}
2110
2111impl PhysicalUIMap {
2112    /// Create a new physical UI mapping.
2113    pub const fn new(physical_ui_type_id: u32, note_expression_type_id: u32) -> Self {
2114        Self {
2115            physical_ui_type_id,
2116            note_expression_type_id,
2117        }
2118    }
2119
2120    /// Map X-axis to a note expression.
2121    pub const fn x_axis(note_expression_type_id: u32) -> Self {
2122        Self::new(physical_ui::X_MOVEMENT, note_expression_type_id)
2123    }
2124
2125    /// Map Y-axis (Slide) to a note expression.
2126    pub const fn y_axis(note_expression_type_id: u32) -> Self {
2127        Self::new(physical_ui::Y_MOVEMENT, note_expression_type_id)
2128    }
2129
2130    /// Map Pressure to a note expression.
2131    pub const fn pressure(note_expression_type_id: u32) -> Self {
2132        Self::new(physical_ui::PRESSURE, note_expression_type_id)
2133    }
2134}
2135
2136// =============================================================================
2137// MPE Support Types (VST3 SDK 3.6.12)
2138// =============================================================================
2139
2140/// MPE (MIDI Polyphonic Expression) input device settings.
2141///
2142/// Defines the MPE zone configuration for incoming MIDI.
2143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2144pub struct MpeInputDeviceSettings {
2145    /// Master channel (0 = channel 1, typically 0 for lower zone).
2146    pub master_channel: i32,
2147    /// First member channel (typically 1 for lower zone).
2148    pub member_begin_channel: i32,
2149    /// Last member channel (typically 14 for lower zone).
2150    pub member_end_channel: i32,
2151}
2152
2153impl Default for MpeInputDeviceSettings {
2154    fn default() -> Self {
2155        Self {
2156            master_channel: 0,
2157            member_begin_channel: 1,
2158            member_end_channel: 14,
2159        }
2160    }
2161}
2162
2163impl MpeInputDeviceSettings {
2164    /// Create MPE settings for the lower zone (default configuration).
2165    pub const fn lower_zone() -> Self {
2166        Self {
2167            master_channel: 0,
2168            member_begin_channel: 1,
2169            member_end_channel: 14,
2170        }
2171    }
2172
2173    /// Create MPE settings for the upper zone.
2174    pub const fn upper_zone() -> Self {
2175        Self {
2176            master_channel: 15,
2177            member_begin_channel: 14,
2178            member_end_channel: 1,
2179        }
2180    }
2181
2182    /// Create custom MPE settings.
2183    pub const fn new(master: i32, begin: i32, end: i32) -> Self {
2184        Self {
2185            master_channel: master,
2186            member_begin_channel: begin,
2187            member_end_channel: end,
2188        }
2189    }
2190}