ot-tools-io 0.9.0

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 © 2024 Mike Robeson [dijksterhuis]
*/

use crate::generics::{Tracks, Trigs};
use crate::parts::{
    LfoParamsValues, MidiTrackArpParamsValues, MidiTrackCc1ParamsValues, MidiTrackCc2ParamsValues,
    MidiTrackMidiParamsValues,
};
use crate::patterns::settings::{TrackPatternSettings, TrackPerTrackModeScale};
use crate::patterns::tracks::TrigRepeatsConditionsAndOffsets;
use crate::{Defaults, HasHeaderField, OtToolsIoError};
use itertools::Itertools;
use ot_tools_io_derive::{ArrayDefaults, AsMutDerive, AsRefDerive, BoxedArrayDefaults};
use serde::{Deserialize, Serialize};
use serde_big_array::BigArray;
use std::array::from_fn;

/// Header array for a MIDI track section in binary data files: `MTRA`
const MIDI_TRACK_HEADER: [u8; 4] = [0x4d, 0x54, 0x52, 0x41];

/// MIDI Track parameter locks.
#[derive(
    Copy,
    Clone,
    Debug,
    Eq,
    Hash,
    Ord,
    PartialEq,
    PartialOrd,
    Serialize,
    Deserialize,
    AsMutDerive,
    AsRefDerive,
    ArrayDefaults,
    BoxedArrayDefaults,
)]
pub struct MidiTrackParameterLocks {
    pub midi: MidiTrackMidiParamsValues,
    pub lfo: LfoParamsValues,
    pub arp: MidiTrackArpParamsValues,
    pub ctrl1: MidiTrackCc1ParamsValues,
    pub ctrl2: MidiTrackCc2ParamsValues,

    #[serde(with = "BigArray")]
    unknown: [u8; 2],
}

impl Default for MidiTrackParameterLocks {
    fn default() -> Self {
        // 255 -> disabled

        // NOTE: the `part.rs` `default` methods for each of these type has
        // fields all set to the correct defaults for the TRACK view, not p-lock
        // trigS. So don't try and use the type's `default` method here as you
        // will end up with a bunch of p-locks on trigs for all the default
        // values. (Although maybe that's a desired feature for some workflows).

        // Yes, this comment is duplicated above. It is to make sur you've seen
        // it.

        Self {
            midi: MidiTrackMidiParamsValues {
                note: 255,
                vel: 255,
                len: 255,
                not2: 255,
                not3: 255,
                not4: 255,
            },
            lfo: LfoParamsValues {
                spd1: 255,
                spd2: 255,
                spd3: 255,
                dep1: 255,
                dep2: 255,
                dep3: 255,
            },
            arp: MidiTrackArpParamsValues {
                tran: 255,
                leg: 255,
                mode: 255,
                spd: 255,
                rnge: 255,
                nlen: 255,
            },
            ctrl1: MidiTrackCc1ParamsValues {
                pb: 255,
                at: 255,
                cc1: 255,
                cc2: 255,
                cc3: 255,
                cc4: 255,
            },
            ctrl2: MidiTrackCc2ParamsValues {
                cc5: 255,
                cc6: 255,
                cc7: 255,
                cc8: 255,
                cc9: 255,
                cc10: 255,
            },
            unknown: [255, 255],
        }
    }
}

/// MIDI Track Trig masks.
/// Can be converted into an array of booleans using the `get_track_trigs_from_bitmasks` function.
/// See `AudioTrackTrigMasks` for more information.
///
/// Trig mask arrays have data stored in this order, which is slightly confusing (pay attention to the difference with 7 + 8!):
/// 1. 1st half of the 4th page
/// 2. 2nd half of the 4th page
/// 3. 1st half of the 3rd page
/// 4. 2nd half of the 3rd page
/// 5. 1st half of the 2nd page
/// 6. 2nd half of the 2nd page
/// 7. 2nd half of the 1st page
/// 8. 1st half of the 1st page
#[derive(
    Copy,
    Clone,
    Debug,
    Eq,
    Hash,
    Ord,
    PartialEq,
    PartialOrd,
    Serialize,
    Deserialize,
    AsMutDerive,
    AsRefDerive,
)]
pub struct MidiTrackTrigMasks {
    /// Note Trig masks.
    pub trigger: Tracks<u8>,

    /// Trigless Trig masks.
    pub trigless: Tracks<u8>,

    /// Parameter Lock Trig masks.
    /// Note this only stores data for exclusive parameter lock *trigs* (light green trigs).
    pub plock: Tracks<u8>,

    /// Swing trigs mask.
    pub swing: Tracks<u8>,

    /// this is a block of 8, so looks like a trig mask for tracks,
    /// but I can't think of what it could be.
    pub unknown: Tracks<u8>,
}

impl Default for MidiTrackTrigMasks {
    fn default() -> Self {
        Self {
            trigger: Tracks::new(from_fn(|_| 0)),
            trigless: Tracks::new(from_fn(|_| 0)),
            plock: Tracks::new(from_fn(|_| 0)),
            swing: Tracks::new(from_fn(|_| 170)),
            unknown: Tracks::new(from_fn(|_| 0)),
        }
    }
}

/// Track trigs assigned on an Audio Track within a Pattern
///
/// No `Copy` trait as the `plocks` has a `Box<T>` type
#[derive(
    Clone,
    Debug,
    Eq,
    Hash,
    Ord,
    PartialEq,
    PartialOrd,
    Serialize,
    Deserialize,
    AsMutDerive,
    AsRefDerive,
    BoxedArrayDefaults,
)]
pub struct MidiTrackTrigs {
    /// Header data section
    ///
    /// example data:
    /// ```text
    /// MTRA
    /// 4d 54 52 41
    /// ```
    #[serde(with = "BigArray")]
    pub header: [u8; 4],

