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}