beamer_core/
midi_cc_state.rs

1//! Runtime state for MIDI CC emulation.
2//!
3//! This module provides [`MidiCcState`], which holds the current values of
4//! MIDI controllers. Unlike `MidiCcConfig` (pure configuration), `MidiCcState`
5//! contains runtime state with atomic values for thread-safe access.
6//!
7//! **This type is framework-internal.** Plugin authors don't need to create
8//! or manage `MidiCcState` - the VST3 wrapper handles it automatically.
9//! Plugins can read current CC values via [`ProcessContext::midi_cc()`].
10
11use std::sync::atomic::{AtomicU64, Ordering};
12
13use crate::midi_cc_config::{controller, MidiCcConfig, MAX_CC_CONTROLLER};
14use crate::params::{ParamFlags, ParamInfo, Parameters, UnitInfo, Units, ROOT_UNIT_ID};
15use crate::types::{ParamId, ParamValue};
16
17// =============================================================================
18// Constants
19// =============================================================================
20
21/// Base parameter ID for MIDI CC emulation parameters.
22///
23/// Uses a high value to avoid collision with user-defined parameters.
24/// The controller number (0-129) is added to get the final param ID.
25///
26/// - CC 0-127: Standard MIDI CCs
27/// - CC 128: Channel aftertouch (kAfterTouch)
28/// - CC 129: Pitch bend (kPitchBend)
29pub const MIDI_CC_PARAM_BASE: u32 = 0x10000000; // 268435456
30
31// =============================================================================
32// MidiCcState
33// =============================================================================
34
35/// Runtime state for MIDI CC emulation.
36///
37/// Created by the framework from a [`MidiCcConfig`]. Holds current controller
38/// values as atomic floats for thread-safe access from both host and audio threads.
39///
40/// Plugin authors can read values via [`ProcessContext::midi_cc()`]:
41///
42/// ```ignore
43/// fn process(&mut self, buffer: &mut Buffer, aux: &mut AuxiliaryBuffers, context: &ProcessContext) {
44///     if let Some(cc) = context.midi_cc() {
45///         let pitch_bend = cc.pitch_bend();  // -1.0 to 1.0
46///         let mod_wheel = cc.mod_wheel();    // 0.0 to 1.0
47///         let volume = cc.cc(7);             // 0.0 to 1.0
48///     }
49/// }
50/// ```
51pub struct MidiCcState {
52    /// Enabled controller flags (copied from config)
53    enabled: [bool; MAX_CC_CONTROLLER],
54    /// Current values (normalized 0.0-1.0, stored as f64 bits)
55    values: [AtomicU64; MAX_CC_CONTROLLER],
56    /// Pre-computed parameter info for enabled controllers
57    param_infos: Vec<CcParamInfo>,
58    /// Total enabled controller count
59    enabled_count: usize,
60}
61
62/// Internal storage for parameter info
63struct CcParamInfo {
64    controller: u8,
65    info: ParamInfo,
66}
67
68impl MidiCcState {
69    /// Create state from configuration.
70    ///
71    /// This is called by the framework when initializing the VST3 wrapper.
72    pub fn from_config(config: &MidiCcConfig) -> Self {
73        // Initialize all atomic values to 0.0 (or 0.5 for pitch bend center)
74        let values = std::array::from_fn(|i| {
75            let default: f64 = if i == controller::PITCH_BEND as usize {
76                0.5 // Pitch bend center
77            } else {
78                0.0
79            };
80            AtomicU64::new(default.to_bits())
81        });
82
83        // Copy enabled flags and build param infos
84        let enabled = *config.enabled_flags();
85        let mut param_infos = Vec::new();
86        let mut enabled_count = 0;
87
88        for (i, &is_enabled) in enabled.iter().enumerate() {
89            if is_enabled {
90                enabled_count += 1;
91                let controller = i as u8;
92                let info = Self::create_param_info(controller);
93                param_infos.push(CcParamInfo { controller, info });
94            }
95        }
96
97        Self {
98            enabled,
99            values,
100            param_infos,
101            enabled_count,
102        }
103    }
104
105    // =========================================================================
106    // Value Access (for plugins via ProcessContext)
107    // =========================================================================
108
109    /// Get the current pitch bend value (-1.0 to 1.0).
110    ///
111    /// Returns 0.0 if pitch bend is not enabled.
112    #[inline]
113    pub fn pitch_bend(&self) -> f32 {
114        if self.enabled[controller::PITCH_BEND as usize] {
115            let normalized = self.get_normalized_internal(controller::PITCH_BEND);
116            (normalized * 2.0 - 1.0) as f32
117        } else {
118            0.0
119        }
120    }
121
122    /// Get the current aftertouch value (0.0 to 1.0).
123    ///
124    /// Returns 0.0 if aftertouch is not enabled.
125    #[inline]
126    pub fn aftertouch(&self) -> f32 {
127        if self.enabled[controller::AFTERTOUCH as usize] {
128            self.get_normalized_internal(controller::AFTERTOUCH) as f32
129        } else {
130            0.0
131        }
132    }
133
134    /// Get the current mod wheel value (CC 1, 0.0 to 1.0).
135    ///
136    /// Returns 0.0 if mod wheel is not enabled.
137    #[inline]
138    pub fn mod_wheel(&self) -> f32 {
139        self.cc(1)
140    }
141
142    /// Get the current value of a MIDI CC (0.0 to 1.0).
143    ///
144    /// Returns 0.0 if the CC is not enabled.
145    #[inline]
146    pub fn cc(&self, cc: u8) -> f32 {
147        if (cc as usize) < MAX_CC_CONTROLLER && self.enabled[cc as usize] {
148            self.get_normalized_internal(cc) as f32
149        } else {
150            0.0
151        }
152    }
153
154    // =========================================================================
155    // Query Methods
156    // =========================================================================
157
158    /// Check if a controller is enabled.
159    #[inline]
160    pub fn has_controller(&self, controller: u8) -> bool {
161        if (controller as usize) < MAX_CC_CONTROLLER {
162            self.enabled[controller as usize]
163        } else {
164            false
165        }
166    }
167
168    /// Check if pitch bend is enabled.
169    #[inline]
170    pub fn has_pitch_bend(&self) -> bool {
171        self.enabled[controller::PITCH_BEND as usize]
172    }
173
174    /// Check if aftertouch is enabled.
175    #[inline]
176    pub fn has_aftertouch(&self) -> bool {
177        self.enabled[controller::AFTERTOUCH as usize]
178    }
179
180    /// Get the number of enabled controllers.
181    #[inline]
182    pub fn enabled_count(&self) -> usize {
183        self.enabled_count
184    }
185
186    /// Iterate over enabled controller numbers.
187    pub fn enabled_controllers(&self) -> impl Iterator<Item = u8> + '_ {
188        self.param_infos.iter().map(|info| info.controller)
189    }
190
191    // =========================================================================
192    // Parameter ID Helpers
193    // =========================================================================
194
195    /// Get parameter ID for a controller.
196    #[inline]
197    pub const fn param_id(controller: u8) -> u32 {
198        MIDI_CC_PARAM_BASE + controller as u32
199    }
200
201    /// Check if a parameter ID belongs to MIDI CC emulation.
202    #[inline]
203    pub const fn is_midi_cc_param(param_id: u32) -> bool {
204        param_id >= MIDI_CC_PARAM_BASE && param_id < MIDI_CC_PARAM_BASE + MAX_CC_CONTROLLER as u32
205    }
206
207    /// Extract controller number from a MIDI CC parameter ID.
208    ///
209    /// Returns `None` if the param_id is not a MIDI CC parameter.
210    #[inline]
211    pub const fn param_id_to_controller(param_id: u32) -> Option<u8> {
212        if Self::is_midi_cc_param(param_id) {
213            Some((param_id - MIDI_CC_PARAM_BASE) as u8)
214        } else {
215            None
216        }
217    }
218
219    // =========================================================================
220    // Internal Methods
221    // =========================================================================
222
223    fn create_param_info(controller: u8) -> ParamInfo {
224        let id = Self::param_id(controller);
225
226        // Determine name based on controller
227        let (name, short_name): (&'static str, &'static str) = match controller {
228            controller::PITCH_BEND => ("Pitch Bend", "PB"),
229            controller::AFTERTOUCH => ("Aftertouch", "AT"),
230            1 => ("Mod Wheel", "MW"),
231            2 => ("Breath Controller", "BC"),
232            7 => ("Volume", "Vol"),
233            10 => ("Pan", "Pan"),
234            11 => ("Expression", "Exp"),
235            64 => ("Sustain Pedal", "Sus"),
236            _ => ("MIDI CC", "CC"),
237        };
238
239        let default = if controller == controller::PITCH_BEND {
240            0.5
241        } else {
242            0.0
243        };
244
245        ParamInfo {
246            id,
247            name,
248            short_name,
249            units: "",
250            default_normalized: default,
251            step_count: 0,
252            flags: ParamFlags {
253                can_automate: true,
254                is_readonly: false,
255                is_bypass: false,
256                is_list: false,
257                is_hidden: true, // Hidden from DAW parameter list
258            },
259            unit_id: ROOT_UNIT_ID,
260        }
261    }
262
263    #[inline]
264    fn get_normalized_internal(&self, controller: u8) -> f64 {
265        let idx = controller as usize;
266        if idx < MAX_CC_CONTROLLER {
267            f64::from_bits(self.values[idx].load(Ordering::Relaxed))
268        } else {
269            0.0
270        }
271    }
272
273    fn set_normalized_internal(&self, controller: u8, value: f64) {
274        let idx = controller as usize;
275        if idx < MAX_CC_CONTROLLER {
276            self.values[idx].store(value.clamp(0.0, 1.0).to_bits(), Ordering::Relaxed);
277        }
278    }
279}
280
281// SAFETY: AtomicU64 is Send + Sync, and all other fields are either primitive or Vec
282unsafe impl Send for MidiCcState {}
283unsafe impl Sync for MidiCcState {}
284
285impl core::fmt::Debug for MidiCcState {
286    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
287        let enabled: Vec<u8> = self
288            .enabled
289            .iter()
290            .enumerate()
291            .filter_map(|(i, &e)| if e { Some(i as u8) } else { None })
292            .collect();
293        f.debug_struct("MidiCcState")
294            .field("enabled_controllers", &enabled)
295            .field("enabled_count", &self.enabled_count)
296            .finish()
297    }
298}
299
300// =============================================================================
301// Parameters Trait Implementation (for VST3 integration)
302// =============================================================================
303
304impl Parameters for MidiCcState {
305    fn count(&self) -> usize {
306        self.enabled_count
307    }
308
309    fn info(&self, index: usize) -> Option<&ParamInfo> {
310        self.param_infos.get(index).map(|i| &i.info)
311    }
312
313    fn get_normalized(&self, id: ParamId) -> ParamValue {
314        if let Some(controller) = Self::param_id_to_controller(id) {
315            self.get_normalized_internal(controller)
316        } else {
317            0.0
318        }
319    }
320
321    fn set_normalized(&self, id: ParamId, value: ParamValue) {
322        if let Some(controller) = Self::param_id_to_controller(id) {
323            self.set_normalized_internal(controller, value);
324        }
325    }
326
327    fn normalized_to_string(&self, id: ParamId, normalized: ParamValue) -> String {
328        if let Some(controller) = Self::param_id_to_controller(id) {
329            if controller == controller::PITCH_BEND {
330                // Display pitch bend as bipolar semitones (assuming ±2 semitones default)
331                // Center (0.5 normalized) = 0 st, min (0.0) = -2 st, max (1.0) = +2 st
332                let semitones = (normalized * 2.0 - 1.0) * 2.0;
333                return format!("{:+.1} st", semitones);
334            }
335        }
336        format!("{:.0}", normalized * 127.0)
337    }
338
339    fn string_to_normalized(&self, _id: ParamId, string: &str) -> Option<ParamValue> {
340        // Try parsing as 0-127
341        if let Ok(v) = string.parse::<f64>() {
342            return Some((v / 127.0).clamp(0.0, 1.0));
343        }
344        // Try parsing as percentage
345        if let Some(v) = string.strip_suffix('%') {
346            if let Ok(v) = v.trim().parse::<f64>() {
347                return Some((v / 100.0).clamp(0.0, 1.0));
348            }
349        }
350        None
351    }
352
353    fn normalized_to_plain(&self, _id: ParamId, normalized: ParamValue) -> ParamValue {
354        normalized * 127.0
355    }
356
357    fn plain_to_normalized(&self, _id: ParamId, plain: ParamValue) -> ParamValue {
358        (plain / 127.0).clamp(0.0, 1.0)
359    }
360}
361
362// =============================================================================
363// Units Trait Implementation (no parameter grouping for hidden params)
364// =============================================================================
365
366impl Units for MidiCcState {
367    fn unit_count(&self) -> usize {
368        1 // Only root unit
369    }
370
371    fn unit_info(&self, index: usize) -> Option<UnitInfo> {
372        if index == 0 {
373            Some(UnitInfo::root())
374        } else {
375            None
376        }
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383
384    #[test]
385    fn test_from_config() {
386        let config = MidiCcConfig::new()
387            .with_pitch_bend()
388            .with_mod_wheel()
389            .with_ccs(&[7, 64]);
390
391        let state = MidiCcState::from_config(&config);
392
393        assert!(state.has_pitch_bend());
394        assert!(state.has_controller(1)); // mod wheel
395        assert!(state.has_controller(7)); // volume
396        assert!(state.has_controller(64)); // sustain
397        assert!(!state.has_aftertouch());
398        assert_eq!(state.enabled_count(), 4);
399    }
400
401    #[test]
402    fn test_pitch_bend_default() {
403        let config = MidiCcConfig::new().with_pitch_bend();
404        let state = MidiCcState::from_config(&config);
405
406        // Pitch bend default: 0.5 normalized (center), which maps to 0.0 bipolar
407        assert!((state.pitch_bend() - 0.0).abs() < 0.01);
408    }
409
410    #[test]
411    fn test_set_and_get() {
412        let config = MidiCcConfig::new().with_pitch_bend().with_mod_wheel();
413        let state = MidiCcState::from_config(&config);
414
415        // Set pitch bend to max (1.0 normalized = +1.0 bipolar)
416        let pb_id = MidiCcState::param_id(controller::PITCH_BEND);
417        state.set_normalized(pb_id, 1.0);
418        assert!((state.pitch_bend() - 1.0).abs() < 0.01);
419
420        // Set pitch bend to min (0.0 normalized = -1.0 bipolar)
421        state.set_normalized(pb_id, 0.0);
422        assert!((state.pitch_bend() - (-1.0)).abs() < 0.01);
423
424        // Set mod wheel
425        let mw_id = MidiCcState::param_id(1);
426        state.set_normalized(mw_id, 0.75);
427        assert!((state.mod_wheel() - 0.75).abs() < 0.01);
428    }
429
430    #[test]
431    fn test_param_id_helpers() {
432        assert_eq!(MidiCcState::param_id(1), MIDI_CC_PARAM_BASE + 1);
433        assert_eq!(MidiCcState::param_id(129), MIDI_CC_PARAM_BASE + 129);
434
435        assert!(MidiCcState::is_midi_cc_param(MIDI_CC_PARAM_BASE));
436        assert!(MidiCcState::is_midi_cc_param(MIDI_CC_PARAM_BASE + 129));
437        assert!(!MidiCcState::is_midi_cc_param(0));
438        assert!(!MidiCcState::is_midi_cc_param(MIDI_CC_PARAM_BASE + 200));
439
440        assert_eq!(
441            MidiCcState::param_id_to_controller(MIDI_CC_PARAM_BASE + 1),
442            Some(1)
443        );
444        assert_eq!(MidiCcState::param_id_to_controller(100), None);
445    }
446}