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, TimeStretchMode, 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 time-stretch algorithm applied to the sample.
86    /// See the [`TimeStretchMode`] enum for suitable choices.
87    pub stretch: u32,
88
89    /// Default loop mode applied to the sample.
90    /// See the [`LoopMode`] 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 [`TrigQuantizationMode`] 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    pub trim_start: u32,
105
106    /// Where the trim end marker is placed for the sample.
107    /// When the sample is being played in normal mode (i.e. not using slices),
108    /// the Octatrack will not play samples past this point.
109    /// By default, trim length should be equal to trim end,
110    /// and probably loop length too for drum hit sample chains.
111    pub trim_end: u32,
112
113    /// Start position for any loops. Default should be the same as trim start.
114    /// Measured in bars.
115    /// A note from the Octatrack manual on loop point/start behaviour:
116    /// > If a loop point is set, the sample will play from the start point to the
117    /// > end point, then loop from the loop point to the end point
118    pub loop_start: u32,
119
120    /// 64 length array containing `Slice`s.
121    /// See the `Slice` struct for more details.
122    /// Any empty slice positions should have zero-valued struct fields.
123    #[serde(with = "BigArray")]
124    pub slices: [Slice; 64],
125
126    /// Number of usable `Slice`s in this sample.
127    /// Used by the Octatrack to ignore zero-valued `Slice`s in the `slices` array when loading the sample.
128    pub slices_len: u32,
129
130    /// Checksum value for the struct.
131    /// This must be calculated **after** the struct is created on little-endian systems
132    /// (requires byte swapping all struct fields to get the correct checksum value).
133    pub checksum: u16,
134}
135
136impl SwapBytes for SampleSettingsFile {
137    fn swap_bytes(self) -> RBoxErr<Self> {
138        let mut bswapped_slices: [Slice; 64] = self.slices;
139
140        for (i, slice) in self.slices.iter().enumerate() {
141            bswapped_slices[i] = slice.swap_bytes()?;
142        }
143
144        let bswapped = Self {
145            header: SAMPLES_HEADER,
146            datatype_version: SAMPLES_FILE_VERSION,
147            unknown: self.unknown,
148            tempo: self.tempo.swap_bytes(),
149            trim_len: self.trim_len.swap_bytes(),
150            loop_len: self.loop_len.swap_bytes(),
151            stretch: self.stretch.swap_bytes(),
152            loop_mode: self.loop_mode.swap_bytes(),
153            gain: self.gain.swap_bytes(),
154            quantization: self.quantization.swap_bytes(),
155            trim_start: self.trim_start.swap_bytes(),
156            trim_end: self.trim_end.swap_bytes(),
157            loop_start: self.loop_start.swap_bytes(),
158            slices: bswapped_slices,
159            slices_len: self.slices_len.swap_bytes(),
160            checksum: self.checksum.swap_bytes(),
161        };
162
163        Ok(bswapped)
164    }
165}
166
167impl SampleSettingsFile {
168    /// Create a new `SampleSettingsFile`
169    ///
170    /// Values like `tempo` and `gain` need to be normalized to relevant OT
171    /// bounds before using this method.
172    /// ```rust
173    /// # use std::array::from_fn;
174    /// # use ot_tools_io::samples::{SampleSettingsFile, TrimConfig, LoopConfig};
175    /// # use ot_tools_io::{
176    /// #    TrigQuantizationMode,
177    /// #    LoopMode,
178    /// #    TimeStretchMode,
179    /// # };
180    /// # use ot_tools_io::slices::{Slices, Slice};
181    /// # let slice_arr: [Slice; 64] = from_fn(|_| Slice::default());
182    /// # let slices: Slices = Slices { slices: slice_arr, count: 0 };
183    /// let x = SampleSettingsFile::new(
184    ///     &2880,
185    ///     // ...
186    ///     # &TimeStretchMode::default(),
187    ///     # &TrigQuantizationMode::default(),
188    ///     &48,
189    ///     // ...
190    ///     # &TrimConfig { start: 0, end: 100, length: 25 },
191    ///     # &LoopConfig { start: 0, length: 100, mode: LoopMode::Off },
192    ///     # &slices,
193    /// ).unwrap();
194    /// assert_eq!(x.tempo, 2880);
195    /// assert_eq!(x.gain, 48);
196    /// ```
197    pub fn new(
198        tempo: &u32,
199        stretch: &TimeStretchMode,
200        quantization: &TrigQuantizationMode,
201        gain: &u16,
202        trim_config: &TrimConfig,
203        loop_config: &LoopConfig,
204        slices: &Slices,
205    ) -> RBoxErr<Self> {
206        #[allow(clippy::manual_range_contains)]
207        if tempo < &720 || tempo > &7200 {
208            return Err(SampleSettingsErrors::TempoOutOfBounds.into());
209        }
210
211        #[allow(clippy::manual_range_contains)]
212        if gain < &0 || gain > &96 {
213            return Err(SampleSettingsErrors::GainOutOfBounds.into());
214        }
215
216        Ok(Self {
217            header: SAMPLES_HEADER,
218            datatype_version: SAMPLES_FILE_VERSION,
219            unknown: 0,
220            gain: *gain,
221            stretch: stretch.value()?,
222            tempo: *tempo,
223            quantization: quantization.value()? as u8,
224            trim_start: trim_config.start,
225            trim_end: trim_config.end,
226            trim_len: trim_config.length, // bin lengths are multiplied by 100?
227            loop_start: loop_config.start,
228            loop_len: loop_config.length, // bin lengths are multiplied by 100?
229            loop_mode: loop_config.mode.value()?,
230            slices: slices.slices,
231            slices_len: slices.count,
232            checksum: 0,
233        })
234    }
235}
236
237// For samples, need to run special decode method as need to flip bytes depending on endianness
238impl Decode for SampleSettingsFile {
239    /// Decode raw bytes of a `.ot` data file into a new struct,
240    /// swap byte values if system is little-endian then do some minor
241    /// post-processing to get user-friendly settings values.
242    fn decode(bytes: &[u8]) -> RBoxErr<Self> {
243        let decoded: Self = bincode::deserialize(bytes)?;
244        let mut bswapd = decoded.clone();
245
246        // swapping bytes is one required when running on little-endian systems
247        if cfg!(target_endian = "little") {
248            bswapd = decoded.swap_bytes()?;
249        }
250
251        Ok(bswapd)
252    }
253}
254
255// For sample data, need swap bytes depending on endianness and calculate a checksum
256impl Encode for SampleSettingsFile {
257    /// Encodes struct data to binary representation, after some pre-processing.
258    ///
259    /// Before serializing, will:
260    /// 1. modify tempo and gain values to machine ranges
261    /// 2. swaps bytes of values (when current system is little-endian)
262    /// 3. generate checksum value
263    fn encode(&self) -> RBoxErr<Vec<u8>> {
264        let mut chkd = self.clone();
265        chkd.checksum = self.calculate_checksum()?;
266
267        let encoded = if cfg!(target_endian = "little") {
268            bincode::serialize(&chkd.swap_bytes()?)?
269        } else {
270            bincode::serialize(&chkd)?
271        };
272        Ok(encoded)
273    }
274}
275
276impl CalculateChecksum for SampleSettingsFile {
277    fn calculate_checksum(&self) -> RBoxErr<u16> {
278        let bytes = bincode::serialize(&self)?;
279
280        // skip header and checksum byte values
281        let checksum_bytes = &bytes[16..bytes.len() - 2];
282
283        let chk: u32 = checksum_bytes
284            .iter()
285            .map(|x| *x as u32)
286            .sum::<u32>()
287            .rem_euclid(u16::MAX as u32 + 1);
288
289        Ok(chk as u16)
290    }
291}
292
293impl CheckHeader for SampleSettingsFile {
294    fn check_header(&self) -> bool {
295        self.header == SAMPLES_HEADER
296    }
297}
298
299impl CheckChecksum for SampleSettingsFile {
300    fn check_checksum(&self) -> RBoxErr<bool> {
301        Ok(self.checksum == self.calculate_checksum()?)
302    }
303}
304
305impl CheckFileVersion for SampleSettingsFile {
306    fn check_file_version(&self) -> RBoxErr<bool> {
307        Ok(self.datatype_version == SAMPLES_FILE_VERSION)
308    }
309}
310
311// impl CheckIntegrity for SampleSettingsFile {}
312
313#[cfg(test)]
314mod test {
315    use crate::samples::{LoopConfig, TrimConfig};
316    use crate::slices::{Slice, Slices};
317    use crate::LoopMode;
318
319    fn create_mock_configs_blank() -> (TrimConfig, LoopConfig, Slices) {
320        let trim_config = TrimConfig {
321            start: 0,
322            end: 0,
323            length: 0,
324        };
325
326        let loop_config = LoopConfig {
327            start: 0,
328            length: 0,
329            mode: LoopMode::Normal,
330        };
331
332        let default_slice = Slice {
333            trim_start: 0,
334            trim_end: 0,
335            loop_start: 0xFFFFFFFF,
336        };
337
338        let slices: [Slice; 64] = [default_slice; 64];
339
340        let slice_conf = Slices { slices, count: 0 };
341
342        (trim_config, loop_config, slice_conf)
343    }
344
345    mod checksum {
346        /*
347        mod array_based {
348            use crate::CalculateChecksum;
349            use std::array::from_fn;
350
351            #[test]
352            #[ignore]
353            fn ok_no_overflow_all_255() {
354                let bytes: [u8; 832] = from_fn(|_| 255);
355                assert_eq!(
356                    get_checksum(&bytes).unwrap(),
357                    10962,
358                    "overflows should wrap"
359                );
360            }
361            #[test]
362            #[ignore]
363            fn ok_no_overflow() {
364                let bytes: [u8; 832] = from_fn(|_| 78);
365
366                assert_eq!(
367                    get_checksum(&bytes).unwrap(),
368                    63492,
369                    "random value that shouldn't overflow at all"
370                );
371            }
372
373            #[test]
374            #[ignore]
375            fn ok_64_slice_len() {
376                let bytes: [u8; 832] = from_fn(|_| 64);
377                assert_eq!(get_checksum(&bytes).unwrap(), 52096,);
378            }
379
380            #[test]
381            #[ignore]
382            fn ok_one_bytes() {
383                let bytes: [u8; 832] = from_fn(|_| 1);
384                assert_eq!(
385                    get_checksum(&bytes).unwrap(),
386                    814,
387                    "low values definitely don't overflow"
388                );
389            }
390
391            #[test]
392            #[ignore]
393            fn ok_zeroed() {
394                let bytes: [u8; 832] = from_fn(|_| 0);
395                assert_eq!(
396                    get_checksum(&bytes).unwrap(),
397                    0,
398                    "unknown if this is correct behaviour"
399                );
400            }
401        }
402        */
403
404        mod file_read {
405            use crate::samples::{CalculateChecksum, SampleSettingsFile};
406            use crate::test_utils::get_samples_dirpath;
407            use crate::OctatrackFileIO;
408
409            #[test]
410            fn zero_slices_trig_quant_one_step() {
411                let path = get_samples_dirpath()
412                    .join("checksum")
413                    .join("0slices-tq1step.ot");
414                let data = SampleSettingsFile::from_data_file(&path).unwrap();
415                assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
416            }
417
418            #[test]
419            fn zero_slices() {
420                let path = get_samples_dirpath().join("checksum").join("0slices.ot");
421                let data = SampleSettingsFile::from_data_file(&path).unwrap();
422                assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
423            }
424
425            #[test]
426            fn one_slice() {
427                let path = get_samples_dirpath().join("checksum").join("1slices.ot");
428                let data = SampleSettingsFile::from_data_file(&path).unwrap();
429                assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
430            }
431            #[test]
432            fn two_slices() {
433                let path = get_samples_dirpath().join("checksum").join("2slices.ot");
434                let data = SampleSettingsFile::from_data_file(&path).unwrap();
435                assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
436            }
437            #[test]
438            fn four_slices() {
439                let path = get_samples_dirpath().join("checksum").join("4slices.ot");
440                let data = SampleSettingsFile::from_data_file(&path).unwrap();
441                assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
442            }
443            #[test]
444            fn eight_slices() {
445                let path = get_samples_dirpath().join("checksum").join("8slices.ot");
446                let data = SampleSettingsFile::from_data_file(&path).unwrap();
447                assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
448            }
449            #[test]
450            fn sixteen_slices() {
451                let path = get_samples_dirpath().join("checksum").join("16slices.ot");
452                let data = SampleSettingsFile::from_data_file(&path).unwrap();
453                assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
454            }
455            #[test]
456            fn thirty_two_slices() {
457                let path = get_samples_dirpath().join("checksum").join("32slices.ot");
458                let data = SampleSettingsFile::from_data_file(&path).unwrap();
459                assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
460            }
461            #[test]
462            fn forty_eight_slices() {
463                let path = get_samples_dirpath().join("checksum").join("48slices.ot");
464                let data = SampleSettingsFile::from_data_file(&path).unwrap();
465                assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
466            }
467            #[test]
468            fn sixty_two_slices() {
469                let path = get_samples_dirpath().join("checksum").join("62slices.ot");
470                let data = SampleSettingsFile::from_data_file(&path).unwrap();
471                assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
472            }
473            #[test]
474            fn sixty_three_slices() {
475                let path = get_samples_dirpath().join("checksum").join("63slices.ot");
476                let data = SampleSettingsFile::from_data_file(&path).unwrap();
477                assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
478            }
479            #[test]
480            fn sixty_four_slices_correct() {
481                let path = get_samples_dirpath().join("checksum").join("64slices.ot");
482                let data = SampleSettingsFile::from_data_file(&path).unwrap();
483                assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
484            }
485        }
486        mod file_write {
487            use crate::samples::SampleSettingsFile;
488            use crate::test_utils::get_samples_dirpath;
489            use crate::OctatrackFileIO;
490
491            fn helper_write(test_name: String) {
492                let mut src_path = get_samples_dirpath().join("checksum").join(&test_name);
493                src_path.set_extension("ot");
494
495                let test_dir_path = std::env::temp_dir()
496                    .join("ot-tools-io")
497                    .join("samples")
498                    .join(&test_name);
499
500                let mut test_infile_path = test_dir_path.join("test");
501                test_infile_path.set_extension("ot");
502                let _ = std::fs::create_dir_all(&test_dir_path);
503
504                let valid = SampleSettingsFile::from_data_file(&src_path).unwrap();
505
506                // need to reset checksum as it gets calculated during encoding
507                let mut to_write = valid.clone();
508                to_write.checksum = 0;
509
510                to_write.to_data_file(&test_infile_path).unwrap();
511                let written = SampleSettingsFile::from_data_file(&test_infile_path);
512
513                assert!(written.is_ok());
514                assert_eq!(valid, written.unwrap());
515            }
516
517            #[test]
518            fn zero_slices_trig_quant_one_step() {
519                helper_write("0slices-tq1step".to_string());
520            }
521
522            #[test]
523            fn zero_slices() {
524                helper_write("0slices".to_string());
525            }
526
527            #[test]
528            fn one_slice() {
529                helper_write("1slices".to_string());
530            }
531            #[test]
532            fn two_slices() {
533                helper_write("2slices".to_string());
534            }
535            #[test]
536            fn four_slices() {
537                helper_write("4slices".to_string());
538            }
539            #[test]
540            fn eight_slices() {
541                helper_write("8slices".to_string());
542            }
543            #[test]
544            fn sixteen_slices() {
545                helper_write("16slices".to_string());
546            }
547            #[test]
548            fn thirty_two_slices() {
549                helper_write("32slices".to_string());
550            }
551            #[test]
552            fn forty_eight_slices() {
553                helper_write("48slices".to_string());
554            }
555            #[test]
556            fn sixty_two_slices() {
557                helper_write("62slices".to_string());
558            }
559            #[test]
560            fn sixty_three_slices() {
561                helper_write("63slices".to_string());
562            }
563            #[test]
564            fn sixty_four_slices_correct() {
565                helper_write("64slices".to_string());
566            }
567        }
568    }
569
570    mod create_new {
571
572        use super::create_mock_configs_blank;
573        use crate::samples::SampleSettingsFile;
574        use crate::{TimeStretchMode, TrigQuantizationMode};
575
576        #[test]
577        fn err_oob_tempo() {
578            let (trim_conf, loop_conf, slices) = create_mock_configs_blank();
579
580            let composed_chain = SampleSettingsFile::new(
581                &7201,
582                &TimeStretchMode::Off,
583                &TrigQuantizationMode::PatternLength,
584                &46,
585                &trim_conf,
586                &loop_conf,
587                &slices,
588            );
589
590            assert!(composed_chain.is_err());
591        }
592
593        #[test]
594        fn err_invalid_gain() {
595            let (trim_conf, loop_conf, slices) = create_mock_configs_blank();
596
597            let composed_chain = SampleSettingsFile::new(
598                &2880,
599                &TimeStretchMode::Off,
600                &TrigQuantizationMode::PatternLength,
601                &97,
602                &trim_conf,
603                &loop_conf,
604                &slices,
605            );
606
607            assert!(composed_chain.is_err());
608        }
609    }
610
611    mod integrity {
612        use super::create_mock_configs_blank;
613        use crate::samples::{SampleSettingsFile, DEFAULT_GAIN, DEFAULT_TEMPO};
614        use crate::CheckHeader;
615        use crate::{TimeStretchMode, TrigQuantizationMode};
616        #[test]
617        fn true_valid_header() {
618            let (trim_conf, loop_conf, slices) = create_mock_configs_blank();
619            let new = SampleSettingsFile::new(
620                &DEFAULT_TEMPO,
621                &TimeStretchMode::Off,
622                &TrigQuantizationMode::PatternLength,
623                &DEFAULT_GAIN,
624                &trim_conf,
625                &loop_conf,
626                &slices,
627            );
628            assert!(new.unwrap().check_header());
629        }
630
631        #[test]
632        fn false_invalid_header() {
633            let (trim_conf, loop_conf, slices) = create_mock_configs_blank();
634            let new = SampleSettingsFile::new(
635                &DEFAULT_TEMPO,
636                &TimeStretchMode::Off,
637                &TrigQuantizationMode::PatternLength,
638                &DEFAULT_GAIN,
639                &trim_conf,
640                &loop_conf,
641                &slices,
642            );
643            let mut mutated = new.unwrap();
644            mutated.header[0] = 0x00;
645            mutated.header[20] = 111;
646            assert!(!mutated.check_header());
647        }
648    }
649}
650
651/// A helper struct for the OT Sample's Trim settings
652
653#[derive(PartialEq, Debug, Clone, Copy)]
654pub struct TrimConfig {
655    /// Start of full audio sample (n samples)
656    pub start: u32,
657    /// End of full audio sample (n samples)
658    pub end: u32,
659    /// Length of audio sample to play before stopping/looping playback
660    /// NOTE: This is measured in number of bars.
661    pub length: u32,
662}
663
664impl TrimConfig {
665    pub fn from_decoded(decoded: &SampleSettingsFile) -> RBoxErr<Self> {
666        let new = TrimConfig {
667            start: decoded.trim_start,
668            end: decoded.trim_end,
669            length: decoded.trim_len,
670        };
671        Ok(new)
672    }
673}
674
675/// A helper struct for the OT Sample's Loop settings
676
677#[derive(PartialEq, Debug, Clone, Copy)]
678pub struct LoopConfig {
679    /// Loop start position for the audio sample (n samples).
680    pub start: u32,
681
682    /// Length of the loop for the audio sample (n samples).
683    pub length: u32,
684
685    /// Type of looping mode.
686    pub mode: LoopMode,
687}
688
689impl LoopConfig {
690    pub fn new(start: u32, length: u32, mode: LoopMode) -> RBoxErr<Self> {
691        Ok(Self {
692            start,
693            length,
694            mode,
695        })
696    }
697
698    pub fn from_decoded(decoded: &SampleSettingsFile) -> RBoxErr<Self> {
699        Self::new(
700            decoded.loop_start,
701            decoded.loop_len,
702            LoopMode::from_value(&decoded.loop_mode).unwrap_or(LoopMode::Off),
703        )
704    }
705}
706
707#[cfg(test)]
708#[allow(unused_imports)]
709mod config_tests {
710
711    mod test_sample_loop_config {
712        mod test_new {
713
714            use crate::samples::{LoopConfig, LoopMode};
715
716            #[test]
717            fn test_new_sample_loop_config_loop_off() {
718                assert_eq!(
719                    LoopConfig::new(0, 10, LoopMode::Off).unwrap(),
720                    LoopConfig {
721                        start: 0,
722                        length: 10,
723                        mode: LoopMode::Off
724                    }
725                );
726            }
727
728            #[test]
729            fn test_new_sample_loop_config_umin_start_umax_length() {
730                assert_eq!(
731                    LoopConfig::new(u32::MIN, u32::MAX, LoopMode::Off).unwrap(),
732                    LoopConfig {
733                        start: u32::MIN,
734                        length: u32::MAX,
735                        mode: LoopMode::Off
736                    }
737                );
738            }
739
740            #[test]
741            fn test_new_sample_loop_config_loop_normal() {
742                assert_eq!(
743                    LoopConfig::new(0, 10, LoopMode::Normal).unwrap(),
744                    LoopConfig {
745                        start: 0,
746                        length: 10,
747                        mode: LoopMode::Normal
748                    }
749                );
750            }
751
752            #[test]
753            fn test_new_sample_loop_config_loop_pingpong() {
754                assert_eq!(
755                    LoopConfig::new(0, 10, LoopMode::PingPong).unwrap(),
756                    LoopConfig {
757                        start: 0,
758                        length: 10,
759                        mode: LoopMode::PingPong
760                    }
761                );
762            }
763        }
764    }
765
766    mod test_from_decoded {
767
768        use crate::slices::Slice;
769        use crate::{
770            samples::{
771                LoopConfig, LoopMode, SampleSettingsFile, SAMPLES_FILE_VERSION, SAMPLES_HEADER,
772            },
773            OptionEnumValueConvert,
774        };
775
776        #[test]
777        fn test_umin_start_umax_len() {
778            let decoded = SampleSettingsFile {
779                header: SAMPLES_HEADER,
780                datatype_version: SAMPLES_FILE_VERSION,
781                unknown: 0,
782                tempo: 128000,
783                trim_len: 0,
784                loop_len: u32::MAX,
785                stretch: 0,
786                loop_mode: LoopMode::Off.value().unwrap(),
787                gain: 0,
788                quantization: 0,
789                trim_start: 0,
790                trim_end: 0,
791                loop_start: u32::MIN,
792                slices: [Slice {
793                    trim_start: 0,
794                    trim_end: 0,
795                    loop_start: 0,
796                }; 64],
797                slices_len: 0,
798                checksum: 0,
799            };
800
801            assert_eq!(
802                LoopConfig::from_decoded(&decoded).unwrap(),
803                LoopConfig {
804                    start: u32::MIN,
805                    length: u32::MAX,
806                    mode: LoopMode::Off
807                }
808            );
809        }
810
811        #[test]
812        fn test_loop_off() {
813            let decoded = SampleSettingsFile {
814                header: SAMPLES_HEADER,
815                datatype_version: SAMPLES_FILE_VERSION,
816                unknown: 0,
817                tempo: 128000,
818                trim_len: 0,
819                loop_len: 10,
820                stretch: 0,
821                loop_mode: LoopMode::Off.value().unwrap(),
822                gain: 0,
823                quantization: 0,
824                trim_start: 0,
825                trim_end: 0,
826                loop_start: 0,
827                slices: [Slice {
828                    trim_start: 0,
829                    trim_end: 0,
830                    loop_start: 0,
831                }; 64],
832                slices_len: 0,
833                checksum: 0,
834            };
835
836            assert_eq!(
837                LoopConfig::from_decoded(&decoded).unwrap(),
838                LoopConfig {
839                    start: 0,
840                    length: 10,
841                    mode: LoopMode::Off
842                }
843            );
844        }
845
846        #[test]
847        fn test_loop_normal() {
848            let decoded = SampleSettingsFile {
849                header: SAMPLES_HEADER,
850                datatype_version: SAMPLES_FILE_VERSION,
851                unknown: 0,
852                tempo: 128000,
853                trim_len: 0,
854                loop_len: 10,
855                stretch: 0,
856                loop_mode: LoopMode::Normal.value().unwrap(),
857                gain: 0,
858                quantization: 0,
859                trim_start: 0,
860                trim_end: 0,
861                loop_start: 0,
862                slices: [Slice {
863                    trim_start: 0,
864                    trim_end: 0,
865                    loop_start: 0,
866                }; 64],
867                slices_len: 0,
868                checksum: 0,
869            };
870
871            assert_eq!(
872                LoopConfig::from_decoded(&decoded).unwrap(),
873                LoopConfig {
874                    start: 0,
875                    length: 10,
876                    mode: LoopMode::Normal
877                }
878            );
879        }
880
881        #[test]
882        fn test_loop_pingpong() {
883            let decoded = SampleSettingsFile {
884                header: SAMPLES_HEADER,
885                datatype_version: SAMPLES_FILE_VERSION,
886                unknown: 0,
887                tempo: 128000,
888                trim_len: 0,
889                loop_len: 10,
890                stretch: 0,
891                loop_mode: LoopMode::PingPong.value().unwrap(),
892                gain: 0,
893                quantization: 0,
894                trim_start: 0,
895                trim_end: 0,
896                loop_start: 0,
897                slices: [Slice {
898                    trim_start: 0,
899                    trim_end: 0,
900                    loop_start: 0,
901                }; 64],
902                slices_len: 0,
903                checksum: 0,
904            };
905
906            assert_eq!(
907                LoopConfig::from_decoded(&decoded).unwrap(),
908                LoopConfig {
909                    start: 0,
910                    length: 10,
911                    mode: LoopMode::PingPong
912                }
913            );
914        }
915    }
916}