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