/*
SPDX-License-Identifier: GPL-3.0-or-later
Copyright © 2026 Mike Robeson [dijksterhuis]
*/
//! Types and ser/de of `project.*` data files.
mod metadata;
pub mod settings;
mod slots;
mod states;
pub use crate::projects::{
metadata::OsMetadata, settings::Settings, slots::ProjectSlotsError, slots::SlotAttributes,
slots::DEFAULT_GAIN, slots::DEFAULT_TEMPO, states::State,
};
use crate::generics::Slots;
use crate::settings::InvalidValueError;
use crate::{
HasChecksumField, HasFileVersionField, HasHeaderField, OctatrackFileIO, OtToolsIoError,
};
use ot_tools_io_derive::{AsMutDerive, AsRefDerive, IsDefaultCheck};
use serde::{Deserialize, Serialize};
use std::{
cmp::PartialEq, collections::HashMap, fmt::Debug, fmt::Display, num::ParseIntError,
str::FromStr, str::ParseBoolError,
};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ProjectParseError {
#[error("failed to parse integer value")]
Int(#[from] ParseIntError),
#[error("failed to parse boolean value")]
Bool(#[from] ParseBoolError),
#[error("failed to parse string value")]
String,
#[error("failed to parse value: {0} (ot_tools_io::common_options::InvalidValueErrors)")]
InvalidValue(#[from] InvalidValueError),
#[error("failed to load hash map for parsing")]
HashMap,
#[error("missing field: {key} (likely incompatible OS version)")]
MissingField { key: String },
#[error("failed to parse footer data")]
Footer,
#[error("infallible")]
Infallible(#[from] std::convert::Infallible),
}
/// Project file errors
#[derive(Debug, Error)]
pub enum ProjectError {
/// Type cannot be checksummed
#[error("type cannot be checksummed")]
NotChecksummable,
/// Type does not have a data patch file version field
#[error("type does not have a file patch version field")]
NoFilePatchVersionField,
/// Type does not have a header field
#[error("type does not have a header field")]
NoHeaderField,
}
/// The Elektron Octatrack OS project versions this library can be used with.
pub const ALLOWED_OS_VERSIONS: [&str; 3] = ["1.40A", "1.40B", "1.40C"];
// NOTE: https://stackoverflow.com/questions/67352894/rust-error-conversion-for-generic-fromstr-unwrap-errors-out-with-unsatisfied-tr
/// Return the string value of a `HashMap<_, String>` parsed into specified type `T`
// TODO: Need to handle errors more cleanly
fn parse_hashmap_string_value<T: FromStr>(
hmap: &HashMap<String, String>,
key: &str,
default_str: Option<&str>,
) -> Result<T, ProjectParseError>
where
<T as FromStr>::Err: Debug,
ProjectParseError: From<<T as FromStr>::Err>,
{
let val = match default_str {
Some(x) => Ok(hmap.get(key).cloned().unwrap_or(x.to_string())),
None => hmap
.get(key)
.cloned()
.ok_or(ProjectParseError::MissingField {
key: key.to_string().to_uppercase(),
}),
}?;
let parsed = val.parse::<T>()?;
Ok(parsed)
}
/// Return the string value of a `HashMap<_, String>` parsed into a boolean value
/// (any parsed value != 1 returns `false`)
fn parse_hashmap_string_value_bool(
hmap: &HashMap<String, String>,
key: &str,
default_str: Option<&str>,
) -> Result<bool, ProjectParseError> {
let val = parse_hashmap_string_value::<u8>(hmap, key, default_str)?;
Ok(matches!(val, 1))
}
/// Extract ASCII string project data for a specified section as a HashMap of k-v pairs.
fn string_to_hashmap(
data: &str,
section: &SectionHeader,
) -> Result<HashMap<String, String>, ProjectParseError> {
let start = format!("[{section}]");
let end = format!("[/{section}]");
let start_idx = data.find(&start).ok_or(ProjectParseError::HashMap)?;
let start_idx_shifted: usize = start_idx + start.len();
let end_idx = data.find(&end).ok_or(ProjectParseError::HashMap)?;
let section: String = data[start_idx_shifted..end_idx].to_string();
let mut hmap: HashMap<String, String> = HashMap::new();
let mut trig_mode_midi_field_idx = 1;
for split_s in section.split("\r\n") {
// new line splits returns empty fields :/
if !split_s.is_empty() {
let key_pair_string = split_s.to_string();
let mut key_pair_split: Vec<&str> = key_pair_string.split('=').collect();
// there are 8x TRIG_MODE_MIDI key value pairs in project settings data
// but the keys do not have audio track number indicators. i assume they're
// stored in order of the midi track number, and each subsequent one we
// read is the next track.
let key_renamed: String = format!("trig_mode_midi_track_{}", trig_mode_midi_field_idx);
if key_pair_split[0] == "TRIG_MODE_MIDI" {
key_pair_split[0] = key_renamed.as_str();
trig_mode_midi_field_idx += 1;
}
hmap.insert(
key_pair_split[0].to_string().to_ascii_lowercase(),
key_pair_split[1].to_string(),
);
}
}
Ok(hmap)
}
#[derive(Debug, Error)]
#[error("invalid project section header value")]
struct InvalidSectionHeaderValue;
/// ASCII data section headings within an Octatrack `project.*` file
#[derive(Debug, PartialEq)]
enum SectionHeader {
Meta,
States,
Settings,
Samples,
}
impl TryFrom<&str> for SectionHeader {
type Error = InvalidSectionHeaderValue;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value.to_ascii_uppercase().as_str() {
"META" => Ok(Self::Meta),
"STATES" => Ok(Self::States),
"SETTINGS" => Ok(Self::Settings),
"SAMPLES" => Ok(Self::Samples),
_ => Err(InvalidSectionHeaderValue),
}
}
}
impl TryFrom<String> for SectionHeader {
type Error = InvalidSectionHeaderValue;
fn try_from(value: String) -> Result<Self, Self::Error> {
value.as_str().try_into()
}
}
impl Display for SectionHeader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let str = match self {
Self::Meta => "META".to_string(),
Self::States => "STATES".to_string(),
Self::Settings => "SETTINGS".to_string(),
Self::Samples => "SAMPLES".to_string(),
};
write!(f, "{str}")
}
}
#[cfg(test)]
mod test_project_section {
mod try_from_str {
use crate::projects::{InvalidSectionHeaderValue, SectionHeader};
#[test]
fn no_match_is_err() {
assert_eq!(
SectionHeader::try_from("skfsdkfjskdh")
.unwrap_err()
.to_string(),
InvalidSectionHeaderValue.to_string()
)
}
#[test]
fn uppercase_meta() {
assert_eq!(
SectionHeader::try_from("META").unwrap(),
SectionHeader::Meta,
)
}
#[test]
fn uppercase_states() {
assert_eq!(
SectionHeader::try_from("STATES").unwrap(),
SectionHeader::States,
)
}
#[test]
fn uppercase_settings() {
assert_eq!(
SectionHeader::try_from("SETTINGS").unwrap(),
SectionHeader::Settings,
)
}
#[test]
fn uppercase_samples() {
assert_eq!(
SectionHeader::try_from("SAMPLES").unwrap(),
SectionHeader::Samples,
)
}
#[test]
fn lowercase_meta() {
assert_eq!(
SectionHeader::try_from("meta").unwrap(),
SectionHeader::Meta,
)
}
#[test]
fn lowercase_states() {
assert_eq!(
SectionHeader::try_from("states").unwrap(),
SectionHeader::States,
)
}
#[test]
fn lowercase_settings() {
assert_eq!(
SectionHeader::try_from("settings").unwrap(),
SectionHeader::Settings,
)
}
#[test]
fn lowercase_samples() {
assert_eq!(
SectionHeader::try_from("samples").unwrap(),
SectionHeader::Samples,
)
}
}
mod try_from_string {
use crate::projects::SectionHeader;
#[test]
fn no_match_is_err() {
assert!(SectionHeader::try_from("skfsdkfjskdh".to_string()).is_err())
}
#[test]
fn uppercase_meta() {
assert_eq!(
SectionHeader::try_from("META".to_string()).unwrap(),
SectionHeader::Meta,
)
}
#[test]
fn uppercase_states() {
assert_eq!(
SectionHeader::try_from("STATES".to_string()).unwrap(),
SectionHeader::States,
)
}
#[test]
fn uppercase_settings() {
assert_eq!(
SectionHeader::try_from("SETTINGS".to_string()).unwrap(),
SectionHeader::Settings,
)
}
#[test]
fn uppercase_samples() {
assert_eq!(
SectionHeader::try_from("SAMPLES".to_string()).unwrap(),
SectionHeader::Samples,
)
}
#[test]
fn lowercase_meta() {
assert_eq!(
SectionHeader::try_from("meta".to_string()).unwrap(),
SectionHeader::Meta,
)
}
#[test]
fn lowercase_states() {
assert_eq!(
SectionHeader::try_from("states".to_string()).unwrap(),
SectionHeader::States,
)
}
#[test]
fn lowercase_settings() {
assert_eq!(
SectionHeader::try_from("settings".to_string()).unwrap(),
SectionHeader::Settings,
)
}
#[test]
fn lowercase_samples() {
assert_eq!(
SectionHeader::try_from("samples".to_string()).unwrap(),
SectionHeader::Samples,
)
}
}
mod to_string {
use crate::projects::SectionHeader;
#[test]
fn section_heading_value_meta() {
assert_eq!(SectionHeader::Meta.to_string(), "META".to_string())
}
#[test]
fn section_heading_value_states() {
assert_eq!(SectionHeader::States.to_string(), "STATES".to_string())
}
#[test]
fn section_heading_value_settings() {
assert_eq!(SectionHeader::Settings.to_string(), "SETTINGS".to_string())
}
#[test]
fn section_heading_value_samples() {
assert_eq!(SectionHeader::Samples.to_string(), "SAMPLES".to_string())
}
}
}
/// A parsed representation of an Octatrack Project file (`project.work` or `project.strd`).
///
/// **Note**: We derive [`serde::Deserialize`] and [`serde::Serialize`] on this (and all the included subtypes).
/// But project files are actually string data being parsed directly without
/// any [`serde`]-ing or [`bincode`]-ing. This may change in the future to custom
/// Serialize/Deserialize implementations for string source file data.
/// But that will only happen if i can work out how to differentiate between
/// deserializing a yaml/json string and a raw data file's string data
/// (we use [`serde::de::Deserializer::is_human_readable`] for arrangements
/// to differentiate between YAML/JSON and string data files, which is a problem here as
/// we'll be testing for the difference between YAML/JSON (string data) and String (string data).)
///
/// No `Copy` trait as neither [`OsMetadata`] nor [`SlotAttributes`] support the `Copy` trait
// TODO: Switch to custom Deserialize/Serialize parsing from string data,
// as per arrangements.
#[derive(
Clone,
Debug,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
IsDefaultCheck,
Serialize,
Deserialize,
AsMutDerive,
AsRefDerive,
)]
pub struct ProjectFile {
/// Metadata key-value pairs from a Project file.
pub metadata: OsMetadata,
/// Settings key-value pairs from a Project file.
pub settings: Settings,
/// States key-value pairs from a Project file.
pub states: State,
/// Slots key-value pairs from a Project file.
pub slots: Slots<Option<SlotAttributes>>,
}
impl ProjectFile {
/// Check to see if the [`ProjectFile`] has a valid OS version for the
/// current version of this library. The OS version string is contained in
/// the `project.*` file under the `x.metadata.os_version` field. Multiple
/// OS versions could be supported at any one time.
///
/// ```rust
/// # use ot_tools_io::ProjectFile;
/// # use ot_tools_io::OctatrackFileIO;
/// # let path = std::path::PathBuf::from("test-data")
/// # .join("blank-project")
/// # .join("project.work");
/// let proj = ProjectFile::from_data_file(&path).unwrap();
/// assert!(proj.check_compatible_os_version().unwrap());
/// ```
pub fn check_compatible_os_version(&self) -> Result<bool, OtToolsIoError> {
Ok(crate::ALLOWED_OS_VERSIONS
.iter()
.any(|x| self.metadata.os_version.ends_with(x)))
}
}
#[cfg(test)]
mod check_os_version {
use crate::test_utils::get_blank_proj_dirpath;
use crate::OctatrackFileIO;
use crate::OtToolsIoError;
use crate::ProjectFile;
#[test]
fn valid() -> Result<(), OtToolsIoError> {
let path = get_blank_proj_dirpath().join("project.work");
let proj = ProjectFile::from_data_file(&path)?;
assert!(proj.check_compatible_os_version()?);
Ok(())
}
#[test]
fn invalid_random() -> Result<(), OtToolsIoError> {
let path = get_blank_proj_dirpath().join("project.work");
let mut proj = ProjectFile::from_data_file(&path)?;
proj.metadata.os_version = "sakjhasjfh".to_string();
assert!(!proj.check_compatible_os_version()?);
Ok(())
}
#[test]
fn invalid_old() -> Result<(), OtToolsIoError> {
let path = get_blank_proj_dirpath().join("project.work");
let mut proj = ProjectFile::from_data_file(&path)?;
proj.metadata.os_version = "1.39D".to_string();
assert!(!proj.check_compatible_os_version()?);
Ok(())
}
#[test]
fn invalid_new() -> Result<(), OtToolsIoError> {
let path = get_blank_proj_dirpath().join("project.work");
let mut proj = ProjectFile::from_data_file(&path)?;
proj.metadata.os_version = "1.41A".to_string();
assert!(!proj.check_compatible_os_version()?);
Ok(())
}
}
impl Default for ProjectFile {
fn default() -> Self {
let metadata = OsMetadata::default();
let states = State::default();
let settings = Settings::default();
let slots = Slots::<Option<SlotAttributes>>::default();
Self {
metadata,
settings,
states,
slots,
}
}
}
impl std::fmt::Display for ProjectFile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
let states_header =
"############################\r\n# Project States\r\n############################"
.to_string();
let settings_header =
"############################\r\n# Project Settings\r\n############################"
.to_string();
let slots_header =
"############################\r\n# Samples\r\n############################".to_string();
let footer = "############################".to_string();
let metadata_string: String = self.metadata.to_string();
let states_string: String = self.states.to_string();
let settings_string: String = self.settings.to_string();
let sample_slots_string = self.slots.to_string();
let v: Vec<String> = vec![
// META and SETTINGS are subsections of 'Project Settings'...
// so have to do this joining in `ProjectFile`
settings_header,
metadata_string,
settings_string,
states_header,
states_string,
slots_header,
sample_slots_string,
footer,
];
let mut project_string = v.join("\r\n\r\n");
project_string.push_str("\r\n\r\n");
write!(f, "{project_string}")
}
}
impl OctatrackFileIO for ProjectFile {
// For project data, need to read the data in as a string, then split the
// structs out from the string data.
fn from_bytes(bytes: &[u8]) -> Result<Self, OtToolsIoError> {
let (cow, _, _) = encoding_rs::WINDOWS_1258.decode(bytes);
let s = cow.into_owned();
let metadata = OsMetadata::from_str(&s)?;
let states = State::from_str(&s)?;
let settings = Settings::from_str(&s)?;
let slots = Slots::<Option<SlotAttributes>>::from_str(&s)?;
Ok(Self {
metadata,
settings,
states,
slots,
})
}
// For project data, need to convert to string values again then into bytes
fn to_bytes(&self) -> Result<Vec<u8>, OtToolsIoError> {
let s = self.to_string();
let (cow, _, _) = encoding_rs::WINDOWS_1258.encode(&s);
Ok(cow.to_vec())
}
}
impl HasChecksumField for ProjectFile {
fn calculate_checksum(&self) -> Result<u16, OtToolsIoError> {
Err(ProjectError::NotChecksummable.into())
}
fn update_checksum(&mut self) -> Result<(), OtToolsIoError> {
Err(ProjectError::NotChecksummable.into())
}
fn check_checksum(&self) -> Result<bool, OtToolsIoError> {
Err(ProjectError::NotChecksummable.into())
}
}
#[cfg(test)]
mod checksum_field {
use crate::test_utils::get_blank_proj_dirpath;
use crate::{HasChecksumField, OctatrackFileIO, OtToolsIoError, ProjectFile};
#[test]
fn fail_calculate_checksum() -> Result<(), OtToolsIoError> {
let infile = get_blank_proj_dirpath().join("project.work");
let p = ProjectFile::from_data_file(&infile)?;
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)");
Ok(())
}
#[test]
fn fail_check_checksum() -> Result<(), OtToolsIoError> {
let infile = get_blank_proj_dirpath().join("project.work");
let p = ProjectFile::from_data_file(&infile)?;
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)");
Ok(())
}
}
impl HasHeaderField for ProjectFile {
fn check_header(&self) -> Result<bool, OtToolsIoError> {
Err(ProjectError::NoHeaderField.into())
}
}
#[cfg(test)]
mod header_field {
use crate::test_utils::get_blank_proj_dirpath;
use crate::{HasHeaderField, OctatrackFileIO, OtToolsIoError, ProjectFile};
#[test]
fn fail_check_header() -> Result<(), OtToolsIoError> {
let infile = get_blank_proj_dirpath().join("project.work");
let p = ProjectFile::from_data_file(&infile)?;
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)");
Ok(())
}
}
impl HasFileVersionField for ProjectFile {
fn check_file_version(&self) -> Result<bool, OtToolsIoError> {
Err(ProjectError::NoFilePatchVersionField.into())
}
}
#[cfg(test)]
mod file_version_field {
use crate::test_utils::get_blank_proj_dirpath;
use crate::{HasFileVersionField, OctatrackFileIO, OtToolsIoError, ProjectFile};
#[test]
fn fail_check_version() -> Result<(), OtToolsIoError> {
let infile = get_blank_proj_dirpath().join("project.work");
let p = ProjectFile::from_data_file(&infile)?;
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)");
Ok(())
}
}
#[cfg(test)]
#[allow(unused_imports)]
mod tests {
use super::*;
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";
const DEFAULT_STR_META: &str = "[META]\r\nTYPE=OCTATRACK DPS-1 PROJECT\r\nVERSION=19\r\nOS_VERSION=R0177 1.40B\r\n[/META]";
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]";
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]";
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]";
// make sure to_string() and display() for a project type displays
// the file format representation, not a debug struct
mod test_to_string_display {
use super::*;
#[test]
fn test_full_to_string() {
assert_eq!(ProjectFile::default().to_string(), DEFAULT_STR_FILE);
assert_eq!(format!["{:#}", ProjectFile::default()], DEFAULT_STR_FILE);
}
#[test]
fn test_metadata_to_string() {
assert_eq!(OsMetadata::default().to_string(), DEFAULT_STR_META);
assert_eq!(format!["{:#}", OsMetadata::default()], DEFAULT_STR_META);
}
#[test]
fn test_states_to_string() {
assert_eq!(State::default().to_string(), DEFAULT_STR_STATE);
assert_eq!(format!["{:#}", State::default()], DEFAULT_STR_STATE);
}
#[test]
fn test_settings_to_string() {
assert_eq!(Settings::default().to_string(), DEFAULT_STR_SETTINGS);
assert_eq!(format!["{:#}", Settings::default()], DEFAULT_STR_SETTINGS);
}
#[test]
fn test_sample_slots_to_string() {
assert_eq!(
Slots::<Option<SlotAttributes>>::default().to_string(),
DEFAULT_STR_SLOTS,
);
assert_eq!(
format!["{:#}", Slots::<Option<SlotAttributes>>::default()],
DEFAULT_STR_SLOTS
);
}
}
// make sure debug formatting is not the same as display formatting
mod test_to_string_debug {
use super::*;
#[test]
fn test_full_to_string() {
assert_ne!(
format!["{:#?}", ProjectFile::default()],
DEFAULT_STR_FILE,
"debug formatting should not be 'file' representation",
);
}
#[test]
fn test_metadata_to_string() {
assert_ne!(
format!["{:#?}", OsMetadata::default().to_string()],
DEFAULT_STR_META,
"debug formatting should not be 'file' representation",
);
}
#[test]
fn test_states_to_string() {
assert_ne!(
format!["{:#?}", State::default().to_string()],
DEFAULT_STR_STATE,
"debug formatting should not be 'file' representation",
);
}
#[test]
fn test_settings_to_string() {
assert_ne!(
format!["{:#?}", Settings::default().to_string()],
DEFAULT_STR_SETTINGS,
"debug formatting should not be 'file' representation",
);
}
#[test]
fn test_sample_slots_to_string() {
assert_ne!(
format![
"{:#?}",
Slots::<Option<SlotAttributes>>::default().to_string()
],
DEFAULT_STR_SLOTS,
"debug formatting should not be 'file' representation",
);
}
}
}