ot_tools_io/
slices.rs

1/*
2SPDX-License-Identifier: GPL-3.0-or-later
3Copyright © 2024 Mike Robeson [dijksterhuis]
4*/
5
6//! Slice data structs for sample files (`.ot` files).
7
8use crate::markers::SlotMarkers;
9use crate::traits::SwapBytes;
10use ot_tools_io_derive::IsDefaultCheck;
11use serde::{Deserialize, Serialize};
12use std::array::from_fn;
13use thiserror::Error;
14
15#[derive(Debug, Error)]
16pub enum SliceError {
17    #[error("invalid slice loop point: {value}")]
18    LoopPoint { value: u32 },
19    #[error("invalid slice trim: start={start} end={end}")]
20    Trim { start: u32, end: u32 },
21}
22
23/// Positions of a 'slice' within a single WAV file.
24/// IMPORTANT: slice points are not measured in bars like `SampleAttributes`,
25/// but instead use *audio sample* positions from the audio file.
26#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Copy, Hash, IsDefaultCheck)]
27pub struct Slice {
28    /// Start position for the `Slice`.
29    pub trim_start: u32,
30
31    /// End position for the `Slice`.
32    pub trim_end: u32,
33
34    /// Loop start position for the `Slice`. This is actually `Loop Point` in
35    /// the Octatrack manual.
36    /// > If a loop point is set, the sample will play from the start point to the
37    /// > end point, then loop from the loop point to the end point
38    ///
39    /// Note that a `0xFFFFFFFF` value disables the loop point for the slice.
40    pub loop_start: u32,
41}
42
43pub const SLICE_LOOP_POINT_DISABLED: u32 = 0xFFFFFFFF;
44
45impl Slice {
46    /// Create a new slice.
47    ///
48    /// WARNING: The default setting for `loop_start` changes depending on whether you are working
49    /// with a [`crate::SampleSettingsFile`] or a [`crate::MarkersFile`] ... and whether a sample has
50    /// been loaded into a slot or not for a [`crate::MarkersFile`].
51    ///
52    /// Providing `None` as the `loop_start` argument will default to the
53    /// [`SLICE_LOOP_POINT_DISABLED`] value (`0xFFFFFFFF`).
54    ///
55    /// ```rust
56    /// use ot_tools_io::slices::{Slice, SLICE_LOOP_POINT_DISABLED};
57    ///
58    /// assert_eq!(
59    ///     Slice::new(100, 200, None).unwrap(),
60    ///     Slice { trim_start: 100, trim_end: 200, loop_start: SLICE_LOOP_POINT_DISABLED}
61    /// );
62    /// assert_eq!(
63    ///     Slice::new(100, 200, Some(0)).unwrap(),
64    ///     Slice { trim_start: 100, trim_end: 200, loop_start: 0}
65    /// );
66    /// assert_eq!(
67    ///     Slice::new(100, 200, Some(150)).unwrap(),
68    ///     Slice { trim_start: 100, trim_end: 200, loop_start: 150}
69    /// );
70    /// // cannot have a loop point at the trim_end marker
71    /// assert_eq!(
72    ///     Slice::new(100, 200, Some(200)).unwrap_err().to_string(),
73    ///     "invalid slice loop point: 200".to_string()
74    /// );
75    /// // can have a loop point at the trim_start marker
76    /// assert_eq!(
77    ///     Slice::new(100, 200, Some(100)).unwrap(),
78    ///     Slice { trim_start: 100, trim_end: 200, loop_start: 100}
79    /// );
80    /// // cannot have a loop point out of the trim range
81    /// assert_eq!(
82    ///     Slice::new(100, 200, Some(500)).unwrap_err().to_string(),
83    ///     "invalid slice loop point: 500".to_string()
84    /// );
85    /// // cannot have a loop point out of the trim range
86    /// assert_eq!(
87    ///     Slice::new(100, 200, Some(50)).unwrap_err().to_string(),
88    ///     "invalid slice loop point: 50".to_string()
89    /// );
90    /// ```
91    pub fn new(
92        trim_start: u32,
93        trim_end: u32,
94        loop_start: Option<u32>,
95    ) -> Result<Self, SliceError> {
96        if trim_start > trim_end {
97            return Err(SliceError::Trim {
98                start: trim_start,
99                end: trim_end,
100            });
101        }
102
103        // default is disabled
104        // TODO: ONLY IN OT FILES!
105        let loop_point = loop_start.unwrap_or(SLICE_LOOP_POINT_DISABLED);
106
107        let x = Self {
108            trim_start,
109            trim_end,
110            loop_start: loop_point,
111        };
112
113        x.validate()?;
114
115        Ok(x)
116    }
117
118    // TODO: Check the logic of this
119    pub fn validate(&self) -> Result<bool, SliceError> {
120        // NOTE: Trim start can be equal to trim end when an empty slice
121        if self.trim_start > self.trim_end {
122            return Err(SliceError::Trim {
123                start: self.trim_start,
124                end: self.trim_end,
125            });
126        }
127        if !crate::check_loop_point(self.loop_start, self.trim_start, self.trim_end) {
128            return Err(SliceError::LoopPoint {
129                value: self.loop_start,
130            });
131        }
132
133        Ok(true)
134    }
135}
136
137#[allow(clippy::derivable_impls)]
138impl Default for Slice {
139    fn default() -> Self {
140        Self {
141            trim_start: 0,
142            trim_end: 0,
143            // initialized data for slices is always 0 ...
144            // BUT when we create a slice on the device, the loop point is set
145            // to DISABLED (0xFFFFFFFF) ...
146            //
147            // but users don't see that the data is actually mutated after
148            // initialization ... so this is one of those weird places where
149            // 'default' is not what a user might expect to see as 'default'
150            loop_start: 0,
151        }
152    }
153}
154
155impl SwapBytes for Slice {
156    fn swap_bytes(self) -> Self {
157        Self {
158            trim_start: self.trim_start.swap_bytes(),
159            trim_end: self.trim_end.swap_bytes(),
160            loop_start: self.loop_start.swap_bytes(),
161        }
162    }
163}
164
165/// A collection of `Slice` objects.
166#[derive(Debug)]
167pub struct Slices {
168    /// `Slice` objects, must be 64 elements in length.
169    pub slices: [Slice; 64],
170
171    /// Number of non-zero valued `Slice` objects in the `slices` field array.
172    pub count: u32,
173}
174
175impl Default for Slices {
176    fn default() -> Self {
177        let slices: [Slice; 64] = from_fn(|_| Slice::default());
178        Slices { slices, count: 0 }
179    }
180}
181
182impl From<SlotMarkers> for Slices {
183    fn from(value: SlotMarkers) -> Self {
184        Self {
185            slices: value.slices,
186            count: value.slice_count,
187        }
188    }
189}
190
191impl From<&SlotMarkers> for Slices {
192    fn from(value: &SlotMarkers) -> Self {
193        Self::from(value.clone())
194    }
195}
196
197#[cfg(test)]
198mod test {
199
200    use crate::slices::Slice;
201
202    #[test]
203    fn ok_no_offset_no_loop() {
204        let valid = Slice {
205            trim_start: 0,
206            trim_end: 1000,
207            loop_start: 0xFFFFFFFF,
208        };
209
210        let s = Slice::new(0, 1000, None);
211
212        assert!(s.is_ok());
213        assert_eq!(valid, s.unwrap());
214    }
215
216    #[test]
217    fn ok_loop_point() {
218        let valid = Slice {
219            trim_start: 0,
220            trim_end: 1000,
221            loop_start: 0,
222        };
223
224        let s = Slice::new(0, 1000, Some(0));
225
226        assert!(s.is_ok());
227        assert_eq!(valid, s.unwrap());
228    }
229
230    #[test]
231    fn err_loop_end() {
232        let s = Slice::new(0, 1000, Some(1000));
233        assert!(s.is_err());
234        assert_eq!(s.unwrap_err().to_string(), "invalid slice loop point: 1000");
235    }
236
237    #[test]
238    fn err_trim() {
239        let s = Slice::new(1001, 1000, None);
240        assert!(s.is_err());
241        assert_eq!(
242            s.unwrap_err().to_string(),
243            "invalid slice trim: start=1001 end=1000"
244        );
245    }
246
247    #[test]
248    fn ok_offset_100_no_loop() {
249        let valid = Slice {
250            trim_start: 100,
251            trim_end: 1100,
252            loop_start: 0xFFFFFFFF,
253        };
254
255        let s = Slice::new(100, 1100, None);
256
257        assert!(s.is_ok());
258        assert_eq!(valid, s.unwrap());
259    }
260
261    #[test]
262    fn ok_offset_100_with_loop_200() {
263        let valid = Slice {
264            trim_start: 100,
265            trim_end: 1100,
266            loop_start: 200,
267        };
268
269        let s = Slice::new(100, 1100, Some(200));
270
271        assert!(s.is_ok());
272        assert_eq!(valid, s.unwrap());
273    }
274}