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
177impl TryFrom<&HashMap<String, String>> for SlotAttributes {
178    type Error = ProjectParseError;
179    fn try_from(value: &HashMap<String, String>) -> Result<Self, Self::Error> {
180        let slot_id = parse_id(value)?;
181
182        let sample_slot_type = value
183            .get("type")
184            .ok_or(ProjectParseError::HashMap)?
185            .to_string();
186        let slot_type = SlotType::try_from(sample_slot_type)?;
187
188        let path_str = value.get("path").ok_or(ProjectParseError::HashMap)?;
189        let path = PathBuf::from_str(path_str).map_err(|_| ProjectParseError::String)?;
190
191        let loop_mode = parse_loop_mode(value)?;
192        let timestrech_mode = parse_tstrech_mode(value)?;
193        let trig_quantization_mode = parse_trig_quantize_mode(value)?;
194        let gain = parse_gain(value)?;
195        let bpm = parse_tempo(value)?;
196
197        let sample_struct = Self {
198            slot_type,
199            slot_id,
200            path: if path != PathBuf::from("") {
201                Some(path)
202            } else {
203                None
204            },
205            timestrech_mode,
206            loop_mode,
207            trig_quantization_mode,
208            gain,
209            bpm,
210        };
211
212        Ok(sample_struct)
213    }
214}
215
216impl FromStr for SlotAttributes {
217    type Err = ProjectParseError;
218
219    fn from_str(s: &str) -> Result<Self, Self::Err> {
220        let k_v: Vec<Vec<&str>> = s
221            .strip_prefix("\r\n\r\n[SAMPLE]\r\n")
222            .ok_or(ProjectParseError::HashMap)?
223            .strip_suffix("\r\n")
224            .ok_or(ProjectParseError::HashMap)?
225            .split("\r\n")
226            .map(|x: &str| x.split('=').collect_vec())
227            .filter(|x: &Vec<&str>| x.len() == 2)
228            .collect_vec();
229
230        let mut hmap: HashMap<String, String> = HashMap::new();
231        for key_value_pair in k_v {
232            hmap.insert(
233                key_value_pair[0].to_string().to_lowercase(),
234                key_value_pair[1].to_string(),
235            );
236        }
237
238        let sample_struct = SlotAttributes::try_from(&hmap)?;
239        Ok(sample_struct)
240    }
241}
242
243impl fmt::Display for SlotAttributes {
244    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
245        let mut s = "[SAMPLE]\r\n".to_string();
246        s.push_str(&format!("TYPE={}", self.slot_type));
247        s.push_str("\r\n");
248        // NOTE: Slot ID data is always prefixed with leading zeros
249        s.push_str(format!("SLOT={:0>3}", self.slot_id).as_str());
250        s.push_str("\r\n");
251        // NOTE: Handle recording buffers having empty paths
252        if let Some(path) = &self.path {
253            /*
254            HACK: Remove escape characters from the path string.
255
256            A path like `my\file.wav` ends up like `my\\\\[...loads more \ chars...]\\\file.wav`.
257            The Octatrack will attempt to load the project and then a catastrophic error message pops up.
258            Helpfully, it will have made changes to the `project.work` file already which means you
259            have to manually fix it yourself.
260
261            This looks like some recursive injection of escape characters during parsing of the PATH field.
262            Basically ... don't use the `\` escape character in path names!
263            */
264            s.push_str(
265                format!("PATH={path:#?}")
266                    .replace('"', "") // should not have quotes on PATH fields
267                    .replace("\\", "") // ^ need to remove escape chars (`\`)
268                    .as_str(),
269            );
270        } else {
271            s.push_str("PATH=");
272        }
273        s.push_str("\r\n");
274        s.push_str(format!("BPMx24={}", self.bpm).as_str());
275        s.push_str("\r\n");
276        s.push_str(format!("TSMODE={}", self.timestrech_mode as u8).as_str());
277        s.push_str("\r\n");
278        s.push_str(format!("LOOPMODE={}", self.loop_mode as u8).as_str());
279        s.push_str("\r\n");
280        s.push_str(format!("GAIN={}", self.gain).as_str());
281        s.push_str("\r\n");
282        s.push_str(format!("TRIGQUANTIZATION={}", self.trig_quantization_mode as u8).as_str());
283        s.push_str("\r\n[/SAMPLE]");
284        write!(f, "{s:#}")
285    }
286}
287
288// YAML/JSON Deserialization for Sample Slot
289// we can get away with just defining map deserialization for YAML/JSON.
290// `ToString`/`FromStr` implementations are currently used for the actual data file.
291impl<'de> Deserialize<'de> for SlotAttributes {
292    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
293    where
294        D: Deserializer<'de>,
295    {
296        enum Field {
297            SlotType,
298            SlotId,
299            Path,
300            Timestretch,
301            Loop,
302            Quant,
303            Gain,
304            Bpm,
305        }
306
307        // TODO: FIELDS_MAP: Tuple array
308        const FIELDS: &[&str] = &[
309            "slot_type",
310            "slot_id",
311            "path",
312            "timestrech_mode",
313            "loop_mode",
314            "trig_quantization_mode",
315            "gain",
316            "bpm",
317        ];
318
319        impl<'de> Deserialize<'de> for Field {
320            fn deserialize<D>(deserializer: D) -> Result<Field, D::Error>
321            where
322                D: Deserializer<'de>,
323            {
324                struct FieldVisitor;
325
326                impl Visitor<'_> for FieldVisitor {
327                    type Value = Field;
328
329                    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
330                        formatter.write_str(
331                            FIELDS
332                                .iter()
333                                .map(|x| format!["`{x}`"])
334                                .collect::<Vec<_>>()
335                                .join(" or ")
336                                .as_str(),
337                        )
338                    }
339
340                    fn visit_str<E>(self, value: &str) -> Result<Field, E>
341                    where
342                        E: de::Error,
343                    {
344                        match value {
345                            "slot_type" => Ok(Field::SlotType),
346                            "slot_id" => Ok(Field::SlotId),
347                            "path" => Ok(Field::Path),
348                            "timestrech_mode" => Ok(Field::Timestretch),
349                            "loop_mode" => Ok(Field::Loop),
350                            "trig_quantization_mode" => Ok(Field::Quant),
351                            "gain" => Ok(Field::Gain),
352                            "bpm" => Ok(Field::Bpm),
353                            _ => Err(de::Error::unknown_field(value, FIELDS)),
354                        }
355                    }
356                }
357
358                deserializer.deserialize_identifier(FieldVisitor)
359            }
360        }
361
362        struct SlotAttributesVisitor;
363
364        impl<'de> Visitor<'de> for SlotAttributesVisitor {
365            type Value = SlotAttributes;
366
367            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
368                formatter.write_str("struct SlotAttributes")
369            }
370
371            fn visit_map<V>(self, mut map: V) -> Result<SlotAttributes, V::Error>
372            where
373                V: MapAccess<'de>,
374            {
375                let mut slot_type = None;
376                let mut slot_id = None;
377                let mut path = None;
378                let mut timestretch_mode = None;
379                let mut loop_mode = None;
380                let mut trig_quantization_mode = None;
381                let mut gain = None;
382                let mut bpm = None;
383
384                while let Some(key) = map.next_key()? {
385                    match key {
386                        Field::SlotType => {
387                            if slot_type.is_some() {
388                                return Err(de::Error::duplicate_field("slot_type"));
389                            }
390                            slot_type = Some(map.next_value::<SlotType>()?);
391                        }
392                        Field::SlotId => {
393                            if slot_id.is_some() {
394                                return Err(de::Error::duplicate_field("slot_id"));
395                            }
396                            slot_id = Some(map.next_value::<u8>()?);
397                        }
398                        Field::Path => {
399                            if path.is_some() {
400                                return Err(de::Error::duplicate_field("path"));
401                            }
402                            path = Some(map.next_value::<PathBuf>()?);
403                        }
404                        Field::Timestretch => {
405                            if timestretch_mode.is_some() {
406                                return Err(de::Error::duplicate_field("timestretch_mode"));
407                            }
408                            timestretch_mode = Some(map.next_value::<TimeStretchMode>()?);
409                        }
410                        Field::Loop => {
411                            if loop_mode.is_some() {
412                                return Err(de::Error::duplicate_field("loop_mode"));
413                            }
414                            loop_mode = Some(map.next_value::<LoopMode>()?);
415                        }
416                        Field::Quant => {
417                            if trig_quantization_mode.is_some() {
418                                return Err(de::Error::duplicate_field("trig_quantization_mode"));
419                            }
420                            trig_quantization_mode =
421                                Some(map.next_value::<TrigQuantizationMode>()?);
422                        }
423                        Field::Gain => {
424                            if gain.is_some() {
425                                return Err(de::Error::duplicate_field("gain"));
426                            }
427                            gain = Some(map.next_value::<u8>()?);
428                        }
429                        Field::Bpm => {
430                            if bpm.is_some() {
431                                return Err(de::Error::duplicate_field("bpm"));
432                            }
433                            bpm = Some(map.next_value::<u16>()?);
434                        }
435                    }
436                }
437
438                let slot = SlotAttributes {
439                    slot_type: slot_type.ok_or_else(|| de::Error::missing_field("slot_type"))?,
440                    slot_id: slot_id.ok_or_else(|| de::Error::missing_field("slot_type"))?,
441                    path, // allowed to be missing to handle recording buffer empty paths
442                    timestrech_mode: timestretch_mode
443                        .ok_or_else(|| de::Error::missing_field("trimstretch_mode"))?,
444                    loop_mode: loop_mode.ok_or_else(|| de::Error::missing_field("loop_mode"))?,
445                    trig_quantization_mode: trig_quantization_mode
446                        .ok_or_else(|| de::Error::missing_field("trig_quantization_mode"))?,
447                    gain: gain.ok_or_else(|| de::Error::missing_field("gain"))?,
448                    bpm: bpm.ok_or_else(|| de::Error::missing_field("bpm"))?,
449                };
450
451                Ok(slot)
452            }
453        }
454
455        deserializer.deserialize_struct("SampleSlot", FIELDS, SlotAttributesVisitor)
456    }
457}
458
459/// Container type for all sample slots.
460///
461/// The `project.*` data files store all sample slots together in a single 1-indexed array.
462/// This tends to lead to a lot of additional yak-shaving when interacting with sample slots.
463/// So this type models the octatrack's UI, displaying slots in 2x arrays: Flex and Static.
464/// These individual arrays are 0-indexed (easier lookups within for loops etc), but the
465/// slots themselves retain their 1-indexed slot IDs.
466///
467/// NOTE: This type only includes the ATTRIBUTE data for slots. Markers data is
468/// stored separately in [crate::markers::MarkersFile].
469///
470/// NOTE: I'm aware of the dangerous naming here -- `SlotX` versus `SlotsX` ...
471/// lean into the type system to stop you from using the wrong one! `SlotsX` is
472/// a container, while `SlotX` is the single slot with it's data (naming things
473/// is hard, okay).
474///
475/// To create a new [SlotsAttributes] instance, start with a mutable default
476/// as it will generate the recording buffer flex slots for you!
477/// ```rust
478/// # use ot_tools_io::settings::SlotType;
479/// # use ot_tools_io::projects::SlotsAttributes;
480///
481/// let mut slots = SlotsAttributes::default();
482/// for i in (128..=135_usize) {
483///    let s = slots.flex_slots[i].clone();
484///    assert_eq!(s.unwrap().slot_type, SlotType::Flex)
485/// }
486/// ```
487#[derive(Eq, PartialEq, Clone, Debug, Serialize, IsDefaultCheck)]
488pub struct SlotsAttributes {
489    pub static_slots: Array<Option<SlotAttributes>, 128>,
490    pub flex_slots: Array<Option<SlotAttributes>, 136>,
491}
492
493// YAML/JSON Deserialization for Sample Slots
494// we can get away with just defining map deserialization for YAML/JSON.
495// `ToString`/`FromStr` implementations are currently used for the actual data file.
496impl<'de> Deserialize<'de> for SlotsAttributes {
497    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
498    where
499        D: Deserializer<'de>,
500    {
501        enum Field {
502            Static,
503            Flex,
504        }
505
506        impl<'de> Deserialize<'de> for Field {
507            fn deserialize<D>(deserializer: D) -> Result<Field, D::Error>
508            where
509                D: Deserializer<'de>,
510            {
511                struct FieldVisitor;
512
513                impl Visitor<'_> for FieldVisitor {
514                    type Value = Field;
515
516                    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
517                        formatter.write_str("`static_slots` or `flex_slots`")
518                    }
519
520                    fn visit_str<E>(self, value: &str) -> Result<Field, E>
521                    where
522                        E: de::Error,
523                    {
524                        match value {
525                            "static_slots" => Ok(Field::Static),
526                            "flex_slots" => Ok(Field::Flex),
527                            _ => Err(de::Error::unknown_field(value, FIELDS)),
528                        }
529                    }
530                }
531
532                deserializer.deserialize_identifier(FieldVisitor)
533            }
534        }
535
536        struct SampleSlotsVisitor;
537
538        impl<'de> Visitor<'de> for SampleSlotsVisitor {
539            type Value = SlotsAttributes;
540
541            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
542                formatter.write_str("struct SampleSlots")
543            }
544
545            fn visit_unit<E>(self) -> Result<SlotsAttributes, E> {
546                Ok(SlotsAttributes::default())
547            }
548            fn visit_map<V>(self, mut map: V) -> Result<SlotsAttributes, V::Error>
549            where
550                V: MapAccess<'de>,
551            {
552                let mut static_slots = None;
553                let mut flex_slots = None;
554                while let Some(key) = map.next_key()? {
555                    match key {
556                        Field::Static => {
557                            if static_slots.is_some() {
558                                return Err(de::Error::duplicate_field("static_slots"));
559                            }
560                            static_slots =
561                                Some(map.next_value::<Array<Option<SlotAttributes>, 128>>()?);
562                        }
563                        Field::Flex => {
564                            if flex_slots.is_some() {
565                                return Err(de::Error::duplicate_field("flex_slots"));
566                            }
567                            flex_slots =
568                                Some(map.next_value::<Array<Option<SlotAttributes>, 136>>()?);
569                        }
570                    }
571                }
572                let s_slots =
573                    static_slots.ok_or_else(|| de::Error::missing_field("static_slots"))?;
574
575                let f_slots = flex_slots.ok_or_else(|| de::Error::missing_field("flex_slots"))?;
576
577                let slots = SlotsAttributes {
578                    static_slots: s_slots,
579                    flex_slots: f_slots,
580                };
581
582                Ok(slots)
583            }
584        }
585
586        const FIELDS: &[&str] = &["static_slots", "flex_slots"];
587        deserializer.deserialize_struct("SampleSlots", FIELDS, SampleSlotsVisitor)
588    }
589}
590
591/// Helper function, no public
592fn flex_slot_default_case_switch(i: usize) -> Option<SlotAttributes> {
593    if i <= 127 {
594        None
595    } else {
596        Some(SlotAttributes {
597            slot_type: SlotType::Flex,
598            // WARN: i is a 0-indexed iterable, slot IDs need to be 1-indexed!
599            slot_id: i as u8 + 1,
600            path: None,
601            timestrech_mode: TimeStretchMode::default(),
602            loop_mode: LoopMode::default(),
603            trig_quantization_mode: TrigQuantizationMode::default(),
604            gain: 72,
605            bpm: 2880,
606        })
607    }
608}
609
610impl Default for SlotsAttributes {
611    fn default() -> Self {
612        Self {
613            static_slots: Array(std::array::from_fn(|_| None)),
614            flex_slots: Array(std::array::from_fn(flex_slot_default_case_switch)),
615        }
616    }
617}
618
619impl FromStr for SlotsAttributes {
620    type Err = ProjectParseError;
621
622    fn from_str(s: &str) -> Result<Self, Self::Err> {
623        let footer_stripped = s
624            .strip_suffix("\r\n\r\n############################\r\n\r\n")
625            .ok_or(ProjectParseError::Footer)?;
626
627        let data_window: Vec<&str> = footer_stripped
628            .split("############################\r\n# Samples\r\n############################")
629            .collect();
630
631        let mut samples_string: Vec<&str> = data_window[1].split("[/SAMPLE]").collect();
632        // last one is always a blank string.
633        samples_string.pop();
634
635        // mutate from default as we always need recording buffers populated
636        let mut slots = Self::default();
637
638        for s in &samples_string {
639            // need zero indexing to insert slots into arrays
640            let slot = SlotAttributes::from_str(s)?;
641            let zero_indexed_id = slot.slot_id as usize - 1;
642            match slot.slot_type {
643                SlotType::Static => {
644                    slots.static_slots[zero_indexed_id] = Some(slot);
645                }
646                SlotType::Flex => {
647                    slots.flex_slots[zero_indexed_id] = Some(slot);
648                }
649            }
650        }
651
652        Ok(slots)
653    }
654}
655
656impl fmt::Display for SlotsAttributes {
657    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
658        let mut string_slots: String = "".to_string();
659
660        let slots = vec![self.static_slots.to_vec(), self.flex_slots.to_vec()];
661
662        let slots_concat = itertools::concat(slots).into_iter().flatten();
663
664        for slot in slots_concat {
665            string_slots.push_str(&slot.to_string());
666            string_slots.push_str("\r\n\r\n");
667        }
668        string_slots = string_slots
669            .strip_suffix("\r\n\r\n")
670            .ok_or(fmt::Error)?
671            .to_string();
672        write!(f, "{string_slots:#}")
673    }
674}
675
676#[cfg(test)]
677#[allow(unused_imports)]
678mod test {
679
680    #[test]
681    fn parse_id_001_correct() {
682        let mut hmap = std::collections::HashMap::new();
683        hmap.insert("slot".to_string(), "001".to_string());
684
685        let slot_id = crate::projects::slots::parse_id(&hmap);
686
687        assert_eq!(1, slot_id.unwrap());
688    }
689
690    #[test]
691    fn parse_id_1_correct() {
692        let mut hmap = std::collections::HashMap::new();
693        hmap.insert("slot".to_string(), "1".to_string());
694
695        let slot_id = crate::projects::slots::parse_id(&hmap);
696
697        assert_eq!(1, slot_id.unwrap());
698    }
699
700    #[test]
701    fn parse_id_127_correct() {
702        let mut hmap = std::collections::HashMap::new();
703        hmap.insert("slot".to_string(), "127".to_string());
704
705        let slot_id = crate::projects::slots::parse_id(&hmap);
706
707        assert_eq!(127, slot_id.unwrap());
708    }
709
710    #[test]
711    fn parse_id_099_correct() {
712        let mut hmap = std::collections::HashMap::new();
713        hmap.insert("slot".to_string(), "099".to_string());
714
715        let slot_id = crate::projects::slots::parse_id(&hmap);
716
717        assert_eq!(99, slot_id.unwrap());
718    }
719
720    #[test]
721    fn parse_id_010_correct() {
722        let mut hmap = std::collections::HashMap::new();
723        hmap.insert("slot".to_string(), "010".to_string());
724
725        let slot_id = crate::projects::slots::parse_id(&hmap);
726
727        assert_eq!(10, slot_id.unwrap());
728    }
729
730    #[test]
731    fn test_parse_id_err_bad_value_type_err() {
732        let mut hmap = std::collections::HashMap::new();
733        hmap.insert("slot".to_string(), "AAAA".to_string());
734        let slot_id = crate::projects::slots::parse_id(&hmap);
735        assert!(slot_id.is_err());
736    }
737
738    #[test]
739    fn test_parse_tempo_correct_default() {
740        let mut hmap = std::collections::HashMap::new();
741        hmap.insert("bpmx24".to_string(), "2880".to_string());
742        let r = crate::projects::slots::parse_tempo(&hmap);
743        assert_eq!(2880_u16, r.unwrap());
744    }
745
746    #[test]
747    fn test_parse_tempo_correct_min() {
748        let mut hmap = std::collections::HashMap::new();
749        hmap.insert("bpmx24".to_string(), "720".to_string());
750        let r = crate::projects::slots::parse_tempo(&hmap);
751        assert_eq!(720_u16, r.unwrap());
752    }
753
754    #[test]
755    fn test_parse_tempo_correct_max() {
756        let mut hmap = std::collections::HashMap::new();
757        hmap.insert("bpmx24".to_string(), "7200".to_string());
758        let r = crate::projects::slots::parse_tempo(&hmap);
759        assert_eq!(7200_u16, r.unwrap());
760    }
761
762    #[test]
763    fn test_parse_tempo_bad_value_type_default_return() {
764        let mut hmap = std::collections::HashMap::new();
765        hmap.insert("bpmx24".to_string(), "AAAFSFSFSSFfssafAA".to_string());
766        let r = crate::projects::slots::parse_tempo(&hmap);
767        assert_eq!(r.unwrap(), 2880_u16);
768    }
769
770    #[test]
771    fn test_parse_gain_correct() {
772        let mut hmap = std::collections::HashMap::new();
773        hmap.insert("gain".to_string(), "72".to_string());
774        let r = crate::projects::slots::parse_gain(&hmap);
775        assert_eq!(72, r.unwrap());
776    }
777
778    #[test]
779    fn test_parse_gain_bad_value_type_default_return() {
780        let mut hmap = std::collections::HashMap::new();
781        hmap.insert("gain".to_string(), "AAAFSFSFSSFfssafAA".to_string());
782        let r = crate::projects::slots::parse_gain(&hmap);
783        assert_eq!(r.unwrap(), 72_u8);
784    }
785
786    #[test]
787    fn test_parse_loop_mode_correct_off() {
788        let mut hmap = std::collections::HashMap::new();
789        hmap.insert("loopmode".to_string(), "0".to_string());
790        let r = crate::projects::slots::parse_loop_mode(&hmap);
791        assert_eq!(r.unwrap(), crate::settings::LoopMode::Off);
792    }
793
794    #[test]
795    fn test_parse_loop_mode_correct_normal() {
796        let mut hmap = std::collections::HashMap::new();
797        hmap.insert("loopmode".to_string(), "1".to_string());
798        let r = crate::projects::slots::parse_loop_mode(&hmap);
799        assert_eq!(r.unwrap(), crate::settings::LoopMode::Normal);
800    }
801
802    #[test]
803    fn test_parse_loop_mode_correct_pingpong() {
804        let mut hmap = std::collections::HashMap::new();
805        hmap.insert("loopmode".to_string(), "2".to_string());
806        let r = crate::projects::slots::parse_loop_mode(&hmap);
807        assert_eq!(r.unwrap(), crate::settings::LoopMode::PingPong);
808    }
809
810    #[test]
811    fn test_parse_loop_mode_bad_value_type_default_return() {
812        let mut hmap = std::collections::HashMap::new();
813        hmap.insert("loopmode".to_string(), "AAAFSFSFSSFfssafAA".to_string());
814        let r = crate::projects::slots::parse_loop_mode(&hmap);
815        assert_eq!(r.unwrap(), crate::settings::LoopMode::default());
816    }
817
818    #[test]
819    fn test_parse_tstretch_correct_off() {
820        let mut hmap = std::collections::HashMap::new();
821        hmap.insert("tsmode".to_string(), "0".to_string());
822        let r = crate::projects::slots::parse_tstrech_mode(&hmap);
823        assert_eq!(crate::settings::TimeStretchMode::Off, r.unwrap());
824    }
825
826    #[test]
827    fn test_parse_tstretch_correct_normal() {
828        let mut hmap = std::collections::HashMap::new();
829        hmap.insert("tsmode".to_string(), "2".to_string());
830        let r = crate::projects::slots::parse_tstrech_mode(&hmap);
831        assert_eq!(crate::settings::TimeStretchMode::Normal, r.unwrap());
832    }
833
834    #[test]
835    fn test_parse_tstretch_correct_beat() {
836        let mut hmap = std::collections::HashMap::new();
837        hmap.insert("tsmode".to_string(), "3".to_string());
838        let r = crate::projects::slots::parse_tstrech_mode(&hmap);
839        assert_eq!(crate::settings::TimeStretchMode::Beat, r.unwrap());
840    }
841
842    #[test]
843    fn test_parse_tstretch_bad_value_type_default_return() {
844        let mut hmap = std::collections::HashMap::new();
845        hmap.insert("tsmode".to_string(), "AAAFSFSFSSFfssafAA".to_string());
846        let r = crate::projects::slots::parse_tstrech_mode(&hmap);
847        assert_eq!(r.unwrap(), crate::settings::TimeStretchMode::default());
848    }
849
850    #[test]
851    fn test_parse_tquantize_correct_off() {
852        let mut hmap = std::collections::HashMap::new();
853        hmap.insert("trigquantization".to_string(), "255".to_string());
854        let r = crate::projects::slots::parse_trig_quantize_mode(&hmap);
855        assert_eq!(crate::settings::TrigQuantizationMode::Direct, r.unwrap());
856    }
857
858    #[test]
859    fn test_parse_tquantize_correct_direct() {
860        let mut hmap = std::collections::HashMap::new();
861        hmap.insert("trigquantization".to_string(), "0".to_string());
862        let r = crate::projects::slots::parse_trig_quantize_mode(&hmap);
863        assert_eq!(
864            crate::settings::TrigQuantizationMode::PatternLength,
865            r.unwrap()
866        );
867    }
868
869    #[test]
870    fn test_parse_tquantize_correct_onestep() {
871        let mut hmap = std::collections::HashMap::new();
872        hmap.insert("trigquantization".to_string(), "1".to_string());
873        let r = crate::projects::slots::parse_trig_quantize_mode(&hmap);
874        assert_eq!(crate::settings::TrigQuantizationMode::OneStep, r.unwrap());
875    }
876
877    #[test]
878    fn test_parse_tquantize_correct_twostep() {
879        let mut hmap = std::collections::HashMap::new();
880        hmap.insert("trigquantization".to_string(), "2".to_string());
881        let r = crate::projects::slots::parse_trig_quantize_mode(&hmap);
882        assert_eq!(crate::settings::TrigQuantizationMode::TwoSteps, r.unwrap());
883    }
884
885    #[test]
886    fn test_parse_tquantize_correct_threestep() {
887        let mut hmap = std::collections::HashMap::new();
888        hmap.insert("trigquantization".to_string(), "3".to_string());
889        let r = crate::projects::slots::parse_trig_quantize_mode(&hmap);
890        assert_eq!(
891            crate::settings::TrigQuantizationMode::ThreeSteps,
892            r.unwrap()
893        );
894    }
895
896    #[test]
897    fn test_parse_tquantize_correct_fourstep() {
898        let mut hmap = std::collections::HashMap::new();
899        hmap.insert("trigquantization".to_string(), "4".to_string());
900        let r = crate::projects::slots::parse_trig_quantize_mode(&hmap);
901        assert_eq!(crate::settings::TrigQuantizationMode::FourSteps, r.unwrap());
902    }
903
904    // i'm not going to test every single option. we do that already elsewhere.
905
906    #[test]
907    fn test_parse_tquantize_bad_value_type_default_return() {
908        let mut hmap = std::collections::HashMap::new();
909        hmap.insert(
910            "trigquantization".to_string(),
911            "AAAFSFSFSSFfssafAA".to_string(),
912        );
913        let r = crate::projects::slots::parse_trig_quantize_mode(&hmap);
914        assert_eq!(r.unwrap(), crate::settings::TrigQuantizationMode::default());
915    }
916}