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