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