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::markers::SlotMarkers;
9use crate::projects::SlotAttributes;
10use crate::settings::{LoopMode, TimeStretchMode, TrigQuantizationMode};
11use crate::slices::{Slice, Slices};
12use crate::traits::SwapBytes;
13use crate::{
14    HasChecksumField, HasFileVersionField, HasHeaderField, OctatrackFileIO, OtToolsIoError,
15};
16use ot_tools_io_derive::IntegrityChecks;
17use serde::{Deserialize, Serialize};
18use serde_big_array::BigArray;
19use thiserror::Error;
20
21#[cfg(test)]
22mod test_utils {
23    use super::{
24        LoopConfig, LoopMode, SampleSettingsError, SampleSettingsFile, Slice, Slices,
25        TimeStretchMode, TrigQuantizationMode, TrimConfig, DEFAULT_GAIN, DEFAULT_TEMPO,
26        SAMPLES_FILE_VERSION, SAMPLES_HEADER,
27    };
28
29    pub(crate) fn create_mock_new(
30        tempo: Option<u32>,
31        gain: Option<u16>,
32    ) -> Result<SampleSettingsFile, SampleSettingsError> {
33        let trim_config = TrimConfig {
34            start: 0,
35            end: 0,
36            length: 0,
37        };
38
39        let loop_config = LoopConfig {
40            start: 0,
41            length: 0,
42            mode: LoopMode::default(),
43        };
44
45        let default_slice = Slice {
46            trim_start: 0,
47            trim_end: 0,
48            loop_start: 0xFFFFFFFF,
49        };
50
51        let slices: [Slice; 64] = [default_slice; 64];
52
53        let slice_conf = Slices { slices, count: 0 };
54
55        SampleSettingsFile::new(
56            &tempo.unwrap_or(DEFAULT_TEMPO),
57            &TimeStretchMode::Off,
58            &TrigQuantizationMode::PatternLength,
59            &gain.unwrap_or(DEFAULT_GAIN),
60            &trim_config,
61            &loop_config,
62            &slice_conf,
63        )
64    }
65
66    pub(crate) fn mock_0_slice() -> SampleSettingsFile {
67        SampleSettingsFile {
68            header: SAMPLES_HEADER,
69            datatype_version: SAMPLES_FILE_VERSION,
70            unknown: 1,
71            tempo: 2281,
72            trim_len: 800,
73            loop_len: 800,
74            stretch: 2,
75            loop_mode: 0,
76            gain: 48,
77            quantization: 255,
78            trim_start: 0,
79            trim_end: 890839,
80            loop_start: 0,
81            slices: Slices::default().slices,
82            slices_len: 0,
83            checksum: 998,
84        }
85    }
86}
87
88#[derive(Debug, Error)]
89pub enum SampleSettingsError {
90    #[error("invalid gain, must be u16 in range 0 <= x <= 96: {value}")]
91    GainOutOfBounds { value: u32 },
92    #[error("invalid tempo value, must be u32 in range 720 <= x <= 7200: {value}")]
93    TempoOutOfBounds { value: u32 },
94}
95
96// in `hexdump -C` format:
97// ```
98// FORM....DPS1SMPA
99// ......
100// ```
101/// Standard header bytes in a sample settings file
102pub const SAMPLES_HEADER: [u8; 21] = [
103    0x46, 0x4F, 0x52, 0x4D, 0x00, 0x00, 0x00, 0x00, 0x44, 0x50, 0x53, 0x31, 0x53, 0x4D, 0x50, 0x41,
104    0x00, 0x00, 0x00, 0x00, 0x00,
105];
106
107/// Current/supported datatype version for a sample settings file
108pub const SAMPLES_FILE_VERSION: u8 = 2;
109
110pub const DEFAULT_TEMPO: u32 = 2880;
111pub const DEFAULT_GAIN: u16 = 48;
112
113/// Struct to create a valid Octatrack `.ot` file a.k.a. a sample settings file.
114/// General metadata for the sample's configuration on the OT
115/// and the slice array with pointer positions for the sliced WAV.
116///
117/// NOTE: On the naming for this -- the Octatrack manual specifically refers to
118/// > SAVE SAMPLE SETTINGS will save the trim, slice and attribute settings in a
119/// > separate file and link it to the sample currently being edited.
120/// > -- page 87.
121///
122/// So ... this is the SETTINGS file.
123#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Hash, IntegrityChecks)]
124pub struct SampleSettingsFile {
125    /// Header
126    pub header: [u8; 21],
127
128    /// Datatype's version ID//
129    pub datatype_version: u8,
130
131    /// Unknown data -- can sometimes be 1, usually 0.
132    pub unknown: u8,
133
134    /// Tempo is always the machine UI's BPM multiplied by 24
135    pub tempo: u32,
136
137    /// Number of bars for the sample trim length marker.
138    /// By default, trim length should be equal to trim end,
139    /// and probably loop length too for drum hit sample chains.
140    pub trim_len: u32,
141
142    /// Number of bars for the sample loop length marker.
143    /// By default, loop length should be equal to trim length for sample chains.
144    pub loop_len: u32,
145
146    /// Default time-stretch algorithm applied to the sample.
147    /// See the [`TimeStretchMode`] enum for suitable choices.
148    pub stretch: u32,
149
150    /// Default loop mode applied to the sample.
151    /// See the [`LoopMode`] enum for suitable choices.
152    pub loop_mode: u32,
153
154    /// Gain of the sample.
155    /// -24.0 db <= x <= +24 db range in the machine's UI, with increments of 0.5 db changes.
156    /// 0 <= x <= 96 range in binary data file.
157    pub gain: u16,
158
159    /// Default trig quantization mode applied to the sample.
160    /// See the [`TrigQuantizationMode`] enum for suitable choices.
161    pub quantization: u8,
162
163    /// Where the trim start marker is placed for the sample, measured in bars.
164    /// Default is 0 (start of sample).
165    pub trim_start: u32,
166
167    /// Where the trim end marker is placed for the sample.
168    /// When the sample is being played in normal mode (i.e. not using slices),
169    /// the Octatrack will not play samples past this point.
170    /// By default, trim length should be equal to trim end,
171    /// and probably loop length too for drum hit sample chains.
172    pub trim_end: u32,
173
174    /// Start position for any loops. Default should be the same as trim start.
175    /// Measured in bars.
176    /// A note from the Octatrack manual on loop point/start behaviour:
177    /// > If a loop point is set, the sample will play from the start point to the
178    /// > end point, then loop from the loop point to the end point
179    pub loop_start: u32,
180
181    /// 64 length array containing `Slice`s.
182    /// See the `Slice` struct for more details.
183    /// Any empty slice positions should have zero-valued struct fields.
184    #[serde(with = "BigArray")]
185    pub slices: [Slice; 64],
186
187    /// Number of usable `Slice`s in this sample.
188    /// Used by the Octatrack to ignore zero-valued `Slice`s in the `slices` array when loading the sample.
189    pub slices_len: u32,
190
191    /// Checksum value for the struct.
192    /// This must be calculated **after** the struct is created on little-endian systems
193    /// (requires byte swapping all struct fields to get the correct checksum value).
194    pub checksum: u16,
195}
196
197impl SampleSettingsFile {
198    /// Create a new `SampleSettingsFile`
199    ///
200    /// Values like `tempo` and `gain` need to be normalized to relevant OT
201    /// bounds before using this method.
202    /// ```rust
203    /// # use std::array::from_fn;
204    /// # use ot_tools_io::samples::{SampleSettingsFile, TrimConfig, LoopConfig};
205    /// # use ot_tools_io::settings::{
206    /// #    TrigQuantizationMode,
207    /// #    LoopMode,
208    /// #    TimeStretchMode,
209    /// # };
210    /// # use ot_tools_io::slices::{Slices, Slice};
211    /// # let slice_arr: [Slice; 64] = from_fn(|_| Slice::default());
212    /// # let slices: Slices = Slices { slices: slice_arr, count: 0 };
213    /// let x = SampleSettingsFile::new(
214    ///     &2880,
215    ///     // ...
216    ///     # &TimeStretchMode::default(),
217    ///     # &TrigQuantizationMode::default(),
218    ///     &48,
219    ///     // ...
220    ///     # &TrimConfig { start: 0, end: 100, length: 25 },
221    ///     # &LoopConfig { start: 0, length: 100, mode: LoopMode::Off },
222    ///     # &slices,
223    /// ).unwrap();
224    /// assert_eq!(x.tempo, 2880);
225    /// assert_eq!(x.gain, 48);
226    /// ```
227    pub fn new(
228        tempo: &u32,
229        stretch: &TimeStretchMode,
230        quantization: &TrigQuantizationMode,
231        gain: &u16,
232        trim_config: &TrimConfig,
233        loop_config: &LoopConfig,
234        slices: &Slices,
235    ) -> Result<Self, SampleSettingsError> {
236        #[allow(clippy::manual_range_contains)]
237        if tempo < &720 || tempo > &7200 {
238            return Err(SampleSettingsError::TempoOutOfBounds { value: *tempo });
239        }
240
241        #[allow(clippy::manual_range_contains)]
242        if gain < &0 || gain > &96 {
243            return Err(SampleSettingsError::GainOutOfBounds {
244                value: *gain as u32,
245            });
246        }
247
248        Ok(Self {
249            header: SAMPLES_HEADER,
250            datatype_version: SAMPLES_FILE_VERSION,
251            unknown: 0,
252            gain: *gain,
253            stretch: (*stretch).into(),
254            tempo: *tempo,
255            quantization: (*quantization).into(),
256            trim_start: trim_config.start,
257            trim_end: trim_config.end,
258            trim_len: trim_config.length, // bin lengths are multiplied by 100?
259            loop_start: loop_config.start,
260            loop_len: loop_config.length, // bin lengths are multiplied by 100?
261            loop_mode: loop_config.mode.into(),
262            slices: slices.slices,
263            slices_len: slices.count,
264            checksum: 0,
265        })
266    }
267}
268
269#[cfg(test)]
270mod new {
271    use super::test_utils::create_mock_new;
272    use crate::OtToolsIoError;
273    #[test]
274    fn invalid_oob_temp() -> Result<(), OtToolsIoError> {
275        assert_eq!(
276            create_mock_new(Some(7201), None).unwrap_err().to_string(),
277            "invalid tempo value, must be u32 in range 720 <= x <= 7200: 7201".to_string(),
278        );
279        Ok(())
280    }
281
282    #[test]
283    fn invalid_oob_gain() -> Result<(), OtToolsIoError> {
284        assert_eq!(
285            create_mock_new(None, Some(97)).unwrap_err().to_string(),
286            "invalid gain, must be u16 in range 0 <= x <= 96: 97".to_string(),
287        );
288        Ok(())
289    }
290}
291
292impl SwapBytes for SampleSettingsFile {
293    fn swap_bytes(self) -> Self {
294        let mut bswapped_slices: [Slice; 64] = self.slices;
295
296        for (i, slice) in self.slices.iter().enumerate() {
297            bswapped_slices[i] = slice.swap_bytes();
298        }
299
300        Self {
301            header: SAMPLES_HEADER,
302            datatype_version: SAMPLES_FILE_VERSION,
303            unknown: self.unknown,
304            tempo: self.tempo.swap_bytes(),
305            trim_len: self.trim_len.swap_bytes(),
306            loop_len: self.loop_len.swap_bytes(),
307            stretch: self.stretch.swap_bytes(),
308            loop_mode: self.loop_mode.swap_bytes(),
309            gain: self.gain.swap_bytes(),
310            quantization: self.quantization.swap_bytes(),
311            trim_start: self.trim_start.swap_bytes(),
312            trim_end: self.trim_end.swap_bytes(),
313            loop_start: self.loop_start.swap_bytes(),
314            slices: bswapped_slices,
315            slices_len: self.slices_len.swap_bytes(),
316            checksum: self.checksum.swap_bytes(),
317        }
318    }
319}
320
321impl OctatrackFileIO for SampleSettingsFile {
322    /// Encodes struct data to binary representation, after some pre-processing.
323    ///
324    /// Before serializing, will:
325    /// 1. modify tempo and gain values to machine ranges
326    /// 2. swaps bytes of values (when current system is little-endian)
327    /// 3. generate checksum value
328    fn encode(&self) -> Result<Vec<u8>, OtToolsIoError> {
329        let mut chkd = self.clone();
330        chkd.checksum = self.calculate_checksum()?;
331
332        let encoded = if cfg!(target_endian = "little") {
333            bincode::serialize(&chkd.swap_bytes())?
334        } else {
335            bincode::serialize(&chkd)?
336        };
337        Ok(encoded)
338    }
339
340    /// Decode raw bytes of a `.ot` data file into a new struct,
341    /// swap byte values if system is little-endian then do some minor
342    /// post-processing to get user-friendly settings values.
343    fn decode(bytes: &[u8]) -> Result<Self, OtToolsIoError> {
344        let decoded: Self = bincode::deserialize(bytes)?;
345        let mut bswapd = decoded.clone();
346
347        // swapping bytes is one required when running on little-endian systems
348        if cfg!(target_endian = "little") {
349            bswapd = decoded.swap_bytes();
350        }
351
352        Ok(bswapd)
353    }
354}
355
356#[cfg(test)]
357mod decode {
358    use crate::read_bin_file;
359    use crate::samples::test_utils::mock_0_slice;
360    use crate::test_utils::get_samples_dirpath;
361    use crate::{OctatrackFileIO, OtToolsIoError, SampleSettingsFile};
362    #[test]
363    fn valid() -> Result<(), OtToolsIoError> {
364        let path = get_samples_dirpath().join("checksum").join("0slices.ot");
365        let bytes = read_bin_file(&path)?;
366        let s = SampleSettingsFile::decode(&bytes)?;
367        assert_eq!(s, mock_0_slice());
368        Ok(())
369    }
370}
371
372#[cfg(test)]
373mod encode {
374    use crate::read_bin_file;
375    use crate::samples::test_utils::mock_0_slice;
376    use crate::test_utils::get_samples_dirpath;
377    use crate::{OctatrackFileIO, OtToolsIoError};
378    #[test]
379    fn valid() -> Result<(), OtToolsIoError> {
380        let path = get_samples_dirpath().join("checksum").join("0slices.ot");
381        let bytes = read_bin_file(&path)?;
382        let b = mock_0_slice().encode()?;
383        assert_eq!(b, bytes);
384        Ok(())
385    }
386}
387
388impl HasChecksumField for SampleSettingsFile {
389    // tests for this are in the main tests directory -- `samples_settings_files.rs`
390    fn calculate_checksum(&self) -> Result<u16, OtToolsIoError> {
391        let bytes = bincode::serialize(&self)?;
392
393        // skip header and checksum byte values
394        let checksum_bytes = &bytes[16..bytes.len() - 2];
395
396        let chk: u32 = checksum_bytes
397            .iter()
398            .map(|x| *x as u32)
399            .sum::<u32>()
400            .rem_euclid(u16::MAX as u32 + 1);
401
402        Ok(chk as u16)
403    }
404
405    fn check_checksum(&self) -> Result<bool, OtToolsIoError> {
406        Ok(self.checksum == self.calculate_checksum()?)
407    }
408}
409
410#[cfg(test)]
411mod checksum_field {
412    use super::test_utils::create_mock_new;
413    use crate::{HasChecksumField, OtToolsIoError};
414    #[test]
415    fn valid() -> Result<(), OtToolsIoError> {
416        let mut x = create_mock_new(None, None)?;
417        x.checksum = x.calculate_checksum()?;
418        assert!(x.check_checksum()?);
419        Ok(())
420    }
421
422    #[test]
423    fn invalid() -> Result<(), OtToolsIoError> {
424        let mut x = create_mock_new(None, None)?;
425        x.checksum = x.calculate_checksum()?;
426        x.checksum = 0;
427        assert!(!x.check_checksum()?);
428        Ok(())
429    }
430
431    mod files {
432        use crate::test_utils::get_samples_dirpath;
433        use crate::{HasChecksumField, OctatrackFileIO, OtToolsIoError, SampleSettingsFile};
434
435        fn helper(test_name: String) -> Result<(u16, u16), OtToolsIoError> {
436            let mut src_path = get_samples_dirpath().join("checksum").join(&test_name);
437            src_path.set_extension("ot");
438
439            let valid = SampleSettingsFile::from_data_file(&src_path)?;
440            let mut x = valid.clone();
441            x.checksum = 0;
442
443            Ok((x.calculate_checksum()?, valid.checksum))
444        }
445
446        #[test]
447        fn zero_slices_trig_quant_one_step() -> Result<(), OtToolsIoError> {
448            let (test, valid) = helper("0slices-tq1step".to_string())?;
449            assert_eq!(test, valid);
450            Ok(())
451        }
452
453        #[test]
454        fn zero_slices() -> Result<(), OtToolsIoError> {
455            let (test, valid) = helper("0slices".to_string())?;
456            assert_eq!(test, valid);
457            Ok(())
458        }
459
460        #[test]
461        fn one_slice() -> Result<(), OtToolsIoError> {
462            let (test, valid) = helper("1slices".to_string())?;
463            assert_eq!(test, valid);
464            Ok(())
465        }
466
467        #[test]
468        fn two_slices() -> Result<(), OtToolsIoError> {
469            let (test, valid) = helper("2slices".to_string())?;
470            assert_eq!(test, valid);
471            Ok(())
472        }
473
474        #[test]
475        fn four_slices() -> Result<(), OtToolsIoError> {
476            let (test, valid) = helper("4slices".to_string())?;
477            assert_eq!(test, valid);
478            Ok(())
479        }
480
481        #[test]
482        fn eight_slices() -> Result<(), OtToolsIoError> {
483            let (test, valid) = helper("8slices".to_string())?;
484            assert_eq!(test, valid);
485            Ok(())
486        }
487
488        #[test]
489        fn sixteen_slices() -> Result<(), OtToolsIoError> {
490            let (test, valid) = helper("16slices".to_string())?;
491            assert_eq!(test, valid);
492            Ok(())
493        }
494
495        #[test]
496        fn thirty_two_slices() -> Result<(), OtToolsIoError> {
497            let (test, valid) = helper("32slices".to_string())?;
498            assert_eq!(test, valid);
499            Ok(())
500        }
501
502        #[test]
503        fn forty_eight_slices() -> Result<(), OtToolsIoError> {
504            let (test, valid) = helper("48slices".to_string())?;
505            assert_eq!(test, valid);
506            Ok(())
507        }
508
509        #[test]
510        fn sixty_two_slices() -> Result<(), OtToolsIoError> {
511            let (test, valid) = helper("62slices".to_string())?;
512            assert_eq!(test, valid);
513            Ok(())
514        }
515
516        #[test]
517        fn sixty_three_slices() -> Result<(), OtToolsIoError> {
518            let (test, valid) = helper("63slices".to_string())?;
519            assert_eq!(test, valid);
520            Ok(())
521        }
522
523        #[test]
524        fn sixty_four_slices_correct() -> Result<(), OtToolsIoError> {
525            let (test, valid) = helper("64slices".to_string())?;
526            assert_eq!(test, valid);
527            Ok(())
528        }
529    }
530}
531
532impl HasHeaderField for SampleSettingsFile {
533    fn check_header(&self) -> Result<bool, OtToolsIoError> {
534        Ok(self.header == SAMPLES_HEADER)
535    }
536}
537
538#[cfg(test)]
539mod header_field {
540    use super::test_utils::create_mock_new;
541    use crate::{HasHeaderField, OtToolsIoError};
542    #[test]
543    fn valid() -> Result<(), OtToolsIoError> {
544        assert!(create_mock_new(None, None)?.check_header()?);
545        Ok(())
546    }
547
548    #[test]
549    fn invalid() -> Result<(), OtToolsIoError> {
550        let mut mutated = create_mock_new(None, None)?;
551        mutated.header[0] = 0x00;
552        mutated.header[20] = 111;
553        assert!(!mutated.check_header()?);
554        Ok(())
555    }
556}
557
558impl HasFileVersionField for SampleSettingsFile {
559    fn check_file_version(&self) -> Result<bool, OtToolsIoError> {
560        Ok(self.datatype_version == SAMPLES_FILE_VERSION)
561    }
562}
563
564#[cfg(test)]
565mod file_version_field {
566    use super::test_utils::create_mock_new;
567    use crate::{HasFileVersionField, OtToolsIoError};
568    #[test]
569    fn valid() -> Result<(), OtToolsIoError> {
570        assert!(create_mock_new(None, None)?.check_file_version()?);
571        Ok(())
572    }
573
574    #[test]
575    fn invalid() -> Result<(), OtToolsIoError> {
576        let mut mutated = create_mock_new(None, None)?;
577        mutated.datatype_version = 0;
578        assert!(!mutated.check_file_version()?);
579        Ok(())
580    }
581}
582
583/// Helper function to avoid infinite recursion for the `From<(&SlotAttributes, &SlotMarkers)>` trait
584/// implementation for `SampleSettingsFile`
585fn from_slot_tuple(attrs: SlotAttributes, markers: SlotMarkers) -> SampleSettingsFile {
586    SampleSettingsFile {
587        header: SAMPLES_HEADER,
588        datatype_version: SAMPLES_FILE_VERSION,
589        unknown: 0,
590        tempo: attrs.bpm as u32,
591        // TODO: bar len -- markers.trim_end - markers.trim_offset
592        trim_len: 0,
593        // TODO: bar len -- markers.trim_end - markers.loop_point
594        loop_len: 0,
595        stretch: attrs.timestrech_mode.into(),
596        loop_mode: attrs.loop_mode.into(),
597        gain: attrs.gain as u16,
598        quantization: attrs.trig_quantization_mode.into(),
599        trim_start: markers.trim_offset,
600        trim_end: markers.trim_end,
601        loop_start: markers.loop_point,
602        slices: markers.slices,
603        slices_len: markers.slice_count,
604        checksum: 0,
605    }
606}
607
608impl From<(SlotAttributes, SlotMarkers)> for SampleSettingsFile {
609    fn from(value: (SlotAttributes, SlotMarkers)) -> Self {
610        let (attrs, markers) = value;
611        from_slot_tuple(attrs, markers)
612    }
613}
614
615impl From<(&SlotAttributes, &SlotMarkers)> for SampleSettingsFile {
616    fn from(value: (&SlotAttributes, &SlotMarkers)) -> Self {
617        let (attrs, markers) = value;
618        from_slot_tuple(attrs.clone(), markers.clone())
619    }
620}
621
622/// A helper struct for the OT Sample's Trim settings
623
624#[derive(PartialEq, Debug, Clone, Copy)]
625pub struct TrimConfig {
626    /// Start of full audio sample (n samples)
627    pub start: u32,
628    /// End of full audio sample (n samples)
629    pub end: u32,
630    /// Length of audio sample to play before stopping/looping playback
631    /// NOTE: This is measured in number of bars.
632    pub length: u32,
633}
634
635impl From<SampleSettingsFile> for TrimConfig {
636    fn from(decoded: SampleSettingsFile) -> Self {
637        Self {
638            start: decoded.trim_start,
639            end: decoded.trim_end,
640            length: decoded.trim_len,
641        }
642    }
643}
644
645impl From<&SampleSettingsFile> for TrimConfig {
646    fn from(decoded: &SampleSettingsFile) -> Self {
647        Self::from(decoded.clone())
648    }
649}
650
651impl From<SlotMarkers> for TrimConfig {
652    fn from(value: SlotMarkers) -> Self {
653        Self {
654            start: value.trim_offset,
655            end: value.trim_end,
656            length: value.trim_end - value.trim_offset,
657        }
658    }
659}
660
661impl From<&SlotMarkers> for TrimConfig {
662    fn from(value: &SlotMarkers) -> Self {
663        Self::from(value.clone())
664    }
665}
666
667#[cfg(test)]
668mod trim_config_from {
669    use super::{test_utils::create_mock_new, TrimConfig};
670    use crate::OtToolsIoError;
671
672    #[test]
673    fn owned() -> Result<(), OtToolsIoError> {
674        let s = create_mock_new(None, None)?;
675        assert_eq!(
676            TrimConfig::from(s),
677            TrimConfig {
678                start: 0,
679                end: 0,
680                length: 0,
681            }
682        );
683        Ok(())
684    }
685    #[test]
686    fn borrowed() -> Result<(), OtToolsIoError> {
687        let s = create_mock_new(None, None)?;
688        assert_eq!(
689            TrimConfig::from(&s),
690            TrimConfig {
691                start: 0,
692                end: 0,
693                length: 0,
694            }
695        );
696        Ok(())
697    }
698}
699
700/// A helper struct for the OT Sample's Loop settings
701
702#[derive(PartialEq, Debug, Clone, Copy)]
703pub struct LoopConfig {
704    /// Loop start position for the audio sample (n samples).
705    pub start: u32,
706
707    /// Length of the loop for the audio sample (n samples).
708    pub length: u32,
709
710    /// Type of looping mode.
711    pub mode: LoopMode,
712}
713
714impl LoopConfig {
715    pub fn new(start: u32, length: u32, mode: LoopMode) -> Self {
716        Self {
717            start,
718            length,
719            mode,
720        }
721    }
722}
723
724#[cfg(test)]
725mod loop_config_new {
726
727    use crate::samples::{LoopConfig, LoopMode};
728
729    #[test]
730    fn test_new_sample_loop_config_loop_off() {
731        assert_eq!(
732            LoopConfig::new(0, 10, LoopMode::Off),
733            LoopConfig {
734                start: 0,
735                length: 10,
736                mode: LoopMode::Off
737            }
738        );
739    }
740
741    #[test]
742    fn test_new_sample_loop_config_umin_start_umax_length() {
743        assert_eq!(
744            LoopConfig::new(u32::MIN, u32::MAX, LoopMode::Off),
745            LoopConfig {
746                start: u32::MIN,
747                length: u32::MAX,
748                mode: LoopMode::Off
749            }
750        );
751    }
752
753    #[test]
754    fn test_new_sample_loop_config_loop_normal() {
755        assert_eq!(
756            LoopConfig::new(0, 10, LoopMode::Normal),
757            LoopConfig {
758                start: 0,
759                length: 10,
760                mode: LoopMode::Normal
761            }
762        );
763    }
764
765    #[test]
766    fn test_new_sample_loop_config_loop_pingpong() {
767        assert_eq!(
768            LoopConfig::new(0, 10, LoopMode::PingPong),
769            LoopConfig {
770                start: 0,
771                length: 10,
772                mode: LoopMode::PingPong
773            }
774        );
775    }
776}
777
778impl From<SampleSettingsFile> for LoopConfig {
779    fn from(decoded: SampleSettingsFile) -> Self {
780        Self::new(
781            decoded.loop_start,
782            decoded.loop_len,
783            LoopMode::try_from(&(decoded.loop_mode as u8)).unwrap_or(LoopMode::default()),
784        )
785    }
786}
787
788impl From<&SampleSettingsFile> for LoopConfig {
789    fn from(decoded: &SampleSettingsFile) -> Self {
790        Self::from(decoded.clone())
791    }
792}
793
794#[cfg(test)]
795mod loop_config_from {
796    use super::{
797        test_utils::create_mock_new, LoopConfig, LoopMode, OtToolsIoError, SampleSettingsFile,
798        Slices, DEFAULT_GAIN, DEFAULT_TEMPO, SAMPLES_FILE_VERSION, SAMPLES_HEADER,
799    };
800
801    #[test]
802    fn owned() -> Result<(), OtToolsIoError> {
803        let s = create_mock_new(None, None)?;
804        assert_eq!(
805            LoopConfig::from(s),
806            LoopConfig {
807                start: 0,
808                length: 0,
809                mode: LoopMode::default(),
810            }
811        );
812        Ok(())
813    }
814    #[test]
815    fn borrowed() -> Result<(), OtToolsIoError> {
816        let s = create_mock_new(None, None)?;
817        assert_eq!(
818            LoopConfig::from(&s),
819            LoopConfig {
820                start: 0,
821                length: 0,
822                mode: LoopMode::default(),
823            }
824        );
825        Ok(())
826    }
827
828    fn mock_for_loop_conf(
829        loop_start: u32,
830        loop_len: u32,
831        loop_mode: LoopMode,
832    ) -> SampleSettingsFile {
833        SampleSettingsFile {
834            loop_len,
835            loop_start,
836            loop_mode: loop_mode.into(),
837            header: SAMPLES_HEADER,
838            datatype_version: SAMPLES_FILE_VERSION,
839            tempo: DEFAULT_TEMPO,
840            unknown: 0,
841            trim_len: 0,
842            stretch: 0,
843            gain: DEFAULT_GAIN,
844            quantization: 0,
845            trim_start: 0,
846            trim_end: 0,
847            slices: Slices::default().slices,
848            slices_len: 0,
849            checksum: 0,
850        }
851    }
852
853    #[test]
854    fn test_umin_start_umax_len() {
855        assert_eq!(
856            LoopConfig::from(mock_for_loop_conf(u32::MIN, u32::MAX, LoopMode::Off)),
857            LoopConfig {
858                start: u32::MIN,
859                length: u32::MAX,
860                mode: LoopMode::Off
861            }
862        );
863    }
864
865    #[test]
866    fn test_loop_off() {
867        assert_eq!(
868            LoopConfig::from(mock_for_loop_conf(0, 10, LoopMode::Off)),
869            LoopConfig {
870                start: 0,
871                length: 10,
872                mode: LoopMode::Off
873            }
874        );
875    }
876
877    #[test]
878    fn test_loop_normal() {
879        assert_eq!(
880            LoopConfig::from(mock_for_loop_conf(0, 10, LoopMode::Normal)),
881            LoopConfig {
882                start: 0,
883                length: 10,
884                mode: LoopMode::Normal
885            }
886        );
887    }
888
889    #[test]
890    fn test_loop_pingpong() {
891        assert_eq!(
892            LoopConfig::from(mock_for_loop_conf(0, 10, LoopMode::PingPong)),
893            LoopConfig {
894                start: 0,
895                length: 10,
896                mode: LoopMode::PingPong
897            }
898        );
899    }
900}