ot-tools-io 0.11.3

A library crate for reading/writing binary data files used by the Elektron Octatrack DPS-1.
Documentation
/*
SPDX-License-Identifier: GPL-3.0-or-later
Copyright © 2026 Mike Robeson [dijksterhuis]
*/

//! Models for pattern data within a bank.

mod settings;
mod tracks;

use crate::{Defaults, HasHeaderField, OtToolsIoError};
use ot_tools_io_derive::{
    ArrayDefaults, AsMutDerive, AsRefDerive, BoxedArrayDefaults, IsDefaultCheck,
};
pub use settings::{
    PatternChainBehavior, PatternScaleSettings, TrackPatternSettings, TrackPerTrackModeScale,
};
use std::array::from_fn;
pub use tracks::{
    audio::AudioTrackParameterLockPlayback, audio::AudioTrackParameterLocks,
    audio::AudioTrackTrigMasks, audio::AudioTrackTrigs, midi::MidiTrackParameterLocks,
    midi::MidiTrackTrigMasks, midi::MidiTrackTrigs, TrigRepeatsConditionsAndOffsets,
};

use crate::generics::Tracks;
use serde::{Deserialize, Serialize};
use serde_big_array::BigArray;

const PATTERN_HEADER: [u8; 8] = [0x50, 0x54, 0x52, 0x4e, 0x00, 0x00, 0x00, 0x00];

/// A pattern of trigs stored in the bank.
#[derive(
    Clone,
    Debug,
    Eq,
    Hash,
    Ord,
    PartialEq,
    PartialOrd,
    Serialize,
    Deserialize,
    AsMutDerive,
    AsRefDerive,
    ArrayDefaults,
    BoxedArrayDefaults,
    IsDefaultCheck,
)]
pub struct Pattern {
    /// Header indicating start of pattern section
    ///
    /// example data:
    /// ```text
    /// PTRN....
    /// 50 54 52 4e 00 00 00 00
    /// ```
    #[serde(with = "BigArray")]
    pub header: [u8; 8],

    /// Audio Track data
    pub audio_track_trigs: Tracks<AudioTrackTrigs>,

    /// MIDI Track data
    pub midi_track_trigs: Tracks<MidiTrackTrigs>,

    /// Pattern scaling controls and settings
    pub scale: PatternScaleSettings,

    /// Pattern chaining behaviour and settings
    pub chain_behaviour: PatternChainBehavior,

    /// Unknown data.
    pub unknown: u8,

    /// The Part of a Bank assigned to a Pattern.
    /// Part 1 = 0; Part 2 = 1; Part 3 = 2; Part 4 = 3.
    /// Credit to [@sezare56 on elektronauts for catching this one](https://www.elektronauts.com/t/octalib-a-simple-octatrack-librarian/225192/27)
    pub part_assignment: u8,

    /// Pattern setting for Tempo.
    ///
    /// The Tempo value is split across both `tempo_1` and `tempo_2`.
    /// Yet to figure out how they relate to each other.
    ///
    /// Value of 120 BPM is 11 for this field.
    /// Value of 30 BPM is 2 for this field.
    pub tempo_1: u8,

    /// Pattern setting for Tempo.
    ///
    /// The Tempo value is split across both `tempo_1` and `tempo_2`.
    /// Tet to figure out how they relate to each other.
    ///
    /// Value of 120 BPM is `64` for this field.
    /// Value of 30 BPM is `208` for this field.
    pub tempo_2: u8,
}

impl Default for Pattern {
    fn default() -> Self {
        Self {
            header: PATTERN_HEADER,
            audio_track_trigs: Tracks::<AudioTrackTrigs>::default(),
            midi_track_trigs: Tracks::<MidiTrackTrigs>::default(),
            scale: PatternScaleSettings::default(),
            chain_behaviour: PatternChainBehavior::default(),
            unknown: 0,
            part_assignment: 0,
            // **I believe** these two mask values make the tempo 120.0 BPM
            // don't quote me on that though
            tempo_1: 11,
            tempo_2: 64,
        }
    }
}

impl HasHeaderField for Pattern {
    fn check_header(&self) -> Result<bool, OtToolsIoError> {
        Ok(self.header == PATTERN_HEADER)
    }
}

#[cfg(test)]
mod pattern_header {
    use crate::{
        patterns::Pattern, test_utils::get_blank_proj_dirpath, BankFile, HasHeaderField,
        OctatrackFileIO, OtToolsIoError,
    };
    #[test]
    fn file_read_valid() -> Result<(), OtToolsIoError> {
        let path = get_blank_proj_dirpath().join("bank01.work");
        let pattern = BankFile::from_data_file(&path)?.patterns[0].clone();
        assert!(pattern.check_header()?);
        Ok(())
    }

    #[test]
    fn file_read_invalid() -> Result<(), OtToolsIoError> {
        let path = get_blank_proj_dirpath().join("bank01.work");
        let mut pattern = BankFile::from_data_file(&path)?.patterns[0].clone();
        pattern.header[0] = 254;
        pattern.header[1] = 254;
        pattern.header[2] = 254;
        pattern.header[3] = 254;
        assert!(!pattern.check_header()?);
        Ok(())
    }

    #[test]
    fn default_valid() -> Result<(), OtToolsIoError> {
        let pattern = Pattern::default();
        assert!(pattern.check_header()?);
        Ok(())
    }

    #[test]
    fn default_invalid() -> Result<(), OtToolsIoError> {
        let mut pattern = Pattern::default();
        pattern.header[0] = 0x01;
        pattern.header[1] = 0x01;
        pattern.header[7] = 0x50;
        assert!(!pattern.check_header()?);
        Ok(())
    }
}