use crate::markers::SlotMarkers;
use crate::projects::SlotAttributes;
use crate::settings::{LoopMode, TimeStretchMode, TrigQuantizationMode};
use crate::slices::{Slice, Slices};
use crate::traits::SwapBytes;
use crate::{
HasChecksumField, HasFileVersionField, HasHeaderField, OctatrackFileIO, OtToolsIoError,
};
use ot_tools_io_derive::IntegrityChecks;
use serde::{Deserialize, Serialize};
use serde_big_array::BigArray;
use thiserror::Error;
#[cfg(test)]
mod test_utils {
use super::{
LoopConfig, LoopMode, SampleSettingsError, SampleSettingsFile, Slice, Slices,
TimeStretchMode, TrigQuantizationMode, TrimConfig, DEFAULT_GAIN, DEFAULT_TEMPO,
SAMPLES_FILE_VERSION, SAMPLES_HEADER,
};
pub(crate) fn create_mock_new(
tempo: Option<u32>,
gain: Option<u16>,
) -> Result<SampleSettingsFile, SampleSettingsError> {
let trim_config = TrimConfig {
start: 0,
end: 0,
length: 0,
};
let loop_config = LoopConfig {
start: 0,
length: 0,
mode: LoopMode::default(),
};
let default_slice = Slice {
trim_start: 0,
trim_end: 0,
loop_start: 0xFFFFFFFF,
};
let slices: [Slice; 64] = [default_slice; 64];
let slice_conf = Slices { slices, count: 0 };
SampleSettingsFile::new(
&tempo.unwrap_or(DEFAULT_TEMPO),
&TimeStretchMode::Off,
&TrigQuantizationMode::PatternLength,
&gain.unwrap_or(DEFAULT_GAIN),
&trim_config,
&loop_config,
&slice_conf,
)
}
pub(crate) fn mock_0_slice() -> SampleSettingsFile {
SampleSettingsFile {
header: SAMPLES_HEADER,
datatype_version: SAMPLES_FILE_VERSION,
unknown: 1,
tempo: 2281,
trim_len: 800,
loop_len: 800,
stretch: 2,
loop_mode: 0,
gain: 48,
quantization: 255,
trim_start: 0,
trim_end: 890839,
loop_start: 0,
slices: Slices::default().slices,
slices_len: 0,
checksum: 998,
}
}
}
#[derive(Debug, Error)]
pub enum SampleSettingsError {
#[error("invalid gain, must be u16 in range 0 <= x <= 96: {value}")]
GainOutOfBounds { value: u32 },
#[error("invalid tempo value, must be u32 in range 720 <= x <= 7200: {value}")]
TempoOutOfBounds { value: u32 },
}
pub const SAMPLES_HEADER: [u8; 21] = [
0x46, 0x4F, 0x52, 0x4D, 0x00, 0x00, 0x00, 0x00, 0x44, 0x50, 0x53, 0x31, 0x53, 0x4D, 0x50, 0x41,
0x00, 0x00, 0x00, 0x00, 0x00,
];
pub const SAMPLES_FILE_VERSION: u8 = 2;
pub const DEFAULT_TEMPO: u32 = 2880;
pub const DEFAULT_GAIN: u16 = 48;
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Hash, IntegrityChecks)]
pub struct SampleSettingsFile {
pub header: [u8; 21],
pub datatype_version: u8,
pub unknown: u8,
pub tempo: u32,
pub trim_len: u32,
pub loop_len: u32,
pub stretch: u32,
pub loop_mode: u32,
pub gain: u16,
pub quantization: u8,
pub trim_start: u32,
pub trim_end: u32,
pub loop_start: u32,
#[serde(with = "BigArray")]
pub slices: [Slice; 64],
pub slices_len: u32,
pub checksum: u16,
}
impl SampleSettingsFile {
pub fn new(
tempo: &u32,
stretch: &TimeStretchMode,
quantization: &TrigQuantizationMode,
gain: &u16,
trim_config: &TrimConfig,
loop_config: &LoopConfig,
slices: &Slices,
) -> Result<Self, SampleSettingsError> {
#[allow(clippy::manual_range_contains)]
if tempo < &720 || tempo > &7200 {
return Err(SampleSettingsError::TempoOutOfBounds { value: *tempo });
}
#[allow(clippy::manual_range_contains)]
if gain < &0 || gain > &96 {
return Err(SampleSettingsError::GainOutOfBounds {
value: *gain as u32,
});
}
Ok(Self {
header: SAMPLES_HEADER,
datatype_version: SAMPLES_FILE_VERSION,
unknown: 0,
gain: *gain,
stretch: (*stretch).into(),
tempo: *tempo,
quantization: (*quantization).into(),
trim_start: trim_config.start,
trim_end: trim_config.end,
trim_len: trim_config.length, loop_start: loop_config.start,
loop_len: loop_config.length, loop_mode: loop_config.mode.into(),
slices: slices.slices,
slices_len: slices.count,
checksum: 0,
})
}
}
#[cfg(test)]
mod new {
use super::test_utils::create_mock_new;
use crate::OtToolsIoError;
#[test]
fn invalid_oob_temp() -> Result<(), OtToolsIoError> {
assert_eq!(
create_mock_new(Some(7201), None).unwrap_err().to_string(),
"invalid tempo value, must be u32 in range 720 <= x <= 7200: 7201".to_string(),
);
Ok(())
}
#[test]
fn invalid_oob_gain() -> Result<(), OtToolsIoError> {
assert_eq!(
create_mock_new(None, Some(97)).unwrap_err().to_string(),
"invalid gain, must be u16 in range 0 <= x <= 96: 97".to_string(),
);
Ok(())
}
}
impl SwapBytes for SampleSettingsFile {
fn swap_bytes(self) -> Self {
let mut bswapped_slices: [Slice; 64] = self.slices;
for (i, slice) in self.slices.iter().enumerate() {
bswapped_slices[i] = slice.swap_bytes();
}
Self {
header: SAMPLES_HEADER,
datatype_version: SAMPLES_FILE_VERSION,
unknown: self.unknown,
tempo: self.tempo.swap_bytes(),
trim_len: self.trim_len.swap_bytes(),
loop_len: self.loop_len.swap_bytes(),
stretch: self.stretch.swap_bytes(),
loop_mode: self.loop_mode.swap_bytes(),
gain: self.gain.swap_bytes(),
quantization: self.quantization.swap_bytes(),
trim_start: self.trim_start.swap_bytes(),
trim_end: self.trim_end.swap_bytes(),
loop_start: self.loop_start.swap_bytes(),
slices: bswapped_slices,
slices_len: self.slices_len.swap_bytes(),
checksum: self.checksum.swap_bytes(),
}
}
}
impl OctatrackFileIO for SampleSettingsFile {
fn encode(&self) -> Result<Vec<u8>, OtToolsIoError> {
let mut chkd = self.clone();
chkd.checksum = self.calculate_checksum()?;
let encoded = if cfg!(target_endian = "little") {
bincode::serialize(&chkd.swap_bytes())?
} else {
bincode::serialize(&chkd)?
};
Ok(encoded)
}
fn decode(bytes: &[u8]) -> Result<Self, OtToolsIoError> {
let decoded: Self = bincode::deserialize(bytes)?;
let mut bswapd = decoded.clone();
if cfg!(target_endian = "little") {
bswapd = decoded.swap_bytes();
}
Ok(bswapd)
}
}
#[cfg(test)]
mod decode {
use crate::read_bin_file;
use crate::samples::test_utils::mock_0_slice;
use crate::test_utils::get_samples_dirpath;
use crate::{OctatrackFileIO, OtToolsIoError, SampleSettingsFile};
#[test]
fn valid() -> Result<(), OtToolsIoError> {
let path = get_samples_dirpath().join("checksum").join("0slices.ot");
let bytes = read_bin_file(&path)?;
let s = SampleSettingsFile::decode(&bytes)?;
assert_eq!(s, mock_0_slice());
Ok(())
}
}
#[cfg(test)]
mod encode {
use crate::read_bin_file;
use crate::samples::test_utils::mock_0_slice;
use crate::test_utils::get_samples_dirpath;
use crate::{OctatrackFileIO, OtToolsIoError};
#[test]
fn valid() -> Result<(), OtToolsIoError> {
let path = get_samples_dirpath().join("checksum").join("0slices.ot");
let bytes = read_bin_file(&path)?;
let b = mock_0_slice().encode()?;
assert_eq!(b, bytes);
Ok(())
}
}
impl HasChecksumField for SampleSettingsFile {
fn calculate_checksum(&self) -> Result<u16, OtToolsIoError> {
let bytes = bincode::serialize(&self)?;
let checksum_bytes = &bytes[16..bytes.len() - 2];
let chk: u32 = checksum_bytes
.iter()
.map(|x| *x as u32)
.sum::<u32>()
.rem_euclid(u16::MAX as u32 + 1);
Ok(chk as u16)
}
fn check_checksum(&self) -> Result<bool, OtToolsIoError> {
Ok(self.checksum == self.calculate_checksum()?)
}
}
#[cfg(test)]
mod checksum_field {
use super::test_utils::create_mock_new;
use crate::{HasChecksumField, OtToolsIoError};
#[test]
fn valid() -> Result<(), OtToolsIoError> {
let mut x = create_mock_new(None, None)?;
x.checksum = x.calculate_checksum()?;
assert!(x.check_checksum()?);
Ok(())
}
#[test]
fn invalid() -> Result<(), OtToolsIoError> {
let mut x = create_mock_new(None, None)?;
x.checksum = x.calculate_checksum()?;
x.checksum = 0;
assert!(!x.check_checksum()?);
Ok(())
}
mod files {
use crate::test_utils::get_samples_dirpath;
use crate::{HasChecksumField, OctatrackFileIO, OtToolsIoError, SampleSettingsFile};
fn helper(test_name: String) -> Result<(u16, u16), OtToolsIoError> {
let mut src_path = get_samples_dirpath().join("checksum").join(&test_name);
src_path.set_extension("ot");
let valid = SampleSettingsFile::from_data_file(&src_path)?;
let mut x = valid.clone();
x.checksum = 0;
Ok((x.calculate_checksum()?, valid.checksum))
}
#[test]
fn zero_slices_trig_quant_one_step() -> Result<(), OtToolsIoError> {
let (test, valid) = helper("0slices-tq1step".to_string())?;
assert_eq!(test, valid);
Ok(())
}
#[test]
fn zero_slices() -> Result<(), OtToolsIoError> {
let (test, valid) = helper("0slices".to_string())?;
assert_eq!(test, valid);
Ok(())
}
#[test]
fn one_slice() -> Result<(), OtToolsIoError> {
let (test, valid) = helper("1slices".to_string())?;
assert_eq!(test, valid);
Ok(())
}
#[test]
fn two_slices() -> Result<(), OtToolsIoError> {
let (test, valid) = helper("2slices".to_string())?;
assert_eq!(test, valid);
Ok(())
}
#[test]
fn four_slices() -> Result<(), OtToolsIoError> {
let (test, valid) = helper("4slices".to_string())?;
assert_eq!(test, valid);
Ok(())
}
#[test]
fn eight_slices() -> Result<(), OtToolsIoError> {
let (test, valid) = helper("8slices".to_string())?;
assert_eq!(test, valid);
Ok(())
}
#[test]
fn sixteen_slices() -> Result<(), OtToolsIoError> {
let (test, valid) = helper("16slices".to_string())?;
assert_eq!(test, valid);
Ok(())
}
#[test]
fn thirty_two_slices() -> Result<(), OtToolsIoError> {
let (test, valid) = helper("32slices".to_string())?;
assert_eq!(test, valid);
Ok(())
}
#[test]
fn forty_eight_slices() -> Result<(), OtToolsIoError> {
let (test, valid) = helper("48slices".to_string())?;
assert_eq!(test, valid);
Ok(())
}
#[test]
fn sixty_two_slices() -> Result<(), OtToolsIoError> {
let (test, valid) = helper("62slices".to_string())?;
assert_eq!(test, valid);
Ok(())
}
#[test]
fn sixty_three_slices() -> Result<(), OtToolsIoError> {
let (test, valid) = helper("63slices".to_string())?;
assert_eq!(test, valid);
Ok(())
}
#[test]
fn sixty_four_slices_correct() -> Result<(), OtToolsIoError> {
let (test, valid) = helper("64slices".to_string())?;
assert_eq!(test, valid);
Ok(())
}
}
}
impl HasHeaderField for SampleSettingsFile {
fn check_header(&self) -> Result<bool, OtToolsIoError> {
Ok(self.header == SAMPLES_HEADER)
}
}
#[cfg(test)]
mod header_field {
use super::test_utils::create_mock_new;
use crate::{HasHeaderField, OtToolsIoError};
#[test]
fn valid() -> Result<(), OtToolsIoError> {
assert!(create_mock_new(None, None)?.check_header()?);
Ok(())
}
#[test]
fn invalid() -> Result<(), OtToolsIoError> {
let mut mutated = create_mock_new(None, None)?;
mutated.header[0] = 0x00;
mutated.header[20] = 111;
assert!(!mutated.check_header()?);
Ok(())
}
}
impl HasFileVersionField for SampleSettingsFile {
fn check_file_version(&self) -> Result<bool, OtToolsIoError> {
Ok(self.datatype_version == SAMPLES_FILE_VERSION)
}
}
#[cfg(test)]
mod file_version_field {
use super::test_utils::create_mock_new;
use crate::{HasFileVersionField, OtToolsIoError};
#[test]
fn valid() -> Result<(), OtToolsIoError> {
assert!(create_mock_new(None, None)?.check_file_version()?);
Ok(())
}
#[test]
fn invalid() -> Result<(), OtToolsIoError> {
let mut mutated = create_mock_new(None, None)?;
mutated.datatype_version = 0;
assert!(!mutated.check_file_version()?);
Ok(())
}
}
pub(crate) fn from_slot_tuple(attrs: SlotAttributes, markers: SlotMarkers) -> SampleSettingsFile {
SampleSettingsFile {
header: SAMPLES_HEADER,
datatype_version: SAMPLES_FILE_VERSION,
unknown: 0,
tempo: attrs.bpm as u32,
trim_len: 0,
loop_len: 0,
stretch: attrs.timestrech_mode.into(),
loop_mode: attrs.loop_mode.into(),
gain: attrs.gain as u16,
quantization: attrs.trig_quantization_mode.into(),
trim_start: markers.trim_offset,
trim_end: markers.trim_end,
loop_start: markers.loop_point,
slices: markers.slices,
slices_len: markers.slice_count,
checksum: 0,
}
}
impl From<(SlotAttributes, SlotMarkers)> for SampleSettingsFile {
fn from(value: (SlotAttributes, SlotMarkers)) -> Self {
let (attrs, markers) = value;
from_slot_tuple(attrs, markers)
}
}
impl From<(&SlotAttributes, &SlotMarkers)> for SampleSettingsFile {
fn from(value: (&SlotAttributes, &SlotMarkers)) -> Self {
let (attrs, markers) = value;
from_slot_tuple(attrs.clone(), markers.clone())
}
}
#[cfg(test)]
mod sample_settings_from {
use crate::projects::SlotsAttributes;
use crate::samples::from_slot_tuple;
use crate::MarkersFile;
use crate::OtToolsIoError;
use crate::SampleSettingsFile;
#[test]
fn from_slot_tuple_valid() -> Result<(), OtToolsIoError> {
let slots = SlotsAttributes::default();
let slot = slots
.flex_slots
.iter()
.find(|x| x.is_some())
.unwrap()
.clone();
let markers = MarkersFile::default().flex_slots[0].clone();
from_slot_tuple(slot.unwrap(), markers);
Ok(())
}
#[test]
fn from_owned_valid() -> Result<(), OtToolsIoError> {
let slots = SlotsAttributes::default();
let slot = slots
.flex_slots
.iter()
.find(|x| x.is_some())
.unwrap()
.clone();
let markers = MarkersFile::default().flex_slots[0].clone();
let _ = SampleSettingsFile::from((slot.unwrap(), markers));
Ok(())
}
#[test]
fn from_borrowed_valid() -> Result<(), OtToolsIoError> {
let slots = SlotsAttributes::default();
let slot = slots
.flex_slots
.iter()
.find(|x| x.is_some())
.unwrap()
.clone();
let markers = MarkersFile::default().flex_slots[0].clone();
let _ = SampleSettingsFile::from((&slot.unwrap(), &markers));
Ok(())
}
}
#[derive(PartialEq, Debug, Clone, Copy)]
pub struct TrimConfig {
pub start: u32,
pub end: u32,
pub length: u32,
}
impl From<SampleSettingsFile> for TrimConfig {
fn from(decoded: SampleSettingsFile) -> Self {
Self {
start: decoded.trim_start,
end: decoded.trim_end,
length: decoded.trim_len,
}
}
}
impl From<&SampleSettingsFile> for TrimConfig {
fn from(decoded: &SampleSettingsFile) -> Self {
Self::from(decoded.clone())
}
}
impl From<SlotMarkers> for TrimConfig {
fn from(value: SlotMarkers) -> Self {
Self {
start: value.trim_offset,
end: value.trim_end,
length: value.trim_end - value.trim_offset,
}
}
}
impl From<&SlotMarkers> for TrimConfig {
fn from(value: &SlotMarkers) -> Self {
Self::from(value.clone())
}
}
#[cfg(test)]
mod trim_config_from {
mod sample_settings {
use crate::samples::test_utils::create_mock_new;
use crate::samples::TrimConfig;
use crate::OtToolsIoError;
#[test]
fn owned() -> Result<(), OtToolsIoError> {
let s = create_mock_new(None, None)?;
assert_eq!(
TrimConfig::from(s),
TrimConfig {
start: 0,
end: 0,
length: 0,
}
);
Ok(())
}
#[test]
fn borrowed() -> Result<(), OtToolsIoError> {
let s = create_mock_new(None, None)?;
assert_eq!(
TrimConfig::from(&s),
TrimConfig {
start: 0,
end: 0,
length: 0,
}
);
Ok(())
}
}
mod slot_markers {
use crate::samples::TrimConfig;
use crate::MarkersFile;
use crate::OtToolsIoError;
#[test]
fn owned() -> Result<(), OtToolsIoError> {
let markers = MarkersFile::default().flex_slots[0].clone();
assert_eq!(
TrimConfig::from(markers),
TrimConfig {
start: 0,
end: 0,
length: 0,
}
);
Ok(())
}
#[test]
fn borrowed() -> Result<(), OtToolsIoError> {
let markers = MarkersFile::default().flex_slots[0].clone();
assert_eq!(
TrimConfig::from(&markers),
TrimConfig {
start: 0,
end: 0,
length: 0,
}
);
Ok(())
}
}
}
#[derive(PartialEq, Debug, Clone, Copy)]
pub struct LoopConfig {
pub start: u32,
pub length: u32,
pub mode: LoopMode,
}
impl LoopConfig {
pub fn new(start: u32, length: u32, mode: LoopMode) -> Self {
Self {
start,
length,
mode,
}
}
}
#[cfg(test)]
mod loop_config_new {
use crate::samples::{LoopConfig, LoopMode};
#[test]
fn test_new_sample_loop_config_loop_off() {
assert_eq!(
LoopConfig::new(0, 10, LoopMode::Off),
LoopConfig {
start: 0,
length: 10,
mode: LoopMode::Off
}
);
}
#[test]
fn test_new_sample_loop_config_umin_start_umax_length() {
assert_eq!(
LoopConfig::new(u32::MIN, u32::MAX, LoopMode::Off),
LoopConfig {
start: u32::MIN,
length: u32::MAX,
mode: LoopMode::Off
}
);
}
#[test]
fn test_new_sample_loop_config_loop_normal() {
assert_eq!(
LoopConfig::new(0, 10, LoopMode::Normal),
LoopConfig {
start: 0,
length: 10,
mode: LoopMode::Normal
}
);
}
#[test]
fn test_new_sample_loop_config_loop_pingpong() {
assert_eq!(
LoopConfig::new(0, 10, LoopMode::PingPong),
LoopConfig {
start: 0,
length: 10,
mode: LoopMode::PingPong
}
);
}
}
impl From<SampleSettingsFile> for LoopConfig {
fn from(decoded: SampleSettingsFile) -> Self {
Self::new(
decoded.loop_start,
decoded.loop_len,
LoopMode::try_from(&(decoded.loop_mode as u8)).unwrap_or(LoopMode::default()),
)
}
}
impl From<&SampleSettingsFile> for LoopConfig {
fn from(decoded: &SampleSettingsFile) -> Self {
Self::from(decoded.clone())
}
}
#[cfg(test)]
mod loop_config_from {
use super::{
test_utils::create_mock_new, LoopConfig, LoopMode, OtToolsIoError, SampleSettingsFile,
Slices, DEFAULT_GAIN, DEFAULT_TEMPO, SAMPLES_FILE_VERSION, SAMPLES_HEADER,
};
#[test]
fn owned() -> Result<(), OtToolsIoError> {
let s = create_mock_new(None, None)?;
assert_eq!(
LoopConfig::from(s),
LoopConfig {
start: 0,
length: 0,
mode: LoopMode::default(),
}
);
Ok(())
}
#[test]
fn borrowed() -> Result<(), OtToolsIoError> {
let s = create_mock_new(None, None)?;
assert_eq!(
LoopConfig::from(&s),
LoopConfig {
start: 0,
length: 0,
mode: LoopMode::default(),
}
);
Ok(())
}
fn mock_for_loop_conf(
loop_start: u32,
loop_len: u32,
loop_mode: LoopMode,
) -> SampleSettingsFile {
SampleSettingsFile {
loop_len,
loop_start,
loop_mode: loop_mode.into(),
header: SAMPLES_HEADER,
datatype_version: SAMPLES_FILE_VERSION,
tempo: DEFAULT_TEMPO,
unknown: 0,
trim_len: 0,
stretch: 0,
gain: DEFAULT_GAIN,
quantization: 0,
trim_start: 0,
trim_end: 0,
slices: Slices::default().slices,
slices_len: 0,
checksum: 0,
}
}
#[test]
fn test_umin_start_umax_len() {
assert_eq!(
LoopConfig::from(mock_for_loop_conf(u32::MIN, u32::MAX, LoopMode::Off)),
LoopConfig {
start: u32::MIN,
length: u32::MAX,
mode: LoopMode::Off
}
);
}
#[test]
fn test_loop_off() {
assert_eq!(
LoopConfig::from(mock_for_loop_conf(0, 10, LoopMode::Off)),
LoopConfig {
start: 0,
length: 10,
mode: LoopMode::Off
}
);
}
#[test]
fn test_loop_normal() {
assert_eq!(
LoopConfig::from(mock_for_loop_conf(0, 10, LoopMode::Normal)),
LoopConfig {
start: 0,
length: 10,
mode: LoopMode::Normal
}
);
}
#[test]
fn test_loop_pingpong() {
assert_eq!(
LoopConfig::from(mock_for_loop_conf(0, 10, LoopMode::PingPong)),
LoopConfig {
start: 0,
length: 10,
mode: LoopMode::PingPong
}
);
}
}