    /// Unknown data.
    #[serde(with = "BigArray")]
    pub unknown_1: [u8; 4],

    /// The zero indexed track number
    pub track_id: u8,

    /// MIDI Track Trig masks contain the Trig step locations for different trig types
    pub trig_masks: MidiTrackTrigMasks,

    /// The scale of this MIDI Track in Per Track Pattern mode.
    pub scale_per_track_mode: TrackPerTrackModeScale,

    /// Amount of swing when a Swing Trig is active for the Track.
    /// Maximum is `30` (`80` on device), minimum is `0` (`50` on device).
    pub swing_amount: u8,

    /// Pattern settings for this MIDI Track
    pub pattern_settings: TrackPatternSettings,

    /// trig properties -- p-locks etc.
    /// the big `0xff` value block within tracks basically.
    /// 32 bytes per trig -- 6x parameters for 5x pages plus 2x extra fields at the end.
    ///
    /// For audio tracks, the 2x extra fields at the end are for sample locks,
    /// but there's no such concept for MIDI tracks.
    /// It seems like Elektron devs reused their data structures for P-Locks on both Audio + MIDI tracks.
    // note -- stack overflow if trying to use #[serde(with = "BigArray")]
    pub plocks: Trigs<MidiTrackParameterLocks>,

    /// See the documentation for `AudioTrackTrigs` on how this field works.
    pub trig_offsets_repeats_conditions: Trigs<TrigRepeatsConditionsAndOffsets>,
}

impl MidiTrackTrigMasks {
    const HALF_PAGE_TRIG_BITMASK_VALUES: [u8; 8] = [1, 2, 4, 8, 16, 32, 64, 128];

    /// Given a half-page trig bit mask, get an array of 8x boolean values
    /// indicating whether each trig in the half-page is active or not
    // note, should be safe to unwrap the option -- any u8 value is valid here.
    fn get_halfpage_trigs_from_bitmask_value(bitmask: &u8) -> [bool; 8] {
        let arr: [bool; 8] = Self::HALF_PAGE_TRIG_BITMASK_VALUES
            .iter()
            .map(|x| (bitmask & x) > 0)
            .collect_array()
            .unwrap();
        arr
    }

    /// Given a half-page trig bit mask, get an array of 8x boolean values
    /// indicating where each trig in the half-page is active or not
    // note, should be safe to unwrap the option (see above).
    pub fn get_track_trigs_from_bitmasks(bitmasks: &[u8; 8]) -> Trigs<bool> {
        let x = bitmasks
            .iter()
            .flat_map(Self::get_halfpage_trigs_from_bitmask_value)
            .collect_array()
            .unwrap();

        Trigs::new(x)
    }

    /// returns a [`Trigs`] type of `bool` values indicating whether there's a trigger trig
    /// for a specific trig position
    pub fn trigger(&self) -> Trigs<bool> {
        Self::get_track_trigs_from_bitmasks(&self.trigger)
    }

    /// returns a [`Trigs`] type of `bool` values indicating whether there's a trigless trig
    /// for a specific trig position
    pub fn trigless(&self) -> Trigs<bool> {
        Self::get_track_trigs_from_bitmasks(&self.trigless)
    }

    /// returns a [`Trigs`] type of `bool` values indicating whether there's a plock trig
    /// for a specific trig position
    pub fn plock(&self) -> Trigs<bool> {
        Self::get_track_trigs_from_bitmasks(&self.plock)
    }

    /// returns a [`Trigs`] type of `bool` values indicating whether there's a swing trig
    /// for a specific trig position
    pub fn swing(&self) -> Trigs<bool> {
        Self::get_track_trigs_from_bitmasks(&self.swing)
    }
}

impl Default for MidiTrackTrigs {
    fn default() -> Self {
        Self {
            header: MIDI_TRACK_HEADER,
            unknown_1: from_fn(|_| 0),
            track_id: 0,
            trig_masks: MidiTrackTrigMasks::default(),
            scale_per_track_mode: TrackPerTrackModeScale::default(),
            swing_amount: 0,
            pattern_settings: TrackPatternSettings::default(),
            plocks: Trigs::default(),
            trig_offsets_repeats_conditions: Trigs::<TrigRepeatsConditionsAndOffsets>::default(),
        }
    }
}

// needs to be manually implemented
impl<const N: usize> Defaults<[Self; N]> for MidiTrackTrigs {
    fn defaults() -> [Self; N]
    where
        Self: Default,
    {
        from_fn(|i| Self {
            track_id: i as u8,
            ..Default::default()
        })
    }
}

#[cfg(test)]
mod midi_track_trigs_defaults {

    use super::MidiTrackTrigs;
    use crate::Defaults;

    fn defs() -> [MidiTrackTrigs; 8] {
        MidiTrackTrigs::defaults()
    }

    #[test]
    fn ok_track_ids() -> Result<(), ()> {
        for i in 0..8 {
            println!("Track: {} Track ID: {i}", i + 1);
            assert_eq!(defs()[i].track_id, i as u8);
        }
        Ok(())
    }
}

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

#[cfg(test)]
mod midi_track_trigs_header {
    use super::MidiTrackTrigs;
    use crate::{
        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 x = BankFile::from_data_file(&path)?.patterns[0]
            .clone()
            .midi_track_trigs;
        assert!(x[0].check_header()?);
        Ok(())
    }

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

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

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