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//! [](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}