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 /// Audio Units, CLAP, LV2, 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}