ot-tools-io 0.10.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 © 2026 Mike Robeson [dijksterhuis]
*/

use super::{ArrangeRow, ArrangementError, ARRANGEMENT_DEFAULT_NAME};
use crate::generics::ArrangeRows;
use crate::{Defaults, IsDefault, OtToolsIoError};
use itertools::Itertools;
use ot_tools_io_derive::{AsMutDerive, AsRefDerive};
use serde::{Deserialize, Serialize};
use std::array::from_fn;

/// Base model for an arrangement 'block' within an arrangement binary data file.
/// There are two arrangement 'blocks' in each arrangement file -- enabling the
/// arrangement 'reload ' functionality.
#[derive(
    Copy,
    Clone,
    Debug,
    Eq,
    Hash,
    Ord,
    PartialEq,
    PartialOrd,
    Deserialize,
    Serialize,
    AsRefDerive,
    AsMutDerive,
)]
pub struct Arrangement {
    /// Name of the Arrangement in ASCII values, max length 15 characters
    pub name: [u8; 15], // String,

    /// Unknown data. No idea what this is. Usually `[0, 0]` or `[0, 1]`
    pub unknown_1: [u8; 2],

    /// Number of active rows in the arrangement. Any parsed row data after this number of rows
    /// should be an `ArrangeRow::EmptyRow` variant.
    ///
    /// # WARNING
    /// The maximum number of `ArrangeRows` (256) is a zero value here!
    /// Zero rows (0) are also possible.
    /// You need to check for the presence of rows when using this field!
    pub n_rows: u8,

    /// Rows of the arrangement.
    pub rows: ArrangeRows<ArrangeRow>,
}

impl Default for Arrangement {
    fn default() -> Self {
        Self {
            name: ARRANGEMENT_DEFAULT_NAME,
            unknown_1: from_fn(|_| 0),
            n_rows: 0,
            rows: ArrangeRows::default(),
        }
    }
}

impl IsDefault for Arrangement {
    fn is_default(&self) -> bool {
        let default = &Self::default();

        // when the octatrack creates a new arrangement file, it will reuse a
        // name from a previously created arrangement in a different project
        //
        // no idea why it does this (copying the other file?) but it does it
        // reliably when creating a new project from the project menu.
        default.unknown_1 == self.unknown_1
            && default.n_rows == self.n_rows
            && default.rows == self.rows
    }
}

impl Arrangement {
    /// Create a new arrangement
    pub fn new(
        name: [u8; 15],
        unknown_1: [u8; 2],
        n_rows: u8,
        rows: ArrangeRows<ArrangeRow>,
    ) -> Self {
        Self {
            name,
            unknown_1,
            n_rows,
            rows,
        }
    }

    /// Find the position of the first empty row.
    /// Essentially the real length of the arrangement.
    ///
    /// Will return `0` if the arrangement has no [`ArrangeRow::EmptyRow`] rows.
    /// See explanation in the `n_rows` field for more information.
    ///
    /// ```rust
    /// # use ot_tools_io::{ArrangementFile, OtToolsIoError};
    /// # use ot_tools_io::types::{Arrangement, ArrangeRows, ArrangeRow, ReminderRow};
    /// # fn main() -> Result<(), OtToolsIoError> {
    /// let row = ArrangeRow::ReminderRow(
    ///     ReminderRow::new_from_string("HELLO WORLD!   ")?
    /// );
    ///
    /// // no empty rows
    /// let mut rows = ArrangeRows::new(std::array::from_fn(|_| row));
    /// let arr = Arrangement { rows, ..Default::default()};
    /// assert_eq!(arr.first_empty_row_index(), 0);
    ///
    /// // some empty rows
    /// let mut rows = ArrangeRows::<ArrangeRow>::default();
    /// for i in 0..10 {
    ///     rows[i] = row;
    /// }
    /// let arr = Arrangement { rows, ..Default::default()};
    /// assert_eq!(arr.first_empty_row_index(), 10);
    ///
    /// // only empty rows
    /// let rows = ArrangeRows::<ArrangeRow>::default();
    /// let arr = Arrangement { rows, ..Default::default()};
    /// assert_eq!(arr.first_empty_row_index(), 0);
    ///
    /// # Ok(()) }
    /// ```
    pub fn first_empty_row_index(&self) -> u8 {
        self.rows
            .iter()
            .find_position(|x| matches!(x, ArrangeRow::EmptyRow(_)))
            .map(|x| x.0 as u8)
            // no empty rows -- 256 rows in an arrangement means we wrap around
            // and return 0
            .unwrap_or(0)
    }

