ot_tools_io/projects/
slots.rs

1/*
2SPDX-License-Identifier: GPL-3.0-or-later
3Copyright © 2024 Mike Robeson [dijksterhuis]
4*/
5
6//! Types and parsing of the attributes data for a project's sample slots.
7//! Used in the [`crate::projects::ProjectFile`] type.
8//!
9//! NOTE: Sample slot attributes here refer to the non-'playback markers' data.
10//! See [crate::markers::MarkersFile] for information on trim/loop/slice
11//! settings for a project's sample slot.
12
13/*
14Example data:
15[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=001\r\nPATH=../AUDIO/flex.wav\r\nTRIM_BARSx100=173\r\nTSMODE=2\r\nLOOPMODE=1\r\nGAIN=48\r\nTRIGQUANTIZATION=-1\r\n[/SAMPLE]
16-----
17
18[SAMPLE]
19TYPE=FLEX
20SLOT=001
21PATH=../AUDIO/flex.wav
22TRIM_BARSx100=173
23TSMODE=2
24LOOPMODE=1
25GAIN=48
26TRIGQUANTIZATION=-1
27[/SAMPLE]
28*/
29use crate::projects::parse_hashmap_string_value;
30use crate::projects::ProjectParseError;
31use crate::settings::SlotType;
32
33use crate::settings::{InvalidValueError, LoopMode, TimeStretchMode, TrigQuantizationMode};
34
35use itertools::Itertools;
36use ot_tools_io_derive::IsDefaultCheck;
37use serde::de::{self, Deserializer, MapAccess, Visitor};
38use serde::{Deserialize, Serialize};
39use serde_big_array::Array;
40use std::{collections::HashMap, fmt, path::PathBuf, str::FromStr};
41
42/// A sample slot's global playback settings -- trig quantization, bpm,
43/// timestrech mode ... anything applied to the sample globally .
44/// The Octatrack only stores data when an audio file has been assigned to a sample slot.
45///
46/// NOTE: On the naming for this -- the Octatrack manual specifically refers to
47/// > SAVE SAMPLE SETTINGS will save the trim, slice and attribute settings in a
48/// > separate file and link it to the sample currently being edited.
49/// > -- page 87
50///
51/// So ... these are the Slot ATTRIBUTES which are saved to a settings file.
52#[derive(Serialize, PartialEq, Debug, Clone, Eq, Hash)]
53pub struct SlotAttributes {
54    /// Type of sample: STATIC or FLEX
55    pub slot_type: SlotType,
56
57    /// String ID Number of the slot the sample is assigned to e.g. 001, 002, 003...
58    /// Maximum of 128 entries for STATIC sample slots, but can be up to 136 for flex
59    /// slots as there are 8 recorders + 128 flex slots.
60    pub slot_id: u8,
61
62    /// Relative path to the file on the card from the project directory.
63    ///
64    /// Recording buffer flex slots by default have an empty path attribute,
65    /// which basically means 'no path'. In idiomatic rust that's an option.
66    pub path: Option<PathBuf>,
67
68    /// Current `TimestrechModes` setting for the specific slot. Example: `TSMODE=2`
69    /// See [TimeStretchMode].
70    pub timestrech_mode: TimeStretchMode,
71
72    /// Current `LoopMode` setting for the specific slot.
73    /// See [LoopMode].
74    pub loop_mode: LoopMode,
75
76    /// Current `TrigQuantizationModes` setting for this specific slot.
77    /// This is not used for recording buffer 'flex' tracks.
78    /// See [TrigQuantizationMode].
79    pub trig_quantization_mode: TrigQuantizationMode,
80
81    /// Sample gain. 48 is default as per sample attributes file. maximum 96, minimum 0.
82    pub gain: u8,
83
84    /// BPM of the sample in this slot. The stored representation is the 'real' bpm (float to 2
85    /// decimal places) multiplied by 24.
86    /// Default value is 2880 (120 BPM).
87    /// Max value is 7200 (300 BPM).
88    /// Min value is 720 (30 BPM).
89    pub bpm: u16,
90}
91
92#[allow(clippy::too_many_arguments)] // not my fault there's a bunch of inputs for this...
93impl SlotAttributes {
94    pub fn new(
95        slot_type: SlotType,
96        slot_id: u8,
97        path: Option<PathBuf>,
98        timestretch_mode: Option<TimeStretchMode>,
99        loop_mode: Option<LoopMode>,
100        trig_quantization_mode: Option<TrigQuantizationMode>,
101        gain: Option<u8>,
102        bpm: Option<u16>,
103    ) -> Result<Self, crate::samples::SampleSettingsError> {
104        if let Some(tempo) = bpm {
105            if !(720..=7200).contains(&tempo) {
106                return Err(crate::samples::SampleSettingsError::TempoOutOfBounds {
107                    value: tempo as u32,
108                });
109            }
110        }
111        if let Some(amp) = gain {
112            if !(24..=120).contains(&amp) {
113                return Err(crate::samples::SampleSettingsError::GainOutOfBounds {
114                    value: amp as u32,
115                });
116            }
117        }
118
119        Ok(Self {
120            slot_type,
121            slot_id,
122            path,
123            timestrech_mode: timestretch_mode.unwrap_or_default(),
124            loop_mode: loop_mode.unwrap_or_default(),
125            trig_quantization_mode: trig_quantization_mode.unwrap_or_default(),
126            gain: gain.unwrap_or(72),
127            bpm: bpm.unwrap_or(2880),
128        })
129    }
130}
131
132fn parse_id(hmap: &HashMap<String, String>) -> Result<u8, ProjectParseError> {
133    let x = parse_hashmap_string_value::<u8>(hmap, "slot", None)?;
134    Ok(x)
135}
136
137fn parse_loop_mode(hmap: &HashMap<String, String>) -> Result<LoopMode, InvalidValueError> {
138    let default = LoopMode::default() as u8;
139    let default_str = format!["{default}"];
140
141    let x = parse_hashmap_string_value::<u8>(hmap, "loopmode", Some(default_str.as_str()))
142        .unwrap_or(default);
143    LoopMode::try_from(&x)
144}
145
146fn parse_tstrech_mode(
147    hmap: &HashMap<String, String>,
148) -> Result<TimeStretchMode, InvalidValueError> {
149    let default = TimeStretchMode::default() as u8;
150    let default_str = format!["{default}"];
151
152    let x = parse_hashmap_string_value::<u8>(hmap, "tsmode", Some(default_str.as_str()))
153        .unwrap_or(default);
154    TimeStretchMode::try_from(&x)
155}
156
157fn parse_trig_quantize_mode(
158    hmap: &HashMap<String, String>,
159) -> Result<TrigQuantizationMode, InvalidValueError> {
160    let default = TrigQuantizationMode::default() as u8;
161    let default_str = format!["{default}"];
162    let x = parse_hashmap_string_value::<u8>(hmap, "trigquantization", Some(default_str.as_str()))
163        .unwrap_or(default);
164    TrigQuantizationMode::try_from(x)
165}
166
167fn parse_gain(hmap: &HashMap<String, String>) -> Result<u8, ProjectParseError> {
168    let x = parse_hashmap_string_value::<u8>(hmap, "gain", Some("72")).unwrap_or(72_u8);
169    Ok(x)
170}
171
172fn parse_tempo(hmap: &HashMap<String, String>) -> Result<u16, ProjectParseError> {
173    let x = parse_hashmap_string_value::<u16>(hmap, "bpmx24", Some("2880")).unwrap_or(2880_u16);
174    Ok(x)
175}
176
177fn parse_path(hmap: &HashMap<String, String>) -> Result<PathBuf, ProjectParseError> {
178    let path_str = hmap.get("path").ok_or(ProjectParseError::HashMap)?;
179    let path = PathBuf::from_str(path_str).map_err(|_| ProjectParseError::String)?;
180    Ok(path)
181}
182
183impl TryFrom<&HashMap<String, String>> for SlotAttributes {
184    type Error = ProjectParseError;
185    fn try_from(value: &HashMap<String, String>) -> Result<Self, Self::Error> {
186        let slot_id = parse_id(value)?;
187
188        let sample_slot_type = value
189            .get("type")
190            .ok_or(ProjectParseError::HashMap)?
191            .to_string();
192        let slot_type = SlotType::try_from(sample_slot_type)?;
193
194        let path = parse_path(value)?;
195
196        let loop_mode = parse_loop_mode(value)?;
197        let timestrech_mode = parse_tstrech_mode(value)?;
198        let trig_quantization_mode = parse_trig_quantize_mode(value)?;
199        let gain = parse_gain(value)?;
200        let bpm = parse_tempo(value)?;
201
202        let sample_struct = Self {
203            slot_type,
204            slot_id,
205            path: if path.as_os_str() != "" {
206                Some(path)
207            } else {
208                None
209            },
210            timestrech_mode,
211            loop_mode,
212            trig_quantization_mode,
213            gain,
214            bpm,
215        };
216
217        Ok(sample_struct)
218    }
219}
220
221impl FromStr for SlotAttributes {
222    type Err = ProjectParseError;
223
224    fn from_str(s: &str) -> Result<Self, Self::Err> {
225        let k_v: Vec<Vec<&str>> = s
226            .strip_prefix("\r\n\r\n[SAMPLE]\r\n")
227            .ok_or(ProjectParseError::HashMap)?
228            .strip_suffix("\r\n")
229            .ok_or(ProjectParseError::HashMap)?
230            .split("\r\n")
231            .map(|x: &str| x.split('=').collect_vec())
232            .filter(|x: &Vec<&str>| x.len() == 2)
233            .collect_vec();
234
235        let mut hmap: HashMap<String, String> = HashMap::new();
236        for key_value_pair in k_v {
237            hmap.insert(
238                key_value_pair[0].to_string().to_lowercase(),
239                key_value_pair[1].to_string(),
240            );
241        }
242
243        let sample_struct = SlotAttributes::try_from(&hmap)?;
244        Ok(sample_struct)
245    }
246}
247
248impl fmt::Display for SlotAttributes {
249    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
250        let mut s = "[SAMPLE]\r\n".to_string();
251        s.push_str(&format!("TYPE={}", self.slot_type));
252        s.push_str("\r\n");
253        // NOTE: Slot ID data is always prefixed with leading zeros
254        s.push_str(format!("SLOT={:0>3}", self.slot_id).as_str());
255        s.push_str("\r\n");
256        // NOTE: Handle recording buffers having empty paths
257        if let Some(path) = &self.path {
258            /*
259            HACK: Remove escape characters from the path string.
260
261            A path like `my\file.wav` ends up like `my\\\\[...loads more \ chars...]\\\file.wav`.
262            The Octatrack will attempt to load the project and then a catastrophic error message pops up.
263            Helpfully, it will have made changes to the `project.work` file already which means you
264            have to manually fix it yourself.
265
266            This looks like some recursive injection of escape characters during parsing of the PATH field.
267            Basically ... don't use the `\` escape character in path names!
268            */
269            s.push_str(
270                format!("PATH={path:#?}")
271                    .replace('"', "") // should not have quotes on PATH fields
272                    .replace("\\", "") // ^ need to remove escape chars (`\`)
273                    .as_str(),
274            );
275        } else {
276            s.push_str("PATH=");
277        }
278        s.push_str("\r\n");
279        s.push_str(format!("BPMx24={}", self.bpm).as_str());
280        s.push_str("\r\n");
281        s.push_str(format!("TSMODE={}", self.timestrech_mode as u8).as_str());
282        s.push_str("\r\n");
283        s.push_str(format!("LOOPMODE={}", self.loop_mode as u8).as_str());
284        s.push_str("\r\n");
285        s.push_str(format!("GAIN={}", self.gain).as_str());
286        s.push_str("\r\n");
287        s.push_str(format!("TRIGQUANTIZATION={}", self.trig_quantization_mode as u8).as_str());
288        s.push_str("\r\n[/SAMPLE]");
289        write!(f, "{s:#}")
290    }
291}
292
293// YAML/JSON Deserialization for Sample Slot
294// we can get away with just defining map deserialization for YAML/JSON.
295// `ToString`/`FromStr` implementations are currently used for the actual data file.
296impl<'de> Deserialize<'de> for SlotAttributes {
297    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
298    where
299        D: Deserializer<'de>,
300    {
301        enum Field {
302            SlotType,
303            SlotId,
304            Path,
305            Timestretch,
306            Loop,
307            Quant,
308            Gain,
309            Bpm,
310        }
311
312        // TODO: FIELDS_MAP: Tuple array
313        const FIELDS: &[&str] = &[
314            "slot_type",
315            "slot_id",
316            "path",
317            "timestrech_mode",
318            "loop_mode",
319            "trig_quantization_mode",
320            "gain",
321            "bpm",
322        ];
323
324        impl<'de> Deserialize<'de> for Field {
325            fn deserialize<D>(deserializer: D) -> Result<Field, D::Error>
326            where
327                D: Deserializer<'de>,
328            {
329                struct FieldVisitor;
330
331                impl Visitor<'_> for FieldVisitor {
332                    type Value = Field;
333
334                    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
335                        formatter.write_str(
336                            FIELDS
337                                .iter()
338                                .map(|x| format!["`{x}`"])
339                                .collect::<Vec<_>>()
340                                .join(" or ")
341                                .as_str(),
342                        )
343                    }
344
345                    fn visit_str<E>(self, value: &str) -> Result<Field, E>
346                    where
347                        E: de::Error,
348                    {
349                        match value {
350                            "slot_type" => Ok(Field::SlotType),
351                            "slot_id" => Ok(Field::SlotId),
352                            "path" => Ok(Field::Path),
353                            "timestrech_mode" => Ok(Field::Timestretch),
354                            "loop_mode" => Ok(Field::Loop),
355                            "trig_quantization_mode" => Ok(Field::Quant),
356                            "gain" => Ok(Field::Gain),
357                            "bpm" => Ok(Field::Bpm),
358                            _ => Err(de::Error::unknown_field(value, FIELDS)),
359                        }
360                    }
361                }
362
363                deserializer.deserialize_identifier(FieldVisitor)
364            }
365        }
366
367        struct SlotAttributesVisitor;
368
369        impl<'de> Visitor<'de> for SlotAttributesVisitor {
370            type Value = SlotAttributes;
371
372            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
373                formatter.write_str("struct SlotAttributes")
374            }
375
376            fn visit_map<V>(self, mut map: V) -> Result<SlotAttributes, V::Error>
377            where
378                V: MapAccess<'de>,
379            {
380                let mut slot_type = None;
381                let mut slot_id = None;
382                let mut path = None;
383                let mut timestretch_mode = None;
384                let mut loop_mode = None;
385                let mut trig_quantization_mode = None;
386                let mut gain = None;
387                let mut bpm = None;
388
389                while let Some(key) = map.next_key()? {
390                    match key {
391                        Field::SlotType => {
392                            if slot_type.is_some() {
393                                return Err(de::Error::duplicate_field("slot_type"));
394                            }
395                            slot_type = Some(map.next_value::<SlotType>()?);
396                        }
397                        Field::SlotId => {
398                            if slot_id.is_some() {
399                                return Err(de::Error::duplicate_field("slot_id"));
400                            }
401                            slot_id = Some(map.next_value::<u8>()?);
402                        }
403                        Field::Path => {
404                            if path.is_some() {
405                                return Err(de::Error::duplicate_field("path"));
406                            }
407                            path = Some(map.next_value::<PathBuf>()?);
408                        }
409                        Field::Timestretch => {
410                            if timestretch_mode.is_some() {
411                                return Err(de::Error::duplicate_field("timestretch_mode"));
412                            }
413                            timestretch_mode = Some(map.next_value::<TimeStretchMode>()?);
414                        }
415                        Field::Loop => {
416                            if loop_mode.is_some() {
417                                return Err(de::Error::duplicate_field("loop_mode"));
418                            }
419                            loop_mode = Some(map.next_value::<LoopMode>()?);
420                        }
421                        Field::Quant => {
422                            if trig_quantization_mode.is_some() {
423                                return Err(de::Error::duplicate_field("trig_quantization_mode"));
424                            }
425                            trig_quantization_mode =
426                                Some(map.next_value::<TrigQuantizationMode>()?);
427                        }
428                        Field::Gain => {
429                            if gain.is_some() {
430                                return Err(de::Error::duplicate_field("gain"));
431                            }
432                            gain = Some(map.next_value::<u8>()?);
433                        }
434                        Field::Bpm => {
435                            if bpm.is_some() {
436                                return Err(de::Error::duplicate_field("bpm"));
437                            }
438                            bpm = Some(map.next_value::<u16>()?);
439                        }
440                    }
441                }
442
443                let slot = SlotAttributes {
444                    slot_type: slot_type.ok_or_else(|| de::Error::missing_field("slot_type"))?,
445                    slot_id: slot_id.ok_or_else(|| de::Error::missing_field("slot_type"))?,
446                    path, // allowed to be missing to handle recording buffer empty paths
447                    timestrech_mode: timestretch_mode
448                        .ok_or_else(|| de::Error::missing_field("trimstretch_mode"))?,
449                    loop_mode: loop_mode.ok_or_else(|| de::Error::missing_field("loop_mode"))?,
450                    trig_quantization_mode: trig_quantization_mode
451                        .ok_or_else(|| de::Error::missing_field("trig_quantization_mode"))?,
452                    gain: gain.ok_or_else(|| de::Error::missing_field("gain"))?,
453                    bpm: bpm.ok_or_else(|| de::Error::missing_field("bpm"))?,
454                };
455
456                Ok(slot)
457            }
458        }
459
460        deserializer.deserialize_struct("SampleSlot", FIELDS, SlotAttributesVisitor)
461    }
462}
463
464/// Container type for all sample slots.
465///
466/// The `project.*` data files store all sample slots together in a single 1-indexed array.
467/// This tends to lead to a lot of additional yak-shaving when interacting with sample slots.
468/// So this type models the octatrack's UI, displaying slots in 2x arrays: Flex and Static.
469/// These individual arrays are 0-indexed (easier lookups within for loops etc), but the
470/// slots themselves retain their 1-indexed slot IDs.
471///
472/// NOTE: This type only includes the ATTRIBUTE data for slots. Markers data is
473/// stored separately in [crate::markers::MarkersFile].
474///
475/// NOTE: I'm aware of the dangerous naming here -- `SlotX` versus `SlotsX` ...
476/// lean into the type system to stop you from using the wrong one! `SlotsX` is
477/// a container, while `SlotX` is the single slot with it's data (naming things
478/// is hard, okay).
479///
480/// To create a new [SlotsAttributes] instance, start with a mutable default
481/// as it will generate the recording buffer flex slots for you!
482/// ```rust
483/// # use ot_tools_io::settings::SlotType;
484/// # use ot_tools_io::projects::SlotsAttributes;
485///
486/// let mut slots = SlotsAttributes::default();
487/// for i in (128..=135_usize) {
488///    let s = slots.flex_slots[i].clone();
489///    assert_eq!(s.unwrap().slot_type, SlotType::Flex)
490/// }
491/// ```
492#[derive(Eq, PartialEq, Clone, Debug, Serialize, IsDefaultCheck)]
493pub struct SlotsAttributes {
494    pub static_slots: Array<Option<SlotAttributes>, 128>,
495    pub flex_slots: Array<Option<SlotAttributes>, 136>,
496}
497
498// YAML/JSON Deserialization for Sample Slots
499// we can get away with just defining map deserialization for YAML/JSON.
500// `ToString`/`FromStr` implementations are currently used for the actual data file.
501impl<'de> Deserialize<'de> for SlotsAttributes {
502    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
503    where
504        D: Deserializer<'de>,
505    {
506        enum Field {
507            Static,
508            Flex,
509        }
510
511        impl<'de> Deserialize<'de> for Field {
512            fn deserialize<D>(deserializer: D) -> Result<Field, D::Error>
513            where
514                D: Deserializer<'de>,
515            {
516                struct FieldVisitor;
517
518                impl Visitor<'_> for FieldVisitor {
519                    type Value = Field;
520
521                    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
522                        formatter.write_str("`static_slots` or `flex_slots`")
523                    }
524
525                    fn visit_str<E>(self, value: &str) -> Result<Field, E>
526                    where
527                        E: de::Error,
528                    {
529                        match value {
530                            "static_slots" => Ok(Field::Static),
531                            "flex_slots" => Ok(Field::Flex),
532                            _ => Err(de::Error::unknown_field(value, FIELDS)),
533                        }
534                    }
535                }
536
537                deserializer.deserialize_identifier(FieldVisitor)
538            }
539        }
540
541        struct SampleSlotsVisitor;
542
543        impl<'de> Visitor<'de> for SampleSlotsVisitor {
544            type Value = SlotsAttributes;
545
546            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
547                formatter.write_str("struct SampleSlots")
548            }
549
550            fn visit_unit<E>(self) -> Result<SlotsAttributes, E> {
551                Ok(SlotsAttributes::default())
552            }
553            fn visit_map<V>(self, mut map: V) -> Result<SlotsAttributes, V::Error>
554            where
555                V: MapAccess<'de>,
556            {
557                let mut static_slots = None;
558                let mut flex_slots = None;
559                while let Some(key) = map.next_key()? {
560                    match key {
561                        Field::Static => {
562                            if static_slots.is_some() {
563                                return Err(de::Error::duplicate_field("static_slots"));
564                            }
565                            static_slots =
566                                Some(map.next_value::<Array<Option<SlotAttributes>, 128>>()?);
567                        }
568                        Field::Flex => {
569                            if flex_slots.is_some() {
570                                return Err(de::Error::duplicate_field("flex_slots"));
571                            }
572                            flex_slots =
573                                Some(map.next_value::<Array<Option<SlotAttributes>, 136>>()?);
574                        }
575                    }
576                }
577                let s_slots =
578                    static_slots.ok_or_else(|| de::Error::missing_field("static_slots"))?;
579
580                let f_slots = flex_slots.ok_or_else(|| de::Error::missing_field("flex_slots"))?;
581
582                let slots = SlotsAttributes {
583                    static_slots: s_slots,
584                    flex_slots: f_slots,
585                };
586
587                Ok(slots)
588            }
589        }
590
591        const FIELDS: &[&str] = &["static_slots", "flex_slots"];
592        deserializer.deserialize_struct("SampleSlots", FIELDS, SampleSlotsVisitor)
593    }
594}
595
596/// Helper function, no public
597fn flex_slot_default_case_switch(i: usize) -> Option<SlotAttributes> {
598    if i <= 127 {
599        None
600    } else {
601        Some(SlotAttributes {
602            slot_type: SlotType::Flex,
603            // WARN: i is a 0-indexed iterable, slot IDs need to be 1-indexed!
604            slot_id: i as u8 + 1,
605            path: None,
606            timestrech_mode: TimeStretchMode::default(),
607            loop_mode: LoopMode::default(),
608            trig_quantization_mode: TrigQuantizationMode::default(),
609            gain: 72,
610            bpm: 2880,
611        })
612    }
613}
614
615impl Default for SlotsAttributes {
616    fn default() -> Self {
617        Self {
618            static_slots: Array(std::array::from_fn(|_| None)),
619            flex_slots: Array(std::array::from_fn(flex_slot_default_case_switch)),
620        }
621    }
622}
623
624impl FromStr for SlotsAttributes {
625    type Err = ProjectParseError;
626
627    fn from_str(s: &str) -> Result<Self, Self::Err> {
628        let footer_stripped = s
629            .strip_suffix("\r\n\r\n############################\r\n\r\n")
630            .ok_or(ProjectParseError::Footer)?;
631
632        let data_window: Vec<&str> = footer_stripped
633            .split("############################\r\n# Samples\r\n############################")
634            .collect();
635
636        let mut samples_string: Vec<&str> = data_window[1].split("[/SAMPLE]").collect();
637        // last one is always a blank string.
638        samples_string.pop();
639
640        // mutate from default as we always need recording buffers populated
641        let mut slots = Self::default();
642
643        for s in &samples_string {
644            // need zero indexing to insert slots into arrays
645            let slot = SlotAttributes::from_str(s)?;
646            let zero_indexed_id = slot.slot_id as usize - 1;
647            match slot.slot_type {
648                SlotType::Static => {
649                    slots.static_slots[zero_indexed_id] = Some(slot);
650                }
651                SlotType::Flex => {
652                    slots.flex_slots[zero_indexed_id] = Some(slot);
653                }
654            }
655        }
656
657        Ok(slots)
658    }
659}
660
661impl fmt::Display for SlotsAttributes {
662    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
663        let mut string_slots: String = "".to_string();
664
665        let slots = vec![self.static_slots.to_vec(), self.flex_slots.to_vec()];
666
667        let slots_concat = itertools::concat(slots).into_iter().flatten();
668
669        for slot in slots_concat {
670            string_slots.push_str(&slot.to_string());
671            string_slots.push_str("\r\n\r\n");
672        }
673        string_slots = string_slots
674            .strip_suffix("\r\n\r\n")
675            .ok_or(fmt::Error)?
676            .to_string();
677        write!(f, "{string_slots:#}")
678    }
679}
680
681#[cfg(test)]
682#[allow(unused_imports)]
683mod test {
684
685    #[test]
686    fn parse_id_001_correct() {
687        let mut hmap = std::collections::HashMap::new();
688        hmap.insert("slot".to_string(), "001".to_string());
689
690        let slot_id = crate::projects::slots::parse_id(&hmap);
691
692        assert_eq!(1, slot_id.unwrap());
693    }
694
695    #[test]
696    fn parse_id_1_correct() {
697        let mut hmap = std::collections::HashMap::new();
698        hmap.insert("slot".to_string(), "1".to_string());
699
700        let slot_id = crate::projects::slots::parse_id(&hmap);
701
702        assert_eq!(1, slot_id.unwrap());
703    }
704
705    #[test]
706    fn parse_id_127_correct() {
707        let mut hmap = std::collections::HashMap::new();
708        hmap.insert("slot".to_string(), "127".to_string());
709
710        let slot_id = crate::projects::slots::parse_id(&hmap);
711
712        assert_eq!(127, slot_id.unwrap());
713    }
714
715    #[test]
716    fn parse_id_099_correct() {
717        let mut hmap = std::collections::HashMap::new();
718        hmap.insert("slot".to_string(), "099".to_string());
719
720        let slot_id = crate::projects::slots::parse_id(&hmap);
721
722        assert_eq!(99, slot_id.unwrap());
723    }
724
725    #[test]
726    fn parse_id_010_correct() {
727        let mut hmap = std::collections::HashMap::new();
728        hmap.insert("slot".to_string(), "010".to_string());
729
730        let slot_id = crate::projects::slots::parse_id(&hmap);
731
732        assert_eq!(10, slot_id.unwrap());
733    }
734
735    #[test]
736    fn test_parse_id_err_bad_value_type_err() {
737        let mut hmap = std::collections::HashMap::new();
738        hmap.insert("slot".to_string(), "AAAA".to_string());
739        let slot_id = crate::projects::slots::parse_id(&hmap);
740        assert!(slot_id.is_err());
741    }
742
743    #[test]
744    fn test_parse_tempo_correct_default() {
745        let mut hmap = std::collections::HashMap::new();
746        hmap.insert("bpmx24".to_string(), "2880".to_string());
747        let r = crate::projects::slots::parse_tempo(&hmap);
748        assert_eq!(2880_u16, r.unwrap());
749    }
750
751    #[test]
752    fn test_parse_tempo_correct_min() {
753        let mut hmap = std::collections::HashMap::new();
754        hmap.insert("bpmx24".to_string(), "720".to_string());
755        let r = crate::projects::slots::parse_tempo(&hmap);
756        assert_eq!(720_u16, r.unwrap());
757    }
758
759    #[test]
760    fn test_parse_tempo_correct_max() {
761        let mut hmap = std::collections::HashMap::new();
762        hmap.insert("bpmx24".to_string(), "7200".to_string());
763        let r = crate::projects::slots::parse_tempo(&hmap);
764        assert_eq!(7200_u16, r.unwrap());
765    }
766
767    #[test]
768    fn test_parse_tempo_bad_value_type_default_return() {
769        let mut hmap = std::collections::HashMap::new();
770        hmap.insert("bpmx24".to_string(), "AAAFSFSFSSFfssafAA".to_string());
771        let r = crate::projects::slots::parse_tempo(&hmap);
772        assert_eq!(r.unwrap(), 2880_u16);
773    }
774
775    #[test]
776    fn test_parse_gain_correct() {
777        let mut hmap = std::collections::HashMap::new();
778        hmap.insert("gain".to_string(), "72".to_string());
779        let r = crate::projects::slots::parse_gain(&hmap);
780        assert_eq!(72, r.unwrap());
781    }
782
783    #[test]
784    fn test_parse_gain_bad_value_type_default_return() {
785        let mut hmap = std::collections::HashMap::new();
786        hmap.insert("gain".to_string(), "AAAFSFSFSSFfssafAA".to_string());
787        let r = crate::projects::slots::parse_gain(&hmap);
788        assert_eq!(r.unwrap(), 72_u8);
789    }
790
791    #[test]
792    fn test_parse_loop_mode_correct_off() {
793        let mut hmap = std::collections::HashMap::new();
794        hmap.insert("loopmode".to_string(), "0".to_string());
795        let r = crate::projects::slots::parse_loop_mode(&hmap);
796        assert_eq!(r.unwrap(), crate::settings::LoopMode::Off);
797    }
798
799    #[test]
800    fn test_parse_loop_mode_correct_normal() {
801        let mut hmap = std::collections::HashMap::new();
802        hmap.insert("loopmode".to_string(), "1".to_string());
803        let r = crate::projects::slots::parse_loop_mode(&hmap);
804        assert_eq!(r.unwrap(), crate::settings::LoopMode::Normal);
805    }
806
807    #[test]
808    fn test_parse_loop_mode_correct_pingpong() {
809        let mut hmap = std::collections::HashMap::new();
810        hmap.insert("loopmode".to_string(), "2".to_string());
811        let r = crate::projects::slots::parse_loop_mode(&hmap);
812        assert_eq!(r.unwrap(), crate::settings::LoopMode::PingPong);
813    }
814
815    #[test]
816    fn test_parse_loop_mode_bad_value_type_default_return() {
817        let mut hmap = std::collections::HashMap::new();
818        hmap.insert("loopmode".to_string(), "AAAFSFSFSSFfssafAA".to_string());
819        let r = crate::projects::slots::parse_loop_mode(&hmap);
820        assert_eq!(r.unwrap(), crate::settings::LoopMode::default());
821    }
822
823    #[test]
824    fn test_parse_tstretch_correct_off() {
825        let mut hmap = std::collections::HashMap::new();
826        hmap.insert("tsmode".to_string(), "0".to_string());
827        let r = crate::projects::slots::parse_tstrech_mode(&hmap);
828        assert_eq!(crate::settings::TimeStretchMode::Off, r.unwrap());
829    }
830
831    #[test]
832    fn test_parse_tstretch_correct_normal() {
833        let mut hmap = std::collections::HashMap::new();
834        hmap.insert("tsmode".to_string(), "2".to_string());
835        let r = crate::projects::slots::parse_tstrech_mode(&hmap);
836        assert_eq!(crate::settings::TimeStretchMode::Normal, r.unwrap());
837    }
838
839    #[test]
840    fn test_parse_tstretch_correct_beat() {
841        let mut hmap = std::collections::HashMap::new();
842        hmap.insert("tsmode".to_string(), "3".to_string());
843        let r = crate::projects::slots::parse_tstrech_mode(&hmap);
844        assert_eq!(crate::settings::TimeStretchMode::Beat, r.unwrap());
845    }
846
847    #[test]
848    fn test_parse_tstretch_bad_value_type_default_return() {
849        let mut hmap = std::collections::HashMap::new();
850        hmap.insert("tsmode".to_string(), "AAAFSFSFSSFfssafAA".to_string());
851        let r = crate::projects::slots::parse_tstrech_mode(&hmap);
852        assert_eq!(r.unwrap(), crate::settings::TimeStretchMode::default());
853    }
854
855    #[test]
856    fn test_parse_tquantize_correct_off() {
857        let mut hmap = std::collections::HashMap::new();
858        hmap.insert("trigquantization".to_string(), "255".to_string());
859        let r = crate::projects::slots::parse_trig_quantize_mode(&hmap);
860        assert_eq!(crate::settings::TrigQuantizationMode::Direct, r.unwrap());
861    }
862
863    #[test]
864    fn test_parse_tquantize_correct_direct() {
865        let mut hmap = std::collections::HashMap::new();
866        hmap.insert("trigquantization".to_string(), "0".to_string());
867        let r = crate::projects::slots::parse_trig_quantize_mode(&hmap);
868        assert_eq!(
869            crate::settings::TrigQuantizationMode::PatternLength,
870            r.unwrap()
871        );
872    }
873
874    #[test]
875    fn test_parse_tquantize_correct_onestep() {
876        let mut hmap = std::collections::HashMap::new();
877        hmap.insert("trigquantization".to_string(), "1".to_string());
878        let r = crate::projects::slots::parse_trig_quantize_mode(&hmap);
879        assert_eq!(crate::settings::TrigQuantizationMode::OneStep, r.unwrap());
880    }
881
882    #[test]
883    fn test_parse_tquantize_correct_twostep() {
884        let mut hmap = std::collections::HashMap::new();
885        hmap.insert("trigquantization".to_string(), "2".to_string());
886        let r = crate::projects::slots::parse_trig_quantize_mode(&hmap);
887        assert_eq!(crate::settings::TrigQuantizationMode::TwoSteps, r.unwrap());
888    }
889
890    #[test]
891    fn test_parse_tquantize_correct_threestep() {
892        let mut hmap = std::collections::HashMap::new();
893        hmap.insert("trigquantization".to_string(), "3".to_string());
894        let r = crate::projects::slots::parse_trig_quantize_mode(&hmap);
895        assert_eq!(
896            crate::settings::TrigQuantizationMode::ThreeSteps,
897            r.unwrap()
898        );
899    }
900
901    #[test]
902    fn test_parse_tquantize_correct_fourstep() {
903        let mut hmap = std::collections::HashMap::new();
904        hmap.insert("trigquantization".to_string(), "4".to_string());
905        let r = crate::projects::slots::parse_trig_quantize_mode(&hmap);
906        assert_eq!(crate::settings::TrigQuantizationMode::FourSteps, r.unwrap());
907    }
908
909    // i'm not going to test every single option. we do that already elsewhere.
910
911    #[test]
912    fn test_parse_tquantize_bad_value_type_default_return() {
913        let mut hmap = std::collections::HashMap::new();
914        hmap.insert(
915            "trigquantization".to_string(),
916            "AAAFSFSFSSFfssafAA".to_string(),
917        );
918        let r = crate::projects::slots::parse_trig_quantize_mode(&hmap);
919        assert_eq!(r.unwrap(), crate::settings::TrigQuantizationMode::default());
920    }
921
922    use std::path::PathBuf;
923    #[test]
924    fn test_parse_path_good_utf8_value() {
925        let test_path = "../AUDIO/some/file.wav";
926
927        let mut hmap = std::collections::HashMap::new();
928        hmap.insert("path".to_string(), test_path.to_string());
929        let r = crate::projects::slots::parse_path(&hmap);
930        assert_eq!(r.unwrap(), PathBuf::from(test_path));
931    }
932
933    #[test]
934    fn test_parse_empty_path() {
935        let test_path = "";
936
937        let mut hmap = std::collections::HashMap::new();
938        hmap.insert("path".to_string(), test_path.to_string());
939        let r = crate::projects::slots::parse_path(&hmap);
940        assert_eq!(r.unwrap(), PathBuf::from(test_path));
941    }
942
943    #[test]
944    fn test_parse_non_utf8_path() {
945        let test_path = "../AUDIO/🇯🇲something.wav";
946
947        let mut hmap = std::collections::HashMap::new();
948        hmap.insert("path".to_string(), test_path.to_string());
949        let r = crate::projects::slots::parse_path(&hmap);
950        assert_eq!(r.unwrap(), PathBuf::from(test_path));
951    }
952}