1mod 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#[derive(Debug, Error)]
53pub enum ProjectError {
54 #[error("type cannot be checksummed")]
56 NotChecksummable,
57 #[error("type does not have a file patch version field")]
59 NoFilePatchVersionField,
60 #[error("type does not have a header field")]
62 NoHeaderField,
63}
64
65pub const ALLOWED_OS_VERSIONS: [&str; 3] = ["1.40A", "1.40B", "1.40C"];
67
68fn 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
94fn 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
105fn 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 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 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#[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#[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 pub metadata: OsMetadata,
403
404 pub settings: Settings,
406
407 pub states: State,
409
410 pub slots: Slots<Option<SlotAttributes>>,
412}
413
414impl ProjectFile {
415 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 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 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 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 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 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}