1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct MidiMapping {
12 pub channel: u8,
14
15 pub cc: u8,
17
18 pub param_id: String,
20
21 pub min_value: f32,
23
24 pub max_value: f32,
26
27 pub curve: MappingCurve,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
33pub enum MappingCurve {
34 #[default]
36 Linear,
37 Exponential,
39 Logarithmic,
41}
42
43pub struct MidiLearn {
45 mappings: HashMap<(u8, u8), MidiMapping>,
47
48 learn_mode: Option<LearnState>,
50}
51
52#[derive(Debug, Clone)]
54struct LearnState {
55 param_id: String,
57
58 min_value: f32,
60 max_value: f32,
61
62 curve: MappingCurve,
64}
65
66impl MidiLearn {
67 pub fn new() -> Self {
69 Self {
70 mappings: HashMap::new(),
71 learn_mode: None,
72 }
73 }
74
75 pub fn start_learn(
100 &mut self,
101 param_id: impl Into<String>,
102 min_value: f32,
103 max_value: f32,
104 curve: MappingCurve,
105 ) {
106 self.learn_mode = Some(LearnState {
107 param_id: param_id.into(),
108 min_value,
109 max_value,
110 curve,
111 });
112 }
113
114 pub fn cancel_learn(&mut self) {
116 self.learn_mode = None;
117 }
118
119 pub fn is_learning(&self) -> bool {
121 self.learn_mode.is_some()
122 }
123
124 pub fn process_cc(&mut self, channel: u8, cc: u8, value: u8) -> Option<(String, f32)> {
164 if let Some(learn_state) = self.learn_mode.take() {
166 let mapping = MidiMapping {
167 channel,
168 cc,
169 param_id: learn_state.param_id.clone(),
170 min_value: learn_state.min_value,
171 max_value: learn_state.max_value,
172 curve: learn_state.curve,
173 };
174
175 self.mappings.insert((channel, cc), mapping.clone());
176
177 let mapped_value = Self::map_value(value, &mapping);
179 return Some((mapping.param_id, mapped_value));
180 }
181
182 if let Some(mapping) = self.mappings.get(&(channel, cc)) {
184 let mapped_value = Self::map_value(value, mapping);
185 Some((mapping.param_id.clone(), mapped_value))
186 } else {
187 None
188 }
189 }
190
191 pub fn add_mapping(
202 &mut self,
203 channel: u8,
204 cc: u8,
205 param_id: impl Into<String>,
206 min_value: f32,
207 max_value: f32,
208 curve: MappingCurve,
209 ) {
210 let mapping = MidiMapping {
211 channel,
212 cc,
213 param_id: param_id.into(),
214 min_value,
215 max_value,
216 curve,
217 };
218 self.mappings.insert((channel, cc), mapping);
219 }
220
221 pub fn remove_mapping(&mut self, channel: u8, cc: u8) -> Option<MidiMapping> {
232 self.mappings.remove(&(channel, cc))
233 }
234
235 pub fn get_mapping(&self, channel: u8, cc: u8) -> Option<&MidiMapping> {
237 self.mappings.get(&(channel, cc))
238 }
239
240 pub fn mappings(&self) -> impl Iterator<Item = &MidiMapping> {
242 self.mappings.values()
243 }
244
245 pub fn clear_mappings(&mut self) {
247 self.mappings.clear();
248 }
249
250 pub fn to_json(&self) -> Result<String, serde_json::Error> {
252 let mappings: Vec<_> = self.mappings.values().collect();
253 serde_json::to_string_pretty(&mappings)
254 }
255
256 pub fn from_json(&mut self, json: &str) -> Result<(), serde_json::Error> {
258 let mappings: Vec<MidiMapping> = serde_json::from_str(json)?;
259 self.mappings.clear();
260 for mapping in mappings {
261 self.mappings.insert((mapping.channel, mapping.cc), mapping);
262 }
263 Ok(())
264 }
265
266 fn map_value(cc_value: u8, mapping: &MidiMapping) -> f32 {
268 let normalized = cc_value as f32 / 127.0; let curved = match mapping.curve {
271 MappingCurve::Linear => normalized,
272 MappingCurve::Exponential => {
273 normalized * normalized
275 }
276 MappingCurve::Logarithmic => {
277 if normalized <= 0.0 {
280 0.0
281 } else {
282 (1.0 + 9.0 * normalized).log10()
283 }
284 }
285 };
286
287 mapping.min_value + curved * (mapping.max_value - mapping.min_value)
289 }
290}
291
292impl Default for MidiLearn {
293 fn default() -> Self {
294 Self::new()
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301
302 #[test]
303 fn test_midi_learn_basic() {
304 let mut learn = MidiLearn::new();
305
306 learn.start_learn("gain", 0.0, 1.0, MappingCurve::Linear);
308 assert!(learn.is_learning());
309
310 let result = learn.process_cc(1, 7, 64);
312 assert!(result.is_some());
313 let (param_id, value) = result.unwrap();
314 assert_eq!(param_id, "gain");
315 assert!((value - 0.5).abs() < 0.01);
316
317 assert!(!learn.is_learning());
319
320 assert!(learn.get_mapping(1, 7).is_some());
322 }
323
324 #[test]
325 fn test_midi_learn_cancel() {
326 let mut learn = MidiLearn::new();
327
328 learn.start_learn("gain", 0.0, 1.0, MappingCurve::Linear);
329 assert!(learn.is_learning());
330
331 learn.cancel_learn();
332 assert!(!learn.is_learning());
333
334 let result = learn.process_cc(1, 7, 64);
336 assert!(result.is_none());
337 }
338
339 #[test]
340 fn test_midi_learn_apply_mapping() {
341 let mut learn = MidiLearn::new();
342
343 learn.add_mapping(1, 7, "gain", 0.0, 1.0, MappingCurve::Linear);
345
346 let result = learn.process_cc(1, 7, 0);
348 assert_eq!(result, Some(("gain".to_string(), 0.0)));
349
350 let result = learn.process_cc(1, 7, 127);
351 assert_eq!(result, Some(("gain".to_string(), 1.0)));
352
353 let result = learn.process_cc(1, 7, 64);
354 let (_, value) = result.unwrap();
355 assert!((value - 0.5).abs() < 0.01);
356 }
357
358 #[test]
359 fn test_midi_learn_exponential_curve() {
360 let mut learn = MidiLearn::new();
361
362 learn.add_mapping(1, 7, "cutoff", 20.0, 20000.0, MappingCurve::Exponential);
363
364 let result = learn.process_cc(1, 7, 64);
366 let (_, value) = result.unwrap();
367 assert!(value > 4000.0 && value < 6000.0);
370 }
371
372 #[test]
373 fn test_midi_learn_remove_mapping() {
374 let mut learn = MidiLearn::new();
375
376 learn.add_mapping(1, 7, "gain", 0.0, 1.0, MappingCurve::Linear);
377 assert!(learn.get_mapping(1, 7).is_some());
378
379 let removed = learn.remove_mapping(1, 7);
380 assert!(removed.is_some());
381 assert!(learn.get_mapping(1, 7).is_none());
382 }
383
384 #[test]
385 fn test_midi_learn_json_serialization() {
386 let mut learn = MidiLearn::new();
387
388 learn.add_mapping(1, 7, "gain", 0.0, 1.0, MappingCurve::Linear);
389 learn.add_mapping(1, 74, "cutoff", 20.0, 20000.0, MappingCurve::Exponential);
390
391 let json = learn.to_json().unwrap();
393 assert!(json.contains("gain"));
394 assert!(json.contains("cutoff"));
395
396 let mut learn2 = MidiLearn::new();
398 learn2.from_json(&json).unwrap();
399
400 assert!(learn2.get_mapping(1, 7).is_some());
401 assert!(learn2.get_mapping(1, 74).is_some());
402 }
403}