1use crate::settings::{LoopMode, TimeStretchMode, TrigQuantizationMode};
9use crate::slices::{Slice, Slices};
10use crate::traits::SwapBytes;
11use crate::{
12 HasChecksumField, HasFileVersionField, HasHeaderField, OctatrackFileIO, OtToolsIoError,
13};
14use ot_tools_io_derive::IntegrityChecks;
15use serde::{Deserialize, Serialize};
16use serde_big_array::BigArray;
17use thiserror::Error;
18
19#[cfg(test)]
20mod test_utils {
21 use super::{
22 LoopConfig, LoopMode, SampleSettingsError, SampleSettingsFile, Slice, Slices,
23 TimeStretchMode, TrigQuantizationMode, TrimConfig, DEFAULT_GAIN, DEFAULT_TEMPO,
24 SAMPLES_FILE_VERSION, SAMPLES_HEADER,
25 };
26
27 pub(crate) fn create_mock_new(
28 tempo: Option<u32>,
29 gain: Option<u16>,
30 ) -> Result<SampleSettingsFile, SampleSettingsError> {
31 let trim_config = TrimConfig {
32 start: 0,
33 end: 0,
34 length: 0,
35 };
36
37 let loop_config = LoopConfig {
38 start: 0,
39 length: 0,
40 mode: LoopMode::default(),
41 };
42
43 let default_slice = Slice {
44 trim_start: 0,
45 trim_end: 0,
46 loop_start: 0xFFFFFFFF,
47 };
48
49 let slices: [Slice; 64] = [default_slice; 64];
50
51 let slice_conf = Slices { slices, count: 0 };
52
53 SampleSettingsFile::new(
54 &tempo.unwrap_or(DEFAULT_TEMPO),
55 &TimeStretchMode::Off,
56 &TrigQuantizationMode::PatternLength,
57 &gain.unwrap_or(DEFAULT_GAIN),
58 &trim_config,
59 &loop_config,
60 &slice_conf,
61 )
62 }
63
64 pub(crate) fn mock_0_slice() -> SampleSettingsFile {
65 SampleSettingsFile {
66 header: SAMPLES_HEADER,
67 datatype_version: SAMPLES_FILE_VERSION,
68 unknown: 1,
69 tempo: 2281,
70 trim_len: 800,
71 loop_len: 800,
72 stretch: 2,
73 loop_mode: 0,
74 gain: 48,
75 quantization: 255,
76 trim_start: 0,
77 trim_end: 890839,
78 loop_start: 0,
79 slices: Slices::default().slices,
80 slices_len: 0,
81 checksum: 998,
82 }
83 }
84}
85
86#[derive(Debug, Error)]
87pub enum SampleSettingsError {
88 #[error("invalid gain, must be u16 in range 0 <= x <= 96: {value}")]
89 GainOutOfBounds { value: u32 },
90 #[error("invalid tempo value, must be u32 in range 720 <= x <= 7200: {value}")]
91 TempoOutOfBounds { value: u32 },
92}
93
94pub const SAMPLES_HEADER: [u8; 21] = [
101 0x46, 0x4F, 0x52, 0x4D, 0x00, 0x00, 0x00, 0x00, 0x44, 0x50, 0x53, 0x31, 0x53, 0x4D, 0x50, 0x41,
102 0x00, 0x00, 0x00, 0x00, 0x00,
103];
104
105pub const SAMPLES_FILE_VERSION: u8 = 2;
107
108pub const DEFAULT_TEMPO: u32 = 2880;
109pub const DEFAULT_GAIN: u16 = 48;
110
111#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Hash, IntegrityChecks)]
122pub struct SampleSettingsFile {
123 pub header: [u8; 21],
125
126 pub datatype_version: u8,
128
129 pub unknown: u8,
131
132 pub tempo: u32,
134
135 pub trim_len: u32,
139
140 pub loop_len: u32,
143
144 pub stretch: u32,
147
148 pub loop_mode: u32,
151
152 pub gain: u16,
156
157 pub quantization: u8,
160
161 pub trim_start: u32,
164
165 pub trim_end: u32,
171
172 pub loop_start: u32,
178
179 #[serde(with = "BigArray")]
183 pub slices: [Slice; 64],
184
185 pub slices_len: u32,
188
189 pub checksum: u16,
193}
194
195impl SampleSettingsFile {
196 pub fn new(
226 tempo: &u32,
227 stretch: &TimeStretchMode,
228 quantization: &TrigQuantizationMode,
229 gain: &u16,
230 trim_config: &TrimConfig,
231 loop_config: &LoopConfig,
232 slices: &Slices,
233 ) -> Result<Self, SampleSettingsError> {
234 #[allow(clippy::manual_range_contains)]
235 if tempo < &720 || tempo > &7200 {
236 return Err(SampleSettingsError::TempoOutOfBounds { value: *tempo });
237 }
238
239 #[allow(clippy::manual_range_contains)]
240 if gain < &0 || gain > &96 {
241 return Err(SampleSettingsError::GainOutOfBounds {
242 value: *gain as u32,
243 });
244 }
245
246 Ok(Self {
247 header: SAMPLES_HEADER,
248 datatype_version: SAMPLES_FILE_VERSION,
249 unknown: 0,
250 gain: *gain,
251 stretch: (*stretch).into(),
252 tempo: *tempo,
253 quantization: (*quantization).into(),
254 trim_start: trim_config.start,
255 trim_end: trim_config.end,
256 trim_len: trim_config.length, loop_start: loop_config.start,
258 loop_len: loop_config.length, loop_mode: loop_config.mode.into(),
260 slices: slices.slices,
261 slices_len: slices.count,
262 checksum: 0,
263 })
264 }
265}
266
267#[cfg(test)]
268mod new {
269 use super::test_utils::create_mock_new;
270 use crate::OtToolsIoError;
271 #[test]
272 fn invalid_oob_temp() -> Result<(), OtToolsIoError> {
273 assert_eq!(
274 create_mock_new(Some(7201), None).unwrap_err().to_string(),
275 "invalid tempo value, must be u32 in range 720 <= x <= 7200: 7201".to_string(),
276 );
277 Ok(())
278 }
279
280 #[test]
281 fn invalid_oob_gain() -> Result<(), OtToolsIoError> {
282 assert_eq!(
283 create_mock_new(None, Some(97)).unwrap_err().to_string(),
284 "invalid gain, must be u16 in range 0 <= x <= 96: 97".to_string(),
285 );
286 Ok(())
287 }
288}
289
290impl SwapBytes for SampleSettingsFile {
291 fn swap_bytes(self) -> Self {
292 let mut bswapped_slices: [Slice; 64] = self.slices;
293
294 for (i, slice) in self.slices.iter().enumerate() {
295 bswapped_slices[i] = slice.swap_bytes();
296 }
297
298 Self {
299 header: SAMPLES_HEADER,
300 datatype_version: SAMPLES_FILE_VERSION,
301 unknown: self.unknown,
302 tempo: self.tempo.swap_bytes(),
303 trim_len: self.trim_len.swap_bytes(),
304 loop_len: self.loop_len.swap_bytes(),
305 stretch: self.stretch.swap_bytes(),
306 loop_mode: self.loop_mode.swap_bytes(),
307 gain: self.gain.swap_bytes(),
308 quantization: self.quantization.swap_bytes(),
309 trim_start: self.trim_start.swap_bytes(),
310 trim_end: self.trim_end.swap_bytes(),
311 loop_start: self.loop_start.swap_bytes(),
312 slices: bswapped_slices,
313 slices_len: self.slices_len.swap_bytes(),
314 checksum: self.checksum.swap_bytes(),
315 }
316 }
317}
318
319impl OctatrackFileIO for SampleSettingsFile {
320 fn encode(&self) -> Result<Vec<u8>, OtToolsIoError> {
327 let mut chkd = self.clone();
328 chkd.checksum = self.calculate_checksum()?;
329
330 let encoded = if cfg!(target_endian = "little") {
331 bincode::serialize(&chkd.swap_bytes())?
332 } else {
333 bincode::serialize(&chkd)?
334 };
335 Ok(encoded)
336 }
337
338 fn decode(bytes: &[u8]) -> Result<Self, OtToolsIoError> {
342 let decoded: Self = bincode::deserialize(bytes)?;
343 let mut bswapd = decoded.clone();
344
345 if cfg!(target_endian = "little") {
347 bswapd = decoded.swap_bytes();
348 }
349
350 Ok(bswapd)
351 }
352}
353
354#[cfg(test)]
355mod decode {
356 use crate::read_bin_file;
357 use crate::samples::test_utils::mock_0_slice;
358 use crate::test_utils::get_samples_dirpath;
359 use crate::{OctatrackFileIO, OtToolsIoError, SampleSettingsFile};
360 #[test]
361 fn valid() -> Result<(), OtToolsIoError> {
362 let path = get_samples_dirpath().join("checksum").join("0slices.ot");
363 let bytes = read_bin_file(&path)?;
364 let s = SampleSettingsFile::decode(&bytes)?;
365 assert_eq!(s, mock_0_slice());
366 Ok(())
367 }
368}
369
370#[cfg(test)]
371mod encode {
372 use crate::read_bin_file;
373 use crate::samples::test_utils::mock_0_slice;
374 use crate::test_utils::get_samples_dirpath;
375 use crate::{OctatrackFileIO, OtToolsIoError};
376 #[test]
377 fn valid() -> Result<(), OtToolsIoError> {
378 let path = get_samples_dirpath().join("checksum").join("0slices.ot");
379 let bytes = read_bin_file(&path)?;
380 let b = mock_0_slice().encode()?;
381 assert_eq!(b, bytes);
382 Ok(())
383 }
384}
385
386impl HasChecksumField for SampleSettingsFile {
387 fn calculate_checksum(&self) -> Result<u16, OtToolsIoError> {
389 let bytes = bincode::serialize(&self)?;
390
391 let checksum_bytes = &bytes[16..bytes.len() - 2];
393
394 let chk: u32 = checksum_bytes
395 .iter()
396 .map(|x| *x as u32)
397 .sum::<u32>()
398 .rem_euclid(u16::MAX as u32 + 1);
399
400 Ok(chk as u16)
401 }
402
403 fn check_checksum(&self) -> Result<bool, OtToolsIoError> {
404 Ok(self.checksum == self.calculate_checksum()?)
405 }
406}
407
408#[cfg(test)]
409mod checksum_field {
410 use super::test_utils::create_mock_new;
411 use crate::{HasChecksumField, OtToolsIoError};
412 #[test]
413 fn valid() -> Result<(), OtToolsIoError> {
414 let mut x = create_mock_new(None, None)?;
415 x.checksum = x.calculate_checksum()?;
416 assert!(x.check_checksum()?);
417 Ok(())
418 }
419
420 #[test]
421 fn invalid() -> Result<(), OtToolsIoError> {
422 let mut x = create_mock_new(None, None)?;
423 x.checksum = x.calculate_checksum()?;
424 x.checksum = 0;
425 assert!(!x.check_checksum()?);
426 Ok(())
427 }
428}
429
430impl HasHeaderField for SampleSettingsFile {
431 fn check_header(&self) -> Result<bool, OtToolsIoError> {
432 Ok(self.header == SAMPLES_HEADER)
433 }
434}
435
436#[cfg(test)]
437mod header_field {
438 use super::test_utils::create_mock_new;
439 use crate::{HasHeaderField, OtToolsIoError};
440 #[test]
441 fn valid() -> Result<(), OtToolsIoError> {
442 assert!(create_mock_new(None, None)?.check_header()?);
443 Ok(())
444 }
445
446 #[test]
447 fn invalid() -> Result<(), OtToolsIoError> {
448 let mut mutated = create_mock_new(None, None)?;
449 mutated.header[0] = 0x00;
450 mutated.header[20] = 111;
451 assert!(!mutated.check_header()?);
452 Ok(())
453 }
454}
455
456impl HasFileVersionField for SampleSettingsFile {
457 fn check_file_version(&self) -> Result<bool, OtToolsIoError> {
458 Ok(self.datatype_version == SAMPLES_FILE_VERSION)
459 }
460}
461
462#[cfg(test)]
463mod file_version_field {
464 use super::test_utils::create_mock_new;
465 use crate::{HasFileVersionField, OtToolsIoError};
466 #[test]
467 fn valid() -> Result<(), OtToolsIoError> {
468 assert!(create_mock_new(None, None)?.check_file_version()?);
469 Ok(())
470 }
471
472 #[test]
473 fn invalid() -> Result<(), OtToolsIoError> {
474 let mut mutated = create_mock_new(None, None)?;
475 mutated.datatype_version = 0;
476 assert!(!mutated.check_file_version()?);
477 Ok(())
478 }
479}
480
481#[derive(PartialEq, Debug, Clone, Copy)]
484pub struct TrimConfig {
485 pub start: u32,
487 pub end: u32,
489 pub length: u32,
492}
493
494impl From<SampleSettingsFile> for TrimConfig {
495 fn from(decoded: SampleSettingsFile) -> Self {
496 Self {
497 start: decoded.trim_start,
498 end: decoded.trim_end,
499 length: decoded.trim_len,
500 }
501 }
502}
503
504impl From<&SampleSettingsFile> for TrimConfig {
505 fn from(decoded: &SampleSettingsFile) -> Self {
506 Self::from(decoded.clone())
507 }
508}
509
510#[cfg(test)]
511mod trim_config_from {
512 use super::{test_utils::create_mock_new, TrimConfig};
513 use crate::OtToolsIoError;
514
515 #[test]
516 fn owned() -> Result<(), OtToolsIoError> {
517 let s = create_mock_new(None, None)?;
518 assert_eq!(
519 TrimConfig::from(s),
520 TrimConfig {
521 start: 0,
522 end: 0,
523 length: 0,
524 }
525 );
526 Ok(())
527 }
528 #[test]
529 fn borrowed() -> Result<(), OtToolsIoError> {
530 let s = create_mock_new(None, None)?;
531 assert_eq!(
532 TrimConfig::from(&s),
533 TrimConfig {
534 start: 0,
535 end: 0,
536 length: 0,
537 }
538 );
539 Ok(())
540 }
541}
542
543#[derive(PartialEq, Debug, Clone, Copy)]
546pub struct LoopConfig {
547 pub start: u32,
549
550 pub length: u32,
552
553 pub mode: LoopMode,
555}
556
557impl LoopConfig {
558 pub fn new(start: u32, length: u32, mode: LoopMode) -> Self {
559 Self {
560 start,
561 length,
562 mode,
563 }
564 }
565}
566
567#[cfg(test)]
568mod loop_config_new {
569
570 use crate::samples::{LoopConfig, LoopMode};
571
572 #[test]
573 fn test_new_sample_loop_config_loop_off() {
574 assert_eq!(
575 LoopConfig::new(0, 10, LoopMode::Off),
576 LoopConfig {
577 start: 0,
578 length: 10,
579 mode: LoopMode::Off
580 }
581 );
582 }
583
584 #[test]
585 fn test_new_sample_loop_config_umin_start_umax_length() {
586 assert_eq!(
587 LoopConfig::new(u32::MIN, u32::MAX, LoopMode::Off),
588 LoopConfig {
589 start: u32::MIN,
590 length: u32::MAX,
591 mode: LoopMode::Off
592 }
593 );
594 }
595
596 #[test]
597 fn test_new_sample_loop_config_loop_normal() {
598 assert_eq!(
599 LoopConfig::new(0, 10, LoopMode::Normal),
600 LoopConfig {
601 start: 0,
602 length: 10,
603 mode: LoopMode::Normal
604 }
605 );
606 }
607
608 #[test]
609 fn test_new_sample_loop_config_loop_pingpong() {
610 assert_eq!(
611 LoopConfig::new(0, 10, LoopMode::PingPong),
612 LoopConfig {
613 start: 0,
614 length: 10,
615 mode: LoopMode::PingPong
616 }
617 );
618 }
619}
620
621impl From<SampleSettingsFile> for LoopConfig {
622 fn from(decoded: SampleSettingsFile) -> Self {
623 Self::new(
624 decoded.loop_start,
625 decoded.loop_len,
626 LoopMode::try_from(&(decoded.loop_mode as u8)).unwrap_or(LoopMode::default()),
627 )
628 }
629}
630
631impl From<&SampleSettingsFile> for LoopConfig {
632 fn from(decoded: &SampleSettingsFile) -> Self {
633 Self::from(decoded.clone())
634 }
635}
636
637#[cfg(test)]
638mod loop_config_from {
639 use super::{
640 test_utils::create_mock_new, LoopConfig, LoopMode, OtToolsIoError, SampleSettingsFile,
641 Slices, DEFAULT_GAIN, DEFAULT_TEMPO, SAMPLES_FILE_VERSION, SAMPLES_HEADER,
642 };
643
644 #[test]
645 fn owned() -> Result<(), OtToolsIoError> {
646 let s = create_mock_new(None, None)?;
647 assert_eq!(
648 LoopConfig::from(s),
649 LoopConfig {
650 start: 0,
651 length: 0,
652 mode: LoopMode::default(),
653 }
654 );
655 Ok(())
656 }
657 #[test]
658 fn borrowed() -> Result<(), OtToolsIoError> {
659 let s = create_mock_new(None, None)?;
660 assert_eq!(
661 LoopConfig::from(&s),
662 LoopConfig {
663 start: 0,
664 length: 0,
665 mode: LoopMode::default(),
666 }
667 );
668 Ok(())
669 }
670
671 fn mock_for_loop_conf(
672 loop_start: u32,
673 loop_len: u32,
674 loop_mode: LoopMode,
675 ) -> SampleSettingsFile {
676 SampleSettingsFile {
677 loop_len,
678 loop_start,
679 loop_mode: loop_mode.into(),
680 header: SAMPLES_HEADER,
681 datatype_version: SAMPLES_FILE_VERSION,
682 tempo: DEFAULT_TEMPO,
683 unknown: 0,
684 trim_len: 0,
685 stretch: 0,
686 gain: DEFAULT_GAIN,
687 quantization: 0,
688 trim_start: 0,
689 trim_end: 0,
690 slices: Slices::default().slices,
691 slices_len: 0,
692 checksum: 0,
693 }
694 }
695
696 #[test]
697 fn test_umin_start_umax_len() {
698 assert_eq!(
699 LoopConfig::from(mock_for_loop_conf(u32::MIN, u32::MAX, LoopMode::Off)),
700 LoopConfig {
701 start: u32::MIN,
702 length: u32::MAX,
703 mode: LoopMode::Off
704 }
705 );
706 }
707
708 #[test]
709 fn test_loop_off() {
710 assert_eq!(
711 LoopConfig::from(mock_for_loop_conf(0, 10, LoopMode::Off)),
712 LoopConfig {
713 start: 0,
714 length: 10,
715 mode: LoopMode::Off
716 }
717 );
718 }
719
720 #[test]
721 fn test_loop_normal() {
722 assert_eq!(
723 LoopConfig::from(mock_for_loop_conf(0, 10, LoopMode::Normal)),
724 LoopConfig {
725 start: 0,
726 length: 10,
727 mode: LoopMode::Normal
728 }
729 );
730 }
731
732 #[test]
733 fn test_loop_pingpong() {
734 assert_eq!(
735 LoopConfig::from(mock_for_loop_conf(0, 10, LoopMode::PingPong)),
736 LoopConfig {
737 start: 0,
738 length: 10,
739 mode: LoopMode::PingPong
740 }
741 );
742 }
743}