ot_tools_io/
lib.rs

1/*
2SPDX-License-Identifier: GPL-3.0-or-later
3Copyright © 2024 Mike Robeson [dijksterhuis]
4*/
5
6//! Serialization and Deserialization library for Elektron Octatrack data files.
7//!
8//! ## Important types
9//!
10//! | rust type    | octatrack filename pattern | description |
11//! | ------------ | -------------------------- | ------------|
12//! | [`ArrangementFile`] | `arr??.*` | data for arrangements |
13//! | [`BankFile`] | `bank??.*` | data for parts and patterns |
14//! | [`MarkersFile`] | `markers.*` | start trim/end trim/slices/loop points for sample slots |
15//! | [`ProjectFile`] | `project.*` | project level settings; state; sample slots |
16//! | [`SampleSettingsFile`] | `*.ot` | saved sample settings data, loops slices etc. |
17//!
18//!
19//! Only the above types implement the [`OctatrackFileIO`] super trait, meaning
20//! only these types can be read from / written to the filesystem using the
21//! functions in this library.
22//!
23//! Read the relevant modules in this library for more detailed information on
24//! the data contained in each file.
25//!
26//! ## How do different Octatrack data files relate to each other?
27//! - Arrangements store a `u8` which references a Bank's Pattern, indicating it should play for that Arrange row.
28//!   Zero indexed, where 0 (A01) -> 256 (P16).
29//! - A Bank stores zero-indexed sample slot IDs to indicate which sample should be played when on a given track
30//!   (part machine data and/or track p-lock trigs).
31//! - Changing the sample loaded into a sample slot updates both the `project.*` file (change the trig quantization
32//!   settings, file path etc) and the `markers.*` file (change the trim settings based on either initial load,
33//!   or any data in a relevant `*.ot` sample settings file).
34//! - Data from `project.*`and `markers.*` is written to an `*.ot` file when saving sample attributes data from the
35//!   Octatrack's audio editing menu.
36//! - Loading a sample into a project sample slot (`project.*` and `markers.*` files) reads any data in an `*.ot` files
37//!   and configures the sample slot accordingly.
38//!
39//! ## When do `*.work` and `*.strd` files get modified by the Octatrack?
40//!
41//! - `*.work` files are created when creating a new project `PROJECT MENU -> CHANGE PROJECT -> CREATE NEW`.
42//! - `*.work` files are updated by using the `PROJECT MENU -> SYNC TO CARD` operation.
43//! - `*.work` files are updated when the user performs a `PROJECT MENU -> SAVE PROJECT` operation.
44//! - `*.strd` files are created/updated when the user performs a `PROJECT MENU -> SAVE PROJECT` operation.
45//! - `*.strd` files are not changed by the `PROJECT -> SYNC TO CARD` operation.
46//! - `arr??.strd` files can also be saved via the `ARRANGER MENU -> SAVE ARRANGEMENT` operation.
47//!
48//! ## Notable 'gotcha's
49//! - Project sample slots (`project.*` files) are one-indexed, but their references everywhere else are zero-indexed
50//!   (`bank??.*` and `markers.*` files).
51//! - A 'Disabled' loop point in either `markers.*` or `*.ot` files is a `0xFFFFFFFF` value, but the default
52//!   loop point when creating new `markers.*` file is always `0_u32`. The 'Disabled' value setting is only set
53//!   when a sample is loaded into a sample slot, and an `.ot` file is generated from that sample slot data.
54//!
55//! ## Example code
56//! ```rust
57//! /*
58//! reading, mutating and writing a bank file
59//! */
60//!
61//! use std::path::PathBuf;
62//! use ot_tools_io::{OctatrackFileIO, BankFile};
63//!
64//! let path = PathBuf::from("test-data")
65//!     .join("blank-project")
66//!     .join("bank01.work");
67//!
68//! // read an editable version of the bank file
69//! let mut bank = BankFile::from_data_file(&path).unwrap();
70//!
71//! // change active scenes on the working copy of Part 4
72//! bank.parts.unsaved[3].active_scenes.scene_a = 2;
73//! bank.parts.unsaved[3].active_scenes.scene_b = 6;
74//!
75//! // write to a new bank file
76//! let outfpath = std::env::temp_dir()
77//!     .join("ot-tools-io")
78//!     .join("doctest")
79//!     .join("main_example_1");
80//!
81//! # // when running in cicd env the /tmp/ot-tools-io directory doesn't exist yet
82//! # let _ = std::fs::create_dir_all(outfpath.parent().unwrap());
83//!
84//! &bank.to_data_file(&outfpath).unwrap();
85//! ```
86
87pub mod arrangements;
88pub mod banks;
89mod common_options;
90mod err;
91pub mod markers;
92pub mod parts;
93pub mod patterns;
94pub mod projects;
95pub mod samples;
96pub mod slices;
97#[cfg(test)]
98#[allow(dead_code)]
99mod test_utils;
100mod traits;
101
102pub use crate::arrangements::ArrangementFile;
103pub use crate::banks::BankFile;
104pub use crate::common_options::*;
105pub use crate::err::*;
106pub use crate::markers::MarkersFile;
107pub use crate::projects::ProjectFile;
108pub use crate::samples::SampleSettingsFile;
109pub use crate::traits::*;
110use std::error::Error;
111use std::fs::File;
112use std::io::{Read, Write};
113use std::path::Path;
114
115// todo: sized errors so not necessary to keep Boxing error enum varients
116/// Shorthand type alias for a Result with a Boxed Error
117type RBoxErr<T> = Result<T, Box<dyn Error>>;
118
119fn u8_bytes_to_u16(bytes: &[u8; 2]) -> u16 {
120    ((bytes[0] as u16) << 8) | bytes[1] as u16
121}
122
123#[doc(hidden)]
124/// Read bytes from a file at `path`.
125// ```rust
126// let fpath = std::path::PathBuf::from("test-data")
127//     .join("blank-project")
128//     .join("bank01.work");
129// let r = ot_tools_io::read_bin_file(&fpath);
130// assert!(r.is_ok());
131// assert_eq!(r.unwrap().len(), 636113);
132//```
133fn read_bin_file(path: &Path) -> RBoxErr<Vec<u8>> {
134    let mut infile = File::open(path)?;
135    let mut bytes: Vec<u8> = vec![];
136    let _: usize = infile.read_to_end(&mut bytes)?;
137    Ok(bytes)
138}
139
140#[doc(hidden)]
141/// Write bytes to a file at `path`.
142// ```rust
143// use std::env::temp_dir;
144// use std::array::from_fn;
145//
146// let arr: [u8; 27] = from_fn(|_| 0);
147//
148// let fpath = temp_dir()
149//    .join("ot-tools-io")
150//    .join("doctest")
151//    .join("write_bin_file.example");
152//
153// # use std::fs::create_dir_all;
154// # create_dir_all(&fpath.parent().unwrap()).unwrap();
155// let r = ot_tools_io::write_bin_file(&arr, &fpath);
156// assert!(r.is_ok());
157// assert!(fpath.exists());
158// ```
159fn write_bin_file(bytes: &[u8], path: &Path) -> RBoxErr<()> {
160    let mut file: File = File::create(path)?;
161    file.write_all(bytes)?;
162    Ok(())
163}
164
165#[doc(hidden)]
166/// Read a file at `path` as a string.
167// ```
168// let fpath = std::path::PathBuf::from("test-data")
169//     .join("blank-project")
170//     .join("bank01.work");
171// let r = ot_tools_io::read_bin_file(&fpath);
172// assert!(r.is_ok());
173// assert_eq!(r.unwrap().len(), 636113);
174// ```
175fn read_str_file(path: &Path) -> RBoxErr<String> {
176    let mut file = File::open(path)?;
177    let mut string = String::new();
178    let _ = file.read_to_string(&mut string)?;
179    Ok(string)
180}
181
182#[doc(hidden)]
183/// Write a string to a file at `path`.
184// ```rust
185// use std::env::temp_dir;
186//
187// let data = "abcd".to_string();
188//
189// let fpath = temp_dir()
190//    .join("ot-tools-io")
191//    .join("doctest")
192//    .join("write_str_file.example");
193//
194// # use std::fs::create_dir_all;
195// # create_dir_all(&fpath.parent().unwrap()).unwrap();
196// let r = ot_tools_io::write_str_file(&data, &fpath);
197// assert!(r.is_ok());
198// assert!(fpath.exists());
199// ```
200fn write_str_file(string: &str, path: &Path) -> RBoxErr<()> {
201    let mut file: File = File::create(path)?;
202    write!(file, "{string}")?;
203    Ok(())
204}