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