Skip to main content

ot_tools_io/
samples.rs

1/*
2SPDX-License-Identifier: GPL-3.0-or-later
3Copyright © 2026 Mike Robeson [dijksterhuis]
4*/
5
6//! All types and trait implementations related to [`SampleSettingsFile`]s.
7
8use crate::generics::Slices;
9use crate::markers::SlotMarkers;
10use crate::projects::{SlotAttributes, DEFAULT_GAIN, DEFAULT_TEMPO};
11use crate::settings::{LoopMode, SlotType, TimeStretchMode, TrigQuantizationMode};
12use crate::slices::Slice;
13use crate::traits::SwapBytes;
14use crate::{
15    HasChecksumField, HasFileVersionField, HasHeaderField, OctatrackFileIO, OtToolsIoError,
16};
17use ot_tools_io_derive::{AsMutDerive, AsRefDerive, IntegrityChecks};
18use serde::{Deserialize, Serialize};
19use std::path::PathBuf;
20use thiserror::Error;
21
22#[cfg(test)]
23mod test_utils {
24    use super::{
25        OtToolsIoError, SampleSettingsFile, SlotMarkers, SAMPLES_FILE_VERSION, SAMPLES_HEADER,
26    };
27    use crate::projects::DEFAULT_GAIN;
28    pub(crate) fn create_mock_new_sample_settings(
29        tempo: Option<u32>,
30        gain: Option<u16>,
31    ) -> Result<SampleSettingsFile, OtToolsIoError> {
32        SampleSettingsFile::new(
33            SlotMarkers::default(),
34            gain,
35            tempo,
36            None,
37            None,
38            None,
39            None,
40            None,
41        )
42    }
43
44    pub(crate) fn mock_0_slice_test_file() -> SampleSettingsFile {
45        SampleSettingsFile {
46            header: SAMPLES_HEADER,
47            datatype_version: SAMPLES_FILE_VERSION,
48            unknown: 1,
49            tempo: 2281,
50            trim_bar_len: 800,
51            loop_bar_len: 800,
52            stretch: 2,
53            loop_mode: 0,
54            gain: DEFAULT_GAIN as u16,
55            quantization: 255,
56            trim_start: 0,
57            trim_end: 890839,
58            loop_start: 0,
59            slices: SlotMarkers::default().slices,
60            slices_len: 0,
61            checksum: 998,
62        }
63    }
64}
65
66/// Errors specific to [`SampleSettingsFile`]s.
67///
68/// Most of the time this will be casted into an [`OtToolsIoError`] instance in a return type.
69#[derive(Debug, Error)]
70pub enum SampleSettingsError {
71    #[error("invalid gain, must be u16 in range 0 <= x <= 96: {value}")]
72    GainOutOfBounds { value: u32 },
73    #[error("invalid tempo value, must be u32 in range 720 <= x <= 7200: {value}")]
74    TempoOutOfBounds { value: u32 },
75    #[error("invalid slot_id value, must be 1 to 128 for Static slot type or 136 for Flex slot type: id: {id} type: {slot_type}")]
76    SlotIdOutOfBounds { id: u8, slot_type: SlotType },
77    #[error("invalid checksum value for sample settings file: {checksum}")]
78    InvalidChecksum { checksum: u16 },
79    #[error("invalid file version value for sample settings file: {version}")]
80    InvalidFileVersion { version: u8 },
81}
82
83// in `hexdump -C` format:
84// ```
85// FORM....DPS1SMPA
86// ......
87// ```
88/// Standard header bytes in a sample settings file
89pub const SAMPLES_HEADER: [u8; 21] = [
90    0x46, 0x4F, 0x52, 0x4D, 0x00, 0x00, 0x00, 0x00, 0x44, 0x50, 0x53, 0x31, 0x53, 0x4D, 0x50, 0x41,
91    0x00, 0x00, 0x00, 0x00, 0x00,
92];
93
94/// Current/supported datatype version for a sample settings file
95pub const SAMPLES_FILE_VERSION: u8 = 2;
96
97/// An Octatrack `.ot` file a.k.a. a sample settings file contains trim, slice and attribute
98/// settings for a sample/slot.
99/// Essentially the type is a combination of [`SlotAttributes`] and [`SlotMarkers`] data, excluding
100/// the `slot_id`, `path` and `slot_type` fields from [`SlotAttributes`].
101///
102/// # The Type's Name
103///
104/// The Octatrack manual specifically refers to
105/// > SAVE SAMPLE SETTINGS will save the trim, slice and attribute settings in a
106/// > separate file and link it to the sample currently being edited.
107/// >
108/// > -- page 87.
109///
110/// So this is the settings file for a given sample.
111///
112/// # Missing [`Default`] trait
113///
114/// In practice, a [`SampleSettingsFile`] is usually paired with an audio file.
115/// This library assumes we’re exclusively operating on Octatrack binary data files, that we have no
116/// access to any audio sample files.
117/// As such, there is no way to know what a "default" implementation of this type would like.
118/// So there isn't one.
119///
120/// Instead of calling `default`, always create a new instance with [`SampleSettingsFile::new`].
121///
122/// # Converting to other data types
123///
124/// [`SampleSettingsFile`]s can be converted to/from [`SlotMarkers`] and/or [`SlotAttributes`] using
125/// the information from the below table
126///
127/// | From | To | Via |
128/// | ---- | ---- | ---- |
129/// | [`SampleSettingsFile`] | [`SlotMarkers`] | [`SlotMarkers::from`] |
130/// | [`SampleSettingsFile`] | [`SlotAttributes`] | [`SampleSettingsFile::to_slot_attr`] \* |
131/// | `(SlotAttributes, SlotMarkers)` | [`SampleSettingsFile`] | [`SampleSettingsFile::from`] |
132///
133/// \* requires additional arguments
134///
135/// # Trim Bar Length and Loop Bar Length
136///
137/// These fields are not necessary to have a functional [`SampleSettingsFile`].
138/// If both of these fields are set to zero, but the BPM/tempo field is populated,
139/// then the Octatrack will load the sample using the BPM/tempo field.
140///
141/// To calculate an appropriate value for these two fields we would have to convert from number of
142/// samples to number of bars.
143/// This calculation requires the sample rate and length of the relevant audio sample file,
144/// meaning this library would need access to such audio sample files.
145///
146/// This library assumes we're exclusively operating on Octatrack binary data files,
147/// that we have no access to any audio files.
148///
149/// Therefore, these two fields are always set to zero when using any of the following methods
150/// - [`SampleSettingsFile::new`]
151/// - [`SampleSettingsFile::from`] \*
152///
153/// If you want to set these two values you have to do one of:
154/// - Calculate the appropriate `tempo` value for your desired `trim_bar_len` or `loop_bar_len` and use that
155///   when calling [`SampleSettingsFile::new`] or when creating a new struct from scratch
156///   (leaving the `trim_bar_len` and `loop_bar_len` fields set to zero if creating a struct from scratch).
157/// - Manually create the [`SampleSettingsFile`] from scratch and provide appropriate values for all
158///   fields -- note that this means manually adding header etc. field values too.
159///
160/// Please note that the `trim_bar_len` and `loop_bar_len` fields are **NOT** set to zero during
161/// deserialization.
162/// A [`SampleSettingsFile`]s should be correctly represented when the file has been generated by
163/// the Octatrack itself.
164///
165/// \* [`SampleSettingsFile::from`] will be able to convert it in the future,
166/// but appropriate fields need to be added to the [`SlotAttributes`] type first.
167///
168/// # Little-endian systems
169///
170/// The bytes of this type are swapped during decoding on little-endian systems to ensure data is
171/// written out correctly.
172#[derive(
173    Copy,
174    Clone,
175    Debug,
176    Eq,
177    Hash,
178    Ord,
179    PartialEq,
180    PartialOrd,
181    Serialize,
182    Deserialize,
183    AsMutDerive,
184    AsRefDerive,
185    IntegrityChecks,
186)]
187pub struct SampleSettingsFile {
188    /// Header
189    ///
190    /// ## Validation
191    ///
192    /// Use the [`SampleSettingsFile::check_header`] method from the [`HasHeaderField`] trait.
193    ///
194    /// The [`SampleSettingsFile::validate`] method will call [`SampleSettingsFile::check_header`]
195    /// to verify the current instance's header.
196    pub header: [u8; 21],
197
198    /// Datatype's version ID
199    ///
200    /// ## Validation
201    ///
202    /// Use the [`SampleSettingsFile::check_file_version`] method from the [`HasFileVersionField`] trait.
203    ///
204    /// The [`SampleSettingsFile::validate`] method will call [`SampleSettingsFile::check_file_version`]
205    /// to verify the current instance's version.
206    pub datatype_version: u8,
207
208    /// Unknown data -- can sometimes be 1, usually 0.
209    pub unknown: u8,
210
211    /// Tempo determines the BPM of sample playback.
212    /// In this field, the value is always the machine UI's BPM (human BPM) multiplied by 24.
213    ///
214    /// From page 86 of the Octatrack manual:
215    /// > ORIGINAL TEMPO displays the calculated BPM of the sample.
216    /// > If it is not correct, it can be changed using the LEVEL knob.
217    /// > This setting will affect the sound of a timestretched sample.
218    /// > For correct results it should be set to match the original BPM of the sample.
219    /// > Altering this setting will alter the TRIM LEN (BARS) and LOOP LEN (BARS) settings.
220    ///
221    /// ## Validation
222    ///
223    /// The [`SampleSettingsFile::validate`] method checks whether the field's current value is
224    /// within appropriate bounds.
225    ///
226    /// Validating this field against the `trim_bar_len` and/or `loop_bar_len` fields is outside the scope
227    /// of this library.
228    pub tempo: u32,
229
230    /// Number of bars for determining the BPM of sample playback.
231    /// Should be an appropriate value which corresponds to the `tempo` field.
232    ///
233    /// From page 86 of the Octatrack manual:
234    /// > TRIM LEN (BARS) shows the length of the sample in bars.
235    /// > Altering this setting will alter the ORIGINAL TEMPO and LOOP LEN (BARS) settings.
236    ///
237    /// ## Validation
238    ///
239    /// The [`SampleSettingsFile::validate`] method does not check this field for validity.
240    ///
241    /// Validating this field against the `trim_bar_len` and/or `loop_bar_len` fields is outside the scope
242    /// of this library.
243    pub trim_bar_len: u32,
244
245    /// Number of bars for determining the BPM of sample playback.
246    /// Should be an appropriate value which corresponds to the `tempo` field.
247    ///
248    /// From page 86 of the Octatrack manual:
249    /// > LOOP LEN (BARS) displays the amount of bars the looped section of the sample consists of.
250    /// > Altering this setting will alter the ORIGINAL TEMPO and TRIM LEN (BARS) settings.
251    ///
252    /// ## Validation
253    ///
254    /// The [`SampleSettingsFile::validate`] method does not check this field for validity.
255    ///
256    /// Validating this field against the `trim_bar_len` and/or `loop_bar_len` fields is outside the scope
257    /// of this library.
258    pub loop_bar_len: u32,
259
260    /// Time-stretching algorithm applied to the sample.
261    /// See [`TimeStretchMode`] for suitable choices.
262    ///
263    /// ## No enum usage?
264    ///
265    /// [`TimeStretchMode`] has a base representation of `u8` but a [`SampleSettingsFile`] expects a
266    /// `u32` value for this field.
267    /// As such, we have to convert to `u32` values for this field via [`TimeStretchMode::try_from`]
268    /// instead of the relevant variant.
269    /// While it may be possible to change this in a future version via a custom implementation of
270    /// [`Serialize`] and [`Deserialize`], it is currently out of scope.
271    ///
272    /// Use the [`SampleSettingsFile::new`] method to create a new instance with the appropriate
273    /// enum variant.
274    ///
275    /// ## Validation
276    ///
277    /// The [`SampleSettingsFile::validate`] method checks whether this field has an appropriate value.
278    pub stretch: u32,
279
280    /// Loop mode for the sample.
281    /// See [`LoopMode`] for suitable choices.
282    ///
283    /// ## No enum usage?
284    ///
285    /// [`LoopMode`] has a base representation of `u8` but a [`SampleSettingsFile`] expects a
286    /// `u32` value for this field.
287    /// As such, we have to convert to `u32` values for this field via [`LoopMode::try_from`]
288    /// instead of the relevant variant.
289    /// While it may be possible to change this in a future version via a custom implementation of
290    /// [`Serialize`] and [`Deserialize`], it is currently out of scope.
291    ///
292    /// Use the [`SampleSettingsFile::new`] method to create a new instance with the appropriate
293    /// enum variant.
294    ///
295    /// ## Validation
296    ///
297    /// The [`SampleSettingsFile::validate`] method checks whether this field has an appropriate value.
298    pub loop_mode: u32,
299
300    /// Gain of the sample.
301    /// -24.0 db <= x <= +24 db range in the machine's UI, with increments of 0.5 db changes.
302    /// 0 <= x <= 96 range in binary data file.
303    ///
304    /// ## Validation
305    ///
306    /// The [`SampleSettingsFile::validate`] method checks whether this field has an appropriate
307    /// value within the Octatrack's bounds for gain.
308    pub gain: u16,
309
310    /// Trig Quantization mode applied to the sample.
311    /// See [`TrigQuantizationMode`] for suitable choices.
312    ///
313    /// ## No enum usage?
314    ///
315    /// [`TrigQuantizationMode`] has a base representation of `u8` but a [`SampleSettingsFile`]
316    /// expects a `u32` value for this field.
317    /// As such, we have to convert to `u32` values for this field via
318    /// [`TrigQuantizationMode::try_from`] instead of the relevant variant.
319    /// While it may be possible to change this in a future version via a custom implementation of
320    /// [`Serialize`] and [`Deserialize`], it is currently out of scope.
321    ///
322    /// Use the [`SampleSettingsFile::new`] method to create a new instance with the appropriate
323    /// enum variant.
324    ///
325    /// ## Validation
326    ///
327    /// The [`SampleSettingsFile::validate`] method checks whether this field has an appropriate value.
328    pub quantization: u8,
329
330    /// Where the trim start marker is placed for the sample, measured in bars.
331    /// Default is 0 (start of sample).
332    ///
333    /// ## Validation
334    ///
335    /// The [`SampleSettingsFile::validate`] method checks whether this field is less than `trim_end`
336    pub trim_start: u32,
337
338    /// Where the trim end marker is placed for the sample.
339    /// When the sample is being played in normal mode (i.e. not using slices),
340    /// the Octatrack will not play samples past this point.
341    ///
342    /// ## Validation
343    ///
344    /// The [`SampleSettingsFile::validate`] method checks whether this field is greater than `trim_start`
345    pub trim_end: u32,
346
347    /// Start position for any loops.
348    /// Default is the same as trim start.
349    ///
350    /// A note from the Octatrack manual on loop point/start behaviour:
351    /// > If a loop point is set, the sample will play from the start point to the
352    /// > end point, then loop from the loop point to the end point
353    ///
354    /// ## Validation
355    ///
356    /// The [`SampleSettingsFile::validate`] method checks whether this field is within the bounds
357    /// of `trim_start` and `trim_end` values.
358    pub loop_start: u32,
359
360    /// 64 length array containing [`Slice`]s.
361    /// See the [`Slice`] struct for more details.
362    /// Any empty slice positions should have zero-valued struct fields.
363    ///
364    /// ## Validation
365    ///
366    /// The [`SampleSettingsFile::validate`] method calls [`Slice::validate`]
367    pub slices: Slices<Slice>,
368
369    /// Number of usable [`Slice`]s in this sample.
370    /// Used by the Octatrack to ignore zero-valued [`Slice`]s in the `slices` array when loading the sample.
371    pub slices_len: u32,
372
373    /// Checksum value for the file.
374    /// For now, this value must be calculated and added to the struct **after** the struct is created,
375    /// via [`SampleSettingsFile::calculate_checksum`]
376    ///
377    /// # Example
378    /// ```rust
379    /// use ot_tools_io::types::{SampleSettingsFile, SlotMarkers};
380    /// use ot_tools_io::HasChecksumField;
381    ///
382    /// # use ot_tools_io::OtToolsIoError;
383    /// # fn main() -> Result<(), OtToolsIoError> {
384    /// let mut ss_f = SampleSettingsFile::new(
385    ///     SlotMarkers::default(),
386    ///     None,
387    ///     None,
388    ///     None,
389    ///     None,
390    ///     None,
391    ///     None,
392    ///     None,
393    /// )?;
394    ///
395    /// let checksum = ss_f.calculate_checksum()?;
396    /// ss_f.checksum = checksum;
397    ///
398    /// # Ok(()) }
399    /// ```
400    /// ## Validation
401    ///
402    /// Use the [`SampleSettingsFile::check_checksum`] method to check the validity of the current
403    /// checksum value.
404    pub checksum: u16,
405}
406
407impl SampleSettingsFile {
408    /// Create a new `SampleSettingsFile`
409    ///
410    /// Values like `tempo` and `gain` need to be normalized to relevant Octatrack range bounds
411    /// when using this method.
412    ///
413    /// # Trim Bar Length and Loop Bar Length
414    ///
415    /// If the `trim_bar_len` and `loop_bar_len` arguments are not provided then the corresponding
416    /// field values are set to zero.
417    ///
418    /// When providing these values you should ensure that you have correctly calculated both of
419    /// these values **AND** the tempo value.
420    /// All three are used simultaneously by the Octatrack,
421    /// and I haven't spent any time figuring out which one takes priority
422    /// (I think it's BPM/tempo, but I cannot confirm yet).
423    ///
424    /// See the relevant documentation [here][SampleSettingsFile#trim-length-and-loop-length] for
425    /// more information.
426    /// ```rust
427    /// use std::array::from_fn;
428    /// use ot_tools_io::OtToolsIoError;
429    /// use ot_tools_io::types::{Slices, SlotMarkers, SampleSettingsFile};
430    /// use ot_tools_io::settings::{
431    ///    TrigQuantizationMode,
432    ///    LoopMode,
433    ///    TimeStretchMode,
434    /// };
435    /// # fn main() -> Result<(), OtToolsIoError> {
436    ///
437    /// let marks = SlotMarkers {
438    ///     trim_offset: 0,
439    ///     trim_end: 100,
440    ///     loop_point: 0,
441    ///     slices: Slices::default(),
442    ///     slice_count: 0,
443    /// };
444    /// let x = SampleSettingsFile::new(
445    ///     marks,
446    ///     Some(48),    // -24.0 <-> +24.0 (0 <-> 96)
447    ///     Some(2880),  // bpm x 24 (720 <-> 7200)
448    ///     None,        // trim_bar_len will be zero, see documentation explainer
449    ///     None,        // loop_bar_len will be zero, see documentation explainer
450    ///     Some(TimeStretchMode::default()),
451    ///     Some(TrigQuantizationMode::default()),
452    ///     Some(LoopMode::default()),
453    /// )?;
454    /// assert_eq!(x.trim_start, 0);
455    /// assert_eq!(x.trim_end, 100);
456    /// assert_eq!(x.loop_start, 0);
457    /// assert_eq!(x.trim_bar_len, 0);
458    /// assert_eq!(x.loop_bar_len, 0);
459    /// assert_eq!(x.tempo, 2880);
460    /// assert_eq!(x.gain, 48);
461    /// assert_eq!(x.stretch, 2);  // converted to u32 repr
462    /// assert_eq!(x.quantization, 255);  // converted to u32 repr
463    /// assert_eq!(x.loop_mode, 0);  // converted to u32 repr
464    /// # Ok(()) }
465    /// ```
466    /*
467    can't help this much unless i start creating sub-types, and i'm not too keen on that
468    just now considering what happened with TrimConfig and LoopConfig.
469
470    however, with some upcoming changes to slots, it may be worth thinking about two extra sub-types
471
472    for settings:
473    ```
474    struct SlotSettings {
475        stretch: TimeStretchMode,
476        quantization: TrigQuantizationMode,
477        loop_mode: LoopMode,
478    };
479    ```
480
481    for playback stretch control:
482    (trim bar len, loop bar len and bpm affect the time stretch algorithm)
483    ```
484    struct SlotStretchControl {
485        tempo: Option<u16>,
486        trim_bar_len: Option<u32>,
487        loop_bar_len: Option<u32>,
488    };
489    ```
490    */
491    #[allow(clippy::too_many_arguments)]
492    pub fn new<M: AsRef<SlotMarkers>>(
493        markers: M,
494        gain: Option<u16>,
495        tempo: Option<u32>,
496        trim_bar_len: Option<u32>,
497        loop_bar_len: Option<u32>,
498        stretch: Option<TimeStretchMode>,
499        quantization: Option<TrigQuantizationMode>,
500        loop_mode: Option<LoopMode>,
501    ) -> Result<Self, OtToolsIoError> {
502        let marks = markers.as_ref();
503
504        let mut new = Self {
505            // markers values, required
506            trim_start: marks.trim_offset,
507            trim_end: marks.trim_end,
508            loop_start: marks.loop_point,
509            slices: marks.slices,
510            slices_len: marks.slice_count,
511            // constant values
512            header: SAMPLES_HEADER,
513            datatype_version: SAMPLES_FILE_VERSION,
514            unknown: 0,
515            // optional values
516            gain: gain.unwrap_or(DEFAULT_GAIN as u16),
517            tempo: tempo.unwrap_or(DEFAULT_TEMPO as u32),
518            stretch: stretch.unwrap_or_default().into(),
519            quantization: quantization.unwrap_or_default().into(),
520            loop_mode: loop_mode.unwrap_or_default().into(),
521            trim_bar_len: trim_bar_len.unwrap_or(0), // see docs for explanation of why this is zero
522            loop_bar_len: loop_bar_len.unwrap_or(0), // see docs for explanation of why this is zero
523            // not set here
524            checksum: 0, // set below
525        };
526
527        new.checksum = new.calculate_checksum()?;
528
529        new.validate()?;
530
531        Ok(new)
532    }
533
534    /// Runs a series of validation checks on the field values for an instance of the type.
535    ///
536    /// Will return an appropriate error if a problem is discovered with a field's value.
537    ///
538    /// Read through the fields on [`SampleSettingsFile`] to find out which ones are checked and how
539    /// they are checked.
540    ///
541    /// # Invalid Gain Example
542    /// ```
543    /// use ot_tools_io::OtToolsIoError;
544    /// use ot_tools_io::types::{SampleSettingsFile, SlotMarkers};
545    /// use ot_tools_io::errors::SampleSettingsError;
546    /// # fn main() -> Result<(), OtToolsIoError> {
547    ///
548    /// let ss_f = SampleSettingsFile::new(
549    ///     SlotMarkers::default(),
550    ///     Some(100),
551    ///     None,
552    ///     None,
553    ///     None,
554    ///     None,
555    ///     None,
556    ///     None
557    /// );
558    ///
559    /// assert_eq!(
560    ///     ss_f.unwrap_err().to_string(),
561    ///     OtToolsIoError::SampleSettings(
562    ///         SampleSettingsError::GainOutOfBounds {value: 100}
563    ///     ).to_string(),
564    /// );
565    /// # Ok(()) }
566    /// ```
567    ///
568    /// # Invalid Tempo Example
569    /// ```
570    /// use ot_tools_io::OtToolsIoError;
571    /// use ot_tools_io::types::{SampleSettingsFile, SlotMarkers};
572    /// use ot_tools_io::errors::SampleSettingsError;
573    /// # fn main() -> Result<(), OtToolsIoError> {
574    ///
575    /// let ss_f = SampleSettingsFile::new(
576    ///     SlotMarkers::default(),
577    ///     None,
578    ///     Some(120),
579    ///     None,
580    ///     None,
581    ///     None,
582    ///     None,
583    ///     None
584    /// );
585    ///
586    /// assert_eq!(
587    ///     ss_f.unwrap_err().to_string(),
588    ///     OtToolsIoError::SampleSettings(
589    ///         SampleSettingsError::TempoOutOfBounds {value: 120}
590    ///     ).to_string(),
591    /// );
592    ///
593    /// # Ok(()) }
594    /// ```
595    pub fn validate(&self) -> Result<(), OtToolsIoError> {
596        #[allow(clippy::manual_range_contains)]
597        if self.tempo < 720 || self.tempo > 7200 {
598            return Err(SampleSettingsError::TempoOutOfBounds { value: self.tempo }.into());
599        }
600
601        if self.gain > 96 {
602            return Err(SampleSettingsError::GainOutOfBounds {
603                value: self.gain as u32,
604            }
605            .into());
606        }
607
608        SlotMarkers::from(self).validate()?;
609
610        LoopMode::try_from(self.loop_mode)?;
611        TimeStretchMode::try_from(self.stretch)?;
612        TrigQuantizationMode::try_from(self.quantization)?;
613
614        if !self.check_checksum()? {
615            return Err(SampleSettingsError::InvalidChecksum {
616                checksum: self.checksum,
617            }
618            .into());
619        };
620
621        if !self.check_header()? {
622            return Err(OtToolsIoError::FileHeader);
623        };
624
625        if !self.check_file_version()? {
626            return Err(SampleSettingsError::InvalidFileVersion {
627                version: self.datatype_version,
628            }
629            .into());
630        };
631
632        if !self.check_checksum()? {
633            return Err(SampleSettingsError::InvalidChecksum {
634                checksum: self.checksum,
635            }
636            .into());
637        };
638
639        Ok(())
640    }
641
642    /// Create a new [`SlotAttributes`] instance from this [`SampleSettingsFile`].
643    ///
644    /// Will validate the `slot_id` argument depending on the [`SlotType`] variant provided to the
645    /// `slot_type` argument.
646    ///
647    /// # Valid Example
648    /// ```
649    /// # use ot_tools_io::OtToolsIoError;
650    /// use ot_tools_io::types::{SlotMarkers, SampleSettingsFile, SlotAttributes, SlotType};
651    /// # use ot_tools_io::settings::{TimeStretchMode, LoopMode, TrigQuantizationMode};
652    /// use std::path::PathBuf;
653    /// # fn main() -> Result<(), OtToolsIoError> {
654    ///
655    /// let ss_f = SampleSettingsFile::new(
656    ///     SlotMarkers::default(),
657    ///     None,
658    ///     None,
659    ///     None,
660    ///     None,
661    ///     None,
662    ///     None,
663    ///     None,
664    /// )?;
665    ///
666    /// let attrs = ss_f.to_slot_attr(
667    ///     SlotType::Static,
668    ///     100,
669    ///     Some(PathBuf::from("some/path"))
670    /// )?;
671    ///
672    /// assert_eq!(
673    ///     attrs,
674    ///     SlotAttributes {
675    ///         slot_id: 100,
676    ///         slot_type: SlotType::Static,
677    ///         path: Some(PathBuf::from("some/path")),
678    ///         timestrech_mode: TimeStretchMode::default(),
679    ///         loop_mode: LoopMode::default(),
680    ///         trig_quantization_mode: TrigQuantizationMode::default(),
681    ///         gain: 48,
682    ///         bpm: 2880,
683    ///     }
684    /// );
685    /// # Ok(()) }
686    /// ```
687    ///
688    /// # Error Example
689    /// ```
690    /// use ot_tools_io::OtToolsIoError;
691    /// use ot_tools_io::errors::SampleSettingsError;
692    /// use ot_tools_io::types::{SlotMarkers, SampleSettingsFile, SlotAttributes, SlotType};
693    /// use std::path::PathBuf;
694    /// # fn main() -> Result<(), OtToolsIoError> {
695    ///
696    /// let ss_f = SampleSettingsFile::new(
697    ///     SlotMarkers::default(),
698    ///     None,
699    ///     None,
700    ///     None,
701    ///     None,
702    ///     None,
703    ///     None,
704    ///     None,
705    /// )?;
706    ///
707    /// let attrs_err = ss_f.to_slot_attr(
708    ///     SlotType::Static,
709    ///     200,  // bad slot id!
710    ///     Some(PathBuf::from("some/path"))
711    /// ).unwrap_err();
712    ///
713    /// assert_eq!(
714    ///     attrs_err.to_string(),
715    ///     OtToolsIoError::SampleSettings(
716    ///         SampleSettingsError::SlotIdOutOfBounds { id: 200, slot_type: SlotType::Static }
717    ///     ).to_string()
718    /// );
719    /// # Ok(()) }
720    /// ```
721    pub fn to_slot_attr(
722        &self,
723        slot_type: SlotType,
724        slot_id: u8,
725        slot_path: Option<PathBuf>,
726    ) -> Result<SlotAttributes, OtToolsIoError> {
727        // neither static nor flex slots cannot have id = 0
728        if slot_id == 0 {
729            return Err(SampleSettingsError::SlotIdOutOfBounds {
730                id: slot_id,
731                slot_type,
732            }
733            .into());
734        }
735
736        // static slots cannot have id > 128
737        if slot_type == SlotType::Static && slot_id > 128 {
738            return Err(SampleSettingsError::SlotIdOutOfBounds {
739                id: slot_id,
740                slot_type,
741            }
742            .into());
743        }
744
745        // flex slots cannot have id > 136
746        if slot_type == SlotType::Flex && slot_id > 136 {
747            return Err(SampleSettingsError::SlotIdOutOfBounds {
748                id: slot_id,
749                slot_type,
750            }
751            .into());
752        }
753
754        Ok(SlotAttributes {
755            slot_type,
756            slot_id,
757            path: slot_path,
758            timestrech_mode: TimeStretchMode::try_from(self.stretch)?,
759            loop_mode: LoopMode::try_from(self.loop_mode)?,
760            trig_quantization_mode: TrigQuantizationMode::try_from(self.quantization)?,
761            gain: self.gain as u8,
762            bpm: self.tempo as u16,
763        })
764    }
765
766    /// Returns the `stretch` field as a variant of [`TimeStretchMode`].
767    ///
768    /// Essentially calls [`TimeStretchMode::try_from`]
769    pub fn timestretch_mode_into_setting(&self) -> Result<TimeStretchMode, OtToolsIoError> {
770        TimeStretchMode::try_from(self.stretch).map_err(OtToolsIoError::SettingValue)
771    }
772
773    /// Returns the `loop_mode` field as a variant of [`LoopMode`]
774    ///
775    /// Essentially calls [`LoopMode::try_from`]
776    pub fn loop_mode_into_setting(&self) -> Result<LoopMode, OtToolsIoError> {
777        LoopMode::try_from(self.loop_mode).map_err(OtToolsIoError::SettingValue)
778    }
779
780    /// Returns the `quantization` field as a variant of [`TrigQuantizationMode`]
781    ///
782    /// Essentially calls [`TrigQuantizationMode::try_from`]
783    pub fn trig_quantization_into_setting(&self) -> Result<TrigQuantizationMode, OtToolsIoError> {
784        TrigQuantizationMode::try_from(self.loop_mode).map_err(OtToolsIoError::SettingValue)
785    }
786}
787
788#[cfg(test)]
789mod new {
790    use super::test_utils::create_mock_new_sample_settings;
791    use super::SampleSettingsError;
792    use crate::OtToolsIoError;
793
794    #[test]
795    fn invalid_oob_temp_high() -> Result<(), OtToolsIoError> {
796        assert_eq!(
797            create_mock_new_sample_settings(Some(7201), None)
798                .unwrap_err()
799                .to_string(),
800            OtToolsIoError::SampleSettings(SampleSettingsError::TempoOutOfBounds { value: 7201 })
801                .to_string()
802        );
803        Ok(())
804    }
805
806    #[test]
807    fn invalid_oob_temp_low() -> Result<(), OtToolsIoError> {
808        let s_f = create_mock_new_sample_settings(Some(29), None);
809        assert_eq!(
810            s_f.unwrap_err().to_string(),
811            OtToolsIoError::SampleSettings(SampleSettingsError::TempoOutOfBounds { value: 29 })
812                .to_string()
813        );
814        Ok(())
815    }
816
817    #[test]
818    fn invalid_oob_gain() -> Result<(), OtToolsIoError> {
819        assert_eq!(
820            create_mock_new_sample_settings(None, Some(97))
821                .unwrap_err()
822                .to_string(),
823            OtToolsIoError::SampleSettings(SampleSettingsError::GainOutOfBounds { value: 97 })
824                .to_string()
825        );
826        Ok(())
827    }
828}
829
830#[cfg(test)]
831mod validate {
832    use super::test_utils::create_mock_new_sample_settings;
833    use super::SampleSettingsError;
834    use crate::settings::InvalidValueError;
835    use crate::traits::HasChecksumField;
836    use crate::OtToolsIoError;
837
838    #[test]
839    fn ok() -> Result<(), OtToolsIoError> {
840        let s_f = create_mock_new_sample_settings(None, None)?;
841        assert!(s_f.validate().is_ok());
842        Ok(())
843    }
844
845    // gain and tempo are tested above
846
847    #[test]
848    fn err_stretch() -> Result<(), OtToolsIoError> {
849        let mut s_f = create_mock_new_sample_settings(None, None)?;
850        s_f.stretch = 255;
851        s_f.checksum = s_f.calculate_checksum()?;
852        assert_eq!(
853            s_f.validate().unwrap_err().to_string(),
854            OtToolsIoError::SettingValue(InvalidValueError::TimeStretchMode).to_string()
855        );
856        Ok(())
857    }
858
859    #[test]
860    fn err_loop() -> Result<(), OtToolsIoError> {
861        let mut s_f = create_mock_new_sample_settings(None, None)?;
862        s_f.loop_mode = 255;
863        s_f.checksum = s_f.calculate_checksum()?;
864        assert_eq!(
865            s_f.validate().unwrap_err().to_string(),
866            OtToolsIoError::SettingValue(InvalidValueError::LoopMode).to_string()
867        );
868        Ok(())
869    }
870
871    #[test]
872    fn err_quant() -> Result<(), OtToolsIoError> {
873        let mut s_f = create_mock_new_sample_settings(None, None)?;
874        s_f.quantization = 250; // 255 is an actual value for quantization!
875        s_f.checksum = s_f.calculate_checksum()?;
876        assert_eq!(
877            s_f.validate().unwrap_err().to_string(),
878            OtToolsIoError::SettingValue(InvalidValueError::TrigQuantizationMode).to_string()
879        );
880        Ok(())
881    }
882
883    #[test]
884    fn err_chksum() -> Result<(), OtToolsIoError> {
885        let mut s_f = create_mock_new_sample_settings(None, None)?;
886        s_f.checksum = 1;
887        assert_eq!(
888            s_f.validate().unwrap_err().to_string(),
889            OtToolsIoError::SampleSettings(SampleSettingsError::InvalidChecksum {
890                checksum: s_f.checksum
891            })
892            .to_string(),
893        );
894        Ok(())
895    }
896
897    #[test]
898    fn err_header() -> Result<(), OtToolsIoError> {
899        let mut s_f = create_mock_new_sample_settings(None, None)?;
900        s_f.header[0] = 255;
901        s_f.checksum = s_f.calculate_checksum()?;
902        assert_eq!(
903            s_f.validate().unwrap_err().to_string(),
904            OtToolsIoError::FileHeader.to_string(),
905        );
906        Ok(())
907    }
908
909    #[test]
910    fn err_file_version() -> Result<(), OtToolsIoError> {
911        let mut s_f = create_mock_new_sample_settings(None, None)?;
912        s_f.datatype_version = 255;
913        s_f.checksum = s_f.calculate_checksum()?;
914        assert_eq!(
915            s_f.validate().unwrap_err().to_string(),
916            OtToolsIoError::SampleSettings(SampleSettingsError::InvalidFileVersion {
917                version: s_f.datatype_version
918            })
919            .to_string(),
920        );
921        Ok(())
922    }
923}
924
925#[cfg(test)]
926mod to_slot_attrs {
927    use super::test_utils::create_mock_new_sample_settings;
928    use crate::projects::SlotAttributes;
929    use crate::samples::SampleSettingsError;
930    use crate::settings::SlotType;
931    use crate::OtToolsIoError;
932    use std::path::PathBuf;
933
934    #[test]
935    fn basic_static() -> Result<(), OtToolsIoError> {
936        let settings = create_mock_new_sample_settings(None, None)?;
937
938        let attrs = settings.to_slot_attr(
939            SlotType::Static,
940            1,
941            Some(PathBuf::from("../AUDIO/path.wav")),
942        )?;
943
944        assert_eq!(
945            attrs,
946            SlotAttributes {
947                slot_type: SlotType::Static,
948                slot_id: 1,
949                path: Some(PathBuf::from("../AUDIO/path.wav")),
950                timestrech_mode: Default::default(),
951                loop_mode: Default::default(),
952                trig_quantization_mode: Default::default(),
953                gain: 48,
954                bpm: 2880,
955            },
956        );
957        Ok(())
958    }
959
960    #[test]
961    fn invalid_static_slot_id_0() -> Result<(), OtToolsIoError> {
962        let settings = create_mock_new_sample_settings(None, None)?;
963
964        let attrs = settings.to_slot_attr(
965            SlotType::Static,
966            0,
967            Some(PathBuf::from("../AUDIO/path.wav")),
968        );
969
970        assert_eq!(
971            attrs.unwrap_err().to_string(),
972            OtToolsIoError::SampleSettings(SampleSettingsError::SlotIdOutOfBounds {
973                id: 0,
974                slot_type: SlotType::Static
975            })
976            .to_string(),
977        );
978        Ok(())
979    }
980
981    #[test]
982    fn invalid_static_slot_id_129() -> Result<(), OtToolsIoError> {
983        let settings = create_mock_new_sample_settings(None, None)?;
984
985        let attrs = settings.to_slot_attr(
986            SlotType::Static,
987            129,
988            Some(PathBuf::from("../AUDIO/path.wav")),
989        );
990
991        assert_eq!(
992            attrs.unwrap_err().to_string(),
993            OtToolsIoError::SampleSettings(SampleSettingsError::SlotIdOutOfBounds {
994                id: 129,
995                slot_type: SlotType::Static
996            })
997            .to_string(),
998        );
999        Ok(())
1000    }
1001
1002    #[test]
1003    fn basic_flex() -> Result<(), OtToolsIoError> {
1004        let settings = create_mock_new_sample_settings(None, None)?;
1005
1006        let attrs =
1007            settings.to_slot_attr(SlotType::Flex, 1, Some(PathBuf::from("../AUDIO/path.wav")))?;
1008
1009        assert_eq!(
1010            attrs,
1011            SlotAttributes {
1012                slot_type: SlotType::Flex,
1013                slot_id: 1,
1014                path: Some(PathBuf::from("../AUDIO/path.wav")),
1015                timestrech_mode: Default::default(),
1016                loop_mode: Default::default(),
1017                trig_quantization_mode: Default::default(),
1018                gain: 48,
1019                bpm: 2880,
1020            },
1021        );
1022        Ok(())
1023    }
1024
1025    #[test]
1026    fn invalid_flex_slot_id_0() -> Result<(), OtToolsIoError> {
1027        let settings = create_mock_new_sample_settings(None, None)?;
1028
1029        let attrs =
1030            settings.to_slot_attr(SlotType::Flex, 0, Some(PathBuf::from("../AUDIO/path.wav")));
1031
1032        assert_eq!(
1033            attrs.unwrap_err().to_string(),
1034            OtToolsIoError::SampleSettings(SampleSettingsError::SlotIdOutOfBounds {
1035                id: 0,
1036                slot_type: SlotType::Flex
1037            })
1038            .to_string(),
1039        );
1040        Ok(())
1041    }
1042
1043    #[test]
1044    fn invalid_flex_slot_id_137() -> Result<(), OtToolsIoError> {
1045        let settings = create_mock_new_sample_settings(None, None)?;
1046
1047        let attrs = settings.to_slot_attr(
1048            SlotType::Flex,
1049            137,
1050            Some(PathBuf::from("../AUDIO/path.wav")),
1051        );
1052
1053        assert_eq!(
1054            attrs.unwrap_err().to_string(),
1055            OtToolsIoError::SampleSettings(SampleSettingsError::SlotIdOutOfBounds {
1056                id: 137,
1057                slot_type: SlotType::Flex
1058            })
1059            .to_string(),
1060        );
1061        Ok(())
1062    }
1063}
1064
1065impl SwapBytes for SampleSettingsFile {
1066    fn swap_bytes(self) -> Self {
1067        let mut bswapped_slices = self.slices;
1068
1069        for (i, slice) in self.slices.iter().enumerate() {
1070            bswapped_slices[i] = slice.swap_bytes();
1071        }
1072
1073        Self {
1074            header: SAMPLES_HEADER,
1075            datatype_version: SAMPLES_FILE_VERSION,
1076            unknown: self.unknown,
1077            tempo: self.tempo.swap_bytes(),
1078            trim_bar_len: self.trim_bar_len.swap_bytes(),
1079            loop_bar_len: self.loop_bar_len.swap_bytes(),
1080            stretch: self.stretch.swap_bytes(),
1081            loop_mode: self.loop_mode.swap_bytes(),
1082            gain: self.gain.swap_bytes(),
1083            quantization: self.quantization.swap_bytes(),
1084            trim_start: self.trim_start.swap_bytes(),
1085            trim_end: self.trim_end.swap_bytes(),
1086            loop_start: self.loop_start.swap_bytes(),
1087            slices: bswapped_slices,
1088            slices_len: self.slices_len.swap_bytes(),
1089            checksum: self.checksum.swap_bytes(),
1090        }
1091    }
1092}
1093
1094impl OctatrackFileIO for SampleSettingsFile {
1095    /// Encodes struct data to binary representation, after some pre-processing.
1096    ///
1097    /// Before serializing, will:
1098    /// 1. generate checksum value
1099    /// 2. swap bytes of values (when current system is little-endian)
1100    fn to_bytes(&self) -> Result<Vec<u8>, OtToolsIoError> {
1101        // todo: figure out a non-clone version of this
1102        #[allow(clippy::clone_on_copy)]
1103        let mut chkd = self.clone();
1104        chkd.checksum = self.calculate_checksum()?;
1105
1106        let encoded = if cfg!(target_endian = "little") {
1107            bincode::serialize(&chkd.swap_bytes())?
1108        } else {
1109            bincode::serialize(&chkd)?
1110        };
1111        Ok(encoded)
1112    }
1113
1114    /// Decode raw bytes of a `.ot` data file into a new struct,
1115    /// swapping byte values if system is little-endian.
1116    fn from_bytes(bytes: &[u8]) -> Result<Self, OtToolsIoError> {
1117        let decoded: Self = bincode::deserialize(bytes)?;
1118
1119        // todo: figure out a non-clone version of this
1120        #[allow(clippy::clone_on_copy)]
1121        let mut bswapd = decoded.clone();
1122
1123        // swapping bytes is one required when running on little-endian systems
1124        if cfg!(target_endian = "little") {
1125            bswapd = decoded.swap_bytes();
1126        }
1127
1128        Ok(bswapd)
1129    }
1130}
1131
1132#[cfg(test)]
1133mod from_bytes {
1134    use crate::read_bin_file;
1135    use crate::samples::test_utils::mock_0_slice_test_file;
1136    use crate::test_utils::get_samples_dirpath;
1137    use crate::{OctatrackFileIO, OtToolsIoError, SampleSettingsFile};
1138    #[test]
1139    fn valid() -> Result<(), OtToolsIoError> {
1140        let path = get_samples_dirpath().join("checksum").join("0slices.ot");
1141        let bytes = read_bin_file(&path)?;
1142        let s = SampleSettingsFile::from_bytes(&bytes)?;
1143        assert_eq!(s, mock_0_slice_test_file());
1144        Ok(())
1145    }
1146}
1147
1148#[cfg(test)]
1149mod to_bytes {
1150    use crate::read_bin_file;
1151    use crate::samples::test_utils::mock_0_slice_test_file;
1152    use crate::test_utils::get_samples_dirpath;
1153    use crate::{OctatrackFileIO, OtToolsIoError};
1154    #[test]
1155    fn valid() -> Result<(), OtToolsIoError> {
1156        let path = get_samples_dirpath().join("checksum").join("0slices.ot");
1157        let bytes = read_bin_file(&path)?;
1158        let b = mock_0_slice_test_file().to_bytes()?;
1159        assert_eq!(b, bytes);
1160        Ok(())
1161    }
1162}
1163
1164impl HasChecksumField for SampleSettingsFile {
1165    // tests for this are in the main tests directory -- `samples_settings_files.rs`
1166    fn calculate_checksum(&self) -> Result<u16, OtToolsIoError> {
1167        let bytes = bincode::serialize(&self)?;
1168
1169        // skip header and checksum byte values
1170        let checksum_bytes = &bytes[16..bytes.len() - 2];
1171
1172        let chk: u32 = checksum_bytes
1173            .iter()
1174            .map(|x| *x as u32)
1175            .sum::<u32>()
1176            .rem_euclid(u16::MAX as u32 + 1);
1177
1178        Ok(chk as u16)
1179    }
1180
1181    fn update_checksum(&mut self) -> Result<(), OtToolsIoError> {
1182        self.checksum = self.calculate_checksum()?;
1183        Ok(())
1184    }
1185
1186    fn check_checksum(&self) -> Result<bool, OtToolsIoError> {
1187        Ok(self.checksum == self.calculate_checksum()?)
1188    }
1189}
1190
1191#[cfg(test)]
1192mod checksum_field {
1193    use super::test_utils::create_mock_new_sample_settings;
1194    use crate::{HasChecksumField, OtToolsIoError};
1195    #[test]
1196    fn valid() -> Result<(), OtToolsIoError> {
1197        let mut x = create_mock_new_sample_settings(None, None)?;
1198        x.checksum = x.calculate_checksum()?;
1199        assert!(x.check_checksum()?);
1200        Ok(())
1201    }
1202
1203    #[test]
1204    fn invalid() -> Result<(), OtToolsIoError> {
1205        let mut x = create_mock_new_sample_settings(None, None)?;
1206        x.checksum = x.calculate_checksum()?;
1207        x.checksum = 0;
1208        assert!(!x.check_checksum()?);
1209        Ok(())
1210    }
1211
1212    mod files {
1213        use crate::test_utils::get_samples_dirpath;
1214        use crate::{HasChecksumField, OctatrackFileIO, OtToolsIoError, SampleSettingsFile};
1215
1216        fn helper(test_name: String) -> Result<(u16, u16), OtToolsIoError> {
1217            let mut src_path = get_samples_dirpath().join("checksum").join(&test_name);
1218            src_path.set_extension("ot");
1219
1220            let valid = SampleSettingsFile::from_data_file(&src_path)?;
1221
1222            #[allow(clippy::clone_on_copy)] // want a full deep copy / clone
1223            let mut x = valid.clone();
1224            x.checksum = 0;
1225
1226            Ok((x.calculate_checksum()?, valid.checksum))
1227        }
1228
1229        #[test]
1230        fn zero_slices_trig_quant_one_step() -> Result<(), OtToolsIoError> {
1231            let (test, valid) = helper("0slices-tq1step".to_string())?;
1232            assert_eq!(test, valid);
1233            Ok(())
1234        }
1235
1236        #[test]
1237        fn zero_slices() -> Result<(), OtToolsIoError> {
1238            let (test, valid) = helper("0slices".to_string())?;
1239            assert_eq!(test, valid);
1240            Ok(())
1241        }
1242
1243        #[test]
1244        fn one_slice() -> Result<(), OtToolsIoError> {
1245            let (test, valid) = helper("1slices".to_string())?;
1246            assert_eq!(test, valid);
1247            Ok(())
1248        }
1249
1250        #[test]
1251        fn two_slices() -> Result<(), OtToolsIoError> {
1252            let (test, valid) = helper("2slices".to_string())?;
1253            assert_eq!(test, valid);
1254            Ok(())
1255        }
1256
1257        #[test]
1258        fn four_slices() -> Result<(), OtToolsIoError> {
1259            let (test, valid) = helper("4slices".to_string())?;
1260            assert_eq!(test, valid);
1261            Ok(())
1262        }
1263
1264        #[test]
1265        fn eight_slices() -> Result<(), OtToolsIoError> {
1266            let (test, valid) = helper("8slices".to_string())?;
1267            assert_eq!(test, valid);
1268            Ok(())
1269        }
1270
1271        #[test]
1272        fn sixteen_slices() -> Result<(), OtToolsIoError> {
1273            let (test, valid) = helper("16slices".to_string())?;
1274            assert_eq!(test, valid);
1275            Ok(())
1276        }
1277
1278        #[test]
1279        fn thirty_two_slices() -> Result<(), OtToolsIoError> {
1280            let (test, valid) = helper("32slices".to_string())?;
1281            assert_eq!(test, valid);
1282            Ok(())
1283        }
1284
1285        #[test]
1286        fn forty_eight_slices() -> Result<(), OtToolsIoError> {
1287            let (test, valid) = helper("48slices".to_string())?;
1288            assert_eq!(test, valid);
1289            Ok(())
1290        }
1291
1292        #[test]
1293        fn sixty_two_slices() -> Result<(), OtToolsIoError> {
1294            let (test, valid) = helper("62slices".to_string())?;
1295            assert_eq!(test, valid);
1296            Ok(())
1297        }
1298
1299        #[test]
1300        fn sixty_three_slices() -> Result<(), OtToolsIoError> {
1301            let (test, valid) = helper("63slices".to_string())?;
1302            assert_eq!(test, valid);
1303            Ok(())
1304        }
1305
1306        #[test]
1307        fn sixty_four_slices_correct() -> Result<(), OtToolsIoError> {
1308            let (test, valid) = helper("64slices".to_string())?;
1309            assert_eq!(test, valid);
1310            Ok(())
1311        }
1312    }
1313}
1314
1315impl HasHeaderField for SampleSettingsFile {
1316    fn check_header(&self) -> Result<bool, OtToolsIoError> {
1317        Ok(self.header == SAMPLES_HEADER)
1318    }
1319}
1320
1321#[cfg(test)]
1322mod header_field {
1323    use super::test_utils::create_mock_new_sample_settings;
1324    use crate::{HasHeaderField, OtToolsIoError};
1325    #[test]
1326    fn valid() -> Result<(), OtToolsIoError> {
1327        assert!(create_mock_new_sample_settings(None, None)?.check_header()?);
1328        Ok(())
1329    }
1330
1331    #[test]
1332    fn invalid() -> Result<(), OtToolsIoError> {
1333        let mut mutated = create_mock_new_sample_settings(None, None)?;
1334        mutated.header[0] = 0x00;
1335        mutated.header[20] = 111;
1336        assert!(!mutated.check_header()?);
1337        Ok(())
1338    }
1339}
1340
1341impl HasFileVersionField for SampleSettingsFile {
1342    fn check_file_version(&self) -> Result<bool, OtToolsIoError> {
1343        Ok(self.datatype_version == SAMPLES_FILE_VERSION)
1344    }
1345}
1346
1347#[cfg(test)]
1348mod file_version_field {
1349    use super::test_utils::create_mock_new_sample_settings;
1350    use crate::{HasFileVersionField, OtToolsIoError};
1351    #[test]
1352    fn valid() -> Result<(), OtToolsIoError> {
1353        assert!(create_mock_new_sample_settings(None, None)?.check_file_version()?);
1354        Ok(())
1355    }
1356
1357    #[test]
1358    fn invalid() -> Result<(), OtToolsIoError> {
1359        let mut mutated = create_mock_new_sample_settings(None, None)?;
1360        mutated.datatype_version = 0;
1361        assert!(!mutated.check_file_version()?);
1362        Ok(())
1363    }
1364}
1365
1366impl<A, M> From<(A, M)> for SampleSettingsFile
1367where
1368    A: AsRef<SlotAttributes>,
1369    M: AsRef<SlotMarkers>,
1370{
1371    fn from(value: (A, M)) -> Self {
1372        let attrs = value.0.as_ref();
1373        let markers = value.1.as_ref();
1374
1375        SampleSettingsFile {
1376            header: SAMPLES_HEADER,
1377            datatype_version: SAMPLES_FILE_VERSION,
1378            unknown: 0,
1379            tempo: attrs.bpm as u32,
1380            // todo: does not currently exist as a field in `SlotAttributes`
1381            trim_bar_len: 0,
1382            // todo: does not currently exist as a field in `SlotAttributes`
1383            loop_bar_len: 0,
1384            stretch: attrs.timestrech_mode.into(),
1385            loop_mode: attrs.loop_mode.into(),
1386            gain: attrs.gain as u16,
1387            quantization: attrs.trig_quantization_mode.into(),
1388            trim_start: markers.trim_offset,
1389            trim_end: markers.trim_end,
1390            loop_start: markers.loop_point,
1391            slices: markers.slices,
1392            slices_len: markers.slice_count,
1393            checksum: 0,
1394        }
1395    }
1396}
1397
1398#[cfg(test)]
1399mod sample_settings_from {
1400    use crate::generics::Slots;
1401    use crate::projects::SlotAttributes;
1402    use crate::MarkersFile;
1403    use crate::OtToolsIoError;
1404    use crate::SampleSettingsFile;
1405
1406    #[test]
1407    fn from_owned_valid() -> Result<(), OtToolsIoError> {
1408        let slots = Slots::<Option<SlotAttributes>>::default();
1409        let slot = slots.recording_buffers[0].clone();
1410        let markers = MarkersFile::default().flex_slots[0];
1411        let _ = SampleSettingsFile::from((slot.unwrap(), markers));
1412        Ok(())
1413    }
1414
1415    #[test]
1416    fn from_borrowed_valid() -> Result<(), OtToolsIoError> {
1417        let slots = Slots::<Option<SlotAttributes>>::default();
1418        let slot = slots.recording_buffers[0].clone();
1419        let markers = MarkersFile::default().flex_slots[0];
1420        let _ = SampleSettingsFile::from((&slot.unwrap(), &markers));
1421        Ok(())
1422    }
1423}