Skip to main content

ot_tools_io/
markers.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 [`MarkersFile`]s (`markers.*` files).
7
8mod slot_markers;
9
10use crate::generics::{PlaybackSlots, RecordingBufferSlots};
11use crate::traits::SwapBytes;
12use crate::{
13    HasChecksumField, HasFileVersionField, HasHeaderField, OctatrackFileIO, OtToolsIoError,
14};
15use ot_tools_io_derive::{AsMutDerive, AsRefDerive, IntegrityChecks, IsDefaultCheck};
16use serde::{Deserialize, Serialize};
17use serde_big_array::BigArray;
18pub use slot_markers::{SlotMarkers, SlotMarkersError};
19/*
20# ===== DEFAULT DATA FILE ===== #
2100000000  46 4f 52 4d 00 00 00 00  44 50 53 31 53 41 4d 50  |FORM....DPS1SAMP|
2200000010  00 00 00 00 00 04 00 00  00 00 00 00 00 00 00 00  |................|
2300000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
24# ***** REPEATS *****
2500032890  00 00 00 00 00 00 00 04                           |........|
2600032898
27*/
28
29/// Header data for the markers file
30pub const MARKERS_HEADER: [u8; 21] = [
31    0x46, 0x4f, 0x52, 0x4d, 0x00, 0x00, 0x00, 0x00, 0x44, 0x50, 0x53, 0x31, 0x53, 0x41, 0x4d, 0x50,
32    0x00, 0x00, 0x00, 0x00, 0x00,
33];
34
35/// Current/supported version of markers files.
36pub const MARKERS_FILE_VERSION: u8 = 4;
37
38/// A "Markers" file contains data related to the playback settings of loaded
39/// sample slots
40/// - Trim Start
41/// - Trim End
42/// - Loop Point
43/// - Slices (Start / End / Loop Point)
44///
45/// The Markers file is dependent on a Project's Sample Slots. Switching out a
46/// sample slot updates playback settings in the markers file.
47///
48/// TODO: Figure out exactly WHEN update occurs (when adding a file to the slot?)
49///
50/// The 'markers' file. Contains sample editor data for all slots in a project.
51///
52/// No `Copy` trait on this type as multiple fields are `Box<T>` types
53#[derive(
54    Clone,
55    Debug,
56    Eq,
57    Hash,
58    Ord,
59    PartialEq,
60    PartialOrd,
61    Serialize,
62    Deserialize,
63    AsMutDerive,
64    AsRefDerive,
65    IntegrityChecks,
66    IsDefaultCheck,
67)]
68pub struct MarkersFile {
69    #[serde(with = "BigArray")]
70    pub header: [u8; 21],
71
72    /// version of data file. used in OS upgrades for patching files (and checks
73    /// performed on files during loading of a project).
74    ///
75    /// ### background / context
76    ///
77    /// Got this error on the device when i messed up the default markers file
78    /// when i didn't include the `4` (current value for 1.40B).
79    /// ```text
80    /// >>> 2025-04-24 22:06:00 ERROR Couldn't read from markers file ...
81    /// >>> '/test-set-bankcopy/PROJECT-BLANK/markers.work' ('INVALID FILESIZE')
82    /// ```
83    ///
84    /// Additionally, Banks for projects created with 1.40X have a 'version'
85    /// number of 23, while the LESSSELF/V1 project started with 1.25E has a
86    /// version of 15.
87    ///
88    /// So yep. These weird numbers are version numbers for the data types /
89    /// structures / files.
90    pub datatype_version: u8,
91
92    // note: don't use Slots<T> here just yet.
93    // todo: need to implement Deserialize/Serialize for Slots<SlotMarkers>
94    /// flex slots playback data
95    pub flex_slots: PlaybackSlots<SlotMarkers>,
96
97    /// recording buffer slots playback data
98    pub recording_buffers: RecordingBufferSlots<SlotMarkers>,
99
100    /// static slots playback data
101    pub static_slots: PlaybackSlots<SlotMarkers>,
102
103    // i'm assuming u16 checksum here as well?
104    pub checksum: u16,
105}
106
107impl MarkersFile {
108    pub fn new(
109        flex_slots: PlaybackSlots<SlotMarkers>,
110        recording_buffers: RecordingBufferSlots<SlotMarkers>,
111        static_slots: PlaybackSlots<SlotMarkers>,
112    ) -> Result<Self, OtToolsIoError> {
113        let mut init = Self {
114            header: MARKERS_HEADER,
115            datatype_version: MARKERS_FILE_VERSION,
116            flex_slots,
117            recording_buffers,
118            static_slots,
119            checksum: 0,
120        };
121
122        init.checksum = init.calculate_checksum()?;
123        init.validate()?;
124        Ok(init)
125    }
126
127    pub fn validate(&self) -> Result<(), OtToolsIoError> {
128        for slot in self.flex_slots.iter() {
129            slot.validate()?;
130        }
131        for slot in self.static_slots.iter() {
132            slot.validate()?;
133        }
134        Ok(())
135    }
136
137    /// "De-inits" all slots to zero values, clearing all slot data from the instance.
138    pub fn deinit(&mut self) {
139        for slot in self.static_slots.iter_mut() {
140            slot.deinit();
141        }
142        for slot in self.recording_buffers.iter_mut() {
143            slot.deinit();
144        }
145        for slot in self.flex_slots.iter_mut() {
146            slot.deinit();
147        }
148    }
149}
150
151#[cfg(test)]
152mod deinit {
153    use crate::{IsDefault, MarkersFile, OtToolsIoError};
154    #[test]
155    fn ok() -> Result<(), OtToolsIoError> {
156        let mut x = MarkersFile::default();
157        x.deinit();
158        assert!(x.is_default());
159        Ok(())
160    }
161}
162
163#[cfg(test)]
164mod validate {
165    use crate::markers::SlotMarkersError;
166    use crate::{test_utils::get_blank_proj_dirpath, MarkersFile, OctatrackFileIO, OtToolsIoError};
167
168    #[test]
169    fn valid() -> Result<(), OtToolsIoError> {
170        let path = get_blank_proj_dirpath().join("markers.work");
171        assert!(MarkersFile::from_data_file(&path)?.validate().is_ok());
172        Ok(())
173    }
174
175    #[test]
176    fn invalid_trim_offset() -> Result<(), OtToolsIoError> {
177        let path = get_blank_proj_dirpath().join("markers.work");
178        let mut x = MarkersFile::from_data_file(&path)?;
179        x.flex_slots[0].trim_offset = 100;
180        assert_eq!(
181            x.validate().unwrap_err().to_string(),
182            OtToolsIoError::Markers(SlotMarkersError::Trim { start: 100, end: 0 }).to_string()
183        );
184        Ok(())
185    }
186
187    #[test]
188    fn invalid_slice_count() -> Result<(), OtToolsIoError> {
189        let path = get_blank_proj_dirpath().join("markers.work");
190        let mut x = MarkersFile::from_data_file(&path)?;
191        x.flex_slots[0].slice_count = 100;
192        x.flex_slots[0].trim_end = 1000;
193        x.flex_slots[0].trim_offset = 0;
194        x.flex_slots[0].loop_point = 0;
195        assert_eq!(
196            x.validate().unwrap_err().to_string(),
197            OtToolsIoError::Markers(SlotMarkersError::SliceCount { value: 100 }).to_string()
198        );
199        Ok(())
200    }
201
202    #[test]
203    fn invalid_loop_point() -> Result<(), OtToolsIoError> {
204        let path = get_blank_proj_dirpath().join("markers.work");
205        let mut x = MarkersFile::from_data_file(&path)?;
206        x.flex_slots[0].loop_point = 100;
207        assert_eq!(
208            x.validate().unwrap_err().to_string(),
209            OtToolsIoError::Markers(SlotMarkersError::Loop { value: 100 }).to_string()
210        );
211        Ok(())
212    }
213}
214
215impl Default for MarkersFile {
216    fn default() -> Self {
217        Self::new(
218            PlaybackSlots::<SlotMarkers>::default(),
219            RecordingBufferSlots::<SlotMarkers>::default(),
220            PlaybackSlots::<SlotMarkers>::default(),
221        )
222        .unwrap()
223    }
224}
225
226impl SwapBytes for MarkersFile {
227    fn swap_bytes(self) -> Self {
228        let mut flex_slots = self.flex_slots.clone();
229        #[allow(clippy::clone_on_copy)] // pretty sure we want a deep copy here
230        for (i, slot) in self.flex_slots.iter().enumerate() {
231            flex_slots[i] = slot.clone().swap_bytes();
232        }
233
234        let mut static_slots = self.static_slots.clone();
235        #[allow(clippy::clone_on_copy)] // pretty sure we want a deep copy here
236        for (i, slot) in self.static_slots.iter().enumerate() {
237            static_slots[i] = slot.clone().swap_bytes();
238        }
239
240        let mut recording_buffers = self.recording_buffers;
241        #[allow(clippy::clone_on_copy)] // pretty sure we want a deep copy here
242        for (i, slot) in self.recording_buffers.iter().enumerate() {
243            recording_buffers[i] = slot.clone().swap_bytes();
244        }
245
246        Self {
247            header: self.header,
248            datatype_version: self.datatype_version,
249            flex_slots,
250            recording_buffers,
251            static_slots,
252            checksum: self.checksum.swap_bytes(),
253        }
254    }
255}
256
257impl OctatrackFileIO for MarkersFile {
258    fn to_bytes(&self) -> Result<Vec<u8>, OtToolsIoError>
259    where
260        Self: Serialize,
261    {
262        let mut chkd = self.clone();
263        chkd.checksum = self.calculate_checksum()?;
264        let encoded = if cfg!(target_endian = "little") {
265            bincode::serialize(&chkd.swap_bytes())?
266        } else {
267            bincode::serialize(&chkd)?
268        };
269        Ok(encoded)
270    }
271
272    fn from_bytes(bytes: &[u8]) -> Result<Self, OtToolsIoError>
273    where
274        Self: Sized,
275        Self: for<'a> Deserialize<'a>,
276    {
277        let mut x: Self = bincode::deserialize(bytes)?;
278        #[cfg(target_endian = "little")]
279        {
280            x = x.swap_bytes();
281        }
282
283        Ok(x)
284    }
285}
286
287#[cfg(test)]
288mod from_bytes {
289    use crate::{
290        read_bin_file, test_utils::get_blank_proj_dirpath, MarkersFile, OctatrackFileIO,
291        OtToolsIoError,
292    };
293    #[test]
294    fn valid() -> Result<(), OtToolsIoError> {
295        let path = get_blank_proj_dirpath().join("markers.work");
296        let bytes = read_bin_file(&path)?;
297        let s = MarkersFile::from_bytes(&bytes)?;
298        assert_eq!(s, MarkersFile::default());
299        Ok(())
300    }
301}
302
303#[cfg(test)]
304mod to_bytes {
305    use crate::{
306        read_bin_file, test_utils::get_blank_proj_dirpath, MarkersFile, OctatrackFileIO,
307        OtToolsIoError,
308    };
309    #[test]
310    fn valid() -> Result<(), OtToolsIoError> {
311        let path = get_blank_proj_dirpath().join("markers.work");
312        let bytes = read_bin_file(&path)?;
313        let b = MarkersFile::default().to_bytes()?;
314        assert_eq!(b, bytes);
315        Ok(())
316    }
317}
318
319impl HasChecksumField for MarkersFile {
320    fn calculate_checksum(&self) -> Result<u16, OtToolsIoError> {
321        let bytes = bincode::serialize(self)?;
322        let mut chk: u16 = 0;
323        for byte in &bytes[16..bytes.len() - 2] {
324            chk = chk.wrapping_add(*byte as u16);
325        }
326        Ok(chk)
327    }
328
329    fn update_checksum(&mut self) -> Result<(), OtToolsIoError> {
330        self.checksum = self.calculate_checksum()?;
331        Ok(())
332    }
333
334    fn check_checksum(&self) -> Result<bool, OtToolsIoError> {
335        Ok(self.checksum == self.calculate_checksum()?)
336    }
337}
338
339#[cfg(test)]
340mod checksum_field {
341    use crate::{HasChecksumField, MarkersFile, OtToolsIoError};
342    #[test]
343    fn valid() -> Result<(), OtToolsIoError> {
344        let mut x = MarkersFile::default();
345        x.checksum = x.calculate_checksum()?;
346        assert!(x.check_checksum()?);
347        Ok(())
348    }
349
350    #[test]
351    fn invalid() -> Result<(), OtToolsIoError> {
352        let x = MarkersFile {
353            checksum: u16::MAX,
354            ..Default::default()
355        };
356        assert!(!x.check_checksum()?);
357        Ok(())
358    }
359
360    mod files {
361        use crate::test_utils::{get_blank_proj_dirpath, get_markers_dirpath};
362        use crate::{HasChecksumField, MarkersFile, OctatrackFileIO, OtToolsIoError};
363        use std::path::Path;
364
365        fn helper_test_chksum(fp: &Path) -> Result<(u16, u16), OtToolsIoError> {
366            let valid = MarkersFile::from_data_file(fp)?;
367            let mut test = valid.clone();
368            test.checksum = 0;
369            let chk = test.calculate_checksum()?;
370            Ok((chk, valid.checksum))
371        }
372
373        #[allow(clippy::field_reassign_with_default)]
374        #[test]
375        fn default_method() -> Result<(), OtToolsIoError> {
376            let (_, valid) = helper_test_chksum(&get_blank_proj_dirpath().join("markers.work"))?;
377            let mut x = MarkersFile::default();
378            x.checksum = 0;
379            let test = x.calculate_checksum()?;
380            assert_eq!(test, valid);
381            Ok(())
382        }
383
384        #[test]
385        fn base_proj_default_file() -> Result<(), OtToolsIoError> {
386            let (test, valid) = helper_test_chksum(&get_blank_proj_dirpath().join("markers.work"))?;
387            assert_eq!(test, valid);
388            Ok(())
389        }
390
391        #[test]
392        fn flex_slot_noedit() -> Result<(), OtToolsIoError> {
393            let (test, valid) =
394                helper_test_chksum(&get_markers_dirpath().join("flex-slot-1-noedit.work"))?;
395            assert_eq!(test, valid);
396            Ok(())
397        }
398
399        #[test]
400        fn flex_slot_1_loop() -> Result<(), OtToolsIoError> {
401            let (test, valid) =
402                helper_test_chksum(&get_markers_dirpath().join("flex-slot-1-loop-edit.work"))?;
403            assert_eq!(test, valid);
404            Ok(())
405        }
406
407        #[test]
408        fn flex_slot_1_slice_1_loop() -> Result<(), OtToolsIoError> {
409            let (test, valid) =
410                helper_test_chksum(&get_markers_dirpath().join("flex-slot-1-slice-1-looped.work"))?;
411            assert_eq!(test, valid);
412            Ok(())
413        }
414
415        #[test]
416        fn flex_slot_1_slice_1_noloop() -> Result<(), OtToolsIoError> {
417            let (test, valid) =
418                helper_test_chksum(&get_markers_dirpath().join("flex-slot-1-slice-1-noloop.work"))?;
419            assert_eq!(test, valid);
420            Ok(())
421        }
422
423        #[test]
424        fn flex_slot_1_slice_4_noloop() -> Result<(), OtToolsIoError> {
425            let (test, valid) =
426                helper_test_chksum(&get_markers_dirpath().join("flex-slot-1-slice-4-noloop.work"))?;
427            assert_eq!(test, valid);
428            Ok(())
429        }
430
431        #[test]
432        fn flex_slot_1_start_edit() -> Result<(), OtToolsIoError> {
433            let (test, valid) =
434                helper_test_chksum(&get_markers_dirpath().join("flex-slot-1-start-edit.work"))?;
435            assert_eq!(test, valid);
436            Ok(())
437        }
438
439        #[test]
440        fn flex_slot_128_noedit() -> Result<(), OtToolsIoError> {
441            let (test, valid) =
442                helper_test_chksum(&get_markers_dirpath().join("flex-slot-128-noedit.work"))?;
443            assert_eq!(test, valid);
444            Ok(())
445        }
446
447        #[test]
448        fn recorder_slot_1_noedit() -> Result<(), OtToolsIoError> {
449            let (test, valid) =
450                helper_test_chksum(&get_markers_dirpath().join("recorder-slot-1-noedit.work"))?;
451            assert_eq!(test, valid);
452            Ok(())
453        }
454
455        #[test]
456        fn static_slot_1_noedit() -> Result<(), OtToolsIoError> {
457            let (test, valid) =
458                helper_test_chksum(&get_markers_dirpath().join("static-slot-1-noedit.work"))?;
459            assert_eq!(test, valid);
460            Ok(())
461        }
462
463        #[test]
464        fn static_slot_128_noedit() -> Result<(), OtToolsIoError> {
465            let (test, valid) =
466                helper_test_chksum(&get_markers_dirpath().join("static-slot-128-noedit.work"))?;
467            assert_eq!(test, valid);
468            Ok(())
469        }
470    }
471}
472
473impl HasHeaderField for MarkersFile {
474    fn check_header(&self) -> Result<bool, OtToolsIoError> {
475        Ok(self.header == MARKERS_HEADER)
476    }
477}
478
479#[cfg(test)]
480mod header_field {
481    use crate::{HasHeaderField, MarkersFile, OtToolsIoError};
482    #[test]
483    fn valid() -> Result<(), OtToolsIoError> {
484        assert!(MarkersFile::default().check_header()?);
485        Ok(())
486    }
487
488    #[test]
489    fn invalid() -> Result<(), OtToolsIoError> {
490        let mut mutated = MarkersFile::default();
491        mutated.header[0] = 0x00;
492        mutated.header[20] = 111;
493        assert!(!mutated.check_header()?);
494        Ok(())
495    }
496}
497
498impl HasFileVersionField for MarkersFile {
499    fn check_file_version(&self) -> Result<bool, OtToolsIoError> {
500        Ok(self.datatype_version == MARKERS_FILE_VERSION)
501    }
502}
503
504#[cfg(test)]
505mod file_version_field {
506    use crate::{HasFileVersionField, MarkersFile, OtToolsIoError};
507    #[test]
508    fn valid() -> Result<(), OtToolsIoError> {
509        assert!(MarkersFile::default().check_file_version()?);
510        Ok(())
511    }
512
513    #[test]
514    fn invalid() -> Result<(), OtToolsIoError> {
515        let mut mutated = MarkersFile {
516            datatype_version: 0,
517            ..Default::default()
518        };
519        mutated.datatype_version = 0;
520        assert!(!mutated.check_file_version()?);
521        Ok(())
522    }
523}