    /// Run validation on relevant fields, see the documentation on fields in
    /// [`Self`] to find out what is actually validated.
    pub fn validate(&self) -> Result<(), OtToolsIoError> {
        // bytes in the name equal 0 if "unset" i.e. never set before
        if self
            .name
            .iter()
            .any(|x| super::is_not_valid_ascii_byte(*x) && *x != 0)
        {
            return Err(ArrangementError::Name(self.name).into());
        }

        let (n_rows, first_empty_idx) = (self.n_rows, self.first_empty_row_index());
        if n_rows != first_empty_idx {
            return Err(ArrangementError::RowCount {
                n_rows,
                first_empty_idx,
            }
            .into());
        }

        for row in self.rows.iter() {
            match row {
                ArrangeRow::PatternRow(x) => x.validate()?,
                ArrangeRow::LoopOrJumpOrHaltRow(x) => x.validate()?,
                ArrangeRow::ReminderRow(x) => x.validate()?,
                _ => {}
            }
        }

        Ok(())
    }

    /// We can't just use [`bincode::serialize`] with as we need to do some
    /// custom parsing around `n_rows` and the rows themselves
    pub(super) fn parse_from_bytes(bytes: &[u8; 5650]) -> Result<Self, OtToolsIoError> {
        let name: [u8; 15] = from_fn(|x| bytes[x]);
        let unknown_1: [u8; 2] = from_fn(|x| bytes[x + 15]);
        let n_rows = bytes[17];

        let mut rows: [ArrangeRow; 256] = ArrangeRow::defaults();
        for (idx, row) in rows.iter_mut().enumerate() {
            /*
            IMPORTANT: @dijksterhuis:
            It's not possible to work out if a row should be an
            `ArrangeRow::EmptyRow` exclusively from the bytes for that row.

            An `ArrangeRow::EmptyRow` variant is only used when the current
            row's index is greater than or equal to `n_rows` in an `Arrangement`.
            */
            match idx >= n_rows as usize {
                true => {
                    let offset = 18;
                    let idx_start = offset + (idx * 22);
                    let row_bytes: [u8; 22] = from_fn(|j| bytes[j + idx_start]);
                    *row = ArrangeRow::EmptyRow(row_bytes);
                }
                false => {
                    let offset = 18;
                    let idx_start = offset + (idx * 22);
                    let row_bytes: [u8; 22] = from_fn(|j| bytes[j + idx_start]);
                    *row = ArrangeRow::parse_from_bytes(&row_bytes)?;
                }
            }
        }

        let rows = ArrangeRows::new(rows);

        let block = Arrangement {
            name,
            unknown_1,
            n_rows,
            rows,
        };

        block.validate()?;
        Ok(block)
    }

    /// We can't just use [`bincode::serialize`] with as we need to do some
    /// custom parsing around each row
    pub(super) fn parse_to_bytes(&self) -> Result<Vec<u8>, OtToolsIoError> {
        let mut v = vec![];

        v.append(&mut self.name.to_vec());
        v.append(&mut self.unknown_1.to_vec());
        v.append(&mut vec![self.n_rows]);
        for row in self.rows.iter() {
            v.append(&mut row.parse_to_bytes()?);
        }

        Ok(v)
    }
}

#[cfg(test)]
mod new_and_validate {
    use crate::arrangements::{ArrangeRow, Arrangement, ArrangementError, LoopOrJumpOrHaltRow};
    use crate::generics::ArrangeRows;
    use crate::OtToolsIoError;

