ot_tools_io/lib.rs
1/*
2SPDX-License-Identifier: GPL-3.0-or-later
3Copyright © 2026 Mike Robeson [dijksterhuis]
4*/
5
6//! Library crate for reading/writing data files for the [Elektron Octatrack][0].
7//!
8//! ```rust
9//! // reading, mutating and writing a bank file
10//!
11//! use std::path::PathBuf;
12//! use ot_tools_io::{OctatrackFileIO, BankFile};
13//!
14//! let path = PathBuf::from("test-data")
15//! .join("blank-project")
16//! .join("bank01.work");
17//!
18//! // read an editable version of the bank file
19//! let mut bank = BankFile::from_data_file(&path).unwrap();
20//!
21//! // change active scenes on the working copy of Part 4
22//! bank.parts_unsaved[3].active_scenes.scene_a = 2;
23//! bank.parts_unsaved[3].active_scenes.scene_b = 6;
24//!
25//! // write to a new bank file
26//! let outfpath = std::env::temp_dir()
27//! .join("ot-tools-io")
28//! .join("doctest")
29//! .join("main_example_1");
30//!
31//! # // when running in cicd env the /tmp/ot-tools-io directory doesn't exist yet
32//! # let _ = std::fs::create_dir_all(outfpath.parent().unwrap());
33//! bank.to_data_file(&outfpath).unwrap();
34//! ```
35//!
36//! ## Brief Overview
37//!
38//! Important types and traits are re-exported or defined in the root of the crate's namespace for
39//! your convenience -- everything you need to read and write Octatrack data files should be
40//! available with `use ot_tools_io::*`. Less commonly used types and traits are public within their
41//! specific modules and will require importing.
42//! ```rust
43//! // basic / common imports
44//! use ot_tools_io::{SampleSettingsFile, OctatrackFileIO, HasHeaderField};
45//!
46//! // lower level / less common imports
47//! use ot_tools_io::samples::{SAMPLES_HEADER, SAMPLES_FILE_VERSION};
48//! use ot_tools_io::slices::{Slice, SLICE_LOOP_POINT_DISABLED};
49//! use ot_tools_io::parts::{Part, AudioTrackMachineParamsSetupPickup};
50//! ```
51//!
52//! #### The `OctatrackFileIO` Trait
53//! You'll usually want to import the [`OctatrackFileIO`] trait if you're reading or writing files.
54//! It adds the associated functions & methods for file I/O, including for file I/O for YAML and
55//! JSON files.
56//! ```rust
57//! // write a sample settings file to yaml and json
58//!
59//! use std::path::PathBuf;
60//! use ot_tools_io::{SampleSettingsFile, OctatrackFileIO, HasHeaderField};
61//!
62//! let path = PathBuf::from("test-data")
63//! .join("samples")
64//! .join("sample.ot");
65//!
66//! let otfile = SampleSettingsFile::from_data_file(&path).unwrap();
67//!
68//! let outdir = std::env::temp_dir()
69//! .join("ot-tools-io")
70//! .join("doctest")
71//! .join("write_yaml_and_json");
72//!
73//! # // when running in cicd env the /tmp/ot-tools-io directory doesn't exist yet
74//! # let _ = std::fs::create_dir_all(&outdir);
75//! &otfile.to_yaml_file(&outdir.join("sample.yaml")).unwrap();
76//! &otfile.to_json_file(&outdir.join("sample.json")).unwrap();
77//! ```
78//!
79//! #### The `HasSomeField` Traits
80//! The `Has*Field` traits add methods to a type to perform integrity checks: checksum calculation
81//! and validation, header validation and file patch version validation.
82//! ```rust
83//! // check the header of sample settings file
84//!
85//! use std::path::PathBuf;
86//! use ot_tools_io::{SampleSettingsFile, OctatrackFileIO, HasHeaderField};
87//!
88//! let path = PathBuf::from("test-data")
89//! .join("samples")
90//! .join("sample.ot");
91//!
92//! let otfile = SampleSettingsFile::from_data_file(&path).unwrap();
93//! assert!(otfile.check_header().unwrap())
94//! ```
95//!
96//! #### The `OtToolsIoError` Type
97//! The `OtToolsIoError` type should be used to catch any errors in normal use as it implements
98//! `From` for all other internal errors
99//! ```rust
100//! // handling errors
101//!
102//! use ot_tools_io::OtToolsIoError;
103//! use ot_tools_io::settings::ProgChMidiChannel;
104//!
105//! // always errors
106//! fn try_from_err() -> Result<ProgChMidiChannel, OtToolsIoError> {
107//! Ok(ProgChMidiChannel::try_from(100_i8)?)
108//! }
109//!
110//! assert!(try_from_err().is_err());
111//! assert_eq!(
112//! try_from_err().unwrap_err().to_string(),
113//! "invalid setting value: invalid Program Change MIDI Channel value".to_string(),
114//! );
115//! ```
116//!
117//! #### `SomeFile` Types
118//! Types directly related to some file used by the Octatrack are named as `SomeFile`, where
119//! `Some` is the relevant file base name (`*.ot` files obviously don't have a basename we can
120//! use).
121//!
122//! Only these `*File` types can be read from / written to the filesystem using this crate's
123//! [`OctatrackFileIO`] trait methods / functions.
124//!
125//! | Type | Filename Pattern | Description |
126//! | ------------ | -------------------------- | ------------|
127//! | [`ArrangementFile`] | `arr??.*` | data for arrangements |
128//! | [`BankFile`] | `bank??.*` | data for parts and patterns |
129//! | [`MarkersFile`] | `markers.*` | start trim/end trim/slices/loop points for sample slots |
130//! | [`ProjectFile`] | `project.*` | project level settings; state; sample slots |
131//! | [`SampleSettingsFile`] | `*.ot` | saved sample settings data, loops slices etc. |
132//!
133//! Read the relevant modules in this library for more detailed information on
134//! the data contained in each file.
135//!
136//! ####
137//!
138//! ## Additional Details
139//!
140//! #### Octatrack File Relationships
141//!
142//! - Changing the sample loaded into a sample slot updates both the [`ProjectFile`] file (trig
143//! quantization settings, file path etc) and the [`MarkersFile`] file (trim settings, slices,
144//! loop points).
145//!
146//! - Slot data from [`ProjectFile`]s and [`MarkersFile`]s is written to an [`SampleSettingsFile`]
147//! file when saving sample attributes data from the Octatrack's audio editing menu.
148//!
149//! - Loading a sample into a project sample slot ([`ProjectFile`]s and [`MarkersFile`]s) reads
150//! any data in an [`SampleSettingsFile`] files and configures the sample slot accordingly.
151//!
152//! - A [`BankFile`]'s patterns and parts store zero-indexed sample slot IDs as flex
153//! and static slot references (machine data in a part and track p-lock trigs in a pattern). These
154//! references point to the relevant slot in **_both_** the [`MarkersFile`] and [`ProjectFile`]
155//! for a project.
156//!
157//! - [`ArrangementFile`]s store a `u8` which references a [`BankFile`]'s pattern, indicating
158//! the pattern should be played when the specific row in an arrangement is triggered.
159//! The field is zero-indexed, with the full range used for pattern references 0 (A01) ->
160//! 256 (P16).
161//!
162//!
163//! #### Octatrack Device File Modifications
164//!
165//! - `*.work` files are created when creating a new project
166//! `PROJECT MENU -> CHANGE PROJECT -> CREATE NEW`.
167//! - `*.work` files are updated by using the `PROJECT MENU -> SYNC TO CARD` operation.
168//! - `*.work` files are updated when the user performs a `PROJECT MENU -> SAVE PROJECT` operation.
169//! - `*.strd` files are created/updated when the user performs a `PROJECT MENU -> SAVE PROJECT`
170//! operation.
171//! - `*.strd` files are not changed by the `PROJECT -> SYNC TO CARD` operation.
172//! - `arr??.strd` files can also be saved via the `ARRANGER MENU -> SAVE ARRANGEMENT` operation.
173//!
174//! #### Notable 'gotcha's
175//!
176//! ##### Sample Slot IDs
177//!
178//! Project Sample Slots store their slots with a **_one-indexed_** `slot_id` field.
179//!
180//! **_All other references to sample slots are zero-indexed_** (i.e. [`BankFile`] and
181//! [`MarkersFile`] files).
182//!
183//! I'm using lots of italics and bold formatting here because it is super annoying but there's not
184//! much i can do about it. Remember you will need to convert from one to zero indexed whenever you
185//! deal with Sample Slots.
186//!
187//! ##### Default Loop Point Values Differ Between Types
188//!
189//! A 'Disabled' loop point in either a [`MarkersFile`] or a [`SampleSettingsFile`] is a
190//! `0xFFFFFFFF` value, but the default loop point when creating new [`MarkersFile`] is always
191//! `0_u32`. The 'Disabled' value setting is only set when a sample is loaded into a sample slot,
192//! and a [`SampleSettingsFile`] is generated from that sample slot data.
193//!
194//! ####
195// For Andrey.
196//! ## Slava Ukraini
197//!
198//! I've worked with Ukrainian developers. What is happening to their country is abhorrent.
199//!
200//! [](https://stand-with-ukraine.pp.ua)
201//! ##
202//!
203//! [0]: https://www.elektron.se/explore/octatrack-mkii
204//!
205
206pub mod arrangements;
207pub mod banks;
208pub mod errors;
209mod generics;
210pub mod identifiers;
211mod macros;
212pub mod markers;
213pub mod parts;
214pub mod patterns;
215pub mod projects;
216pub mod samples;
217pub mod settings;
218pub mod slices;
219#[cfg(test)]
220#[allow(dead_code)]
221mod test_utils;
222mod traits;
223
224pub use crate::arrangements::ArrangementFile;
225pub use crate::banks::BankFile;
226pub use crate::markers::MarkersFile;
227pub use crate::projects::ProjectFile;
228pub use crate::samples::SampleSettingsFile;
229
230pub use crate::traits::{
231 CheckFileIntegrity, Defaults, HasChecksumField, HasFileVersionField, HasHeaderField, IsDefault,
232 OctatrackFileIO,
233};
234
235use crate::markers::SlotMarkersError;
236use crate::projects::{ProjectError, ProjectParseError, ProjectSlotsError};
237use crate::samples::SampleSettingsError;
238use crate::settings::InvalidValueError;
239use crate::slices::SliceError;
240use std::fs::File;
241use std::io::{Read, Write};
242use std::path::{Path, PathBuf};
243use thiserror::Error;
244
245/// Global error variant handling. All internally used error types and variants can cast to this
246/// type.
247#[derive(Debug, Error)]
248pub enum OtToolsIoError {
249 /// File could not be found / opened etc.
250 #[error("File OS Error: {source} Path={path} (std::io::Error)")]
251 FileOs {
252 #[source]
253 source: std::io::Error,
254 path: PathBuf,
255 },
256 /// Some other FIle I/O error
257 #[error("File I/O Error: {0} (std::io::Error)")]
258 FileIo(#[from] std::io::Error),
259 /// Bincode could not de/serialize the data
260 #[error("error during binary data decoding / encoding: {0} (bincode::Error)")]
261 Bincode(#[from] bincode::Error),
262 /// Can't read project string data
263 #[error("error reading utf8 string data: {0} (std::str::Utf8Error)")]
264 ReadUtf8(#[from] std::str::Utf8Error),
265 /// Can't parse project string data
266 #[error("error parsing raw project data: {0} (ot_tools_io::projects::ProjectParseError)")]
267 ProjectParse(#[from] ProjectParseError),
268 /// Can't parse project string data
269 #[error("error parsing project slots data: {0} (ot_tools_io::projects::ProjectSlotsError)")]
270 ProjectSlots(#[from] ProjectSlotsError),
271 /// [arrangements] parsing errors
272 #[error("arrangement error: {0}")]
273 Arrangement(#[from] arrangements::ArrangementError),
274 /// Can't parse yaml data
275 #[error("error processing yaml: {0} (serde_norway::Error)")]
276 YamlParse(#[from] serde_norway::Error),
277 /// Can't parse json data
278 #[error("error processing json: {0} (serde_json::Error)")]
279 JsonParse(#[from] serde_json::Error),
280 /// [projects] specific errors
281 #[error(
282 "project files cannot be checked for integrity: {0} (ot_tools_io::projects::ProjectError)"
283 )]
284 ProjectFile(#[from] ProjectError),
285 /// [samples] specific errors
286 #[error("sample settings error: {0} (ot_tools_io::samples::SampleSettingsError)")]
287 SampleSettings(#[from] SampleSettingsError),
288 /// [markers] specific errors
289 #[error("markers error: {0} (ot_tools_io::markers::MarkersErrors)")]
290 Markers(#[from] SlotMarkersError),
291 /// [slices] specific errors
292 #[error("slices error: {0} (ot_tools_io::slices::SlicesErrors)")]
293 Slice(#[from] SliceError),
294 /// [settings] error handling
295 #[error("invalid setting value: {0}")]
296 SettingValue(#[from] InvalidValueError),
297 /// Header for some file type is invalid
298 #[error("invalid header(s) for file")]
299 FileHeader,
300 #[error("invalid index for identifier")]
301 InvalidIndex,
302}
303
304/// Re-export of (most of) the useful types from the crate.
305/// Allows you to use `use crate::types::*` if you want to be lazy.
306pub mod types {
307 /// Helper type to make working with slots *slightly* easier. A 'slot' is
308 /// really the combined [`SlotAttributes`] and [`SlotMarkers`] structs.
309 pub type SlotCombo = (SlotAttributes, SlotMarkers);
310 pub use crate::arrangements::ArrangeRow;
311 pub use crate::arrangements::ArrangementState;
312 pub use crate::arrangements::LoopOrJumpOrHaltRow;
313 pub use crate::arrangements::PatternRow;
314 pub use crate::arrangements::ReminderRow;
315 pub use crate::markers::SlotMarkers;
316 pub use crate::parts::Part;
317 pub use crate::patterns::Pattern;
318 pub use crate::projects::SlotAttributes;
319 pub use crate::settings::SlotType;
320 pub use crate::slices::Slice;
321 pub use crate::ArrangementFile;
322 pub use crate::BankFile;
323 pub use crate::MarkersFile;
324 pub use crate::ProjectFile;
325 pub use crate::SampleSettingsFile;
326
327 // generic array new types
328 pub use crate::generics::ActiveSlot;
329 pub use crate::generics::ArrangeRows;
330 pub use crate::generics::Arrangements;
331 pub use crate::generics::Banks;
332 pub use crate::generics::Parts;
333 pub use crate::generics::Patterns;
334 pub use crate::generics::PlaybackSlots;
335 pub use crate::generics::RecordingBufferSlots;
336 pub use crate::generics::Scenes;
337 pub use crate::generics::Slices;
338 pub use crate::generics::Slots;
339 pub use crate::generics::Tracks;
340 pub use crate::generics::Trigs;
341}
342
343/// The Elektron Octatrack OS project versions this library can be used with.
344/// The `ProjectFile.metadata.os_version` field must contain one of these
345/// string values, otherwise the project is not compatible.
346///
347/// See the [`ProjectFile::check_compatible_os_version`] method for usage
348/// information.
349pub const ALLOWED_OS_VERSIONS: [&str; 3] = ["1.40A", "1.40B", "1.40C"];
350
351#[doc(hidden)]
352/// Read bytes from a file at `path`.
353/// ```compile_fail
354/// let fpath = std::path::PathBuf::from("test-data")
355/// .join("blank-project")
356/// .join("bank01.work");
357/// let r = ot_tools_io::read_bin_file(&fpath);
358/// assert!(r.is_ok());
359/// assert_eq!(r.unwrap().len(), 636113);
360///```
361fn read_bin_file(path: &Path) -> Result<Vec<u8>, OtToolsIoError> {
362 let mut infile = File::open(path).map_err(|e| OtToolsIoError::FileOs {
363 path: path.to_path_buf(),
364 source: e,
365 })?;
366 let mut bytes: Vec<u8> = vec![];
367 let _: usize = infile.read_to_end(&mut bytes)?;
368 Ok(bytes)
369}
370
371#[cfg(test)]
372mod read_bin_file {
373 use crate::test_utils::*;
374 use crate::{read_bin_file, OtToolsIoError};
375
376 #[test]
377 fn ok() -> Result<(), OtToolsIoError> {
378 let path = get_blank_proj_dirpath().join("bank01.work");
379 read_bin_file(&path)?;
380 Ok(())
381 }
382
383 #[test]
384 fn err_file_io() -> Result<(), OtToolsIoError> {
385 let path = get_blank_proj_dirpath().join("NOTNTONTONTNTOTNONT");
386
387 #[cfg(target_os = "windows")]
388 assert_eq!(
389 read_bin_file(&path).unwrap_err().to_string(),
390 format!["File OS Error: The system cannot find the file specified. (os error 2) Path={} (std::io::Error)", path.display()].to_string(),
391 "should throw a OtToolsIoError::FileOS error when file does not exist"
392 );
393
394 #[cfg(target_os = "linux")]
395 assert_eq!(
396 read_bin_file(&path).unwrap_err().to_string(),
397 format![
398 "File OS Error: No such file or directory (os error 2) Path={} (std::io::Error)",
399 path.display()
400 ]
401 .to_string(),
402 "should throw a OtToolsIoError::FileOS error when file does not exist"
403 );
404
405 Ok(())
406 }
407}
408
409#[doc(hidden)]
410/// Write bytes to a file at `path`.
411/// ```compile_fail
412/// use std::env::temp_dir;
413/// use std::array::from_fn;
414///
415/// let arr: [u8; 27] = from_fn(|_| 0);
416///
417/// let fpath = temp_dir()
418/// .join("ot-tools-io")
419/// .join("doctest")
420/// .join("write_bin_file.example");
421///
422/// # use std::fs::create_dir_all;
423/// # create_dir_all(&fpath.parent().unwrap()).unwrap();
424/// let r = ot_tools_io::write_bin_file(&arr, &fpath);
425/// assert!(r.is_ok());
426/// assert!(fpath.exists());
427/// ```
428fn write_bin_file(bytes: &[u8], path: &Path) -> Result<(), OtToolsIoError> {
429 let mut file: File = File::create(path).map_err(|e| OtToolsIoError::FileOs {
430 path: path.to_path_buf(),
431 source: e,
432 })?;
433 file.write_all(bytes)?;
434 Ok(())
435}
436
437#[cfg(test)]
438mod write_bin_file {
439 use crate::{write_bin_file, OtToolsIoError};
440 use std::env::temp_dir;
441 use std::fs::{create_dir_all, remove_file};
442
443 #[test]
444 fn ok() -> Result<(), OtToolsIoError> {
445 let path = temp_dir()
446 .join("ot-tools-io")
447 .join("write_bin_file")
448 .join("ok.bin");
449 create_dir_all(path.parent().unwrap())?;
450 if path.exists() {
451 remove_file(&path)?;
452 };
453 write_bin_file(&[1, 2, 3, 4], &path)?;
454 Ok(())
455 }
456
457 // should fail: attempting to write a file to an existing directory
458 #[test]
459 fn err_file_io() -> Result<(), OtToolsIoError> {
460 let path = temp_dir().join("ot-tools-io").join("write_bin_file");
461 create_dir_all(&path)?;
462
463 #[cfg(target_os = "windows")]
464 assert_eq!(
465 write_bin_file(&[1, 2, 3, 4], &path)
466 .unwrap_err()
467 .to_string(),
468 format![
469 "File OS Error: Access is denied. (os error 5) Path={} (std::io::Error)",
470 path.display()
471 ]
472 .to_string(),
473 "should throw a OtToolsIoError::FileOS error when a file cannot be created"
474 );
475
476 #[cfg(target_os = "linux")]
477 assert_eq!(
478 write_bin_file(&[1, 2, 3, 4], &path)
479 .unwrap_err()
480 .to_string(),
481 format![
482 "File OS Error: Is a directory (os error 21) Path={} (std::io::Error)",
483 path.display()
484 ]
485 .to_string(),
486 "should throw a OtToolsIoError::FileOS error when a file cannot be created"
487 );
488 Ok(())
489 }
490}
491
492#[doc(hidden)]
493/// Read a file at `path` as a string.
494/// ```compile_fail
495/// let fpath = std::path::PathBuf::from("test-data")
496/// .join("blank-project")
497/// .join("bank01.work");
498/// let r = ot_tools_io::read_bin_file(&fpath);
499/// assert!(r.is_ok());
500/// assert_eq!(r.unwrap().len(), 636113);
501/// ```
502fn read_str_file(path: &Path) -> Result<String, OtToolsIoError> {
503 let mut file = File::open(path).map_err(|e| OtToolsIoError::FileOs {
504 path: path.to_path_buf(),
505 source: e,
506 })?;
507 let mut string = String::new();
508 let _ = file.read_to_string(&mut string)?;
509 Ok(string)
510}
511
512#[cfg(test)]
513mod read_str_file {
514 use crate::test_utils::*;
515 use crate::{read_str_file, OtToolsIoError};
516
517 #[test]
518 fn ok() -> Result<(), OtToolsIoError> {
519 let path = get_blank_proj_dirpath().join("project.work");
520 read_str_file(&path)?;
521 Ok(())
522 }
523
524 #[test]
525 fn err_file_io() -> Result<(), OtToolsIoError> {
526 let path = get_blank_proj_dirpath().join("NOTNTONTONTNTOTNONT");
527
528 #[cfg(target_os = "windows")]
529 assert_eq!(
530 read_str_file(&path).unwrap_err().to_string(),
531 format!["File OS Error: The system cannot find the file specified. (os error 2) Path={} (std::io::Error)", path.display()].to_string(),
532 "should throw a OtToolsIoError::FileOS error when file does not exist"
533 );
534
535 #[cfg(target_os = "linux")]
536 assert_eq!(
537 read_str_file(&path).unwrap_err().to_string(),
538 format![
539 "File OS Error: No such file or directory (os error 2) Path={} (std::io::Error)",
540 path.display()
541 ]
542 .to_string(),
543 "should throw a OtToolsIoError::FileOS error when file does not exist"
544 );
545
546 Ok(())
547 }
548}
549
550#[doc(hidden)]
551/// Write a string to a file at `path`.
552/// ```compile_fail
553/// use std::env::temp_dir;
554///
555/// let data = "abcd".to_string();
556///
557/// let fpath = temp_dir()
558/// .join("ot-tools-io")
559/// .join("doctest")
560/// .join("write_str_file.example");
561///
562/// # use std::fs::create_dir_all;
563/// # create_dir_all(&fpath.parent().unwrap()).unwrap();
564/// let r = ot_tools_io::write_str_file(&data, &fpath);
565/// assert!(r.is_ok());
566/// assert!(fpath.exists());
567/// ```
568fn write_str_file(string: &str, path: &Path) -> Result<(), OtToolsIoError> {
569 let mut file: File = File::create(path).map_err(|e| OtToolsIoError::FileOs {
570 path: path.to_path_buf(),
571 source: e,
572 })?;
573 write!(file, "{string}")?;
574 Ok(())
575}
576
577#[cfg(test)]
578mod write_str_file {
579 use crate::{write_str_file, OtToolsIoError};
580 use std::env::temp_dir;
581 use std::fs::{create_dir_all, remove_file};
582
583 #[test]
584 fn ok() -> Result<(), OtToolsIoError> {
585 let path = temp_dir()
586 .join("ot-tools-io")
587 .join("write_str_file")
588 .join("ok.txt");
589 create_dir_all(path.parent().unwrap())?;
590 if path.exists() {
591 remove_file(&path)?;
592 };
593 write_str_file("SOMETHING", &path)?;
594 Ok(())
595 }
596
597 // should fail: attempting to write a file to an existing directory
598 #[test]
599 fn err_file_io() -> Result<(), OtToolsIoError> {
600 let path = temp_dir().join("ot-tools-io").join("write_str_file");
601 create_dir_all(&path)?;
602
603 #[cfg(target_os = "windows")]
604 assert_eq!(
605 write_str_file("SOMETHING", &path).unwrap_err().to_string(),
606 format![
607 "File OS Error: Access is denied. (os error 5) Path={} (std::io::Error)",
608 path.display()
609 ]
610 .to_string(),
611 "should throw a OtToolsIoError::FileOS error when a file cannot be created"
612 );
613
614 #[cfg(target_os = "linux")]
615 assert_eq!(
616 write_str_file("SOMETHING", &path).unwrap_err().to_string(),
617 format![
618 "File OS Error: Is a directory (os error 21) Path={} (std::io::Error)",
619 path.display()
620 ]
621 .to_string(),
622 "should throw a OtToolsIoError::FileOS error when a file cannot be created"
623 );
624 Ok(())
625 }
626}
627
628fn loop_point_is_in_trim_range(loop_point: u32, trim_start: u32, trim_end: u32) -> bool {
629 loop_point >= trim_start && loop_point < trim_end
630}