Skip to main content

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 Processor 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    // Raw MIDI 1.0 byte parsing
1491    // =========================================================================
1492
1493    /// Parse a MIDI 1.0 channel voice message from raw bytes.
1494    ///
1495    /// This is the standard way to convert raw MIDI bytes (as received from
1496    /// plugin hosts or hardware MIDI) into beamer's `MidiEvent` format.
1497    ///
1498    /// # Arguments
1499    ///
1500    /// * `sample_offset` - Sample position within the current buffer
1501    /// * `status` - MIDI status byte with channel masked out (0x80-0xF0)
1502    /// * `channel` - MIDI channel (0-15)
1503    /// * `data1` - First data byte (note number, CC number, etc.)
1504    /// * `data2` - Second data byte (velocity, CC value, etc.)
1505    ///
1506    /// # Returns
1507    ///
1508    /// `Some(MidiEvent)` for supported channel voice messages, `None` for
1509    /// unsupported message types (system messages, etc.)
1510    ///
1511    /// # Supported Messages
1512    ///
1513    /// | Status | Message Type     | data1        | data2         |
1514    /// |--------|------------------|--------------|---------------|
1515    /// | 0x80   | Note Off         | note number  | velocity      |
1516    /// | 0x90   | Note On          | note number  | velocity      |
1517    /// | 0xA0   | Poly Pressure    | note number  | pressure      |
1518    /// | 0xB0   | Control Change   | CC number    | value         |
1519    /// | 0xC0   | Program Change   | program      | (ignored)     |
1520    /// | 0xD0   | Channel Pressure | pressure     | (ignored)     |
1521    /// | 0xE0   | Pitch Bend       | LSB          | MSB           |
1522    ///
1523    /// # Notes
1524    ///
1525    /// - Note On with velocity 0 is converted to Note Off (per MIDI spec)
1526    /// - Velocities and CC values are normalized: 0-127 → 0.0-1.0
1527    /// - Pitch bend is normalized: 0-16383 → -1.0 to 1.0 (center at 8192)
1528    /// - `note_id` is set to the pitch value for basic voice allocation
1529    /// - `tuning` and `length` default to 0
1530    ///
1531    /// # Example
1532    ///
1533    /// ```
1534    /// use beamer_core::MidiEvent;
1535    ///
1536    /// // Parse a Note On: channel 0, note 60 (middle C), velocity 100
1537    /// let event = MidiEvent::from_midi1_bytes(0, 0x90, 0, 60, 100);
1538    /// assert!(event.is_some());
1539    /// ```
1540    #[inline]
1541    pub fn from_midi1_bytes(
1542        sample_offset: u32,
1543        status: u8,
1544        channel: MidiChannel,
1545        data1: u8,
1546        data2: u8,
1547    ) -> Option<Self> {
1548        match status {
1549            0x80 => Some(Self::note_off(
1550                sample_offset,
1551                channel,
1552                data1,
1553                data2 as f32 / 127.0,
1554                data1 as NoteId,
1555                0.0,
1556            )),
1557            0x90 => {
1558                if data2 == 0 {
1559                    // Note On with velocity 0 = Note Off (per MIDI spec)
1560                    Some(Self::note_off(
1561                        sample_offset,
1562                        channel,
1563                        data1,
1564                        0.0,
1565                        data1 as NoteId,
1566                        0.0,
1567                    ))
1568                } else {
1569                    Some(Self::note_on(
1570                        sample_offset,
1571                        channel,
1572                        data1,
1573                        data2 as f32 / 127.0,
1574                        data1 as NoteId,
1575                        0.0,
1576                        0,
1577                    ))
1578                }
1579            }
1580            0xA0 => Some(Self::poly_pressure(
1581                sample_offset,
1582                channel,
1583                data1,
1584                data2 as f32 / 127.0,
1585                data1 as NoteId,
1586            )),
1587            0xB0 => Some(Self::control_change(
1588                sample_offset,
1589                channel,
1590                data1,
1591                data2 as f32 / 127.0,
1592            )),
1593            0xC0 => Some(Self::program_change(sample_offset, channel, data1)),
1594            0xD0 => Some(Self::channel_pressure(
1595                sample_offset,
1596                channel,
1597                data1 as f32 / 127.0,
1598            )),
1599            0xE0 => {
1600                // Pitch bend: data1 = LSB (bits 0-6), data2 = MSB (bits 7-13)
1601                // Raw value: 0-16383, center at 8192
1602                // Normalized: -1.0 to 1.0
1603                let raw_value = ((data2 as u16) << 7) | (data1 as u16);
1604                let normalized = (raw_value as f32 - 8192.0) / 8192.0;
1605                Some(Self::pitch_bend(sample_offset, channel, normalized))
1606            }
1607            _ => None, // System messages, etc. not supported
1608        }
1609    }
1610
1611    // =========================================================================
1612    // Advanced VST3 event constructors
1613    // =========================================================================
1614
1615    /// Create a SysEx event.
1616    ///
1617    /// Note: This allocates the SysEx data on the heap. SysEx messages are
1618    /// relatively rare, so the allocation is acceptable.
1619    pub fn sysex(sample_offset: u32, data: &[u8]) -> Self {
1620        let mut sysex = SysEx::new();
1621        let copy_len = data.len().min(MAX_SYSEX_SIZE);
1622        sysex.data[..copy_len].copy_from_slice(&data[..copy_len]);
1623        sysex.len = copy_len as u16;
1624        Self {
1625            sample_offset,
1626            event: MidiEventKind::SysEx(Box::new(sysex)),
1627        }
1628    }
1629
1630    /// Create a Note Expression value event.
1631    pub const fn note_expression_value(
1632        sample_offset: u32,
1633        note_id: NoteId,
1634        expression_type: u32,
1635        value: f64,
1636    ) -> Self {
1637        Self {
1638            sample_offset,
1639            event: MidiEventKind::NoteExpressionValue(NoteExpressionValue {
1640                note_id,
1641                expression_type,
1642                value,
1643            }),
1644        }
1645    }
1646
1647    /// Create a Note Expression integer event.
1648    pub const fn note_expression_int(
1649        sample_offset: u32,
1650        note_id: NoteId,
1651        expression_type: u32,
1652        value: u64,
1653    ) -> Self {
1654        Self {
1655            sample_offset,
1656            event: MidiEventKind::NoteExpressionInt(NoteExpressionInt {
1657                note_id,
1658                expression_type,
1659                value,
1660            }),
1661        }
1662    }
1663
1664    /// Create a Note Expression text event.
1665    ///
1666    /// Note: This is not `const` because it initializes the fixed-size buffer.
1667    pub fn note_expression_text(
1668        sample_offset: u32,
1669        note_id: NoteId,
1670        expression_type: u32,
1671        text: &str,
1672    ) -> Self {
1673        let mut expr = NoteExpressionText {
1674            note_id,
1675            expression_type,
1676            text: [0u8; MAX_EXPRESSION_TEXT_SIZE],
1677            text_len: 0,
1678        };
1679        let bytes = text.as_bytes();
1680        let copy_len = bytes.len().min(MAX_EXPRESSION_TEXT_SIZE);
1681        expr.text[..copy_len].copy_from_slice(&bytes[..copy_len]);
1682        expr.text_len = copy_len as u8;
1683        Self {
1684            sample_offset,
1685            event: MidiEventKind::NoteExpressionText(expr),
1686        }
1687    }
1688
1689    /// Create a Chord info event.
1690    ///
1691    /// Note: This is not `const` because it initializes the fixed-size buffer.
1692    pub fn chord_info(
1693        sample_offset: u32,
1694        root: i8,
1695        bass_note: i8,
1696        mask: u16,
1697        name: &str,
1698    ) -> Self {
1699        let mut info = ChordInfo {
1700            root,
1701            bass_note,
1702            mask,
1703            name: [0u8; MAX_CHORD_NAME_SIZE],
1704            name_len: 0,
1705        };
1706        let bytes = name.as_bytes();
1707        let copy_len = bytes.len().min(MAX_CHORD_NAME_SIZE);
1708        info.name[..copy_len].copy_from_slice(&bytes[..copy_len]);
1709        info.name_len = copy_len as u8;
1710        Self {
1711            sample_offset,
1712            event: MidiEventKind::ChordInfo(info),
1713        }
1714    }
1715
1716    /// Create a Scale info event.
1717    ///
1718    /// Note: This is not `const` because it initializes the fixed-size buffer.
1719    pub fn scale_info(sample_offset: u32, root: i8, mask: u16, name: &str) -> Self {
1720        let mut info = ScaleInfo {
1721            root,
1722            mask,
1723            name: [0u8; MAX_SCALE_NAME_SIZE],
1724            name_len: 0,
1725        };
1726        let bytes = name.as_bytes();
1727        let copy_len = bytes.len().min(MAX_SCALE_NAME_SIZE);
1728        info.name[..copy_len].copy_from_slice(&bytes[..copy_len]);
1729        info.name_len = copy_len as u8;
1730        Self {
1731            sample_offset,
1732            event: MidiEventKind::ScaleInfo(info),
1733        }
1734    }
1735
1736    // =========================================================================
1737    // Event transformation
1738    // =========================================================================
1739
1740    /// Create a new event with the same timing but different event data.
1741    ///
1742    /// This preserves the `sample_offset` while replacing the `MidiEventKind`.
1743    /// Useful when transforming MIDI events where you've already matched on
1744    /// the event type and want to create a modified version.
1745    ///
1746    /// # Arguments
1747    /// * `kind` - The new event data
1748    ///
1749    /// # Returns
1750    /// A new `MidiEvent` with the same `sample_offset` but new event data.
1751    ///
1752    /// # Example
1753    /// ```ignore
1754    /// MidiEventKind::NoteOn(note_on) => {
1755    ///     output.push(event.clone().with(MidiEventKind::NoteOn(NoteOn {
1756    ///         pitch: new_pitch,
1757    ///         velocity: new_velocity,
1758    ///         ..*note_on  // Copy channel, note_id, tuning, length
1759    ///     })));
1760    /// }
1761    /// ```
1762    pub fn with(self, kind: MidiEventKind) -> Self {
1763        MidiEvent {
1764            sample_offset: self.sample_offset,
1765            event: kind,
1766        }
1767    }
1768}
1769
1770/// Maximum number of MIDI events per buffer.
1771/// This is a reasonable limit for real-time processing.
1772pub const MAX_MIDI_EVENTS: usize = 1024;
1773
1774/// A buffer for collecting MIDI events during processing.
1775///
1776/// Uses a fixed-size array to avoid heap allocation during processing.
1777/// Events should be added in chronological order (by sample_offset).
1778#[derive(Debug)]
1779pub struct MidiBuffer {
1780    events: [MidiEvent; MAX_MIDI_EVENTS],
1781    len: usize,
1782    /// Set to true when a push fails due to buffer exhaustion
1783    overflowed: bool,
1784}
1785
1786impl MidiBuffer {
1787    /// Create a new empty MIDI buffer.
1788    ///
1789    /// Uses `std::array::from_fn` with `MidiEvent::default()` since
1790    /// `MidiEvent` is no longer `Copy` (due to `Box<SysEx>`).
1791    pub fn new() -> Self {
1792        Self {
1793            events: std::array::from_fn(|_| MidiEvent::default()),
1794            len: 0,
1795            overflowed: false,
1796        }
1797    }
1798
1799    /// Clear all events from the buffer.
1800    #[inline]
1801    pub fn clear(&mut self) {
1802        self.len = 0;
1803        self.overflowed = false;
1804    }
1805
1806    /// Returns the number of events in the buffer.
1807    #[inline]
1808    pub fn len(&self) -> usize {
1809        self.len
1810    }
1811
1812    /// Returns true if the buffer is empty.
1813    #[inline]
1814    pub fn is_empty(&self) -> bool {
1815        self.len == 0
1816    }
1817
1818    /// Returns true if any push failed since the last clear.
1819    #[inline]
1820    pub fn has_overflowed(&self) -> bool {
1821        self.overflowed
1822    }
1823
1824    /// Push an event to the buffer.
1825    ///
1826    /// Returns `true` if the event was added, `false` if the buffer is full.
1827    /// Sets the overflow flag when the buffer is exhausted.
1828    #[inline]
1829    pub fn push(&mut self, event: MidiEvent) -> bool {
1830        if self.len < MAX_MIDI_EVENTS {
1831            self.events[self.len] = event;
1832            self.len += 1;
1833            true
1834        } else {
1835            self.overflowed = true;
1836            false
1837        }
1838    }
1839
1840    /// Iterate over events in the buffer.
1841    #[inline]
1842    pub fn iter(&self) -> impl Iterator<Item = &MidiEvent> {
1843        self.events[..self.len].iter()
1844    }
1845
1846    /// Get the events as a slice.
1847    ///
1848    /// This is useful for passing to functions that expect `&[MidiEvent]`.
1849    #[inline]
1850    pub fn as_slice(&self) -> &[MidiEvent] {
1851        &self.events[..self.len]
1852    }
1853}
1854
1855impl Default for MidiBuffer {
1856    fn default() -> Self {
1857        Self::new()
1858    }
1859}
1860
1861// =============================================================================
1862// Note Expression Controller Types (VST3 SDK 3.5.0)
1863// =============================================================================
1864
1865/// Flags for Note Expression type configuration.
1866#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1867pub struct NoteExpressionTypeFlags(pub i32);
1868
1869impl NoteExpressionTypeFlags {
1870    /// No special flags.
1871    pub const NONE: Self = Self(0);
1872    /// Event is bipolar (centered around 0), otherwise unipolar (0 to 1).
1873    pub const IS_BIPOLAR: Self = Self(1 << 0);
1874    /// Event occurs only once at the start of the note.
1875    pub const IS_ONE_SHOT: Self = Self(1 << 1);
1876    /// Expression applies absolute change (not relative/offset).
1877    pub const IS_ABSOLUTE: Self = Self(1 << 2);
1878    /// The associated_parameter_id field is valid.
1879    pub const ASSOCIATED_PARAMETER_ID_VALID: Self = Self(1 << 3);
1880
1881    /// Check if a flag is set.
1882    pub const fn contains(&self, flag: Self) -> bool {
1883        (self.0 & flag.0) != 0
1884    }
1885
1886    /// Combine flags.
1887    pub const fn or(self, other: Self) -> Self {
1888        Self(self.0 | other.0)
1889    }
1890}
1891
1892/// Value description for a Note Expression type.
1893#[derive(Debug, Clone, Copy, PartialEq, Default)]
1894pub struct NoteExpressionValueDesc {
1895    /// Minimum value (usually 0.0).
1896    pub minimum: f64,
1897    /// Maximum value (usually 1.0).
1898    pub maximum: f64,
1899    /// Default/center value.
1900    pub default_value: f64,
1901    /// Number of discrete steps (0 = continuous).
1902    pub step_count: i32,
1903}
1904
1905impl NoteExpressionValueDesc {
1906    /// Create a continuous unipolar value description (0.0 to 1.0).
1907    pub const fn unipolar() -> Self {
1908        Self {
1909            minimum: 0.0,
1910            maximum: 1.0,
1911            default_value: 0.0,
1912            step_count: 0,
1913        }
1914    }
1915
1916    /// Create a continuous bipolar value description (-1.0 to 1.0, center at 0.0).
1917    pub const fn bipolar() -> Self {
1918        Self {
1919            minimum: -1.0,
1920            maximum: 1.0,
1921            default_value: 0.0,
1922            step_count: 0,
1923        }
1924    }
1925
1926    /// Create a tuning value description (in semitones).
1927    pub const fn tuning(range_semitones: f64) -> Self {
1928        Self {
1929            minimum: -range_semitones,
1930            maximum: range_semitones,
1931            default_value: 0.0,
1932            step_count: 0,
1933        }
1934    }
1935}
1936
1937/// Maximum length for note expression title strings.
1938pub const MAX_NOTE_EXPRESSION_TITLE_SIZE: usize = 64;
1939
1940/// Information about a Note Expression type.
1941///
1942/// Used to advertise which note expressions the plugin supports.
1943#[derive(Clone, Copy)]
1944pub struct NoteExpressionTypeInfo {
1945    /// Unique identifier for this expression type.
1946    /// Use constants from [`note_expression`] module or custom IDs.
1947    pub type_id: u32,
1948    /// Display title (e.g., "Volume", "Tuning").
1949    pub title: [u8; MAX_NOTE_EXPRESSION_TITLE_SIZE],
1950    /// Title length.
1951    pub title_len: u8,
1952    /// Short title (e.g., "Vol", "Tun").
1953    pub short_title: [u8; MAX_NOTE_EXPRESSION_TITLE_SIZE],
1954    /// Short title length.
1955    pub short_title_len: u8,
1956    /// Unit label (e.g., "dB", "semitones").
1957    pub units: [u8; MAX_NOTE_EXPRESSION_TITLE_SIZE],
1958    /// Units length.
1959    pub units_len: u8,
1960    /// Unit ID for grouping (-1 for none).
1961    pub unit_id: i32,
1962    /// Value range description.
1963    pub value_desc: NoteExpressionValueDesc,
1964    /// Associated parameter ID for automation mapping (-1 for none).
1965    pub associated_parameter_id: i32,
1966    /// Configuration flags.
1967    pub flags: NoteExpressionTypeFlags,
1968}
1969
1970impl NoteExpressionTypeInfo {
1971    /// Create a new Note Expression type info.
1972    pub fn new(type_id: u32, title: &str, short_title: &str) -> Self {
1973        let mut info = Self {
1974            type_id,
1975            title: [0u8; MAX_NOTE_EXPRESSION_TITLE_SIZE],
1976            title_len: 0,
1977            short_title: [0u8; MAX_NOTE_EXPRESSION_TITLE_SIZE],
1978            short_title_len: 0,
1979            units: [0u8; MAX_NOTE_EXPRESSION_TITLE_SIZE],
1980            units_len: 0,
1981            unit_id: -1,
1982            value_desc: NoteExpressionValueDesc::unipolar(),
1983            associated_parameter_id: -1,
1984            flags: NoteExpressionTypeFlags::NONE,
1985        };
1986        info.set_title(title);
1987        info.set_short_title(short_title);
1988        info
1989    }
1990
1991    /// Set the title.
1992    pub fn set_title(&mut self, title: &str) {
1993        let bytes = title.as_bytes();
1994        let len = bytes.len().min(MAX_NOTE_EXPRESSION_TITLE_SIZE);
1995        self.title[..len].copy_from_slice(&bytes[..len]);
1996        self.title_len = len as u8;
1997    }
1998
1999    /// Set the short title.
2000    pub fn set_short_title(&mut self, short_title: &str) {
2001        let bytes = short_title.as_bytes();
2002        let len = bytes.len().min(MAX_NOTE_EXPRESSION_TITLE_SIZE);
2003        self.short_title[..len].copy_from_slice(&bytes[..len]);
2004        self.short_title_len = len as u8;
2005    }
2006
2007    /// Set the units label.
2008    pub fn set_units(&mut self, units: &str) {
2009        let bytes = units.as_bytes();
2010        let len = bytes.len().min(MAX_NOTE_EXPRESSION_TITLE_SIZE);
2011        self.units[..len].copy_from_slice(&bytes[..len]);
2012        self.units_len = len as u8;
2013    }
2014
2015    /// Get the title as a string slice.
2016    pub fn title_str(&self) -> &str {
2017        core::str::from_utf8(&self.title[..self.title_len as usize]).unwrap_or("")
2018    }
2019
2020    /// Get the short title as a string slice.
2021    pub fn short_title_str(&self) -> &str {
2022        core::str::from_utf8(&self.short_title[..self.short_title_len as usize]).unwrap_or("")
2023    }
2024
2025    /// Get the units as a string slice.
2026    pub fn units_str(&self) -> &str {
2027        core::str::from_utf8(&self.units[..self.units_len as usize]).unwrap_or("")
2028    }
2029
2030    /// Builder: set value description.
2031    pub fn with_value_desc(mut self, desc: NoteExpressionValueDesc) -> Self {
2032        self.value_desc = desc;
2033        self
2034    }
2035
2036    /// Builder: set flags.
2037    pub fn with_flags(mut self, flags: NoteExpressionTypeFlags) -> Self {
2038        self.flags = flags;
2039        self
2040    }
2041
2042    /// Builder: set units.
2043    pub fn with_units(mut self, units: &str) -> Self {
2044        self.set_units(units);
2045        self
2046    }
2047
2048    /// Builder: set associated parameter.
2049    pub fn with_associated_parameter(mut self, parameter_id: i32) -> Self {
2050        self.associated_parameter_id = parameter_id;
2051        self.flags = self.flags.or(NoteExpressionTypeFlags::ASSOCIATED_PARAMETER_ID_VALID);
2052        self
2053    }
2054}
2055
2056impl core::fmt::Debug for NoteExpressionTypeInfo {
2057    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
2058        f.debug_struct("NoteExpressionTypeInfo")
2059            .field("type_id", &self.type_id)
2060            .field("title", &self.title_str())
2061            .field("short_title", &self.short_title_str())
2062            .field("units", &self.units_str())
2063            .field("value_desc", &self.value_desc)
2064            .field("flags", &self.flags)
2065            .finish()
2066    }
2067}
2068
2069impl Default for NoteExpressionTypeInfo {
2070    fn default() -> Self {
2071        Self::new(note_expression::INVALID, "", "")
2072    }
2073}
2074
2075// =============================================================================
2076// Keyswitch Controller Types (VST3 SDK 3.5.0)
2077// =============================================================================
2078
2079/// Keyswitch type identifiers.
2080pub mod keyswitch_type {
2081    /// Keyswitch triggered by a single note on/off.
2082    pub const NOTE_ON_KEY: u32 = 0;
2083    /// Keyswitch that must be held (pressed while playing).
2084    pub const ON_THE_FLY: u32 = 1;
2085    /// Keyswitch that toggles on/off with repeated presses.
2086    pub const ON_RELEASE: u32 = 2;
2087    /// Keyswitch triggered by a range of keys.
2088    pub const KEY_RANGE: u32 = 3;
2089}
2090
2091/// Maximum length for keyswitch title strings.
2092pub const MAX_KEYSWITCH_TITLE_SIZE: usize = 64;
2093
2094/// Information about a keyswitch (articulation).
2095///
2096/// Used by sample libraries and orchestral instruments to describe
2097/// available articulation switches.
2098#[derive(Clone, Copy)]
2099pub struct KeyswitchInfo {
2100    /// Keyswitch type (see [`keyswitch_type`] module).
2101    pub type_id: u32,
2102    /// Display title (e.g., "Staccato", "Legato").
2103    pub title: [u8; MAX_KEYSWITCH_TITLE_SIZE],
2104    /// Title length.
2105    pub title_len: u8,
2106    /// Short title (e.g., "Stac", "Leg").
2107    pub short_title: [u8; MAX_KEYSWITCH_TITLE_SIZE],
2108    /// Short title length.
2109    pub short_title_len: u8,
2110    /// Minimum key in the keyswitch range (MIDI note 0-127).
2111    pub keyswitch_min: i32,
2112    /// Maximum key in the keyswitch range (MIDI note 0-127).
2113    pub keyswitch_max: i32,
2114    /// Remapped key (-1 if not remapped).
2115    pub key_remapped: i32,
2116    /// Unit ID for grouping (-1 for none).
2117    pub unit_id: i32,
2118    /// Flags (reserved for future use).
2119    pub flags: i32,
2120}
2121
2122impl KeyswitchInfo {
2123    /// Create a new keyswitch info for a single key.
2124    pub fn new(type_id: u32, title: &str, key: i32) -> Self {
2125        let mut info = Self {
2126            type_id,
2127            title: [0u8; MAX_KEYSWITCH_TITLE_SIZE],
2128            title_len: 0,
2129            short_title: [0u8; MAX_KEYSWITCH_TITLE_SIZE],
2130            short_title_len: 0,
2131            keyswitch_min: key,
2132            keyswitch_max: key,
2133            key_remapped: -1,
2134            unit_id: -1,
2135            flags: 0,
2136        };
2137        info.set_title(title);
2138        info
2139    }
2140
2141    /// Create a keyswitch for a range of keys.
2142    pub fn key_range(type_id: u32, title: &str, min_key: i32, max_key: i32) -> Self {
2143        let mut info = Self::new(type_id, title, min_key);
2144        info.keyswitch_max = max_key;
2145        info
2146    }
2147
2148    /// Set the title.
2149    pub fn set_title(&mut self, title: &str) {
2150        let bytes = title.as_bytes();
2151        let len = bytes.len().min(MAX_KEYSWITCH_TITLE_SIZE);
2152        self.title[..len].copy_from_slice(&bytes[..len]);
2153        self.title_len = len as u8;
2154    }
2155
2156    /// Set the short title.
2157    pub fn set_short_title(&mut self, short_title: &str) {
2158        let bytes = short_title.as_bytes();
2159        let len = bytes.len().min(MAX_KEYSWITCH_TITLE_SIZE);
2160        self.short_title[..len].copy_from_slice(&bytes[..len]);
2161        self.short_title_len = len as u8;
2162    }
2163
2164    /// Get the title as a string slice.
2165    pub fn title_str(&self) -> &str {
2166        core::str::from_utf8(&self.title[..self.title_len as usize]).unwrap_or("")
2167    }
2168
2169    /// Get the short title as a string slice.
2170    pub fn short_title_str(&self) -> &str {
2171        core::str::from_utf8(&self.short_title[..self.short_title_len as usize]).unwrap_or("")
2172    }
2173
2174    /// Builder: set short title.
2175    pub fn with_short_title(mut self, short_title: &str) -> Self {
2176        self.set_short_title(short_title);
2177        self
2178    }
2179
2180    /// Builder: set remapped key.
2181    pub fn with_key_remapped(mut self, key: i32) -> Self {
2182        self.key_remapped = key;
2183        self
2184    }
2185}
2186
2187impl core::fmt::Debug for KeyswitchInfo {
2188    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
2189        f.debug_struct("KeyswitchInfo")
2190            .field("type_id", &self.type_id)
2191            .field("title", &self.title_str())
2192            .field("keyswitch_min", &self.keyswitch_min)
2193            .field("keyswitch_max", &self.keyswitch_max)
2194            .finish()
2195    }
2196}
2197
2198impl Default for KeyswitchInfo {
2199    fn default() -> Self {
2200        Self::new(keyswitch_type::NOTE_ON_KEY, "", 0)
2201    }
2202}
2203
2204// =============================================================================
2205// Physical UI Mapping Types (VST3 SDK 3.6.11)
2206// =============================================================================
2207
2208/// Physical UI type identifiers for MPE and physical controllers.
2209pub mod physical_ui {
2210    /// X-axis movement (horizontal slide on MPE controllers).
2211    pub const X_MOVEMENT: u32 = 0;
2212    /// Y-axis movement (vertical slide / "Slide" on MPE controllers).
2213    pub const Y_MOVEMENT: u32 = 1;
2214    /// Pressure (aftertouch on MPE controllers).
2215    pub const PRESSURE: u32 = 2;
2216    /// Type face (for instruments with multiple playing styles).
2217    pub const TYPE_FACE: u32 = 3;
2218    /// Reserved value for unassigned/unknown.
2219    pub const INVALID: u32 = u32::MAX;
2220}
2221
2222/// Maps a physical UI input to a Note Expression output.
2223///
2224/// Used to define how MPE controllers map to note expression types.
2225#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2226pub struct PhysicalUIMap {
2227    /// Physical UI type (see [`physical_ui`] module).
2228    pub physical_ui_type_id: u32,
2229    /// Note expression type to map to (see [`note_expression`] module).
2230    pub note_expression_type_id: u32,
2231}
2232
2233impl PhysicalUIMap {
2234    /// Create a new physical UI mapping.
2235    pub const fn new(physical_ui_type_id: u32, note_expression_type_id: u32) -> Self {
2236        Self {
2237            physical_ui_type_id,
2238            note_expression_type_id,
2239        }
2240    }
2241
2242    /// Map X-axis to a note expression.
2243    pub const fn x_axis(note_expression_type_id: u32) -> Self {
2244        Self::new(physical_ui::X_MOVEMENT, note_expression_type_id)
2245    }
2246
2247    /// Map Y-axis (Slide) to a note expression.
2248    pub const fn y_axis(note_expression_type_id: u32) -> Self {
2249        Self::new(physical_ui::Y_MOVEMENT, note_expression_type_id)
2250    }
2251
2252    /// Map Pressure to a note expression.
2253    pub const fn pressure(note_expression_type_id: u32) -> Self {
2254        Self::new(physical_ui::PRESSURE, note_expression_type_id)
2255    }
2256}
2257
2258// =============================================================================
2259// MPE Support Types (VST3 SDK 3.6.12)
2260// =============================================================================
2261
2262/// MPE (MIDI Polyphonic Expression) input device settings.
2263///
2264/// Defines the MPE zone configuration for incoming MIDI.
2265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2266pub struct MpeInputDeviceSettings {
2267    /// Master channel (0 = channel 1, typically 0 for lower zone).
2268    pub master_channel: i32,
2269    /// First member channel (typically 1 for lower zone).
2270    pub member_begin_channel: i32,
2271    /// Last member channel (typically 14 for lower zone).
2272    pub member_end_channel: i32,
2273}
2274
2275impl Default for MpeInputDeviceSettings {
2276    fn default() -> Self {
2277        Self {
2278            master_channel: 0,
2279            member_begin_channel: 1,
2280            member_end_channel: 14,
2281        }
2282    }
2283}
2284
2285impl MpeInputDeviceSettings {
2286    /// Create MPE settings for the lower zone (default configuration).
2287    pub const fn lower_zone() -> Self {
2288        Self {
2289            master_channel: 0,
2290            member_begin_channel: 1,
2291            member_end_channel: 14,
2292        }
2293    }
2294
2295    /// Create MPE settings for the upper zone.
2296    pub const fn upper_zone() -> Self {
2297        Self {
2298            master_channel: 15,
2299            member_begin_channel: 14,
2300            member_end_channel: 1,
2301        }
2302    }
2303
2304    /// Create custom MPE settings.
2305    pub const fn new(master: i32, begin: i32, end: i32) -> Self {
2306        Self {
2307            master_channel: master,
2308            member_begin_channel: begin,
2309            member_end_channel: end,
2310        }
2311    }
2312}