1pub mod configs;
9pub mod options;
10pub mod slices;
11
12use crate::{
13 samples::options::{SampleAttributeTimestrechMode, SampleAttributeTrigQuantizationMode},
14 samples::{
15 configs::{SampleLoopConfig, SampleTrimConfig},
16 slices::{Slice, Slices},
17 },
18 CalculateChecksum, CheckChecksum, CheckHeader, CheckIntegrity, Decode, Encode,
19 OptionEnumValueConvert, RBoxErr,
20};
21use ot_tools_io_derive::Decodeable;
22use serde::{Deserialize, Serialize};
23use serde_big_array::BigArray;
24
25pub const SAMPLES_HEADER: [u8; 21] = [
32 0x46, 0x4F, 0x52, 0x4D, 0x00, 0x00, 0x00, 0x00, 0x44, 0x50, 0x53, 0x31, 0x53, 0x4D, 0x50, 0x41,
33 0x00, 0x00, 0x00, 0x00, 0x00,
34];
35
36pub const SAMPLES_FILE_VERSION: u8 = 2;
38
39pub trait SwapBytes {
42 type T;
44
45 fn swap_bytes(self) -> RBoxErr<Self::T>;
48}
49
50#[derive(Debug)]
51enum SampleAttributeErrors {
52 TempoOutOfBounds,
53 GainOutOfBounds,
54}
55impl std::fmt::Display for SampleAttributeErrors {
56 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
57 match self {
58 Self::GainOutOfBounds => write!(f, "invalid gain, must be in range -24.0 <= x <= 24.0"),
59 Self::TempoOutOfBounds => write!(
60 f,
61 "invalid tempo value, must be in range 30.0 <= x <= 300.0"
62 ),
63 }
64 }
65}
66impl std::error::Error for SampleAttributeErrors {}
67
68fn bin_gain_from_human(human_gain: &f32) -> RBoxErr<u16> {
72 #[allow(clippy::manual_range_contains)]
74 if human_gain < &-24.0 || human_gain > &24.0 {
75 Err(SampleAttributeErrors::GainOutOfBounds.into())
76 } else {
77 let new_gain_f32 = (2.0 * (10.0 * (human_gain + 24.0)).round()) * 0.1;
78 Ok(new_gain_f32 as u16)
79 }
80}
81
82fn bin_tempo_from_human(human_tempo: &f32) -> RBoxErr<u32> {
86 #[allow(clippy::manual_range_contains)]
88 if human_tempo < &30.0 || human_tempo > &300.0 {
89 Err(SampleAttributeErrors::TempoOutOfBounds.into())
90 } else {
91 let bin_tempo_f32 = human_tempo * 24.0;
92 Ok(bin_tempo_f32 as u32)
93 }
94}
95
96#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
100pub struct SampleAttributes {
101 pub header: [u8; 21],
103
104 pub datatype_version: u8,
106
107 pub unknown: u8,
109
110 pub tempo: u32,
112
113 pub trim_len: u32,
117
118 pub loop_len: u32,
121
122 pub stretch: u32,
125
126 pub loop_mode: u32,
129
130 pub gain: u16,
134
135 pub quantization: u8,
138
139 pub trim_start: u32,
143
144 pub trim_end: u32,
151
152 pub loop_start: u32,
158
159 #[serde(with = "BigArray")]
163 pub slices: [Slice; 64],
164
165 pub slices_len: u32,
168
169 pub checksum: u16,
173}
174
175impl SwapBytes for SampleAttributes {
176 type T = SampleAttributes;
179
180 fn swap_bytes(self) -> RBoxErr<Self::T> {
183 let mut bswapped_slices: [Slice; 64] = self.slices;
184
185 for (i, slice) in self.slices.iter().enumerate() {
186 bswapped_slices[i] = slice.swap_bytes()?;
187 }
188
189 let bswapped = Self {
190 header: SAMPLES_HEADER,
191 datatype_version: SAMPLES_FILE_VERSION,
192 unknown: self.unknown,
193 tempo: self.tempo.swap_bytes(),
194 trim_len: self.trim_len.swap_bytes(),
195 loop_len: self.loop_len.swap_bytes(),
196 stretch: self.stretch.swap_bytes(),
197 loop_mode: self.loop_mode.swap_bytes(),
198 gain: self.gain.swap_bytes(),
199 quantization: self.quantization.swap_bytes(),
200 trim_start: self.trim_start.swap_bytes(),
201 trim_end: self.trim_end.swap_bytes(),
202 loop_start: self.loop_start.swap_bytes(),
203 slices: bswapped_slices,
204 slices_len: self.slices_len.swap_bytes(),
205 checksum: self.checksum.swap_bytes(),
206 };
207
208 Ok(bswapped)
209 }
210}
211
212impl SampleAttributes {
213 pub fn new(
245 tempo: &f32,
246 stretch: &SampleAttributeTimestrechMode,
247 quantization: &SampleAttributeTrigQuantizationMode,
248 gain: &f32,
249 trim_config: &SampleTrimConfig,
250 loop_config: &SampleLoopConfig,
251 slices: &Slices,
252 ) -> RBoxErr<Self> {
253 Ok(Self {
254 header: SAMPLES_HEADER,
255 datatype_version: SAMPLES_FILE_VERSION,
256 unknown: 0,
257 gain: bin_gain_from_human(gain)?,
258 stretch: stretch.value()?,
259 tempo: bin_tempo_from_human(tempo)?,
260 quantization: quantization.value()? as u8,
261 trim_start: trim_config.start,
262 trim_end: trim_config.end,
263 trim_len: trim_config.length, loop_start: loop_config.start,
265 loop_len: loop_config.length, loop_mode: loop_config.mode.value()?,
267 slices: slices.slices,
268 slices_len: slices.count,
269 checksum: 0,
270 })
271 }
272}
273
274impl Decode for SampleAttributes {
276 fn decode(bytes: &[u8]) -> RBoxErr<Self> {
280 let decoded: Self = bincode::deserialize(bytes)?;
281 let mut bswapd = decoded.clone();
282
283 if cfg!(target_endian = "little") {
285 bswapd = decoded.swap_bytes()?;
286 }
287
288 Ok(bswapd)
289 }
290}
291
292impl Encode for SampleAttributes {
294 fn encode(&self) -> RBoxErr<Vec<u8>> {
301 Ok(bincode::serialize(self)?)
302 }
303}
304
305impl CalculateChecksum for SampleAttributes {
306 fn calculate_checksum(&self) -> RBoxErr<u16> {
307 let bytes = self.encode()?;
308
309 let checksum_bytes = &bytes[16..bytes.len() - 2];
311
312 let chk: u32 = checksum_bytes
313 .iter()
314 .map(|x| *x as u32)
315 .sum::<u32>()
316 .rem_euclid(u16::MAX as u32 + 1);
317
318 Ok(chk as u16)
319 }
320}
321
322impl CheckHeader for SampleAttributes {
323 fn check_header(&self) -> bool {
324 self.header == SAMPLES_HEADER
325 }
326}
327
328impl CheckChecksum for SampleAttributes {
329 fn check_checksum(&self) -> RBoxErr<bool> {
330 Ok(self.checksum == self.calculate_checksum()?)
331 }
332}
333
334impl CheckIntegrity for SampleAttributes {}
335
336#[derive(Debug, Serialize, Deserialize, Decodeable)]
338pub struct SampleAttributesRawBytes {
339 #[serde(with = "BigArray")]
340 pub data: [u8; 832],
341}
342
343#[cfg(test)]
344mod test {
345 use crate::samples::configs::{SampleLoopConfig, SampleTrimConfig};
346 use crate::samples::options::SampleAttributeLoopMode;
347 use crate::samples::slices::{Slice, Slices};
348
349 fn create_mock_configs_blank() -> (SampleTrimConfig, SampleLoopConfig, Slices) {
350 let trim_config = SampleTrimConfig {
351 start: 0,
352 end: 0,
353 length: 0,
354 };
355
356 let loop_config = SampleLoopConfig {
357 start: 0,
358 length: 0,
359 mode: SampleAttributeLoopMode::Normal,
360 };
361
362 let default_slice = Slice {
363 trim_start: 0,
364 trim_end: 0,
365 loop_start: 0xFFFFFFFF,
366 };
367
368 let slices: [Slice; 64] = [default_slice; 64];
369
370 let slice_conf = Slices { slices, count: 0 };
371
372 (trim_config, loop_config, slice_conf)
373 }
374
375 mod checksum {
376 mod file_based {
435 use crate::read_type_from_bin_file;
436 use crate::samples::{CalculateChecksum, SampleAttributes};
437 use crate::test_utils::get_samples_dirpath;
438
439 #[test]
440 fn zero_slices_trig_quant_one_step() {
441 let path = get_samples_dirpath()
442 .join("checksum")
443 .join("0slices-tq1step.ot");
444 let data = read_type_from_bin_file::<SampleAttributes>(&path).unwrap();
445 assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
446 }
447
448 #[test]
449 fn zero_slices() {
450 let path = get_samples_dirpath().join("checksum").join("0slices.ot");
451 let data = read_type_from_bin_file::<SampleAttributes>(&path).unwrap();
452 assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
453 }
454
455 #[test]
456 fn one_slice() {
457 let path = get_samples_dirpath().join("checksum").join("1slices.ot");
458 let data = read_type_from_bin_file::<SampleAttributes>(&path).unwrap();
459 assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
460 }
461 #[test]
462 fn two_slices() {
463 let path = get_samples_dirpath().join("checksum").join("2slices.ot");
464 let data = read_type_from_bin_file::<SampleAttributes>(&path).unwrap();
465 assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
466 }
467 #[test]
468 fn four_slices() {
469 let path = get_samples_dirpath().join("checksum").join("4slices.ot");
470 let data = read_type_from_bin_file::<SampleAttributes>(&path).unwrap();
471 assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
472 }
473 #[test]
474 fn eight_slices() {
475 let path = get_samples_dirpath().join("checksum").join("8slices.ot");
476 let data = read_type_from_bin_file::<SampleAttributes>(&path).unwrap();
477 assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
478 }
479 #[test]
480 fn sixteen_slices() {
481 let path = get_samples_dirpath().join("checksum").join("16slices.ot");
482 let data = read_type_from_bin_file::<SampleAttributes>(&path).unwrap();
483 assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
484 }
485 #[test]
486 fn thirty_two_slices() {
487 let path = get_samples_dirpath().join("checksum").join("32slices.ot");
488 let data = read_type_from_bin_file::<SampleAttributes>(&path).unwrap();
489 assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
490 }
491 #[test]
492 fn forty_eight_slices() {
493 let path = get_samples_dirpath().join("checksum").join("48slices.ot");
494 let data = read_type_from_bin_file::<SampleAttributes>(&path).unwrap();
495 assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
496 }
497 #[test]
498 fn sixty_two_slices() {
499 let path = get_samples_dirpath().join("checksum").join("62slices.ot");
500 let data = read_type_from_bin_file::<SampleAttributes>(&path).unwrap();
501 assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
502 }
503 #[test]
504 fn sixty_three_slices() {
505 let path = get_samples_dirpath().join("checksum").join("63slices.ot");
506 let data = read_type_from_bin_file::<SampleAttributes>(&path).unwrap();
507 assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
508 }
509 #[test]
510 fn sixty_four_slices_correct() {
511 let path = get_samples_dirpath().join("checksum").join("64slices.ot");
512 let data = read_type_from_bin_file::<SampleAttributes>(&path).unwrap();
513 assert_eq!(data.calculate_checksum().unwrap(), data.checksum);
514 }
515 }
516 }
517
518 mod bin_tempo_from_human {
519 use crate::samples::bin_tempo_from_human;
520
521 #[test]
522 fn err_oob_too_low() {
523 assert!(bin_tempo_from_human(&29.9).is_err());
524 }
525 #[test]
526 fn err_oob_too_high() {
527 assert!(bin_tempo_from_human(&300.1).is_err());
528 }
529
530 #[test]
531 fn ok_30() {
532 assert_eq!(bin_tempo_from_human(&30.0).unwrap(), 720);
533 }
534 #[test]
535 fn ok_300() {
536 assert_eq!(bin_tempo_from_human(&300.0).unwrap(), 7200);
537 }
538
539 #[test]
540 fn ok_120() {
541 assert_eq!(bin_tempo_from_human(&120.0).unwrap(), 2880);
542 }
543
544 #[test]
545 fn ok_100() {
546 assert_eq!(bin_tempo_from_human(&100.0).unwrap(), 2400);
547 }
548 }
549
550 mod bin_gain_from_human {
551 use crate::samples::bin_gain_from_human;
552
553 #[test]
554 fn err_oob_too_low() {
555 assert!(bin_gain_from_human(&-24.1).is_err());
556 }
557 #[test]
558 fn err_oob_too_high() {
559 assert!(bin_gain_from_human(&24.1).is_err());
560 }
561
562 #[test]
563 fn ok_negative_24() {
564 assert_eq!(bin_gain_from_human(&-24.0).unwrap(), 0);
565 }
566 #[test]
567 fn ok_positive_24() {
568 assert_eq!(bin_gain_from_human(&24.0).unwrap(), 96);
569 }
570
571 #[test]
572 fn ok_positive_23_half() {
573 assert_eq!(bin_gain_from_human(&23.5).unwrap(), 95);
574 }
575
576 #[test]
577 fn ok_negative_23_half() {
578 assert_eq!(bin_gain_from_human(&-23.5).unwrap(), 1);
579 }
580
581 #[test]
582 fn ok_zero() {
583 assert_eq!(bin_gain_from_human(&0.0).unwrap(), 48);
584 }
585
586 #[test]
587 fn ok_zero_half() {
588 assert_eq!(bin_gain_from_human(&0.5).unwrap(), 49);
589 }
590
591 #[test]
592 fn ok_zero_minus_half() {
593 assert_eq!(bin_gain_from_human(&-0.5).unwrap(), 47);
594 }
595 }
596
597 mod create_new {
598
599 use super::create_mock_configs_blank;
600 use crate::samples::options::{
601 SampleAttributeTimestrechMode, SampleAttributeTrigQuantizationMode,
602 };
603 use crate::samples::SampleAttributes;
604
605 #[test]
606 fn err_oob_tempo() {
607 let (trim_conf, loop_conf, slices) = create_mock_configs_blank();
608
609 let composed_chain = SampleAttributes::new(
610 &10000.0,
611 &SampleAttributeTimestrechMode::Off,
612 &SampleAttributeTrigQuantizationMode::PatternLength,
613 &0.0,
614 &trim_conf,
615 &loop_conf,
616 &slices,
617 );
618
619 assert!(composed_chain.is_err());
620 }
621
622 #[test]
623 fn err_invalid_gain() {
624 let (trim_conf, loop_conf, slices) = create_mock_configs_blank();
625
626 let composed_chain = SampleAttributes::new(
627 &125.0,
628 &SampleAttributeTimestrechMode::Off,
629 &SampleAttributeTrigQuantizationMode::PatternLength,
630 &300.0,
631 &trim_conf,
632 &loop_conf,
633 &slices,
634 );
635
636 assert!(composed_chain.is_err());
637 }
638 }
639
640 mod integrity {
641 use super::create_mock_configs_blank;
642 use crate::samples::options::{
643 SampleAttributeTimestrechMode, SampleAttributeTrigQuantizationMode,
644 };
645 use crate::samples::SampleAttributes;
646 use crate::CheckHeader;
647 #[test]
648 fn true_valid_header() {
649 let (trim_conf, loop_conf, slices) = create_mock_configs_blank();
650 let new = SampleAttributes::new(
651 &120.0,
652 &SampleAttributeTimestrechMode::Off,
653 &SampleAttributeTrigQuantizationMode::PatternLength,
654 &0.0,
655 &trim_conf,
656 &loop_conf,
657 &slices,
658 );
659 assert!(new.unwrap().check_header());
660 }
661
662 #[test]
663 fn false_invalid_header() {
664 let (trim_conf, loop_conf, slices) = create_mock_configs_blank();
665 let new = SampleAttributes::new(
666 &120.0,
667 &SampleAttributeTimestrechMode::Off,
668 &SampleAttributeTrigQuantizationMode::PatternLength,
669 &0.0,
670 &trim_conf,
671 &loop_conf,
672 &slices,
673 );
674 let mut mutated = new.unwrap();
675 mutated.header[0] = 0x00;
676 mutated.header[20] = 111;
677 assert!(!mutated.check_header());
678 }
679 }
680}