use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MidiMapping {
pub channel: u8,
pub cc: u8,
pub param_id: String,
pub min_value: f32,
pub max_value: f32,
pub curve: MappingCurve,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum MappingCurve {
#[default]
Linear,
Exponential,
Logarithmic,
}
pub struct MidiLearn {
mappings: HashMap<(u8, u8), MidiMapping>,
learn_mode: Option<LearnState>,
}
#[derive(Debug, Clone)]
struct LearnState {
param_id: String,
min_value: f32,
max_value: f32,
curve: MappingCurve,
}
impl MidiLearn {
pub fn new() -> Self {
Self {
mappings: HashMap::new(),
learn_mode: None,
}
}
pub fn start_learn(
&mut self,
param_id: impl Into<String>,
min_value: f32,
max_value: f32,
curve: MappingCurve,
) {
self.learn_mode = Some(LearnState {
param_id: param_id.into(),
min_value,
max_value,
curve,
});
}
pub fn cancel_learn(&mut self) {
self.learn_mode = None;
}
pub fn is_learning(&self) -> bool {
self.learn_mode.is_some()
}
pub fn process_cc(&mut self, channel: u8, cc: u8, value: u8) -> Option<(String, f32)> {
if let Some(learn_state) = self.learn_mode.take() {
let mapping = MidiMapping {
channel,
cc,
param_id: learn_state.param_id.clone(),
min_value: learn_state.min_value,
max_value: learn_state.max_value,
curve: learn_state.curve,
};
self.mappings.insert((channel, cc), mapping.clone());
let mapped_value = Self::map_value(value, &mapping);
return Some((mapping.param_id, mapped_value));
}
if let Some(mapping) = self.mappings.get(&(channel, cc)) {
let mapped_value = Self::map_value(value, mapping);
Some((mapping.param_id.clone(), mapped_value))
} else {
None
}
}
pub fn add_mapping(
&mut self,
channel: u8,
cc: u8,
param_id: impl Into<String>,
min_value: f32,
max_value: f32,
curve: MappingCurve,
) {
let mapping = MidiMapping {
channel,
cc,
param_id: param_id.into(),
min_value,
max_value,
curve,
};
self.mappings.insert((channel, cc), mapping);
}
pub fn remove_mapping(&mut self, channel: u8, cc: u8) -> Option<MidiMapping> {
self.mappings.remove(&(channel, cc))
}
pub fn get_mapping(&self, channel: u8, cc: u8) -> Option<&MidiMapping> {
self.mappings.get(&(channel, cc))
}
pub fn mappings(&self) -> impl Iterator<Item = &MidiMapping> {
self.mappings.values()
}
pub fn clear_mappings(&mut self) {
self.mappings.clear();
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
let mappings: Vec<_> = self.mappings.values().collect();
serde_json::to_string_pretty(&mappings)
}
pub fn from_json(&mut self, json: &str) -> Result<(), serde_json::Error> {
let mappings: Vec<MidiMapping> = serde_json::from_str(json)?;
self.mappings.clear();
for mapping in mappings {
self.mappings.insert((mapping.channel, mapping.cc), mapping);
}
Ok(())
}
fn map_value(cc_value: u8, mapping: &MidiMapping) -> f32 {
let normalized = cc_value as f32 / 127.0;
let curved = match mapping.curve {
MappingCurve::Linear => normalized,
MappingCurve::Exponential => {
normalized * normalized
}
MappingCurve::Logarithmic => {
if normalized <= 0.0 {
0.0
} else {
(1.0 + 9.0 * normalized).log10()
}
}
};
mapping.min_value + curved * (mapping.max_value - mapping.min_value)
}
}
impl Default for MidiLearn {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_midi_learn_basic() {
let mut learn = MidiLearn::new();
learn.start_learn("gain", 0.0, 1.0, MappingCurve::Linear);
assert!(learn.is_learning());
let result = learn.process_cc(1, 7, 64);
assert!(result.is_some());
let (param_id, value) = result.unwrap();
assert_eq!(param_id, "gain");
assert!((value - 0.5).abs() < 0.01);
assert!(!learn.is_learning());
assert!(learn.get_mapping(1, 7).is_some());
}
#[test]
fn test_midi_learn_cancel() {
let mut learn = MidiLearn::new();
learn.start_learn("gain", 0.0, 1.0, MappingCurve::Linear);
assert!(learn.is_learning());
learn.cancel_learn();
assert!(!learn.is_learning());
let result = learn.process_cc(1, 7, 64);
assert!(result.is_none());
}
#[test]
fn test_midi_learn_apply_mapping() {
let mut learn = MidiLearn::new();
learn.add_mapping(1, 7, "gain", 0.0, 1.0, MappingCurve::Linear);
let result = learn.process_cc(1, 7, 0);
assert_eq!(result, Some(("gain".to_string(), 0.0)));
let result = learn.process_cc(1, 7, 127);
assert_eq!(result, Some(("gain".to_string(), 1.0)));
let result = learn.process_cc(1, 7, 64);
let (_, value) = result.unwrap();
assert!((value - 0.5).abs() < 0.01);
}
#[test]
fn test_midi_learn_exponential_curve() {
let mut learn = MidiLearn::new();
learn.add_mapping(1, 7, "cutoff", 20.0, 20000.0, MappingCurve::Exponential);
let result = learn.process_cc(1, 7, 64);
let (_, value) = result.unwrap();
assert!(value > 4000.0 && value < 6000.0);
}
#[test]
fn test_midi_learn_remove_mapping() {
let mut learn = MidiLearn::new();
learn.add_mapping(1, 7, "gain", 0.0, 1.0, MappingCurve::Linear);
assert!(learn.get_mapping(1, 7).is_some());
let removed = learn.remove_mapping(1, 7);
assert!(removed.is_some());
assert!(learn.get_mapping(1, 7).is_none());
}
#[test]
fn test_midi_learn_json_serialization() {
let mut learn = MidiLearn::new();
learn.add_mapping(1, 7, "gain", 0.0, 1.0, MappingCurve::Linear);
learn.add_mapping(1, 74, "cutoff", 20.0, 20000.0, MappingCurve::Exponential);
let json = learn.to_json().unwrap();
assert!(json.contains("gain"));
assert!(json.contains("cutoff"));
let mut learn2 = MidiLearn::new();
learn2.from_json(&json).unwrap();
assert!(learn2.get_mapping(1, 7).is_some());
assert!(learn2.get_mapping(1, 74).is_some());
}
}