ot_tools_io/
lib.rs

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