Skip to main content

ot_tools_io/
projects.rs

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