ot_tools_io/
markers.rs

1/*
2SPDX-License-Identifier: GPL-3.0-or-later
3Copyright © 2024 Mike Robeson [dijksterhuis]
4*/
5
6//! Types and ser/de of `markers.*` binary data files.
7//!
8//! A "Markers" file contains data related to the playback settings of loaded
9//! sample slots
10//! - Trim Start
11//! - Trim End
12//! - Loop Point
13//! - Slices (Start / End / Loop Point)
14//!
15//! The Markers file is dependent on a Project's Sample Slots. Switching out a
16//! sample slot updates playback settings in the markers file.
17//!
18//! TODO: Figure out exactly WHEN update occurs (when adding a file to the slot?)
19
20// TODO
21use crate::slices::{Slice, SliceError};
22use crate::traits::SwapBytes;
23use crate::{
24    HasChecksumField, HasFileVersionField, HasHeaderField, IsDefault, OctatrackFileIO,
25    OtToolsIoError,
26};
27use ot_tools_io_derive::{IntegrityChecks, IsDefaultCheck};
28use serde::{Deserialize, Serialize};
29use serde_big_array::{Array, BigArray};
30use std::array::from_fn;
31use thiserror::Error;
32/*
33# ===== DEFAULT DATA FILE ===== #
3400000000  46 4f 52 4d 00 00 00 00  44 50 53 31 53 41 4d 50  |FORM....DPS1SAMP|
3500000010  00 00 00 00 00 04 00 00  00 00 00 00 00 00 00 00  |................|
3600000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
37# ***** REPEATS *****
3800032890  00 00 00 00 00 00 00 04                           |........|
3900032898
40*/
41
42#[derive(Debug, Error)]
43pub enum MarkersError {
44    #[error("invalid loop point: {value}")]
45    Loop { value: u32 },
46    #[error("invalid trim: start={start} end={end}")]
47    Trim { start: u32, end: u32 },
48    #[error("invalid slice count: {value}")]
49    SliceCount { value: u32 },
50    #[error("invalid slice")]
51    Slice(#[from] SliceError),
52}
53
54/// Header data for the markers file
55pub const MARKERS_HEADER: [u8; 21] = [
56    0x46, 0x4f, 0x52, 0x4d, 0x00, 0x00, 0x00, 0x00, 0x44, 0x50, 0x53, 0x31, 0x53, 0x41, 0x4d, 0x50,
57    0x00, 0x00, 0x00, 0x00, 0x00,
58];
59
60/// Current/supported version of markers files.
61pub const MARKERS_FILE_VERSION: u8 = 4;
62
63/// The 'markers' file. Contains sample editor data for all slots in a project.
64#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, IntegrityChecks, IsDefaultCheck)]
65pub struct MarkersFile {
66    #[serde(with = "BigArray")]
67    pub header: [u8; 21],
68
69    /// version of data file. used in OS upgrades for patching files (and checks
70    /// performed on files during loading of a project).
71    ///
72    /// ### background / context
73    ///
74    /// Got this error on the device when i messed up the default markers file
75    /// when i didn't include the `4` (current value for 1.40B).
76    /// ```text
77    /// >>> 2025-04-24 22:06:00 ERROR Couldn't read from markers file ...
78    /// >>> '/test-set-bankcopy/PROJECT-BLANK/markers.work' ('INVALID FILESIZE')
79    /// ```
80    ///
81    /// Additionally, Banks for projects created with 1.40X have a 'version'
82    /// number of 23, while the LESSSELF/V1 project started with 1.25E has a
83    /// version of 15.
84    ///
85    /// So yep. These weird numbers are version numbers for the data types /
86    /// structures / files.
87    pub datatype_version: u8,
88
89    /// flex slots playback data
90    pub flex_slots: Box<Array<SlotMarkers, 136>>,
91
92    /// static slots playback data
93    pub static_slots: Box<Array<SlotMarkers, 128>>,
94
95    // i'm assuming u16 checksum here as well?
96    pub checksum: u16,
97}
98
99impl MarkersFile {
100    fn new(
101        flex_slots: [SlotMarkers; 136],
102        static_slots: [SlotMarkers; 128],
103    ) -> Result<Self, OtToolsIoError> {
104        let mut init = Self {
105            header: MARKERS_HEADER,
106            datatype_version: MARKERS_FILE_VERSION,
107            flex_slots: Array(flex_slots).into(),
108            static_slots: Array(static_slots).into(),
109            checksum: 0,
110        };
111
112        init.checksum = init.calculate_checksum()?;
113        init.validate()?;
114        Ok(init)
115    }
116
117    fn validate(&self) -> Result<bool, MarkersError> {
118        for slot in self.flex_slots.iter() {
119            slot.validate()?;
120        }
121        for slot in self.static_slots.iter() {
122            slot.validate()?;
123        }
124
125        Ok(true)
126    }
127}
128
129#[cfg(test)]
130mod markers_file_validate {
131    use crate::{test_utils::get_blank_proj_dirpath, MarkersFile, OctatrackFileIO, OtToolsIoError};
132    #[test]
133    fn valid() -> Result<(), OtToolsIoError> {
134        let path = get_blank_proj_dirpath().join("markers.work");
135        assert!(MarkersFile::from_data_file(&path)?.validate()?);
136        Ok(())
137    }
138
139    #[test]
140    fn invalid_trim_offset() -> Result<(), OtToolsIoError> {
141        let path = get_blank_proj_dirpath().join("markers.work");
142        let mut x = MarkersFile::from_data_file(&path)?;
143        x.flex_slots[0].trim_offset = 100;
144        assert_eq!(
145            x.validate().unwrap_err().to_string(),
146            "invalid trim: start=100 end=0".to_string()
147        );
148        Ok(())
149    }
150
151    #[test]
152    fn invalid_slice_count() -> Result<(), OtToolsIoError> {
153        let path = get_blank_proj_dirpath().join("markers.work");
154        let mut x = MarkersFile::from_data_file(&path)?;
155        x.flex_slots[0].slice_count = 100;
156        assert_eq!(
157            x.validate().unwrap_err().to_string(),
158            "invalid slice count: 100".to_string()
159        );
160        Ok(())
161    }
162
163    #[test]
164    fn invalid_loop_point() -> Result<(), OtToolsIoError> {
165        let path = get_blank_proj_dirpath().join("markers.work");
166        let mut x = MarkersFile::from_data_file(&path)?;
167        x.flex_slots[0].loop_point = 100;
168        assert_eq!(
169            x.validate().unwrap_err().to_string(),
170            "invalid loop point: 100".to_string()
171        );
172        Ok(())
173    }
174}
175
176impl Default for MarkersFile {
177    fn default() -> Self {
178        Self::new(
179            from_fn(|_| SlotMarkers::default()),
180            from_fn(|_| SlotMarkers::default()),
181        )
182        .unwrap()
183    }
184}
185
186impl SwapBytes for MarkersFile {
187    fn swap_bytes(self) -> Self {
188        let mut flex_slots = self.flex_slots.clone();
189        for (i, slot) in self.flex_slots.iter().enumerate() {
190            flex_slots[i] = slot.clone().swap_bytes();
191        }
192
193        let mut static_slots = self.static_slots.clone();
194        for (i, slot) in self.static_slots.iter().enumerate() {
195            static_slots[i] = slot.clone().swap_bytes();
196        }
197
198        Self {
199            header: self.header,
200            datatype_version: self.datatype_version,
201            flex_slots,
202            static_slots,
203            checksum: self.checksum.swap_bytes(),
204        }
205    }
206}
207
208impl OctatrackFileIO for MarkersFile {
209    fn encode(&self) -> Result<Vec<u8>, OtToolsIoError>
210    where
211        Self: Serialize,
212    {
213        let mut chkd = self.clone();
214        chkd.checksum = self.calculate_checksum()?;
215        let encoded = if cfg!(target_endian = "little") {
216            bincode::serialize(&chkd.swap_bytes())?
217        } else {
218            bincode::serialize(&chkd)?
219        };
220        Ok(encoded)
221    }
222
223    fn decode(bytes: &[u8]) -> Result<Self, OtToolsIoError>
224    where
225        Self: Sized,
226        Self: for<'a> Deserialize<'a>,
227    {
228        let mut x: Self = bincode::deserialize(bytes)?;
229        #[cfg(target_endian = "little")]
230        {
231            x = x.swap_bytes();
232        }
233
234        Ok(x)
235    }
236}
237
238#[cfg(test)]
239mod decode {
240    use crate::{
241        read_bin_file, test_utils::get_blank_proj_dirpath, MarkersFile, OctatrackFileIO,
242        OtToolsIoError,
243    };
244    #[test]
245    fn valid() -> Result<(), OtToolsIoError> {
246        let path = get_blank_proj_dirpath().join("markers.work");
247        let bytes = read_bin_file(&path)?;
248        let s = MarkersFile::decode(&bytes)?;
249        assert_eq!(s, MarkersFile::default());
250        Ok(())
251    }
252}
253
254#[cfg(test)]
255mod encode {
256    use crate::{
257        read_bin_file, test_utils::get_blank_proj_dirpath, MarkersFile, OctatrackFileIO,
258        OtToolsIoError,
259    };
260    #[test]
261    fn valid() -> Result<(), OtToolsIoError> {
262        let path = get_blank_proj_dirpath().join("markers.work");
263        let bytes = read_bin_file(&path)?;
264        let b = MarkersFile::default().encode()?;
265        assert_eq!(b, bytes);
266        Ok(())
267    }
268}
269
270impl HasChecksumField for MarkersFile {
271    fn calculate_checksum(&self) -> Result<u16, OtToolsIoError> {
272        let bytes = bincode::serialize(self)?;
273        let mut chk: u16 = 0;
274        for byte in &bytes[16..bytes.len() - 2] {
275            chk = chk.wrapping_add(*byte as u16);
276        }
277        Ok(chk)
278    }
279    fn check_checksum(&self) -> Result<bool, OtToolsIoError> {
280        Ok(self.checksum == self.calculate_checksum()?)
281    }
282}
283
284#[cfg(test)]
285mod checksum_field {
286    use crate::{HasChecksumField, MarkersFile, OtToolsIoError};
287    #[test]
288    fn valid() -> Result<(), OtToolsIoError> {
289        let mut x = MarkersFile::default();
290        x.checksum = x.calculate_checksum()?;
291        assert!(x.check_checksum()?);
292        Ok(())
293    }
294
295    #[test]
296    fn invalid() -> Result<(), OtToolsIoError> {
297        let mut x = MarkersFile::default();
298        x.checksum = x.calculate_checksum()?;
299        x.checksum = 0;
300        assert!(!x.check_checksum()?);
301        Ok(())
302    }
303}
304
305impl HasHeaderField for MarkersFile {
306    fn check_header(&self) -> Result<bool, OtToolsIoError> {
307        Ok(self.header == MARKERS_HEADER)
308    }
309}
310
311#[cfg(test)]
312mod header_field {
313    use crate::{HasHeaderField, MarkersFile, OtToolsIoError};
314    #[test]
315    fn valid() -> Result<(), OtToolsIoError> {
316        assert!(MarkersFile::default().check_header()?);
317        Ok(())
318    }
319
320    #[test]
321    fn invalid() -> Result<(), OtToolsIoError> {
322        let mut mutated = MarkersFile::default();
323        mutated.header[0] = 0x00;
324        mutated.header[20] = 111;
325        assert!(!mutated.check_header()?);
326        Ok(())
327    }
328}
329
330impl HasFileVersionField for MarkersFile {
331    fn check_file_version(&self) -> Result<bool, OtToolsIoError> {
332        Ok(self.datatype_version == MARKERS_FILE_VERSION)
333    }
334}
335
336#[cfg(test)]
337mod file_version_field {
338    use crate::{HasFileVersionField, MarkersFile, OtToolsIoError};
339    #[test]
340    fn valid() -> Result<(), OtToolsIoError> {
341        assert!(MarkersFile::default().check_file_version()?);
342        Ok(())
343    }
344
345    #[test]
346    fn invalid() -> Result<(), OtToolsIoError> {
347        let mut mutated = MarkersFile {
348            datatype_version: 0,
349            ..Default::default()
350        };
351        mutated.datatype_version = 0;
352        assert!(!mutated.check_file_version()?);
353        Ok(())
354    }
355}
356
357/// Slot playback trigger points/markers. Generally, data related to trim, looping and slices for each slot.
358/// Global slot settings like quantization/time-stretch are stored in the [`crate::projects::SlotAttributes`] type
359///
360/// NOTE: On the naming for this -- the Octatrack manual specifically refers to
361/// > SAVE SAMPLE SETTINGS will save the trim, slice and attribute settings in a
362/// > separate file and link it to the sample currently being edited.
363/// > -- page 87
364///
365/// So ... these are the Slot MARKERS (trim/loop/slice markers) which are saved
366/// to a settings file.
367#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Hash)]
368pub struct SlotMarkers {
369    /// main sample trim offset (start)
370    pub trim_offset: u32,
371
372    /// main sample trim end
373    pub trim_end: u32,
374
375    /// main sample loop point
376    pub loop_point: u32,
377
378    /// slices array for slice position editing
379    #[serde(with = "BigArray")]
380    pub slices: [Slice; 64],
381
382    /// number of slices
383    pub slice_count: u32,
384}
385
386impl Default for SlotMarkers {
387    fn default() -> Self {
388        Self {
389            trim_offset: 0,
390            trim_end: 0,
391            loop_point: 0,
392            slices: from_fn(|_| Slice::default()),
393            slice_count: 0,
394        }
395    }
396}
397
398impl SwapBytes for SlotMarkers {
399    fn swap_bytes(self) -> Self {
400        let mut slices: [Slice; 64] = self.slices;
401
402        for (i, slice) in self.slices.iter().enumerate() {
403            slices[i] = slice.swap_bytes();
404        }
405
406        Self {
407            trim_offset: self.trim_offset.swap_bytes(),
408            trim_end: self.trim_end.swap_bytes(),
409            loop_point: self.loop_point.swap_bytes(),
410            slices,
411            slice_count: self.slice_count.swap_bytes(),
412        }
413    }
414}
415
416impl SlotMarkers {
417    fn validate(&self) -> Result<bool, MarkersError> {
418        for slice in self.slices.iter() {
419            slice.validate()?;
420        }
421        if self.trim_offset > self.trim_end {
422            return Err(MarkersError::Trim {
423                start: self.trim_offset,
424                end: self.trim_end,
425            });
426        }
427
428        let slice_count = self.slices.iter().filter(|x| !x.is_default()).count();
429
430        if self.slice_count != slice_count as u32 {
431            return Err(MarkersError::SliceCount {
432                value: self.slice_count,
433            });
434        }
435
436        if !crate::check_loop_point(self.loop_point, self.trim_offset, self.trim_end) {
437            return Err(MarkersError::Loop {
438                value: self.loop_point,
439            });
440        }
441
442        Ok(true)
443    }
444}
445
446#[cfg(test)]
447mod slot_markers_validate {
448    use super::SlotMarkers;
449    use crate::OtToolsIoError;
450    #[test]
451    fn valid() -> Result<(), OtToolsIoError> {
452        assert!(SlotMarkers::default().validate()?);
453        Ok(())
454    }
455
456    #[test]
457    fn invalid_trim_offset() -> Result<(), OtToolsIoError> {
458        let x = SlotMarkers {
459            trim_offset: 100,
460            ..SlotMarkers::default()
461        };
462        assert_eq!(
463            x.validate().unwrap_err().to_string(),
464            "invalid trim: start=100 end=0".to_string()
465        );
466        Ok(())
467    }
468
469    #[test]
470    fn invalid_slice_count() -> Result<(), OtToolsIoError> {
471        let x = SlotMarkers {
472            slice_count: 100,
473            ..SlotMarkers::default()
474        };
475        assert_eq!(
476            x.validate().unwrap_err().to_string(),
477            "invalid slice count: 100".to_string()
478        );
479        Ok(())
480    }
481
482    #[test]
483    fn invalid_loop_point() -> Result<(), OtToolsIoError> {
484        let x = SlotMarkers {
485            loop_point: 100,
486            ..SlotMarkers::default()
487        };
488        assert_eq!(
489            x.validate().unwrap_err().to_string(),
490            "invalid loop point: 100".to_string()
491        );
492        Ok(())
493    }
494}