ot_tools_io/
samples.rs

1/*
2SPDX-License-Identifier: GPL-3.0-or-later
3Copyright © 2024 Mike Robeson [dijksterhuis]
4*/
5
6//! Types and ser/de of `*.ot` binary data files.
7
8use 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
17// in `hexdump -C` format:
18// ```
19// FORM....DPS1SMPA
20// ......
21// ```
22/// Standard header bytes in a sample settings file
23pub 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
28/// Current/supported datatype version for a sample settings file
29pub 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/// Struct to create a valid Octatrack `.ot` file a.k.a a sample settings file.
53/// General metadata for the sample's configuration on the OT
54/// and the slice array with pointer positions for the sliced WAV.
55///
56/// NOTE: On the naming for this -- the Octatrack manual specifically refers to
57/// > SAVE SAMPLE SETTINGS will save the trim, slice and attribute settings in a
58/// > separate file and link it to the sample currently being edited.
59/// > -- page 87.
60///
61/// So ... this is the SETTINGS file.
62#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Hash, OctatrackFile, IntegrityChecks)]
63pub struct SampleSettingsFile {
64    /// Header
65    pub header: [u8; 21],
66
67    /// Datatype's version ID//
68    pub datatype_version: u8,
69
70    /// Unknown data
71    pub unknown: u8,
72
73    /// Tempo is always the machine UI's BPM multiplied by 24
74    pub tempo: u32,
75
76    /// Number of bars for the sample trim length marker.
77    /// By default, trim length should be equal to trim end,
78    /// and probably loop length too for drum hit sample chains.
79    pub trim_len: u32,
80
81    /// Number of bars for the sample loop length marker.
82    /// By default, loop length should be equal to trim length for sample chains.
83    pub loop_len: u32,
84
85    /// Default timestrech algorithm applied to the sample.
86    /// See the `ot_sample::options::SampleTimestrechModes` enum for suitable choices.
87    pub stretch: u32,
88
89    /// Default loop mode applied to the sample.
90    /// See the `ot_sample::options::SampleLoopModes` enum for suitable choices.
91    pub loop_mode: u32,
92
93    /// Gain of the sample.
94    /// -24.0 db <= x <= +24 db range in the machine's UI, with increments of 0.5 db changes.
95    /// 0 <= x <= 96 range in binary data file.
96    pub gain: u16,
97
98    /// Default trig quantization mode applied to the sample.
99    /// See the `ot_sample::options::SampleTrigQuantizationModes` enum for suitable choices.
100    pub quantization: u8,
101
102    /// Where the trim start marker is placed for the sample, measured in bars.
103    /// Default is 0 (start of sample).
104    /// TODO: measured in bars? or samples?
105    pub trim_start: u32,
106
107    /// Where the trim end marker is placed for the sample.
108    /// When the sample is being played in normal mode (i.e. not using slices),
109    /// the Octatrack will not play samples past this point.
110    /// By default, trim length should be equal to trim end,
111    /// and probably loop length too for drum hit sample chains.
112    /// TODO: measured in bars? or samples?
113    pub trim_end: u32,
114
115    /// Start position for any loops. Default should be the same as trim start.
116    /// Measured in bars.
117    /// A note from the Octatrack manual on loop point/start behaviour:
118    /// > If a loop point is set, the sample will play from the start point to the
119    /// > end point, then loop from the loop point to the end point
120    pub loop_start: u32,
121
122    /// 64 length array containing `Slice`s.
123    /// See the `Slice` struct for more details.
124    /// Any empty slice positions should have zero-valued struct fields.
125    #[serde(with = "BigArray")]
126    pub slices: [Slice; 64],
127
128    /// Number of usuable `Slice`s in this sample.
129    /// Used by the Octatrack to ignore zero-valued `Slice`s in the `slices` array when loading the sample.
130    pub slices_len: u32,
131
132    /// Checksum value for the struct.
133    /// This must be calculated **after** the struct is created on little-endian systems
134    /// (requires byte swapping all struct fields to get the correct checksum value).
135    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    /// Create a new `SampleSettingsFile`
171    ///
172    /// Values like `tempo` and `gain` need to be normalised to relevant OT
173    /// bounds before using this method.
174    /// ```rust
175    /// # use std::array::from_fn;
176    /// # use ot_tools_io::samples::{SampleSettingsFile, TrimConfig, LoopConfig};
177    /// # use ot_tools_io::{
178    /// #    TrigQuantizationMode,
179    /// #    LoopMode,
180    /// #    TimestrechMode,
181    /// # };
182    /// # use ot_tools_io::slices::{Slices, Slice};
183    /// # let slice_arr: [Slice; 64] = from_fn(|_| Slice::default());
184    /// # let slices: Slices = Slices { slices: slice_arr, count: 0 };
185    /// let x = SampleSettingsFile::new(
186    ///     &2880,
187    ///     // ...
188    ///     # &TimestrechMode::default(),
189    ///     # &TrigQuantizationMode::default(),
190    ///     &48,
191    ///     // ...
192    ///     # &TrimConfig { start: 0, end: 100, length: 25 },
193    ///     # &LoopConfig { start: 0, length: 100, mode: LoopMode::Off },
194    ///     # &slices,
195    /// ).unwrap();
196    /// assert_eq!(x.tempo, 2880);
197    /// assert_eq!(x.gain, 48);
198    /// ```
199    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, // bin lengths are multiplied by 100?
229            loop_start: loop_config.start,
230            loop_len: loop_config.length, // bin lengths are multiplied by 100?
231            loop_mode: loop_config.mode.value()?,
232            slices: slices.slices,
233            slices_len: slices.count,
234            checksum: 0,
235        })
236    }
237}
238
239// For samples, need to run special decode method as need to flip bytes depending on endianness
240impl Decode for SampleSettingsFile {
241    /// Decode raw bytes of a `.ot` data file into a new struct,
242    /// swap byte values if system is little-endian then do some minor
243    /// post-processing to get user-friendly settings values.
244    fn decode(bytes: &[u8]) -> RBoxErr<Self> {
245        let decoded: Self = bincode::deserialize(bytes)?;
246        let mut bswapd = decoded.clone();
247
248        // swapping bytes is one required when running on little-endian systems
249        if cfg!(target_endian = "little") {
250            bswapd = decoded.swap_bytes()?;
251        }
252
253        Ok(bswapd)
254    }
255}
256
257// For sample data, need swap bytes depending on endianness and calculate a checksum
258impl Encode for SampleSettingsFile {
259    /// Encodes struct data to binary representation, after some pre-processing.
260    ///
261    /// Before serializing, will:
262    /// 1. modify tempo and gain values to machine ranges
263    /// 2. swaps bytes of values (when current system is little-endian)
264    /// 3. generate checksum value
265    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        // skip header and checksum byte values
283        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// impl CheckIntegrity for SampleSettingsFile {}
314
315#[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        /*
349        mod array_based {
350            use crate::CalculateChecksum;
351            use std::array::from_fn;
352
353            #[test]
354            #[ignore]
355            fn ok_no_overflow_all_255() {
356                let bytes: [u8; 832] = from_fn(|_| 255);
357                assert_eq!(
358                    get_checksum(&bytes).unwrap(),
359                    10962,
360                    "overflows should wrap"
361                );
362            }
363            #[test]
364            #[ignore]
365            fn ok_no_overflow() {
366                let bytes: [u8; 832] = from_fn(|_| 78);
367
368                assert_eq!(
369                    get_checksum(&bytes).unwrap(),
370                    63492,
371                    "random value that shouldn't overflow at all"
372                );
373            }
374
375            #[test]
376            #[ignore]
377            fn ok_64_slice_len() {
378                let bytes: [u8; 832] = from_fn(|_| 64);
379                assert_eq!(get_checksum(&bytes).unwrap(), 52096,);
380            }
381
382            #[test]
383            #[ignore]
384            fn ok_one_bytes() {
385                let bytes: [u8; 832] = from_fn(|_| 1);
386                assert_eq!(
387                    get_checksum(&bytes).unwrap(),
388                    814,
389                    "low values definitely don't overflow"
390                );
391            }
392
393            #[test]
394            #[ignore]
395            fn ok_zeroed() {
396                let bytes: [u8; 832] = from_fn(|_| 0);
397                assert_eq!(
398                    get_checksum(&bytes).unwrap(),
399                    0,
400                    "unknown if this is correct behaviour"
401                );
402            }
403        }
404        */
405
406        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                // need to reset checksum as it gets cacluated during encoding
509                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/// A helper struct for the OT Sample's Trim settings
654
655#[derive(PartialEq, Debug, Clone, Copy)]
656pub struct TrimConfig {
657    /// Start of full audio sample (n samples)
658    pub start: u32,
659    /// End of full audio sample (n samples)
660    pub end: u32,
661    /// Length of audio sample to play before stopping/looping playback
662    /// NOTE: This is measured in number of bars.
663    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/// A helper struct for the OT Sample's Loop settings
678
679#[derive(PartialEq, Debug, Clone, Copy)]
680pub struct LoopConfig {
681    /// Loop start position for the audio sample (n samples).
682    pub start: u32,
683
684    /// Length of the loop for the audio sample (n samples).
685    pub length: u32,
686
687    /// Type of looping mode.
688    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}