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("missing field: {key} (likely incompatible OS version)")]
43    MissingField { key: String },
44    #[error("failed to parse footer data")]
45    Footer,
46    #[error("infallible")]
47    Infallible(#[from] std::convert::Infallible),
48}
49
50/// Project file errors
51#[derive(Debug, Error)]
52pub enum ProjectError {
53    /// Type cannot be checksummed
54    #[error("type cannot be checksummed")]
55    NotChecksummable,
56    /// Type does not have a data patch file version field
57    #[error("type does not have a file patch version field")]
58    NoFilePatchVersionField,
59    /// Type does not have a header field
60    #[error("type does not have a header field")]
61    NoHeaderField,
62}
63
64/// The Elektron Octatrack OS project versions this library can be used with.
65pub const ALLOWED_OS_VERSIONS: [&str; 3] = ["1.40A", "1.40B", "1.40C"];
66
67// NOTE: https://stackoverflow.com/questions/67352894/rust-error-conversion-for-generic-fromstr-unwrap-errors-out-with-unsatisfied-tr
68/// Return the string value of a `HashMap<_, String>` parsed into specified type `T`
69// TODO: Need to handle errors more cleanly
70fn parse_hashmap_string_value<T: FromStr>(
71    hmap: &HashMap<String, String>,
72    key: &str,
73    default_str: Option<&str>,
74) -> Result<T, ProjectParseError>
75where
76    <T as FromStr>::Err: Debug,
77    ProjectParseError: From<<T as FromStr>::Err>,
78{
79    let val = match default_str {
80        Some(x) => Ok(hmap.get(key).cloned().unwrap_or(x.to_string())),
81        None => hmap
82            .get(key)
83            .cloned()
84            .ok_or(ProjectParseError::MissingField {
85                key: key.to_string().to_uppercase(),
86            }),
87    }?;
88
89    let parsed = val.parse::<T>()?;
90    Ok(parsed)
91}
92
93/// Return the string value of a `HashMap<_, String>` parsed into a boolean value
94/// (any parsed value != 1 returns `false`)
95fn parse_hashmap_string_value_bool(
96    hmap: &HashMap<String, String>,
97    key: &str,
98    default_str: Option<&str>,
99) -> Result<bool, ProjectParseError> {
100    let val = parse_hashmap_string_value::<u8>(hmap, key, default_str)?;
101    Ok(matches!(val, 1))
102}
103
104/// Extract ASCII string project data for a specified section as a HashMap of k-v pairs.
105fn string_to_hashmap(
106    data: &str,
107    section: &SectionHeader,
108) -> Result<HashMap<String, String>, ProjectParseError> {
109    let start = format!("[{section}]");
110    let end = format!("[/{section}]");
111
112    let start_idx = data.find(&start).ok_or(ProjectParseError::HashMap)?;
113    let start_idx_shifted: usize = start_idx + start.len();
114    let end_idx = data.find(&end).ok_or(ProjectParseError::HashMap)?;
115
116    let section: String = data[start_idx_shifted..end_idx].to_string();
117
118    let mut hmap: HashMap<String, String> = HashMap::new();
119    let mut trig_mode_midi_field_idx = 1;
120
121    for split_s in section.split("\r\n") {
122        // new line splits returns empty fields :/
123
124        if !split_s.is_empty() {
125            let key_pair_string = split_s.to_string();
126            let mut key_pair_split: Vec<&str> = key_pair_string.split('=').collect();
127
128            // there are 8x TRIG_MODE_MIDI key value pairs in project settings data
129            // but the keys do not have audio track number indicators. i assume they're
130            // stored in order of the midi track number, and each subsequent one we
131            // read is the next track.
132            let key_renamed: String = format!("trig_mode_midi_track_{}", &trig_mode_midi_field_idx);
133            if key_pair_split[0] == "TRIG_MODE_MIDI" {
134                key_pair_split[0] = key_renamed.as_str();
135                trig_mode_midi_field_idx += 1;
136            }
137
138            hmap.insert(
139                key_pair_split[0].to_string().to_ascii_lowercase(),
140                key_pair_split[1].to_string(),
141            );
142        }
143    }
144
145    Ok(hmap)
146}
147
148#[derive(Debug, Error)]
149#[error("invalid project section header value")]
150struct InvalidSectionHeaderValue;
151
152/// ASCII data section headings within an Octatrack `project.*` file
153#[derive(Debug, PartialEq)]
154enum SectionHeader {
155    Meta,
156    States,
157    Settings,
158    Samples,
159}
160
161impl TryFrom<&str> for SectionHeader {
162    type Error = InvalidSectionHeaderValue;
163    fn try_from(value: &str) -> Result<Self, Self::Error> {
164        match value.to_ascii_uppercase().as_str() {
165            "META" => Ok(Self::Meta),
166            "STATES" => Ok(Self::States),
167            "SETTINGS" => Ok(Self::Settings),
168            "SAMPLES" => Ok(Self::Samples),
169            _ => Err(InvalidSectionHeaderValue),
170        }
171    }
172}
173
174impl TryFrom<String> for SectionHeader {
175    type Error = InvalidSectionHeaderValue;
176    fn try_from(value: String) -> Result<Self, Self::Error> {
177        value.as_str().try_into()
178    }
179}
180
181impl Display for SectionHeader {
182    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183        let str = match self {
184            Self::Meta => "META".to_string(),
185            Self::States => "STATES".to_string(),
186            Self::Settings => "SETTINGS".to_string(),
187            Self::Samples => "SAMPLES".to_string(),
188        };
189        write!(f, "{str}")
190    }
191}
192
193#[cfg(test)]
194mod test_project_section {
195
196    mod try_from_str {
197        use crate::projects::{InvalidSectionHeaderValue, SectionHeader};
198
199        #[test]
200        fn no_match_is_err() {
201            assert_eq!(
202                SectionHeader::try_from("skfsdkfjskdh")
203                    .unwrap_err()
204                    .to_string(),
205                InvalidSectionHeaderValue.to_string()
206            )
207        }
208
209        #[test]
210        fn uppercase_meta() {
211            assert_eq!(
212                SectionHeader::try_from("META").unwrap(),
213                SectionHeader::Meta,
214            )
215        }
216
217        #[test]
218        fn uppercase_states() {
219            assert_eq!(
220                SectionHeader::try_from("STATES").unwrap(),
221                SectionHeader::States,
222            )
223        }
224
225        #[test]
226        fn uppercase_settings() {
227            assert_eq!(
228                SectionHeader::try_from("SETTINGS").unwrap(),
229                SectionHeader::Settings,
230            )
231        }
232
233        #[test]
234        fn uppercase_samples() {
235            assert_eq!(
236                SectionHeader::try_from("SAMPLES").unwrap(),
237                SectionHeader::Samples,
238            )
239        }
240
241        #[test]
242        fn lowercase_meta() {
243            assert_eq!(
244                SectionHeader::try_from("meta").unwrap(),
245                SectionHeader::Meta,
246            )
247        }
248
249        #[test]
250        fn lowercase_states() {
251            assert_eq!(
252                SectionHeader::try_from("states").unwrap(),
253                SectionHeader::States,
254            )
255        }
256
257        #[test]
258        fn lowercase_settings() {
259            assert_eq!(
260                SectionHeader::try_from("settings").unwrap(),
261                SectionHeader::Settings,
262            )
263        }
264
265        #[test]
266        fn lowercase_samples() {
267            assert_eq!(
268                SectionHeader::try_from("samples").unwrap(),
269                SectionHeader::Samples,
270            )
271        }
272    }
273
274    mod try_from_string {
275        use crate::projects::SectionHeader;
276
277        #[test]
278        fn no_match_is_err() {
279            assert!(SectionHeader::try_from("skfsdkfjskdh".to_string()).is_err())
280        }
281
282        #[test]
283        fn uppercase_meta() {
284            assert_eq!(
285                SectionHeader::try_from("META".to_string()).unwrap(),
286                SectionHeader::Meta,
287            )
288        }
289
290        #[test]
291        fn uppercase_states() {
292            assert_eq!(
293                SectionHeader::try_from("STATES".to_string()).unwrap(),
294                SectionHeader::States,
295            )
296        }
297
298        #[test]
299        fn uppercase_settings() {
300            assert_eq!(
301                SectionHeader::try_from("SETTINGS".to_string()).unwrap(),
302                SectionHeader::Settings,
303            )
304        }
305
306        #[test]
307        fn uppercase_samples() {
308            assert_eq!(
309                SectionHeader::try_from("SAMPLES".to_string()).unwrap(),
310                SectionHeader::Samples,
311            )
312        }
313
314        #[test]
315        fn lowercase_meta() {
316            assert_eq!(
317                SectionHeader::try_from("meta".to_string()).unwrap(),
318                SectionHeader::Meta,
319            )
320        }
321
322        #[test]
323        fn lowercase_states() {
324            assert_eq!(
325                SectionHeader::try_from("states".to_string()).unwrap(),
326                SectionHeader::States,
327            )
328        }
329
330        #[test]
331        fn lowercase_settings() {
332            assert_eq!(
333                SectionHeader::try_from("settings".to_string()).unwrap(),
334                SectionHeader::Settings,
335            )
336        }
337        #[test]
338        fn lowercase_samples() {
339            assert_eq!(
340                SectionHeader::try_from("samples".to_string()).unwrap(),
341                SectionHeader::Samples,
342            )
343        }
344    }
345
346    mod to_string {
347        use crate::projects::SectionHeader;
348        #[test]
349        fn section_heading_value_meta() {
350            assert_eq!(SectionHeader::Meta.to_string(), "META".to_string())
351        }
352
353        #[test]
354        fn section_heading_value_states() {
355            assert_eq!(SectionHeader::States.to_string(), "STATES".to_string())
356        }
357
358        #[test]
359        fn section_heading_value_settings() {
360            assert_eq!(SectionHeader::Settings.to_string(), "SETTINGS".to_string())
361        }
362
363        #[test]
364        fn section_heading_value_samples() {
365            assert_eq!(SectionHeader::Samples.to_string(), "SAMPLES".to_string())
366        }
367    }
368}
369
370/// A parsed representation of an Octatrack Project file (`project.work` or `project.strd`).
371///
372/// **Note**: We derive [`serde::Deserialize`] and [`serde::Serialize`] on this (and all the included subtypes).
373/// But project files are actually string data being parsed directly without
374/// any [`serde`]-ing or [`bincode`]-ing. This may change in the future to custom
375/// Serialize/Deserialize implementations for string source file data.
376/// But that will only happen if i can work out how to differentiate between
377/// deserializing a yaml/json string and a raw data file's string data
378/// (we use [`serde::de::Deserializer::is_human_readable`] for arrangements
379/// to differentiate between YAML/JSON and string data files, which is a problem here as
380/// we'll be testing for the difference between YAML/JSON (string data) and String (string data).)
381// TODO: Switch to custom Deserialize/Serialize parsing from string data,
382//       as per arrangements.
383#[derive(IsDefaultCheck, Serialize, Deserialize, PartialEq, Debug, Clone)]
384pub struct ProjectFile {
385    /// Metadata key-value pairs from a Project file.
386    pub metadata: OsMetadata,
387
388    /// Settings key-value pairs from a Project file.
389    pub settings: Settings,
390
391    /// States key-value pairs from a Project file.
392    pub states: State,
393
394    /// Slots key-value pairs from a Project file.
395    pub slots: SlotsAttributes,
396}
397
398impl ProjectFile {
399    /// Check to see if the [`ProjectFile`] has a valid OS version for the
400    /// current version of this library. The OS version string is contained in
401    /// the `project.*` file under the `x.metadata.os_version` field. Multiple
402    /// OS versions could be supported at any one time.
403    ///
404    /// ```rust
405    /// # use ot_tools_io::ProjectFile;
406    /// # use ot_tools_io::OctatrackFileIO;
407    /// # let path = std::path::PathBuf::from("test-data")
408    /// #     .join("blank-project")
409    /// #     .join("project.work");
410    /// let proj = ProjectFile::from_data_file(&path).unwrap();
411    /// assert!(proj.check_compatible_os_version().unwrap());
412    /// ```
413    pub fn check_compatible_os_version(&self) -> Result<bool, OtToolsIoError> {
414        Ok(crate::ALLOWED_OS_VERSIONS
415            .iter()
416            .any(|x| self.metadata.os_version.ends_with(x)))
417    }
418}
419
420#[cfg(test)]
421mod check_os_version {
422
423    use crate::test_utils::get_blank_proj_dirpath;
424    use crate::OctatrackFileIO;
425    use crate::OtToolsIoError;
426    use crate::ProjectFile;
427
428    #[test]
429    fn valid() -> Result<(), OtToolsIoError> {
430        let path = get_blank_proj_dirpath().join("project.work");
431        let proj = ProjectFile::from_data_file(&path)?;
432        assert!(proj.check_compatible_os_version()?);
433        Ok(())
434    }
435
436    #[test]
437    fn invalid_random() -> Result<(), OtToolsIoError> {
438        let path = get_blank_proj_dirpath().join("project.work");
439        let mut proj = ProjectFile::from_data_file(&path)?;
440        proj.metadata.os_version = "sakjhasjfh".to_string();
441        assert!(!proj.check_compatible_os_version()?);
442        Ok(())
443    }
444
445    #[test]
446    fn invalid_old() -> Result<(), OtToolsIoError> {
447        let path = get_blank_proj_dirpath().join("project.work");
448        let mut proj = ProjectFile::from_data_file(&path)?;
449        proj.metadata.os_version = "1.39D".to_string();
450        assert!(!proj.check_compatible_os_version()?);
451        Ok(())
452    }
453
454    #[test]
455    fn invalid_new() -> Result<(), OtToolsIoError> {
456        let path = get_blank_proj_dirpath().join("project.work");
457        let mut proj = ProjectFile::from_data_file(&path)?;
458        proj.metadata.os_version = "1.41A".to_string();
459        assert!(!proj.check_compatible_os_version()?);
460        Ok(())
461    }
462}
463
464impl Default for ProjectFile {
465    fn default() -> Self {
466        let metadata = OsMetadata::default();
467        let states = State::default();
468        let settings = Settings::default();
469        let slots = SlotsAttributes::default();
470
471        Self {
472            metadata,
473            settings,
474            states,
475            slots,
476        }
477    }
478}
479
480impl std::fmt::Display for ProjectFile {
481    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
482        let states_header =
483            "############################\r\n# Project States\r\n############################"
484                .to_string();
485        let settings_header =
486            "############################\r\n# Project Settings\r\n############################"
487                .to_string();
488        let slots_header =
489            "############################\r\n# Samples\r\n############################".to_string();
490        let footer = "############################".to_string();
491
492        let metadata_string: String = self.metadata.to_string();
493        let states_string: String = self.states.to_string();
494        let settings_string: String = self.settings.to_string();
495
496        let sample_slots_string = self.slots.to_string();
497
498        let v: Vec<String> = vec![
499            // META and SETTINGS are subsections of 'Project Settings'...
500            // so have to do this joining in `ProjectFile`
501            settings_header,
502            metadata_string,
503            settings_string,
504            states_header,
505            states_string,
506            slots_header,
507            sample_slots_string,
508            footer,
509        ];
510        let mut project_string = v.join("\r\n\r\n");
511        project_string.push_str("\r\n\r\n");
512
513        write!(f, "{project_string}")
514    }
515}
516
517impl OctatrackFileIO for ProjectFile {
518    // For project data, need to convert to string values again then into bytes
519    fn encode(&self) -> Result<Vec<u8>, OtToolsIoError> {
520        let s = self.to_string();
521        let (cow, _, _) = encoding_rs::WINDOWS_1258.encode(&s);
522        Ok(cow.to_vec())
523    }
524
525    // For project data, need to read the data in as a string, then split the
526    // structs out from the string data.
527    fn decode(bytes: &[u8]) -> Result<Self, OtToolsIoError> {
528        let (cow, _, _) = encoding_rs::WINDOWS_1258.decode(bytes);
529        let s = cow.into_owned();
530
531        let metadata = OsMetadata::from_str(&s)?;
532        let states = State::from_str(&s)?;
533        let settings = Settings::from_str(&s)?;
534        let slots = SlotsAttributes::from_str(&s)?;
535
536        Ok(Self {
537            metadata,
538            settings,
539            states,
540            slots,
541        })
542    }
543}
544
545impl HasChecksumField for ProjectFile {
546    fn calculate_checksum(&self) -> Result<u16, OtToolsIoError> {
547        Err(ProjectError::NotChecksummable.into())
548    }
549    fn check_checksum(&self) -> Result<bool, OtToolsIoError> {
550        Err(ProjectError::NotChecksummable.into())
551    }
552}
553
554#[cfg(test)]
555mod checksum_field {
556    use crate::test_utils::get_blank_proj_dirpath;
557    use crate::{HasChecksumField, OctatrackFileIO, OtToolsIoError, ProjectFile};
558
559    #[test]
560    fn fail_calculate_checksum() -> Result<(), OtToolsIoError> {
561        let infile = get_blank_proj_dirpath().join("project.work");
562        let p = ProjectFile::from_data_file(&infile)?;
563        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)");
564
565        Ok(())
566    }
567    #[test]
568    fn fail_check_checksum() -> Result<(), OtToolsIoError> {
569        let infile = get_blank_proj_dirpath().join("project.work");
570        let p = ProjectFile::from_data_file(&infile)?;
571        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)");
572
573        Ok(())
574    }
575}
576
577impl HasHeaderField for ProjectFile {
578    fn check_header(&self) -> Result<bool, OtToolsIoError> {
579        Err(ProjectError::NoHeaderField.into())
580    }
581}
582
583#[cfg(test)]
584mod header_field {
585    use crate::test_utils::get_blank_proj_dirpath;
586    use crate::{HasHeaderField, OctatrackFileIO, OtToolsIoError, ProjectFile};
587
588    #[test]
589    fn fail_check_header() -> Result<(), OtToolsIoError> {
590        let infile = get_blank_proj_dirpath().join("project.work");
591        let p = ProjectFile::from_data_file(&infile)?;
592        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)");
593
594        Ok(())
595    }
596}
597
598impl HasFileVersionField for ProjectFile {
599    fn check_file_version(&self) -> Result<bool, OtToolsIoError> {
600        Err(ProjectError::NoFilePatchVersionField.into())
601    }
602}
603
604#[cfg(test)]
605mod file_version_field {
606    use crate::test_utils::get_blank_proj_dirpath;
607    use crate::{HasFileVersionField, OctatrackFileIO, OtToolsIoError, ProjectFile};
608
609    #[test]
610    fn fail_check_version() -> Result<(), OtToolsIoError> {
611        let infile = get_blank_proj_dirpath().join("project.work");
612        let p = ProjectFile::from_data_file(&infile)?;
613        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)");
614
615        Ok(())
616    }
617}
618
619#[cfg(test)]
620#[allow(unused_imports)]
621mod tests {
622    use super::*;
623
624    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";
625    const DEFAULT_STR_META: &str = "[META]\r\nTYPE=OCTATRACK DPS-1 PROJECT\r\nVERSION=19\r\nOS_VERSION=R0177     1.40B\r\n[/META]";
626    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]";
627    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]";
628    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]";
629
630    // make sure to_string() and display() for a project type displays
631    // the file format representation, not a debug struct
632    mod test_to_string_display {
633        use super::*;
634
635        #[test]
636        fn test_full_to_string() {
637            assert_eq!(ProjectFile::default().to_string(), DEFAULT_STR_FILE);
638            assert_eq!(format!["{:#}", ProjectFile::default()], DEFAULT_STR_FILE);
639        }
640
641        #[test]
642        fn test_metadata_to_string() {
643            assert_eq!(OsMetadata::default().to_string(), DEFAULT_STR_META);
644            assert_eq!(format!["{:#}", OsMetadata::default()], DEFAULT_STR_META);
645        }
646
647        #[test]
648        fn test_states_to_string() {
649            assert_eq!(State::default().to_string(), DEFAULT_STR_STATE);
650            assert_eq!(format!["{:#}", State::default()], DEFAULT_STR_STATE);
651        }
652
653        #[test]
654        fn test_settings_to_string() {
655            assert_eq!(Settings::default().to_string(), DEFAULT_STR_SETTINGS);
656            assert_eq!(format!["{:#}", Settings::default()], DEFAULT_STR_SETTINGS);
657        }
658
659        #[test]
660        fn test_sample_slots_to_string() {
661            assert_eq!(SlotsAttributes::default().to_string(), DEFAULT_STR_SLOTS);
662            assert_eq!(
663                format!["{:#}", SlotsAttributes::default()],
664                DEFAULT_STR_SLOTS
665            );
666        }
667    }
668
669    // make sure debug formatting is not the same as display formatting
670    mod test_to_string_debug {
671        use super::*;
672
673        #[test]
674        fn test_full_to_string() {
675            assert_ne!(
676                format!["{:#?}", ProjectFile::default()],
677                DEFAULT_STR_FILE,
678                "debug formatting should not be 'file' representation",
679            );
680        }
681
682        #[test]
683        fn test_metadata_to_string() {
684            assert_ne!(
685                format!["{:#?}", OsMetadata::default().to_string()],
686                DEFAULT_STR_META,
687                "debug formatting should not be 'file' representation",
688            );
689        }
690
691        #[test]
692        fn test_states_to_string() {
693            assert_ne!(
694                format!["{:#?}", State::default().to_string()],
695                DEFAULT_STR_STATE,
696                "debug formatting should not be 'file' representation",
697            );
698        }
699
700        #[test]
701        fn test_settings_to_string() {
702            assert_ne!(
703                format!["{:#?}", Settings::default().to_string()],
704                DEFAULT_STR_SETTINGS,
705                "debug formatting should not be 'file' representation",
706            );
707        }
708
709        #[test]
710        fn test_sample_slots_to_string() {
711            assert_ne!(
712                format!["{:#?}", SlotsAttributes::default().to_string()],
713                DEFAULT_STR_SLOTS,
714                "debug formatting should not be 'file' representation",
715            );
716        }
717    }
718}