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, SLICE_LOOP_POINT_DEFAULT};
22use crate::{
23    CalculateChecksum, CheckChecksum, CheckFileVersion, CheckHeader, Decode, Encode, IsDefault,
24    OtToolsIoErrors, RBoxErr, SwapBytes,
25};
26use ot_tools_io_derive::{Decodeable, IntegrityChecks, OctatrackFile};
27use serde::{Deserialize, Serialize};
28use serde_big_array::{Array, BigArray};
29use std::array::from_fn;
30/*
31# ===== DEFAULT DATA FILE ===== #
3200000000  46 4f 52 4d 00 00 00 00  44 50 53 31 53 41 4d 50  |FORM....DPS1SAMP|
3300000010  00 00 00 00 00 04 00 00  00 00 00 00 00 00 00 00  |................|
3400000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
35# ***** REPEATS *****
3600032890  00 00 00 00 00 00 00 04                           |........|
3700032898
38*/
39
40/// Header data for the markers file
41pub const MARKERS_HEADER: [u8; 21] = [
42    0x46, 0x4f, 0x52, 0x4d, 0x00, 0x00, 0x00, 0x00, 0x44, 0x50, 0x53, 0x31, 0x53, 0x41, 0x4d, 0x50,
43    0x00, 0x00, 0x00, 0x00, 0x00,
44];
45
46/// Current/supported version of markers files.
47pub const MARKERS_FILE_VERSION: u8 = 4;
48
49/// Slot playback trigger points/markers. Generally, data related to trim, looping and slices for each slot.
50/// Global slot settings like quantization/time-stretch are stored in the [`crate::projects::slots::SlotAttributes`] type
51///
52/// NOTE: On the naming for this -- the Octatrack manual specifically refers to
53/// > SAVE SAMPLE SETTINGS will save the trim, slice and attribute settings in a
54/// > separate file and link it to the sample currently being edited.
55/// > -- page 87
56///
57/// So ... these are the Slot MARKERS (trim/loop/slice markers) which are saved
58/// to a settings file.
59#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Decodeable, Hash)]
60pub struct SlotMarkers {
61    /// main sample trim offset (start)
62    pub trim_offset: u32,
63
64    /// main sample trim end
65    pub trim_end: u32,
66
67    /// main sample loop point
68    pub loop_point: u32,
69
70    /// slices array for slice position editing
71    #[serde(with = "BigArray")]
72    pub slices: [Slice; 64],
73
74    /// number of slices
75    pub slice_count: u32,
76}
77
78impl SlotMarkers {
79    fn validate(&self) -> RBoxErr<bool> {
80        for slice in self.slices.iter() {
81            slice.validate()?;
82        }
83        let trim_ok = self.trim_offset < self.trim_end;
84        let slice_count_ok = self.slice_count == self.slices.len() as u32;
85        let loop_start_ok = self.loop_point >= self.trim_offset;
86        let loop_end_ok =
87            self.loop_point <= self.trim_end || self.loop_point == SLICE_LOOP_POINT_DEFAULT;
88
89        if !(trim_ok && slice_count_ok && loop_start_ok && loop_end_ok) {
90            return Err(OtToolsIoErrors::TodoError.into());
91        }
92
93        Ok(true)
94    }
95}
96
97impl SwapBytes for SlotMarkers {
98    fn swap_bytes(self) -> RBoxErr<Self> {
99        let mut slices: [Slice; 64] = self.slices;
100
101        for (i, slice) in self.slices.iter().enumerate() {
102            slices[i] = slice.swap_bytes()?;
103        }
104
105        let bswapped = Self {
106            trim_offset: self.trim_offset.swap_bytes(),
107            trim_end: self.trim_end.swap_bytes(),
108            loop_point: self.loop_point.swap_bytes(),
109            slices,
110            slice_count: self.slice_count.swap_bytes(),
111        };
112
113        Ok(bswapped)
114    }
115}
116
117impl Default for SlotMarkers {
118    fn default() -> Self {
119        Self {
120            trim_offset: 0,
121            trim_end: 0,
122            loop_point: 0,
123            slices: from_fn(|_| Slice::default()),
124            slice_count: 0,
125        }
126    }
127}
128
129/// The 'markers' file. Contains sample editor data for all slots in a project.
130#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, OctatrackFile, IntegrityChecks)]
131pub struct MarkersFile {
132    #[serde(with = "BigArray")]
133    pub header: [u8; 21],
134
135    /// version of data file. used in OS upgrades for patching files (and checks
136    /// performed on files during loading of a project).
137    ///
138    /// ### background / context
139    ///
140    /// Got this error on the device when i messed up the default markers file
141    /// when i didn't include the `4` (current value for 1.40B).
142    /// ```text
143    /// >>> 2025-04-24 22:06:00 ERROR Couldn't read from markers file ...
144    /// >>> '/test-set-bankcopy/PROJECT-BLANK/markers.work' ('INVALID FILESIZE')
145    /// ```
146    ///
147    /// Additionally, Banks for projects created with 1.40X have a 'version'
148    /// number of 23, while the LESSSELF/V1 project started with 1.25E has a
149    /// version of 15.
150    ///
151    /// So yep. These weird numbers are version numbers for the data types /
152    /// structures / files.
153    pub datatype_version: u8,
154
155    /// flex slots playback data
156    pub flex_slots: Box<Array<SlotMarkers, 136>>,
157
158    /// static slots playback data
159    pub static_slots: Box<Array<SlotMarkers, 128>>,
160
161    // i'm assuming u16 checksum here as well?
162    pub checksum: u16,
163}
164
165impl MarkersFile {
166    #[allow(dead_code)]
167    fn validate(&self) -> RBoxErr<bool> {
168        for slot in self.flex_slots.iter() {
169            slot.validate()?;
170        }
171        for slot in self.static_slots.iter() {
172            slot.validate()?;
173        }
174
175        Ok(true)
176    }
177}
178
179impl SwapBytes for MarkersFile {
180    fn swap_bytes(self) -> RBoxErr<Self> {
181        let mut flex_slots = self.flex_slots.clone();
182        for (i, slot) in self.flex_slots.iter().enumerate() {
183            flex_slots[i] = slot.clone().swap_bytes()?;
184        }
185
186        let mut static_slots = self.static_slots.clone();
187        for (i, slot) in self.static_slots.iter().enumerate() {
188            static_slots[i] = slot.clone().swap_bytes()?;
189        }
190
191        let bswapped = Self {
192            header: self.header,
193            datatype_version: self.datatype_version,
194            flex_slots,
195            static_slots,
196            checksum: self.checksum.swap_bytes(),
197        };
198
199        Ok(bswapped)
200    }
201}
202
203impl Decode for MarkersFile {
204    fn decode(bytes: &[u8]) -> RBoxErr<Self>
205    where
206        Self: Sized,
207        Self: for<'a> Deserialize<'a>,
208    {
209        let mut x: Self = bincode::deserialize(bytes)?;
210        #[cfg(target_endian = "little")]
211        {
212            x = x.swap_bytes()?;
213        }
214
215        Ok(x)
216    }
217}
218
219impl Encode for MarkersFile {
220    fn encode(&self) -> RBoxErr<Vec<u8>> {
221        let mut chkd = self.clone();
222        chkd.checksum = self.calculate_checksum()?;
223        let encoded = if cfg!(target_endian = "little") {
224            bincode::serialize(&chkd.swap_bytes()?)?
225        } else {
226            bincode::serialize(&chkd)?
227        };
228        Ok(encoded)
229    }
230}
231
232impl MarkersFile {
233    fn new(flex_slots: [SlotMarkers; 136], static_slots: [SlotMarkers; 128]) -> RBoxErr<Self> {
234        let mut init = Self {
235            header: MARKERS_HEADER,
236            datatype_version: MARKERS_FILE_VERSION,
237            flex_slots: Array(flex_slots).into(),
238            static_slots: Array(static_slots).into(),
239            checksum: 0,
240        };
241
242        init.checksum = init.calculate_checksum()?;
243        Ok(init)
244    }
245}
246
247impl Default for MarkersFile {
248    fn default() -> Self {
249        Self::new(
250            from_fn(|_| SlotMarkers::default()),
251            from_fn(|_| SlotMarkers::default()),
252        )
253        .unwrap()
254    }
255}
256
257impl IsDefault for MarkersFile {
258    fn is_default(&self) -> bool {
259        let default = &mut MarkersFile::default();
260        default == self
261    }
262}
263
264impl CalculateChecksum for MarkersFile {
265    fn calculate_checksum(&self) -> RBoxErr<u16> {
266        let bytes = bincode::serialize(self)?;
267        let mut chk: u16 = 0;
268        for byte in &bytes[16..bytes.len() - 2] {
269            chk = chk.wrapping_add(*byte as u16);
270        }
271        Ok(chk)
272    }
273}
274
275impl CheckHeader for MarkersFile {
276    fn check_header(&self) -> bool {
277        self.header == MARKERS_HEADER
278    }
279}
280
281impl CheckChecksum for MarkersFile {
282    fn check_checksum(&self) -> RBoxErr<bool> {
283        Ok(self.checksum == self.calculate_checksum()?)
284    }
285}
286
287impl CheckFileVersion for MarkersFile {
288    fn check_file_version(&self) -> RBoxErr<bool> {
289        Ok(self.datatype_version == MARKERS_FILE_VERSION)
290    }
291}