Skip to main content

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