1use crate::api::{Command, CommandData};
2use crate::midi::MidiEvent;
3use csv::StringRecord;
4use std::fs::File;
5use std::io;
6use std::str::FromStr;
7
8#[cfg(test)]
9mod tests {
10 use crate::api::LooperCommand::{RecordOverdubPlay, SetPan};
11 use crate::api::{Command, CommandData, LooperTarget};
12 use crate::config::{DataValue, MidiMapping, FILE_HEADER};
13 use std::fs::File;
14 use std::io::Write;
15 use tempfile::NamedTempFile;
16
17 #[test]
18 fn test_load_midi_mapping() {
19 let _ = fern::Dispatch::new()
20 .level(log::LevelFilter::Debug)
21 .chain(fern::Output::call(|record| println!("{}", record.args())))
22 .apply();
23
24 let mut file = NamedTempFile::new().unwrap();
25 {
26 let file = file.as_file_mut();
27 writeln!(file, "{}", FILE_HEADER).unwrap();
28 writeln!(file, "*\t22\t127\tRecordOverdubPlay\t0").unwrap();
29 writeln!(file, "*\t23\t*\tSetMetronomeLevel\t50").unwrap();
30 writeln!(file, "1\t24\t6\tStart").unwrap();
31 writeln!(file, "1\t24\t0-127\tSetPan\tSelected\t$data").unwrap();
32 file.flush().unwrap();
33 }
34
35 let mapping = MidiMapping::from_file(
36 &file.path().to_string_lossy(),
37 &File::open(&file.path()).unwrap(),
38 )
39 .unwrap();
40
41 assert_eq!(None, mapping[0].channel);
42 assert_eq!(22, mapping[0].controller);
43 assert_eq!(DataValue::Value(127), mapping[0].data);
44 assert_eq!(
45 Command::Looper(RecordOverdubPlay, LooperTarget::Index(0)),
46 (mapping[0].command)(CommandData { data: 127 })
47 );
48
49 assert_eq!(None, mapping[1].channel);
50 assert_eq!(23, mapping[1].controller);
51 assert_eq!(DataValue::Any, mapping[1].data);
52 assert_eq!(
53 Command::SetMetronomeLevel(50),
54 (mapping[1].command)(CommandData { data: 39 })
55 );
56
57 assert_eq!(Some(1), mapping[2].channel);
58 assert_eq!(24, mapping[2].controller);
59 assert_eq!(DataValue::Value(6), mapping[2].data);
60 assert_eq!(
61 Command::Start,
62 (mapping[2].command)(CommandData { data: 39 })
63 );
64
65 assert_eq!(Some(1), mapping[3].channel);
66 assert_eq!(24, mapping[3].controller);
67 assert_eq!(DataValue::Range(0, 127), mapping[3].data);
68 assert_eq!(
69 Command::Looper(SetPan(1.0), LooperTarget::Selected),
70 (mapping[3].command)(CommandData { data: 127 })
71 );
72 }
73}
74
75pub static FILE_HEADER: &str = "Channel\tController\tData\tCommand\tArg1\tArg2\tArg3";
76
77pub struct Config {
78 pub midi_mappings: Vec<MidiMapping>,
79}
80
81impl Config {
82 pub fn new() -> Config {
83 Config {
84 midi_mappings: vec![],
85 }
86 }
87}
88
89#[derive(Debug, PartialEq)]
90pub enum DataValue {
91 Any,
92 Range(u8, u8),
93 Value(u8),
94}
95
96impl DataValue {
97 fn parse(s: &str) -> Option<DataValue> {
98 if s == "*" {
99 return Some(DataValue::Any);
100 }
101
102 if let Ok(v) = u8::from_str(s) {
103 if v <= 127 {
104 return Some(DataValue::Value(v));
105 }
106 }
107
108 let split: Vec<u8> = s.split("-").filter_map(|s| u8::from_str(s).ok()).collect();
109
110 if split.len() == 2 && split[0] <= 127 && split[1] <= 127 && split[0] < split[1] {
111 return Some(DataValue::Range(split[0], split[1]));
112 }
113
114 None
115 }
116
117 fn matches(&self, data: u8) -> bool {
118 match self {
119 DataValue::Any => true,
120 DataValue::Range(a, b) => (*a..=*b).contains(&data),
121 DataValue::Value(a) => *a == data,
122 }
123 }
124}
125
126pub struct MidiMapping {
127 pub channel: Option<u8>,
128 pub controller: u8,
129 pub data: DataValue,
130 pub command: Box<dyn Fn(CommandData) -> Command + Send>,
131}
132
133impl MidiMapping {
134 pub fn from_file(name: &str, file: &File) -> io::Result<Vec<MidiMapping>> {
135 let mut rdr = csv::ReaderBuilder::new()
136 .delimiter(b'\t')
137 .flexible(true)
138 .has_headers(true)
139 .from_reader(file);
140
141 let mut mappings = vec![];
142 let mut caught_error = false;
143
144 for result in rdr.records() {
145 let record = result?;
146
147 match Self::from_record(&record) {
148 Ok(mm) => mappings.push(mm),
149 Err(err) => {
150 caught_error = true;
151 if let Some(pos) = record.position() {
152 error!(
153 "Failed to load midi mapping on line {}: {}",
154 pos.line(),
155 err
156 );
157 } else {
158 error!("Failed to load midi mapping: {}", err);
159 }
160 }
161 }
162 }
163
164 if caught_error {
165 Err(io::Error::new(
166 io::ErrorKind::Other,
167 format!("Failed to parse midi mappings from {}", name),
168 ))
169 } else {
170 Ok(mappings)
171 }
172 }
173
174 fn from_record(record: &StringRecord) -> Result<MidiMapping, String> {
175 let channel = record.get(0).ok_or("No channel field".to_string())?;
176
177 let channel = match channel {
178 "*" => None,
179 c => Some(
180 u8::from_str(c)
181 .map_err(|_| "Channel must be * or a number".to_string())
182 .and_then(|c| {
183 if c >= 1 && c <= 16 {
184 Ok(c)
185 } else {
186 Err("Channel must be between 1 and 16".to_string())
187 }
188 })?,
189 ),
190 };
191
192 let controller = record
193 .get(1)
194 .ok_or("No controller field".to_string())
195 .and_then(|c| u8::from_str(c).map_err(|_| "Controller is not a number".to_string()))?;
196
197 let data = record
198 .get(2)
199 .ok_or("No data field".to_string())
200 .map(DataValue::parse)?
201 .ok_or("Invalid data format (expected either *, a range like 15-20, or a single value like 127")?;
202
203 let args: Vec<&str> = record.iter().skip(4).collect();
204
205 let command = record
206 .get(3)
207 .ok_or("No command field".to_string())
208 .and_then(|c| Command::from_str(c, &args))?;
209
210 Ok(MidiMapping {
211 channel,
212 controller,
213 data,
214 command,
215 })
216 }
217
218 pub fn command_for_event(&self, event: &MidiEvent) -> Option<Command> {
219 match event {
220 MidiEvent::ControllerChange {
221 channel,
222 controller,
223 data,
224 } => {
225 if (self.channel.is_none() || self.channel.unwrap() == *channel)
226 && (self.controller == *controller)
227 && (self.data.matches(*data))
228 {
229 return Some((self.command)(CommandData { data: *data }));
230 }
231 }
232 }
233
234 None
235 }
236}