ot_tools_io/
projects.rs

1/*
2SPDX-License-Identifier: GPL-3.0-or-later
3Copyright © 2024 Mike Robeson [dijksterhuis]
4*/
5
6//! Types and ser/de of `project.*` data files.
7
8mod 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/// Project file errors
47#[derive(Debug, Error)]
48pub enum ProjectError {
49    /// Type cannot be checksummed
50    #[error("type cannot be checksummed")]
51    NotChecksummable,
52    /// Type does not have a data patch file version field
53    #[error("type does not have a file patch version field")]
54    NoFilePatchVersionField,
55    /// Type does not have a header field
56    #[error("type does not have a header field")]
57    NoHeaderField,
58}
59
60// NOTE: https://stackoverflow.com/questions/67352894/rust-error-conversion-for-generic-fromstr-unwrap-errors-out-with-unsatisfied-tr
61/// Return the string value of a `HashMap<_, String>` parsed into specified type `T`
62fn 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
76/// Return the string value of a `HashMap<_, String>` parsed into a boolean value
77/// (any parsed value != 1 returns `false`)
78fn 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
87/// Extract ASCII string project data for a specified section as a HashMap of k-v pairs.
88fn 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        // new line splits returns empty fields :/
106
107        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            // there are 8x TRIG_MODE_MIDI key value pairs in project settings data
112            // but the keys do not have audio track number indicators. i assume they're
113            // stored in order of the midi track number, and each subsequent one we
114            // read is the next track.
115            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/// ASCII data section headings within an Octatrack `project.*` file
136#[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/// A parsed representation of an Octatrack Project file (`project.work` or `project.strd`).
354///
355/// **Note**: We derive [`serde::Deserialize`] and [`serde::Serialize`] on this (and all the included subtypes).
356/// But project files are actually string data being parsed directly without
357/// any [`serde`]-ing or [`bincode`]-ing. This may change in the future to custom
358/// Serialize/Deserialize implementations for string source file data.
359/// But that will only happen if i can work out how to differentiate between
360/// deserializing a yaml/json string and a raw data file's string data
361/// (we use [`serde::de::Deserializer::is_human_readable`] for arrangements
362/// to differentiate between YAML/JSON and string data files, which is a problem here as
363/// we'll be testing for the difference between YAML/JSON (string data) and String (string data).)
364// TODO: Switch to custom Deserialize/Serialize parsing from string data,
365//       as per arrangements.
366#[derive(IsDefaultCheck, Serialize, Deserialize, PartialEq, Debug, Clone)]
367pub struct ProjectFile {
368    /// Metadata key-value pairs from a Project file.
369    pub metadata: OsMetadata,
370
371    /// Settings key-value pairs from a Project file.
372    pub settings: Settings,
373
374    /// States key-value pairs from a Project file.
375    pub states: State,
376
377    /// Slots key-value pairs from a Project file.
378    pub slots: SlotsAttributes,
379}
380
381impl OctatrackFileIO for ProjectFile {
382    // For project data, need to convert to string values again then into bytes
383    fn encode(&self) -> Result<Vec<u8>, OtToolsIoError> {
384        let data = self.to_string();
385        let bytes: Vec<u8> = data.bytes().collect::<Vec<u8>>();
386        Ok(bytes)
387    }
388
389    // For project data, need to read bytes as an utf string, then split the structs out from the string
390    // data.
391    fn decode(bytes: &[u8]) -> Result<Self, OtToolsIoError> {
392        let s = std::str::from_utf8(bytes)?.to_string();
393
394        let metadata = OsMetadata::from_str(&s)?;
395        let states = State::from_str(&s)?;
396        let settings = Settings::from_str(&s)?;
397        let slots = SlotsAttributes::from_str(&s)?;
398
399        Ok(Self {
400            metadata,
401            settings,
402            states,
403            slots,
404        })
405    }
406}
407
408impl Default for ProjectFile {
409    fn default() -> Self {
410        let metadata = OsMetadata::default();
411        let states = State::default();
412        let settings = Settings::default();
413        let slots = SlotsAttributes::default();
414
415        Self {
416            metadata,
417            settings,
418            states,
419            slots,
420        }
421    }
422}
423
424impl HasChecksumField for ProjectFile {
425    fn calculate_checksum(&self) -> Result<u16, OtToolsIoError> {
426        Err(ProjectError::NotChecksummable.into())
427    }
428    fn check_checksum(&self) -> Result<bool, OtToolsIoError> {
429        Err(ProjectError::NotChecksummable.into())
430    }
431}
432
433impl HasFileVersionField for ProjectFile {
434    fn check_file_version(&self) -> Result<bool, OtToolsIoError> {
435        Err(ProjectError::NoFilePatchVersionField.into())
436    }
437}
438
439impl HasHeaderField for ProjectFile {
440    fn check_header(&self) -> Result<bool, OtToolsIoError> {
441        Err(ProjectError::NoHeaderField.into())
442    }
443}
444
445impl std::fmt::Display for ProjectFile {
446    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
447        let states_header =
448            "############################\r\n# Project States\r\n############################"
449                .to_string();
450        let settings_header =
451            "############################\r\n# Project Settings\r\n############################"
452                .to_string();
453        let slots_header =
454            "############################\r\n# Samples\r\n############################".to_string();
455        let footer = "############################".to_string();
456
457        let metadata_string: String = self.metadata.to_string();
458        let states_string: String = self.states.to_string();
459        let settings_string: String = self.settings.to_string();
460
461        let sample_slots_string = self.slots.to_string();
462
463        let v: Vec<String> = vec![
464            // META and SETTINGS are subsections of 'Project Settings'...
465            // so have to do this joining in `ProjectFile`
466            settings_header,
467            metadata_string,
468            settings_string,
469            states_header,
470            states_string,
471            slots_header,
472            sample_slots_string,
473            footer,
474        ];
475        let mut project_string = v.join("\r\n\r\n");
476        project_string.push_str("\r\n\r\n");
477
478        write!(f, "{project_string}")
479    }
480}
481
482#[cfg(test)]
483#[allow(unused_imports)]
484mod tests {
485    use super::*;
486
487    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";
488    const DEFAULT_STR_META: &str = "[META]\r\nTYPE=OCTATRACK DPS-1 PROJECT\r\nVERSION=19\r\nOS_VERSION=R0177     1.40B\r\n[/META]";
489    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]";
490    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]";
491    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]";
492
493    // make sure to_string() and display() for a project type displays
494    // the file format representation, not a debug struct
495    mod test_to_string_display {
496        use super::*;
497
498        #[test]
499        fn test_full_to_string() {
500            assert_eq!(ProjectFile::default().to_string(), DEFAULT_STR_FILE);
501            assert_eq!(format!["{:#}", ProjectFile::default()], DEFAULT_STR_FILE);
502        }
503
504        #[test]
505        fn test_metadata_to_string() {
506            assert_eq!(OsMetadata::default().to_string(), DEFAULT_STR_META);
507            assert_eq!(format!["{:#}", OsMetadata::default()], DEFAULT_STR_META);
508        }
509
510        #[test]
511        fn test_states_to_string() {
512            assert_eq!(State::default().to_string(), DEFAULT_STR_STATE);
513            assert_eq!(format!["{:#}", State::default()], DEFAULT_STR_STATE);
514        }
515
516        #[test]
517        fn test_settings_to_string() {
518            assert_eq!(Settings::default().to_string(), DEFAULT_STR_SETTINGS);
519            assert_eq!(format!["{:#}", Settings::default()], DEFAULT_STR_SETTINGS);
520        }
521
522        #[test]
523        fn test_sample_slots_to_string() {
524            assert_eq!(SlotsAttributes::default().to_string(), DEFAULT_STR_SLOTS);
525            assert_eq!(
526                format!["{:#}", SlotsAttributes::default()],
527                DEFAULT_STR_SLOTS
528            );
529        }
530    }
531
532    // make sure debug formatting is not the same as display formatting
533    mod test_to_string_debug {
534        use super::*;
535
536        #[test]
537        fn test_full_to_string() {
538            assert_ne!(
539                format!["{:#?}", ProjectFile::default()],
540                DEFAULT_STR_FILE,
541                "debug formatting should not be 'file' representation",
542            );
543        }
544
545        #[test]
546        fn test_metadata_to_string() {
547            assert_ne!(
548                format!["{:#?}", OsMetadata::default().to_string()],
549                DEFAULT_STR_META,
550                "debug formatting should not be 'file' representation",
551            );
552        }
553
554        #[test]
555        fn test_states_to_string() {
556            assert_ne!(
557                format!["{:#?}", State::default().to_string()],
558                DEFAULT_STR_STATE,
559                "debug formatting should not be 'file' representation",
560            );
561        }
562
563        #[test]
564        fn test_settings_to_string() {
565            assert_ne!(
566                format!["{:#?}", Settings::default().to_string()],
567                DEFAULT_STR_SETTINGS,
568                "debug formatting should not be 'file' representation",
569            );
570        }
571
572        #[test]
573        fn test_sample_slots_to_string() {
574            assert_ne!(
575                format!["{:#?}", SlotsAttributes::default().to_string()],
576                DEFAULT_STR_SLOTS,
577                "debug formatting should not be 'file' representation",
578            );
579        }
580    }
581}