ot_tools_io/samples.rs
1/*
2SPDX-License-Identifier: GPL-3.0-or-later
3Copyright © 2026 Mike Robeson [dijksterhuis]
4*/
5
6//! All types and trait implementations related to [`SampleSettingsFile`]s.
7
8use crate::generics::Slices;
9use crate::markers::SlotMarkers;
10use crate::projects::{SlotAttributes, DEFAULT_GAIN, DEFAULT_TEMPO};
11use crate::settings::{LoopMode, SlotType, TimeStretchMode, TrigQuantizationMode};
12use crate::slices::Slice;
13use crate::traits::SwapBytes;
14use crate::{
15 HasChecksumField, HasFileVersionField, HasHeaderField, OctatrackFileIO, OtToolsIoError,
16};
17use ot_tools_io_derive::{AsMutDerive, AsRefDerive, IntegrityChecks};
18use serde::{Deserialize, Serialize};
19use std::path::PathBuf;
20use thiserror::Error;
21
22#[cfg(test)]
23mod test_utils {
24 use super::{
25 OtToolsIoError, SampleSettingsFile, SlotMarkers, SAMPLES_FILE_VERSION, SAMPLES_HEADER,
26 };
27 use crate::projects::DEFAULT_GAIN;
28 pub(crate) fn create_mock_new_sample_settings(
29 tempo: Option<u32>,
30 gain: Option<u16>,
31 ) -> Result<SampleSettingsFile, OtToolsIoError> {
32 SampleSettingsFile::new(
33 SlotMarkers::default(),
34 gain,
35 tempo,
36 None,
37 None,
38 None,
39 None,
40 None,
41 )
42 }
43
44 pub(crate) fn mock_0_slice_test_file() -> SampleSettingsFile {
45 SampleSettingsFile {
46 header: SAMPLES_HEADER,
47 datatype_version: SAMPLES_FILE_VERSION,
48 unknown: 1,
49 tempo: 2281,
50 trim_bar_len: 800,
51 loop_bar_len: 800,
52 stretch: 2,
53 loop_mode: 0,
54 gain: DEFAULT_GAIN as u16,
55 quantization: 255,
56 trim_start: 0,
57 trim_end: 890839,
58 loop_start: 0,
59 slices: SlotMarkers::default().slices,
60 slices_len: 0,
61 checksum: 998,
62 }
63 }
64}
65
66/// Errors specific to [`SampleSettingsFile`]s.
67///
68/// Most of the time this will be casted into an [`OtToolsIoError`] instance in a return type.
69#[derive(Debug, Error)]
70pub enum SampleSettingsError {
71 #[error("invalid gain, must be u16 in range 0 <= x <= 96: {value}")]
72 GainOutOfBounds { value: u32 },
73 #[error("invalid tempo value, must be u32 in range 720 <= x <= 7200: {value}")]
74 TempoOutOfBounds { value: u32 },
75 #[error("invalid slot_id value, must be 1 to 128 for Static slot type or 136 for Flex slot type: id: {id} type: {slot_type}")]
76 SlotIdOutOfBounds { id: u8, slot_type: SlotType },
77 #[error("invalid checksum value for sample settings file: {checksum}")]
78 InvalidChecksum { checksum: u16 },
79 #[error("invalid file version value for sample settings file: {version}")]
80 InvalidFileVersion { version: u8 },
81}
82
83// in `hexdump -C` format:
84// ```
85// FORM....DPS1SMPA
86// ......
87// ```
88/// Standard header bytes in a sample settings file
89pub const SAMPLES_HEADER: [u8; 21] = [
90 0x46, 0x4F, 0x52, 0x4D, 0x00, 0x00, 0x00, 0x00, 0x44, 0x50, 0x53, 0x31, 0x53, 0x4D, 0x50, 0x41,
91 0x00, 0x00, 0x00, 0x00, 0x00,
92];
93
94/// Current/supported datatype version for a sample settings file
95pub const SAMPLES_FILE_VERSION: u8 = 2;
96
97/// An Octatrack `.ot` file a.k.a. a sample settings file contains trim, slice and attribute
98/// settings for a sample/slot.
99/// Essentially the type is a combination of [`SlotAttributes`] and [`SlotMarkers`] data, excluding
100/// the `slot_id`, `path` and `slot_type` fields from [`SlotAttributes`].
101///
102/// # The Type's Name
103///
104/// The Octatrack manual specifically refers to
105/// > SAVE SAMPLE SETTINGS will save the trim, slice and attribute settings in a
106/// > separate file and link it to the sample currently being edited.
107/// >
108/// > -- page 87.
109///
110/// So this is the settings file for a given sample.
111///
112/// # Missing [`Default`] trait
113///
114/// In practice, a [`SampleSettingsFile`] is usually paired with an audio file.
115/// This library assumes we’re exclusively operating on Octatrack binary data files, that we have no
116/// access to any audio sample files.
117/// As such, there is no way to know what a "default" implementation of this type would like.
118/// So there isn't one.
119///
120/// Instead of calling `default`, always create a new instance with [`SampleSettingsFile::new`].
121///
122/// # Converting to other data types
123///
124/// [`SampleSettingsFile`]s can be converted to/from [`SlotMarkers`] and/or [`SlotAttributes`] using
125/// the information from the below table
126///
127/// | From | To | Via |
128/// | ---- | ---- | ---- |
129/// | [`SampleSettingsFile`] | [`SlotMarkers`] | [`SlotMarkers::from`] |
130/// | [`SampleSettingsFile`] | [`SlotAttributes`] | [`SampleSettingsFile::to_slot_attr`] \* |
131/// | `(SlotAttributes, SlotMarkers)` | [`SampleSettingsFile`] | [`SampleSettingsFile::from`] |
132///
133/// \* requires additional arguments
134///
135/// # Trim Bar Length and Loop Bar Length
136///
137/// These fields are not necessary to have a functional [`SampleSettingsFile`].
138/// If both of these fields are set to zero, but the BPM/tempo field is populated,
139/// then the Octatrack will load the sample using the BPM/tempo field.
140///
141/// To calculate an appropriate value for these two fields we would have to convert from number of
142/// samples to number of bars.
143/// This calculation requires the sample rate and length of the relevant audio sample file,
144/// meaning this library would need access to such audio sample files.
145///
146/// This library assumes we're exclusively operating on Octatrack binary data files,
147/// that we have no access to any audio files.
148///
149/// Therefore, these two fields are always set to zero when using any of the following methods
150/// - [`SampleSettingsFile::new`]
151/// - [`SampleSettingsFile::from`] \*
152///
153/// If you want to set these two values you have to do one of:
154/// - Calculate the appropriate `tempo` value for your desired `trim_bar_len` or `loop_bar_len` and use that
155/// when calling [`SampleSettingsFile::new`] or when creating a new struct from scratch
156/// (leaving the `trim_bar_len` and `loop_bar_len` fields set to zero if creating a struct from scratch).
157/// - Manually create the [`SampleSettingsFile`] from scratch and provide appropriate values for all
158/// fields -- note that this means manually adding header etc. field values too.
159///
160/// Please note that the `trim_bar_len` and `loop_bar_len` fields are **NOT** set to zero during
161/// deserialization.
162/// A [`SampleSettingsFile`]s should be correctly represented when the file has been generated by
163/// the Octatrack itself.
164///
165/// \* [`SampleSettingsFile::from`] will be able to convert it in the future,
166/// but appropriate fields need to be added to the [`SlotAttributes`] type first.
167///
168/// # Little-endian systems
169///
170/// The bytes of this type are swapped during decoding on little-endian systems to ensure data is
171/// written out correctly.
172#[derive(
173 Copy,
174 Clone,
175 Debug,
176 Eq,
177 Hash,
178 Ord,
179 PartialEq,
180 PartialOrd,
181 Serialize,
182 Deserialize,
183 AsMutDerive,
184 AsRefDerive,
185 IntegrityChecks,
186)]
187pub struct SampleSettingsFile {
188 /// Header
189 ///
190 /// ## Validation
191 ///
192 /// Use the [`SampleSettingsFile::check_header`] method from the [`HasHeaderField`] trait.
193 ///
194 /// The [`SampleSettingsFile::validate`] method will call [`SampleSettingsFile::check_header`]
195 /// to verify the current instance's header.
196 pub header: [u8; 21],
197
198 /// Datatype's version ID
199 ///
200 /// ## Validation
201 ///
202 /// Use the [`SampleSettingsFile::check_file_version`] method from the [`HasFileVersionField`] trait.
203 ///
204 /// The [`SampleSettingsFile::validate`] method will call [`SampleSettingsFile::check_file_version`]
205 /// to verify the current instance's version.
206 pub datatype_version: u8,
207
208 /// Unknown data -- can sometimes be 1, usually 0.
209 pub unknown: u8,
210
211 /// Tempo determines the BPM of sample playback.
212 /// In this field, the value is always the machine UI's BPM (human BPM) multiplied by 24.
213 ///
214 /// From page 86 of the Octatrack manual:
215 /// > ORIGINAL TEMPO displays the calculated BPM of the sample.
216 /// > If it is not correct, it can be changed using the LEVEL knob.
217 /// > This setting will affect the sound of a timestretched sample.
218 /// > For correct results it should be set to match the original BPM of the sample.
219 /// > Altering this setting will alter the TRIM LEN (BARS) and LOOP LEN (BARS) settings.
220 ///
221 /// ## Validation
222 ///
223 /// The [`SampleSettingsFile::validate`] method checks whether the field's current value is
224 /// within appropriate bounds.
225 ///
226 /// Validating this field against the `trim_bar_len` and/or `loop_bar_len` fields is outside the scope
227 /// of this library.
228 pub tempo: u32,
229
230 /// Number of bars for determining the BPM of sample playback.
231 /// Should be an appropriate value which corresponds to the `tempo` field.
232 ///
233 /// From page 86 of the Octatrack manual:
234 /// > TRIM LEN (BARS) shows the length of the sample in bars.
235 /// > Altering this setting will alter the ORIGINAL TEMPO and LOOP LEN (BARS) settings.
236 ///
237 /// ## Validation
238 ///
239 /// The [`SampleSettingsFile::validate`] method does not check this field for validity.
240 ///
241 /// Validating this field against the `trim_bar_len` and/or `loop_bar_len` fields is outside the scope
242 /// of this library.
243 pub trim_bar_len: u32,
244
245 /// Number of bars for determining the BPM of sample playback.
246 /// Should be an appropriate value which corresponds to the `tempo` field.
247 ///
248 /// From page 86 of the Octatrack manual:
249 /// > LOOP LEN (BARS) displays the amount of bars the looped section of the sample consists of.
250 /// > Altering this setting will alter the ORIGINAL TEMPO and TRIM LEN (BARS) settings.
251 ///
252 /// ## Validation
253 ///
254 /// The [`SampleSettingsFile::validate`] method does not check this field for validity.
255 ///
256 /// Validating this field against the `trim_bar_len` and/or `loop_bar_len` fields is outside the scope
257 /// of this library.
258 pub loop_bar_len: u32,
259
260 /// Time-stretching algorithm applied to the sample.
261 /// See [`TimeStretchMode`] for suitable choices.
262 ///
263 /// ## No enum usage?
264 ///
265 /// [`TimeStretchMode`] has a base representation of `u8` but a [`SampleSettingsFile`] expects a
266 /// `u32` value for this field.
267 /// As such, we have to convert to `u32` values for this field via [`TimeStretchMode::try_from`]
268 /// instead of the relevant variant.
269 /// While it may be possible to change this in a future version via a custom implementation of
270 /// [`Serialize`] and [`Deserialize`], it is currently out of scope.
271 ///
272 /// Use the [`SampleSettingsFile::new`] method to create a new instance with the appropriate
273 /// enum variant.
274 ///
275 /// ## Validation
276 ///
277 /// The [`SampleSettingsFile::validate`] method checks whether this field has an appropriate value.
278 pub stretch: u32,
279
280 /// Loop mode for the sample.
281 /// See [`LoopMode`] for suitable choices.
282 ///
283 /// ## No enum usage?
284 ///
285 /// [`LoopMode`] has a base representation of `u8` but a [`SampleSettingsFile`] expects a
286 /// `u32` value for this field.
287 /// As such, we have to convert to `u32` values for this field via [`LoopMode::try_from`]
288 /// instead of the relevant variant.
289 /// While it may be possible to change this in a future version via a custom implementation of
290 /// [`Serialize`] and [`Deserialize`], it is currently out of scope.
291 ///
292 /// Use the [`SampleSettingsFile::new`] method to create a new instance with the appropriate
293 /// enum variant.
294 ///
295 /// ## Validation
296 ///
297 /// The [`SampleSettingsFile::validate`] method checks whether this field has an appropriate value.
298 pub loop_mode: u32,
299
300 /// Gain of the sample.
301 /// -24.0 db <= x <= +24 db range in the machine's UI, with increments of 0.5 db changes.
302 /// 0 <= x <= 96 range in binary data file.
303 ///
304 /// ## Validation
305 ///
306 /// The [`SampleSettingsFile::validate`] method checks whether this field has an appropriate
307 /// value within the Octatrack's bounds for gain.
308 pub gain: u16,
309
310 /// Trig Quantization mode applied to the sample.
311 /// See [`TrigQuantizationMode`] for suitable choices.
312 ///
313 /// ## No enum usage?
314 ///
315 /// [`TrigQuantizationMode`] has a base representation of `u8` but a [`SampleSettingsFile`]
316 /// expects a `u32` value for this field.
317 /// As such, we have to convert to `u32` values for this field via
318 /// [`TrigQuantizationMode::try_from`] instead of the relevant variant.
319 /// While it may be possible to change this in a future version via a custom implementation of
320 /// [`Serialize`] and [`Deserialize`], it is currently out of scope.
321 ///
322 /// Use the [`SampleSettingsFile::new`] method to create a new instance with the appropriate
323 /// enum variant.
324 ///
325 /// ## Validation
326 ///
327 /// The [`SampleSettingsFile::validate`] method checks whether this field has an appropriate value.
328 pub quantization: u8,
329
330 /// Where the trim start marker is placed for the sample, measured in bars.
331 /// Default is 0 (start of sample).
332 ///
333 /// ## Validation
334 ///
335 /// The [`SampleSettingsFile::validate`] method checks whether this field is less than `trim_end`
336 pub trim_start: u32,
337
338 /// Where the trim end marker is placed for the sample.
339 /// When the sample is being played in normal mode (i.e. not using slices),
340 /// the Octatrack will not play samples past this point.
341 ///
342 /// ## Validation
343 ///
344 /// The [`SampleSettingsFile::validate`] method checks whether this field is greater than `trim_start`
345 pub trim_end: u32,
346
347 /// Start position for any loops.
348 /// Default is the same as trim start.
349 ///
350 /// A note from the Octatrack manual on loop point/start behaviour:
351 /// > If a loop point is set, the sample will play from the start point to the
352 /// > end point, then loop from the loop point to the end point
353 ///
354 /// ## Validation
355 ///
356 /// The [`SampleSettingsFile::validate`] method checks whether this field is within the bounds
357 /// of `trim_start` and `trim_end` values.
358 pub loop_start: u32,
359
360 /// 64 length array containing [`Slice`]s.
361 /// See the [`Slice`] struct for more details.
362 /// Any empty slice positions should have zero-valued struct fields.
363 ///
364 /// ## Validation
365 ///
366 /// The [`SampleSettingsFile::validate`] method calls [`Slice::validate`]
367 pub slices: Slices<Slice>,
368
369 /// Number of usable [`Slice`]s in this sample.
370 /// Used by the Octatrack to ignore zero-valued [`Slice`]s in the `slices` array when loading the sample.
371 pub slices_len: u32,
372
373 /// Checksum value for the file.
374 /// For now, this value must be calculated and added to the struct **after** the struct is created,
375 /// via [`SampleSettingsFile::calculate_checksum`]
376 ///
377 /// # Example
378 /// ```rust
379 /// use ot_tools_io::types::{SampleSettingsFile, SlotMarkers};
380 /// use ot_tools_io::HasChecksumField;
381 ///
382 /// # use ot_tools_io::OtToolsIoError;
383 /// # fn main() -> Result<(), OtToolsIoError> {
384 /// let mut ss_f = SampleSettingsFile::new(
385 /// SlotMarkers::default(),
386 /// None,
387 /// None,
388 /// None,
389 /// None,
390 /// None,
391 /// None,
392 /// None,
393 /// )?;
394 ///
395 /// let checksum = ss_f.calculate_checksum()?;
396 /// ss_f.checksum = checksum;
397 ///
398 /// # Ok(()) }
399 /// ```
400 /// ## Validation
401 ///
402 /// Use the [`SampleSettingsFile::check_checksum`] method to check the validity of the current
403 /// checksum value.
404 pub checksum: u16,
405}
406
407impl SampleSettingsFile {
408 /// Create a new `SampleSettingsFile`
409 ///
410 /// Values like `tempo` and `gain` need to be normalized to relevant Octatrack range bounds
411 /// when using this method.
412 ///
413 /// # Trim Bar Length and Loop Bar Length
414 ///
415 /// If the `trim_bar_len` and `loop_bar_len` arguments are not provided then the corresponding
416 /// field values are set to zero.
417 ///
418 /// When providing these values you should ensure that you have correctly calculated both of
419 /// these values **AND** the tempo value.
420 /// All three are used simultaneously by the Octatrack,
421 /// and I haven't spent any time figuring out which one takes priority
422 /// (I think it's BPM/tempo, but I cannot confirm yet).
423 ///
424 /// See the relevant documentation [here][SampleSettingsFile#trim-length-and-loop-length] for
425 /// more information.
426 /// ```rust
427 /// use std::array::from_fn;
428 /// use ot_tools_io::OtToolsIoError;
429 /// use ot_tools_io::types::{Slices, SlotMarkers, SampleSettingsFile};
430 /// use ot_tools_io::settings::{
431 /// TrigQuantizationMode,
432 /// LoopMode,
433 /// TimeStretchMode,
434 /// };
435 /// # fn main() -> Result<(), OtToolsIoError> {
436 ///
437 /// let marks = SlotMarkers {
438 /// trim_offset: 0,
439 /// trim_end: 100,
440 /// loop_point: 0,
441 /// slices: Slices::default(),
442 /// slice_count: 0,
443 /// };
444 /// let x = SampleSettingsFile::new(
445 /// marks,
446 /// Some(48), // -24.0 <-> +24.0 (0 <-> 96)
447 /// Some(2880), // bpm x 24 (720 <-> 7200)
448 /// None, // trim_bar_len will be zero, see documentation explainer
449 /// None, // loop_bar_len will be zero, see documentation explainer
450 /// Some(TimeStretchMode::default()),
451 /// Some(TrigQuantizationMode::default()),
452 /// Some(LoopMode::default()),
453 /// )?;
454 /// assert_eq!(x.trim_start, 0);
455 /// assert_eq!(x.trim_end, 100);
456 /// assert_eq!(x.loop_start, 0);
457 /// assert_eq!(x.trim_bar_len, 0);
458 /// assert_eq!(x.loop_bar_len, 0);
459 /// assert_eq!(x.tempo, 2880);
460 /// assert_eq!(x.gain, 48);
461 /// assert_eq!(x.stretch, 2); // converted to u32 repr
462 /// assert_eq!(x.quantization, 255); // converted to u32 repr
463 /// assert_eq!(x.loop_mode, 0); // converted to u32 repr
464 /// # Ok(()) }
465 /// ```
466 /*
467 can't help this much unless i start creating sub-types, and i'm not too keen on that
468 just now considering what happened with TrimConfig and LoopConfig.
469
470 however, with some upcoming changes to slots, it may be worth thinking about two extra sub-types
471
472 for settings:
473 ```
474 struct SlotSettings {
475 stretch: TimeStretchMode,
476 quantization: TrigQuantizationMode,
477 loop_mode: LoopMode,
478 };
479 ```
480
481 for playback stretch control:
482 (trim bar len, loop bar len and bpm affect the time stretch algorithm)
483 ```
484 struct SlotStretchControl {
485 tempo: Option<u16>,
486 trim_bar_len: Option<u32>,
487 loop_bar_len: Option<u32>,
488 };
489 ```
490 */
491 #[allow(clippy::too_many_arguments)]
492 pub fn new<M: AsRef<SlotMarkers>>(
493 markers: M,
494 gain: Option<u16>,
495 tempo: Option<u32>,
496 trim_bar_len: Option<u32>,
497 loop_bar_len: Option<u32>,
498 stretch: Option<TimeStretchMode>,
499 quantization: Option<TrigQuantizationMode>,
500 loop_mode: Option<LoopMode>,
501 ) -> Result<Self, OtToolsIoError> {
502 let marks = markers.as_ref();
503
504 let mut new = Self {
505 // markers values, required
506 trim_start: marks.trim_offset,
507 trim_end: marks.trim_end,
508 loop_start: marks.loop_point,
509 slices: marks.slices,
510 slices_len: marks.slice_count,
511 // constant values
512 header: SAMPLES_HEADER,
513 datatype_version: SAMPLES_FILE_VERSION,
514 unknown: 0,
515 // optional values
516 gain: gain.unwrap_or(DEFAULT_GAIN as u16),
517 tempo: tempo.unwrap_or(DEFAULT_TEMPO as u32),
518 stretch: stretch.unwrap_or_default().into(),
519 quantization: quantization.unwrap_or_default().into(),
520 loop_mode: loop_mode.unwrap_or_default().into(),
521 trim_bar_len: trim_bar_len.unwrap_or(0), // see docs for explanation of why this is zero
522 loop_bar_len: loop_bar_len.unwrap_or(0), // see docs for explanation of why this is zero
523 // not set here
524 checksum: 0, // set below
525 };
526
527 new.checksum = new.calculate_checksum()?;
528
529 new.validate()?;
530
531 Ok(new)
532 }
533
534 /// Runs a series of validation checks on the field values for an instance of the type.
535 ///
536 /// Will return an appropriate error if a problem is discovered with a field's value.
537 ///
538 /// Read through the fields on [`SampleSettingsFile`] to find out which ones are checked and how
539 /// they are checked.
540 ///
541 /// # Invalid Gain Example
542 /// ```
543 /// use ot_tools_io::OtToolsIoError;
544 /// use ot_tools_io::types::{SampleSettingsFile, SlotMarkers};
545 /// use ot_tools_io::errors::SampleSettingsError;
546 /// # fn main() -> Result<(), OtToolsIoError> {
547 ///
548 /// let ss_f = SampleSettingsFile::new(
549 /// SlotMarkers::default(),
550 /// Some(100),
551 /// None,
552 /// None,
553 /// None,
554 /// None,
555 /// None,
556 /// None
557 /// );
558 ///
559 /// assert_eq!(
560 /// ss_f.unwrap_err().to_string(),
561 /// OtToolsIoError::SampleSettings(
562 /// SampleSettingsError::GainOutOfBounds {value: 100}
563 /// ).to_string(),
564 /// );
565 /// # Ok(()) }
566 /// ```
567 ///
568 /// # Invalid Tempo Example
569 /// ```
570 /// use ot_tools_io::OtToolsIoError;
571 /// use ot_tools_io::types::{SampleSettingsFile, SlotMarkers};
572 /// use ot_tools_io::errors::SampleSettingsError;
573 /// # fn main() -> Result<(), OtToolsIoError> {
574 ///
575 /// let ss_f = SampleSettingsFile::new(
576 /// SlotMarkers::default(),
577 /// None,
578 /// Some(120),
579 /// None,
580 /// None,
581 /// None,
582 /// None,
583 /// None
584 /// );
585 ///
586 /// assert_eq!(
587 /// ss_f.unwrap_err().to_string(),
588 /// OtToolsIoError::SampleSettings(
589 /// SampleSettingsError::TempoOutOfBounds {value: 120}
590 /// ).to_string(),
591 /// );
592 ///
593 /// # Ok(()) }
594 /// ```
595 pub fn validate(&self) -> Result<(), OtToolsIoError> {
596 #[allow(clippy::manual_range_contains)]
597 if self.tempo < 720 || self.tempo > 7200 {
598 return Err(SampleSettingsError::TempoOutOfBounds { value: self.tempo }.into());
599 }
600
601 if self.gain > 96 {
602 return Err(SampleSettingsError::GainOutOfBounds {
603 value: self.gain as u32,
604 }
605 .into());
606 }
607
608 SlotMarkers::from(self).validate()?;
609
610 LoopMode::try_from(self.loop_mode)?;
611 TimeStretchMode::try_from(self.stretch)?;
612 TrigQuantizationMode::try_from(self.quantization)?;
613
614 if !self.check_checksum()? {
615 return Err(SampleSettingsError::InvalidChecksum {
616 checksum: self.checksum,
617 }
618 .into());
619 };
620
621 if !self.check_header()? {
622 return Err(OtToolsIoError::FileHeader);
623 };
624
625 if !self.check_file_version()? {
626 return Err(SampleSettingsError::InvalidFileVersion {
627 version: self.datatype_version,
628 }
629 .into());
630 };
631
632 if !self.check_checksum()? {
633 return Err(SampleSettingsError::InvalidChecksum {
634 checksum: self.checksum,
635 }
636 .into());
637 };
638
639 Ok(())
640 }
641
642 /// Create a new [`SlotAttributes`] instance from this [`SampleSettingsFile`].
643 ///
644 /// Will validate the `slot_id` argument depending on the [`SlotType`] variant provided to the
645 /// `slot_type` argument.
646 ///
647 /// # Valid Example
648 /// ```
649 /// # use ot_tools_io::OtToolsIoError;
650 /// use ot_tools_io::types::{SlotMarkers, SampleSettingsFile, SlotAttributes, SlotType};
651 /// # use ot_tools_io::settings::{TimeStretchMode, LoopMode, TrigQuantizationMode};
652 /// use std::path::PathBuf;
653 /// # fn main() -> Result<(), OtToolsIoError> {
654 ///
655 /// let ss_f = SampleSettingsFile::new(
656 /// SlotMarkers::default(),
657 /// None,
658 /// None,
659 /// None,
660 /// None,
661 /// None,
662 /// None,
663 /// None,
664 /// )?;
665 ///
666 /// let attrs = ss_f.to_slot_attr(
667 /// SlotType::Static,
668 /// 100,
669 /// Some(PathBuf::from("some/path"))
670 /// )?;
671 ///
672 /// assert_eq!(
673 /// attrs,
674 /// SlotAttributes {
675 /// slot_id: 100,
676 /// slot_type: SlotType::Static,
677 /// path: Some(PathBuf::from("some/path")),
678 /// timestrech_mode: TimeStretchMode::default(),
679 /// loop_mode: LoopMode::default(),
680 /// trig_quantization_mode: TrigQuantizationMode::default(),
681 /// gain: 48,
682 /// bpm: 2880,
683 /// }
684 /// );
685 /// # Ok(()) }
686 /// ```
687 ///
688 /// # Error Example
689 /// ```
690 /// use ot_tools_io::OtToolsIoError;
691 /// use ot_tools_io::errors::SampleSettingsError;
692 /// use ot_tools_io::types::{SlotMarkers, SampleSettingsFile, SlotAttributes, SlotType};
693 /// use std::path::PathBuf;
694 /// # fn main() -> Result<(), OtToolsIoError> {
695 ///
696 /// let ss_f = SampleSettingsFile::new(
697 /// SlotMarkers::default(),
698 /// None,
699 /// None,
700 /// None,
701 /// None,
702 /// None,
703 /// None,
704 /// None,
705 /// )?;
706 ///
707 /// let attrs_err = ss_f.to_slot_attr(
708 /// SlotType::Static,
709 /// 200, // bad slot id!
710 /// Some(PathBuf::from("some/path"))
711 /// ).unwrap_err();
712 ///
713 /// assert_eq!(
714 /// attrs_err.to_string(),
715 /// OtToolsIoError::SampleSettings(
716 /// SampleSettingsError::SlotIdOutOfBounds { id: 200, slot_type: SlotType::Static }
717 /// ).to_string()
718 /// );
719 /// # Ok(()) }
720 /// ```
721 pub fn to_slot_attr(
722 &self,
723 slot_type: SlotType,
724 slot_id: u8,
725 slot_path: Option<PathBuf>,
726 ) -> Result<SlotAttributes, OtToolsIoError> {
727 // neither static nor flex slots cannot have id = 0
728 if slot_id == 0 {
729 return Err(SampleSettingsError::SlotIdOutOfBounds {
730 id: slot_id,
731 slot_type,
732 }
733 .into());
734 }
735
736 // static slots cannot have id > 128
737 if slot_type == SlotType::Static && slot_id > 128 {
738 return Err(SampleSettingsError::SlotIdOutOfBounds {
739 id: slot_id,
740 slot_type,
741 }
742 .into());
743 }
744
745 // flex slots cannot have id > 136
746 if slot_type == SlotType::Flex && slot_id > 136 {
747 return Err(SampleSettingsError::SlotIdOutOfBounds {
748 id: slot_id,
749 slot_type,
750 }
751 .into());
752 }
753
754 Ok(SlotAttributes {
755 slot_type,
756 slot_id,
757 path: slot_path,
758 timestrech_mode: TimeStretchMode::try_from(self.stretch)?,
759 loop_mode: LoopMode::try_from(self.loop_mode)?,
760 trig_quantization_mode: TrigQuantizationMode::try_from(self.quantization)?,
761 gain: self.gain as u8,
762 bpm: self.tempo as u16,
763 })
764 }
765
766 /// Returns the `stretch` field as a variant of [`TimeStretchMode`].
767 ///
768 /// Essentially calls [`TimeStretchMode::try_from`]
769 pub fn timestretch_mode_into_setting(&self) -> Result<TimeStretchMode, OtToolsIoError> {
770 TimeStretchMode::try_from(self.stretch).map_err(OtToolsIoError::SettingValue)
771 }
772
773 /// Returns the `loop_mode` field as a variant of [`LoopMode`]
774 ///
775 /// Essentially calls [`LoopMode::try_from`]
776 pub fn loop_mode_into_setting(&self) -> Result<LoopMode, OtToolsIoError> {
777 LoopMode::try_from(self.loop_mode).map_err(OtToolsIoError::SettingValue)
778 }
779
780 /// Returns the `quantization` field as a variant of [`TrigQuantizationMode`]
781 ///
782 /// Essentially calls [`TrigQuantizationMode::try_from`]
783 pub fn trig_quantization_into_setting(&self) -> Result<TrigQuantizationMode, OtToolsIoError> {
784 TrigQuantizationMode::try_from(self.loop_mode).map_err(OtToolsIoError::SettingValue)
785 }
786}
787
788#[cfg(test)]
789mod new {
790 use super::test_utils::create_mock_new_sample_settings;
791 use super::SampleSettingsError;
792 use crate::OtToolsIoError;
793
794 #[test]
795 fn invalid_oob_temp_high() -> Result<(), OtToolsIoError> {
796 assert_eq!(
797 create_mock_new_sample_settings(Some(7201), None)
798 .unwrap_err()
799 .to_string(),
800 OtToolsIoError::SampleSettings(SampleSettingsError::TempoOutOfBounds { value: 7201 })
801 .to_string()
802 );
803 Ok(())
804 }
805
806 #[test]
807 fn invalid_oob_temp_low() -> Result<(), OtToolsIoError> {
808 let s_f = create_mock_new_sample_settings(Some(29), None);
809 assert_eq!(
810 s_f.unwrap_err().to_string(),
811 OtToolsIoError::SampleSettings(SampleSettingsError::TempoOutOfBounds { value: 29 })
812 .to_string()
813 );
814 Ok(())
815 }
816
817 #[test]
818 fn invalid_oob_gain() -> Result<(), OtToolsIoError> {
819 assert_eq!(
820 create_mock_new_sample_settings(None, Some(97))
821 .unwrap_err()
822 .to_string(),
823 OtToolsIoError::SampleSettings(SampleSettingsError::GainOutOfBounds { value: 97 })
824 .to_string()
825 );
826 Ok(())
827 }
828}
829
830#[cfg(test)]
831mod validate {
832 use super::test_utils::create_mock_new_sample_settings;
833 use super::SampleSettingsError;
834 use crate::settings::InvalidValueError;
835 use crate::traits::HasChecksumField;
836 use crate::OtToolsIoError;
837
838 #[test]
839 fn ok() -> Result<(), OtToolsIoError> {
840 let s_f = create_mock_new_sample_settings(None, None)?;
841 assert!(s_f.validate().is_ok());
842 Ok(())
843 }
844
845 // gain and tempo are tested above
846
847 #[test]
848 fn err_stretch() -> Result<(), OtToolsIoError> {
849 let mut s_f = create_mock_new_sample_settings(None, None)?;
850 s_f.stretch = 255;
851 s_f.checksum = s_f.calculate_checksum()?;
852 assert_eq!(
853 s_f.validate().unwrap_err().to_string(),
854 OtToolsIoError::SettingValue(InvalidValueError::TimeStretchMode).to_string()
855 );
856 Ok(())
857 }
858
859 #[test]
860 fn err_loop() -> Result<(), OtToolsIoError> {
861 let mut s_f = create_mock_new_sample_settings(None, None)?;
862 s_f.loop_mode = 255;
863 s_f.checksum = s_f.calculate_checksum()?;
864 assert_eq!(
865 s_f.validate().unwrap_err().to_string(),
866 OtToolsIoError::SettingValue(InvalidValueError::LoopMode).to_string()
867 );
868 Ok(())
869 }
870
871 #[test]
872 fn err_quant() -> Result<(), OtToolsIoError> {
873 let mut s_f = create_mock_new_sample_settings(None, None)?;
874 s_f.quantization = 250; // 255 is an actual value for quantization!
875 s_f.checksum = s_f.calculate_checksum()?;
876 assert_eq!(
877 s_f.validate().unwrap_err().to_string(),
878 OtToolsIoError::SettingValue(InvalidValueError::TrigQuantizationMode).to_string()
879 );
880 Ok(())
881 }
882
883 #[test]
884 fn err_chksum() -> Result<(), OtToolsIoError> {
885 let mut s_f = create_mock_new_sample_settings(None, None)?;
886 s_f.checksum = 1;
887 assert_eq!(
888 s_f.validate().unwrap_err().to_string(),
889 OtToolsIoError::SampleSettings(SampleSettingsError::InvalidChecksum {
890 checksum: s_f.checksum
891 })
892 .to_string(),
893 );
894 Ok(())
895 }
896
897 #[test]
898 fn err_header() -> Result<(), OtToolsIoError> {
899 let mut s_f = create_mock_new_sample_settings(None, None)?;
900 s_f.header[0] = 255;
901 s_f.checksum = s_f.calculate_checksum()?;
902 assert_eq!(
903 s_f.validate().unwrap_err().to_string(),
904 OtToolsIoError::FileHeader.to_string(),
905 );
906 Ok(())
907 }
908
909 #[test]
910 fn err_file_version() -> Result<(), OtToolsIoError> {
911 let mut s_f = create_mock_new_sample_settings(None, None)?;
912 s_f.datatype_version = 255;
913 s_f.checksum = s_f.calculate_checksum()?;
914 assert_eq!(
915 s_f.validate().unwrap_err().to_string(),
916 OtToolsIoError::SampleSettings(SampleSettingsError::InvalidFileVersion {
917 version: s_f.datatype_version
918 })
919 .to_string(),
920 );
921 Ok(())
922 }
923}
924
925#[cfg(test)]
926mod to_slot_attrs {
927 use super::test_utils::create_mock_new_sample_settings;
928 use crate::projects::SlotAttributes;
929 use crate::samples::SampleSettingsError;
930 use crate::settings::SlotType;
931 use crate::OtToolsIoError;
932 use std::path::PathBuf;
933
934 #[test]
935 fn basic_static() -> Result<(), OtToolsIoError> {
936 let settings = create_mock_new_sample_settings(None, None)?;
937
938 let attrs = settings.to_slot_attr(
939 SlotType::Static,
940 1,
941 Some(PathBuf::from("../AUDIO/path.wav")),
942 )?;
943
944 assert_eq!(
945 attrs,
946 SlotAttributes {
947 slot_type: SlotType::Static,
948 slot_id: 1,
949 path: Some(PathBuf::from("../AUDIO/path.wav")),
950 timestrech_mode: Default::default(),
951 loop_mode: Default::default(),
952 trig_quantization_mode: Default::default(),
953 gain: 48,
954 bpm: 2880,
955 },
956 );
957 Ok(())
958 }
959
960 #[test]
961 fn invalid_static_slot_id_0() -> Result<(), OtToolsIoError> {
962 let settings = create_mock_new_sample_settings(None, None)?;
963
964 let attrs = settings.to_slot_attr(
965 SlotType::Static,
966 0,
967 Some(PathBuf::from("../AUDIO/path.wav")),
968 );
969
970 assert_eq!(
971 attrs.unwrap_err().to_string(),
972 OtToolsIoError::SampleSettings(SampleSettingsError::SlotIdOutOfBounds {
973 id: 0,
974 slot_type: SlotType::Static
975 })
976 .to_string(),
977 );
978 Ok(())
979 }
980
981 #[test]
982 fn invalid_static_slot_id_129() -> Result<(), OtToolsIoError> {
983 let settings = create_mock_new_sample_settings(None, None)?;
984
985 let attrs = settings.to_slot_attr(
986 SlotType::Static,
987 129,
988 Some(PathBuf::from("../AUDIO/path.wav")),
989 );
990
991 assert_eq!(
992 attrs.unwrap_err().to_string(),
993 OtToolsIoError::SampleSettings(SampleSettingsError::SlotIdOutOfBounds {
994 id: 129,
995 slot_type: SlotType::Static
996 })
997 .to_string(),
998 );
999 Ok(())
1000 }
1001
1002 #[test]
1003 fn basic_flex() -> Result<(), OtToolsIoError> {
1004 let settings = create_mock_new_sample_settings(None, None)?;
1005
1006 let attrs =
1007 settings.to_slot_attr(SlotType::Flex, 1, Some(PathBuf::from("../AUDIO/path.wav")))?;
1008
1009 assert_eq!(
1010 attrs,
1011 SlotAttributes {
1012 slot_type: SlotType::Flex,
1013 slot_id: 1,
1014 path: Some(PathBuf::from("../AUDIO/path.wav")),
1015 timestrech_mode: Default::default(),
1016 loop_mode: Default::default(),
1017 trig_quantization_mode: Default::default(),
1018 gain: 48,
1019 bpm: 2880,
1020 },
1021 );
1022 Ok(())
1023 }
1024
1025 #[test]
1026 fn invalid_flex_slot_id_0() -> Result<(), OtToolsIoError> {
1027 let settings = create_mock_new_sample_settings(None, None)?;
1028
1029 let attrs =
1030 settings.to_slot_attr(SlotType::Flex, 0, Some(PathBuf::from("../AUDIO/path.wav")));
1031
1032 assert_eq!(
1033 attrs.unwrap_err().to_string(),
1034 OtToolsIoError::SampleSettings(SampleSettingsError::SlotIdOutOfBounds {
1035 id: 0,
1036 slot_type: SlotType::Flex
1037 })
1038 .to_string(),
1039 );
1040 Ok(())
1041 }
1042
1043 #[test]
1044 fn invalid_flex_slot_id_137() -> Result<(), OtToolsIoError> {
1045 let settings = create_mock_new_sample_settings(None, None)?;
1046
1047 let attrs = settings.to_slot_attr(
1048 SlotType::Flex,
1049 137,
1050 Some(PathBuf::from("../AUDIO/path.wav")),
1051 );
1052
1053 assert_eq!(
1054 attrs.unwrap_err().to_string(),
1055 OtToolsIoError::SampleSettings(SampleSettingsError::SlotIdOutOfBounds {
1056 id: 137,
1057 slot_type: SlotType::Flex
1058 })
1059 .to_string(),
1060 );
1061 Ok(())
1062 }
1063}
1064
1065impl SwapBytes for SampleSettingsFile {
1066 fn swap_bytes(self) -> Self {
1067 let mut bswapped_slices = self.slices;
1068
1069 for (i, slice) in self.slices.iter().enumerate() {
1070 bswapped_slices[i] = slice.swap_bytes();
1071 }
1072
1073 Self {
1074 header: SAMPLES_HEADER,
1075 datatype_version: SAMPLES_FILE_VERSION,
1076 unknown: self.unknown,
1077 tempo: self.tempo.swap_bytes(),
1078 trim_bar_len: self.trim_bar_len.swap_bytes(),
1079 loop_bar_len: self.loop_bar_len.swap_bytes(),
1080 stretch: self.stretch.swap_bytes(),
1081 loop_mode: self.loop_mode.swap_bytes(),
1082 gain: self.gain.swap_bytes(),
1083 quantization: self.quantization.swap_bytes(),
1084 trim_start: self.trim_start.swap_bytes(),
1085 trim_end: self.trim_end.swap_bytes(),
1086 loop_start: self.loop_start.swap_bytes(),
1087 slices: bswapped_slices,
1088 slices_len: self.slices_len.swap_bytes(),
1089 checksum: self.checksum.swap_bytes(),
1090 }
1091 }
1092}
1093
1094impl OctatrackFileIO for SampleSettingsFile {
1095 /// Encodes struct data to binary representation, after some pre-processing.
1096 ///
1097 /// Before serializing, will:
1098 /// 1. generate checksum value
1099 /// 2. swap bytes of values (when current system is little-endian)
1100 fn to_bytes(&self) -> Result<Vec<u8>, OtToolsIoError> {
1101 // todo: figure out a non-clone version of this
1102 #[allow(clippy::clone_on_copy)]
1103 let mut chkd = self.clone();
1104 chkd.checksum = self.calculate_checksum()?;
1105
1106 let encoded = if cfg!(target_endian = "little") {
1107 bincode::serialize(&chkd.swap_bytes())?
1108 } else {
1109 bincode::serialize(&chkd)?
1110 };
1111 Ok(encoded)
1112 }
1113
1114 /// Decode raw bytes of a `.ot` data file into a new struct,
1115 /// swapping byte values if system is little-endian.
1116 fn from_bytes(bytes: &[u8]) -> Result<Self, OtToolsIoError> {
1117 let decoded: Self = bincode::deserialize(bytes)?;
1118
1119 // todo: figure out a non-clone version of this
1120 #[allow(clippy::clone_on_copy)]
1121 let mut bswapd = decoded.clone();
1122
1123 // swapping bytes is one required when running on little-endian systems
1124 if cfg!(target_endian = "little") {
1125 bswapd = decoded.swap_bytes();
1126 }
1127
1128 Ok(bswapd)
1129 }
1130}
1131
1132#[cfg(test)]
1133mod from_bytes {
1134 use crate::read_bin_file;
1135 use crate::samples::test_utils::mock_0_slice_test_file;
1136 use crate::test_utils::get_samples_dirpath;
1137 use crate::{OctatrackFileIO, OtToolsIoError, SampleSettingsFile};
1138 #[test]
1139 fn valid() -> Result<(), OtToolsIoError> {
1140 let path = get_samples_dirpath().join("checksum").join("0slices.ot");
1141 let bytes = read_bin_file(&path)?;
1142 let s = SampleSettingsFile::from_bytes(&bytes)?;
1143 assert_eq!(s, mock_0_slice_test_file());
1144 Ok(())
1145 }
1146}
1147
1148#[cfg(test)]
1149mod to_bytes {
1150 use crate::read_bin_file;
1151 use crate::samples::test_utils::mock_0_slice_test_file;
1152 use crate::test_utils::get_samples_dirpath;
1153 use crate::{OctatrackFileIO, OtToolsIoError};
1154 #[test]
1155 fn valid() -> Result<(), OtToolsIoError> {
1156 let path = get_samples_dirpath().join("checksum").join("0slices.ot");
1157 let bytes = read_bin_file(&path)?;
1158 let b = mock_0_slice_test_file().to_bytes()?;
1159 assert_eq!(b, bytes);
1160 Ok(())
1161 }
1162}
1163
1164impl HasChecksumField for SampleSettingsFile {
1165 // tests for this are in the main tests directory -- `samples_settings_files.rs`
1166 fn calculate_checksum(&self) -> Result<u16, OtToolsIoError> {
1167 let bytes = bincode::serialize(&self)?;
1168
1169 // skip header and checksum byte values
1170 let checksum_bytes = &bytes[16..bytes.len() - 2];
1171
1172 let chk: u32 = checksum_bytes
1173 .iter()
1174 .map(|x| *x as u32)
1175 .sum::<u32>()
1176 .rem_euclid(u16::MAX as u32 + 1);
1177
1178 Ok(chk as u16)
1179 }
1180
1181 fn update_checksum(&mut self) -> Result<(), OtToolsIoError> {
1182 self.checksum = self.calculate_checksum()?;
1183 Ok(())
1184 }
1185
1186 fn check_checksum(&self) -> Result<bool, OtToolsIoError> {
1187 Ok(self.checksum == self.calculate_checksum()?)
1188 }
1189}
1190
1191#[cfg(test)]
1192mod checksum_field {
1193 use super::test_utils::create_mock_new_sample_settings;
1194 use crate::{HasChecksumField, OtToolsIoError};
1195 #[test]
1196 fn valid() -> Result<(), OtToolsIoError> {
1197 let mut x = create_mock_new_sample_settings(None, None)?;
1198 x.checksum = x.calculate_checksum()?;
1199 assert!(x.check_checksum()?);
1200 Ok(())
1201 }
1202
1203 #[test]
1204 fn invalid() -> Result<(), OtToolsIoError> {
1205 let mut x = create_mock_new_sample_settings(None, None)?;
1206 x.checksum = x.calculate_checksum()?;
1207 x.checksum = 0;
1208 assert!(!x.check_checksum()?);
1209 Ok(())
1210 }
1211
1212 mod files {
1213 use crate::test_utils::get_samples_dirpath;
1214 use crate::{HasChecksumField, OctatrackFileIO, OtToolsIoError, SampleSettingsFile};
1215
1216 fn helper(test_name: String) -> Result<(u16, u16), OtToolsIoError> {
1217 let mut src_path = get_samples_dirpath().join("checksum").join(&test_name);
1218 src_path.set_extension("ot");
1219
1220 let valid = SampleSettingsFile::from_data_file(&src_path)?;
1221
1222 #[allow(clippy::clone_on_copy)] // want a full deep copy / clone
1223 let mut x = valid.clone();
1224 x.checksum = 0;
1225
1226 Ok((x.calculate_checksum()?, valid.checksum))
1227 }
1228
1229 #[test]
1230 fn zero_slices_trig_quant_one_step() -> Result<(), OtToolsIoError> {
1231 let (test, valid) = helper("0slices-tq1step".to_string())?;
1232 assert_eq!(test, valid);
1233 Ok(())
1234 }
1235
1236 #[test]
1237 fn zero_slices() -> Result<(), OtToolsIoError> {
1238 let (test, valid) = helper("0slices".to_string())?;
1239 assert_eq!(test, valid);
1240 Ok(())
1241 }
1242
1243 #[test]
1244 fn one_slice() -> Result<(), OtToolsIoError> {
1245 let (test, valid) = helper("1slices".to_string())?;
1246 assert_eq!(test, valid);
1247 Ok(())
1248 }
1249
1250 #[test]
1251 fn two_slices() -> Result<(), OtToolsIoError> {
1252 let (test, valid) = helper("2slices".to_string())?;
1253 assert_eq!(test, valid);
1254 Ok(())
1255 }
1256
1257 #[test]
1258 fn four_slices() -> Result<(), OtToolsIoError> {
1259 let (test, valid) = helper("4slices".to_string())?;
1260 assert_eq!(test, valid);
1261 Ok(())
1262 }
1263
1264 #[test]
1265 fn eight_slices() -> Result<(), OtToolsIoError> {
1266 let (test, valid) = helper("8slices".to_string())?;
1267 assert_eq!(test, valid);
1268 Ok(())
1269 }
1270
1271 #[test]
1272 fn sixteen_slices() -> Result<(), OtToolsIoError> {
1273 let (test, valid) = helper("16slices".to_string())?;
1274 assert_eq!(test, valid);
1275 Ok(())
1276 }
1277
1278 #[test]
1279 fn thirty_two_slices() -> Result<(), OtToolsIoError> {
1280 let (test, valid) = helper("32slices".to_string())?;
1281 assert_eq!(test, valid);
1282 Ok(())
1283 }
1284
1285 #[test]
1286 fn forty_eight_slices() -> Result<(), OtToolsIoError> {
1287 let (test, valid) = helper("48slices".to_string())?;
1288 assert_eq!(test, valid);
1289 Ok(())
1290 }
1291
1292 #[test]
1293 fn sixty_two_slices() -> Result<(), OtToolsIoError> {
1294 let (test, valid) = helper("62slices".to_string())?;
1295 assert_eq!(test, valid);
1296 Ok(())
1297 }
1298
1299 #[test]
1300 fn sixty_three_slices() -> Result<(), OtToolsIoError> {
1301 let (test, valid) = helper("63slices".to_string())?;
1302 assert_eq!(test, valid);
1303 Ok(())
1304 }
1305
1306 #[test]
1307 fn sixty_four_slices_correct() -> Result<(), OtToolsIoError> {
1308 let (test, valid) = helper("64slices".to_string())?;
1309 assert_eq!(test, valid);
1310 Ok(())
1311 }
1312 }
1313}
1314
1315impl HasHeaderField for SampleSettingsFile {
1316 fn check_header(&self) -> Result<bool, OtToolsIoError> {
1317 Ok(self.header == SAMPLES_HEADER)
1318 }
1319}
1320
1321#[cfg(test)]
1322mod header_field {
1323 use super::test_utils::create_mock_new_sample_settings;
1324 use crate::{HasHeaderField, OtToolsIoError};
1325 #[test]
1326 fn valid() -> Result<(), OtToolsIoError> {
1327 assert!(create_mock_new_sample_settings(None, None)?.check_header()?);
1328 Ok(())
1329 }
1330
1331 #[test]
1332 fn invalid() -> Result<(), OtToolsIoError> {
1333 let mut mutated = create_mock_new_sample_settings(None, None)?;
1334 mutated.header[0] = 0x00;
1335 mutated.header[20] = 111;
1336 assert!(!mutated.check_header()?);
1337 Ok(())
1338 }
1339}
1340
1341impl HasFileVersionField for SampleSettingsFile {
1342 fn check_file_version(&self) -> Result<bool, OtToolsIoError> {
1343 Ok(self.datatype_version == SAMPLES_FILE_VERSION)
1344 }
1345}
1346
1347#[cfg(test)]
1348mod file_version_field {
1349 use super::test_utils::create_mock_new_sample_settings;
1350 use crate::{HasFileVersionField, OtToolsIoError};
1351 #[test]
1352 fn valid() -> Result<(), OtToolsIoError> {
1353 assert!(create_mock_new_sample_settings(None, None)?.check_file_version()?);
1354 Ok(())
1355 }
1356
1357 #[test]
1358 fn invalid() -> Result<(), OtToolsIoError> {
1359 let mut mutated = create_mock_new_sample_settings(None, None)?;
1360 mutated.datatype_version = 0;
1361 assert!(!mutated.check_file_version()?);
1362 Ok(())
1363 }
1364}
1365
1366impl<A, M> From<(A, M)> for SampleSettingsFile
1367where
1368 A: AsRef<SlotAttributes>,
1369 M: AsRef<SlotMarkers>,
1370{
1371 fn from(value: (A, M)) -> Self {
1372 let attrs = value.0.as_ref();
1373 let markers = value.1.as_ref();
1374
1375 SampleSettingsFile {
1376 header: SAMPLES_HEADER,
1377 datatype_version: SAMPLES_FILE_VERSION,
1378 unknown: 0,
1379 tempo: attrs.bpm as u32,
1380 // todo: does not currently exist as a field in `SlotAttributes`
1381 trim_bar_len: 0,
1382 // todo: does not currently exist as a field in `SlotAttributes`
1383 loop_bar_len: 0,
1384 stretch: attrs.timestrech_mode.into(),
1385 loop_mode: attrs.loop_mode.into(),
1386 gain: attrs.gain as u16,
1387 quantization: attrs.trig_quantization_mode.into(),
1388 trim_start: markers.trim_offset,
1389 trim_end: markers.trim_end,
1390 loop_start: markers.loop_point,
1391 slices: markers.slices,
1392 slices_len: markers.slice_count,
1393 checksum: 0,
1394 }
1395 }
1396}
1397
1398#[cfg(test)]
1399mod sample_settings_from {
1400 use crate::generics::Slots;
1401 use crate::projects::SlotAttributes;
1402 use crate::MarkersFile;
1403 use crate::OtToolsIoError;
1404 use crate::SampleSettingsFile;
1405
1406 #[test]
1407 fn from_owned_valid() -> Result<(), OtToolsIoError> {
1408 let slots = Slots::<Option<SlotAttributes>>::default();
1409 let slot = slots.recording_buffers[0].clone();
1410 let markers = MarkersFile::default().flex_slots[0];
1411 let _ = SampleSettingsFile::from((slot.unwrap(), markers));
1412 Ok(())
1413 }
1414
1415 #[test]
1416 fn from_borrowed_valid() -> Result<(), OtToolsIoError> {
1417 let slots = Slots::<Option<SlotAttributes>>::default();
1418 let slot = slots.recording_buffers[0].clone();
1419 let markers = MarkersFile::default().flex_slots[0];
1420 let _ = SampleSettingsFile::from((&slot.unwrap(), &markers));
1421 Ok(())
1422 }
1423}