Skip to main content

ot_tools_io/
patterns.rs

1/*
2SPDX-License-Identifier: GPL-3.0-or-later
3Copyright © 2026 Mike Robeson [dijksterhuis]
4*/
5
6//! Models for pattern data within a bank.
7
8mod settings;
9mod tracks;
10
11use crate::{Defaults, HasHeaderField, OtToolsIoError};
12use ot_tools_io_derive::{
13    ArrayDefaults, AsMutDerive, AsRefDerive, BoxedArrayDefaults, IsDefaultCheck,
14};
15pub use settings::{
16    PatternChainBehavior, PatternScaleSettings, TrackPatternSettings, TrackPerTrackModeScale,
17};
18use std::array::from_fn;
19pub use tracks::{
20    audio::AudioTrackParameterLockPlayback, audio::AudioTrackParameterLocks,
21    audio::AudioTrackTrigMasks, audio::AudioTrackTrigs, midi::MidiTrackParameterLocks,
22    midi::MidiTrackTrigMasks, midi::MidiTrackTrigs, TrigRepeatsConditionsAndOffsets,
23};
24
25use crate::generics::Tracks;
26use serde::{Deserialize, Serialize};
27use serde_big_array::BigArray;
28
29const PATTERN_HEADER: [u8; 8] = [0x50, 0x54, 0x52, 0x4e, 0x00, 0x00, 0x00, 0x00];
30
31/// A pattern of trigs stored in the bank.
32#[derive(
33    Clone,
34    Debug,
35    Eq,
36    Hash,
37    Ord,
38    PartialEq,
39    PartialOrd,
40    Serialize,
41    Deserialize,
42    AsMutDerive,
43    AsRefDerive,
44    ArrayDefaults,
45    BoxedArrayDefaults,
46    IsDefaultCheck,
47)]
48pub struct Pattern {
49    /// Header indicating start of pattern section
50    ///
51    /// example data:
52    /// ```text
53    /// PTRN....
54    /// 50 54 52 4e 00 00 00 00
55    /// ```
56    #[serde(with = "BigArray")]
57    pub header: [u8; 8],
58
59    /// Audio Track data
60    pub audio_track_trigs: Tracks<AudioTrackTrigs>,
61
62    /// MIDI Track data
63    pub midi_track_trigs: Tracks<MidiTrackTrigs>,
64
65    /// Pattern scaling controls and settings
66    pub scale: PatternScaleSettings,
67
68    /// Pattern chaining behaviour and settings
69    pub chain_behaviour: PatternChainBehavior,
70
71    /// Unknown data.
72    pub unknown: u8,
73
74    /// The Part of a Bank assigned to a Pattern.
75    /// Part 1 = 0; Part 2 = 1; Part 3 = 2; Part 4 = 3.
76    /// Credit to [@sezare56 on elektronauts for catching this one](https://www.elektronauts.com/t/octalib-a-simple-octatrack-librarian/225192/27)
77    pub part_assignment: u8,
78
79    /// Pattern setting for Tempo.
80    ///
81    /// The Tempo value is split across both `tempo_1` and `tempo_2`.
82    /// Yet to figure out how they relate to each other.
83    ///
84    /// Value of 120 BPM is 11 for this field.
85    /// Value of 30 BPM is 2 for this field.
86    pub tempo_1: u8,
87
88    /// Pattern setting for Tempo.
89    ///
90    /// The Tempo value is split across both `tempo_1` and `tempo_2`.
91    /// Tet to figure out how they relate to each other.
92    ///
93    /// Value of 120 BPM is `64` for this field.
94    /// Value of 30 BPM is `208` for this field.
95    pub tempo_2: u8,
96}
97
98impl Default for Pattern {
99    fn default() -> Self {
100        Self {
101            header: PATTERN_HEADER,
102            audio_track_trigs: Tracks::<AudioTrackTrigs>::default(),
103            midi_track_trigs: Tracks::<MidiTrackTrigs>::default(),
104            scale: PatternScaleSettings::default(),
105            chain_behaviour: PatternChainBehavior::default(),
106            unknown: 0,
107            part_assignment: 0,
108            // **I believe** these two mask values make the tempo 120.0 BPM
109            // don't quote me on that though
110            tempo_1: 11,
111            tempo_2: 64,
112        }
113    }
114}
115
116impl HasHeaderField for Pattern {
117    fn check_header(&self) -> Result<bool, OtToolsIoError> {
118        Ok(self.header == PATTERN_HEADER)
119    }
120}
121
122#[cfg(test)]
123mod pattern_header {
124    use crate::{
125        patterns::Pattern, test_utils::get_blank_proj_dirpath, BankFile, HasHeaderField,
126        OctatrackFileIO, OtToolsIoError,
127    };
128    #[test]
129    fn file_read_valid() -> Result<(), OtToolsIoError> {
130        let path = get_blank_proj_dirpath().join("bank01.work");
131        let pattern = BankFile::from_data_file(&path)?.patterns[0].clone();
132        assert!(pattern.check_header()?);
133        Ok(())
134    }
135
136    #[test]
137    fn file_read_invalid() -> Result<(), OtToolsIoError> {
138        let path = get_blank_proj_dirpath().join("bank01.work");
139        let mut pattern = BankFile::from_data_file(&path)?.patterns[0].clone();
140        pattern.header[0] = 254;
141        pattern.header[1] = 254;
142        pattern.header[2] = 254;
143        pattern.header[3] = 254;
144        assert!(!pattern.check_header()?);
145        Ok(())
146    }
147
148    #[test]
149    fn default_valid() -> Result<(), OtToolsIoError> {
150        let pattern = Pattern::default();
151        assert!(pattern.check_header()?);
152        Ok(())
153    }
154
155    #[test]
156    fn default_invalid() -> Result<(), OtToolsIoError> {
157        let mut pattern = Pattern::default();
158        pattern.header[0] = 0x01;
159        pattern.header[1] = 0x01;
160        pattern.header[7] = 0x50;
161        assert!(!pattern.check_header()?);
162        Ok(())
163    }
164}