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