ot-tools-io 0.6.1

A library crate for reading/writing binary data files used by the Elektron Octatrack DPS-1.
Documentation
/*
SPDX-License-Identifier: GPL-3.0-or-later
Copyright © 2024 Mike Robeson [dijksterhuis]
*/

//! Slice data structs for sample files (`.ot` files).

use crate::markers::SlotMarkers;
use crate::traits::SwapBytes;
use ot_tools_io_derive::IsDefaultCheck;
use serde::{Deserialize, Serialize};
use std::array::from_fn;
use thiserror::Error;

#[derive(Debug, Error, PartialEq)]
pub enum SliceError {
    #[error("invalid slice loop point: {value}")]
    LoopPoint { value: u32 },
    #[error("invalid slice trim: start={start} end={end}")]
    Trim { start: u32, end: u32 },
}

/// Positions of a 'slice' within a single WAV file.
/// IMPORTANT: slice points are not measured in bars like `SampleAttributes`,
/// but instead use *audio sample* positions from the audio file.
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Copy, Hash, IsDefaultCheck)]
pub struct Slice {
    /// Start position for the `Slice`.
    pub trim_start: u32,

    /// End position for the `Slice`.
    pub trim_end: u32,

    /// Loop start position for the `Slice`. This is actually `Loop Point` in
    /// the Octatrack manual.
    /// > If a loop point is set, the sample will play from the start point to the
    /// > end point, then loop from the loop point to the end point
    ///
    /// Note that a `0xFFFFFFFF` value disables the loop point for the slice.
    pub loop_start: u32,
}

pub const SLICE_LOOP_POINT_DISABLED: u32 = 0xFFFFFFFF;

impl Slice {
    /// Create a new slice.
    ///
    /// WARNING: The default setting for `loop_start` changes depending on whether you are working
    /// with a [`crate::SampleSettingsFile`] or a [`crate::MarkersFile`] ... and whether a sample has
    /// been loaded into a slot or not for a [`crate::MarkersFile`].
    ///
    /// Providing `None` as the `loop_start` argument will default to the
    /// [`SLICE_LOOP_POINT_DISABLED`] value (`0xFFFFFFFF`).
    ///
    /// ```rust
    /// use ot_tools_io::slices::{Slice, SLICE_LOOP_POINT_DISABLED};
    ///
    /// // has loop point
    /// assert_eq!(
    ///     Slice::new(100, 200, Some(150)).unwrap(),
    ///     Slice { trim_start: 100, trim_end: 200, loop_start: 150}
    /// );
    /// // no loop point for sample settings file data
    /// assert_eq!(
    ///     Slice::new(100, 200, None).unwrap(),
    ///     Slice { trim_start: 100, trim_end: 200, loop_start: SLICE_LOOP_POINT_DISABLED}
    /// );
    /// // GOTCHA! no loop point is zero for markers file data!
    /// assert_eq!(
    ///     Slice::new(100, 200, Some(0)).unwrap(),
    ///     Slice { trim_start: 100, trim_end: 200, loop_start: 0}
    /// );
    /// // trim start can equal trim end (usually empty slices)
    /// assert_eq!(
    ///     Slice::new(100, 100, None).unwrap(),
    ///     Slice { trim_start: 100, trim_end: 100, loop_start: SLICE_LOOP_POINT_DISABLED}
    /// );
    /// // cannot have trim start after trim end
    /// assert_eq!(
    ///     Slice::new(200, 100, None).unwrap_err().to_string(),
    ///     "invalid slice trim: start=200 end=100".to_string()
    /// );
    /// // can have a loop point at trim start
    /// assert_eq!(
    ///     Slice::new(100, 200, Some(100)).unwrap(),
    ///     Slice { trim_start: 100, trim_end: 200, loop_start: 100}
    /// );
    /// // cannot have a loop point at trim end
    /// assert_eq!(
    ///     Slice::new(100, 200, Some(200)).unwrap_err().to_string(),
    ///     "invalid slice loop point: 200".to_string()
    /// );
    /// // cannot have a loop point before trim start
    /// assert_eq!(
    ///     Slice::new(100, 200, Some(50)).unwrap_err().to_string(),
    ///     "invalid slice loop point: 50".to_string()
    /// );
    /// // cannot have a loop point after trim end
    /// assert_eq!(
    ///     Slice::new(100, 200, Some(500)).unwrap_err().to_string(),
    ///     "invalid slice loop point: 500".to_string()
    /// );
    /// ```
    pub fn new(
        trim_start: u32,
        trim_end: u32,
        loop_start: Option<u32>,
    ) -> Result<Self, SliceError> {
        if trim_start > trim_end {
            return Err(SliceError::Trim {
                start: trim_start,
                end: trim_end,
            });
        }

        // default is disabled
        // TODO: ONLY IN OT FILES!
        let loop_point = loop_start.unwrap_or(SLICE_LOOP_POINT_DISABLED);

        let x = Self {
            trim_start,
            trim_end,
            loop_start: loop_point,
        };

        x.validate()?;

        Ok(x)
    }

