1mod metadata;
9pub mod settings;
10mod slots;
11mod states;
12
13pub use crate::projects::{
14    metadata::OsMetadata, settings::Settings, slots::SlotAttributes, slots::SlotsAttributes,
15    states::State,
16};
17
18use crate::settings::InvalidValueError;
19use crate::{
20    HasChecksumField, HasFileVersionField, HasHeaderField, OctatrackFileIO, OtToolsIoError,
21};
22use ot_tools_io_derive::IsDefaultCheck;
23use serde::{Deserialize, Serialize};
24use std::{
25    cmp::PartialEq, collections::HashMap, fmt::Debug, fmt::Display, num::ParseIntError,
26    str::FromStr, str::ParseBoolError,
27};
28use thiserror::Error;
29
30#[derive(Debug, Error)]
31pub enum ProjectParseError {
32    #[error("failed to parse integer value")]
33    Int(#[from] ParseIntError),
34    #[error("failed to parse boolean value")]
35    Bool(#[from] ParseBoolError),
36    #[error("failed to parse string value")]
37    String,
38    #[error("failed to parse value: {0} (ot_tools_io::common_options::InvalidValueErrors)")]
39    InvalidValue(#[from] InvalidValueError),
40    #[error("failed to load hash map for parsing")]
41    HashMap,
42    #[error("failed to parse footer data")]
43    Footer,
44}
45
46#[derive(Debug, Error)]
48pub enum ProjectError {
49    #[error("type cannot be checksummed")]
51    NotChecksummable,
52    #[error("type does not have a file patch version field")]
54    NoFilePatchVersionField,
55    #[error("type does not have a header field")]
57    NoHeaderField,
58}
59
60fn parse_hashmap_string_value<T: FromStr>(
63    hmap: &HashMap<String, String>,
64    key: &str,
65    default_str: Option<&str>,
66) -> Result<T, <T as FromStr>::Err>
67where
68    <T as FromStr>::Err: Debug,
69{
70    match default_str {
71        Some(x) => hmap.get(key).unwrap_or(&x.to_string()).parse::<T>(),
72        None => hmap.get(key).unwrap().parse::<T>(),
73    }
74}
75
76fn parse_hashmap_string_value_bool(
79    hmap: &HashMap<String, String>,
80    key: &str,
81    default_str: Option<&str>,
82) -> Result<bool, <u8 as FromStr>::Err> {
83    let val = parse_hashmap_string_value::<u8>(hmap, key, default_str)?;
84    Ok(matches!(val, 1))
85}
86
87fn string_to_hashmap(
89    data: &str,
90    section: &SectionHeader,
91) -> Result<HashMap<String, String>, ProjectParseError> {
92    let start = format!("[{section}]");
93    let end = format!("[/{section}]");
94
95    let start_idx = data.find(&start).ok_or(ProjectParseError::HashMap)?;
96    let start_idx_shifted: usize = start_idx + start.len();
97    let end_idx = data.find(&end).ok_or(ProjectParseError::HashMap)?;
98
99    let section: String = data[start_idx_shifted..end_idx].to_string();
100
101    let mut hmap: HashMap<String, String> = HashMap::new();
102    let mut trig_mode_midi_field_idx = 1;
103
104    for split_s in section.split("\r\n") {
105        if !split_s.is_empty() {
108            let key_pair_string = split_s.to_string();
109            let mut key_pair_split: Vec<&str> = key_pair_string.split('=').collect();
110
111            let key_renamed: String = format!("trig_mode_midi_track_{}", &trig_mode_midi_field_idx);
116            if key_pair_split[0] == "TRIG_MODE_MIDI" {
117                key_pair_split[0] = key_renamed.as_str();
118                trig_mode_midi_field_idx += 1;
119            }
120
121            hmap.insert(
122                key_pair_split[0].to_string().to_ascii_lowercase(),
123                key_pair_split[1].to_string(),
124            );
125        }
126    }
127
128    Ok(hmap)
129}
130
131#[derive(Debug, Error)]
132#[error("invalid project section header value")]
133struct InvalidSectionHeaderValue;
134
135#[derive(Debug, PartialEq)]
137enum SectionHeader {
138    Meta,
139    States,
140    Settings,
141    Samples,
142}
143
144impl TryFrom<&str> for SectionHeader {
145    type Error = InvalidSectionHeaderValue;
146    fn try_from(value: &str) -> Result<Self, Self::Error> {
147        match value.to_ascii_uppercase().as_str() {
148            "META" => Ok(Self::Meta),
149            "STATES" => Ok(Self::States),
150            "SETTINGS" => Ok(Self::Settings),
151            "SAMPLES" => Ok(Self::Samples),
152            _ => Err(InvalidSectionHeaderValue),
153        }
154    }
155}
156
157impl TryFrom<String> for SectionHeader {
158    type Error = InvalidSectionHeaderValue;
159    fn try_from(value: String) -> Result<Self, Self::Error> {
160        value.as_str().try_into()
161    }
162}
163
164impl Display for SectionHeader {
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        let str = match self {
167            Self::Meta => "META".to_string(),
168            Self::States => "STATES".to_string(),
169            Self::Settings => "SETTINGS".to_string(),
170            Self::Samples => "SAMPLES".to_string(),
171        };
172        write!(f, "{str}")
173    }
174}
175
176#[cfg(test)]
177mod test_project_section {
178
179    mod try_from_str {
180        use crate::projects::{InvalidSectionHeaderValue, SectionHeader};
181
182        #[test]
183        fn no_match_is_err() {
184            assert_eq!(
185                SectionHeader::try_from("skfsdkfjskdh")
186                    .unwrap_err()
187                    .to_string(),
188                InvalidSectionHeaderValue.to_string()
189            )
190        }
191
192        #[test]
193        fn uppercase_meta() {
194            assert_eq!(
195                SectionHeader::try_from("META").unwrap(),
196                SectionHeader::Meta,
197            )
198        }
199
200        #[test]
201        fn uppercase_states() {
202            assert_eq!(
203                SectionHeader::try_from("STATES").unwrap(),
204                SectionHeader::States,
205            )
206        }
207
208        #[test]
209        fn uppercase_settings() {
210            assert_eq!(
211                SectionHeader::try_from("SETTINGS").unwrap(),
212                SectionHeader::Settings,
213            )
214        }
215
216        #[test]
217        fn uppercase_samples() {
218            assert_eq!(
219                SectionHeader::try_from("SAMPLES").unwrap(),
220                SectionHeader::Samples,
221            )
222        }
223
224        #[test]
225        fn lowercase_meta() {
226            assert_eq!(
227                SectionHeader::try_from("meta").unwrap(),
228                SectionHeader::Meta,
229            )
230        }
231
232        #[test]
233        fn lowercase_states() {
234            assert_eq!(
235                SectionHeader::try_from("states").unwrap(),
236                SectionHeader::States,
237            )
238        }
239
240        #[test]
241        fn lowercase_settings() {
242            assert_eq!(
243                SectionHeader::try_from("settings").unwrap(),
244                SectionHeader::Settings,
245            )
246        }
247
248        #[test]
249        fn lowercase_samples() {
250            assert_eq!(
251                SectionHeader::try_from("samples").unwrap(),
252                SectionHeader::Samples,
253            )
254        }
255    }
256
257    mod try_from_string {
258        use crate::projects::SectionHeader;
259
260        #[test]
261        fn no_match_is_err() {
262            assert!(SectionHeader::try_from("skfsdkfjskdh".to_string()).is_err())
263        }
264
265        #[test]
266        fn uppercase_meta() {
267            assert_eq!(
268                SectionHeader::try_from("META".to_string()).unwrap(),
269                SectionHeader::Meta,
270            )
271        }
272
273        #[test]
274        fn uppercase_states() {
275            assert_eq!(
276                SectionHeader::try_from("STATES".to_string()).unwrap(),
277                SectionHeader::States,
278            )
279        }
280
281        #[test]
282        fn uppercase_settings() {
283            assert_eq!(
284                SectionHeader::try_from("SETTINGS".to_string()).unwrap(),
285                SectionHeader::Settings,
286            )
287        }
288
289        #[test]
290        fn uppercase_samples() {
291            assert_eq!(
292                SectionHeader::try_from("SAMPLES".to_string()).unwrap(),
293                SectionHeader::Samples,
294            )
295        }
296
297        #[test]
298        fn lowercase_meta() {
299            assert_eq!(
300                SectionHeader::try_from("meta".to_string()).unwrap(),
301                SectionHeader::Meta,
302            )
303        }
304
305        #[test]
306        fn lowercase_states() {
307            assert_eq!(
308                SectionHeader::try_from("states".to_string()).unwrap(),
309                SectionHeader::States,
310            )
311        }
312
313        #[test]
314        fn lowercase_settings() {
315            assert_eq!(
316                SectionHeader::try_from("settings".to_string()).unwrap(),
317                SectionHeader::Settings,
318            )
319        }
320        #[test]
321        fn lowercase_samples() {
322            assert_eq!(
323                SectionHeader::try_from("samples".to_string()).unwrap(),
324                SectionHeader::Samples,
325            )
326        }
327    }
328
329    mod to_string {
330        use crate::projects::SectionHeader;
331        #[test]
332        fn section_heading_value_meta() {
333            assert_eq!(SectionHeader::Meta.to_string(), "META".to_string())
334        }
335
336        #[test]
337        fn section_heading_value_states() {
338            assert_eq!(SectionHeader::States.to_string(), "STATES".to_string())
339        }
340
341        #[test]
342        fn section_heading_value_settings() {
343            assert_eq!(SectionHeader::Settings.to_string(), "SETTINGS".to_string())
344        }
345
346        #[test]
347        fn section_heading_value_samples() {
348            assert_eq!(SectionHeader::Samples.to_string(), "SAMPLES".to_string())
349        }
350    }
351}
352
353#[derive(IsDefaultCheck, Serialize, Deserialize, PartialEq, Debug, Clone)]
367pub struct ProjectFile {
368    pub metadata: OsMetadata,
370
371    pub settings: Settings,
373
374    pub states: State,
376
377    pub slots: SlotsAttributes,
379}
380
381impl Default for ProjectFile {
382    fn default() -> Self {
383        let metadata = OsMetadata::default();
384        let states = State::default();
385        let settings = Settings::default();
386        let slots = SlotsAttributes::default();
387
388        Self {
389            metadata,
390            settings,
391            states,
392            slots,
393        }
394    }
395}
396
397impl std::fmt::Display for ProjectFile {
398    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
399        let states_header =
400            "############################\r\n# Project States\r\n############################"
401                .to_string();
402        let settings_header =
403            "############################\r\n# Project Settings\r\n############################"
404                .to_string();
405        let slots_header =
406            "############################\r\n# Samples\r\n############################".to_string();
407        let footer = "############################".to_string();
408
409        let metadata_string: String = self.metadata.to_string();
410        let states_string: String = self.states.to_string();
411        let settings_string: String = self.settings.to_string();
412
413        let sample_slots_string = self.slots.to_string();
414
415        let v: Vec<String> = vec![
416            settings_header,
419            metadata_string,
420            settings_string,
421            states_header,
422            states_string,
423            slots_header,
424            sample_slots_string,
425            footer,
426        ];
427        let mut project_string = v.join("\r\n\r\n");
428        project_string.push_str("\r\n\r\n");
429
430        write!(f, "{project_string}")
431    }
432}
433
434impl OctatrackFileIO for ProjectFile {
435    fn encode(&self) -> Result<Vec<u8>, OtToolsIoError> {
437        let data = self.to_string();
438        let bytes: Vec<u8> = data.bytes().collect::<Vec<u8>>();
439        Ok(bytes)
440    }
441
442    fn decode(bytes: &[u8]) -> Result<Self, OtToolsIoError> {
445        let s = std::str::from_utf8(bytes)?.to_string();
446
447        let metadata = OsMetadata::from_str(&s)?;
448        let states = State::from_str(&s)?;
449        let settings = Settings::from_str(&s)?;
450        let slots = SlotsAttributes::from_str(&s)?;
451
452        Ok(Self {
453            metadata,
454            settings,
455            states,
456            slots,
457        })
458    }
459}
460
461impl HasChecksumField for ProjectFile {
462    fn calculate_checksum(&self) -> Result<u16, OtToolsIoError> {
463        Err(ProjectError::NotChecksummable.into())
464    }
465    fn check_checksum(&self) -> Result<bool, OtToolsIoError> {
466        Err(ProjectError::NotChecksummable.into())
467    }
468}
469
470#[cfg(test)]
471mod checksum_field {
472    use crate::test_utils::get_blank_proj_dirpath;
473    use crate::{HasChecksumField, OctatrackFileIO, OtToolsIoError, ProjectFile};
474
475    #[test]
476    fn fail_calculate_checksum() -> Result<(), OtToolsIoError> {
477        let infile = get_blank_proj_dirpath().join("project.work");
478        let p = ProjectFile::from_data_file(&infile)?;
479        assert_eq!(p.calculate_checksum().unwrap_err().to_string(), "project files cannot be checked for integrity: type cannot be checksummed (ot_tools_io::projects::ProjectError)");
480
481        Ok(())
482    }
483    #[test]
484    fn fail_check_checksum() -> Result<(), OtToolsIoError> {
485        let infile = get_blank_proj_dirpath().join("project.work");
486        let p = ProjectFile::from_data_file(&infile)?;
487        assert_eq!(p.check_checksum().unwrap_err().to_string(), "project files cannot be checked for integrity: type cannot be checksummed (ot_tools_io::projects::ProjectError)");
488
489        Ok(())
490    }
491}
492
493impl HasHeaderField for ProjectFile {
494    fn check_header(&self) -> Result<bool, OtToolsIoError> {
495        Err(ProjectError::NoHeaderField.into())
496    }
497}
498
499#[cfg(test)]
500mod header_field {
501    use crate::test_utils::get_blank_proj_dirpath;
502    use crate::{HasHeaderField, OctatrackFileIO, OtToolsIoError, ProjectFile};
503
504    #[test]
505    fn fail_check_header() -> Result<(), OtToolsIoError> {
506        let infile = get_blank_proj_dirpath().join("project.work");
507        let p = ProjectFile::from_data_file(&infile)?;
508        assert_eq!(p.check_header().unwrap_err().to_string(), "project files cannot be checked for integrity: type does not have a header field (ot_tools_io::projects::ProjectError)");
509
510        Ok(())
511    }
512}
513
514impl HasFileVersionField for ProjectFile {
515    fn check_file_version(&self) -> Result<bool, OtToolsIoError> {
516        Err(ProjectError::NoFilePatchVersionField.into())
517    }
518}
519
520#[cfg(test)]
521mod file_version_field {
522    use crate::test_utils::get_blank_proj_dirpath;
523    use crate::{HasFileVersionField, OctatrackFileIO, OtToolsIoError, ProjectFile};
524
525    #[test]
526    fn fail_check_version() -> Result<(), OtToolsIoError> {
527        let infile = get_blank_proj_dirpath().join("project.work");
528        let p = ProjectFile::from_data_file(&infile)?;
529        assert_eq!(p.check_file_version().unwrap_err().to_string(),  "project files cannot be checked for integrity: type does not have a file patch version field (ot_tools_io::projects::ProjectError)");
530
531        Ok(())
532    }
533}
534
535#[cfg(test)]
536#[allow(unused_imports)]
537mod tests {
538    use super::*;
539
540    const DEFAULT_STR_FILE: &str = "############################\r\n# Project Settings\r\n############################\r\n\r\n[META]\r\nTYPE=OCTATRACK DPS-1 PROJECT\r\nVERSION=19\r\nOS_VERSION=R0177     1.40B\r\n[/META]\r\n\r\n[SETTINGS]\r\nWRITEPROTECTED=0\r\nTEMPOx24=2880\r\nPATTERN_TEMPO_ENABLED=0\r\nMIDI_CLOCK_SEND=0\r\nMIDI_CLOCK_RECEIVE=0\r\nMIDI_TRANSPORT_SEND=0\r\nMIDI_TRANSPORT_RECEIVE=0\r\nMIDI_PROGRAM_CHANGE_SEND=0\r\nMIDI_PROGRAM_CHANGE_SEND_CH=-1\r\nMIDI_PROGRAM_CHANGE_RECEIVE=0\r\nMIDI_PROGRAM_CHANGE_RECEIVE_CH=-1\r\nMIDI_TRIG_CH1=0\r\nMIDI_TRIG_CH2=1\r\nMIDI_TRIG_CH3=2\r\nMIDI_TRIG_CH4=3\r\nMIDI_TRIG_CH5=4\r\nMIDI_TRIG_CH6=5\r\nMIDI_TRIG_CH7=6\r\nMIDI_TRIG_CH8=7\r\nMIDI_AUTO_CHANNEL=10\r\nMIDI_SOFT_THRU=0\r\nMIDI_AUDIO_TRK_CC_IN=1\r\nMIDI_AUDIO_TRK_CC_OUT=3\r\nMIDI_AUDIO_TRK_NOTE_IN=1\r\nMIDI_AUDIO_TRK_NOTE_OUT=3\r\nMIDI_MIDI_TRK_CC_IN=1\r\nPATTERN_CHANGE_CHAIN_BEHAVIOR=0\r\nPATTERN_CHANGE_AUTO_SILENCE_TRACKS=0\r\nPATTERN_CHANGE_AUTO_TRIG_LFOS=0\r\nLOAD_24BIT_FLEX=0\r\nDYNAMIC_RECORDERS=0\r\nRECORD_24BIT=0\r\nRESERVED_RECORDER_COUNT=8\r\nRESERVED_RECORDER_LENGTH=16\r\nINPUT_DELAY_COMPENSATION=0\r\nGATE_AB=127\r\nGATE_CD=127\r\nGAIN_AB=64\r\nGAIN_CD=64\r\nDIR_AB=0\r\nDIR_CD=0\r\nPHONES_MIX=64\r\nMAIN_TO_CUE=0\r\nMASTER_TRACK=0\r\nCUE_STUDIO_MODE=0\r\nMAIN_LEVEL=64\r\nCUE_LEVEL=64\r\nMETRONOME_TIME_SIGNATURE=3\r\nMETRONOME_TIME_SIGNATURE_DENOMINATOR=2\r\nMETRONOME_PREROLL=0\r\nMETRONOME_CUE_VOLUME=32\r\nMETRONOME_MAIN_VOLUME=0\r\nMETRONOME_PITCH=12\r\nMETRONOME_TONAL=1\r\nMETRONOME_ENABLED=0\r\nTRIG_MODE_MIDI=0\r\nTRIG_MODE_MIDI=0\r\nTRIG_MODE_MIDI=0\r\nTRIG_MODE_MIDI=0\r\nTRIG_MODE_MIDI=0\r\nTRIG_MODE_MIDI=0\r\nTRIG_MODE_MIDI=0\r\nTRIG_MODE_MIDI=0\r\n[/SETTINGS]\r\n\r\n############################\r\n# Project States\r\n############################\r\n\r\n[STATES]\r\nBANK=0\r\nPATTERN=0\r\nARRANGEMENT=0\r\nARRANGEMENT_MODE=0\r\nPART=0\r\nTRACK=0\r\nTRACK_OTHERMODE=0\r\nSCENE_A_MUTE=0\r\nSCENE_B_MUTE=0\r\nTRACK_CUE_MASK=0\r\nTRACK_MUTE_MASK=0\r\nTRACK_SOLO_MASK=0\r\nMIDI_TRACK_MUTE_MASK=0\r\nMIDI_TRACK_SOLO_MASK=0\r\nMIDI_MODE=0\r\n[/STATES]\r\n\r\n############################\r\n# Samples\r\n############################\r\n\r\n[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=129\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=130\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=131\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=132\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=133\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=134\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=135\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=136\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n############################\r\n\r\n";
541    const DEFAULT_STR_META: &str = "[META]\r\nTYPE=OCTATRACK DPS-1 PROJECT\r\nVERSION=19\r\nOS_VERSION=R0177     1.40B\r\n[/META]";
542    const DEFAULT_STR_STATE: &str = "[STATES]\r\nBANK=0\r\nPATTERN=0\r\nARRANGEMENT=0\r\nARRANGEMENT_MODE=0\r\nPART=0\r\nTRACK=0\r\nTRACK_OTHERMODE=0\r\nSCENE_A_MUTE=0\r\nSCENE_B_MUTE=0\r\nTRACK_CUE_MASK=0\r\nTRACK_MUTE_MASK=0\r\nTRACK_SOLO_MASK=0\r\nMIDI_TRACK_MUTE_MASK=0\r\nMIDI_TRACK_SOLO_MASK=0\r\nMIDI_MODE=0\r\n[/STATES]";
543    const DEFAULT_STR_SETTINGS: &str = "[SETTINGS]\r\nWRITEPROTECTED=0\r\nTEMPOx24=2880\r\nPATTERN_TEMPO_ENABLED=0\r\nMIDI_CLOCK_SEND=0\r\nMIDI_CLOCK_RECEIVE=0\r\nMIDI_TRANSPORT_SEND=0\r\nMIDI_TRANSPORT_RECEIVE=0\r\nMIDI_PROGRAM_CHANGE_SEND=0\r\nMIDI_PROGRAM_CHANGE_SEND_CH=-1\r\nMIDI_PROGRAM_CHANGE_RECEIVE=0\r\nMIDI_PROGRAM_CHANGE_RECEIVE_CH=-1\r\nMIDI_TRIG_CH1=0\r\nMIDI_TRIG_CH2=1\r\nMIDI_TRIG_CH3=2\r\nMIDI_TRIG_CH4=3\r\nMIDI_TRIG_CH5=4\r\nMIDI_TRIG_CH6=5\r\nMIDI_TRIG_CH7=6\r\nMIDI_TRIG_CH8=7\r\nMIDI_AUTO_CHANNEL=10\r\nMIDI_SOFT_THRU=0\r\nMIDI_AUDIO_TRK_CC_IN=1\r\nMIDI_AUDIO_TRK_CC_OUT=3\r\nMIDI_AUDIO_TRK_NOTE_IN=1\r\nMIDI_AUDIO_TRK_NOTE_OUT=3\r\nMIDI_MIDI_TRK_CC_IN=1\r\nPATTERN_CHANGE_CHAIN_BEHAVIOR=0\r\nPATTERN_CHANGE_AUTO_SILENCE_TRACKS=0\r\nPATTERN_CHANGE_AUTO_TRIG_LFOS=0\r\nLOAD_24BIT_FLEX=0\r\nDYNAMIC_RECORDERS=0\r\nRECORD_24BIT=0\r\nRESERVED_RECORDER_COUNT=8\r\nRESERVED_RECORDER_LENGTH=16\r\nINPUT_DELAY_COMPENSATION=0\r\nGATE_AB=127\r\nGATE_CD=127\r\nGAIN_AB=64\r\nGAIN_CD=64\r\nDIR_AB=0\r\nDIR_CD=0\r\nPHONES_MIX=64\r\nMAIN_TO_CUE=0\r\nMASTER_TRACK=0\r\nCUE_STUDIO_MODE=0\r\nMAIN_LEVEL=64\r\nCUE_LEVEL=64\r\nMETRONOME_TIME_SIGNATURE=3\r\nMETRONOME_TIME_SIGNATURE_DENOMINATOR=2\r\nMETRONOME_PREROLL=0\r\nMETRONOME_CUE_VOLUME=32\r\nMETRONOME_MAIN_VOLUME=0\r\nMETRONOME_PITCH=12\r\nMETRONOME_TONAL=1\r\nMETRONOME_ENABLED=0\r\nTRIG_MODE_MIDI=0\r\nTRIG_MODE_MIDI=0\r\nTRIG_MODE_MIDI=0\r\nTRIG_MODE_MIDI=0\r\nTRIG_MODE_MIDI=0\r\nTRIG_MODE_MIDI=0\r\nTRIG_MODE_MIDI=0\r\nTRIG_MODE_MIDI=0\r\n[/SETTINGS]";
544    const DEFAULT_STR_SLOTS: &str = "[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=129\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=130\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=131\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=132\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=133\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=134\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=135\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=136\r\nPATH=\r\nBPMx24=2880\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]";
545
546    mod test_to_string_display {
549        use super::*;
550
551        #[test]
552        fn test_full_to_string() {
553            assert_eq!(ProjectFile::default().to_string(), DEFAULT_STR_FILE);
554            assert_eq!(format!["{:#}", ProjectFile::default()], DEFAULT_STR_FILE);
555        }
556
557        #[test]
558        fn test_metadata_to_string() {
559            assert_eq!(OsMetadata::default().to_string(), DEFAULT_STR_META);
560            assert_eq!(format!["{:#}", OsMetadata::default()], DEFAULT_STR_META);
561        }
562
563        #[test]
564        fn test_states_to_string() {
565            assert_eq!(State::default().to_string(), DEFAULT_STR_STATE);
566            assert_eq!(format!["{:#}", State::default()], DEFAULT_STR_STATE);
567        }
568
569        #[test]
570        fn test_settings_to_string() {
571            assert_eq!(Settings::default().to_string(), DEFAULT_STR_SETTINGS);
572            assert_eq!(format!["{:#}", Settings::default()], DEFAULT_STR_SETTINGS);
573        }
574
575        #[test]
576        fn test_sample_slots_to_string() {
577            assert_eq!(SlotsAttributes::default().to_string(), DEFAULT_STR_SLOTS);
578            assert_eq!(
579                format!["{:#}", SlotsAttributes::default()],
580                DEFAULT_STR_SLOTS
581            );
582        }
583    }
584
585    mod test_to_string_debug {
587        use super::*;
588
589        #[test]
590        fn test_full_to_string() {
591            assert_ne!(
592                format!["{:#?}", ProjectFile::default()],
593                DEFAULT_STR_FILE,
594                "debug formatting should not be 'file' representation",
595            );
596        }
597
598        #[test]
599        fn test_metadata_to_string() {
600            assert_ne!(
601                format!["{:#?}", OsMetadata::default().to_string()],
602                DEFAULT_STR_META,
603                "debug formatting should not be 'file' representation",
604            );
605        }
606
607        #[test]
608        fn test_states_to_string() {
609            assert_ne!(
610                format!["{:#?}", State::default().to_string()],
611                DEFAULT_STR_STATE,
612                "debug formatting should not be 'file' representation",
613            );
614        }
615
616        #[test]
617        fn test_settings_to_string() {
618            assert_ne!(
619                format!["{:#?}", Settings::default().to_string()],
620                DEFAULT_STR_SETTINGS,
621                "debug formatting should not be 'file' representation",
622            );
623        }
624
625        #[test]
626        fn test_sample_slots_to_string() {
627            assert_ne!(
628                format!["{:#?}", SlotsAttributes::default().to_string()],
629                DEFAULT_STR_SLOTS,
630                "debug formatting should not be 'file' representation",
631            );
632        }
633    }
634}