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
8pub mod metadata;
9pub mod options;
10pub mod settings;
11pub mod slots;
12pub mod states;
13
14use crate::projects::{
15    metadata::ProjectMetadata, options::ProjectSampleSlotType, settings::ProjectSettings,
16    slots::ProjectSampleSlot, states::ProjectStates,
17};
18use crate::{Decode, Encode, IsDefault, OptionEnumValueConvert, OtToolsIoErrors, RBoxErr};
19use serde::{Deserialize, Serialize};
20use std::cmp::PartialEq;
21use std::{collections::HashMap, fmt::Debug, str::FromStr};
22
23/// Trait to use when a new struct can be created from some hashmap with all the necessary fields.
24trait FromHashMap {
25    /// Type for `HashMap` keys
26    type A;
27
28    /// Type for `HashMap` values
29    type B;
30
31    /// Type for `Self`
32    type T;
33
34    /// Crete a new struct from a `HashMap`.
35    fn from_hashmap(hmap: &HashMap<Self::A, Self::B>) -> RBoxErr<Self::T>;
36}
37
38/// Adds a method to parse a project subtype from a raw string (reading a project file).
39trait ProjectFromString {
40    /// Type for `Self`
41    type T;
42
43    /// Crete a new struct by parsing a `String`.
44    fn from_string(data: &str) -> RBoxErr<Self::T>;
45}
46
47// TODO: implement std ToString trait (s that the name for it?).
48/// Adds a method to convert a subtype of a project to raw string data (writing a project file).
49trait ProjectToString {
50    /// Crete a new struct by parsing a `String`.
51    fn to_string(&self) -> RBoxErr<String>;
52}
53
54/// Return the string value of a `HashMap<_, String>` parsed into specified type `T`
55fn parse_hashmap_string_value<T: FromStr>(
56    hmap: &HashMap<String, String>,
57    key: &str,
58    default_str: Option<&str>,
59) -> Result<T, <T as FromStr>::Err>
60where
61    <T as FromStr>::Err: Debug,
62{
63    match default_str {
64        Some(x) => hmap.get(key).unwrap_or(&x.to_string()).parse::<T>(),
65        None => hmap.get(key).unwrap().parse::<T>(),
66    }
67}
68
69/// Return the string value of a `HashMap<_, String>` parsed into a boolean value
70/// (any parsed value != 1 returns `false`)
71fn parse_hashmap_string_value_bool(
72    hmap: &HashMap<String, String>,
73    key: &str,
74    default_str: Option<&str>,
75) -> RBoxErr<bool> {
76    // NOTE: https://rust-lang.github.io/rust-clippy/master/index.html#match_like_matches_macro
77    Ok(matches!(
78        parse_hashmap_string_value::<u8>(hmap, key, default_str)?,
79        1
80    ))
81}
82
83/// Extract ASCII string project data for a specified section as a HashMap of k-v pairs.
84fn string_to_hashmap(
85    data: &str,
86    section: &ProjectRawFileSection,
87) -> RBoxErr<HashMap<String, String>> {
88    let start_idx: usize = data.find(&section.start_string()?).unwrap();
89    let start_idx_shifted: usize = start_idx + section.start_string()?.len();
90    let end_idx: usize = data.find(&section.end_string()?).unwrap();
91
92    let section: String = data[start_idx_shifted..end_idx].to_string();
93
94    let mut hmap: HashMap<String, String> = HashMap::new();
95    let mut trig_mode_midi_field_idx = 1;
96
97    for split_s in section.split("\r\n") {
98        // new line splits returns empty fields :/
99
100        if !split_s.is_empty() {
101            let key_pair_string = split_s.to_string();
102            let mut key_pair_split: Vec<&str> = key_pair_string.split('=').collect();
103
104            // there are 8x TRIG_MODE_MIDI key value pairs in project settings data
105            // but the keys do not have audio track number indicators. i assume they're
106            // stored in order of the midi track number, and each subsequent one we
107            // read is the next track.
108            let key_renamed: String = format!("trig_mode_midi_track_{}", &trig_mode_midi_field_idx);
109            if key_pair_split[0] == "TRIG_MODE_MIDI" {
110                key_pair_split[0] = key_renamed.as_str();
111                trig_mode_midi_field_idx += 1;
112            }
113
114            hmap.insert(
115                key_pair_split[0].to_string().to_ascii_lowercase(),
116                key_pair_split[1].to_string(),
117            );
118        }
119    }
120
121    Ok(hmap)
122}
123
124fn sslots_vec_to_string(v: &[ProjectSampleSlot]) -> String {
125    let sslots_mapped: Vec<String> = v.iter().map(|x| x.to_string().unwrap()).collect();
126
127    sslots_mapped.join("\r\n\r\n")
128}
129
130/// ASCII data section headings within an Octatrack `project.*` file
131#[derive(Debug, PartialEq)]
132enum ProjectRawFileSection {
133    Meta,
134    States,
135    Settings,
136    Samples,
137}
138
139impl OptionEnumValueConvert for ProjectRawFileSection {
140    type T = ProjectRawFileSection;
141    type V = String;
142
143    fn from_value(v: &Self::V) -> RBoxErr<Self::T> {
144        match v.to_ascii_uppercase().as_str() {
145            "META" => Ok(Self::Meta),
146            "STATES" => Ok(Self::States),
147            "SETTINGS" => Ok(Self::Settings),
148            "SAMPLES" => Ok(Self::Samples),
149            _ => Err(OtToolsIoErrors::NoMatchingOptionEnumValue.into()),
150        }
151    }
152
153    // TODO: doesn't need a Result here as should never error
154    fn value(&self) -> RBoxErr<Self::V> {
155        match self {
156            Self::Meta => Ok("META".to_string()),
157            Self::States => Ok("STATES".to_string()),
158            Self::Settings => Ok("SETTINGS".to_string()),
159            Self::Samples => Ok("SAMPLES".to_string()),
160        }
161    }
162}
163
164impl ProjectRawFileSection {
165    fn start_string(&self) -> RBoxErr<String> {
166        Ok(format!("[{}]", self.value()?))
167    }
168    fn end_string(&self) -> RBoxErr<String> {
169        Ok(format!("[/{}]", self.value()?))
170    }
171}
172
173/// A parsed representation of an Octatrack Project file (`project.work` or `project.strd`).
174#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
175pub struct ProjectFile {
176    /// Metadata key-value pairs from a Project file.
177    pub metadata: ProjectMetadata,
178
179    /// Settings key-value pairs from a Project file.
180    pub settings: ProjectSettings,
181
182    /// States key-value pairs from a Project file.
183    pub states: ProjectStates,
184
185    /// Slots key-value pairs from a Project file.
186    pub slots: Vec<ProjectSampleSlot>,
187}
188
189impl ProjectFile {
190    pub fn update_sample_slot_id(
191        &mut self,
192        old_slot_id: &u8,
193        new_slot_id: &u8,
194        sample_type: Option<ProjectSampleSlotType>,
195    ) -> RBoxErr<()> {
196        use itertools::Itertools;
197        let type_filt = sample_type.unwrap_or(ProjectSampleSlotType::Static);
198
199        let sample_slot_find: Option<(usize, ProjectSampleSlot)> = self
200            .slots
201            .clone()
202            .into_iter()
203            .find_position(|x| x.slot_id == *old_slot_id && x.sample_type == type_filt);
204
205        // there are samples assigned to slots
206        // TODO: option plain unwrap
207        if sample_slot_find.is_some() {
208            println!("Found matchin slot id");
209            let mut sample_slot = sample_slot_find.clone().unwrap().1;
210            sample_slot.slot_id = *new_slot_id;
211            self.slots[sample_slot_find.unwrap().0] = sample_slot;
212        }
213
214        Ok(())
215    }
216}
217
218impl Default for ProjectFile {
219    fn default() -> Self {
220        let metadata = ProjectMetadata::default();
221        let states = ProjectStates::default();
222        let settings = ProjectSettings::default();
223
224        let slots: Vec<ProjectSampleSlot> = ProjectSampleSlot::defaults();
225
226        ProjectFile {
227            metadata,
228            settings,
229            states,
230            slots,
231        }
232    }
233}
234
235impl IsDefault for ProjectFile {
236    fn is_default(&self) -> bool {
237        &ProjectFile::default() == self
238    }
239}
240
241impl ProjectToString for ProjectFile {
242    /// Turn a Project struct into a String configuration, ready for writing to binary data files
243    fn to_string(&self) -> RBoxErr<String> {
244        let states_header =
245            "############################\r\n# Project States\r\n############################"
246                .to_string();
247        let settings_header =
248            "############################\r\n# Project Settings\r\n############################"
249                .to_string();
250        let slots_header =
251            "############################\r\n# Samples\r\n############################".to_string();
252        let footer = "############################".to_string();
253
254        let metadata_string: String = self.metadata.to_string()?;
255        let states_string: String = self.states.to_string()?;
256        let settings_string: String = self.settings.to_string()?;
257
258        let sslots_string = sslots_vec_to_string(&self.slots);
259
260        let v: Vec<String> = vec![
261            settings_header,
262            metadata_string,
263            settings_string,
264            states_header,
265            states_string,
266            slots_header,
267            sslots_string,
268            footer,
269        ];
270
271        let mut project_string = v.join("\r\n\r\n");
272        project_string.push_str("\r\n\r\n");
273        Ok(project_string)
274    }
275}
276
277// For project data, need to read bytes as an utf string, then split the structs out from the string
278// data.
279impl Decode for ProjectFile {
280    fn decode(bytes: &[u8]) -> RBoxErr<Self> {
281        let s = std::str::from_utf8(bytes)?.to_string();
282
283        let metadata = ProjectMetadata::from_string(&s)?;
284        let states = ProjectStates::from_string(&s)?;
285        let settings = ProjectSettings::from_string(&s)?;
286        let slots = ProjectSampleSlot::from_string(&s)?;
287
288        Ok(Self {
289            metadata,
290            settings,
291            states,
292            slots,
293        })
294    }
295}
296
297// For project data, need to convert to string values again then into bytes
298impl Encode for ProjectFile {
299    fn encode(&self) -> RBoxErr<Vec<u8>> {
300        let data = self.to_string()?;
301        let bytes: Vec<u8> = data.bytes().collect::<Vec<u8>>();
302        Ok(bytes)
303    }
304}
305
306#[cfg(test)]
307#[allow(unused_imports)]
308mod tests {
309    use super::*;
310
311    mod test_spec {
312        use super::*;
313
314        #[test]
315        fn section_heading_from_value_no_match_is_err() {
316            assert!(ProjectRawFileSection::from_value(&"skfsdkfjskdh".to_string()).is_err())
317        }
318
319        #[test]
320        fn section_heading_from_value_uppercase_meta() {
321            assert_eq!(
322                ProjectRawFileSection::from_value(&"META".to_string()).unwrap(),
323                ProjectRawFileSection::Meta,
324            )
325        }
326
327        #[test]
328        fn section_heading_from_value_uppercase_states() {
329            assert_eq!(
330                ProjectRawFileSection::from_value(&"STATES".to_string()).unwrap(),
331                ProjectRawFileSection::States,
332            )
333        }
334
335        #[test]
336        fn section_heading_from_value_uppercase_settings() {
337            assert_eq!(
338                ProjectRawFileSection::from_value(&"SETTINGS".to_string()).unwrap(),
339                ProjectRawFileSection::Settings,
340            )
341        }
342
343        #[test]
344        fn section_heading_from_value_uppercase_samples() {
345            assert_eq!(
346                ProjectRawFileSection::from_value(&"SAMPLES".to_string()).unwrap(),
347                ProjectRawFileSection::Samples,
348            )
349        }
350
351        #[test]
352        fn section_heading_from_value_lowercase_meta() {
353            assert_eq!(
354                ProjectRawFileSection::from_value(&"meta".to_string()).unwrap(),
355                ProjectRawFileSection::Meta,
356            )
357        }
358
359        #[test]
360        fn section_heading_from_value_lowercase_states() {
361            assert_eq!(
362                ProjectRawFileSection::from_value(&"states".to_string()).unwrap(),
363                ProjectRawFileSection::States,
364            )
365        }
366
367        #[test]
368        fn section_heading_from_value_lowercase_settings() {
369            assert_eq!(
370                ProjectRawFileSection::from_value(&"settings".to_string()).unwrap(),
371                ProjectRawFileSection::Settings,
372            )
373        }
374
375        #[test]
376        fn section_heading_from_value_lowercase_samples() {
377            assert_eq!(
378                ProjectRawFileSection::from_value(&"samples".to_string()).unwrap(),
379                ProjectRawFileSection::Samples,
380            )
381        }
382
383        #[test]
384        fn section_heading_value_meta() {
385            assert_eq!(
386                ProjectRawFileSection::Meta.value().unwrap(),
387                "META".to_string(),
388            )
389        }
390
391        #[test]
392        fn section_heading_value_states() {
393            assert_eq!(
394                ProjectRawFileSection::States.value().unwrap(),
395                "STATES".to_string(),
396            )
397        }
398
399        #[test]
400        fn section_heading_value_settings() {
401            assert_eq!(
402                ProjectRawFileSection::Settings.value().unwrap(),
403                "SETTINGS".to_string(),
404            )
405        }
406
407        #[test]
408        fn section_heading_value_samples() {
409            assert_eq!(
410                ProjectRawFileSection::Samples.value().unwrap(),
411                "SAMPLES".to_string(),
412            )
413        }
414    }
415
416    mod test_read {
417        use super::*;
418
419        #[test]
420        fn test_full_to_string() {
421            let valid = "############################\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\nTRIM_BARSx100=0\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\nTRIM_BARSx100=0\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\nTRIM_BARSx100=0\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\nTRIM_BARSx100=0\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\nTRIM_BARSx100=0\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\nTRIM_BARSx100=0\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\nTRIM_BARSx100=0\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\nTRIM_BARSx100=0\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]\r\n\r\n############################\r\n\r\n";
422            assert_eq!(ProjectFile::default().to_string().unwrap(), valid);
423        }
424
425        #[test]
426        fn test_metadata_to_string() {
427            let valid = "[META]\r\nTYPE=OCTATRACK DPS-1 PROJECT\r\nVERSION=19\r\nOS_VERSION=R0177     1.40B\r\n[/META]";
428            assert_eq!(ProjectMetadata::default().to_string().unwrap(), valid);
429        }
430
431        #[test]
432        fn test_states_to_string() {
433            let valid = "[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]";
434            assert_eq!(ProjectStates::default().to_string().unwrap(), valid);
435        }
436
437        #[test]
438        fn test_settings_to_string() {
439            let valid = "[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]";
440            assert_eq!(ProjectSettings::default().to_string().unwrap(), valid);
441        }
442
443        #[test]
444        fn test_sslots_to_string() {
445            let valid = "[SAMPLE]\r\nTYPE=FLEX\r\nSLOT=129\r\nPATH=\r\nTRIM_BARSx100=0\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\nTRIM_BARSx100=0\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\nTRIM_BARSx100=0\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\nTRIM_BARSx100=0\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\nTRIM_BARSx100=0\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\nTRIM_BARSx100=0\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\nTRIM_BARSx100=0\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\nTRIM_BARSx100=0\r\nTSMODE=2\r\nLOOPMODE=0\r\nGAIN=72\r\nTRIGQUANTIZATION=255\r\n[/SAMPLE]";
446            assert_eq!(sslots_vec_to_string(&ProjectSampleSlot::defaults()), valid);
447        }
448    }
449}