mod deserialize;
mod serialize;
use crate::{
Defaults, HasChecksumField, HasFileVersionField, HasHeaderField, IsDefault, OctatrackFileIO,
OtToolsIoError,
};
use ot_tools_io_derive::{
ArrayDefaults, AsMutDerive, AsRefDerive, BoxedBigArrayDefaults, IntegrityChecks,
};
use serde::Serialize;
use serde_big_array::Array;
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,
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 encode(&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 decode {
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::decode(&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 encode {
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.encode()?;
assert_eq!(b, bytes);
Ok(())
}
}
impl HasChecksumField for ArrangementFile {
fn calculate_checksum(&self) -> Result<u16, OtToolsIoError> {
Ok(0)
}
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::{OctatrackFileIO, OtToolsIoError};
use std::path::Path;
fn arr_eq_helper(a: &ArrangementFile, b: &ArrangementFile) {
assert_eq!(a.header, b.header);
assert_eq!(a.datatype_version, b.datatype_version);
assert_eq!(a.unk1, b.unk1);
assert_eq!(a.unk2, b.unk2);
assert_eq!(a.arrangements_saved_state, b.arrangements_saved_state);
assert_eq!(a.arrangement_state_current, b.arrangement_state_current);
assert_eq!(a.arrangement_state_previous, b.arrangement_state_previous);
}
#[allow(dead_code)]
fn get_checksum_octarranger(bytes: &[u8]) -> Result<u16, OtToolsIoError> {
let bytes_no_header_no_chk = &bytes[16..bytes.len() - 2];
let mut chk: u16 = 0;
for byte_pair in &bytes_no_header_no_chk
.chunks(2)
.map(|x| x.to_vec())
.filter(|x| x.len() > 1)
.collect::<Vec<Vec<u8>>>()
{
let first_byte = byte_pair[0] as u16;
let second_byte = byte_pair[1] as u16;
chk = second_byte.wrapping_add(first_byte.wrapping_add(chk));
}
Ok(chk)
}
fn get_checksum_simple(bytes: &[u8]) -> Result<u16, OtToolsIoError> {
let bytes_no_header_no_chk = &bytes[16..bytes.len() - 2];
let mut prev;
let mut chk: u32 = 0;
for byte in bytes_no_header_no_chk {
prev = chk;
chk = chk.wrapping_add((*byte as u32).wrapping_add(0));
if byte != &0 {
println!("chk: {chk} diff: {}", chk - prev);
}
}
println!("CHK32: {chk}");
Ok((chk).wrapping_mul(1) as u16)
}
#[allow(dead_code)]
fn get_checksum_bank(bytes: &[u8]) -> Result<u16, OtToolsIoError> {
let bytes_no_header_no_chk = &bytes[16..bytes.len() - 2];
let default_bytes = &bincode::serialize(&ArrangementFile::default())?;
let def_important_bytes = &default_bytes[16..bytes.len() - 2];
let default_checksum: i32 = 1870;
let mut byte_diffs: i32 = 0;
for (byte, def_byte) in bytes_no_header_no_chk.iter().zip(def_important_bytes) {
let byte_diff = (*byte as i32) - (*def_byte as i32);
if byte_diff != 0 {
byte_diffs += byte_diff;
}
}
let check = byte_diffs * 256 + default_checksum;
let modded = check.rem_euclid(65535);
Ok(modded as u16)
}
fn helper_test_chksum(fp: &Path) {
let valid = ArrangementFile::from_data_file(fp).unwrap();
let mut test = valid.clone();
test.checksum = 0;
arr_eq_helper(&test, &valid);
let bytes = bincode::serialize(&test).unwrap();
let r = get_checksum_simple(&bytes);
assert!(r.is_ok());
let res = r.unwrap();
let s_attr_chk: u32 = bytes[16..bytes.len() - 2]
.iter()
.map(|x| *x as u32)
.sum::<u32>()
.rem_euclid(u16::MAX as u32 + 1);
let non_zero_bytes = bytes.iter().filter(|b| b > &&0).count();
let non_zero_sum = bytes
.iter()
.cloned()
.filter(|b| b > &0)
.map(|x| x as u32)
.sum::<u32>();
println!(
"l: {} r: {} non_zero_bytes {} sum total: {} s-attr {} diff {} (or {})",
res,
valid.checksum,
non_zero_bytes,
non_zero_sum,
s_attr_chk,
res.wrapping_sub(valid.checksum),
valid.checksum.wrapping_sub(res)
);
println!(
"checksum bytes: {:?} target bytes: {:?}",
[(res >> 8) as u8, res as u8],
[(valid.checksum >> 8) as u8, valid.checksum as u8]
);
assert_eq!(res, valid.checksum);
}
#[test]
fn blank_project_file() {
helper_test_chksum(&get_blank_proj_dirpath().join("arr01.work"));
}
#[test]
fn blank() {
helper_test_chksum(&get_arrange_dirpath().join("blank.work"));
}
#[test]
fn blank_diffname1() {
helper_test_chksum(&get_arrange_dirpath().join("blank-diffname1.work"));
}
#[test]
fn blank_diffname2() {
helper_test_chksum(&get_arrange_dirpath().join("blank-diffname2.work"));
}
#[test]
#[ignore]
fn one_rem_row_notext() {
helper_test_chksum(&get_arrange_dirpath().join("1-rem-blank-txt.work"));
}
#[test]
#[ignore]
fn one_rem_row_wtext() {
helper_test_chksum(&get_arrange_dirpath().join("1-rem-CHAIN-txt.work"));
}
#[test]
#[ignore]
fn two_rem_row_wtext() {
helper_test_chksum(&get_arrange_dirpath().join("2-rem-CHAIN-txt.work"));
}
#[test]
#[ignore]
fn four_patterns() {
helper_test_chksum(&get_arrange_dirpath().join("4-patterns.work"));
}
#[test]
#[ignore]
fn one_pattern() {
helper_test_chksum(&get_arrange_dirpath().join("1-pattern.work"));
}
#[test]
#[ignore]
fn one_halt() {
helper_test_chksum(&get_arrange_dirpath().join("1-halt.work"));
}
#[test]
#[ignore]
fn one_pattern_1_loop() {
helper_test_chksum(&get_arrange_dirpath().join("1-patt-1-loop.work"));
}
#[test]
#[ignore]
fn one_pattern_1_jump_1_loop() {
helper_test_chksum(&get_arrange_dirpath().join("1-patt-1-jump-1-loop.work"));
}
#[test]
#[ignore]
fn one_pattern_1_jump_1_loop_1_halt() {
helper_test_chksum(&get_arrange_dirpath().join("1-patt-1-jump-1-loop-1-halt.work"));
}
#[test]
#[ignore]
fn full_options() {
helper_test_chksum(&get_arrange_dirpath().join("full-options.work"));
}
#[test]
#[ignore]
fn full_options_no_rems() {
helper_test_chksum(&get_arrange_dirpath().join("full-options-no-rems.work"));
}
#[test]
fn no_saved_flag() {
helper_test_chksum(&get_arrange_dirpath().join("no-saved-flag.work"));
}
#[test]
fn with_saved_flag() {
helper_test_chksum(&get_arrange_dirpath().join("with-saved-flag.work"));
}
#[test]
fn blank_samename_saved() {
helper_test_chksum(&get_arrange_dirpath().join("blank-samename-saved-chktest.work"));
}
#[test]
fn blank_samename_unsaved() {
helper_test_chksum(&get_arrange_dirpath().join("blank-samename-unsaved-chktest.work"));
}
}
}
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: Box<Array<ArrangeRow, 256>>,
}
impl Default for ArrangementBlock {
fn default() -> Self {
Self {
name: ARRANGEMENT_DEFAULT_NAME,
unknown_1: from_fn(|_| 0),
n_rows: 0,
rows: ArrangeRow::defaults(),
}
}
}
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,
BoxedBigArrayDefaults,
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()
}
}