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
628impl HasHeaderField for AudioTrackTrigs {
629 fn check_header(&self) -> Result<bool, OtToolsIoError> {
630 Ok(self.header == AUDIO_TRACK_HEADER)
631 }
632}
633
634/// MIDI Track Trig masks.
635/// Can be converted into an array of booleans using the `get_track_trigs_from_bitmasks` function.
636/// See `AudioTrackTrigMasks` for more information.
637///
638/// Trig mask arrays have data stored in this order, which is slightly confusing (pay attention to the difference with 7 + 8!):
639/// 1. 1st half of the 4th page
640/// 2. 2nd half of the 4th page
641/// 3. 1st half of the 3rd page
642/// 4. 2nd half of the 3rd page
643/// 5. 1st half of the 2nd page
644/// 6. 2nd half of the 2nd page
645/// 7. 2nd half of the 1st page
646/// 8. 1st half of the 1st page
647#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Copy)]
648pub struct MidiTrackTrigMasks {
649 /// Note Trig masks.
650 #[serde(with = "BigArray")]
651 pub trigger: [u8; 8],
652
653 /// Trigless Trig masks.
654 #[serde(with = "BigArray")]
655 pub trigless: [u8; 8],
656
657 /// Parameter Lock Trig masks.
658 /// Note this only stores data for exclusive parameter lock *trigs* (light green trigs).
659 #[serde(with = "BigArray")]
660 pub plock: [u8; 8],
661
662 /// Swing trigs mask.
663 #[serde(with = "BigArray")]
664 pub swing: [u8; 8],
665
666 /// this is a block of 8, so looks like a trig mask for tracks,
667 /// but I can't think of what it could be.
668 #[serde(with = "BigArray")]
669 pub unknown: [u8; 8],
670}
671
672impl Default for MidiTrackTrigMasks {
673 fn default() -> Self {
674 Self {
675 trigger: from_fn(|_| 0),
676 trigless: from_fn(|_| 0),
677 plock: from_fn(|_| 0),
678 swing: from_fn(|_| 170),
679 unknown: from_fn(|_| 0),
680 }
681 }
682}
683
684/// Track trigs assigned on an Audio Track within a Pattern
685#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, BoxedBigArrayDefaults)]
686pub struct MidiTrackTrigs {
687 /// Header data section
688 ///
689 /// example data:
690 /// ```text
691 /// MTRA
692 /// 4d 54 52 41
693 /// ```
694 #[serde(with = "BigArray")]
695 pub header: [u8; 4],
696
697 /// Unknown data.
698 #[serde(with = "BigArray")]
699 pub unknown_1: [u8; 4],
700
701 /// The zero indexed track number
702 pub track_id: u8,
703
704 /// MIDI Track Trig masks contain the Trig step locations for different trig types
705 pub trig_masks: MidiTrackTrigMasks,
706
707 /// The scale of this MIDI Track in Per Track Pattern mode.
708 pub scale_per_track_mode: TrackPerTrackModeScale,
709
710 /// Amount of swing when a Swing Trig is active for the Track.
711 /// Maximum is `30` (`80` on device), minimum is `0` (`50` on device).
712 pub swing_amount: u8,
713
714 /// Pattern settings for this MIDI Track
715 pub pattern_settings: TrackPatternSettings,
716
717 /// trig properties -- p-locks etc.
718 /// the big `0xff` value block within tracks basically.
719 /// 32 bytes per trig -- 6x parameters for 5x pages plus 2x extra fields at the end.
720 ///
721 /// For audio tracks, the 2x extra fields at the end are for sample locks,
722 /// but there's no such concept for MIDI tracks.
723 /// It seems like Elektron devs reused their data structures for P-Locks on both Audio + MIDI tracks.
724 // note -- stack overflow if trying to use #[serde(with = "BigArray")]
725 pub plocks: Box<Array<MidiTrackParameterLocks, 64>>,
726
727 /// See the documentation for `AudioTrackTrigs` on how this field works.
728 #[serde(with = "BigArray")]
729 pub trig_offsets_repeats_conditions: [[u8; 2]; 64],
730}
731
732impl Default for MidiTrackTrigs {
733 fn default() -> Self {
734 Self {
735 header: MIDI_TRACK_HEADER,
736 unknown_1: from_fn(|_| 0),
737 track_id: 0,
738 trig_masks: MidiTrackTrigMasks::default(),
739 scale_per_track_mode: TrackPerTrackModeScale::default(),
740 swing_amount: 0,
741 pattern_settings: TrackPatternSettings::default(),
742 plocks: MidiTrackParameterLocks::defaults(),
743 trig_offsets_repeats_conditions: from_fn(|_| [0, 0]),
744 }
745 }
746}
747
748// needs to be manually implemented
749impl<const N: usize> Defaults<[Self; N]> for MidiTrackTrigs {
750 fn defaults() -> [Self; N]
751 where
752 Self: Default,
753 {
754 from_fn(|i| Self {
755 track_id: i as u8,
756 ..Default::default()
757 })
758 }
759}
760
761impl HasHeaderField for MidiTrackTrigs {
762 fn check_header(&self) -> Result<bool, OtToolsIoError> {
763 Ok(self.header == MIDI_TRACK_HEADER)
764 }
765}
766
767/// Pattern level scaling settings.
768/// Some of these settings still apply when the pattern is in Per-Track scaling mode.
769#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
770pub struct PatternScaleSettings {
771 /// Multiply this value by `master_len_per_track` to get
772 /// the real Master Length in Per Track Pattern mode.
773 ///
774 /// This field must be set to `255` when Master Length in
775 /// Per Track Pattern mode is set to `INF`.
776 ///
777 /// ```text
778 /// 0: From 2 steps to 255 steps.
779 /// 1: From 256 steps to 511 steps.
780 /// 2: From 512 steps to 767 steps.
781 /// 3: From 768 steps to 1023 steps.
782 /// 4: 1024 steps only.
783 /// 255: `INF`.
784 /// ```
785 pub master_len_per_track_multiplier: u8,
786
787 /// Master Length in Per Track Pattern mode.
788 /// Must multiply this by multiplier like this `(x + 1) * (mult + 1)` to get the real number.
789 ///
790 /// This field must be set to `255` when Master Length in
791 /// Per Track Pattern mode is set to `INF`.
792 ///
793 /// Minimum value is 2 when the multiplier equals 0.
794 pub master_len_per_track: u8,
795
796 /// The Audio Track's Scale when Pattern is in Per Track mode.
797 ///
798 /// Options
799 /// ```text
800 /// 0 -> 2x
801 /// 1 -> 3/2x
802 /// 2 -> 1x (Default)
803 /// 3 -> 3/4x
804 /// 4 -> 1/2x
805 /// 5 -> 1/4x
806 /// 6 -> 1/8x
807 /// ```
808 pub master_scale_per_track: u8,
809
810 /// Master Pattern Length.
811 /// Determines Pattern Length for all Tracks when NOT in Per Track mode.
812 pub master_len: u8,
813
814 /// Master Pattern playback multiplier.
815 ///
816 /// Options
817 /// ```text
818 /// 0 -> 2x
819 /// 1 -> 3/2x
820 /// 2 -> 1x (Default)
821 /// 3 -> 3/4x
822 /// 4 -> 1/2x
823 /// 5 -> 1/4x
824 /// 6 -> 1/8x
825 /// ```
826 pub master_scale: u8,
827
828 /// Scale mode for the Pattern.
829 ///
830 /// Options
831 /// ```text
832 /// NORMAL: 0 (Default)
833 /// PER TRACK: 1
834 /// ```
835 pub scale_mode: u8,
836}
837
838impl Default for PatternScaleSettings {
839 fn default() -> Self {
840 Self {
841 master_len_per_track_multiplier: 0,
842 master_len_per_track: 16,
843 master_scale_per_track: 2,
844 master_len: 16,
845 master_scale: 2,
846 scale_mode: 0,
847 }
848 }
849}
850
851/// Chaining behaviour for the pattern.
852#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
853pub struct PatternChainBehavior {
854 /// When `use_project_setting` field is set to `1`/`true`
855 /// this field should be set to `N/A` with a value of `255`.
856 pub use_pattern_setting: u8,
857
858 /// Pattern Chain Behaviour -- Use the Project level setting for chain
859 /// behaviour and disable any Pattern level chaining behaviour.
860 /// Numeric Boolean.
861 /// When this is `1` the `use_pattern_setting` should be set to `255`.
862 pub use_project_setting: u8,
863}
864
865// allow the verbose implementation to keep things
866// - (a) standardized across all types
867// - (b) easier for non-rustaceans to follow when reading through data structures
868#[allow(clippy::derivable_impls)]
869impl Default for PatternChainBehavior {
870 fn default() -> Self {
871 Self {
872 use_pattern_setting: 0,
873 use_project_setting: 0,
874 }
875 }
876}
877
878/// A pattern of trigs stored in the bank.
879#[derive(PartialEq, Debug, Serialize, Deserialize, Clone, ArrayDefaults, BoxedBigArrayDefaults)]
880pub struct Pattern {
881 /// Header indicating start of pattern section
882 ///
883 /// example data:
884 /// ```text
885 /// PTRN....
886 /// 50 54 52 4e 00 00 00 00
887 /// ```
888 #[serde(with = "BigArray")]
889 pub header: [u8; 8],
890
891 /// Audio Track data
892 #[serde(with = "BigArray")]
893 pub audio_track_trigs: [AudioTrackTrigs; 8],
894
895 /// MIDI Track data
896 #[serde(with = "BigArray")]
897 pub midi_track_trigs: [MidiTrackTrigs; 8],
898
899 /// Pattern scaling controls and settings
900 pub scale: PatternScaleSettings,
901
902 /// Pattern chaining behaviour and settings
903 pub chain_behaviour: PatternChainBehavior,
904
905 /// Unknown data.
906 pub unknown: u8,
907
908 /// The Part of a Bank assigned to a Pattern.
909 /// Part 1 = 0; Part 2 = 1; Part 3 = 2; Part 4 = 3.
910 /// Credit to [@sezare56 on elektronauts for catching this one](https://www.elektronauts.com/t/octalib-a-simple-octatrack-librarian/225192/27)
911 pub part_assignment: u8,
912
913 /// Pattern setting for Tempo.
914 ///
915 /// The Tempo value is split across both `tempo_1` and `tempo_2`.
916 /// Yet to figure out how they relate to each other.
917 ///
918 /// Value of 120 BPM is 11 for this field.
919 /// Value of 30 BPM is 2 for this field.
920 pub tempo_1: u8,
921
922 /// Pattern setting for Tempo.
923 ///
924 /// The Tempo value is split across both `tempo_1` and `tempo_2`.
925 /// Tet to figure out how they relate to each other.
926 ///
927 /// Value of 120 BPM is `64` for this field.
928 /// Value of 30 BPM is `208` for this field.
929 pub tempo_2: u8,
930}
931
932impl Default for Pattern {
933 fn default() -> Self {
934 Self {
935 header: PATTERN_HEADER,
936 audio_track_trigs: AudioTrackTrigs::defaults(),
937 midi_track_trigs: MidiTrackTrigs::defaults(),
938 scale: PatternScaleSettings::default(),
939 chain_behaviour: PatternChainBehavior::default(),
940 unknown: 0,
941 part_assignment: 0,
942 // **I believe** these two mask values make the tempo 120.0 BPM
943 // don't quote me on that though
944 tempo_1: 11,
945 tempo_2: 64,
946 }
947 }
948}
949
950impl HasHeaderField for Pattern {
951 fn check_header(&self) -> Result<bool, OtToolsIoError> {
952 Ok(self.header == PATTERN_HEADER)
953 }
954}
955
956#[cfg(test)]
957#[allow(unused_imports)]
958mod test {
959
960 mod track_trig_defaults {
961
962 mod audio {
963 use crate::patterns::AudioTrackTrigs;
964 use crate::Defaults;
965
966 fn defs() -> [AudioTrackTrigs; 8] {
967 AudioTrackTrigs::defaults()
968 }
969
970 #[test]
971 fn ok_track_ids() -> Result<(), ()> {
972 for i in 0..8 {
973 println!("Track: {} Track ID: {i}", i + 1);
974 assert_eq!(defs()[i].track_id, i as u8);
975 }
976 Ok(())
977 }
978 }
979 mod midi {
980 use crate::patterns::MidiTrackTrigs;
981 use crate::{Defaults, OtToolsIoError};
982
983 fn defs() -> [MidiTrackTrigs; 8] {
984 MidiTrackTrigs::defaults()
985 }
986
987 #[test]
988 fn ok_track_ids() -> Result<(), ()> {
989 for i in 0..8 {
990 println!("Track: {} Track ID: {i}", i + 1);
991 assert_eq!(defs()[i].track_id, i as u8);
992 }
993 Ok(())
994 }
995 }
996 }
997
998 mod integrity {
999 mod pattern {
1000 // valid header: [0x50, 0x54, 0x52, 0x4e, 0x00, 0x00, 0x00, 0x00];
1001 use crate::patterns::Pattern;
1002 use crate::{HasHeaderField, OtToolsIoError};
1003
1004 #[test]
1005 fn true_valid_header() -> Result<(), OtToolsIoError> {
1006 let pattern = Pattern::default();
1007 assert!(pattern.check_header()?);
1008 Ok(())
1009 }
1010
1011 #[test]
1012 fn false_invalid_header() -> Result<(), OtToolsIoError> {
1013 let mut pattern = Pattern::default();
1014 pattern.header[0] = 0x01;
1015 pattern.header[1] = 0x01;
1016 pattern.header[7] = 0x50;
1017 assert!(!pattern.check_header()?);
1018 Ok(())
1019 }
1020 }
1021 mod audio_track_trigs {
1022 use crate::patterns::AudioTrackTrigs;
1023 use crate::{HasHeaderField, OtToolsIoError};
1024
1025 #[test]
1026 fn true_valid_header() -> Result<(), OtToolsIoError> {
1027 let trigs = AudioTrackTrigs::default();
1028 assert!(trigs.check_header()?);
1029 Ok(())
1030 }
1031
1032 #[test]
1033 fn false_invalid_header() -> Result<(), OtToolsIoError> {
1034 let mut trigs = AudioTrackTrigs::default();
1035 trigs.header[0] = 0x01;
1036 trigs.header[1] = 0x01;
1037 trigs.header[2] = 0x50;
1038 assert!(!trigs.check_header()?);
1039 Ok(())
1040 }
1041 }
1042 mod midi_track_trigs {
1043 use crate::patterns::MidiTrackTrigs;
1044 use crate::{HasHeaderField, OtToolsIoError};
1045
1046 #[test]
1047 fn true_valid_header() -> Result<(), OtToolsIoError> {
1048 let trigs = MidiTrackTrigs::default();
1049 assert!(trigs.check_header()?);
1050 Ok(())
1051 }
1052
1053 #[test]
1054 fn false_invalid_header() -> Result<(), OtToolsIoError> {
1055 let mut trigs = MidiTrackTrigs::default();
1056 trigs.header[0] = 0x01;
1057 trigs.header[1] = 0x01;
1058 trigs.header[2] = 0x50;
1059 assert!(!trigs.check_header()?);
1060 Ok(())
1061 }
1062 }
1063 }
1064}