    #[test]
    fn ok() -> Result<(), OtToolsIoError> {
        let arr = Arrangement::new(
            std::array::from_fn(|_| 48),
            [0, 0],
            0,
            ArrangeRows::default(),
        );
        arr.validate()?;
        Ok(())
    }

    #[test]
    fn invalid_rows() -> Result<(), OtToolsIoError> {
        let arr = Arrangement::new(
            std::array::from_fn(|_| 48),
            [0, 0],
            20,
            ArrangeRows::default(),
        );
        assert_eq!(
            arr.validate().err().unwrap().to_string(),
            OtToolsIoError::Arrangement(ArrangementError::RowCount {
                n_rows: 20,
                first_empty_idx: 0
            })
            .to_string(),
        );
        Ok(())
    }

    #[test]
    fn invalid_name() -> Result<(), OtToolsIoError> {
        let arr = Arrangement::new(
            [20, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48],
            [0, 0],
            0,
            ArrangeRows::default(),
        );
        assert_eq!(
            arr.validate().err().unwrap().to_string(),
            OtToolsIoError::Arrangement(ArrangementError::Name([
                20, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 48
            ]))
            .to_string(),
        );
        Ok(())
    }
    #[test]
    fn invalid_row() -> Result<(), OtToolsIoError> {
        let mut rows = ArrangeRows::default();

        let bad_row = ArrangeRow::LoopOrJumpOrHaltRow(LoopOrJumpOrHaltRow {
            loop_count: 200,
            row_target: 0,
            unused: Default::default(),
        });

        rows[0] = bad_row;

        let arr = Arrangement::new(std::array::from_fn(|_| 48), [0, 0], 1, rows);
        assert_eq!(
            arr.validate().err().unwrap().to_string(),
            OtToolsIoError::Arrangement(ArrangementError::LoopCount(200)).to_string(),
        );
        Ok(())
    }
}

#[cfg(test)]
mod parse_to_bytes {
    use crate::arrangements::{ArrangeRow, Arrangement, PatternRow};
    use crate::generics::ArrangeRows;

    #[test]
    fn arrangement_block() {
        let expected_rows: [ArrangeRow; 256] = std::array::from_fn(|i| {
            if i < 10 {
                ArrangeRow::PatternRow(PatternRow {
                    pattern_id: 1,
                    repetitions: 1,
                    unused_1: 0,
                    mute_mask: 1,
                    unused_2: 0,
                    tempo_1: 1,
                    tempo_2: 1,
                    scene_a: 1,
                    scene_b: 1,
                    unused_3: 0,
                    offset: 1,
                    unused_4: 0,
                    length: 1,
                    midi_transpose: [8, 1, 1, 1, 1, 1, 1, 8],
                })
            } else {
                ArrangeRow::default()
            }
        });

        let rows = ArrangeRows::new(expected_rows);

        let expected = Arrangement {
            name: [10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10],
            unknown_1: [10, 9],
            n_rows: 10,
            rows,
        };

        // // TODO: Need to do modulo on index to create pattern row data
        // let _: [u8; 5652] = std::array::from_fn(|x| {
        //     match x {
        //         // start name
        //         0 => 10,
        //         // end name
        //         13 => 10,
        //         // unk1 start
        //         14 => 10,
        //         // unk2 end
        //         15 => 9,
        //         // n rows
        //         16 => 10,
        //         // unk2 start
        //         5650 => 10,
        //         // unk2 end
        //         5651 => 9,
        //         _ => 0,
        //     }
        // });
        // let r = bincode::serialize(&expected);
        let r = expected.parse_to_bytes();
        println!("{r:?}");
        assert!(r.is_ok());
        let v = r.unwrap();
        assert_eq!(5650, v.len());
    }
}

#[cfg(test)]
mod parse_from_bytes {
    use crate::arrangements::mocks::mock_arrangement_block_empty_rows;
    use crate::arrangements::Arrangement;

    #[test]
    fn valid_empty_rows() {
        let (expected, test_bytes) = mock_arrangement_block_empty_rows();
        let r = Arrangement::parse_from_bytes(&test_bytes);
        assert_eq!(expected, r.unwrap());
    }
}