mod deserialize;
mod serialize;
use crate::generics::ArrangeRows;
use crate::{
Defaults, HasChecksumField, HasFileVersionField, HasHeaderField, IsDefault, OctatrackFileIO,
OtToolsIoError,
};
use ot_tools_io_derive::{
ArrayDefaults, AsMutDerive, AsRefDerive, BoxedArrayDefaults, IntegrityChecks,
};
use serde::Serialize;
use std::array::from_fn;
pub const ARRANGEMENT_FILE_HEADER: [u8; 21] = [
70, 79, 82, 77, 0, 0, 0, 0, 68, 80, 83, 49, 65, 82, 82, 65, 0, 0, 0, 0, 0,
];
pub const ARRANGEMENT_FILE_VERSION: u8 = 6;
const ARRANGEMENT_DEFAULT_NAME: [u8; 15] =
[79, 67, 84, 65, 84, 79, 79, 76, 83, 45, 65, 82, 82, 32, 32];
#[derive(
Clone,
Debug,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
Serialize,
IntegrityChecks,
ArrayDefaults,
BoxedArrayDefaults,
AsRefDerive,
AsMutDerive,
)]
pub struct ArrangementFile {
pub header: [u8; 21],
pub datatype_version: u8,
pub unk1: [u8; 2],
pub arrangement_state_current: ArrangementBlock,
pub unk2: u8,
pub saved_flag: u8,
pub arrangement_state_previous: ArrangementBlock,
pub arrangements_saved_state: [u8; 8],
pub checksum: u16,
}
impl Default for ArrangementFile {
fn default() -> Self {
Self {
header: ARRANGEMENT_FILE_HEADER,
datatype_version: ARRANGEMENT_FILE_VERSION,
unk1: from_fn(|_| 0),
arrangement_state_current: ArrangementBlock::default(),
unk2: 0,
saved_flag: 0,
arrangement_state_previous: ArrangementBlock::default(),
arrangements_saved_state: from_fn(|_| 1),
checksum: 1973,
}
}
}
impl IsDefault for ArrangementFile {
fn is_default(&self) -> bool {
let default = &ArrangementFile::default();
self.arrangement_state_current.is_default()
&& self.arrangement_state_previous.is_default()
&& default.unk1 == self.unk1
&& default.unk2 == self.unk2
}
}
#[cfg(test)]
mod is_default {
use crate::arrangements::ArrangementFile;
use crate::test_utils::get_arrange_dirpath;
use crate::{IsDefault, OctatrackFileIO};
#[test]
fn true_not_modified_default() {
assert!(ArrangementFile::default().is_default())
}
#[test]
fn true_not_modified_file() {
let arr =
ArrangementFile::from_data_file(&get_arrange_dirpath().join("blank.work")).unwrap();
assert!(arr.is_default())
}
#[test]
fn false_modified_file() {
let arr = ArrangementFile::from_data_file(&get_arrange_dirpath().join("full-options.work"))
.unwrap();
assert!(!arr.is_default())
}
}
impl OctatrackFileIO for ArrangementFile {
fn from_bytes(bytes: &[u8]) -> Result<Self, OtToolsIoError> {
Ok(bincode::deserialize(bytes)?)
}
fn to_bytes(&self) -> Result<Vec<u8>, OtToolsIoError> {
let mut swapped = self.clone();
if cfg!(target_endian = "little") {
swapped.checksum = self.checksum.swap_bytes();
}
let bytes = bincode::serialize(&swapped)?;
Ok(bytes)
}
}
#[cfg(test)]
mod from_bytes {
use crate::{
read_bin_file, test_utils::get_arrange_dirpath, ArrangementFile, OctatrackFileIO,
OtToolsIoError,
};
const HACKED_ARR_NAME: [u8; 15] = [68, 73, 70, 70, 78, 65, 77, 69, 66, 76, 65, 78, 75, 49, 0];
#[test]
fn valid() -> Result<(), OtToolsIoError> {
let path = get_arrange_dirpath().join("blank.work");
let bytes = read_bin_file(&path)?;
let s = ArrangementFile::from_bytes(&bytes)?;
let mut x = ArrangementFile::default();
x.arrangement_state_current.name = HACKED_ARR_NAME;
x.arrangement_state_previous.name = HACKED_ARR_NAME;
x.saved_flag = 1;
assert_eq!(s, x);
Ok(())
}
}
#[cfg(test)]
mod to_bytes {
const HACKED_ARR_NAME: [u8; 15] = [68, 73, 70, 70, 78, 65, 77, 69, 66, 76, 65, 78, 75, 49, 0];
use crate::{
read_bin_file, test_utils::get_arrange_dirpath, ArrangementFile, OctatrackFileIO,
OtToolsIoError,
};
#[test]
fn valid() -> Result<(), OtToolsIoError> {
let path = get_arrange_dirpath().join("blank.work");
let bytes = read_bin_file(&path)?;
let mut x = ArrangementFile::default();
x.arrangement_state_current.name = HACKED_ARR_NAME;
x.arrangement_state_previous.name = HACKED_ARR_NAME;
x.saved_flag = 1;
let b = x.to_bytes()?;
assert_eq!(b, bytes);
Ok(())
}
}
impl HasChecksumField for ArrangementFile {
fn calculate_checksum(&self) -> Result<u16, OtToolsIoError> {
let bytes = bincode::serialize(self)?;
let payload = &bytes[16..bytes.len() - 2];
let mut checksum: u16 = 0;
for b in payload {
checksum = checksum.wrapping_add(*b as u16);
}
Ok(checksum)
}
fn update_checksum(&mut self) -> Result<(), OtToolsIoError> {
self.checksum = self.calculate_checksum()?;
Ok(())
}
fn check_checksum(&self) -> Result<bool, OtToolsIoError> {
Ok(self.checksum == self.calculate_checksum()?)
}
}
#[cfg(test)]
mod checksum_field {
use crate::{ArrangementFile, HasChecksumField, OtToolsIoError};
#[test]
fn valid() -> Result<(), OtToolsIoError> {
let mut x = ArrangementFile::default();
x.checksum = x.calculate_checksum()?;
assert!(x.check_checksum()?);
Ok(())
}
#[test]
fn invalid() -> Result<(), OtToolsIoError> {
let x = ArrangementFile {
checksum: u16::MAX,
..Default::default()
};
assert!(!x.check_checksum()?);
Ok(())
}
mod files {
use crate::arrangements::ArrangementFile;
use crate::test_utils::{get_arrange_dirpath, get_blank_proj_dirpath};
use crate::{HasChecksumField, OctatrackFileIO, OtToolsIoError};
use std::path::Path;
fn helper_test_chksum(fp: &Path) -> Result<(u16, u16), OtToolsIoError> {
let valid = ArrangementFile::from_data_file(fp)?;
let mut test = valid.clone();
test.checksum = 0;
let chk = test.calculate_checksum()?;
Ok((chk, valid.checksum))
}
#[test]
fn blank_project_file() -> Result<(), OtToolsIoError> {
let (test, valid) = helper_test_chksum(&get_blank_proj_dirpath().join("arr01.work"))?;
assert_eq!(test, valid);
Ok(())
}
#[test]
fn blank() -> Result<(), OtToolsIoError> {
let (test, valid) = helper_test_chksum(&get_arrange_dirpath().join("blank.work"))?;
assert_eq!(test, valid);
Ok(())
}
#[test]
fn blank_diffname1() -> Result<(), OtToolsIoError> {
let (test, valid) =
helper_test_chksum(&get_arrange_dirpath().join("blank-diffname1.work"))?;
assert_eq!(test, valid);
Ok(())
}
#[test]
fn blank_diffname2() -> Result<(), OtToolsIoError> {
let (test, valid) =
helper_test_chksum(&get_arrange_dirpath().join("blank-diffname2.work"))?;
assert_eq!(test, valid);
Ok(())
}
#[test]
#[ignore = "arrangement round-trip data loss"]
fn one_rem_row_notext() -> Result<(), OtToolsIoError> {
let (test, valid) =
helper_test_chksum(&get_arrange_dirpath().join("1-rem-blank-txt.work"))?;
assert_eq!(test, valid);
Ok(())
}
#[test]
#[ignore = "arrangement round-trip data loss"]
fn one_rem_row_wtext() -> Result<(), OtToolsIoError> {
let (test, valid) =
helper_test_chksum(&get_arrange_dirpath().join("1-rem-CHAIN-txt.work"))?;
assert_eq!(test, valid);
Ok(())
}
#[test]
#[ignore = "arrangement round-trip data loss"]
fn two_rem_row_wtext() -> Result<(), OtToolsIoError> {
let (test, valid) =
helper_test_chksum(&get_arrange_dirpath().join("2-rem-CHAIN-txt.work"))?;
assert_eq!(test, valid);
Ok(())
}
#[test]
fn four_patterns() -> Result<(), OtToolsIoError> {
let (test, valid) = helper_test_chksum(&get_arrange_dirpath().join("4-patterns.work"))?;
assert_eq!(test, valid);
Ok(())
}
#[test]
#[ignore = "arrangement round-trip data loss"]
fn one_pattern() -> Result<(), OtToolsIoError> {
let (test, valid) = helper_test_chksum(&get_arrange_dirpath().join("1-pattern.work"))?;
assert_eq!(test, valid);
Ok(())
}
#[test]
#[ignore = "arrangement round-trip data loss"]
fn one_halt() -> Result<(), OtToolsIoError> {
let (test, valid) = helper_test_chksum(&get_arrange_dirpath().join("1-halt.work"))?;
assert_eq!(test, valid);
Ok(())
}
#[test]
#[ignore = "arrangement round-trip data loss"]
fn one_pattern_1_loop() -> Result<(), OtToolsIoError> {
let (test, valid) =
helper_test_chksum(&get_arrange_dirpath().join("1-patt-1-loop.work"))?;
assert_eq!(test, valid);
Ok(())
}
#[test]
#[ignore = "arrangement round-trip data loss"]
fn one_pattern_1_jump_1_loop() -> Result<(), OtToolsIoError> {
let (test, valid) =
helper_test_chksum(&get_arrange_dirpath().join("1-patt-1-jump-1-loop.work"))?;
assert_eq!(test, valid);
Ok(())
}
#[test]
#[ignore = "arrangement round-trip data loss"]
fn one_pattern_1_jump_1_loop_1_halt() -> Result<(), OtToolsIoError> {
let (test, valid) = helper_test_chksum(
&get_arrange_dirpath().join("1-patt-1-jump-1-loop-1-halt.work"),
)?;
assert_eq!(test, valid);
Ok(())
}
#[test]
#[ignore = "arrangement round-trip data loss"]
fn full_options() -> Result<(), OtToolsIoError> {
let (test, valid) =
helper_test_chksum(&get_arrange_dirpath().join("full-options.work"))?;
assert_eq!(test, valid);
Ok(())
}
#[test]
#[ignore = "arrangement round-trip data loss"]
fn full_options_no_rems() -> Result<(), OtToolsIoError> {
let (test, valid) =
helper_test_chksum(&get_arrange_dirpath().join("full-options-no-rems.work"))?;
assert_eq!(test, valid);
Ok(())
}
#[test]
fn no_saved_flag() -> Result<(), OtToolsIoError> {
let (test, valid) =
helper_test_chksum(&get_arrange_dirpath().join("no-saved-flag.work"))?;
assert_eq!(test, valid);
Ok(())
}
#[test]
fn with_saved_flag() -> Result<(), OtToolsIoError> {
let (test, valid) =
helper_test_chksum(&get_arrange_dirpath().join("with-saved-flag.work"))?;
assert_eq!(test, valid);
Ok(())
}
#[test]
fn blank_samename_saved() -> Result<(), OtToolsIoError> {
let (test, valid) = helper_test_chksum(
&get_arrange_dirpath().join("blank-samename-saved-chktest.work"),
)?;
assert_eq!(test, valid);
Ok(())
}
#[test]
fn blank_samename_unsaved() -> Result<(), OtToolsIoError> {
let (test, valid) = helper_test_chksum(
&get_arrange_dirpath().join("blank-samename-unsaved-chktest.work"),
)?;
assert_eq!(test, valid);
Ok(())
}
}
}
impl HasHeaderField for ArrangementFile {
fn check_header(&self) -> Result<bool, OtToolsIoError> {
Ok(self.header == ARRANGEMENT_FILE_HEADER)
}
}
#[cfg(test)]
mod header_field {
use crate::{ArrangementFile, HasHeaderField, OtToolsIoError};
#[test]
fn valid() -> Result<(), OtToolsIoError> {
assert!(ArrangementFile::default().check_header()?);
Ok(())
}
#[test]
fn invalid() -> Result<(), OtToolsIoError> {
let mut mutated = ArrangementFile::default();
mutated.header[0] = 0x00;
mutated.header[1] = 200;
mutated.header[2] = 126;
assert!(!mutated.check_header()?);
Ok(())
}
}
impl HasFileVersionField for ArrangementFile {
fn check_file_version(&self) -> Result<bool, OtToolsIoError> {
Ok(self.datatype_version == ARRANGEMENT_FILE_VERSION)
}
}
#[cfg(test)]
mod file_version_field {
use crate::{ArrangementFile, HasFileVersionField, OtToolsIoError};
#[test]
fn valid() -> Result<(), OtToolsIoError> {
assert!(ArrangementFile::default().check_file_version()?);
Ok(())
}
#[test]
fn invalid() -> Result<(), OtToolsIoError> {
let x = ArrangementFile {
datatype_version: 0,
..Default::default()
};
assert!(!x.check_file_version()?);
Ok(())
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, AsRefDerive, AsMutDerive)]
pub struct ArrangementBlock {
pub name: [u8; 15],
pub unknown_1: [u8; 2],
pub n_rows: u8,
pub rows: ArrangeRows<ArrangeRow>,
}
impl Default for ArrangementBlock {
fn default() -> Self {
Self {
name: ARRANGEMENT_DEFAULT_NAME,
unknown_1: from_fn(|_| 0),
n_rows: 0,
rows: ArrangeRows::default(),
}
}
}
impl IsDefault for ArrangementBlock {
fn is_default(&self) -> bool {
let default = &Self::default();
default.unknown_1 == self.unknown_1
&& default.n_rows == self.n_rows
&& default.rows == self.rows
}
}
#[derive(
Clone,
Debug,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
ArrayDefaults,
BoxedArrayDefaults,
AsRefDerive,
AsMutDerive,
)]
pub enum ArrangeRow {
PatternRow {
pattern_id: u8,
repetitions: u8,
mute_mask: u8,
tempo_1: u8,
tempo_2: u8,
scene_a: u8,
scene_b: u8,
offset: u8,
length: u8,
midi_transpose: [u8; 8],
},
LoopOrJumpOrHaltRow {
loop_count: u8,
row_target: u8,
},
ReminderRow(String),
EmptyRow(),
}
impl Default for ArrangeRow {
fn default() -> Self {
Self::EmptyRow()
}
}