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*/
29
30use crate::{
31    projects::{parse_hashmap_string_value, FromHashMap},
32    IsDefault, LoopMode, OptionEnumValueConvert, OtToolsIoErrors, RBoxErr, TimeStretchMode,
33    TrigQuantizationMode,
34};
35use itertools::Itertools;
36use serde::de::{self, Deserializer, MapAccess, Visitor};
37use serde::{Deserialize, Serialize};
38use serde_big_array::Array;
39use std::fmt;
40use std::{collections::HashMap, 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    ) -> RBoxErr<Self> {
104        if let Some(tempo) = bpm {
105            if !(720..=7200).contains(&tempo) {
106                return Err(crate::samples::SampleSettingsErrors::TempoOutOfBounds.into());
107            }
108        }
109        if let Some(amp) = gain {
110            if !(24..=120).contains(&amp) {
111                return Err(crate::samples::SampleSettingsErrors::GainOutOfBounds.into());
112            }
113        }
114
115        Ok(Self {
116            slot_type,
117            slot_id,
118            path,
119            timestrech_mode: timestretch_mode.unwrap_or_default(),
120            loop_mode: loop_mode.unwrap_or_default(),
121            trig_quantization_mode: trig_quantization_mode.unwrap_or_default(),
122            gain: gain.unwrap_or(72),
123            bpm: bpm.unwrap_or(2880),
124        })
125    }
126}
127
128fn parse_id(hmap: &HashMap<String, String>) -> RBoxErr<u8> {
129    let x = parse_hashmap_string_value::<u8>(hmap, "slot", None)?;
130    Ok(x)
131}
132
133fn parse_loop_mode(hmap: &HashMap<String, String>) -> RBoxErr<LoopMode> {
134    // TODO: generic trait definition instead of bounded trait (can't remember the name for the
135    //       technique i used here but i know what i mean) -- allow for string value return etc
136    let default = LoopMode::default().value()?;
137    let default_str = format!["{default}"];
138
139    let x = parse_hashmap_string_value::<u32>(hmap, "loopmode", Some(default_str.as_str()))
140        .unwrap_or(default);
141    LoopMode::from_value(&x)
142}
143
144fn parse_tstrech_mode(hmap: &HashMap<String, String>) -> RBoxErr<TimeStretchMode> {
145    // TODO: generic trait definition instead of bounded trait (can't remember the name for the
146    //       technique i used here but i know what i mean) -- allow for string value return etc
147    let default = TimeStretchMode::default().value()?;
148    let default_str = format!["{default}"];
149
150    let x = parse_hashmap_string_value::<u32>(hmap, "tsmode", Some(default_str.as_str()))
151        .unwrap_or(default);
152    TimeStretchMode::from_value(&x)
153}
154
155fn parse_trig_quantize_mode(hmap: &HashMap<String, String>) -> RBoxErr<TrigQuantizationMode> {
156    // TODO: generic trait definition instead of bounded trait (can't remember the name for the
157    //       technique i used here but i know what i mean) -- allow for string value return etc
158    let default = TrigQuantizationMode::default().value()?;
159    let default_str = format!["{default}"];
160    let x = parse_hashmap_string_value::<u32>(hmap, "trigquantization", Some(default_str.as_str()))
161        .unwrap_or(default);
162    TrigQuantizationMode::from_value(&x)
163}
164
165fn parse_gain(hmap: &HashMap<String, String>) -> RBoxErr<u8> {
166    let x = parse_hashmap_string_value::<u8>(hmap, "gain", Some("72")).unwrap_or(72_u8);
167    Ok(x)
168}
169
170fn parse_tempo(hmap: &HashMap<String, String>) -> RBoxErr<u16> {
171    let x = parse_hashmap_string_value::<u16>(hmap, "bpmx24", Some("2880")).unwrap_or(2880_u16);
172    Ok(x)
173}
174
175// cannot use FromProjectStringData because it expects a lone Self result, rather than a Vec.
176impl FromHashMap for SlotAttributes {
177    type A = String;
178    type B = String;
179    type T = SlotAttributes;
180
181    fn from_hashmap(hmap: &HashMap<Self::A, Self::B>) -> RBoxErr<Self::T> {
182        let slot_id = parse_id(hmap)?;
183        let sample_slot_type = hmap
184            .get("type")
185            .ok_or(OtToolsIoErrors::ProjectSampleSlotParsingError)?
186            .to_string();
187        let slot_type = SlotType::from_value(&sample_slot_type)?;
188        let path = PathBuf::from_str(
189            hmap.get("path")
190                .ok_or(OtToolsIoErrors::ProjectSampleSlotParsingError)?,
191        )?;
192        let loop_mode = parse_loop_mode(hmap)?;
193        let timestrech_mode = parse_tstrech_mode(hmap)?;
194        let trig_quantization_mode = parse_trig_quantize_mode(hmap)?;
195        let gain = parse_gain(hmap)?;
196        let bpm = parse_tempo(hmap)?;
197
198        let sample_struct = Self {
199            slot_type,
200            slot_id,
201            path: if path != PathBuf::from("") {
202                Some(path)
203            } else {
204                None
205            },
206            timestrech_mode,
207            loop_mode,
208            trig_quantization_mode,
209            gain,
210            bpm,
211        };
212
213        Ok(sample_struct)
214    }
215}
216
217impl FromStr for SlotAttributes {
218    type Err = Box<dyn std::error::Error>;
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(OtToolsIoErrors::ProjectSampleSlotParsingError)?
223            .strip_suffix("\r\n")
224            .ok_or(OtToolsIoErrors::ProjectSampleSlotParsingError)?
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::from_hashmap(&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.value().map_err(|_| fmt::Error)?).as_str());
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(
277            format!(
278                "TSMODE={}",
279                self.timestrech_mode.value().map_err(|_| fmt::Error)?
280            )
281            .as_str(),
282        );
283        s.push_str("\r\n");
284        s.push_str(
285            format!(
286                "LOOPMODE={}",
287                self.loop_mode.value().map_err(|_| fmt::Error)?
288            )
289            .as_str(),
290        );
291        s.push_str("\r\n");
292        s.push_str(format!("GAIN={}", self.gain).as_str());
293        s.push_str("\r\n");
294        s.push_str(
295            format!(
296                "TRIGQUANTIZATION={}",
297                self.trig_quantization_mode
298                    .value()
299                    .map_err(|_| fmt::Error)?
300            )
301            .as_str(),
302        );
303        s.push_str("\r\n[/SAMPLE]");
304        write!(f, "{s:#}")
305    }
306}
307
308// YAML/JSON Deserialization for Sample Slot
309// we can get away with just defining map deserialization for YAML/JSON.
310// `ToString`/`FromStr` implementations are currently used for the actual data file.
311impl<'de> Deserialize<'de> for SlotAttributes {
312    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
313    where
314        D: Deserializer<'de>,
315    {
316        enum Field {
317            SlotType,
318            SlotId,
319            Path,
320            Timestretch,
321            Loop,
322            Quant,
323            Gain,
324            Bpm,
325        }
326
327        // TODO: FIELDS_MAP: Tuple array
328        const FIELDS: &[&str] = &[
329            "slot_type",
330            "slot_id",
331            "path",
332            "timestrech_mode",
333            "loop_mode",
334            "trig_quantization_mode",
335            "gain",
336            "bpm",
337        ];
338
339        impl<'de> Deserialize<'de> for Field {
340            fn deserialize<D>(deserializer: D) -> Result<Field, D::Error>
341            where
342                D: Deserializer<'de>,
343            {
344                struct FieldVisitor;
345
346                impl Visitor<'_> for FieldVisitor {
347                    type Value = Field;
348
349                    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
350                        formatter.write_str(
351                            FIELDS
352                                .iter()
353                                .map(|x| format!["`{x}`"])
354                                .collect::<Vec<_>>()
355                                .join(" or ")
356                                .as_str(),
357                        )
358                    }
359
360                    fn visit_str<E>(self, value: &str) -> Result<Field, E>
361                    where
362                        E: de::Error,
363                    {
364                        match value {
365                            "slot_type" => Ok(Field::SlotType),
366                            "slot_id" => Ok(Field::SlotId),
367                            "path" => Ok(Field::Path),
368                            "timestrech_mode" => Ok(Field::Timestretch),
369                            "loop_mode" => Ok(Field::Loop),
370                            "trig_quantization_mode" => Ok(Field::Quant),
371                            "gain" => Ok(Field::Gain),
372                            "bpm" => Ok(Field::Bpm),
373                            _ => Err(de::Error::unknown_field(value, FIELDS)),
374                        }
375                    }
376                }
377
378                deserializer.deserialize_identifier(FieldVisitor)
379            }
380        }
381
382        struct SlotAttributesVisitor;
383
384        impl<'de> Visitor<'de> for SlotAttributesVisitor {
385            type Value = SlotAttributes;
386
387            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
388                formatter.write_str("struct SlotAttributes")
389            }
390
391            fn visit_map<V>(self, mut map: V) -> Result<SlotAttributes, V::Error>
392            where
393                V: MapAccess<'de>,
394            {
395                let mut slot_type = None;
396                let mut slot_id = None;
397                let mut path = None;
398                let mut timestretch_mode = None;
399                let mut loop_mode = None;
400                let mut trig_quantization_mode = None;
401                let mut gain = None;
402                let mut bpm = None;
403
404                while let Some(key) = map.next_key()? {
405                    match key {
406                        Field::SlotType => {
407                            if slot_type.is_some() {
408                                return Err(de::Error::duplicate_field("slot_type"));
409                            }
410                            slot_type = Some(map.next_value::<SlotType>()?);
411                        }
412                        Field::SlotId => {
413                            if slot_id.is_some() {
414                                return Err(de::Error::duplicate_field("slot_id"));
415                            }
416                            slot_id = Some(map.next_value::<u8>()?);
417                        }
418                        Field::Path => {
419                            if path.is_some() {
420                                return Err(de::Error::duplicate_field("path"));
421                            }
422                            path = Some(map.next_value::<PathBuf>()?);
423                        }
424                        Field::Timestretch => {
425                            if timestretch_mode.is_some() {
426                                return Err(de::Error::duplicate_field("timestretch_mode"));
427                            }
428                            timestretch_mode = Some(map.next_value::<TimeStretchMode>()?);
429                        }
430                        Field::Loop => {
431                            if loop_mode.is_some() {
432                                return Err(de::Error::duplicate_field("loop_mode"));
433                            }
434                            loop_mode = Some(map.next_value::<LoopMode>()?);
435                        }
436                        Field::Quant => {
437                            if trig_quantization_mode.is_some() {
438                                return Err(de::Error::duplicate_field("trig_quantization_mode"));
439                            }
440                            trig_quantization_mode =
441                                Some(map.next_value::<TrigQuantizationMode>()?);
442                        }
443                        Field::Gain => {
444                            if gain.is_some() {
445                                return Err(de::Error::duplicate_field("gain"));
446                            }
447                            gain = Some(map.next_value::<u8>()?);
448                        }
449                        Field::Bpm => {
450                            if bpm.is_some() {
451                                return Err(de::Error::duplicate_field("bpm"));
452                            }
453                            bpm = Some(map.next_value::<u16>()?);
454                        }
455                    }
456                }
457
458                let slot = SlotAttributes {
459                    slot_type: slot_type.ok_or_else(|| de::Error::missing_field("slot_type"))?,
460                    slot_id: slot_id.ok_or_else(|| de::Error::missing_field("slot_type"))?,
461                    path, // allowed to be missing to handle recording buffer empty paths
462                    timestrech_mode: timestretch_mode
463                        .ok_or_else(|| de::Error::missing_field("trimstretch_mode"))?,
464                    loop_mode: loop_mode.ok_or_else(|| de::Error::missing_field("loop_mode"))?,
465                    trig_quantization_mode: trig_quantization_mode
466                        .ok_or_else(|| de::Error::missing_field("trig_quantization_mode"))?,
467                    gain: gain.ok_or_else(|| de::Error::missing_field("gain"))?,
468                    bpm: bpm.ok_or_else(|| de::Error::missing_field("bpm"))?,
469                };
470
471                Ok(slot)
472            }
473        }
474
475        deserializer.deserialize_struct("SampleSlot", FIELDS, SlotAttributesVisitor)
476    }
477}
478
479/// Container type for all sample slots.
480///
481/// The `project.*` data files store all sample slots together in a single 1-indexed array.
482/// This tends to lead to a lot of additional yak-shaving when interacting with sample slots.
483/// So this type models the octatrack's UI, displaying slots in 2x arrays: Flex and Static.
484/// These individual arrays are 0-indexed (easier lookups within for loops etc), but the
485/// slots themselves retain their 1-indexed slot IDs.
486///
487/// NOTE: This type only includes the ATTRIBUTE data for slots. Markers data is
488/// stored separately in [crate::markers::MarkersFile].
489///
490/// NOTE: I'm aware of the dangerous naming here -- `SlotX` versus `SlotsX` ...
491/// lean into the type system to stop you from using the wrong one! `SlotsX` is
492/// a container, while `SlotX` is the single slot with it's data (naming things
493/// is hard, okay).
494///
495/// To create a new [SlotsAttributes] instance, start with a mutable default
496/// as it will generate the recording buffer flex slots for you!
497/// ```rust
498/// # use ot_tools_io::projects::slots::SlotType;
499/// # use ot_tools_io::projects::slots::SlotsAttributes;
500///
501/// let mut slots = SlotsAttributes::default();
502/// for i in (128..=135_usize) {
503///    let s = slots.flex_slots[i].clone();
504///    assert_eq!(s.unwrap().slot_type, SlotType::Flex)
505/// }
506/// ```
507#[derive(Eq, PartialEq, Clone, Debug, Serialize)]
508pub struct SlotsAttributes {
509    pub static_slots: Array<Option<SlotAttributes>, 128>,
510    pub flex_slots: Array<Option<SlotAttributes>, 136>,
511}
512
513// YAML/JSON Deserialization for Sample Slots
514// we can get away with just defining map deserialization for YAML/JSON.
515// `ToString`/`FromStr` implementations are currently used for the actual data file.
516impl<'de> Deserialize<'de> for SlotsAttributes {
517    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
518    where
519        D: Deserializer<'de>,
520    {
521        enum Field {
522            Static,
523            Flex,
524        }
525
526        impl<'de> Deserialize<'de> for Field {
527            fn deserialize<D>(deserializer: D) -> Result<Field, D::Error>
528            where
529                D: Deserializer<'de>,
530            {
531                struct FieldVisitor;
532
533                impl Visitor<'_> for FieldVisitor {
534                    type Value = Field;
535
536                    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
537                        formatter.write_str("`static_slots` or `flex_slots`")
538                    }
539
540                    fn visit_str<E>(self, value: &str) -> Result<Field, E>
541                    where
542                        E: de::Error,
543                    {
544                        match value {
545                            "static_slots" => Ok(Field::Static),
546                            "flex_slots" => Ok(Field::Flex),
547                            _ => Err(de::Error::unknown_field(value, FIELDS)),
548                        }
549                    }
550                }
551
552                deserializer.deserialize_identifier(FieldVisitor)
553            }
554        }
555
556        struct SampleSlotsVisitor;
557
558        impl<'de> Visitor<'de> for SampleSlotsVisitor {
559            type Value = SlotsAttributes;
560
561            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
562                formatter.write_str("struct SampleSlots")
563            }
564
565            fn visit_unit<E>(self) -> Result<SlotsAttributes, E> {
566                Ok(SlotsAttributes::default())
567            }
568            fn visit_map<V>(self, mut map: V) -> Result<SlotsAttributes, V::Error>
569            where
570                V: MapAccess<'de>,
571            {
572                let mut static_slots = None;
573                let mut flex_slots = None;
574                while let Some(key) = map.next_key()? {
575                    match key {
576                        Field::Static => {
577                            if static_slots.is_some() {
578                                return Err(de::Error::duplicate_field("static_slots"));
579                            }
580                            static_slots =
581                                Some(map.next_value::<Array<Option<SlotAttributes>, 128>>()?);
582                        }
583                        Field::Flex => {
584                            if flex_slots.is_some() {
585                                return Err(de::Error::duplicate_field("flex_slots"));
586                            }
587                            flex_slots =
588                                Some(map.next_value::<Array<Option<SlotAttributes>, 136>>()?);
589                        }
590                    }
591                }
592                let s_slots =
593                    static_slots.ok_or_else(|| de::Error::missing_field("static_slots"))?;
594
595                let f_slots = flex_slots.ok_or_else(|| de::Error::missing_field("flex_slots"))?;
596
597                let slots = SlotsAttributes {
598                    static_slots: s_slots,
599                    flex_slots: f_slots,
600                };
601
602                Ok(slots)
603            }
604        }
605
606        const FIELDS: &[&str] = &["static_slots", "flex_slots"];
607        deserializer.deserialize_struct("SampleSlots", FIELDS, SampleSlotsVisitor)
608    }
609}
610
611/// Helper function, no public
612fn flex_slot_default_case_switch(i: usize) -> Option<SlotAttributes> {
613    if i <= 127 {
614        None
615    } else {
616        Some(SlotAttributes {
617            slot_type: SlotType::Flex,
618            // WARN: i is a 0-indexed iterable, slot IDs need to be 1-indexed!
619            slot_id: i as u8 + 1,
620            path: None,
621            timestrech_mode: TimeStretchMode::default(),
622            loop_mode: LoopMode::default(),
623            trig_quantization_mode: TrigQuantizationMode::default(),
624            gain: 72,
625            bpm: 2880,
626        })
627    }
628}
629
630impl Default for SlotsAttributes {
631    fn default() -> Self {
632        Self {
633            static_slots: Array(std::array::from_fn(|_| None)),
634            flex_slots: Array(std::array::from_fn(flex_slot_default_case_switch)),
635        }
636    }
637}
638
639impl IsDefault for SlotsAttributes {
640    fn is_default(&self) -> bool {
641        *self == SlotsAttributes::default()
642    }
643}
644
645impl FromStr for SlotsAttributes {
646    type Err = OtToolsIoErrors;
647
648    fn from_str(s: &str) -> Result<Self, Self::Err> {
649        // TODO: option plain unwrap
650        let footer_stripped = s
651            .strip_suffix("\r\n\r\n############################\r\n\r\n")
652            .ok_or(OtToolsIoErrors::ProjectSampleSlotParsingError)?;
653
654        let data_window: Vec<&str> = footer_stripped
655            .split("############################\r\n# Samples\r\n############################")
656            .collect();
657
658        let mut samples_string: Vec<&str> = data_window[1].split("[/SAMPLE]").collect();
659        // last one is always a blank string.
660        samples_string.pop();
661
662        // mutate from default as we always need recording buffers populated
663        let mut slots = Self::default();
664
665        for s in &samples_string {
666            // need zero indexing to insert slots into arrays
667            let slot = SlotAttributes::from_str(s)
668                .map_err(|_| OtToolsIoErrors::ProjectSampleSlotParsingError)?;
669            let zero_indexed_id = slot.slot_id as usize - 1;
670            match slot.slot_type {
671                SlotType::Static => {
672                    slots.static_slots[zero_indexed_id] = Some(slot);
673                }
674                SlotType::Flex => {
675                    slots.flex_slots[zero_indexed_id] = Some(slot);
676                }
677            }
678        }
679
680        Ok(slots)
681    }
682}
683
684impl fmt::Display for SlotsAttributes {
685    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
686        let mut string_slots: String = "".to_string();
687
688        let slots = vec![self.static_slots.to_vec(), self.flex_slots.to_vec()];
689
690        let slots_concat = itertools::concat(slots).into_iter().flatten();
691
692        for slot in slots_concat {
693            string_slots.push_str(&slot.to_string());
694            string_slots.push_str("\r\n\r\n");
695        }
696        string_slots = string_slots
697            .strip_suffix("\r\n\r\n")
698            .ok_or(fmt::Error)?
699            .to_string();
700        write!(f, "{string_slots:#}")
701    }
702}
703
704#[cfg(test)]
705#[allow(unused_imports)]
706mod test {
707
708    #[test]
709    fn parse_id_001_correct() {
710        let mut hmap = std::collections::HashMap::new();
711        hmap.insert("slot".to_string(), "001".to_string());
712
713        let slot_id = crate::projects::slots::parse_id(&hmap);
714
715        assert_eq!(1, slot_id.unwrap());
716    }
717
718    #[test]
719    fn parse_id_1_correct() {
720        let mut hmap = std::collections::HashMap::new();
721        hmap.insert("slot".to_string(), "1".to_string());
722
723        let slot_id = crate::projects::slots::parse_id(&hmap);
724
725        assert_eq!(1, slot_id.unwrap());
726    }
727
728    #[test]
729    fn parse_id_127_correct() {
730        let mut hmap = std::collections::HashMap::new();
731        hmap.insert("slot".to_string(), "127".to_string());
732
733        let slot_id = crate::projects::slots::parse_id(&hmap);
734
735        assert_eq!(127, slot_id.unwrap());
736    }
737
738    #[test]
739    fn parse_id_099_correct() {
740        let mut hmap = std::collections::HashMap::new();
741        hmap.insert("slot".to_string(), "099".to_string());
742
743        let slot_id = crate::projects::slots::parse_id(&hmap);
744
745        assert_eq!(99, slot_id.unwrap());
746    }
747
748    #[test]
749    fn parse_id_010_correct() {
750        let mut hmap = std::collections::HashMap::new();
751        hmap.insert("slot".to_string(), "010".to_string());
752
753        let slot_id = crate::projects::slots::parse_id(&hmap);
754
755        assert_eq!(10, slot_id.unwrap());
756    }
757
758    #[test]
759    fn test_parse_id_err_bad_value_type_err() {
760        let mut hmap = std::collections::HashMap::new();
761        hmap.insert("slot".to_string(), "AAAA".to_string());
762        let slot_id = crate::projects::slots::parse_id(&hmap);
763        assert!(slot_id.is_err());
764    }
765
766    #[test]
767    fn test_parse_tempo_correct_default() {
768        let mut hmap = std::collections::HashMap::new();
769        hmap.insert("bpmx24".to_string(), "2880".to_string());
770        let r = crate::projects::slots::parse_tempo(&hmap);
771        assert_eq!(2880_u16, r.unwrap());
772    }
773
774    #[test]
775    fn test_parse_tempo_correct_min() {
776        let mut hmap = std::collections::HashMap::new();
777        hmap.insert("bpmx24".to_string(), "720".to_string());
778        let r = crate::projects::slots::parse_tempo(&hmap);
779        assert_eq!(720_u16, r.unwrap());
780    }
781
782    #[test]
783    fn test_parse_tempo_correct_max() {
784        let mut hmap = std::collections::HashMap::new();
785        hmap.insert("bpmx24".to_string(), "7200".to_string());
786        let r = crate::projects::slots::parse_tempo(&hmap);
787        assert_eq!(7200_u16, r.unwrap());
788    }
789
790    #[test]
791    fn test_parse_tempo_bad_value_type_default_return() {
792        let mut hmap = std::collections::HashMap::new();
793        hmap.insert("bpmx24".to_string(), "AAAFSFSFSSFfssafAA".to_string());
794        let r = crate::projects::slots::parse_tempo(&hmap);
795        assert_eq!(r.unwrap(), 2880_u16);
796    }
797
798    #[test]
799    fn test_parse_gain_correct() {
800        let mut hmap = std::collections::HashMap::new();
801        hmap.insert("gain".to_string(), "72".to_string());
802        let r = crate::projects::slots::parse_gain(&hmap);
803        assert_eq!(72, r.unwrap());
804    }
805
806    #[test]
807    fn test_parse_gain_bad_value_type_default_return() {
808        let mut hmap = std::collections::HashMap::new();
809        hmap.insert("gain".to_string(), "AAAFSFSFSSFfssafAA".to_string());
810        let r = crate::projects::slots::parse_gain(&hmap);
811        assert_eq!(r.unwrap(), 72_u8);
812    }
813
814    #[test]
815    fn test_parse_loop_mode_correct_off() {
816        let mut hmap = std::collections::HashMap::new();
817        hmap.insert("loopmode".to_string(), "0".to_string());
818        let r = crate::projects::slots::parse_loop_mode(&hmap);
819        assert_eq!(r.unwrap(), crate::LoopMode::Off);
820    }
821
822    #[test]
823    fn test_parse_loop_mode_correct_normal() {
824        let mut hmap = std::collections::HashMap::new();
825        hmap.insert("loopmode".to_string(), "1".to_string());
826        let r = crate::projects::slots::parse_loop_mode(&hmap);
827        assert_eq!(r.unwrap(), crate::LoopMode::Normal);
828    }
829
830    #[test]
831    fn test_parse_loop_mode_correct_pingpong() {
832        let mut hmap = std::collections::HashMap::new();
833        hmap.insert("loopmode".to_string(), "2".to_string());
834        let r = crate::projects::slots::parse_loop_mode(&hmap);
835        assert_eq!(r.unwrap(), crate::LoopMode::PingPong);
836    }
837
838    #[test]
839    fn test_parse_loop_mode_bad_value_type_default_return() {
840        let mut hmap = std::collections::HashMap::new();
841        hmap.insert("loopmode".to_string(), "AAAFSFSFSSFfssafAA".to_string());
842        let r = crate::projects::slots::parse_loop_mode(&hmap);
843        assert_eq!(r.unwrap(), crate::LoopMode::default());
844    }
845
846    #[test]
847    fn test_parse_tstretch_correct_off() {
848        let mut hmap = std::collections::HashMap::new();
849        hmap.insert("tsmode".to_string(), "0".to_string());
850        let r = crate::projects::slots::parse_tstrech_mode(&hmap);
851        assert_eq!(crate::TimeStretchMode::Off, r.unwrap());
852    }
853
854    #[test]
855    fn test_parse_tstretch_correct_normal() {
856        let mut hmap = std::collections::HashMap::new();
857        hmap.insert("tsmode".to_string(), "2".to_string());
858        let r = crate::projects::slots::parse_tstrech_mode(&hmap);
859        assert_eq!(crate::TimeStretchMode::Normal, r.unwrap());
860    }
861
862    #[test]
863    fn test_parse_tstretch_correct_beat() {
864        let mut hmap = std::collections::HashMap::new();
865        hmap.insert("tsmode".to_string(), "3".to_string());
866        let r = crate::projects::slots::parse_tstrech_mode(&hmap);
867        assert_eq!(crate::TimeStretchMode::Beat, r.unwrap());
868    }
869
870    #[test]
871    fn test_parse_tstretch_bad_value_type_default_return() {
872        let mut hmap = std::collections::HashMap::new();
873        hmap.insert("tsmode".to_string(), "AAAFSFSFSSFfssafAA".to_string());
874        let r = crate::projects::slots::parse_tstrech_mode(&hmap);
875        assert_eq!(r.unwrap(), crate::TimeStretchMode::default());
876    }
877
878    #[test]
879    fn test_parse_tquantize_correct_off() {
880        let mut hmap = std::collections::HashMap::new();
881        hmap.insert("trigquantization".to_string(), "255".to_string());
882        let r = crate::projects::slots::parse_trig_quantize_mode(&hmap);
883        assert_eq!(crate::TrigQuantizationMode::Direct, r.unwrap());
884    }
885
886    #[test]
887    fn test_parse_tquantize_correct_direct() {
888        let mut hmap = std::collections::HashMap::new();
889        hmap.insert("trigquantization".to_string(), "0".to_string());
890        let r = crate::projects::slots::parse_trig_quantize_mode(&hmap);
891        assert_eq!(crate::TrigQuantizationMode::PatternLength, r.unwrap());
892    }
893
894    #[test]
895    fn test_parse_tquantize_correct_onestep() {
896        let mut hmap = std::collections::HashMap::new();
897        hmap.insert("trigquantization".to_string(), "1".to_string());
898        let r = crate::projects::slots::parse_trig_quantize_mode(&hmap);
899        assert_eq!(crate::TrigQuantizationMode::OneStep, r.unwrap());
900    }
901
902    #[test]
903    fn test_parse_tquantize_correct_twostep() {
904        let mut hmap = std::collections::HashMap::new();
905        hmap.insert("trigquantization".to_string(), "2".to_string());
906        let r = crate::projects::slots::parse_trig_quantize_mode(&hmap);
907        assert_eq!(crate::TrigQuantizationMode::TwoSteps, r.unwrap());
908    }
909
910    #[test]
911    fn test_parse_tquantize_correct_threestep() {
912        let mut hmap = std::collections::HashMap::new();
913        hmap.insert("trigquantization".to_string(), "3".to_string());
914        let r = crate::projects::slots::parse_trig_quantize_mode(&hmap);
915        assert_eq!(crate::TrigQuantizationMode::ThreeSteps, r.unwrap());
916    }
917
918    #[test]
919    fn test_parse_tquantize_correct_fourstep() {
920        let mut hmap = std::collections::HashMap::new();
921        hmap.insert("trigquantization".to_string(), "4".to_string());
922        let r = crate::projects::slots::parse_trig_quantize_mode(&hmap);
923        assert_eq!(crate::TrigQuantizationMode::FourSteps, r.unwrap());
924    }
925
926    // i'm not going to test every single option. we do that already elsewhere.
927
928    #[test]
929    fn test_parse_tquantize_bad_value_type_default_return() {
930        let mut hmap = std::collections::HashMap::new();
931        hmap.insert(
932            "trigquantization".to_string(),
933            "AAAFSFSFSSFfssafAA".to_string(),
934        );
935        let r = crate::projects::slots::parse_trig_quantize_mode(&hmap);
936        assert_eq!(r.unwrap(), crate::TrigQuantizationMode::default());
937    }
938}
939
940/// Slot's machine type (cannot access flex samples for a static machine etc).
941#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Eq, Hash)]
942pub enum SlotType {
943    /// Static machine slot
944    Static,
945
946    /// Flex machine slot
947    Flex,
948}
949
950impl SlotType {
951    pub fn other(&self) -> Self {
952        match self {
953            Self::Static => Self::Flex,
954            Self::Flex => Self::Static,
955        }
956    }
957}
958
959impl OptionEnumValueConvert<String> for SlotType {
960    fn from_value(v: &String) -> RBoxErr<Self> {
961        match v.to_ascii_uppercase().as_str() {
962            "STATIC" => Ok(SlotType::Static),
963            "FLEX" => Ok(SlotType::Flex),
964            _ => Err(OtToolsIoErrors::NoMatchingOptionEnumValue.into()),
965        }
966    }
967
968    fn value(&self) -> RBoxErr<String> {
969        match self {
970            SlotType::Static => Ok("STATIC".to_string()),
971            SlotType::Flex => Ok("FLEX".to_string()),
972        }
973    }
974}
975
976#[cfg(test)]
977mod test_spec {
978    mod value {
979        // NOTE: @dijksterhuis: have to import the trait to use it
980        use crate::projects::slots::SlotType;
981        use crate::OptionEnumValueConvert;
982
983        #[test]
984        fn test_static() {
985            assert_eq!(SlotType::Static.value().unwrap(), "STATIC");
986        }
987        #[test]
988        fn test_flex() {
989            assert_eq!(SlotType::Flex.value().unwrap(), "FLEX");
990        }
991    }
992
993    mod from_value {
994        use crate::projects::slots::SlotType;
995        use crate::OptionEnumValueConvert;
996
997        #[test]
998        fn test_error() {
999            assert!(SlotType::from_value(&"SOME INCORRECT STRING".to_string()).is_err(),);
1000        }
1001
1002        #[test]
1003        fn test_static_upper() {
1004            assert_eq!(
1005                SlotType::Static,
1006                SlotType::from_value(&"STATIC".to_string()).unwrap(),
1007            );
1008        }
1009
1010        #[test]
1011        fn test_static_lower() {
1012            assert_eq!(
1013                SlotType::Static,
1014                SlotType::from_value(&"static".to_string()).unwrap(),
1015            );
1016        }
1017
1018        #[test]
1019        fn test_flex_upper() {
1020            assert_eq!(
1021                SlotType::Flex,
1022                SlotType::from_value(&"FLEX".to_string()).unwrap(),
1023            );
1024        }
1025
1026        #[test]
1027        fn test_flex_lower() {
1028            assert_eq!(
1029                SlotType::Flex,
1030                SlotType::from_value(&"flex".to_string()).unwrap(),
1031            );
1032        }
1033    }
1034}