1use crate::markers::SlotMarkers;
9use crate::projects::SlotAttributes;
10use crate::settings::{LoopMode, TimeStretchMode, TrigQuantizationMode};
11use crate::slices::{Slice, Slices};
12use crate::traits::SwapBytes;
13use crate::{
14 HasChecksumField, HasFileVersionField, HasHeaderField, OctatrackFileIO, OtToolsIoError,
15};
16use ot_tools_io_derive::IntegrityChecks;
17use serde::{Deserialize, Serialize};
18use serde_big_array::BigArray;
19use thiserror::Error;
20
21#[cfg(test)]
22mod test_utils {
23 use super::{
24 LoopConfig, LoopMode, SampleSettingsError, SampleSettingsFile, Slice, Slices,
25 TimeStretchMode, TrigQuantizationMode, TrimConfig, DEFAULT_GAIN, DEFAULT_TEMPO,
26 SAMPLES_FILE_VERSION, SAMPLES_HEADER,
27 };
28
29 pub(crate) fn create_mock_new(
30 tempo: Option<u32>,
31 gain: Option<u16>,
32 ) -> Result<SampleSettingsFile, SampleSettingsError> {
33 let trim_config = TrimConfig {
34 start: 0,
35 end: 0,
36 length: 0,
37 };
38
39 let loop_config = LoopConfig {
40 start: 0,
41 length: 0,
42 mode: LoopMode::default(),
43 };
44
45 let default_slice = Slice {
46 trim_start: 0,
47 trim_end: 0,
48 loop_start: 0xFFFFFFFF,
49 };
50
51 let slices: [Slice; 64] = [default_slice; 64];
52
53 let slice_conf = Slices { slices, count: 0 };
54
55 SampleSettingsFile::new(
56 &tempo.unwrap_or(DEFAULT_TEMPO),
57 &TimeStretchMode::Off,
58 &TrigQuantizationMode::PatternLength,
59 &gain.unwrap_or(DEFAULT_GAIN),
60 &trim_config,
61 &loop_config,
62 &slice_conf,
63 )
64 }
65
66 pub(crate) fn mock_0_slice() -> SampleSettingsFile {
67 SampleSettingsFile {
68 header: SAMPLES_HEADER,
69 datatype_version: SAMPLES_FILE_VERSION,
70 unknown: 1,
71 tempo: 2281,
72 trim_len: 800,
73 loop_len: 800,
74 stretch: 2,
75 loop_mode: 0,
76 gain: 48,
77 quantization: 255,
78 trim_start: 0,
79 trim_end: 890839,
80 loop_start: 0,
81 slices: Slices::default().slices,
82 slices_len: 0,
83 checksum: 998,
84 }
85 }
86}
87
88#[derive(Debug, Error)]
89pub enum SampleSettingsError {
90 #[error("invalid gain, must be u16 in range 0 <= x <= 96: {value}")]
91 GainOutOfBounds { value: u32 },
92 #[error("invalid tempo value, must be u32 in range 720 <= x <= 7200: {value}")]
93 TempoOutOfBounds { value: u32 },
94}
95
96pub const SAMPLES_HEADER: [u8; 21] = [
103 0x46, 0x4F, 0x52, 0x4D, 0x00, 0x00, 0x00, 0x00, 0x44, 0x50, 0x53, 0x31, 0x53, 0x4D, 0x50, 0x41,
104 0x00, 0x00, 0x00, 0x00, 0x00,
105];
106
107pub const SAMPLES_FILE_VERSION: u8 = 2;
109
110pub const DEFAULT_TEMPO: u32 = 2880;
111pub const DEFAULT_GAIN: u16 = 48;
112
113#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Hash, IntegrityChecks)]
124pub struct SampleSettingsFile {
125 pub header: [u8; 21],
127
128 pub datatype_version: u8,
130
131 pub unknown: u8,
133
134 pub tempo: u32,
136
137 pub trim_len: u32,
141
142 pub loop_len: u32,
145
146 pub stretch: u32,
149
150 pub loop_mode: u32,
153
154 pub gain: u16,
158
159 pub quantization: u8,
162
163 pub trim_start: u32,
166
167 pub trim_end: u32,
173
174 pub loop_start: u32,
180
181 #[serde(with = "BigArray")]
185 pub slices: [Slice; 64],
186
187 pub slices_len: u32,
190
191 pub checksum: u16,
195}
196
197impl SampleSettingsFile {
198 pub fn new(
228 tempo: &u32,
229 stretch: &TimeStretchMode,
230 quantization: &TrigQuantizationMode,
231 gain: &u16,
232 trim_config: &TrimConfig,
233 loop_config: &LoopConfig,
234 slices: &Slices,
235 ) -> Result<Self, SampleSettingsError> {
236 #[allow(clippy::manual_range_contains)]
237 if tempo < &720 || tempo > &7200 {
238 return Err(SampleSettingsError::TempoOutOfBounds { value: *tempo });
239 }
240
241 #[allow(clippy::manual_range_contains)]
242 if gain < &0 || gain > &96 {
243 return Err(SampleSettingsError::GainOutOfBounds {
244 value: *gain as u32,
245 });
246 }
247
248 Ok(Self {
249 header: SAMPLES_HEADER,
250 datatype_version: SAMPLES_FILE_VERSION,
251 unknown: 0,
252 gain: *gain,
253 stretch: (*stretch).into(),
254 tempo: *tempo,
255 quantization: (*quantization).into(),
256 trim_start: trim_config.start,
257 trim_end: trim_config.end,
258 trim_len: trim_config.length, loop_start: loop_config.start,
260 loop_len: loop_config.length, loop_mode: loop_config.mode.into(),
262 slices: slices.slices,
263 slices_len: slices.count,
264 checksum: 0,
265 })
266 }
267}
268
269#[cfg(test)]
270mod new {
271 use super::test_utils::create_mock_new;
272 use crate::OtToolsIoError;
273 #[test]
274 fn invalid_oob_temp() -> Result<(), OtToolsIoError> {
275 assert_eq!(
276 create_mock_new(Some(7201), None).unwrap_err().to_string(),
277 "invalid tempo value, must be u32 in range 720 <= x <= 7200: 7201".to_string(),
278 );
279 Ok(())
280 }
281
282 #[test]
283 fn invalid_oob_gain() -> Result<(), OtToolsIoError> {
284 assert_eq!(
285 create_mock_new(None, Some(97)).unwrap_err().to_string(),
286 "invalid gain, must be u16 in range 0 <= x <= 96: 97".to_string(),
287 );
288 Ok(())
289 }
290}
291
292impl SwapBytes for SampleSettingsFile {
293 fn swap_bytes(self) -> Self {
294 let mut bswapped_slices: [Slice; 64] = self.slices;
295
296 for (i, slice) in self.slices.iter().enumerate() {
297 bswapped_slices[i] = slice.swap_bytes();
298 }
299
300 Self {
301 header: SAMPLES_HEADER,
302 datatype_version: SAMPLES_FILE_VERSION,
303 unknown: self.unknown,
304 tempo: self.tempo.swap_bytes(),
305 trim_len: self.trim_len.swap_bytes(),
306 loop_len: self.loop_len.swap_bytes(),
307 stretch: self.stretch.swap_bytes(),
308 loop_mode: self.loop_mode.swap_bytes(),
309 gain: self.gain.swap_bytes(),
310 quantization: self.quantization.swap_bytes(),
311 trim_start: self.trim_start.swap_bytes(),
312 trim_end: self.trim_end.swap_bytes(),
313 loop_start: self.loop_start.swap_bytes(),
314 slices: bswapped_slices,
315 slices_len: self.slices_len.swap_bytes(),
316 checksum: self.checksum.swap_bytes(),
317 }
318 }
319}
320
321impl OctatrackFileIO for SampleSettingsFile {
322 fn encode(&self) -> Result<Vec<u8>, OtToolsIoError> {
329 let mut chkd = self.clone();
330 chkd.checksum = self.calculate_checksum()?;
331
332 let encoded = if cfg!(target_endian = "little") {
333 bincode::serialize(&chkd.swap_bytes())?
334 } else {
335 bincode::serialize(&chkd)?
336 };
337 Ok(encoded)
338 }
339
340 fn decode(bytes: &[u8]) -> Result<Self, OtToolsIoError> {
344 let decoded: Self = bincode::deserialize(bytes)?;
345 let mut bswapd = decoded.clone();
346
347 if cfg!(target_endian = "little") {
349 bswapd = decoded.swap_bytes();
350 }
351
352 Ok(bswapd)
353 }
354}
355
356#[cfg(test)]
357mod decode {
358 use crate::read_bin_file;
359 use crate::samples::test_utils::mock_0_slice;
360 use crate::test_utils::get_samples_dirpath;
361 use crate::{OctatrackFileIO, OtToolsIoError, SampleSettingsFile};
362 #[test]
363 fn valid() -> Result<(), OtToolsIoError> {
364 let path = get_samples_dirpath().join("checksum").join("0slices.ot");
365 let bytes = read_bin_file(&path)?;
366 let s = SampleSettingsFile::decode(&bytes)?;
367 assert_eq!(s, mock_0_slice());
368 Ok(())
369 }
370}
371
372#[cfg(test)]
373mod encode {
374 use crate::read_bin_file;
375 use crate::samples::test_utils::mock_0_slice;
376 use crate::test_utils::get_samples_dirpath;
377 use crate::{OctatrackFileIO, OtToolsIoError};
378 #[test]
379 fn valid() -> Result<(), OtToolsIoError> {
380 let path = get_samples_dirpath().join("checksum").join("0slices.ot");
381 let bytes = read_bin_file(&path)?;
382 let b = mock_0_slice().encode()?;
383 assert_eq!(b, bytes);
384 Ok(())
385 }
386}
387
388impl HasChecksumField for SampleSettingsFile {
389 fn calculate_checksum(&self) -> Result<u16, OtToolsIoError> {
391 let bytes = bincode::serialize(&self)?;
392
393 let checksum_bytes = &bytes[16..bytes.len() - 2];
395
396 let chk: u32 = checksum_bytes
397 .iter()
398 .map(|x| *x as u32)
399 .sum::<u32>()
400 .rem_euclid(u16::MAX as u32 + 1);
401
402 Ok(chk as u16)
403 }
404
405 fn check_checksum(&self) -> Result<bool, OtToolsIoError> {
406 Ok(self.checksum == self.calculate_checksum()?)
407 }
408}
409
410#[cfg(test)]
411mod checksum_field {
412 use super::test_utils::create_mock_new;
413 use crate::{HasChecksumField, OtToolsIoError};
414 #[test]
415 fn valid() -> Result<(), OtToolsIoError> {
416 let mut x = create_mock_new(None, None)?;
417 x.checksum = x.calculate_checksum()?;
418 assert!(x.check_checksum()?);
419 Ok(())
420 }
421
422 #[test]
423 fn invalid() -> Result<(), OtToolsIoError> {
424 let mut x = create_mock_new(None, None)?;
425 x.checksum = x.calculate_checksum()?;
426 x.checksum = 0;
427 assert!(!x.check_checksum()?);
428 Ok(())
429 }
430
431 mod files {
432 use crate::test_utils::get_samples_dirpath;
433 use crate::{HasChecksumField, OctatrackFileIO, OtToolsIoError, SampleSettingsFile};
434
435 fn helper(test_name: String) -> Result<(u16, u16), OtToolsIoError> {
436 let mut src_path = get_samples_dirpath().join("checksum").join(&test_name);
437 src_path.set_extension("ot");
438
439 let valid = SampleSettingsFile::from_data_file(&src_path)?;
440 let mut x = valid.clone();
441 x.checksum = 0;
442
443 Ok((x.calculate_checksum()?, valid.checksum))
444 }
445
446 #[test]
447 fn zero_slices_trig_quant_one_step() -> Result<(), OtToolsIoError> {
448 let (test, valid) = helper("0slices-tq1step".to_string())?;
449 assert_eq!(test, valid);
450 Ok(())
451 }
452
453 #[test]
454 fn zero_slices() -> Result<(), OtToolsIoError> {
455 let (test, valid) = helper("0slices".to_string())?;
456 assert_eq!(test, valid);
457 Ok(())
458 }
459
460 #[test]
461 fn one_slice() -> Result<(), OtToolsIoError> {
462 let (test, valid) = helper("1slices".to_string())?;
463 assert_eq!(test, valid);
464 Ok(())
465 }
466
467 #[test]
468 fn two_slices() -> Result<(), OtToolsIoError> {
469 let (test, valid) = helper("2slices".to_string())?;
470 assert_eq!(test, valid);
471 Ok(())
472 }
473
474 #[test]
475 fn four_slices() -> Result<(), OtToolsIoError> {
476 let (test, valid) = helper("4slices".to_string())?;
477 assert_eq!(test, valid);
478 Ok(())
479 }
480
481 #[test]
482 fn eight_slices() -> Result<(), OtToolsIoError> {
483 let (test, valid) = helper("8slices".to_string())?;
484 assert_eq!(test, valid);
485 Ok(())
486 }
487
488 #[test]
489 fn sixteen_slices() -> Result<(), OtToolsIoError> {
490 let (test, valid) = helper("16slices".to_string())?;
491 assert_eq!(test, valid);
492 Ok(())
493 }
494
495 #[test]
496 fn thirty_two_slices() -> Result<(), OtToolsIoError> {
497 let (test, valid) = helper("32slices".to_string())?;
498 assert_eq!(test, valid);
499 Ok(())
500 }
501
502 #[test]
503 fn forty_eight_slices() -> Result<(), OtToolsIoError> {
504 let (test, valid) = helper("48slices".to_string())?;
505 assert_eq!(test, valid);
506 Ok(())
507 }
508
509 #[test]
510 fn sixty_two_slices() -> Result<(), OtToolsIoError> {
511 let (test, valid) = helper("62slices".to_string())?;
512 assert_eq!(test, valid);
513 Ok(())
514 }
515
516 #[test]
517 fn sixty_three_slices() -> Result<(), OtToolsIoError> {
518 let (test, valid) = helper("63slices".to_string())?;
519 assert_eq!(test, valid);
520 Ok(())
521 }
522
523 #[test]
524 fn sixty_four_slices_correct() -> Result<(), OtToolsIoError> {
525 let (test, valid) = helper("64slices".to_string())?;
526 assert_eq!(test, valid);
527 Ok(())
528 }
529 }
530}
531
532impl HasHeaderField for SampleSettingsFile {
533 fn check_header(&self) -> Result<bool, OtToolsIoError> {
534 Ok(self.header == SAMPLES_HEADER)
535 }
536}
537
538#[cfg(test)]
539mod header_field {
540 use super::test_utils::create_mock_new;
541 use crate::{HasHeaderField, OtToolsIoError};
542 #[test]
543 fn valid() -> Result<(), OtToolsIoError> {
544 assert!(create_mock_new(None, None)?.check_header()?);
545 Ok(())
546 }
547
548 #[test]
549 fn invalid() -> Result<(), OtToolsIoError> {
550 let mut mutated = create_mock_new(None, None)?;
551 mutated.header[0] = 0x00;
552 mutated.header[20] = 111;
553 assert!(!mutated.check_header()?);
554 Ok(())
555 }
556}
557
558impl HasFileVersionField for SampleSettingsFile {
559 fn check_file_version(&self) -> Result<bool, OtToolsIoError> {
560 Ok(self.datatype_version == SAMPLES_FILE_VERSION)
561 }
562}
563
564#[cfg(test)]
565mod file_version_field {
566 use super::test_utils::create_mock_new;
567 use crate::{HasFileVersionField, OtToolsIoError};
568 #[test]
569 fn valid() -> Result<(), OtToolsIoError> {
570 assert!(create_mock_new(None, None)?.check_file_version()?);
571 Ok(())
572 }
573
574 #[test]
575 fn invalid() -> Result<(), OtToolsIoError> {
576 let mut mutated = create_mock_new(None, None)?;
577 mutated.datatype_version = 0;
578 assert!(!mutated.check_file_version()?);
579 Ok(())
580 }
581}
582
583fn from_slot_tuple(attrs: SlotAttributes, markers: SlotMarkers) -> SampleSettingsFile {
586 SampleSettingsFile {
587 header: SAMPLES_HEADER,
588 datatype_version: SAMPLES_FILE_VERSION,
589 unknown: 0,
590 tempo: attrs.bpm as u32,
591 trim_len: 0,
593 loop_len: 0,
595 stretch: attrs.timestrech_mode.into(),
596 loop_mode: attrs.loop_mode.into(),
597 gain: attrs.gain as u16,
598 quantization: attrs.trig_quantization_mode.into(),
599 trim_start: markers.trim_offset,
600 trim_end: markers.trim_end,
601 loop_start: markers.loop_point,
602 slices: markers.slices,
603 slices_len: markers.slice_count,
604 checksum: 0,
605 }
606}
607
608impl From<(SlotAttributes, SlotMarkers)> for SampleSettingsFile {
609 fn from(value: (SlotAttributes, SlotMarkers)) -> Self {
610 let (attrs, markers) = value;
611 from_slot_tuple(attrs, markers)
612 }
613}
614
615impl From<(&SlotAttributes, &SlotMarkers)> for SampleSettingsFile {
616 fn from(value: (&SlotAttributes, &SlotMarkers)) -> Self {
617 let (attrs, markers) = value;
618 from_slot_tuple(attrs.clone(), markers.clone())
619 }
620}
621
622#[derive(PartialEq, Debug, Clone, Copy)]
625pub struct TrimConfig {
626 pub start: u32,
628 pub end: u32,
630 pub length: u32,
633}
634
635impl From<SampleSettingsFile> for TrimConfig {
636 fn from(decoded: SampleSettingsFile) -> Self {
637 Self {
638 start: decoded.trim_start,
639 end: decoded.trim_end,
640 length: decoded.trim_len,
641 }
642 }
643}
644
645impl From<&SampleSettingsFile> for TrimConfig {
646 fn from(decoded: &SampleSettingsFile) -> Self {
647 Self::from(decoded.clone())
648 }
649}
650
651impl From<SlotMarkers> for TrimConfig {
652 fn from(value: SlotMarkers) -> Self {
653 Self {
654 start: value.trim_offset,
655 end: value.trim_end,
656 length: value.trim_end - value.trim_offset,
657 }
658 }
659}
660
661impl From<&SlotMarkers> for TrimConfig {
662 fn from(value: &SlotMarkers) -> Self {
663 Self::from(value.clone())
664 }
665}
666
667#[cfg(test)]
668mod trim_config_from {
669 use super::{test_utils::create_mock_new, TrimConfig};
670 use crate::OtToolsIoError;
671
672 #[test]
673 fn owned() -> Result<(), OtToolsIoError> {
674 let s = create_mock_new(None, None)?;
675 assert_eq!(
676 TrimConfig::from(s),
677 TrimConfig {
678 start: 0,
679 end: 0,
680 length: 0,
681 }
682 );
683 Ok(())
684 }
685 #[test]
686 fn borrowed() -> Result<(), OtToolsIoError> {
687 let s = create_mock_new(None, None)?;
688 assert_eq!(
689 TrimConfig::from(&s),
690 TrimConfig {
691 start: 0,
692 end: 0,
693 length: 0,
694 }
695 );
696 Ok(())
697 }
698}
699
700#[derive(PartialEq, Debug, Clone, Copy)]
703pub struct LoopConfig {
704 pub start: u32,
706
707 pub length: u32,
709
710 pub mode: LoopMode,
712}
713
714impl LoopConfig {
715 pub fn new(start: u32, length: u32, mode: LoopMode) -> Self {
716 Self {
717 start,
718 length,
719 mode,
720 }
721 }
722}
723
724#[cfg(test)]
725mod loop_config_new {
726
727 use crate::samples::{LoopConfig, LoopMode};
728
729 #[test]
730 fn test_new_sample_loop_config_loop_off() {
731 assert_eq!(
732 LoopConfig::new(0, 10, LoopMode::Off),
733 LoopConfig {
734 start: 0,
735 length: 10,
736 mode: LoopMode::Off
737 }
738 );
739 }
740
741 #[test]
742 fn test_new_sample_loop_config_umin_start_umax_length() {
743 assert_eq!(
744 LoopConfig::new(u32::MIN, u32::MAX, LoopMode::Off),
745 LoopConfig {
746 start: u32::MIN,
747 length: u32::MAX,
748 mode: LoopMode::Off
749 }
750 );
751 }
752
753 #[test]
754 fn test_new_sample_loop_config_loop_normal() {
755 assert_eq!(
756 LoopConfig::new(0, 10, LoopMode::Normal),
757 LoopConfig {
758 start: 0,
759 length: 10,
760 mode: LoopMode::Normal
761 }
762 );
763 }
764
765 #[test]
766 fn test_new_sample_loop_config_loop_pingpong() {
767 assert_eq!(
768 LoopConfig::new(0, 10, LoopMode::PingPong),
769 LoopConfig {
770 start: 0,
771 length: 10,
772 mode: LoopMode::PingPong
773 }
774 );
775 }
776}
777
778impl From<SampleSettingsFile> for LoopConfig {
779 fn from(decoded: SampleSettingsFile) -> Self {
780 Self::new(
781 decoded.loop_start,
782 decoded.loop_len,
783 LoopMode::try_from(&(decoded.loop_mode as u8)).unwrap_or(LoopMode::default()),
784 )
785 }
786}
787
788impl From<&SampleSettingsFile> for LoopConfig {
789 fn from(decoded: &SampleSettingsFile) -> Self {
790 Self::from(decoded.clone())
791 }
792}
793
794#[cfg(test)]
795mod loop_config_from {
796 use super::{
797 test_utils::create_mock_new, LoopConfig, LoopMode, OtToolsIoError, SampleSettingsFile,
798 Slices, DEFAULT_GAIN, DEFAULT_TEMPO, SAMPLES_FILE_VERSION, SAMPLES_HEADER,
799 };
800
801 #[test]
802 fn owned() -> Result<(), OtToolsIoError> {
803 let s = create_mock_new(None, None)?;
804 assert_eq!(
805 LoopConfig::from(s),
806 LoopConfig {
807 start: 0,
808 length: 0,
809 mode: LoopMode::default(),
810 }
811 );
812 Ok(())
813 }
814 #[test]
815 fn borrowed() -> Result<(), OtToolsIoError> {
816 let s = create_mock_new(None, None)?;
817 assert_eq!(
818 LoopConfig::from(&s),
819 LoopConfig {
820 start: 0,
821 length: 0,
822 mode: LoopMode::default(),
823 }
824 );
825 Ok(())
826 }
827
828 fn mock_for_loop_conf(
829 loop_start: u32,
830 loop_len: u32,
831 loop_mode: LoopMode,
832 ) -> SampleSettingsFile {
833 SampleSettingsFile {
834 loop_len,
835 loop_start,
836 loop_mode: loop_mode.into(),
837 header: SAMPLES_HEADER,
838 datatype_version: SAMPLES_FILE_VERSION,
839 tempo: DEFAULT_TEMPO,
840 unknown: 0,
841 trim_len: 0,
842 stretch: 0,
843 gain: DEFAULT_GAIN,
844 quantization: 0,
845 trim_start: 0,
846 trim_end: 0,
847 slices: Slices::default().slices,
848 slices_len: 0,
849 checksum: 0,
850 }
851 }
852
853 #[test]
854 fn test_umin_start_umax_len() {
855 assert_eq!(
856 LoopConfig::from(mock_for_loop_conf(u32::MIN, u32::MAX, LoopMode::Off)),
857 LoopConfig {
858 start: u32::MIN,
859 length: u32::MAX,
860 mode: LoopMode::Off
861 }
862 );
863 }
864
865 #[test]
866 fn test_loop_off() {
867 assert_eq!(
868 LoopConfig::from(mock_for_loop_conf(0, 10, LoopMode::Off)),
869 LoopConfig {
870 start: 0,
871 length: 10,
872 mode: LoopMode::Off
873 }
874 );
875 }
876
877 #[test]
878 fn test_loop_normal() {
879 assert_eq!(
880 LoopConfig::from(mock_for_loop_conf(0, 10, LoopMode::Normal)),
881 LoopConfig {
882 start: 0,
883 length: 10,
884 mode: LoopMode::Normal
885 }
886 );
887 }
888
889 #[test]
890 fn test_loop_pingpong() {
891 assert_eq!(
892 LoopConfig::from(mock_for_loop_conf(0, 10, LoopMode::PingPong)),
893 LoopConfig {
894 start: 0,
895 length: 10,
896 mode: LoopMode::PingPong
897 }
898 );
899 }
900}