1use crate::slices::{Slice, Slices};
9use crate::{
10 CalculateChecksum, CheckChecksum, CheckFileVersion, CheckHeader, Decode, Encode, LoopMode,
11 OptionEnumValueConvert, RBoxErr, SwapBytes, TimeStretchMode, TrigQuantizationMode,
12};
13use ot_tools_io_derive::{IntegrityChecks, OctatrackFile};
14use serde::{Deserialize, Serialize};
15use serde_big_array::BigArray;
16
17pub const SAMPLES_HEADER: [u8; 21] = [
24 0x46, 0x4F, 0x52, 0x4D, 0x00, 0x00, 0x00, 0x00, 0x44, 0x50, 0x53, 0x31, 0x53, 0x4D, 0x50, 0x41,
25 0x00, 0x00, 0x00, 0x00, 0x00,
26];
27
28pub const SAMPLES_FILE_VERSION: u8 = 2;
30
31#[derive(Debug)]
32pub enum SampleSettingsErrors {
33 TempoOutOfBounds,
34 GainOutOfBounds,
35}
36impl std::fmt::Display for SampleSettingsErrors {
37 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
38 match self {
39 Self::GainOutOfBounds => write!(f, "invalid gain, must be in range -24.0 <= x <= 24.0"),
40 Self::TempoOutOfBounds => write!(
41 f,
42 "invalid tempo value, must be in range 30.0 <= x <= 300.0"
43 ),
44 }
45 }
46}
47impl std::error::Error for SampleSettingsErrors {}
48
49pub const DEFAULT_TEMPO: u32 = 2880;
50pub const DEFAULT_GAIN: u16 = 48;
51
52#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Hash, OctatrackFile, IntegrityChecks)]
63pub struct SampleSettingsFile {
64 pub header: [u8; 21],
66
67 pub datatype_version: u8,
69
70 pub unknown: u8,
72
73 pub tempo: u32,
75
76 pub trim_len: u32,
80
81 pub loop_len: u32,
84
85 pub stretch: u32,
88
89 pub loop_mode: u32,
92
93 pub gain: u16,
97
98 pub quantization: u8,
101
102 pub trim_start: u32,
105
106 pub trim_end: u32,
112
113 pub loop_start: u32,
119
120 #[serde(with = "BigArray")]
124 pub slices: [Slice; 64],
125
126 pub slices_len: u32,
129
130 pub checksum: u16,
134}
135
136impl SwapBytes for SampleSettingsFile {
137 fn swap_bytes(self) -> RBoxErr<Self> {
138 let mut bswapped_slices: [Slice; 64] = self.slices;
139
140 for (i, slice) in self.slices.iter().enumerate() {
141 bswapped_slices[i] = slice.swap_bytes()?;
142 }
143
144 let bswapped = Self {
145 header: SAMPLES_HEADER,
146 datatype_version: SAMPLES_FILE_VERSION,
147 unknown: self.unknown,
148 tempo: self.tempo.swap_bytes(),
149 trim_len: self.trim_len.swap_bytes(),
150 loop_len: self.loop_len.swap_bytes(),
151 stretch: self.stretch.swap_bytes(),
152 loop_mode: self.loop_mode.swap_bytes(),
153 gain: self.gain.swap_bytes(),
154 quantization: self.quantization.swap_bytes(),
155 trim_start: self.trim_start.swap_bytes(),
156 trim_end: self.trim_end.swap_bytes(),
157 loop_start: self.loop_start.swap_bytes(),
158 slices: bswapped_slices,
159 slices_len: self.slices_len.swap_bytes(),
160 checksum: self.checksum.swap_bytes(),
161 };
162
163 Ok(bswapped)
164 }
165}
166
167impl SampleSettingsFile {
168 pub fn new(
198 tempo: &u32,
199 stretch: &TimeStretchMode,
200 quantization: &TrigQuantizationMode,
201 gain: &u16,
202 trim_config: &TrimConfig,
203 loop_config: &LoopConfig,
204 slices: &Slices,
205 ) -> RBoxErr<Self> {
206 #[allow(clippy::manual_range_contains)]
207 if tempo < &720 || tempo > &7200 {
208 return Err(SampleSettingsErrors::TempoOutOfBounds.into());
209 }
210
211 #[allow(clippy::manual_range_contains)]
212 if gain < &0 || gain > &96 {
213 return Err(SampleSettingsErrors::GainOutOfBounds.into());
214 }
215
216 Ok(Self {
217 header: SAMPLES_HEADER,
218 datatype_version: SAMPLES_FILE_VERSION,
219 unknown: 0,
220 gain: *gain,
221 stretch: stretch.value()?,
222 tempo: *tempo,
223 quantization: quantization.value()? as u8,
224 trim_start: trim_config.start,
225 trim_end: trim_config.end,
226 trim_len: trim_config.length, loop_start: loop_config.start,
228 loop_len: loop_config.length, loop_mode: loop_config.mode.value()?,
230 slices: slices.slices,
231 slices_len: slices.count,
232 checksum: 0,
233 })
234 }
235}
236
237impl Decode for SampleSettingsFile {
239 fn decode(bytes: &[u8]) -> RBoxErr<Self> {
243 let decoded: Self = bincode::deserialize(bytes)?;
244 let mut bswapd = decoded.clone();
245
246 if cfg!(target_endian = "little") {
248 bswapd = decoded.swap_bytes()?;
249 }
250
251 Ok(bswapd)
252 }
253}
254
255impl Encode for SampleSettingsFile {
257 fn encode(&self) -> RBoxErr<Vec<u8>> {
264 let mut chkd = self.clone();
265 chkd.checksum = self.calculate_checksum()?;
266
267 let encoded = if cfg!(target_endian = "little") {
268 bincode::serialize(&chkd.swap_bytes()?)?
269 } else {
270 bincode::serialize(&chkd)?
271 };
272 Ok(encoded)
273 }
274}
275
276impl CalculateChecksum for SampleSettingsFile {
277 fn calculate_checksum(&self) -> RBoxErr<u16> {
278 let bytes = bincode::serialize(&self)?;
279
280 let checksum_bytes = &bytes[16..bytes.len() - 2];
282
283 let chk: u32 = checksum_bytes
284 .iter()
285 .map(|x| *x as u32)
286 .sum::<u32>()
287 .rem_euclid(u16::MAX as u32 + 1);
288
289 Ok(chk as u16)
290 }
291}
292
293impl CheckHeader for SampleSettingsFile {
294 fn check_header(&self) -> bool {
295 self.header == SAMPLES_HEADER
296 }
297}
298
299impl CheckChecksum for SampleSettingsFile {
300 fn check_checksum(&self) -> RBoxErr<bool> {
301 Ok(self.checksum == self.calculate_checksum()?)
302 }
303}
304
305impl CheckFileVersion for SampleSettingsFile {
306 fn check_file_version(&self) -> RBoxErr<bool> {
307 Ok(self.datatype_version == SAMPLES_FILE_VERSION)
308 }
309}
310
311#[cfg(test)]
314mod test {
315 use crate::samples::{LoopConfig, TrimConfig};
316 use crate::slices::{Slice, Slices};
317 use crate::LoopMode;
318
319 fn create_mock_configs_blank() -> (TrimConfig, LoopConfig, Slices) {
320 let trim_config = TrimConfig {
321 start: 0,
322 end: 0,
323 length: 0,
324 };
325
326 let loop_config = LoopConfig {
327 start: 0,
328 length: 0,
329 mode: LoopMode::Normal,
330 };
331
332 let default_slice = Slice {
333 trim_start: 0,
334 trim_end: 0,
335 loop_start: 0xFFFFFFFF,
336 };
337
338 let slices: [Slice; 64] = [default_slice; 64];
339
340 let slice_conf = Slices { slices, count: 0 };
341
342 (trim_config, loop_config, slice_conf)
343 }
344
345 mod checksum {
346 mod file_read {
405 use crate::samples::{CalculateChecksum, SampleSettingsFile};
406 use crate::test_utils::get_samples_dirpath;
407 use crate::OctatrackFileIO;
408
409 #[test]
410 fn zero_slices_trig_quant_one_step() {
411 let path = get_samples_dirpath()
412 .join("checksum")
413 .join("0slices-tq1step.ot");
414 let data = SampleSettingsFile::from_data_file(&path).unwrap();
415 assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
416 }
417
418 #[test]
419 fn zero_slices() {
420 let path = get_samples_dirpath().join("checksum").join("0slices.ot");
421 let data = SampleSettingsFile::from_data_file(&path).unwrap();
422 assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
423 }
424
425 #[test]
426 fn one_slice() {
427 let path = get_samples_dirpath().join("checksum").join("1slices.ot");
428 let data = SampleSettingsFile::from_data_file(&path).unwrap();
429 assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
430 }
431 #[test]
432 fn two_slices() {
433 let path = get_samples_dirpath().join("checksum").join("2slices.ot");
434 let data = SampleSettingsFile::from_data_file(&path).unwrap();
435 assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
436 }
437 #[test]
438 fn four_slices() {
439 let path = get_samples_dirpath().join("checksum").join("4slices.ot");
440 let data = SampleSettingsFile::from_data_file(&path).unwrap();
441 assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
442 }
443 #[test]
444 fn eight_slices() {
445 let path = get_samples_dirpath().join("checksum").join("8slices.ot");
446 let data = SampleSettingsFile::from_data_file(&path).unwrap();
447 assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
448 }
449 #[test]
450 fn sixteen_slices() {
451 let path = get_samples_dirpath().join("checksum").join("16slices.ot");
452 let data = SampleSettingsFile::from_data_file(&path).unwrap();
453 assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
454 }
455 #[test]
456 fn thirty_two_slices() {
457 let path = get_samples_dirpath().join("checksum").join("32slices.ot");
458 let data = SampleSettingsFile::from_data_file(&path).unwrap();
459 assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
460 }
461 #[test]
462 fn forty_eight_slices() {
463 let path = get_samples_dirpath().join("checksum").join("48slices.ot");
464 let data = SampleSettingsFile::from_data_file(&path).unwrap();
465 assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
466 }
467 #[test]
468 fn sixty_two_slices() {
469 let path = get_samples_dirpath().join("checksum").join("62slices.ot");
470 let data = SampleSettingsFile::from_data_file(&path).unwrap();
471 assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
472 }
473 #[test]
474 fn sixty_three_slices() {
475 let path = get_samples_dirpath().join("checksum").join("63slices.ot");
476 let data = SampleSettingsFile::from_data_file(&path).unwrap();
477 assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
478 }
479 #[test]
480 fn sixty_four_slices_correct() {
481 let path = get_samples_dirpath().join("checksum").join("64slices.ot");
482 let data = SampleSettingsFile::from_data_file(&path).unwrap();
483 assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
484 }
485 }
486 mod file_write {
487 use crate::samples::SampleSettingsFile;
488 use crate::test_utils::get_samples_dirpath;
489 use crate::OctatrackFileIO;
490
491 fn helper_write(test_name: String) {
492 let mut src_path = get_samples_dirpath().join("checksum").join(&test_name);
493 src_path.set_extension("ot");
494
495 let test_dir_path = std::env::temp_dir()
496 .join("ot-tools-io")
497 .join("samples")
498 .join(&test_name);
499
500 let mut test_infile_path = test_dir_path.join("test");
501 test_infile_path.set_extension("ot");
502 let _ = std::fs::create_dir_all(&test_dir_path);
503
504 let valid = SampleSettingsFile::from_data_file(&src_path).unwrap();
505
506 let mut to_write = valid.clone();
508 to_write.checksum = 0;
509
510 to_write.to_data_file(&test_infile_path).unwrap();
511 let written = SampleSettingsFile::from_data_file(&test_infile_path);
512
513 assert!(written.is_ok());
514 assert_eq!(valid, written.unwrap());
515 }
516
517 #[test]
518 fn zero_slices_trig_quant_one_step() {
519 helper_write("0slices-tq1step".to_string());
520 }
521
522 #[test]
523 fn zero_slices() {
524 helper_write("0slices".to_string());
525 }
526
527 #[test]
528 fn one_slice() {
529 helper_write("1slices".to_string());
530 }
531 #[test]
532 fn two_slices() {
533 helper_write("2slices".to_string());
534 }
535 #[test]
536 fn four_slices() {
537 helper_write("4slices".to_string());
538 }
539 #[test]
540 fn eight_slices() {
541 helper_write("8slices".to_string());
542 }
543 #[test]
544 fn sixteen_slices() {
545 helper_write("16slices".to_string());
546 }
547 #[test]
548 fn thirty_two_slices() {
549 helper_write("32slices".to_string());
550 }
551 #[test]
552 fn forty_eight_slices() {
553 helper_write("48slices".to_string());
554 }
555 #[test]
556 fn sixty_two_slices() {
557 helper_write("62slices".to_string());
558 }
559 #[test]
560 fn sixty_three_slices() {
561 helper_write("63slices".to_string());
562 }
563 #[test]
564 fn sixty_four_slices_correct() {
565 helper_write("64slices".to_string());
566 }
567 }
568 }
569
570 mod create_new {
571
572 use super::create_mock_configs_blank;
573 use crate::samples::SampleSettingsFile;
574 use crate::{TimeStretchMode, TrigQuantizationMode};
575
576 #[test]
577 fn err_oob_tempo() {
578 let (trim_conf, loop_conf, slices) = create_mock_configs_blank();
579
580 let composed_chain = SampleSettingsFile::new(
581 &7201,
582 &TimeStretchMode::Off,
583 &TrigQuantizationMode::PatternLength,
584 &46,
585 &trim_conf,
586 &loop_conf,
587 &slices,
588 );
589
590 assert!(composed_chain.is_err());
591 }
592
593 #[test]
594 fn err_invalid_gain() {
595 let (trim_conf, loop_conf, slices) = create_mock_configs_blank();
596
597 let composed_chain = SampleSettingsFile::new(
598 &2880,
599 &TimeStretchMode::Off,
600 &TrigQuantizationMode::PatternLength,
601 &97,
602 &trim_conf,
603 &loop_conf,
604 &slices,
605 );
606
607 assert!(composed_chain.is_err());
608 }
609 }
610
611 mod integrity {
612 use super::create_mock_configs_blank;
613 use crate::samples::{SampleSettingsFile, DEFAULT_GAIN, DEFAULT_TEMPO};
614 use crate::CheckHeader;
615 use crate::{TimeStretchMode, TrigQuantizationMode};
616 #[test]
617 fn true_valid_header() {
618 let (trim_conf, loop_conf, slices) = create_mock_configs_blank();
619 let new = SampleSettingsFile::new(
620 &DEFAULT_TEMPO,
621 &TimeStretchMode::Off,
622 &TrigQuantizationMode::PatternLength,
623 &DEFAULT_GAIN,
624 &trim_conf,
625 &loop_conf,
626 &slices,
627 );
628 assert!(new.unwrap().check_header());
629 }
630
631 #[test]
632 fn false_invalid_header() {
633 let (trim_conf, loop_conf, slices) = create_mock_configs_blank();
634 let new = SampleSettingsFile::new(
635 &DEFAULT_TEMPO,
636 &TimeStretchMode::Off,
637 &TrigQuantizationMode::PatternLength,
638 &DEFAULT_GAIN,
639 &trim_conf,
640 &loop_conf,
641 &slices,
642 );
643 let mut mutated = new.unwrap();
644 mutated.header[0] = 0x00;
645 mutated.header[20] = 111;
646 assert!(!mutated.check_header());
647 }
648 }
649}
650
651#[derive(PartialEq, Debug, Clone, Copy)]
654pub struct TrimConfig {
655 pub start: u32,
657 pub end: u32,
659 pub length: u32,
662}
663
664impl TrimConfig {
665 pub fn from_decoded(decoded: &SampleSettingsFile) -> RBoxErr<Self> {
666 let new = TrimConfig {
667 start: decoded.trim_start,
668 end: decoded.trim_end,
669 length: decoded.trim_len,
670 };
671 Ok(new)
672 }
673}
674
675#[derive(PartialEq, Debug, Clone, Copy)]
678pub struct LoopConfig {
679 pub start: u32,
681
682 pub length: u32,
684
685 pub mode: LoopMode,
687}
688
689impl LoopConfig {
690 pub fn new(start: u32, length: u32, mode: LoopMode) -> RBoxErr<Self> {
691 Ok(Self {
692 start,
693 length,
694 mode,
695 })
696 }
697
698 pub fn from_decoded(decoded: &SampleSettingsFile) -> RBoxErr<Self> {
699 Self::new(
700 decoded.loop_start,
701 decoded.loop_len,
702 LoopMode::from_value(&decoded.loop_mode).unwrap_or(LoopMode::Off),
703 )
704 }
705}
706
707#[cfg(test)]
708#[allow(unused_imports)]
709mod config_tests {
710
711 mod test_sample_loop_config {
712 mod test_new {
713
714 use crate::samples::{LoopConfig, LoopMode};
715
716 #[test]
717 fn test_new_sample_loop_config_loop_off() {
718 assert_eq!(
719 LoopConfig::new(0, 10, LoopMode::Off).unwrap(),
720 LoopConfig {
721 start: 0,
722 length: 10,
723 mode: LoopMode::Off
724 }
725 );
726 }
727
728 #[test]
729 fn test_new_sample_loop_config_umin_start_umax_length() {
730 assert_eq!(
731 LoopConfig::new(u32::MIN, u32::MAX, LoopMode::Off).unwrap(),
732 LoopConfig {
733 start: u32::MIN,
734 length: u32::MAX,
735 mode: LoopMode::Off
736 }
737 );
738 }
739
740 #[test]
741 fn test_new_sample_loop_config_loop_normal() {
742 assert_eq!(
743 LoopConfig::new(0, 10, LoopMode::Normal).unwrap(),
744 LoopConfig {
745 start: 0,
746 length: 10,
747 mode: LoopMode::Normal
748 }
749 );
750 }
751
752 #[test]
753 fn test_new_sample_loop_config_loop_pingpong() {
754 assert_eq!(
755 LoopConfig::new(0, 10, LoopMode::PingPong).unwrap(),
756 LoopConfig {
757 start: 0,
758 length: 10,
759 mode: LoopMode::PingPong
760 }
761 );
762 }
763 }
764 }
765
766 mod test_from_decoded {
767
768 use crate::slices::Slice;
769 use crate::{
770 samples::{
771 LoopConfig, LoopMode, SampleSettingsFile, SAMPLES_FILE_VERSION, SAMPLES_HEADER,
772 },
773 OptionEnumValueConvert,
774 };
775
776 #[test]
777 fn test_umin_start_umax_len() {
778 let decoded = SampleSettingsFile {
779 header: SAMPLES_HEADER,
780 datatype_version: SAMPLES_FILE_VERSION,
781 unknown: 0,
782 tempo: 128000,
783 trim_len: 0,
784 loop_len: u32::MAX,
785 stretch: 0,
786 loop_mode: LoopMode::Off.value().unwrap(),
787 gain: 0,
788 quantization: 0,
789 trim_start: 0,
790 trim_end: 0,
791 loop_start: u32::MIN,
792 slices: [Slice {
793 trim_start: 0,
794 trim_end: 0,
795 loop_start: 0,
796 }; 64],
797 slices_len: 0,
798 checksum: 0,
799 };
800
801 assert_eq!(
802 LoopConfig::from_decoded(&decoded).unwrap(),
803 LoopConfig {
804 start: u32::MIN,
805 length: u32::MAX,
806 mode: LoopMode::Off
807 }
808 );
809 }
810
811 #[test]
812 fn test_loop_off() {
813 let decoded = SampleSettingsFile {
814 header: SAMPLES_HEADER,
815 datatype_version: SAMPLES_FILE_VERSION,
816 unknown: 0,
817 tempo: 128000,
818 trim_len: 0,
819 loop_len: 10,
820 stretch: 0,
821 loop_mode: LoopMode::Off.value().unwrap(),
822 gain: 0,
823 quantization: 0,
824 trim_start: 0,
825 trim_end: 0,
826 loop_start: 0,
827 slices: [Slice {
828 trim_start: 0,
829 trim_end: 0,
830 loop_start: 0,
831 }; 64],
832 slices_len: 0,
833 checksum: 0,
834 };
835
836 assert_eq!(
837 LoopConfig::from_decoded(&decoded).unwrap(),
838 LoopConfig {
839 start: 0,
840 length: 10,
841 mode: LoopMode::Off
842 }
843 );
844 }
845
846 #[test]
847 fn test_loop_normal() {
848 let decoded = SampleSettingsFile {
849 header: SAMPLES_HEADER,
850 datatype_version: SAMPLES_FILE_VERSION,
851 unknown: 0,
852 tempo: 128000,
853 trim_len: 0,
854 loop_len: 10,
855 stretch: 0,
856 loop_mode: LoopMode::Normal.value().unwrap(),
857 gain: 0,
858 quantization: 0,
859 trim_start: 0,
860 trim_end: 0,
861 loop_start: 0,
862 slices: [Slice {
863 trim_start: 0,
864 trim_end: 0,
865 loop_start: 0,
866 }; 64],
867 slices_len: 0,
868 checksum: 0,
869 };
870
871 assert_eq!(
872 LoopConfig::from_decoded(&decoded).unwrap(),
873 LoopConfig {
874 start: 0,
875 length: 10,
876 mode: LoopMode::Normal
877 }
878 );
879 }
880
881 #[test]
882 fn test_loop_pingpong() {
883 let decoded = SampleSettingsFile {
884 header: SAMPLES_HEADER,
885 datatype_version: SAMPLES_FILE_VERSION,
886 unknown: 0,
887 tempo: 128000,
888 trim_len: 0,
889 loop_len: 10,
890 stretch: 0,
891 loop_mode: LoopMode::PingPong.value().unwrap(),
892 gain: 0,
893 quantization: 0,
894 trim_start: 0,
895 trim_end: 0,
896 loop_start: 0,
897 slices: [Slice {
898 trim_start: 0,
899 trim_end: 0,
900 loop_start: 0,
901 }; 64],
902 slices_len: 0,
903 checksum: 0,
904 };
905
906 assert_eq!(
907 LoopConfig::from_decoded(&decoded).unwrap(),
908 LoopConfig {
909 start: 0,
910 length: 10,
911 mode: LoopMode::PingPong
912 }
913 );
914 }
915 }
916}