ot_tools_io/
patterns.rs

1/*
2SPDX-License-Identifier: GPL-3.0-or-later
3Copyright © 2024 Mike Robeson [dijksterhuis]
4*/
5
6//! Models for pattern data within a bank.
7use crate::{Defaults, HasHeaderField, OtToolsIoError};
8use ot_tools_io_derive::{
9    ArrayDefaults, BoxedBigArrayDefaults, ContainerArrayMethods, IsDefaultCheck,
10};
11use std::array::from_fn;
12use std::slice::{Iter, IterMut};
13
14use crate::parts::{
15    AudioTrackAmpParamsValues, AudioTrackFxParamsValues, LfoParamsValues, MidiTrackArpParamsValues,
16    MidiTrackCc1ParamsValues, MidiTrackCc2ParamsValues, MidiTrackMidiParamsValues,
17};
18use serde::{Deserialize, Serialize};
19use serde_big_array::{Array, BigArray};
20
21const PATTERN_HEADER: [u8; 8] = [0x50, 0x54, 0x52, 0x4e, 0x00, 0x00, 0x00, 0x00];
22
23/// Header array for a MIDI track section in binary data files: `MTRA`
24const MIDI_TRACK_HEADER: [u8; 4] = [0x4d, 0x54, 0x52, 0x41];
25
26/// Header array for a MIDI track section in binary data files: `TRAC`
27const AUDIO_TRACK_HEADER: [u8; 4] = [0x54, 0x52, 0x41, 0x43];
28
29/// A Trig's parameter locks on the Playback/Machine page for an Audio Track.
30#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Copy)]
31pub struct AudioTrackParameterLockPlayback {
32    pub param1: u8,
33    pub param2: u8,
34    pub param3: u8,
35    pub param4: u8,
36    pub param5: u8,
37    pub param6: u8,
38}
39
40impl Default for AudioTrackParameterLocks {
41    fn default() -> Self {
42        // 255 -> disabled
43
44        // NOTE: the `part.rs` `default` methods for each of these type has
45        // fields all set to the correct defaults for the TRACK view, not p-lock
46        // trigS. So don't try and use the type's `default` method here as you
47        // will end up with a bunch of p-locks on trigs for all the default
48        // values. (Although maybe that's a desired feature for some workflows).
49
50        // Yes, this comment is duplicated below. It is to make sur you've seen
51        // it.
52        Self {
53            machine: AudioTrackParameterLockPlayback {
54                param1: 255,
55                param2: 255,
56                param3: 255,
57                param4: 255,
58                param5: 255,
59                param6: 255,
60            },
61            lfo: LfoParamsValues {
62                spd1: 255,
63                spd2: 255,
64                spd3: 255,
65                dep1: 255,
66                dep2: 255,
67                dep3: 255,
68            },
69            amp: AudioTrackAmpParamsValues {
70                atk: 255,
71                hold: 255,
72                rel: 255,
73                vol: 255,
74                bal: 255,
75                f: 255,
76            },
77            fx1: AudioTrackFxParamsValues {
78                param_1: 255,
79                param_2: 255,
80                param_3: 255,
81                param_4: 255,
82                param_5: 255,
83                param_6: 255,
84            },
85            fx2: AudioTrackFxParamsValues {
86                param_1: 255,
87                param_2: 255,
88                param_3: 255,
89                param_4: 255,
90                param_5: 255,
91                param_6: 255,
92            },
93            static_slot_id: 255,
94            flex_slot_id: 255,
95        }
96    }
97}
98
99/// A single trig's parameter locks on an Audio Track.
100#[derive(
101    Debug,
102    Serialize,
103    Deserialize,
104    Clone,
105    PartialEq,
106    Copy,
107    ArrayDefaults,
108    BoxedBigArrayDefaults,
109    IsDefaultCheck,
110)]
111pub struct AudioTrackParameterLocks {
112    pub machine: AudioTrackParameterLockPlayback,
113    pub lfo: LfoParamsValues,
114    pub amp: AudioTrackAmpParamsValues,
115    pub fx1: AudioTrackFxParamsValues,
116    pub fx2: AudioTrackFxParamsValues,
117    /// P-Lock to change an audio track's static machine sample slot assignment per trig
118    pub static_slot_id: u8,
119    /// P-Lock to change an audio track's flex machine sample slot assignment per trig
120    pub flex_slot_id: u8,
121}
122
123#[derive(
124    Debug,
125    Serialize,
126    Deserialize,
127    Clone,
128    PartialEq,
129    ArrayDefaults,
130    BoxedBigArrayDefaults,
131    IsDefaultCheck,
132    ContainerArrayMethods,
133)]
134pub struct AudioTrackParameterLocksArray(pub Box<Array<AudioTrackParameterLocks, 64>>);
135
136/// MIDI Track parameter locks.
137#[derive(
138    Debug, Serialize, Deserialize, Clone, PartialEq, Copy, ArrayDefaults, BoxedBigArrayDefaults,
139)]
140pub struct MidiTrackParameterLocks {
141    pub midi: MidiTrackMidiParamsValues,
142    pub lfo: LfoParamsValues,
143    pub arp: MidiTrackArpParamsValues,
144    pub ctrl1: MidiTrackCc1ParamsValues,
145    pub ctrl2: MidiTrackCc2ParamsValues,
146
147    #[serde(with = "BigArray")]
148    unknown: [u8; 2],
149}
150
151impl Default for MidiTrackParameterLocks {
152    fn default() -> Self {
153        // 255 -> disabled
154
155        // NOTE: the `part.rs` `default` methods for each of these type has
156        // fields all set to the correct defaults for the TRACK view, not p-lock
157        // trigS. So don't try and use the type's `default` method here as you
158        // will end up with a bunch of p-locks on trigs for all the default
159        // values. (Although maybe that's a desired feature for some workflows).
160
161        // Yes, this comment is duplicated above. It is to make sur you've seen
162        // it.
163
164        Self {
165            midi: MidiTrackMidiParamsValues {
166                note: 255,
167                vel: 255,
168                len: 255,
169                not2: 255,
170                not3: 255,
171                not4: 255,
172            },
173            lfo: LfoParamsValues {
174                spd1: 255,
175                spd2: 255,
176                spd3: 255,
177                dep1: 255,
178                dep2: 255,
179                dep3: 255,
180            },
181            arp: MidiTrackArpParamsValues {
182                tran: 255,
183                leg: 255,
184                mode: 255,
185                spd: 255,
186                rnge: 255,
187                nlen: 255,
188            },
189            ctrl1: MidiTrackCc1ParamsValues {
190                pb: 255,
191                at: 255,
192                cc1: 255,
193                cc2: 255,
194                cc3: 255,
195                cc4: 255,
196            },
197            ctrl2: MidiTrackCc2ParamsValues {
198                cc5: 255,
199                cc6: 255,
200                cc7: 255,
201                cc8: 255,
202                cc9: 255,
203                cc10: 255,
204            },
205            unknown: [255, 255],
206        }
207    }
208}
209
210/// Audio & MIDI Track Pattern playback settings.
211#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Copy)]
212pub struct TrackPatternSettings {
213    /// Silence any existing audio playback on the Audio Track when switching Patterns.
214    pub start_silent: u8,
215
216    /// Trigger Audio Track playback without any quantization or syncing to other Audio Tracks.
217    pub plays_free: u8,
218
219    /// Quantization when this Audio Track is Triggered for Playback.
220    ///
221    /// Options
222    /// ```text
223    /// N/A and ONE: 0 (Default)
224    /// ONE2: 1
225    /// HOLD: 2
226    /// ```
227    pub trig_mode: u8,
228
229    /// Track Trigger Quantization.
230    ///
231    /// Options
232    /// ```text
233    /// N/A and TR.LEN: 0 (Default)
234    /// 1/16: 1
235    /// 2/16: 2
236    /// 3/16: 3
237    /// 4/16: 4
238    /// 6/16: 5
239    /// 8/16: 6
240    /// 12/16: 7
241    /// 16/16: 8
242    /// 24/16: 9
243    /// 32/16: 10
244    /// 48/16: 11
245    /// 64/16: 12
246    /// 96/16: 13
247    /// 128/16: 14
248    /// 192/16: 15
249    /// 256/16: 16
250    /// DIRECT: 255
251    /// ```
252    pub trig_quant: u8,
253
254    /// Whether to play the track as a `ONESHOT` track.
255    pub oneshot_trk: u8,
256}
257
258impl Default for TrackPatternSettings {
259    fn default() -> Self {
260        Self {
261            start_silent: 255,
262            plays_free: 0,
263            trig_mode: 0,
264            trig_quant: 0,
265            oneshot_trk: 0,
266        }
267    }
268}
269
270/// Trig bitmasks array for Audio Tracks.
271/// Can be converted into an array of booleans using the `get_track_trigs_from_bitmasks` function.
272///
273/// Trig bitmask arrays have bitmasks stored in this order, which is slightly confusing (pay attention to the difference with 7 + 8!):
274/// 1. 1st half of the 4th page
275/// 2. 2nd half of the 4th page
276/// 3. 1st half of the 3rd page
277/// 4. 2nd half of the 3rd page
278/// 5. 1st half of the 2nd page
279/// 6. 2nd half of the 2nd page
280/// 7. 2nd half of the 1st page
281/// 8. 1st half of the 1st page
282///
283/// ### Bitmask values for trig positions
284/// With single trigs in a half-page
285/// ```text
286/// positions
287/// 1 2 3 4 5 6 7 8 | mask value
288/// ----------------|-----------
289/// - - - - - - - - | 0
290/// x - - - - - - - | 1
291/// - x - - - - - - | 2
292/// - - x - - - - - | 4
293/// - - - x - - - - | 8
294/// - - - - x - - - | 16
295/// - - - - - x - - | 32
296/// - - - - - - x - | 64
297/// - - - - - - - x | 128
298/// ```
299///
300/// When there are multiple trigs in a half-page, the individual position values are summed together:
301///
302/// ```text
303/// 1 2 3 4 5 6 7 8 | mask value
304/// ----------------|-----------
305/// x x - - - - - - | 1 + 2 = 3
306/// x x x x - - - - | 1 + 2 + 4 + 8 = 15
307/// ```
308/// ### Fuller diagram of mask values
309///
310/// ```text
311/// positions
312/// 1 2 3 4 5 6 7 8 | mask value
313/// ----------------|-----------
314/// x - - - - - - - | 1
315/// - x - - - - - - | 2
316/// x x - - - - - - | 3
317/// - - x - - - - - | 4
318/// x - x - - - - - | 5
319/// - x x - - - - - | 6
320/// x x x - - - - - | 7
321/// - - - x - - - - | 8
322/// x - - x - - - - | 9
323/// - x - x - - - - | 10
324/// x x - x - - - - | 11
325/// - - x x - - - - | 12
326/// x - x x - - - - | 13
327/// - x x x - - - - | 14
328/// x x x x - - - - | 15
329/// ................|....
330/// x x x x x x - - | 63
331/// ................|....
332/// - - - - - - - x | 128
333/// ................|....
334/// - x - x - x - x | 170
335/// ................|....
336/// - - - - x x x x | 240
337/// ................|....
338/// x x x x x x x x | 255
339/// ```
340///
341#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
342pub struct AudioTrackTrigMasks {
343    /// Trigger Trig masks -- indicate which Trigger Trigs are active.
344    /// Base track Trig masks are stored backwards, meaning
345    /// the first 8 Trig positions are the last bytes in this section.
346    #[serde(with = "BigArray")]
347    pub trigger: [u8; 8],
348
349    /// Envelope Trig masks -- indicate which Envelope Trigs are active.
350    /// See the description of the `trig_trig_masks` field for an
351    /// explanation of how the masking works.
352    #[serde(with = "BigArray")]
353    pub trigless: [u8; 8],
354
355    /// Parameter-Lock Trig masks -- indicate which Parameter-Lock Trigs are active.
356    /// See the description of the `trig_trig_masks` field for an
357    /// explanation of how the masking works.    
358    #[serde(with = "BigArray")]
359    pub plock: [u8; 8],
360
361    /// Hold Trig masks -- indicate which Hold Trigs are active.
362    /// See the description of the `trig_trig_masks` field for an
363    /// explanation of how the masking works.
364    #[serde(with = "BigArray")]
365    pub oneshot: [u8; 8],
366
367    /// Recorder Trig masks -- indicate which Recorder Trigs are active.
368    /// These seem to function differently to the main Track Trig masks.
369    /// Filling up Recorder Trigs on a Pattern results in a 32 length array
370    /// instead of 8 length.
371    /// Possible that the Trig type is stored in this array as well.
372    #[serde(with = "BigArray")]
373    pub recorder: [u8; 32],
374
375    /// Swing trigs Trig masks.
376    #[serde(with = "BigArray")]
377    pub swing: [u8; 8],
378
379    /// Parameter Slide trigs Trig masks.
380    #[serde(with = "BigArray")]
381    pub slide: [u8; 8],
382}
383
384impl Default for AudioTrackTrigMasks {
385    fn default() -> Self {
386        Self {
387            trigger: from_fn(|_| 0),
388            trigless: from_fn(|_| 0),
389            plock: from_fn(|_| 0),
390            oneshot: from_fn(|_| 0),
391            recorder: from_fn(|_| 0),
392            swing: from_fn(|_| 170),
393            slide: from_fn(|_| 0),
394        }
395    }
396}
397
398/// Audio Track custom scaling when the Pattern is in PER TRACK scale mode.
399#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Copy)]
400pub struct TrackPerTrackModeScale {
401    /// The Audio Track's Length when Pattern is in Per Track mode.
402    /// Default: 16
403    pub per_track_len: u8,
404
405    /// The Audio Track's Scale when Pattern is in Per Track mode.
406    ///
407    /// Options
408    /// ```text
409    /// 0 -> 2x
410    /// 1 -> 3/2x
411    /// 2 -> 1x (Default)
412    /// 3 -> 3/4x
413    /// 4 -> 1/2x
414    /// 5 -> 1/4x
415    /// 6 -> 1/8x
416    /// ```
417    pub per_track_scale: u8,
418}
419
420impl Default for TrackPerTrackModeScale {
421    fn default() -> Self {
422        Self {
423            per_track_len: 16,
424            per_track_scale: 2,
425        }
426    }
427}
428
429/// Track trigs assigned on an Audio Track within a Pattern
430#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, BoxedBigArrayDefaults)]
431pub struct AudioTrackTrigs {
432    /// Header data section
433    ///
434    /// example data:
435    /// ```text
436    /// TRAC
437    /// 54 52 41 43
438    /// ```
439    #[serde(with = "BigArray")]
440    pub header: [u8; 4],
441
442    /// Unknown data.
443    #[serde(with = "BigArray")]
444    pub unknown_1: [u8; 4],
445
446    /// The zero indexed track number
447    pub track_id: u8,
448
449    /// Trig masks contain the Trig step locations for different trig types
450    pub trig_masks: AudioTrackTrigMasks,
451
452    /// The scale of this Audio Track in Per Track Pattern mode.
453    pub scale_per_track_mode: TrackPerTrackModeScale,
454
455    /// Amount of swing when a Swing Trig is active for the Track.
456    /// Maximum is `30` (`80` on device), minimum is `0` (`50` on device).
457    pub swing_amount: u8,
458
459    /// Pattern settings for this Audio Track
460    pub pattern_settings: TrackPatternSettings,
461
462    /// Unknown data.
463    pub unknown_2: u8,
464
465    /// Parameter-Lock data for all Trigs.
466    // note -- stack overflow if tring to use #[serde(with = "BigArray")]
467    pub plocks: AudioTrackParameterLocksArray,
468
469    /// What the hell is this field?!?!
470    /// It **has to** be something to do with trigs, but i have no idea what it could be.
471    #[serde(with = "BigArray")]
472    pub unknown_3: [u8; 64],
473
474    /// Trig Offsets, Trig Counts and Trig Conditions
475    /// ====
476    /// This is ..... slightly frustrating.
477    ///
478    /// This 64 length array consisting of a pair of bytes for each array element hold three
479    /// data references... Trig Cunts and Trig Conditions use the two bytes independently,
480    /// so they're easier to explain first
481    ///
482    /// Trig Counts and Trig Conditions
483    /// ====
484    ///
485    /// Trig Counts and Trig Conditions data is interleaved for each trig.
486    /// For Trig position 1, array index 0 is the count value and array index 1 is the Trig
487    /// Condition.
488    ///
489    /// For trig counts (1st byte), the value (zero-indexed) is multiplied by 32.
490    /// - 8 trig counts (7 repeats) --> 7 * 3 = 224
491    /// - 4 trig counts (3 repeats) -- 3 * 32 = 96
492    /// - 1 trig counts (0 repeats) -- 0 * 32 = 0
493    ///
494    /// For conditionals, see the `TrigCondition` enum and associated traits for more details.
495    /// The maximum value for a Trig Condition byte is 64.
496    ///
497    /// ```rust
498    /// // no trig micro-timings at all
499    /// [
500    ///     // trig 1
501    ///     [
502    ///         0,   // trig counts (number)
503    ///         0,   // trig condition (enum rep)
504    ///     ],
505    ///     // trig 2
506    ///     [
507    ///         224, // trig counts (max value)
508    ///         64,  // trig condition (max value)
509    ///     ],
510    ///     // trig 3
511    ///     [
512    ///         32,  // trig counts (minimum non-zero value)
513    ///         1,   // trig condition (minimum non-zero value)
514    ///     ],
515    ///     // ... and so on
516    /// ];
517    /// ```
518    ///
519    /// Trig Offsets
520    /// ====
521    ///
522    /// Trig Offset values use both of these interleaved bytes on top of the
523    /// trig repeat and trig condition values... Which makes life more complex
524    /// and somewhat frustrating.
525    ///
526    /// Inspected values
527    /// - -23/384 -> 1st byte 20, 2nd byte 128
528    /// - -1/32 -> 1st byte 26, 2nd byte 0
529    /// - -1/64 -> 1st byte 29, 2nd byte 0
530    /// - -1/128 -> 1st byte 30, 2nd byte 128
531    /// - 1/128 -> 1st byte 1, 2nd byte 128
532    /// - 1/64 -> 1st byte 3, 2nd byte 0
533    /// - 1/32 -> 1st byte 6, 2nd byte 0
534    /// - 23/384 -> 1st byte 11, 2nd byte 128
535    ///
536    /// #### 1st byte
537    /// The 1st byte only has 31 possible values: 255 - 224 (trig count max) = 31.
538    /// So it makes sense sort of that this is a mask? I guess?
539    ///
540    /// #### 2nd byte
541    /// From what I can tell, the second offset byte is either 0 or 128.
542    /// So a 2nd byte for an offset adjusted trig with a `8:8` trig condition is either
543    /// - 128 + 64 = 192
544    /// - 0 + 64 = 64
545    ///
546    /// So you will need to a `x.rem_euclid(128)` somewhere if you want to parse this.
547    ///
548    /// Combining the trig offset with trig count and trig conditions, we end up with
549    /// ```rust
550    /// [
551    ///     // trig one, -23/384 offset with 1x trig count and None condition
552    ///     [
553    ///         20,  // 20 + (32 * 0)
554    ///         128, // 128 + 0
555    ///     ],
556    ///     // trig two, -23/384 offset with 2x trig count and Fill condition
557    ///     [
558    ///         52,  // 20 + (32 * 1)
559    ///         129, // 128 + 1
560    ///     ],
561    ///     // trig three, -23/384 offset with 3x trig count and Fill condition
562    ///     [
563    ///         84,  // 20 + (32 * 2)
564    ///         129, // 128 + 1
565    ///     ],
566    ///     // trig four, -23/384 offset with 3x trig count and NotFill condition
567    ///     [
568    ///         84,  // 20 + (32 * 2)
569    ///         130, // 128 + 2
570    ///     ],
571    ///     // trig five, +1/32 offset with 2x trig count and Fill condition
572    ///     [
573    ///         38,  // 6 + (32 * 1)
574    ///         1,   // 0 + 1
575    ///     ],
576    ///     // trig six, +1/32 offset with 3x trig count and Fill condition
577    ///     [
578    ///         70,  // 6 + (32 * 2)
579    ///         1,   // 0 + 1
580    ///     ],
581    ///     // trig seven, +1/32 offset with 3x trig count and NotFill condition
582    ///     [
583    ///         70,  // 6 + (32 * 2)
584    ///         2,   // 0 + 2
585    ///     ],
586    ///     // .... and so on
587    /// ];
588    /// ```
589    ///
590    /// #### Extending pages and offsets
591    ///
592    /// If you have a trig offset on Trig 1 with only one pattern page activated,
593    /// the trig offsets for Trig 1 are replicated over the relevant trig
594    /// positions for each first trig in the inactive pages in this array.
595    ///
596    /// So, for a 1/32 offset on trig 1 with only one page active, you get the
597    /// following values showing up in this array:
598    /// - pair of bytes at array index 15 -> 1/32
599    /// - pair of bytes at array index 31 -> 1/32
600    /// - pair of bytes at array index 47 -> 1/32
601    ///
602    /// This does not happen for offset values at any other trig position
603    /// (from what I can tell in my limited testing -- trig values 2-4 and 9-11
604    /// inclusive are not replicated in the same way).
605    ///
606    /// This 'replicating trig offset values over unused pages' behaviour does
607    /// not happen for trig counts. I haven't tested whether this applies to trig
608    /// conditions yet.
609    ///
610    /// It seems that this behaviour could be to make sure the octatrack plays
611    /// correctly offset trigs when you extend a page live, i.e. when extending
612    /// a one-page pattern to a two-page pattern, if there is a negative offset
613    /// value there the octatrack will need to play the offset trig before the
614    /// first page has completed.
615    ///
616    /// Or it could be a bug :shrug:
617    #[serde(with = "BigArray")]
618    pub trig_offsets_repeats_conditions: [[u8; 2]; 64],
619}
620
621impl Default for AudioTrackTrigs {
622    fn default() -> Self {
623        Self {
624            header: AUDIO_TRACK_HEADER,
625            unknown_1: from_fn(|_| 0),
626            track_id: 0,
627            trig_masks: AudioTrackTrigMasks::default(),
628            scale_per_track_mode: TrackPerTrackModeScale::default(),
629            swing_amount: 0,
630            pattern_settings: TrackPatternSettings::default(),
631            unknown_2: 0,
632            plocks: AudioTrackParameterLocksArray::default(),
633            unknown_3: from_fn(|_| 0),
634            trig_offsets_repeats_conditions: from_fn(|_| [0, 0]),
635        }
636    }
637}
638
639// need to implement manually to handle track_id field
640impl<const N: usize> Defaults<[Self; N]> for AudioTrackTrigs {
641    fn defaults() -> [Self; N]
642    where
643        Self: Default,
644    {
645        from_fn(|i| Self {
646            track_id: i as u8,
647            ..Default::default()
648        })
649    }
650}
651
652#[cfg(test)]
653mod audio_track_trigs_defaults {
654    use crate::patterns::AudioTrackTrigs;
655    use crate::Defaults;
656
657    fn defs() -> [AudioTrackTrigs; 8] {
658        AudioTrackTrigs::defaults()
659    }
660
661    #[test]
662    fn ok_track_ids() -> Result<(), ()> {
663        for i in 0..8 {
664            println!("Track: {} Track ID: {i}", i + 1);
665            assert_eq!(defs()[i].track_id, i as u8);
666        }
667        Ok(())
668    }
669}
670
671impl HasHeaderField for AudioTrackTrigs {
672    fn check_header(&self) -> Result<bool, OtToolsIoError> {
673        Ok(self.header == AUDIO_TRACK_HEADER)
674    }
675}
676
677#[cfg(test)]
678mod audio_track_trigs_header {
679    use crate::patterns::AudioTrackTrigs;
680    use crate::{
681        test_utils::get_blank_proj_dirpath, BankFile, HasHeaderField, OctatrackFileIO,
682        OtToolsIoError,
683    };
684    #[test]
685    fn file_read_valid() -> Result<(), OtToolsIoError> {
686        let path = get_blank_proj_dirpath().join("bank01.work");
687        let x = BankFile::from_data_file(&path)?.patterns[0]
688            .clone()
689            .audio_track_trigs;
690        assert!(x[0].check_header()?);
691        Ok(())
692    }
693
694    #[test]
695    fn file_read_invalid() -> Result<(), OtToolsIoError> {
696        let path = get_blank_proj_dirpath().join("bank01.work");
697        let x = BankFile::from_data_file(&path)?.patterns[0]
698            .clone()
699            .audio_track_trigs;
700        let mut trigs = x[0].clone();
701        trigs.header[0] = 254;
702        trigs.header[1] = 254;
703        trigs.header[2] = 254;
704        trigs.header[3] = 254;
705        assert!(!trigs.check_header()?);
706        Ok(())
707    }
708
709    #[test]
710    fn default_valid() -> Result<(), OtToolsIoError> {
711        let trigs = AudioTrackTrigs::default();
712        assert!(trigs.check_header()?);
713        Ok(())
714    }
715
716    #[test]
717    fn default_invalid() -> Result<(), OtToolsIoError> {
718        let mut trigs = AudioTrackTrigs::default();
719        trigs.header[0] = 0x01;
720        trigs.header[1] = 0x01;
721        trigs.header[2] = 0x50;
722        assert!(!trigs.check_header()?);
723        Ok(())
724    }
725}
726
727#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, ContainerArrayMethods)]
728pub struct AudioTrackTrigsArray(pub [AudioTrackTrigs; 8]);
729
730/// MIDI Track Trig masks.
731/// Can be converted into an array of booleans using the `get_track_trigs_from_bitmasks` function.
732/// See `AudioTrackTrigMasks` for more information.
733///
734/// Trig mask arrays have data stored in this order, which is slightly confusing (pay attention to the difference with 7 + 8!):
735/// 1. 1st half of the 4th page
736/// 2. 2nd half of the 4th page
737/// 3. 1st half of the 3rd page
738/// 4. 2nd half of the 3rd page
739/// 5. 1st half of the 2nd page
740/// 6. 2nd half of the 2nd page
741/// 7. 2nd half of the 1st page
742/// 8. 1st half of the 1st page
743#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Copy)]
744pub struct MidiTrackTrigMasks {
745    /// Note Trig masks.
746    #[serde(with = "BigArray")]
747    pub trigger: [u8; 8],
748
749    /// Trigless Trig masks.
750    #[serde(with = "BigArray")]
751    pub trigless: [u8; 8],
752
753    /// Parameter Lock Trig masks.
754    /// Note this only stores data for exclusive parameter lock *trigs* (light green trigs).
755    #[serde(with = "BigArray")]
756    pub plock: [u8; 8],
757
758    /// Swing trigs mask.
759    #[serde(with = "BigArray")]
760    pub swing: [u8; 8],
761
762    /// this is a block of 8, so looks like a trig mask for tracks,
763    /// but I can't think of what it could be.
764    #[serde(with = "BigArray")]
765    pub unknown: [u8; 8],
766}
767
768impl Default for MidiTrackTrigMasks {
769    fn default() -> Self {
770        Self {
771            trigger: from_fn(|_| 0),
772            trigless: from_fn(|_| 0),
773            plock: from_fn(|_| 0),
774            swing: from_fn(|_| 170),
775            unknown: from_fn(|_| 0),
776        }
777    }
778}
779
780/// Track trigs assigned on an Audio Track within a Pattern
781#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, BoxedBigArrayDefaults)]
782pub struct MidiTrackTrigs {
783    /// Header data section
784    ///
785    /// example data:
786    /// ```text
787    /// MTRA
788    /// 4d 54 52 41
789    /// ```
790    #[serde(with = "BigArray")]
791    pub header: [u8; 4],
792
793    /// Unknown data.
794    #[serde(with = "BigArray")]
795    pub unknown_1: [u8; 4],
796
797    /// The zero indexed track number
798    pub track_id: u8,
799
800    /// MIDI Track Trig masks contain the Trig step locations for different trig types
801    pub trig_masks: MidiTrackTrigMasks,
802
803    /// The scale of this MIDI Track in Per Track Pattern mode.
804    pub scale_per_track_mode: TrackPerTrackModeScale,
805
806    /// Amount of swing when a Swing Trig is active for the Track.
807    /// Maximum is `30` (`80` on device), minimum is `0` (`50` on device).
808    pub swing_amount: u8,
809
810    /// Pattern settings for this MIDI Track
811    pub pattern_settings: TrackPatternSettings,
812
813    /// trig properties -- p-locks etc.
814    /// the big `0xff` value block within tracks basically.
815    /// 32 bytes per trig -- 6x parameters for 5x pages plus 2x extra fields at the end.
816    ///
817    /// For audio tracks, the 2x extra fields at the end are for sample locks,
818    /// but there's no such concept for MIDI tracks.
819    /// It seems like Elektron devs reused their data structures for P-Locks on both Audio + MIDI tracks.
820    // note -- stack overflow if trying to use #[serde(with = "BigArray")]
821    pub plocks: Box<Array<MidiTrackParameterLocks, 64>>,
822
823    /// See the documentation for `AudioTrackTrigs` on how this field works.
824    #[serde(with = "BigArray")]
825    pub trig_offsets_repeats_conditions: [[u8; 2]; 64],
826}
827
828impl Default for MidiTrackTrigs {
829    fn default() -> Self {
830        Self {
831            header: MIDI_TRACK_HEADER,
832            unknown_1: from_fn(|_| 0),
833            track_id: 0,
834            trig_masks: MidiTrackTrigMasks::default(),
835            scale_per_track_mode: TrackPerTrackModeScale::default(),
836            swing_amount: 0,
837            pattern_settings: TrackPatternSettings::default(),
838            plocks: MidiTrackParameterLocks::defaults(),
839            trig_offsets_repeats_conditions: from_fn(|_| [0, 0]),
840        }
841    }
842}
843
844// needs to be manually implemented
845impl<const N: usize> Defaults<[Self; N]> for MidiTrackTrigs {
846    fn defaults() -> [Self; N]
847    where
848        Self: Default,
849    {
850        from_fn(|i| Self {
851            track_id: i as u8,
852            ..Default::default()
853        })
854    }
855}
856
857#[cfg(test)]
858mod midi_track_trigs_defaults {
859
860    use crate::patterns::MidiTrackTrigs;
861    use crate::Defaults;
862
863    fn defs() -> [MidiTrackTrigs; 8] {
864        MidiTrackTrigs::defaults()
865    }
866
867    #[test]
868    fn ok_track_ids() -> Result<(), ()> {
869        for i in 0..8 {
870            println!("Track: {} Track ID: {i}", i + 1);
871            assert_eq!(defs()[i].track_id, i as u8);
872        }
873        Ok(())
874    }
875}
876
877impl HasHeaderField for MidiTrackTrigs {
878    fn check_header(&self) -> Result<bool, OtToolsIoError> {
879        Ok(self.header == MIDI_TRACK_HEADER)
880    }
881}
882
883#[cfg(test)]
884mod midi_track_trigs_header {
885    use crate::patterns::MidiTrackTrigs;
886    use crate::{
887        test_utils::get_blank_proj_dirpath, BankFile, HasHeaderField, OctatrackFileIO,
888        OtToolsIoError,
889    };
890    #[test]
891    fn file_read_valid() -> Result<(), OtToolsIoError> {
892        let path = get_blank_proj_dirpath().join("bank01.work");
893        let x = BankFile::from_data_file(&path)?.patterns[0]
894            .clone()
895            .midi_track_trigs;
896        assert!(x[0].check_header()?);
897        Ok(())
898    }
899
900    #[test]
901    fn file_read_invalid() -> Result<(), OtToolsIoError> {
902        let path = get_blank_proj_dirpath().join("bank01.work");
903        let x = BankFile::from_data_file(&path)?.patterns[0]
904            .clone()
905            .midi_track_trigs;
906        let mut trigs = x[0].clone();
907        trigs.header[0] = 254;
908        trigs.header[1] = 254;
909        trigs.header[2] = 254;
910        trigs.header[3] = 254;
911        assert!(!trigs.check_header()?);
912        Ok(())
913    }
914
915    #[test]
916    fn default_valid() -> Result<(), OtToolsIoError> {
917        let trigs = MidiTrackTrigs::default();
918        assert!(trigs.check_header()?);
919        Ok(())
920    }
921
922    #[test]
923    fn default_invalid() -> Result<(), OtToolsIoError> {
924        let mut trigs = MidiTrackTrigs::default();
925        trigs.header[0] = 0x01;
926        trigs.header[1] = 0x01;
927        trigs.header[2] = 0x50;
928        assert!(!trigs.check_header()?);
929        Ok(())
930    }
931}
932
933#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, ContainerArrayMethods)]
934pub struct MidiTrackTrigsArray(pub [MidiTrackTrigs; 8]);
935
936/// Pattern level scaling settings.
937/// Some of these settings still apply when the pattern is in Per-Track scaling mode.
938#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
939pub struct PatternScaleSettings {
940    /// Multiply this value by `master_len_per_track` to get
941    /// the real Master Length in Per Track Pattern mode.
942    ///
943    /// This field must be set to `255` when Master Length in
944    /// Per Track Pattern mode is set to `INF`.
945    ///
946    /// ```text
947    /// 0: From 2 steps to 255 steps.
948    /// 1: From 256 steps to 511 steps.
949    /// 2: From 512 steps to 767 steps.
950    /// 3: From 768 steps to 1023 steps.
951    /// 4: 1024 steps only.
952    /// 255: `INF`.
953    /// ```
954    pub master_len_per_track_multiplier: u8,
955
956    /// Master Length in Per Track Pattern mode.
957    /// Must multiply this by multiplier like this `(x + 1) * (mult + 1)` to get the real number.
958    ///
959    /// This field must be set to `255` when Master Length in
960    /// Per Track Pattern mode is set to `INF`.
961    ///
962    /// Minimum value is 2 when the multiplier equals 0.
963    pub master_len_per_track: u8,
964
965    /// The Audio Track's Scale when Pattern is in Per Track mode.
966    ///
967    /// Options
968    /// ```text
969    /// 0 -> 2x
970    /// 1 -> 3/2x
971    /// 2 -> 1x (Default)
972    /// 3 -> 3/4x
973    /// 4 -> 1/2x
974    /// 5 -> 1/4x
975    /// 6 -> 1/8x
976    /// ```
977    pub master_scale_per_track: u8,
978
979    /// Master Pattern Length.
980    /// Determines Pattern Length for all Tracks when NOT in Per Track mode.
981    pub master_len: u8,
982
983    /// Master Pattern playback multiplier.
984    ///
985    /// Options
986    /// ```text
987    /// 0 -> 2x
988    /// 1 -> 3/2x
989    /// 2 -> 1x (Default)
990    /// 3 -> 3/4x
991    /// 4 -> 1/2x
992    /// 5 -> 1/4x
993    /// 6 -> 1/8x
994    /// ```
995    pub master_scale: u8,
996
997    /// Scale mode for the Pattern.
998    ///
999    /// Options
1000    /// ```text
1001    /// NORMAL: 0 (Default)
1002    /// PER TRACK: 1
1003    /// ```
1004    pub scale_mode: u8,
1005}
1006
1007impl Default for PatternScaleSettings {
1008    fn default() -> Self {
1009        Self {
1010            master_len_per_track_multiplier: 0,
1011            master_len_per_track: 16,
1012            master_scale_per_track: 2,
1013            master_len: 16,
1014            master_scale: 2,
1015            scale_mode: 0,
1016        }
1017    }
1018}
1019
1020/// Chaining behaviour for the pattern.
1021#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
1022pub struct PatternChainBehavior {
1023    /// When `use_project_setting` field is set to `1`/`true`
1024    /// this field should be set to `N/A` with a value of `255`.
1025    pub use_pattern_setting: u8,
1026
1027    /// Pattern Chain Behaviour -- Use the Project level setting for chain
1028    /// behaviour and disable any Pattern level chaining behaviour.
1029    /// Numeric Boolean.
1030    /// When this is `1` the `use_pattern_setting` should be set to `255`.
1031    pub use_project_setting: u8,
1032}
1033
1034// allow the verbose implementation to keep things
1035// - (a) standardized across all types
1036// - (b) easier for non-rustaceans to follow when reading through data structures
1037#[allow(clippy::derivable_impls)]
1038impl Default for PatternChainBehavior {
1039    fn default() -> Self {
1040        Self {
1041            use_pattern_setting: 0,
1042            use_project_setting: 0,
1043        }
1044    }
1045}
1046
1047/// A pattern of trigs stored in the bank.
1048#[derive(
1049    PartialEq,
1050    Debug,
1051    Serialize,
1052    Deserialize,
1053    Clone,
1054    ArrayDefaults,
1055    BoxedBigArrayDefaults,
1056    IsDefaultCheck,
1057)]
1058pub struct Pattern {
1059    /// Header indicating start of pattern section
1060    ///
1061    /// example data:
1062    /// ```text
1063    /// PTRN....
1064    /// 50 54 52 4e 00 00 00 00
1065    /// ```
1066    #[serde(with = "BigArray")]
1067    pub header: [u8; 8],
1068
1069    /// Audio Track data
1070    pub audio_track_trigs: AudioTrackTrigsArray,
1071
1072    /// MIDI Track data
1073    pub midi_track_trigs: MidiTrackTrigsArray,
1074
1075    /// Pattern scaling controls and settings
1076    pub scale: PatternScaleSettings,
1077
1078    /// Pattern chaining behaviour and settings
1079    pub chain_behaviour: PatternChainBehavior,
1080
1081    /// Unknown data.
1082    pub unknown: u8,
1083
1084    /// The Part of a Bank assigned to a Pattern.
1085    /// Part 1 = 0; Part 2 = 1; Part 3 = 2; Part 4 = 3.
1086    /// Credit to [@sezare56 on elektronauts for catching this one](https://www.elektronauts.com/t/octalib-a-simple-octatrack-librarian/225192/27)
1087    pub part_assignment: u8,
1088
1089    /// Pattern setting for Tempo.
1090    ///
1091    /// The Tempo value is split across both `tempo_1` and `tempo_2`.
1092    /// Yet to figure out how they relate to each other.
1093    ///
1094    /// Value of 120 BPM is 11 for this field.
1095    /// Value of 30 BPM is 2 for this field.
1096    pub tempo_1: u8,
1097
1098    /// Pattern setting for Tempo.
1099    ///
1100    /// The Tempo value is split across both `tempo_1` and `tempo_2`.
1101    /// Tet to figure out how they relate to each other.
1102    ///
1103    /// Value of 120 BPM is `64` for this field.
1104    /// Value of 30 BPM is `208` for this field.
1105    pub tempo_2: u8,
1106}
1107
1108impl Default for Pattern {
1109    fn default() -> Self {
1110        Self {
1111            header: PATTERN_HEADER,
1112            audio_track_trigs: AudioTrackTrigsArray::default(),
1113            midi_track_trigs: MidiTrackTrigsArray::default(),
1114            scale: PatternScaleSettings::default(),
1115            chain_behaviour: PatternChainBehavior::default(),
1116            unknown: 0,
1117            part_assignment: 0,
1118            // **I believe** these two mask values make the tempo 120.0 BPM
1119            // don't quote me on that though
1120            tempo_1: 11,
1121            tempo_2: 64,
1122        }
1123    }
1124}
1125
1126impl HasHeaderField for Pattern {
1127    fn check_header(&self) -> Result<bool, OtToolsIoError> {
1128        Ok(self.header == PATTERN_HEADER)
1129    }
1130}
1131
1132#[cfg(test)]
1133mod pattern_header {
1134    use crate::{
1135        patterns::Pattern, test_utils::get_blank_proj_dirpath, BankFile, HasHeaderField,
1136        OctatrackFileIO, OtToolsIoError,
1137    };
1138    #[test]
1139    fn file_read_valid() -> Result<(), OtToolsIoError> {
1140        let path = get_blank_proj_dirpath().join("bank01.work");
1141        let pattern = BankFile::from_data_file(&path)?.patterns[0].clone();
1142        assert!(pattern.check_header()?);
1143        Ok(())
1144    }
1145
1146    #[test]
1147    fn file_read_invalid() -> Result<(), OtToolsIoError> {
1148        let path = get_blank_proj_dirpath().join("bank01.work");
1149        let mut pattern = BankFile::from_data_file(&path)?.patterns[0].clone();
1150        pattern.header[0] = 254;
1151        pattern.header[1] = 254;
1152        pattern.header[2] = 254;
1153        pattern.header[3] = 254;
1154        assert!(!pattern.check_header()?);
1155        Ok(())
1156    }
1157
1158    #[test]
1159    fn default_valid() -> Result<(), OtToolsIoError> {
1160        let pattern = Pattern::default();
1161        assert!(pattern.check_header()?);
1162        Ok(())
1163    }
1164
1165    #[test]
1166    fn default_invalid() -> Result<(), OtToolsIoError> {
1167        let mut pattern = Pattern::default();
1168        pattern.header[0] = 0x01;
1169        pattern.header[1] = 0x01;
1170        pattern.header[7] = 0x50;
1171        assert!(!pattern.check_header()?);
1172        Ok(())
1173    }
1174}
1175
1176#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, ContainerArrayMethods)]
1177pub struct PatternArray(pub Box<Array<Pattern, 16>>);