beamer_core/
midi_params.rs

1//! MIDI CC parameter emulation for VST3 compatibility.
2//!
3//! VST3 doesn't send MIDI CC, pitch bend, or aftertouch directly to plugins.
4//! Instead, most DAWs convert these to parameter changes via the `IMidiMapping`
5//! interface. This module provides `MidiCcParams` which creates hidden parameters
6//! for MIDI controllers and converts parameter changes back to MIDI events.
7//!
8//! # Usage
9//!
10//! ```ignore
11//! use beamer_core::{Plugin, MidiCcParams};
12//!
13//! struct MyPlugin {
14//!     params: MyParams,
15//!     midi_cc_params: MidiCcParams,
16//! }
17//!
18//! impl Plugin for MyPlugin {
19//!     fn midi_cc_params(&self) -> Option<&MidiCcParams> {
20//!         Some(&self.midi_cc_params)
21//!     }
22//!
23//!     fn create() -> Self {
24//!         Self {
25//!             params: MyParams::default(),
26//!             midi_cc_params: MidiCcParams::new()
27//!                 .with_pitch_bend()
28//!                 .with_mod_wheel()
29//!                 .with_ccs(&[7, 10, 11, 64]),  // Volume, Pan, Expression, Sustain
30//!         }
31//!     }
32//! }
33//! ```
34//!
35//! # How It Works
36//!
37//! 1. Plugin configures `MidiCcParams` with desired controllers
38//! 2. VST3 wrapper exposes hidden parameters for each enabled controller
39//! 3. DAW queries `IMidiMapping` and gets parameter IDs for MIDI controllers
40//! 4. When DAW sends parameter changes, wrapper converts them to `MidiEvent`
41//! 5. Plugin receives MIDI events normally via `process_midi()`
42
43use std::sync::atomic::{AtomicU64, Ordering};
44
45use crate::params::{ParamFlags, ParamInfo, Parameters, Units, ROOT_UNIT_ID};
46use crate::types::{ParamId, ParamValue};
47
48// =============================================================================
49// Constants
50// =============================================================================
51
52/// Base parameter ID for MIDI CC emulation parameters.
53///
54/// Uses a high value to avoid collision with user-defined parameters.
55/// The controller number (0-129) is added to get the final param ID.
56///
57/// - CC 0-127: Standard MIDI CCs
58/// - CC 128: Channel aftertouch (kAfterTouch)
59/// - CC 129: Pitch bend (kPitchBend)
60pub const MIDI_CC_PARAM_BASE: u32 = 0x10000000; // 268435456
61
62/// Maximum supported controller number (pitch bend = 129)
63const MAX_CONTROLLER: usize = 130;
64
65/// Extended MIDI controller numbers (from VST3 SDK ivstmidicontrollers.h)
66pub mod controller {
67    /// Channel pressure / aftertouch
68    pub const AFTERTOUCH: u8 = 128;
69    /// Pitch bend wheel
70    pub const PITCH_BEND: u8 = 129;
71}
72
73// =============================================================================
74// MidiCcParams
75// =============================================================================
76
77/// Hidden parameters for MIDI CC emulation.
78///
79/// Create with the builder pattern to specify which controllers to emulate.
80/// The VST3 wrapper will expose hidden parameters and convert parameter
81/// changes back to MIDI events.
82///
83/// # Example
84///
85/// ```ignore
86/// let cc_params = MidiCcParams::new()
87///     .with_pitch_bend()
88///     .with_aftertouch()
89///     .with_mod_wheel()
90///     .with_ccs(&[7, 10, 11, 64]);  // Volume, Pan, Expression, Sustain
91/// ```
92pub struct MidiCcParams {
93    /// Enabled controller flags (bitset for 0-127, plus special flags)
94    enabled: [bool; MAX_CONTROLLER],
95    /// Current values (normalized 0.0-1.0, stored as f64 bits)
96    values: [AtomicU64; MAX_CONTROLLER],
97    /// Pre-computed parameter info for enabled controllers
98    param_infos: Vec<CcParamInfo>,
99    /// Total enabled controller count
100    enabled_count: usize,
101}
102
103/// Internal storage for parameter info
104struct CcParamInfo {
105    controller: u8,
106    info: ParamInfo,
107}
108
109impl MidiCcParams {
110    /// Create a new `MidiCcParams` with no controllers enabled.
111    ///
112    /// Use builder methods to enable specific controllers.
113    pub fn new() -> Self {
114        // Initialize all atomic values to 0.0 (or 0.5 for pitch bend center)
115        let values = std::array::from_fn(|_| AtomicU64::new(0));
116
117        Self {
118            enabled: [false; MAX_CONTROLLER],
119            values,
120            param_infos: Vec::new(),
121            enabled_count: 0,
122        }
123    }
124
125    // =========================================================================
126    // Builder Methods
127    // =========================================================================
128
129    /// Enable pitch bend emulation (controller 129).
130    ///
131    /// Pitch bend uses normalized 0.0-1.0 where 0.5 is center.
132    /// The framework converts this to -1.0 to 1.0 for `MidiEvent::pitch_bend`.
133    pub fn with_pitch_bend(mut self) -> Self {
134        self.enable_controller(controller::PITCH_BEND);
135        // Set default to 0.5 (center position)
136        self.values[controller::PITCH_BEND as usize]
137            .store(0.5f64.to_bits(), Ordering::Relaxed);
138        self
139    }
140
141    /// Enable channel aftertouch emulation (controller 128).
142    pub fn with_aftertouch(mut self) -> Self {
143        self.enable_controller(controller::AFTERTOUCH);
144        self
145    }
146
147    /// Enable mod wheel emulation (CC 1).
148    pub fn with_mod_wheel(mut self) -> Self {
149        self.enable_controller(1);
150        self
151    }
152
153    /// Enable a specific MIDI CC (0-127).
154    pub fn with_cc(mut self, cc: u8) -> Self {
155        if cc < 128 {
156            self.enable_controller(cc);
157        }
158        self
159    }
160
161    /// Enable multiple MIDI CCs.
162    pub fn with_ccs(mut self, ccs: &[u8]) -> Self {
163        for &cc in ccs {
164            if cc < 128 {
165                self.enable_controller(cc);
166            }
167        }
168        self
169    }
170
171    /// Enable all standard MIDI CCs (0-127).
172    ///
173    /// **Warning**: This creates 128 hidden parameters. Use sparingly.
174    pub fn with_all_ccs(mut self) -> Self {
175        for cc in 0..128 {
176            self.enable_controller(cc);
177        }
178        self
179    }
180
181    // =========================================================================
182    // Query Methods
183    // =========================================================================
184
185    /// Check if a controller is enabled.
186    pub fn has_controller(&self, controller: u8) -> bool {
187        if (controller as usize) < MAX_CONTROLLER {
188            self.enabled[controller as usize]
189        } else {
190            false
191        }
192    }
193
194    /// Check if pitch bend is enabled.
195    pub fn has_pitch_bend(&self) -> bool {
196        self.has_controller(controller::PITCH_BEND)
197    }
198
199    /// Check if aftertouch is enabled.
200    pub fn has_aftertouch(&self) -> bool {
201        self.has_controller(controller::AFTERTOUCH)
202    }
203
204    /// Get the current pitch bend value (-1.0 to 1.0).
205    ///
206    /// Returns 0.0 if pitch bend is not enabled.
207    pub fn pitch_bend(&self) -> f32 {
208        if self.has_pitch_bend() {
209            let normalized = self.get_normalized_internal(controller::PITCH_BEND);
210            (normalized * 2.0 - 1.0) as f32
211        } else {
212            0.0
213        }
214    }
215
216    /// Get the current aftertouch value (0.0 to 1.0).
217    ///
218    /// Returns 0.0 if aftertouch is not enabled.
219    pub fn aftertouch(&self) -> f32 {
220        if self.has_aftertouch() {
221            self.get_normalized_internal(controller::AFTERTOUCH) as f32
222        } else {
223            0.0
224        }
225    }
226
227    /// Get the current value of a MIDI CC (0.0 to 1.0).
228    ///
229    /// Returns 0.0 if the CC is not enabled.
230    pub fn cc(&self, cc: u8) -> f32 {
231        if cc < 128 && self.has_controller(cc) {
232            self.get_normalized_internal(cc) as f32
233        } else {
234            0.0
235        }
236    }
237
238    /// Get the mod wheel value (CC 1, 0.0 to 1.0).
239    pub fn mod_wheel(&self) -> f32 {
240        self.cc(1)
241    }
242
243    /// Get the number of enabled controllers.
244    pub fn enabled_count(&self) -> usize {
245        self.enabled_count
246    }
247
248    /// Iterate over enabled controller numbers.
249    pub fn enabled_controllers(&self) -> impl Iterator<Item = u8> + '_ {
250        self.param_infos.iter().map(|info| info.controller)
251    }
252
253    // =========================================================================
254    // Parameter ID Helpers
255    // =========================================================================
256
257    /// Get parameter ID for a controller.
258    #[inline]
259    pub const fn param_id(controller: u8) -> u32 {
260        MIDI_CC_PARAM_BASE + controller as u32
261    }
262
263    /// Check if a parameter ID belongs to MIDI CC emulation.
264    #[inline]
265    pub const fn is_midi_cc_param(param_id: u32) -> bool {
266        param_id >= MIDI_CC_PARAM_BASE && param_id < MIDI_CC_PARAM_BASE + MAX_CONTROLLER as u32
267    }
268
269    /// Extract controller number from a MIDI CC parameter ID.
270    ///
271    /// Returns `None` if the param_id is not a MIDI CC parameter.
272    #[inline]
273    pub const fn param_id_to_controller(param_id: u32) -> Option<u8> {
274        if Self::is_midi_cc_param(param_id) {
275            Some((param_id - MIDI_CC_PARAM_BASE) as u8)
276        } else {
277            None
278        }
279    }
280
281    // =========================================================================
282    // Internal Methods
283    // =========================================================================
284
285    fn enable_controller(&mut self, controller: u8) {
286        let idx = controller as usize;
287        if idx < MAX_CONTROLLER && !self.enabled[idx] {
288            self.enabled[idx] = true;
289            self.enabled_count += 1;
290
291            // Create parameter info
292            let info = self.create_param_info(controller);
293            self.param_infos.push(CcParamInfo { controller, info });
294        }
295    }
296
297    fn create_param_info(&self, controller: u8) -> ParamInfo {
298        let id = Self::param_id(controller);
299
300        // Determine name based on controller
301        let (name, short_name): (&'static str, &'static str) = match controller {
302            controller::PITCH_BEND => ("Pitch Bend", "PB"),
303            controller::AFTERTOUCH => ("Aftertouch", "AT"),
304            1 => ("Mod Wheel", "MW"),
305            2 => ("Breath Controller", "BC"),
306            7 => ("Volume", "Vol"),
307            10 => ("Pan", "Pan"),
308            11 => ("Expression", "Exp"),
309            64 => ("Sustain Pedal", "Sus"),
310            _ => ("MIDI CC", "CC"),
311        };
312
313        let default = if controller == controller::PITCH_BEND { 0.5 } else { 0.0 };
314
315        ParamInfo {
316            id,
317            name,
318            short_name,
319            units: "",
320            default_normalized: default,
321            step_count: 0,
322            flags: ParamFlags {
323                can_automate: true,
324                is_readonly: false,
325                is_bypass: false,
326                is_list: false,
327                is_hidden: true,  // Hidden from DAW parameter list
328            },
329            unit_id: ROOT_UNIT_ID,
330        }
331    }
332
333    fn get_normalized_internal(&self, controller: u8) -> f64 {
334        let idx = controller as usize;
335        if idx < MAX_CONTROLLER {
336            f64::from_bits(self.values[idx].load(Ordering::Relaxed))
337        } else {
338            0.0
339        }
340    }
341
342    fn set_normalized_internal(&self, controller: u8, value: f64) {
343        let idx = controller as usize;
344        if idx < MAX_CONTROLLER {
345            self.values[idx].store(value.clamp(0.0, 1.0).to_bits(), Ordering::Relaxed);
346        }
347    }
348}
349
350impl Default for MidiCcParams {
351    fn default() -> Self {
352        Self::new()
353    }
354}
355
356// SAFETY: AtomicU64 is Send + Sync, and all other fields are either primitive or Vec
357unsafe impl Send for MidiCcParams {}
358unsafe impl Sync for MidiCcParams {}
359
360// =============================================================================
361// Parameters Trait Implementation (for VST3 integration)
362// =============================================================================
363
364impl Parameters for MidiCcParams {
365    fn count(&self) -> usize {
366        self.enabled_count
367    }
368
369    fn info(&self, index: usize) -> Option<&ParamInfo> {
370        self.param_infos.get(index).map(|i| &i.info)
371    }
372
373    fn get_normalized(&self, id: ParamId) -> ParamValue {
374        if let Some(controller) = Self::param_id_to_controller(id) {
375            self.get_normalized_internal(controller)
376        } else {
377            0.0
378        }
379    }
380
381    fn set_normalized(&self, id: ParamId, value: ParamValue) {
382        if let Some(controller) = Self::param_id_to_controller(id) {
383            self.set_normalized_internal(controller, value);
384        }
385    }
386
387    fn normalized_to_string(&self, id: ParamId, normalized: ParamValue) -> String {
388        if let Some(controller) = Self::param_id_to_controller(id) {
389            if controller == controller::PITCH_BEND {
390                let bend = (normalized * 2.0 - 1.0) * 100.0;
391                return format!("{:+.0}%", bend);
392            }
393        }
394        format!("{:.0}", normalized * 127.0)
395    }
396
397    fn string_to_normalized(&self, _id: ParamId, string: &str) -> Option<ParamValue> {
398        // Try parsing as 0-127
399        if let Ok(v) = string.parse::<f64>() {
400            return Some((v / 127.0).clamp(0.0, 1.0));
401        }
402        // Try parsing as percentage
403        if let Some(v) = string.strip_suffix('%') {
404            if let Ok(v) = v.trim().parse::<f64>() {
405                return Some((v / 100.0).clamp(0.0, 1.0));
406            }
407        }
408        None
409    }
410
411    fn normalized_to_plain(&self, _id: ParamId, normalized: ParamValue) -> ParamValue {
412        normalized * 127.0
413    }
414
415    fn plain_to_normalized(&self, _id: ParamId, plain: ParamValue) -> ParamValue {
416        (plain / 127.0).clamp(0.0, 1.0)
417    }
418}
419
420// =============================================================================
421// Units Trait Implementation (no parameter grouping for hidden params)
422// =============================================================================
423
424impl Units for MidiCcParams {
425    fn unit_count(&self) -> usize {
426        1 // Only root unit
427    }
428
429    fn unit_info(&self, index: usize) -> Option<crate::params::UnitInfo> {
430        if index == 0 {
431            Some(crate::params::UnitInfo::root())
432        } else {
433            None
434        }
435    }
436}
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441
442    #[test]
443    fn test_builder() {
444        let params = MidiCcParams::new()
445            .with_pitch_bend()
446            .with_mod_wheel()
447            .with_ccs(&[7, 64]);
448
449        assert!(params.has_pitch_bend());
450        assert!(!params.has_aftertouch());
451        assert!(params.has_controller(1)); // mod wheel
452        assert!(params.has_controller(7)); // volume
453        assert!(params.has_controller(64)); // sustain
454        assert!(!params.has_controller(10)); // pan not enabled
455        assert_eq!(params.enabled_count(), 4);
456    }
457
458    #[test]
459    fn test_param_id() {
460        assert_eq!(MidiCcParams::param_id(1), MIDI_CC_PARAM_BASE + 1);
461        assert_eq!(MidiCcParams::param_id(129), MIDI_CC_PARAM_BASE + 129);
462
463        assert!(MidiCcParams::is_midi_cc_param(MIDI_CC_PARAM_BASE));
464        assert!(MidiCcParams::is_midi_cc_param(MIDI_CC_PARAM_BASE + 129));
465        assert!(!MidiCcParams::is_midi_cc_param(0));
466        assert!(!MidiCcParams::is_midi_cc_param(MIDI_CC_PARAM_BASE + 200));
467
468        assert_eq!(MidiCcParams::param_id_to_controller(MIDI_CC_PARAM_BASE + 1), Some(1));
469        assert_eq!(MidiCcParams::param_id_to_controller(100), None);
470    }
471
472    #[test]
473    fn test_values() {
474        let params = MidiCcParams::new()
475            .with_pitch_bend()
476            .with_mod_wheel();
477
478        // Pitch bend default: 0.5 normalized (center), which maps to 0.0 in bipolar range
479        assert!((params.pitch_bend() - 0.0).abs() < 0.01);
480
481        // Test setting values via Parameters trait
482        let pb_id = MidiCcParams::param_id(controller::PITCH_BEND);
483        Parameters::set_normalized(&params, pb_id, 1.0);
484        assert!((params.pitch_bend() - 1.0).abs() < 0.01);
485
486        Parameters::set_normalized(&params, pb_id, 0.0);
487        assert!((params.pitch_bend() - (-1.0)).abs() < 0.01);
488    }
489}