ot_tools_io/
arrangements.rs

1/*
2SPDX-License-Identifier: GPL-3.0-or-later
3Copyright © 2024 Mike Robeson [dijksterhuis]
4*/
5
6//! Types and ser/de for `arr??.*` binary data files.
7//!
8//! Proper checksum calcuations are not yet implemented. But this doesn't seem
9//! to have an impact on loading arrangements onto the Octatrack.
10
11mod deserialize;
12mod serialize;
13
14use crate::{
15    CalculateChecksum, CheckChecksum, CheckHeader, CheckIntegrity, DefaultsArrayBoxed, Encode,
16    IsDefault, RBoxErr,
17};
18use ot_tools_io_derive::{Decodeable, DefaultsAsBoxedBigArray};
19use serde::{Deserialize, Serialize};
20use serde_big_array::{Array, BigArray};
21use std::array::from_fn;
22
23/// Current file header data
24pub const ARRANGEMENT_FILE_HEADER: [u8; 21] = [
25    70, 79, 82, 77, 0, 0, 0, 0, 68, 80, 83, 49, 65, 82, 82, 65, 0, 0, 0, 0, 0,
26];
27
28/// Current/supported version of arrangements files.
29pub const ARRANGEMENT_FILE_VERSION: u8 = 6;
30
31/// `"OT_TOOLS_ARR` -- this is a custom name specifically created for ot-tools.
32/// The octatrack will normally copy the name of a previously created arrangement
33/// when creating arrangements on project creation. Not sure why, but it means
34/// arrangements never have a single default name.
35const ARRANGEMENT_DEFAULT_NAME: [u8; 15] =
36    [79, 67, 84, 65, 84, 79, 79, 76, 83, 45, 65, 82, 82, 32, 32];
37
38// max length: 11336 bytes
39/// Base model for `arr??.*` arrangement binary data files.
40#[derive(Debug, Clone, Serialize, PartialEq, Decodeable)]
41pub struct ArrangementFile {
42    /// Header data:
43    /// ```text
44    /// ASCII: FORM....DPS1ARRA........
45    /// Hex: 46 4f 52 4d 00 00 00 00 44 50 53 31 41 52 52 41 00 00 00 00 00 06
46    /// U8: [70, 79, 82, 77, 0, 0, 0, 0, 68, 80, 83, 49, 65, 82, 82, 65, 0, 0, 0, 0, 0, 6]
47    /// ```
48    pub header: [u8; 21],
49
50    /// Patch version of this data type. Is inspected on proejct load to determine if
51    /// the octatrack is able to load a project from current OS, or whether the project
52    /// files need to be patched and updated.
53    pub datatype_version: u8,
54
55    /// Dunno. Example data:
56    /// ```text
57    /// [0, 0]
58    /// ```
59    pub unk1: [u8; 2],
60
61    /// Current arrangement data in active use.
62    /// This block is written when saving via Project Menu -> SYNC TO CARD.
63    ///
64    /// The second block is written when saving the arrangement via Arranger Menu -> SAVE.
65    // #[serde(with = "BigArray")]
66    pub arrangement_state_current: ArrangementBlock,
67
68    /// Dunno.
69    pub unk2: u8,
70    /// Whether the arrangement has been saved within the arrangement menu (*not* whether project has been saved)
71    pub saved_flag: u8,
72
73    /// Arrangement data from the previous saved state.
74    /// This block is written when saving the arrangement via Arranger Menu -> SAVE.
75    pub arrangement_state_previous: ArrangementBlock,
76    /// The current save/unsaved state of all loaded arrangements.
77    /// 'Saved' means there's been at least one ARRANGER MENU save operation performed,
78    /// and `ArrangementBlock` data has been written to the `arrangement_state_previous` field.
79    ///
80    /// Example data:
81    /// ```text
82    /// Arrangement 1 has been saved: [1, 0, 0, 0, 0, 0, 0, 0]
83    /// Arrangement 2 has been saved: [0, 1, 0, 0, 0, 0, 0, 0]
84    /// Arrangement 2, 7 & 8 have been saved: [0, 1, 0, 0, 0, 0, 1, 1]
85    /// ```
86    pub arrangements_saved_state: [u8; 8],
87    /// Checksum for the file.
88    pub checksum: u16,
89}
90
91impl CalculateChecksum for ArrangementFile {
92    fn calculate_checksum(&self) -> RBoxErr<u16> {
93        Ok(0)
94    }
95}
96
97impl Encode for ArrangementFile {
98    fn encode(&self) -> RBoxErr<Vec<u8>> {
99        // TODO: oh god it looks like i might need to swap byte order on everything,
100        //       possibly including bank data swapping bytes is one required when
101        //       running on little-endian systems
102        let mut swapped = self.clone();
103        if cfg!(target_endian = "little") {
104            swapped.checksum = self.checksum.swap_bytes();
105        }
106        let bytes = bincode::serialize(&swapped)?;
107        Ok(bytes)
108    }
109}
110
111impl Default for ArrangementFile {
112    fn default() -> Self {
113        // TODO: Clean up once checksums are dealt with
114        // let init = Self {
115        // let init = Self {
116        //     header: ARRANGEMENT_FILE_HEADER,
117        //     unk1: from_fn(|_| 0),
118        //     arrangement_state_current: ArrangementBlock::default(),
119        //     // TODO
120        //     unk2: 0,
121        //     // TODO
122        //     saved_flag: 1,
123        //     arrangement_state_previous: ArrangementBlock::default(),
124        //     // WARN: by default this actually always 0, but generating test data where these things
125        //     // are 'inactive' is basically impossible!
126        //     // for now, I'm setting this to default a 1-values array, but in reality it depends...
127        //     // cba to type this out fully. gonna bite me later i know it. basically i need to check
128        //     // if arrangement chaining affects this flag or if the arrangement has been saved.
129        //     arrangements_saved_state: from_fn(|_| 1),
130        //     checksum: 1973,
131        // };
132
133        // let bytes = bincode::serialize(&init).unwrap();
134        // let checksum = get_checksum(&bytes);
135        // init.checksum = checksum.unwrap();
136        // init
137
138        Self {
139            header: ARRANGEMENT_FILE_HEADER,
140            datatype_version: ARRANGEMENT_FILE_VERSION,
141            unk1: from_fn(|_| 0),
142            arrangement_state_current: ArrangementBlock::default(),
143            // TODO
144            unk2: 0,
145            // TODO
146            saved_flag: 0,
147            arrangement_state_previous: ArrangementBlock::default(),
148            // WARN: by default this actually always 0, but generating test data where these things
149            // are 'inactive' is basically impossible!
150            // for now, I'm setting this to default a 1-values array, but in reality it depends...
151            // cba to type this out fully. gonna bite me later i know it. basically i need to check
152            // if arrangement chaining affects this flag or if the arrangement has been saved.
153            arrangements_saved_state: from_fn(|_| 1),
154            checksum: 1973,
155        }
156    }
157}
158
159impl CheckHeader for ArrangementFile {
160    fn check_header(&self) -> bool {
161        self.header == ARRANGEMENT_FILE_HEADER
162    }
163}
164
165impl CheckChecksum for ArrangementFile {
166    fn check_checksum(&self) -> RBoxErr<bool> {
167        Ok(self.checksum == self.calculate_checksum()?)
168    }
169}
170
171impl CheckIntegrity for ArrangementFile {}
172
173impl IsDefault for ArrangementFile {
174    fn is_default(&self) -> bool {
175        let default = &ArrangementFile::default();
176        // check everything except the arrangement name fields (see
177        // ArrangementBlock's IsDefault implementation for more details)
178        self.arrangement_state_current.is_default()
179            && self.arrangement_state_previous.is_default()
180            && default.unk1 == self.unk1
181            && default.unk2 == self.unk2
182    }
183}
184
185/// Base model for an arrangement 'block' within an arrangement binary data file.
186/// There are two arrangement 'blocks' in each arrangement file -- enabling the
187/// arrangement 'reload ' functionality.
188#[derive(Debug, Eq, PartialEq, Clone)]
189pub struct ArrangementBlock {
190    /// Name of the Arrangement in ASCII values, max length 15 characters
191    pub name: [u8; 15], // String,
192
193    /// Unknown data. No idea what this is. Usually [0, 0].
194    pub unknown_1: [u8; 2],
195
196    /// Number of active rows in the arrangement. Any parsed row data after this number of rows
197    /// should be an `ArrangeRow::EmptyRow` variant.
198    ///
199    /// WARNING: Max number of `ArrangeRows` returns a zero value here!
200    pub n_rows: u8,
201
202    /// Rows of the arrangement. Maximum 256 rows possible.
203    pub rows: Box<Array<ArrangeRow, 256>>,
204}
205
206impl Default for ArrangementBlock {
207    fn default() -> Self {
208        Self {
209            name: ARRANGEMENT_DEFAULT_NAME,
210            unknown_1: from_fn(|_| 0),
211            n_rows: 0,
212            rows: ArrangeRow::defaults(),
213        }
214    }
215}
216
217impl IsDefault for ArrangementBlock {
218    fn is_default(&self) -> bool {
219        let default = &Self::default();
220
221        // when the octatrack creates a new arrangement file, it will reuse a
222        // name from a previously created arrangement in a different project
223        //
224        // no idea why it does this (copying the other file?) but it does it
225        // reliably when creating a new project from the project menu.
226        default.unknown_1 == self.unknown_1
227            && default.n_rows == self.n_rows
228            && default.rows == self.rows
229    }
230}
231
232/// Base model for an arranger row within an arrangement block.
233#[derive(Debug, PartialEq, Eq, Clone, DefaultsAsBoxedBigArray)]
234pub enum ArrangeRow {
235    /// pattern choice and playback
236    PatternRow {
237        // row_type: u8,
238        /// Which Pattern should be played at this point. Patterns are indexed from 0 (A01) -> 256 (P16).
239        pattern_id: u8,
240        /// How many times to play this arrangement row.
241        repetitions: u8,
242        // unused_1: u8,
243        /// How track muting is applied during this arrangement row.
244        mute_mask: u8,
245        // unused_2: u8,
246        /// First part of the Tempo mask for this row.
247        /// Needs to be combined with `tempo_2` to work out the actual tempo (not sure how it works yet).
248        tempo_1: u8,
249        /// Second part of the Tempo mask for this row.
250        /// Needs to be combined with `tempo_1` to work out the actual tempo (not sure how it works yet).
251        tempo_2: u8,
252        /// Which scene is assigned to Scene slot A when this arrangement row is playing.
253        scene_a: u8,
254        /// Which scene is assigned to Scene slot B when this arrangement row is playing.
255        scene_b: u8,
256        // unused_3: u8,
257        /// Which trig to start Playing the pattern on.
258        offset: u8,
259        // unused_4: u8,
260        /// How many trigs to play the pattern for.
261        /// Note that this value always has `offset` added to it.
262        /// So a length on the machine display of 64 when the offset is 32 will result in a value of 96 in the file data.
263        length: u8,
264        /// MIDI Track transposes for all 8 midi channels.
265        /// 1 -> 48 values are positive transpose settings.
266        /// 255 (-1) -> 207 (-48) values are negative transpose settings.
267        midi_transpose: [u8; 8],
268    },
269    /// Loop/Jump/Halt rows are all essentially just loops. Example: Jumps are an infinite loop.
270    /// So these are bundled into one type.
271    ///
272    /// Loops are `loop_count = 0 -> 65` and the `row_target` is any row before this one (`loop_count=0` is infinite looping).
273    /// Halts are `loop_count = 0` and the `row_target` is this row.
274    /// Jumps are `loop_count = 0` and the `row_target` is any row after this one.
275    LoopOrJumpOrHaltRow {
276        /// How many times to loop to the `row_target`. Only applies to loops.
277        loop_count: u8,
278        /// The row number to loop back to, jump to, or end at.
279        row_target: u8,
280    },
281    /// A row of ASCII text data with 15 maximum length.
282    ReminderRow(String),
283    /// Row is not in use. Only used in an `ArrangementBlock` as a placeholder for null basically.
284    EmptyRow(),
285}
286
287impl Default for ArrangeRow {
288    fn default() -> Self {
289        Self::EmptyRow()
290    }
291}
292
293/// An arrangement file as raw bytes. Only useful for lazy debugging.
294#[derive(Debug, Serialize, Deserialize, Decodeable)]
295pub struct ArrangementFileRawBytes {
296    #[serde(with = "BigArray")]
297    pub data: [u8; 11336],
298}
299
300#[cfg(test)]
301mod test {
302    mod integrity_check {
303        use crate::arrangements::ArrangementFile;
304        use crate::CheckHeader;
305
306        #[test]
307        fn true_valid_header() {
308            let arr = ArrangementFile::default();
309            assert!(arr.check_header());
310        }
311
312        #[test]
313        fn false_invalid_header() {
314            let mut arr = ArrangementFile::default();
315            arr.header[0] = 0x01;
316            arr.header[1] = 0x01;
317            arr.header[2] = 0x50;
318            assert!(!arr.check_header());
319        }
320    }
321
322    mod is_default {
323        use crate::arrangements::ArrangementFile;
324        use crate::test_utils::get_arrange_dirpath;
325        use crate::{read_type_from_bin_file, IsDefault};
326
327        #[test]
328        fn true_not_modified_default() {
329            assert!(ArrangementFile::default().is_default())
330        }
331        #[test]
332        fn true_not_modified_file() {
333            let arr = read_type_from_bin_file::<ArrangementFile>(
334                &get_arrange_dirpath().join("blank.work"),
335            )
336            .unwrap();
337            assert!(arr.is_default())
338        }
339        #[test]
340        fn false_modified_file() {
341            let arr = read_type_from_bin_file::<ArrangementFile>(
342                &get_arrange_dirpath().join("full-options.work"),
343            )
344            .unwrap();
345            assert!(!arr.is_default())
346        }
347    }
348}
349
350#[cfg(test)]
351mod checksum {
352    use crate::arrangements::ArrangementFile;
353    use crate::test_utils::get_arrange_dirpath;
354    use crate::{read_type_from_bin_file, RBoxErr};
355    use std::path::Path;
356
357    // make sure arrangements we're testing are equal first, ignoring the checksums
358    fn arr_eq_helper(a: &ArrangementFile, b: &ArrangementFile) {
359        assert_eq!(a.header, b.header);
360        assert_eq!(a.datatype_version, b.datatype_version);
361        assert_eq!(a.unk1, b.unk1);
362        assert_eq!(a.unk2, b.unk2);
363        assert_eq!(a.arrangements_saved_state, b.arrangements_saved_state);
364        assert_eq!(a.arrangement_state_current, b.arrangement_state_current);
365        assert_eq!(a.arrangement_state_previous, b.arrangement_state_previous);
366    }
367
368    // :eyes: https://github.com/beeb/octarranger/blob/master/src/js/stores/OctaStore.js#L150-L174
369    // this only seems to work for default / blank patterns (with name changes)
370    // and the 4x patterns test case ...?
371    //
372    // ... which is the same as the get_checksum function below :/
373    //
374    // yeah it's basically the same fucntion. when you factor all the crud out,
375    // it basically turns into an overcomplicated u16 wrapped sum of individual
376    // bytes
377    #[allow(dead_code)]
378    fn get_checksum_octarranger(bytes: &[u8]) -> RBoxErr<u16> {
379        let bytes_no_header_no_chk = &bytes[16..bytes.len() - 2];
380
381        let mut chk: u16 = 0;
382        for byte_pair in &bytes_no_header_no_chk
383            .chunks(2)
384            .map(|x| x.to_vec())
385            .filter(|x| x.len() > 1)
386            .collect::<Vec<Vec<u8>>>()
387        {
388            let first_byte = byte_pair[0] as u16;
389            let second_byte = byte_pair[1] as u16;
390            chk = second_byte.wrapping_add(first_byte.wrapping_add(chk));
391        }
392
393        Ok(chk)
394    }
395
396    fn get_checksum_simple(bytes: &[u8]) -> RBoxErr<u16> {
397        let bytes_no_header_no_chk = &bytes[16..bytes.len() - 2];
398
399        let mut prev;
400        let mut chk: u32 = 0;
401        for byte in bytes_no_header_no_chk {
402            prev = chk;
403            chk = chk.wrapping_add((*byte as u32).wrapping_add(0));
404            if byte != &0 {
405                println!("chk: {chk} diff: {}", chk - prev);
406            }
407        }
408        println!("CHK32: {chk}");
409        Ok((chk).wrapping_mul(1) as u16)
410    }
411
412    // TODO: Very dirty implementation
413    // not working for arrangements -- looks like these have a different checksum implementation
414    #[allow(dead_code)]
415    fn get_checksum_bank(bytes: &[u8]) -> RBoxErr<u16> {
416        let bytes_no_header_no_chk = &bytes[16..bytes.len() - 2];
417        let default_bytes = &bincode::serialize(&ArrangementFile::default())?;
418        let def_important_bytes = &default_bytes[16..bytes.len() - 2];
419        let default_checksum: i32 = 1870;
420        let mut byte_diffs: i32 = 0;
421        for (byte, def_byte) in bytes_no_header_no_chk.iter().zip(def_important_bytes) {
422            let byte_diff = (*byte as i32) - (*def_byte as i32);
423            if byte_diff != 0 {
424                byte_diffs += byte_diff;
425            }
426        }
427        let check = byte_diffs * 256 + default_checksum;
428        let modded = check.rem_euclid(65535);
429        Ok(modded as u16)
430    }
431
432    fn helper_test_chksum(fp: &Path) {
433        let valid = read_type_from_bin_file::<ArrangementFile>(fp).unwrap();
434        let mut test = valid.clone();
435        test.checksum = 0;
436        arr_eq_helper(&test, &valid);
437
438        let bytes = bincode::serialize(&test).unwrap();
439        let r = get_checksum_simple(&bytes);
440        assert!(r.is_ok());
441        let res = r.unwrap();
442        let s_attr_chk: u32 = *&bytes[16..bytes.len() - 2]
443            .iter()
444            .map(|x| *x as u32)
445            .sum::<u32>()
446            .rem_euclid(u16::MAX as u32 + 1);
447
448        let non_zero_bytes = bytes.iter().filter(|b| b > &&0).count();
449        let non_zero_sum = bytes
450            .iter()
451            .cloned()
452            .filter(|b| b > &0)
453            .map(|x| x as u32)
454            .sum::<u32>();
455
456        println!(
457            "l: {} r: {} non_zero_bytes {} sum total: {} s-attr {} diff {} (or {})",
458            res,
459            valid.checksum,
460            non_zero_bytes,
461            non_zero_sum,
462            s_attr_chk,
463            res.wrapping_sub(valid.checksum),
464            valid.checksum.wrapping_sub(res)
465        );
466        println!(
467            "checksum bytes: {:?} target bytes: {:?}",
468            [(res >> 8) as u8, res as u8],
469            [(valid.checksum >> 8) as u8, valid.checksum as u8]
470        );
471        assert_eq!(res, valid.checksum);
472    }
473
474    // works with original sample attrs implementation
475    #[test]
476    fn blank() {
477        helper_test_chksum(&get_arrange_dirpath().join("blank.work"));
478    }
479
480    // works with original sample attrs implementation
481    #[test]
482    fn blank_diffname1() {
483        helper_test_chksum(&get_arrange_dirpath().join("blank-diffname1.work"));
484    }
485
486    // works with original sample attrs implementation
487    #[test]
488    fn blank_diffname2() {
489        helper_test_chksum(&get_arrange_dirpath().join("blank-diffname2.work"));
490    }
491
492    // current diff to sample attrs impl = 142 * 8 (1136)
493    #[test]
494    #[ignore]
495    fn one_rem_row_notext() {
496        helper_test_chksum(&get_arrange_dirpath().join("1-rem-blank-txt.work"));
497    }
498
499    // current difference = 1796
500    // so the CHAIN text adds 660
501    #[test]
502    #[ignore]
503    fn one_rem_row_wtext() {
504        helper_test_chksum(&get_arrange_dirpath().join("1-rem-CHAIN-txt.work"));
505    }
506
507    #[test]
508    #[ignore]
509    fn two_rem_row_wtext() {
510        helper_test_chksum(&get_arrange_dirpath().join("2-rem-CHAIN-txt.work"));
511    }
512
513    #[test]
514    #[ignore]
515    fn four_patterns() {
516        helper_test_chksum(&get_arrange_dirpath().join("4-patterns.work"));
517        assert!(false)
518    }
519
520    #[test]
521    #[ignore]
522    fn one_pattern() {
523        helper_test_chksum(&get_arrange_dirpath().join("1-pattern.work"));
524    }
525
526    #[test]
527    #[ignore]
528    fn one_halt() {
529        helper_test_chksum(&get_arrange_dirpath().join("1-halt.work"));
530        // works for this specific case only
531
532        // let bytes = bincode::serialize(&arr).unwrap();
533        // let bytes_no_header_no_chk = &bytes[16..bytes.len() - 2];
534        //
535        // let mut chk: u16 = 0;
536        // for byte in bytes_no_header_no_chk {
537        //     chk = chk.wrapping_add(*byte as u16);
538        // }
539        // let checksum = chk.wrapping_mul(2).wrapping_add(254);
540        //
541        // println!(
542        //     "checksum bytes: {:?} target bytes: {:?}",
543        //     [(checksum >> 8) as u8, checksum as u8],
544        //     [(valid.checksum >> 8) as u8, valid.checksum as u8]
545        // );
546        // println!(
547        //     "diff: {} (or {})",
548        //     checksum.wrapping_sub(valid.checksum),
549        //     valid.checksum.wrapping_sub(checksum)
550        // );
551        // assert_eq!(checksum, valid.checksum);
552    }
553
554    #[test]
555    #[ignore]
556    fn one_pattern_1_loop() {
557        helper_test_chksum(&get_arrange_dirpath().join("1-patt-1-loop.work"));
558    }
559
560    #[test]
561    #[ignore]
562    fn one_pattern_1_jump_1_loop() {
563        helper_test_chksum(&get_arrange_dirpath().join("1-patt-1-jump-1-loop.work"));
564    }
565
566    #[test]
567    #[ignore]
568    fn one_pattern_1_jump_1_loop_1_halt() {
569        helper_test_chksum(&get_arrange_dirpath().join("1-patt-1-jump-1-loop-1-halt.work"));
570    }
571
572    #[test]
573    #[ignore]
574    fn full_options() {
575        helper_test_chksum(&get_arrange_dirpath().join("full-options.work"));
576    }
577
578    // FIXME: Did i not save/copy this properly? this only has empty rows
579    #[test]
580    #[ignore]
581    fn full_options_no_rems() {
582        helper_test_chksum(&get_arrange_dirpath().join("full-options-no-rems.work"));
583    }
584
585    #[test]
586    fn no_saved_flag() {
587        helper_test_chksum(&get_arrange_dirpath().join("no-saved-flag.work"));
588    }
589
590    #[test]
591    fn with_saved_flag() {
592        helper_test_chksum(&get_arrange_dirpath().join("with-saved-flag.work"));
593    }
594
595    #[test]
596    fn blank_samename_saved() {
597        helper_test_chksum(&get_arrange_dirpath().join("blank-samename-saved-chktest.work"));
598    }
599
600    #[test]
601    fn blank_samename_unsaved() {
602        helper_test_chksum(&get_arrange_dirpath().join("blank-samename-unsaved-chktest.work"));
603    }
604}