use std::sync::atomic::{AtomicU64, Ordering};
use crate::midi_cc_config::{controller, MidiCcConfig, MAX_CC_CONTROLLER};
use crate::parameter_groups::{GroupInfo, ParameterGroups, ROOT_GROUP_ID};
use crate::parameter_info::{ParameterFlags, ParameterInfo, ParameterUnit};
use crate::parameter_store::ParameterStore;
use crate::types::{ParameterId, ParameterValue};
pub const MIDI_CC_PARAM_BASE: u32 = 0x10000000;
pub struct MidiCcState {
enabled: [bool; MAX_CC_CONTROLLER],
values: [AtomicU64; MAX_CC_CONTROLLER],
parameter_infos: Vec<CcParameterInfo>,
enabled_count: usize,
}
struct CcParameterInfo {
controller: u8,
info: ParameterInfo,
}
impl MidiCcState {
pub fn from_config(config: &MidiCcConfig) -> Self {
let values = std::array::from_fn(|i| {
let default: f64 = if i == controller::PITCH_BEND as usize {
0.5 } else {
0.0
};
AtomicU64::new(default.to_bits())
});
let enabled = *config.enabled_flags();
let mut parameter_infos = Vec::new();
let mut enabled_count = 0;
for (i, &is_enabled) in enabled.iter().enumerate() {
if is_enabled {
enabled_count += 1;
let controller = i as u8;
let info = Self::create_parameter_info(controller);
parameter_infos.push(CcParameterInfo { controller, info });
}
}
Self {
enabled,
values,
parameter_infos,
enabled_count,
}
}
#[inline]
pub fn pitch_bend(&self) -> f32 {
if self.enabled[controller::PITCH_BEND as usize] {
let normalized = self.get_normalized_internal(controller::PITCH_BEND);
(normalized * 2.0 - 1.0) as f32
} else {
0.0
}
}
#[inline]
pub fn aftertouch(&self) -> f32 {
if self.enabled[controller::AFTERTOUCH as usize] {
self.get_normalized_internal(controller::AFTERTOUCH) as f32
} else {
0.0
}
}
#[inline]
pub fn mod_wheel(&self) -> f32 {
self.cc(1)
}
#[inline]
pub fn cc(&self, cc: u8) -> f32 {
if (cc as usize) < MAX_CC_CONTROLLER && self.enabled[cc as usize] {
self.get_normalized_internal(cc) as f32
} else {
0.0
}
}
#[inline]
pub fn has_controller(&self, controller: u8) -> bool {
if (controller as usize) < MAX_CC_CONTROLLER {
self.enabled[controller as usize]
} else {
false
}
}
#[inline]
pub fn has_pitch_bend(&self) -> bool {
self.enabled[controller::PITCH_BEND as usize]
}
#[inline]
pub fn has_aftertouch(&self) -> bool {
self.enabled[controller::AFTERTOUCH as usize]
}
#[inline]
pub fn enabled_count(&self) -> usize {
self.enabled_count
}
pub fn enabled_controllers(&self) -> impl Iterator<Item = u8> + '_ {
self.parameter_infos.iter().map(|info| info.controller)
}
#[inline]
pub const fn parameter_id(controller: u8) -> u32 {
MIDI_CC_PARAM_BASE + controller as u32
}
#[inline]
pub const fn is_midi_cc_parameter(parameter_id: u32) -> bool {
parameter_id >= MIDI_CC_PARAM_BASE && parameter_id < MIDI_CC_PARAM_BASE + MAX_CC_CONTROLLER as u32
}
#[inline]
pub const fn parameter_id_to_controller(parameter_id: u32) -> Option<u8> {
if Self::is_midi_cc_parameter(parameter_id) {
Some((parameter_id - MIDI_CC_PARAM_BASE) as u8)
} else {
None
}
}
fn create_parameter_info(controller: u8) -> ParameterInfo {
let id = Self::parameter_id(controller);
let (name, short_name): (&'static str, &'static str) = match controller {
controller::PITCH_BEND => ("Pitch Bend", "PB"),
controller::AFTERTOUCH => ("Aftertouch", "AT"),
1 => ("Mod Wheel", "MW"),
2 => ("Breath Controller", "BC"),
7 => ("Volume", "Vol"),
10 => ("Pan", "Pan"),
11 => ("Expression", "Exp"),
64 => ("Sustain Pedal", "Sus"),
_ => ("MIDI CC", "CC"),
};
let default = if controller == controller::PITCH_BEND {
0.5
} else {
0.0
};
ParameterInfo {
id,
name,
short_name,
units: "",
unit: ParameterUnit::Generic,
default_normalized: default,
step_count: 0,
flags: ParameterFlags {
can_automate: true,
is_readonly: false,
is_bypass: false,
is_list: false,
is_hidden: true, },
group_id: ROOT_GROUP_ID,
}
}
#[inline]
fn get_normalized_internal(&self, controller: u8) -> f64 {
let idx = controller as usize;
if idx < MAX_CC_CONTROLLER {
f64::from_bits(self.values[idx].load(Ordering::Relaxed))
} else {
0.0
}
}
fn set_normalized_internal(&self, controller: u8, value: f64) {
let idx = controller as usize;
if idx < MAX_CC_CONTROLLER {
self.values[idx].store(value.clamp(0.0, 1.0).to_bits(), Ordering::Relaxed);
}
}
}
unsafe impl Send for MidiCcState {}
unsafe impl Sync for MidiCcState {}
impl core::fmt::Debug for MidiCcState {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let enabled: Vec<u8> = self
.enabled
.iter()
.enumerate()
.filter_map(|(i, &e)| if e { Some(i as u8) } else { None })
.collect();
f.debug_struct("MidiCcState")
.field("enabled_controllers", &enabled)
.field("enabled_count", &self.enabled_count)
.finish()
}
}
impl ParameterStore for MidiCcState {
fn count(&self) -> usize {
self.enabled_count
}
fn info(&self, index: usize) -> Option<&ParameterInfo> {
self.parameter_infos.get(index).map(|i| &i.info)
}
fn get_normalized(&self, id: ParameterId) -> ParameterValue {
if let Some(controller) = Self::parameter_id_to_controller(id) {
self.get_normalized_internal(controller)
} else {
0.0
}
}
fn set_normalized(&self, id: ParameterId, value: ParameterValue) {
if let Some(controller) = Self::parameter_id_to_controller(id) {
self.set_normalized_internal(controller, value);
}
}
fn normalized_to_string(&self, id: ParameterId, normalized: ParameterValue) -> String {
if let Some(controller) = Self::parameter_id_to_controller(id) {
if controller == controller::PITCH_BEND {
let semitones = (normalized * 2.0 - 1.0) * 2.0;
return format!("{:+.1} st", semitones);
}
}
format!("{:.0}", normalized * 127.0)
}
fn string_to_normalized(&self, _id: ParameterId, string: &str) -> Option<ParameterValue> {
if let Ok(v) = string.parse::<f64>() {
return Some((v / 127.0).clamp(0.0, 1.0));
}
if let Some(v) = string.strip_suffix('%') {
if let Ok(v) = v.trim().parse::<f64>() {
return Some((v / 100.0).clamp(0.0, 1.0));
}
}
None
}
fn normalized_to_plain(&self, _id: ParameterId, normalized: ParameterValue) -> ParameterValue {
normalized * 127.0
}
fn plain_to_normalized(&self, _id: ParameterId, plain: ParameterValue) -> ParameterValue {
(plain / 127.0).clamp(0.0, 1.0)
}
}
impl ParameterGroups for MidiCcState {
fn group_count(&self) -> usize {
1 }
fn group_info(&self, index: usize) -> Option<GroupInfo> {
if index == 0 {
Some(GroupInfo::root())
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_config() {
let config = MidiCcConfig::new()
.with_pitch_bend()
.with_mod_wheel()
.with_ccs(&[7, 64]);
let state = MidiCcState::from_config(&config);
assert!(state.has_pitch_bend());
assert!(state.has_controller(1)); assert!(state.has_controller(7)); assert!(state.has_controller(64)); assert!(!state.has_aftertouch());
assert_eq!(state.enabled_count(), 4);
}
#[test]
fn test_pitch_bend_default() {
let config = MidiCcConfig::new().with_pitch_bend();
let state = MidiCcState::from_config(&config);
assert!((state.pitch_bend() - 0.0).abs() < 0.01);
}
#[test]
fn test_set_and_get() {
let config = MidiCcConfig::new().with_pitch_bend().with_mod_wheel();
let state = MidiCcState::from_config(&config);
let pb_id = MidiCcState::parameter_id(controller::PITCH_BEND);
state.set_normalized(pb_id, 1.0);
assert!((state.pitch_bend() - 1.0).abs() < 0.01);
state.set_normalized(pb_id, 0.0);
assert!((state.pitch_bend() - (-1.0)).abs() < 0.01);
let mw_id = MidiCcState::parameter_id(1);
state.set_normalized(mw_id, 0.75);
assert!((state.mod_wheel() - 0.75).abs() < 0.01);
}
#[test]
fn test_parameter_id_helpers() {
assert_eq!(MidiCcState::parameter_id(1), MIDI_CC_PARAM_BASE + 1);
assert_eq!(MidiCcState::parameter_id(129), MIDI_CC_PARAM_BASE + 129);
assert!(MidiCcState::is_midi_cc_parameter(MIDI_CC_PARAM_BASE));
assert!(MidiCcState::is_midi_cc_parameter(MIDI_CC_PARAM_BASE + 129));
assert!(!MidiCcState::is_midi_cc_parameter(0));
assert!(!MidiCcState::is_midi_cc_parameter(MIDI_CC_PARAM_BASE + 200));
assert_eq!(
MidiCcState::parameter_id_to_controller(MIDI_CC_PARAM_BASE + 1),
Some(1)
);
assert_eq!(MidiCcState::parameter_id_to_controller(100), None);
}
}