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>>);