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
20use crate::samples::slices::{Slice, SLICE_LOOP_POINT_DEFAULT};
21use crate::samples::SwapBytes;
22use crate::{
23    CalculateChecksum, CheckChecksum, CheckHeader, CheckIntegrity, Decode, Encode, IsDefault,
24    OtToolsIoErrors, RBoxErr,
25};
26use ot_tools_io_derive::Decodeable;
27use serde::{Deserialize, Serialize};
28use serde_big_array::{Array, BigArray};
29use std::array::from_fn;
30
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/// Header data for the markers file
42pub const MARKERS_HEADER: [u8; 21] = [
43    0x46, 0x4f, 0x52, 0x4d, 0x00, 0x00, 0x00, 0x00, 0x44, 0x50, 0x53, 0x31, 0x53, 0x41, 0x4d, 0x50,
44    0x00, 0x00, 0x00, 0x00, 0x00,
45];
46
47/// Current/supported version of markers files.
48pub const MARKERS_FILE_VERSION: u8 = 4;
49
50/// The model for a 'slot' within a [MarkersFile].
51/// Slot settings like quantization are stored in the [crate::projects::slots::ProjectSampleSlot] type
52#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Decodeable)]
53pub struct SlotPlayback {
54    /// main sample trim offset (start)
55    pub trim_offset: u32,
56
57    /// main sample trim end
58    pub trim_end: u32,
59
60    /// main sample loop point
61    pub loop_point: u32,
62
63    /// slices array for slice position editing
64    #[serde(with = "BigArray")]
65    pub slices: [Slice; 64],
66
67    /// number of slices
68    pub slice_count: u32,
69}
70
71impl SlotPlayback {
72    fn validate(&self) -> RBoxErr<bool> {
73        for slice in self.slices.iter() {
74            slice.validate()?;
75        }
76        let trim_ok = self.trim_offset < self.trim_end;
77        let slice_count_ok = self.slice_count == self.slices.len() as u32;
78        let loop_start_ok = self.loop_point >= self.trim_offset;
79        let loop_end_ok =
80            self.loop_point <= self.trim_end || self.loop_point == SLICE_LOOP_POINT_DEFAULT;
81
82        if !(trim_ok && slice_count_ok && loop_start_ok && loop_end_ok) {
83            return Err(OtToolsIoErrors::TodoError.into());
84        }
85
86        Ok(true)
87    }
88}
89
90impl SwapBytes for SlotPlayback {
91    type T = SlotPlayback;
92    fn swap_bytes(self) -> RBoxErr<Self::T> {
93        let mut slices: [Slice; 64] = self.slices;
94
95        for (i, slice) in self.slices.iter().enumerate() {
96            slices[i] = slice.swap_bytes()?;
97        }
98
99        let bswapped = Self {
100            trim_offset: self.trim_offset.swap_bytes(),
101            trim_end: self.trim_end.swap_bytes(),
102            loop_point: self.loop_point.swap_bytes(),
103            slices,
104            slice_count: self.slice_count.swap_bytes(),
105        };
106
107        Ok(bswapped)
108    }
109}
110
111impl Default for SlotPlayback {
112    fn default() -> Self {
113        Self {
114            trim_offset: 0,
115            trim_end: 0,
116            loop_point: 0,
117            slices: from_fn(|_| Slice::default()),
118            slice_count: 0,
119        }
120    }
121}
122
123/// The 'markers' file. Contains sample editor data for all slots in a project.
124#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
125pub struct MarkersFile {
126    #[serde(with = "BigArray")]
127    pub header: [u8; 21],
128
129    /// version of data file. used in OS upgrades for patching files (and checks
130    /// performed on files during loading of a project).
131    ///
132    /// ### background / context
133    ///
134    /// Got this error on the device when i messed up the default markers file
135    /// when i didn't include the `4` (current value for 1.40B).
136    /// ```text
137    /// >>> 2025-04-24 22:06:00 ERROR Couldn't read from markers file ...
138    /// >>> '/test-set-bankcopy/PROJECT-BLANK/markers.work' ('INVALID FILESIZE')
139    /// ```
140    ///
141    /// Additionally, Banks for projects created with 1.40X have a 'version'
142    /// number of 23, while the LESSSELF/V1 project started with 1.25E has a
143    /// version of 15.
144    ///
145    /// So yep. These weird numbers are version numbers for the data types /
146    /// structures / files.
147    pub datatype_version: u8,
148
149    /// audio editor slots for flex slots
150    pub flex_slots: Box<Array<SlotPlayback, 128>>,
151
152    /// 8x dedicated flex recording tracks
153    pub recorder_slots: Box<Array<SlotPlayback, 8>>,
154
155    /// audio editor slots for static slots
156    pub static_slots: Box<Array<SlotPlayback, 128>>,
157
158    // i'm assuming a u16 checksum here as well?
159    pub checksum: u16,
160}
161
162impl MarkersFile {
163    #[allow(dead_code)]
164    fn validate(&self) -> RBoxErr<bool> {
165        for slot in self.flex_slots.iter() {
166            slot.validate()?;
167        }
168        for slot in self.recorder_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    type T = MarkersFile;
181    fn swap_bytes(self) -> RBoxErr<Self::T> {
182        let mut flex_slots = self.flex_slots.clone();
183        for (i, slot) in self.flex_slots.iter().enumerate() {
184            flex_slots[i] = slot.clone().swap_bytes()?;
185        }
186
187        let mut recorder_slots = self.recorder_slots.clone();
188        for (i, slot) in self.recorder_slots.iter().enumerate() {
189            recorder_slots[i] = slot.clone().swap_bytes()?;
190        }
191
192        let mut static_slots = self.static_slots.clone();
193        for (i, slot) in self.static_slots.iter().enumerate() {
194            static_slots[i] = slot.clone().swap_bytes()?;
195        }
196
197        let bswapped = Self {
198            header: self.header,
199            datatype_version: self.datatype_version,
200            flex_slots,
201            recorder_slots,
202            static_slots,
203            checksum: self.checksum.swap_bytes(),
204        };
205
206        Ok(bswapped)
207    }
208}
209
210impl Decode for MarkersFile {
211    fn decode(bytes: &[u8]) -> RBoxErr<Self>
212    where
213        Self: Sized,
214        Self: for<'a> Deserialize<'a>,
215    {
216        let mut x: Self = bincode::deserialize(bytes)?;
217        #[cfg(target_endian = "little")]
218        {
219            x = x.swap_bytes()?;
220        }
221
222        Ok(x)
223    }
224}
225
226impl Encode for MarkersFile {
227    fn encode(&self) -> RBoxErr<Vec<u8>> {
228        let bytes = bincode::serialize(self)?;
229        Ok(bytes)
230    }
231}
232
233impl MarkersFile {
234    fn new(
235        flex_slots: [SlotPlayback; 128],
236        recorder_slots: [SlotPlayback; 8],
237        static_slots: [SlotPlayback; 128],
238    ) -> RBoxErr<Self> {
239        let mut init = Self {
240            header: MARKERS_HEADER,
241            datatype_version: MARKERS_FILE_VERSION,
242            flex_slots: Array(flex_slots).into(),
243            recorder_slots: Array(recorder_slots).into(),
244            static_slots: Array(static_slots).into(),
245            checksum: 0,
246        };
247
248        init.checksum = init.calculate_checksum()?;
249        Ok(init)
250    }
251}
252
253impl Default for MarkersFile {
254    fn default() -> Self {
255        Self::new(
256            from_fn(|_| SlotPlayback::default()),
257            from_fn(|_| SlotPlayback::default()),
258            from_fn(|_| SlotPlayback::default()),
259        )
260        .unwrap()
261    }
262}
263
264impl IsDefault for MarkersFile {
265    fn is_default(&self) -> bool {
266        let default = &mut MarkersFile::default();
267        default == self
268    }
269}
270
271impl CalculateChecksum for MarkersFile {
272    fn calculate_checksum(&self) -> RBoxErr<u16> {
273        let bytes = self.encode()?;
274        let mut chk: u16 = 0;
275        for byte in &bytes[16..bytes.len() - 2] {
276            chk = chk.wrapping_add(*byte as u16);
277        }
278        Ok(chk)
279    }
280}
281
282impl CheckHeader for MarkersFile {
283    fn check_header(&self) -> bool {
284        self.header == MARKERS_HEADER
285    }
286}
287
288impl CheckChecksum for MarkersFile {
289    fn check_checksum(&self) -> RBoxErr<bool> {
290        let calculated = self.calculate_checksum()?;
291        if self.checksum != calculated {
292            eprintln!(
293                "Markers file: Invalid checksum: current={} calcuated={}",
294                self.checksum, calculated,
295            );
296            Err(OtToolsIoErrors::TodoError.into())
297        } else {
298            Ok(true)
299        }
300    }
301}
302
303impl CheckIntegrity for MarkersFile {}
304
305#[derive(Debug, Serialize, Deserialize, Decodeable)]
306pub struct MarkersRawBytes {
307    pub data: Box<Array<u8, 0x00032898>>,
308}
309
310#[cfg(test)]
311mod tests {
312    mod can_read_generated_files {
313        use crate::markers::MarkersFile;
314        use crate::samples::slices::Slice;
315        use crate::test_utils::get_markers_dirpath;
316        use crate::{read_type_from_bin_file, CalculateChecksum};
317
318        const SAMPLE_LEN: u32 = 2679;
319
320        fn test_helper(fname: &str) -> MarkersFile {
321            let fpath = get_markers_dirpath().join(fname);
322            let r = read_type_from_bin_file::<MarkersFile>(&fpath);
323
324            println!("result: {r:?}");
325            assert!(r.is_ok());
326            r.unwrap()
327        }
328
329        fn assert_helper(m: MarkersFile, mut v: MarkersFile) {
330            v.checksum = v.calculate_checksum().unwrap();
331            println!("header");
332            assert_eq!(m.header, v.header);
333            println!("datatype version");
334            assert_eq!(m.datatype_version, v.datatype_version);
335            println!("recorder slots");
336            assert_eq!(m.recorder_slots, v.recorder_slots);
337            println!("flex slots");
338            assert_eq!(m.flex_slots, v.flex_slots);
339            println!("static slots");
340            assert_eq!(m.static_slots, v.static_slots);
341            println!("checksum slots");
342            assert_eq!(m.checksum, v.checksum);
343        }
344
345        #[test]
346        fn flex_slot_1_noedit() {
347            let m = test_helper("flex-slot-1-noedit.work");
348            let mut valid = MarkersFile::default();
349            valid.flex_slots[0].trim_end = SAMPLE_LEN;
350            assert_helper(m, valid);
351        }
352
353        #[test]
354        fn flex_slot_128_noedit() {
355            let m = test_helper("flex-slot-128-noedit.work");
356            let mut valid = MarkersFile::default();
357            valid.flex_slots[127].trim_end = SAMPLE_LEN;
358            assert_helper(m, valid);
359        }
360
361        #[test]
362        fn static_slot_1_noedit() {
363            let m = test_helper("static-slot-1-noedit.work");
364            let mut valid = MarkersFile::default();
365            // FIXME: I edited the recorder slot, then cleared the sample association
366            //        and moved onto statics... but this didn't ACTUALLY CLEAR the
367            //        recording slot data -- recording slots persist!
368            valid.recorder_slots[0].trim_end = SAMPLE_LEN;
369            valid.static_slots[0].trim_end = SAMPLE_LEN;
370            assert_helper(m, valid);
371        }
372
373        #[test]
374        fn static_slot_128_noedit() {
375            let m = test_helper("static-slot-128-noedit.work");
376            let mut valid = MarkersFile::default();
377            // FIXME: I edited the recorder slot, then cleared the sample association
378            //        and moved onto statics... but this didn't ACTUALLY CLEAR the
379            //        recording slot data -- recording slots persist!
380            valid.recorder_slots[0].trim_end = SAMPLE_LEN;
381            valid.static_slots[127].trim_end = SAMPLE_LEN;
382            assert_helper(m, valid);
383        }
384
385        #[test]
386        fn recorder_slot_1_noedit() {
387            let m = test_helper("recorder-slot-1-noedit.work");
388            let mut valid = MarkersFile::default();
389            valid.recorder_slots[0].trim_end = SAMPLE_LEN;
390            assert_helper(m, valid);
391        }
392
393        #[test]
394        fn flex_slot_1_loop_edit() {
395            let m = test_helper("flex-slot-1-loop-edit.work");
396            let mut valid = MarkersFile::default();
397            valid.flex_slots[0].trim_end = SAMPLE_LEN;
398            valid.flex_slots[0].loop_point = 1456;
399            assert_helper(m, valid);
400        }
401
402        #[test]
403        fn flex_slot_1_start_edit() {
404            let m = test_helper("flex-slot-1-start-edit.work");
405            let mut valid = MarkersFile::default();
406            valid.flex_slots[0].trim_end = SAMPLE_LEN;
407            valid.flex_slots[0].trim_offset = 1456;
408            // TODO: Huh?
409            valid.flex_slots[0].loop_point = 1456;
410            assert_helper(m, valid);
411        }
412
413        // didn't get this one?
414        #[ignore]
415        #[test]
416        fn flex_slot_1_end_edit() {
417            let m = test_helper("flex-slot-1-start-edit.work");
418            let mut valid = MarkersFile::default();
419            valid.flex_slots[0].trim_end = SAMPLE_LEN;
420            assert_helper(m, valid);
421        }
422
423        #[test]
424        fn flex_slot_1_slice_1_noloop() {
425            let m = test_helper("flex-slot-1-slice-1-noloop.work");
426            let mut valid = MarkersFile::default();
427            valid.flex_slots[0].trim_end = SAMPLE_LEN;
428            valid.flex_slots[0].slice_count = 1;
429            valid.flex_slots[0].slices[0] = Slice {
430                trim_start: 0,
431                trim_end: 416,
432                loop_start: 0xFFFFFFFF,
433            };
434            assert_helper(m, valid);
435        }
436
437        #[test]
438        fn flex_slot_1_slice_1_looped() {
439            let m = test_helper("flex-slot-1-slice-1-looped.work");
440            let mut valid = MarkersFile::default();
441            valid.flex_slots[0].trim_end = SAMPLE_LEN;
442            valid.flex_slots[0].slice_count = 1;
443            valid.flex_slots[0].slices[0] = Slice {
444                trim_start: 0,
445                trim_end: 416,
446                loop_start: 202,
447            };
448            assert_helper(m, valid);
449        }
450
451        // initial linear slice grid
452        #[test]
453        fn flex_slot_1_slice_4_noloop() {
454            let m = test_helper("flex-slot-1-slice-4-noloop.work");
455            let mut valid = MarkersFile::default();
456            valid.flex_slots[0].trim_end = SAMPLE_LEN;
457            valid.flex_slots[0].slice_count = 4;
458            valid.flex_slots[0].slices[0] = Slice {
459                trim_start: 0,
460                trim_end: 668,
461                loop_start: 0xFFFFFFFF,
462            };
463            valid.flex_slots[0].slices[1] = Slice {
464                trim_start: 668,
465                trim_end: 1338,
466                loop_start: 0xFFFFFFFF,
467            };
468            valid.flex_slots[0].slices[2] = Slice {
469                trim_start: 1338,
470                trim_end: 2008,
471                loop_start: 0xFFFFFFFF,
472            };
473            valid.flex_slots[0].slices[3] = Slice {
474                trim_start: 2008,
475                trim_end: SAMPLE_LEN,
476                loop_start: 0xFFFFFFFF,
477            };
478            assert_helper(m, valid);
479        }
480    }
481}