Skip to main content

ot_tools_io/patterns/tracks/
audio.rs

1/*
2SPDX-License-Identifier: GPL-3.0-or-later
3Copyright © 2024 Mike Robeson [dijksterhuis]
4*/
5
6use crate::generics::{Tracks, Trigs};
7use crate::identifiers::TrackId;
8use crate::parts::{AudioTrackAmpParamsValues, AudioTrackFxParamsValues, LfoParamsValues};
9use crate::patterns::settings::{TrackPatternSettings, TrackPerTrackModeScale};
10use crate::patterns::tracks::TrigRepeatsConditionsAndOffsets;
11use crate::settings::TrigCondition;
12use crate::{Defaults, HasHeaderField, OtToolsIoError};
13use itertools::Itertools;
14use ot_tools_io_derive::{
15    ArrayDefaults, AsMutDerive, AsRefDerive, BoxedArrayDefaults, IsDefaultCheck,
16};
17use serde::{Deserialize, Serialize};
18use serde_big_array::BigArray;
19use std::array::from_fn;
20
21/// Header array for a MIDI track section in binary data files: `TRAC`
22const AUDIO_TRACK_HEADER: [u8; 4] = [0x54, 0x52, 0x41, 0x43];
23
24/// A Trig's parameter locks on the Playback/Machine page for an Audio Track.
25#[derive(
26    Copy,
27    Clone,
28    Debug,
29    Eq,
30    Hash,
31    Ord,
32    PartialEq,
33    PartialOrd,
34    Serialize,
35    Deserialize,
36    AsMutDerive,
37    AsRefDerive,
38)]
39pub struct AudioTrackParameterLockPlayback {
40    pub param1: u8,
41    pub param2: u8,
42    pub param3: u8,
43    pub param4: u8,
44    pub param5: u8,
45    pub param6: u8,
46}
47
48impl Default for AudioTrackParameterLockPlayback {
49    fn default() -> Self {
50        Self {
51            param1: 255,
52            param2: 255,
53            param3: 255,
54            param4: 255,
55            param5: 255,
56            param6: 255,
57        }
58    }
59}
60
61/// A single trig's parameter locks on an Audio Track.
62#[derive(
63    Copy,
64    Clone,
65    Debug,
66    Eq,
67    Hash,
68    Ord,
69    PartialEq,
70    PartialOrd,
71    Serialize,
72    Deserialize,
73    AsMutDerive,
74    AsRefDerive,
75    ArrayDefaults,
76    BoxedArrayDefaults,
77    IsDefaultCheck,
78)]
79pub struct AudioTrackParameterLocks {
80    pub machine: AudioTrackParameterLockPlayback,
81    pub lfo: LfoParamsValues,
82    pub amp: AudioTrackAmpParamsValues,
83    pub fx1: AudioTrackFxParamsValues,
84    pub fx2: AudioTrackFxParamsValues,
85    /// P-Lock to change an audio track's flex machine sample slot assignment per trig
86    pub flex_slot_id: u8,
87    /// P-Lock to change an audio track's static machine sample slot assignment per trig
88    pub static_slot_id: u8,
89}
90
91impl Default for AudioTrackParameterLocks {
92    fn default() -> Self {
93        // 255 -> disabled
94
95        // NOTE: the `part.rs` `default` methods for each of these type has
96        // fields all set to the correct defaults for the TRACK view, not p-lock
97        // trigS. So don't try and use the type's `default` method here as you
98        // will end up with a bunch of p-locks on trigs for all the default
99        // values. (Although maybe that's a desired feature for some workflows).
100
101        // Yes, this comment is duplicated below. It is to make sur you've seen
102        // it.
103        Self {
104            machine: AudioTrackParameterLockPlayback {
105                param1: 255,
106                param2: 255,
107                param3: 255,
108                param4: 255,
109                param5: 255,
110                param6: 255,
111            },
112            lfo: LfoParamsValues {
113                spd1: 255,
114                spd2: 255,
115                spd3: 255,
116                dep1: 255,
117                dep2: 255,
118                dep3: 255,
119            },
120            amp: AudioTrackAmpParamsValues {
121                atk: 255,
122                hold: 255,
123                rel: 255,
124                vol: 255,
125                bal: 255,
126                f: 255,
127            },
128            fx1: AudioTrackFxParamsValues {
129                param_1: 255,
130                param_2: 255,
131                param_3: 255,
132                param_4: 255,
133                param_5: 255,
134                param_6: 255,
135            },
136            fx2: AudioTrackFxParamsValues {
137                param_1: 255,
138                param_2: 255,
139                param_3: 255,
140                param_4: 255,
141                param_5: 255,
142                param_6: 255,
143            },
144            static_slot_id: 255,
145            flex_slot_id: 255,
146        }
147    }
148}
149
150/// Trig bitmasks array for Audio Tracks.
151/// Can be converted into an array of booleans using the `get_track_trigs_from_bitmasks` function.
152///
153/// Trig bitmask arrays have bitmasks stored in this order, which is slightly confusing (pay attention to the difference with 7 + 8!):
154/// 1. 1st half of the 4th page
155/// 2. 2nd half of the 4th page
156/// 3. 1st half of the 3rd page
157/// 4. 2nd half of the 3rd page
158/// 5. 1st half of the 2nd page
159/// 6. 2nd half of the 2nd page
160/// 7. 2nd half of the 1st page
161/// 8. 1st half of the 1st page
162///
163/// ### Bitmask values for trig positions
164/// With single trigs in a half-page
165/// ```text
166/// positions
167/// 1 2 3 4 5 6 7 8 | mask value
168/// ----------------|-----------
169/// - - - - - - - - | 0
170/// x - - - - - - - | 1
171/// - x - - - - - - | 2
172/// - - x - - - - - | 4
173/// - - - x - - - - | 8
174/// - - - - x - - - | 16
175/// - - - - - x - - | 32
176/// - - - - - - x - | 64
177/// - - - - - - - x | 128
178/// ```
179///
180/// When there are multiple trigs in a half-page, the individual position values are summed together:
181///
182/// ```text
183/// 1 2 3 4 5 6 7 8 | mask value
184/// ----------------|-----------
185/// x x - - - - - - | 1 + 2 = 3
186/// x x x x - - - - | 1 + 2 + 4 + 8 = 15
187/// ```
188/// ### Fuller diagram of mask values
189///
190/// ```text
191/// positions
192/// 1 2 3 4 5 6 7 8 | mask value
193/// ----------------|-----------
194/// x - - - - - - - | 1
195/// - x - - - - - - | 2
196/// x x - - - - - - | 3
197/// - - x - - - - - | 4
198/// x - x - - - - - | 5
199/// - x x - - - - - | 6
200/// x x x - - - - - | 7
201/// - - - x - - - - | 8
202/// x - - x - - - - | 9
203/// - x - x - - - - | 10
204/// x x - x - - - - | 11
205/// - - x x - - - - | 12
206/// x - x x - - - - | 13
207/// - x x x - - - - | 14
208/// x x x x - - - - | 15
209/// ................|....
210/// x x x x x x - - | 63
211/// ................|....
212/// - - - - - - - x | 128
213/// ................|....
214/// - x - x - x - x | 170
215/// ................|....
216/// - - - - x x x x | 240
217/// ................|....
218/// x x x x x x x x | 255
219/// ```
220///
221#[derive(
222    Copy,
223    Clone,
224    Debug,
225    Eq,
226    Hash,
227    Ord,
228    PartialEq,
229    PartialOrd,
230    Serialize,
231    Deserialize,
232    AsMutDerive,
233    AsRefDerive,
234)]
235pub struct AudioTrackTrigMasks {
236    /// Trigger Trig masks -- indicate which Trigger Trigs are active.
237    /// Base track Trig masks are stored backwards, meaning
238    /// the first 8 Trig positions are the last bytes in this section.
239    pub trigger: Tracks<u8>,
240
241    /// Envelope Trig masks -- indicate which Envelope Trigs are active.
242    /// See the description of the `trig_trig_masks` field for an
243    /// explanation of how the masking works.
244    pub trigless: Tracks<u8>,
245
246    /// Parameter-Lock Trig masks -- indicate which Parameter-Lock Trigs are active.
247    /// See the description of the `trig_trig_masks` field for an
248    /// explanation of how the masking works.
249    pub plock: Tracks<u8>,
250
251    /// Hold Trig masks -- indicate which Hold Trigs are active.
252    /// See the description of the `trig_trig_masks` field for an
253    /// explanation of how the masking works.
254    pub oneshot: Tracks<u8>,
255
256    /// Recorder Trig masks -- indicate which Recorder Trigs are active.
257    /// These seem to function differently to the main Track Trig masks.
258    /// Filling up Recorder Trigs on a Pattern results in a 32 length array
259    /// instead of 8 length.
260    /// Possible that the Trig type is stored in this array as well.
261    #[serde(with = "BigArray")]
262    pub recorder: [u8; 32],
263
264    /// Swing trigs Trig masks.
265    pub swing: Tracks<u8>,
266
267    /// Parameter Slide trigs Trig masks.
268    pub slide: Tracks<u8>,
269}
270
271impl Default for AudioTrackTrigMasks {
272    fn default() -> Self {
273        Self {
274            trigger: Tracks::new(from_fn(|_| 0)),
275            trigless: Tracks::new(from_fn(|_| 0)),
276            plock: Tracks::new(from_fn(|_| 0)),
277            oneshot: Tracks::new(from_fn(|_| 0)),
278            recorder: from_fn(|_| 0),
279            swing: Tracks::new(from_fn(|_| 170)),
280            slide: Tracks::new(from_fn(|_| 0)),
281        }
282    }
283}
284/// Track trigs assigned on an Audio Track within a Pattern
285///
286/// No `Copy` trait on this type as the `plocks` field is the [`AudioTrackParameterLocks`] type,
287/// which does not implement `Copy`
288#[derive(
289    Clone,
290    Debug,
291    Eq,
292    Hash,
293    Ord,
294    PartialEq,
295    PartialOrd,
296    Serialize,
297    Deserialize,
298    AsMutDerive,
299    AsRefDerive,
300    BoxedArrayDefaults,
301)]
302pub struct AudioTrackTrigs {
303    /// Header data section
304    ///
305    /// example data:
306    /// ```text
307    /// TRAC
308    /// 54 52 41 43
309    /// ```
310    #[serde(with = "BigArray")]
311    pub header: [u8; 4],
312
313    /// Unknown data.
314    #[serde(with = "BigArray")]
315    pub unknown_1: [u8; 4],
316
317    /// The zero indexed track number
318    pub track_id: u8,
319
320    /// Trig masks contain the Trig step locations for different trig types
321    pub trig_masks: AudioTrackTrigMasks,
322
323    /// The scale of this Audio Track in Per Track Pattern mode.
324    pub scale_per_track_mode: TrackPerTrackModeScale,
325
326    /// Amount of swing when a Swing Trig is active for the Track.
327    /// Maximum is `30` (`80` on device), minimum is `0` (`50` on device).
328    pub swing_amount: u8,
329
330    /// Pattern settings for this Audio Track
331    pub pattern_settings: TrackPatternSettings,
332
333    /// Unknown data.
334    pub unknown_2: u8,
335
336    /// Parameter-Lock data for all Trigs.
337    // note -- stack overflow if tring to use #[serde(with = "BigArray")]
338    pub plocks: Trigs<AudioTrackParameterLocks>,
339
340    /// What the hell is this field?!?!
341    /// It **has to** be something to do with trigs, but i have no idea what it could be.
342    pub unknown_3: Trigs<u8>,
343
344    /// Trig Offsets, Trig Counts and Trig Conditions.
345    /// See the documentation for [`TrigRepeatsConditionsAndOffsets`] for a detailed explainer on
346    /// how this field works before attempting to use it!
347    pub trig_offsets_repeats_conditions: Trigs<TrigRepeatsConditionsAndOffsets>,
348}
349
350// todo: duplicated for MidiTrackTrigs -- make this generic?!
351impl AudioTrackTrigs {
352    pub fn track_id(&self) -> Option<TrackId> {
353        TrackId::try_from(self.track_id).ok()
354    }
355
356    // shorthand accessor method because the `trig_masks` name could be confusing
357    pub fn trigs(self) -> AudioTrackTrigMasks {
358        self.trig_masks
359    }
360
361    // shorthand accessor method because the `trig_masks` name could be confusing
362    pub fn trigs_ref(&self) -> &AudioTrackTrigMasks {
363        &self.trig_masks
364    }
365
366    // shorthand accessor method because the `trig_masks` name could be confusing
367    pub fn trigs_mut(&mut self) -> &mut AudioTrackTrigMasks {
368        &mut self.trig_masks
369    }
370
371    pub fn swing_amount(&self) -> u8 {
372        self.swing_amount + 50
373    }
374
375    #[allow(dead_code)]
376    fn trig_offsets(&self) -> [u8; 64] {
377        todo!()
378    }
379
380    #[allow(dead_code)]
381    fn trig_counts(&self) -> [u8; 64] {
382        todo!()
383    }
384
385    // todo: unwraps!
386    pub fn trig_conditions(&self) -> Trigs<TrigCondition> {
387        let trigs = self
388            .trig_offsets_repeats_conditions
389            .iter()
390            .map(|x| x.condition)
391            // note: `rem_euclid` is applied during `try_from` method call to handle wrap around for
392            // the interleaved offsets data
393            .map(|x| TrigCondition::try_from(x).unwrap())
394            .collect_array()
395            .unwrap();
396
397        Trigs::new(trigs)
398    }
399}
400
401impl Default for AudioTrackTrigs {
402    fn default() -> Self {
403        Self {
404            header: AUDIO_TRACK_HEADER,
405            unknown_1: from_fn(|_| 0),
406            track_id: 0,
407            trig_masks: AudioTrackTrigMasks::default(),
408            scale_per_track_mode: TrackPerTrackModeScale::default(),
409            swing_amount: 0,
410            pattern_settings: TrackPatternSettings::default(),
411            unknown_2: 0,
412            plocks: Trigs::<AudioTrackParameterLocks>::default(),
413            unknown_3: Trigs::<u8>::new(from_fn(|_| 0)),
414            trig_offsets_repeats_conditions: Trigs::<TrigRepeatsConditionsAndOffsets>::default(),
415        }
416    }
417}
418
419// need to implement manually to handle track_id field
420impl<const N: usize> Defaults<[Self; N]> for AudioTrackTrigs {
421    fn defaults() -> [Self; N]
422    where
423        Self: Default,
424    {
425        from_fn(|i| Self {
426            track_id: i as u8,
427            ..Default::default()
428        })
429    }
430}
431
432#[cfg(test)]
433mod audio_track_trigs_defaults {
434    use super::AudioTrackTrigs;
435    use crate::Defaults;
436
437    fn defs() -> [AudioTrackTrigs; 8] {
438        AudioTrackTrigs::defaults()
439    }
440
441    #[test]
442    fn ok_track_ids() -> Result<(), ()> {
443        for i in 0..8 {
444            println!("Track: {} Track ID: {i}", i + 1);
445            assert_eq!(defs()[i].track_id, i as u8);
446        }
447        Ok(())
448    }
449}
450
451impl HasHeaderField for AudioTrackTrigs {
452    fn check_header(&self) -> Result<bool, OtToolsIoError> {
453        Ok(self.header == AUDIO_TRACK_HEADER)
454    }
455}
456
457#[cfg(test)]
458mod audio_track_trigs_header {
459    use super::AudioTrackTrigs;
460    use crate::{
461        test_utils::get_blank_proj_dirpath, BankFile, HasHeaderField, OctatrackFileIO,
462        OtToolsIoError,
463    };
464    #[test]
465    fn file_read_valid() -> Result<(), OtToolsIoError> {
466        let path = get_blank_proj_dirpath().join("bank01.work");
467        let x = BankFile::from_data_file(&path)?.patterns[0]
468            .clone()
469            .audio_track_trigs;
470        assert!(x[0].check_header()?);
471        Ok(())
472    }
473
474    #[test]
475    fn file_read_invalid() -> Result<(), OtToolsIoError> {
476        let path = get_blank_proj_dirpath().join("bank01.work");
477        let x = BankFile::from_data_file(&path)?.patterns[0]
478            .clone()
479            .audio_track_trigs;
480        let mut trigs = x[0].clone();
481        trigs.header[0] = 254;
482        trigs.header[1] = 254;
483        trigs.header[2] = 254;
484        trigs.header[3] = 254;
485        assert!(!trigs.check_header()?);
486        Ok(())
487    }
488
489    #[test]
490    fn default_valid() -> Result<(), OtToolsIoError> {
491        let trigs = AudioTrackTrigs::default();
492        assert!(trigs.check_header()?);
493        Ok(())
494    }
495
496    #[test]
497    fn default_invalid() -> Result<(), OtToolsIoError> {
498        let mut trigs = AudioTrackTrigs::default();
499        trigs.header[0] = 0x01;
500        trigs.header[1] = 0x01;
501        trigs.header[2] = 0x50;
502        assert!(!trigs.check_header()?);
503        Ok(())
504    }
505}