ot-tools-io 0.11.2

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::Defaults;
use ot_tools_io_derive::{ArrayDefaults, AsMutDerive, AsRefDerive, BoxedArrayDefaults};
use serde::{Deserialize, Serialize};
use std::array::from_fn;

pub(crate) mod audio;
pub(crate) mod midi;

/// # Trig Repeats, Conditions and Offsets
///
/// This is ..... a slightly frustrating implementation.
///
/// There is a 64 length array consisting of a pair of bytes.
/// Each pair of elements actually represents three different types of data...
/// Trig Counts and Trig Conditions use the two bytes independently,
/// so they're easier to explain first.
///
/// After that -- see the section on Trig Offsets below, which makes everything more complicated.
///
/// ## Trig Counts and Trig Conditions
///
/// Trig Counts and Trig Conditions data is interleaved for each trig.
/// For Trig position 1, array index 0 is the count value and array index 1 is the Trig
/// Condition.
///
/// For trig counts (1st byte), the value (zero-indexed) is multiplied by 32.
/// - 8 trig counts (7 repeats) --> 7 * 3 = 224
/// - 4 trig counts (3 repeats) -- 3 * 32 = 96
/// - 1 trig counts (0 repeats) -- 0 * 32 = 0
///
/// For conditionals, see the `TrigCondition` enum and associated traits for more details.
/// The maximum value for a Trig Condition byte is 64.
///
/// ```rust
/// // no trig micro-timings at all
/// [
///     // trig 1
///     [
///         0,   // trig counts (number)
///         0,   // trig condition (enum rep)
///     ],
///     // trig 2
///     [
///         224, // trig counts (max value)
///         64,  // trig condition (max value)
///     ],
///     // trig 3
///     [
///         32,  // trig counts (minimum non-zero value)
///         1,   // trig condition (minimum non-zero value)
///     ],
///     // ... and so on
/// ];
/// ```
///
/// ## Trig Offsets
///
/// Trig Offset values use both of these interleaved bytes on top of the
/// trig repeat and trig condition values... Which makes life more complex
/// and somewhat frustrating.
///
/// Inspected values
/// - -23/384 -> 1st byte 20, 2nd byte 128
/// - -1/32 -> 1st byte 26, 2nd byte 0
/// - -1/64 -> 1st byte 29, 2nd byte 0
/// - -1/128 -> 1st byte 30, 2nd byte 128
/// - 1/128 -> 1st byte 1, 2nd byte 128
/// - 1/64 -> 1st byte 3, 2nd byte 0
/// - 1/32 -> 1st byte 6, 2nd byte 0
/// - 23/384 -> 1st byte 11, 2nd byte 128
///
/// ### 1st byte
/// The 1st byte only has 31 possible values: 255 - 224 (trig count max) = 31.
/// So it makes sense sort of that this is a mask? I guess?
///
/// ### 2nd byte
/// From what I can tell, the second offset byte is either 0 or 128.
/// So a 2nd byte for an offset adjusted trig with a `8:8` trig condition is either
/// - 128 + 64 = 192
/// - 0 + 64 = 64
///
/// So you will need to a `x.rem_euclid(128)` somewhere if you want to parse this.
///
/// Combining the trig offset with trig count and trig conditions, we end up with
/// ```rust
/// [
///     // trig one, -23/384 offset with 1x trig count and None condition
///     [
///         20,  // 20 + (32 * 0)
///         128, // 128 + 0
///     ],
///     // trig two, -23/384 offset with 2x trig count and Fill condition
///     [
///         52,  // 20 + (32 * 1)
///         129, // 128 + 1
///     ],
///     // trig three, -23/384 offset with 3x trig count and Fill condition
///     [
///         84,  // 20 + (32 * 2)
///         129, // 128 + 1
///     ],
///     // trig four, -23/384 offset with 3x trig count and NotFill condition
///     [
///         84,  // 20 + (32 * 2)
///         130, // 128 + 2
///     ],
///     // trig five, +1/32 offset with 2x trig count and Fill condition
///     [
///         38,  // 6 + (32 * 1)
///         1,   // 0 + 1
///     ],
///     // trig six, +1/32 offset with 3x trig count and Fill condition
///     [
///         70,  // 6 + (32 * 2)
///         1,   // 0 + 1
///     ],
///     // trig seven, +1/32 offset with 3x trig count and NotFill condition
///     [
///         70,  // 6 + (32 * 2)
///         2,   // 0 + 2
///     ],
///     // .... and so on
/// ];
/// ```
///
/// ### Extending pages and offsets
///
/// If you have a trig offset on Trig 1 with only one pattern page activated,
/// the trig offsets for Trig 1 are replicated over the relevant trig
/// positions for each first trig in the inactive pages in this array.
///
/// So, for a 1/32 offset on trig 1 with only one page active, you get the
/// following values showing up in this array:
/// - pair of bytes at array index 15 -> 1/32
/// - pair of bytes at array index 31 -> 1/32
/// - pair of bytes at array index 47 -> 1/32
///
/// This does not happen for offset values at any other trig position
/// (from what I can tell in my limited testing -- trig values 2-4 and 9-11
/// inclusive are not replicated in the same way).
///
/// This 'replicating trig offset values over unused pages' behaviour does
/// not happen for trig counts. I haven't tested whether this applies to trig
/// conditions yet.
///
/// It seems that this behaviour could be to make sure the octatrack plays
/// correctly offset trigs when you extend a page live, i.e. when extending
/// a one-page pattern to a two-page pattern, if there is a negative offset
/// value there the octatrack will need to play the offset trig before the
/// first page has completed.
///
/// Or it could be a bug :shrug:
#[derive(
    Clone,
    Debug,
    Default,
    Eq,
    Hash,
    Ord,
    PartialEq,
    PartialOrd,
    Serialize,
    Deserialize,
    AsMutDerive,
    AsRefDerive,
    ArrayDefaults,
    BoxedArrayDefaults,
)]
pub struct TrigRepeatsConditionsAndOffsets {
    /// Number of trig repeats -- WARNING: also contains interleaved data for trig offsets!
    pub count: u8,
    /// Trig trigger probability condition -- WARNING: also contains interleaved data for trig offsets!
    pub condition: u8,
}