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 SwapBytes for SampleSettingsFile {
196 fn swap_bytes(self) -> Self {
197 let mut bswapped_slices: [Slice; 64] = self.slices;
198
199 for (i, slice) in self.slices.iter().enumerate() {
200 bswapped_slices[i] = slice.swap_bytes();
201 }
202
203 Self {
204 header: SAMPLES_HEADER,
205 datatype_version: SAMPLES_FILE_VERSION,
206 unknown: self.unknown,
207 tempo: self.tempo.swap_bytes(),
208 trim_len: self.trim_len.swap_bytes(),
209 loop_len: self.loop_len.swap_bytes(),
210 stretch: self.stretch.swap_bytes(),
211 loop_mode: self.loop_mode.swap_bytes(),
212 gain: self.gain.swap_bytes(),
213 quantization: self.quantization.swap_bytes(),
214 trim_start: self.trim_start.swap_bytes(),
215 trim_end: self.trim_end.swap_bytes(),
216 loop_start: self.loop_start.swap_bytes(),
217 slices: bswapped_slices,
218 slices_len: self.slices_len.swap_bytes(),
219 checksum: self.checksum.swap_bytes(),
220 }
221 }
222}
223
224impl SampleSettingsFile {
225 pub fn new(
255 tempo: &u32,
256 stretch: &TimeStretchMode,
257 quantization: &TrigQuantizationMode,
258 gain: &u16,
259 trim_config: &TrimConfig,
260 loop_config: &LoopConfig,
261 slices: &Slices,
262 ) -> Result<Self, SampleSettingsError> {
263 #[allow(clippy::manual_range_contains)]
264 if tempo < &720 || tempo > &7200 {
265 return Err(SampleSettingsError::TempoOutOfBounds { value: *tempo });
266 }
267
268 #[allow(clippy::manual_range_contains)]
269 if gain < &0 || gain > &96 {
270 return Err(SampleSettingsError::GainOutOfBounds {
271 value: *gain as u32,
272 });
273 }
274
275 Ok(Self {
276 header: SAMPLES_HEADER,
277 datatype_version: SAMPLES_FILE_VERSION,
278 unknown: 0,
279 gain: *gain,
280 stretch: (*stretch).into(),
281 tempo: *tempo,
282 quantization: (*quantization).into(),
283 trim_start: trim_config.start,
284 trim_end: trim_config.end,
285 trim_len: trim_config.length, loop_start: loop_config.start,
287 loop_len: loop_config.length, loop_mode: loop_config.mode.into(),
289 slices: slices.slices,
290 slices_len: slices.count,
291 checksum: 0,
292 })
293 }
294}
295
296#[cfg(test)]
297mod new {
298 use super::test_utils::create_mock_new;
299 use crate::OtToolsIoError;
300 #[test]
301 fn invalid_oob_temp() -> Result<(), OtToolsIoError> {
302 assert_eq!(
303 create_mock_new(Some(7201), None).unwrap_err().to_string(),
304 "invalid tempo value, must be u32 in range 720 <= x <= 7200: 7201".to_string(),
305 );
306 Ok(())
307 }
308
309 #[test]
310 fn invalid_oob_gain() -> Result<(), OtToolsIoError> {
311 assert_eq!(
312 create_mock_new(None, Some(97)).unwrap_err().to_string(),
313 "invalid gain, must be u16 in range 0 <= x <= 96: 97".to_string(),
314 );
315 Ok(())
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}