ot_tools_io/projects/slots/
parsing.rs1use crate::errors::{InvalidValueError, ProjectParseError};
7use crate::generics::Slots;
8use crate::projects::{parse_hashmap_string_value, SlotAttributes, DEFAULT_GAIN, DEFAULT_TEMPO};
9use crate::settings::{LoopMode, SlotType, TimeStretchMode, TrigQuantizationMode};
10use itertools::Itertools;
11use std::collections::HashMap;
12use std::fmt;
13use std::path::PathBuf;
14use std::str::FromStr;
15
16pub fn parse_id(hmap: &HashMap<String, String>) -> Result<u8, ProjectParseError> {
17 let x = parse_hashmap_string_value::<u8>(hmap, "slot", None)?;
18 Ok(x)
19}
20
21pub fn parse_loop_mode(hmap: &HashMap<String, String>) -> Result<LoopMode, InvalidValueError> {
22 let default = LoopMode::default() as u8;
23 let default_str = format!["{default}"];
24
25 let x = parse_hashmap_string_value::<u8>(hmap, "loopmode", Some(default_str.as_str()))
26 .unwrap_or(default);
27 LoopMode::try_from(&x)
28}
29
30pub fn parse_tstrech_mode(
31 hmap: &HashMap<String, String>,
32) -> Result<TimeStretchMode, InvalidValueError> {
33 let default = TimeStretchMode::default() as u8;
34 let default_str = format!["{default}"];
35
36 let x = parse_hashmap_string_value::<u8>(hmap, "tsmode", Some(default_str.as_str()))
37 .unwrap_or(default);
38 TimeStretchMode::try_from(&x)
39}
40
41pub fn parse_trig_quantize_mode(
42 hmap: &HashMap<String, String>,
43) -> Result<TrigQuantizationMode, InvalidValueError> {
44 let default = TrigQuantizationMode::default() as u8;
45 let default_str = format!["{default}"];
46 let x = parse_hashmap_string_value::<u8>(hmap, "trigquantization", Some(default_str.as_str()))
47 .unwrap_or(default);
48 TrigQuantizationMode::try_from(x)
49}
50
51pub fn parse_gain(hmap: &HashMap<String, String>) -> Result<u8, ProjectParseError> {
52 let x =
57 parse_hashmap_string_value::<u8>(hmap, "gain", Some(format!["{DEFAULT_GAIN}"].as_str()))
58 .unwrap_or(DEFAULT_GAIN);
59 Ok(x)
60}
61
62pub fn parse_tempo(hmap: &HashMap<String, String>) -> Result<u16, ProjectParseError> {
63 let x = parse_hashmap_string_value::<u16>(
64 hmap,
65 "bpmx24",
66 Some(format!["{DEFAULT_TEMPO}"].as_str()),
67 )
68 .unwrap_or(DEFAULT_TEMPO);
69 Ok(x)
70}
71
72pub fn parse_path(hmap: &HashMap<String, String>) -> Result<PathBuf, ProjectParseError> {
73 let path_str = hmap.get("path").ok_or(ProjectParseError::HashMap)?;
74 let path = PathBuf::from_str(path_str).map_err(|_| ProjectParseError::String)?;
75 Ok(path)
76}
77
78impl TryFrom<&HashMap<String, String>> for SlotAttributes {
79 type Error = ProjectParseError;
80 fn try_from(value: &HashMap<String, String>) -> Result<Self, Self::Error> {
81 let slot_id = parse_id(value)?;
82
83 let sample_slot_type = value
84 .get("type")
85 .ok_or(ProjectParseError::HashMap)?
86 .to_string();
87 let slot_type = SlotType::try_from(sample_slot_type)?;
88
89 let path = parse_path(value)?;
90
91 let loop_mode = parse_loop_mode(value)?;
92 let timestrech_mode = parse_tstrech_mode(value)?;
93 let trig_quantization_mode = parse_trig_quantize_mode(value)?;
94 let gain = parse_gain(value)?;
95 let bpm = parse_tempo(value)?;
96
97 let sample_struct = Self {
98 slot_type,
99 slot_id,
100 path: if path.as_os_str() != "" {
101 Some(path)
102 } else {
103 None
104 },
105 timestrech_mode,
106 loop_mode,
107 trig_quantization_mode,
108 gain,
109 bpm,
110 };
111
112 Ok(sample_struct)
113 }
114}
115
116impl FromStr for SlotAttributes {
117 type Err = ProjectParseError;
118
119 fn from_str(s: &str) -> Result<Self, Self::Err> {
120 let k_v: Vec<Vec<&str>> = s
121 .strip_prefix("\r\n\r\n[SAMPLE]\r\n")
122 .ok_or(ProjectParseError::HashMap)?
123 .strip_suffix("\r\n")
124 .ok_or(ProjectParseError::HashMap)?
125 .split("\r\n")
126 .map(|x: &str| x.split('=').collect_vec())
127 .filter(|x: &Vec<&str>| x.len() == 2)
128 .collect_vec();
129
130 let mut hmap: HashMap<String, String> = HashMap::new();
131 for key_value_pair in k_v {
132 hmap.insert(
133 key_value_pair[0].to_string().to_lowercase(),
134 key_value_pair[1].to_string(),
135 );
136 }
137
138 let sample_struct = SlotAttributes::try_from(&hmap)?;
139 Ok(sample_struct)
140 }
141}
142
143impl fmt::Display for SlotAttributes {
144 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
145 let mut s = "[SAMPLE]\r\n".to_string();
146 s.push_str(&format!("TYPE={}", self.slot_type));
147 s.push_str("\r\n");
148 s.push_str(format!("SLOT={:0>3}", self.slot_id).as_str());
150 s.push_str("\r\n");
151 if let Some(path) = &self.path {
153 s.push_str(
165 format!("PATH={path:#?}")
166 .replace('"', "") .replace("\\", "") .as_str(),
169 );
170 } else {
171 s.push_str("PATH=");
172 }
173 s.push_str("\r\n");
174 s.push_str(format!("BPMx24={}", self.bpm).as_str());
175 s.push_str("\r\n");
176 s.push_str(format!("TSMODE={}", self.timestrech_mode as u8).as_str());
177 s.push_str("\r\n");
178 s.push_str(format!("LOOPMODE={}", self.loop_mode as u8).as_str());
179 s.push_str("\r\n");
180 s.push_str(format!("GAIN={}", self.gain).as_str());
181 s.push_str("\r\n");
182 s.push_str(format!("TRIGQUANTIZATION={}", self.trig_quantization_mode as u8).as_str());
183 s.push_str("\r\n[/SAMPLE]");
184 write!(f, "{s:#}")
185 }
186}
187
188impl fmt::Display for Slots<Option<SlotAttributes>> {
189 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
190 let mut string_slots: String = "".to_string();
191
192 let slots = vec![
193 self.static_slots.to_vec(),
194 self.flex_slots.to_vec(),
195 self.recording_buffers.to_vec(),
196 ];
197
198 let slots_concat = itertools::concat(slots).into_iter().flatten();
199
200 for slot in slots_concat {
201 string_slots.push_str(&slot.to_string());
202 string_slots.push_str("\r\n\r\n");
203 }
204 string_slots = string_slots
205 .strip_suffix("\r\n\r\n")
206 .ok_or(fmt::Error)?
207 .to_string();
208 write!(f, "{string_slots:#}")
209 }
210}
211
212#[cfg(test)]
213mod test_slot_attr_display {
214 use crate::projects::slots::test_utils;
215 use crate::OtToolsIoError;
216 #[test]
217 fn valid_full() -> Result<(), OtToolsIoError> {
218 test_utils::new_slot_attr_full_args()?.to_string();
219 Ok(())
220 }
221
222 #[test]
223 fn valid_minimal() -> Result<(), OtToolsIoError> {
224 test_utils::new_slot_attr_minimal_args()?.to_string();
225 Ok(())
226 }
227}
228
229fn collect_slots_by_id_and_type(
230 slots: &mut Slots<Option<SlotAttributes>>,
231 string: &str,
232) -> Result<(), ProjectParseError> {
233 let slot = SlotAttributes::from_str(string)?;
235 let zero_indexed_id = slot.slot_id as usize - 1;
236 match slot.slot_type {
237 SlotType::Static if slot.slot_id <= 128 => {
238 slots.static_slots[zero_indexed_id] = Some(slot);
239 Ok(())
240 }
241 SlotType::Flex if slot.slot_id <= 128 => {
242 slots.flex_slots[zero_indexed_id] = Some(slot);
243 Ok(())
244 }
245 SlotType::Flex if slot.slot_id > 128 && slot.slot_id <= 136 => {
246 let index = slot.slot_id as usize - 129;
247 slots.recording_buffers[index] = Some(slot);
248 Ok(())
249 }
250 _ => Err(ProjectParseError::String),
251 }
252}
253
254impl FromStr for Slots<Option<SlotAttributes>> {
255 type Err = ProjectParseError;
256
257 fn from_str(s: &str) -> Result<Self, Self::Err> {
258 let footer_stripped = s
259 .strip_suffix("\r\n\r\n############################\r\n\r\n")
260 .ok_or(ProjectParseError::Footer)?;
261
262 let data_window: Vec<&str> = footer_stripped
263 .split("############################\r\n# Samples\r\n############################")
264 .collect();
265
266 let mut samples_string: Vec<&str> = data_window[1].split("[/SAMPLE]").collect();
267 samples_string.pop();
269
270 let mut slots = Self::default();
272
273 for s in &samples_string {
274 collect_slots_by_id_and_type(&mut slots, s)?;
275 }
276
277 Ok(slots)
278 }
279}
280
281#[cfg(test)]
282#[allow(unused_imports)]
283mod test {
284
285 #[test]
286 fn parse_id_001_correct() {
287 let mut hmap = std::collections::HashMap::new();
288 hmap.insert("slot".to_string(), "001".to_string());
289
290 let slot_id = crate::projects::slots::parsing::parse_id(&hmap);
291
292 assert_eq!(1, slot_id.unwrap());
293 }
294
295 #[test]
296 fn parse_id_1_correct() {
297 let mut hmap = std::collections::HashMap::new();
298 hmap.insert("slot".to_string(), "1".to_string());
299
300 let slot_id = crate::projects::slots::parsing::parse_id(&hmap);
301
302 assert_eq!(1, slot_id.unwrap());
303 }
304
305 #[test]
306 fn parse_id_127_correct() {
307 let mut hmap = std::collections::HashMap::new();
308 hmap.insert("slot".to_string(), "127".to_string());
309
310 let slot_id = crate::projects::slots::parsing::parse_id(&hmap);
311
312 assert_eq!(127, slot_id.unwrap());
313 }
314
315 #[test]
316 fn parse_id_099_correct() {
317 let mut hmap = std::collections::HashMap::new();
318 hmap.insert("slot".to_string(), "099".to_string());
319
320 let slot_id = crate::projects::slots::parsing::parse_id(&hmap);
321
322 assert_eq!(99, slot_id.unwrap());
323 }
324
325 #[test]
326 fn parse_id_010_correct() {
327 let mut hmap = std::collections::HashMap::new();
328 hmap.insert("slot".to_string(), "010".to_string());
329
330 let slot_id = crate::projects::slots::parsing::parse_id(&hmap);
331
332 assert_eq!(10, slot_id.unwrap());
333 }
334
335 #[test]
336 fn test_parse_id_err_bad_value_type_err() {
337 let mut hmap = std::collections::HashMap::new();
338 hmap.insert("slot".to_string(), "AAAA".to_string());
339 let slot_id = crate::projects::slots::parsing::parse_id(&hmap);
340 assert!(slot_id.is_err());
341 }
342
343 #[test]
344 fn test_parse_tempo_correct_default() {
345 let mut hmap = std::collections::HashMap::new();
346 hmap.insert("bpmx24".to_string(), "2880".to_string());
347 let r = crate::projects::slots::parsing::parse_tempo(&hmap);
348 assert_eq!(2880_u16, r.unwrap());
349 }
350
351 #[test]
352 fn test_parse_tempo_correct_min() {
353 let mut hmap = std::collections::HashMap::new();
354 hmap.insert("bpmx24".to_string(), "720".to_string());
355 let r = crate::projects::slots::parsing::parse_tempo(&hmap);
356 assert_eq!(720_u16, r.unwrap());
357 }
358
359 #[test]
360 fn test_parse_tempo_correct_max() {
361 let mut hmap = std::collections::HashMap::new();
362 hmap.insert("bpmx24".to_string(), "7200".to_string());
363 let r = crate::projects::slots::parsing::parse_tempo(&hmap);
364 assert_eq!(7200_u16, r.unwrap());
365 }
366
367 #[test]
368 fn test_parse_tempo_bad_value_type_default_return() {
369 let mut hmap = std::collections::HashMap::new();
370 hmap.insert("bpmx24".to_string(), "AAAFSFSFSSFfssafAA".to_string());
371 let r = crate::projects::slots::parsing::parse_tempo(&hmap);
372 assert_eq!(r.unwrap(), 2880_u16);
373 }
374
375 #[test]
376 fn test_parse_gain_correct() {
377 let mut hmap = std::collections::HashMap::new();
378 hmap.insert("gain".to_string(), "72".to_string());
379 let r = crate::projects::slots::parsing::parse_gain(&hmap);
380 assert_eq!(72, r.unwrap());
381 }
382
383 #[test]
384 fn test_parse_gain_bad_value_type_default_return() {
385 let mut hmap = std::collections::HashMap::new();
386 hmap.insert("gain".to_string(), "AAAFSFSFSSFfssafAA".to_string());
387 let r = crate::projects::slots::parsing::parse_gain(&hmap);
388 assert_eq!(r.unwrap(), super::DEFAULT_GAIN); }
390
391 #[test]
392 fn test_parse_loop_mode_correct_off() {
393 let mut hmap = std::collections::HashMap::new();
394 hmap.insert("loopmode".to_string(), "0".to_string());
395 let r = crate::projects::slots::parsing::parse_loop_mode(&hmap);
396 assert_eq!(r.unwrap(), crate::settings::LoopMode::Off);
397 }
398
399 #[test]
400 fn test_parse_loop_mode_correct_normal() {
401 let mut hmap = std::collections::HashMap::new();
402 hmap.insert("loopmode".to_string(), "1".to_string());
403 let r = crate::projects::slots::parsing::parse_loop_mode(&hmap);
404 assert_eq!(r.unwrap(), crate::settings::LoopMode::Normal);
405 }
406
407 #[test]
408 fn test_parse_loop_mode_correct_pingpong() {
409 let mut hmap = std::collections::HashMap::new();
410 hmap.insert("loopmode".to_string(), "2".to_string());
411 let r = crate::projects::slots::parsing::parse_loop_mode(&hmap);
412 assert_eq!(r.unwrap(), crate::settings::LoopMode::PingPong);
413 }
414
415 #[test]
416 fn test_parse_loop_mode_bad_value_type_default_return() {
417 let mut hmap = std::collections::HashMap::new();
418 hmap.insert("loopmode".to_string(), "AAAFSFSFSSFfssafAA".to_string());
419 let r = crate::projects::slots::parsing::parse_loop_mode(&hmap);
420 assert_eq!(r.unwrap(), crate::settings::LoopMode::default());
421 }
422
423 #[test]
424 fn test_parse_tstretch_correct_off() {
425 let mut hmap = std::collections::HashMap::new();
426 hmap.insert("tsmode".to_string(), "0".to_string());
427 let r = crate::projects::slots::parsing::parse_tstrech_mode(&hmap);
428 assert_eq!(crate::settings::TimeStretchMode::Off, r.unwrap());
429 }
430
431 #[test]
432 fn test_parse_tstretch_correct_normal() {
433 let mut hmap = std::collections::HashMap::new();
434 hmap.insert("tsmode".to_string(), "2".to_string());
435 let r = crate::projects::slots::parsing::parse_tstrech_mode(&hmap);
436 assert_eq!(crate::settings::TimeStretchMode::Normal, r.unwrap());
437 }
438
439 #[test]
440 fn test_parse_tstretch_correct_beat() {
441 let mut hmap = std::collections::HashMap::new();
442 hmap.insert("tsmode".to_string(), "3".to_string());
443 let r = crate::projects::slots::parsing::parse_tstrech_mode(&hmap);
444 assert_eq!(crate::settings::TimeStretchMode::Beat, r.unwrap());
445 }
446
447 #[test]
448 fn test_parse_tstretch_bad_value_type_default_return() {
449 let mut hmap = std::collections::HashMap::new();
450 hmap.insert("tsmode".to_string(), "AAAFSFSFSSFfssafAA".to_string());
451 let r = crate::projects::slots::parsing::parse_tstrech_mode(&hmap);
452 assert_eq!(r.unwrap(), crate::settings::TimeStretchMode::default());
453 }
454
455 #[test]
456 fn test_parse_tquantize_correct_off() {
457 let mut hmap = std::collections::HashMap::new();
458 hmap.insert("trigquantization".to_string(), "255".to_string());
459 let r = crate::projects::slots::parsing::parse_trig_quantize_mode(&hmap);
460 assert_eq!(crate::settings::TrigQuantizationMode::Direct, r.unwrap());
461 }
462
463 #[test]
464 fn test_parse_tquantize_correct_direct() {
465 let mut hmap = std::collections::HashMap::new();
466 hmap.insert("trigquantization".to_string(), "0".to_string());
467 let r = crate::projects::slots::parsing::parse_trig_quantize_mode(&hmap);
468 assert_eq!(
469 crate::settings::TrigQuantizationMode::PatternLength,
470 r.unwrap()
471 );
472 }
473
474 #[test]
475 fn test_parse_tquantize_correct_onestep() {
476 let mut hmap = std::collections::HashMap::new();
477 hmap.insert("trigquantization".to_string(), "1".to_string());
478 let r = crate::projects::slots::parsing::parse_trig_quantize_mode(&hmap);
479 assert_eq!(crate::settings::TrigQuantizationMode::OneStep, r.unwrap());
480 }
481
482 #[test]
483 fn test_parse_tquantize_correct_twostep() {
484 let mut hmap = std::collections::HashMap::new();
485 hmap.insert("trigquantization".to_string(), "2".to_string());
486 let r = crate::projects::slots::parsing::parse_trig_quantize_mode(&hmap);
487 assert_eq!(crate::settings::TrigQuantizationMode::TwoSteps, r.unwrap());
488 }
489
490 #[test]
491 fn test_parse_tquantize_correct_threestep() {
492 let mut hmap = std::collections::HashMap::new();
493 hmap.insert("trigquantization".to_string(), "3".to_string());
494 let r = crate::projects::slots::parsing::parse_trig_quantize_mode(&hmap);
495 assert_eq!(
496 crate::settings::TrigQuantizationMode::ThreeSteps,
497 r.unwrap()
498 );
499 }
500
501 #[test]
502 fn test_parse_tquantize_correct_fourstep() {
503 let mut hmap = std::collections::HashMap::new();
504 hmap.insert("trigquantization".to_string(), "4".to_string());
505 let r = crate::projects::slots::parsing::parse_trig_quantize_mode(&hmap);
506 assert_eq!(crate::settings::TrigQuantizationMode::FourSteps, r.unwrap());
507 }
508
509 #[test]
512 fn test_parse_tquantize_bad_value_type_default_return() {
513 let mut hmap = std::collections::HashMap::new();
514 hmap.insert(
515 "trigquantization".to_string(),
516 "AAAFSFSFSSFfssafAA".to_string(),
517 );
518 let r = crate::projects::slots::parsing::parse_trig_quantize_mode(&hmap);
519 assert_eq!(r.unwrap(), crate::settings::TrigQuantizationMode::default());
520 }
521
522 use std::path::PathBuf;
523 #[test]
524 fn test_parse_path_good_utf8_value() {
525 let test_path = "../AUDIO/some/file.wav";
526
527 let mut hmap = std::collections::HashMap::new();
528 hmap.insert("path".to_string(), test_path.to_string());
529 let r = crate::projects::slots::parsing::parse_path(&hmap);
530 assert_eq!(r.unwrap(), PathBuf::from(test_path));
531 }
532
533 #[test]
534 fn test_parse_empty_path() {
535 let test_path = "";
536
537 let mut hmap = std::collections::HashMap::new();
538 hmap.insert("path".to_string(), test_path.to_string());
539 let r = crate::projects::slots::parsing::parse_path(&hmap);
540 assert_eq!(r.unwrap(), PathBuf::from(test_path));
541 }
542
543 #[test]
544 fn test_parse_non_utf8_path() {
545 let test_path = "../AUDIO/🇯🇲something.wav";
546
547 let mut hmap = std::collections::HashMap::new();
548 hmap.insert("path".to_string(), test_path.to_string());
549 let r = crate::projects::slots::parsing::parse_path(&hmap);
550 assert_eq!(r.unwrap(), PathBuf::from(test_path));
551 }
552}