    pub fn validate(&self) -> Result<bool, SliceError> {
        // NOTE: Trim start can be equal to trim end when an empty slice
        if self.trim_start > self.trim_end {
            return Err(SliceError::Trim {
                start: self.trim_start,
                end: self.trim_end,
            });
        }
        if !crate::check_loop_point(self.loop_start, self.trim_start, self.trim_end) {
            return Err(SliceError::LoopPoint {
                value: self.loop_start,
            });
        }

        Ok(true)
    }
}

#[allow(clippy::derivable_impls)]
impl Default for Slice {
    fn default() -> Self {
        Self {
            trim_start: 0,
            trim_end: 0,
            // initialized data for slices is always 0 ...
            // BUT when we create a slice on the device, the loop point is set
            // to DISABLED (0xFFFFFFFF) ...
            //
            // but users don't see that the data is actually mutated after
            // initialization ... so this is one of those weird places where
            // 'default' is not what a user might expect to see as 'default'
            loop_start: 0,
        }
    }
}

impl SwapBytes for Slice {
    fn swap_bytes(self) -> Self {
        Self {
            trim_start: self.trim_start.swap_bytes(),
            trim_end: self.trim_end.swap_bytes(),
            loop_start: self.loop_start.swap_bytes(),
        }
    }
}

/// A collection of `Slice` objects.
#[derive(Debug)]
pub struct Slices {
    /// `Slice` objects, must be 64 elements in length.
    pub slices: [Slice; 64],

    /// Number of non-zero valued `Slice` objects in the `slices` field array.
    pub count: u32,
}

impl Default for Slices {
    fn default() -> Self {
        let slices: [Slice; 64] = from_fn(|_| Slice::default());
        Slices { slices, count: 0 }
    }
}

impl From<SlotMarkers> for Slices {
    fn from(value: SlotMarkers) -> Self {
        Self {
            slices: value.slices,
            count: value.slice_count,
        }
    }
}

impl From<&SlotMarkers> for Slices {
    fn from(value: &SlotMarkers) -> Self {
        Self::from(value.clone())
    }
}

#[cfg(test)]
mod slices_from {
    use crate::slices::SliceError;
    use crate::slices::Slices;
    use crate::MarkersFile;

    #[test]
    fn owned() -> Result<(), SliceError> {
        let markers = MarkersFile::default().flex_slots[0].clone();
        let _ = Slices::from(markers);
        Ok(())
    }

    #[test]
    fn borrowed() -> Result<(), SliceError> {
        let markers = MarkersFile::default().flex_slots[0].clone();
        let _ = Slices::from(&markers);
        Ok(())
    }
}

#[cfg(test)]
mod test {

    use crate::slices::Slice;
    use crate::slices::SliceError;

    #[test]
    fn ok_no_offset_no_loop() {
        let valid = Slice {
            trim_start: 0,
            trim_end: 1000,
            loop_start: 0xFFFFFFFF,
        };

        let s = Slice::new(0, 1000, None);

        assert!(s.is_ok());
        assert_eq!(valid, s.unwrap());
    }

    #[test]
    fn ok_loop_point() {
        let valid = Slice {
            trim_start: 0,
            trim_end: 1000,
            loop_start: 0,
        };

        let s = Slice::new(0, 1000, Some(0));

        assert!(s.is_ok());
        assert_eq!(valid, s.unwrap());
    }

    #[test]
    fn err_loop_end() {
        let s = Slice::new(0, 1000, Some(1000));
        assert!(s.is_err());
        assert_eq!(s.unwrap_err(), SliceError::LoopPoint { value: 1000 });
    }

    #[test]
    fn err_trim() {
        let (start, end) = (1001, 1000);
        let s = Slice::new(start, end, None);
        assert!(s.is_err());
        assert_eq!(s.unwrap_err(), SliceError::Trim { start, end });
    }

    #[test]
    fn ok_offset_100_no_loop() {
        let valid = Slice {
            trim_start: 100,
            trim_end: 1100,
            loop_start: 0xFFFFFFFF,
        };

        let s = Slice::new(100, 1100, None);

        assert!(s.is_ok());
        assert_eq!(valid, s.unwrap());
    }

    #[test]
    fn ok_offset_100_with_loop_200() {
        let valid = Slice {
            trim_start: 100,
            trim_end: 1100,
            loop_start: 200,
        };

        let s = Slice::new(100, 1100, Some(200));

        assert!(s.is_ok());
        assert_eq!(valid, s.unwrap());
    }
}