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