ot-tools-io 0.10.1

A library crate for reading/writing binary data files used by the Elektron Octatrack DPS-1.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
/*
SPDX-License-Identifier: GPL-3.0-or-later
Copyright © 2024 Mike Robeson [dijksterhuis]
*/

use crate::generics::{Tracks, Trigs};
use crate::identifiers::TrackId;
use crate::parts::{AudioTrackAmpParamsValues, AudioTrackFxParamsValues, LfoParamsValues};
use crate::patterns::settings::{TrackPatternSettings, TrackPerTrackModeScale};
use crate::patterns::tracks::TrigRepeatsConditionsAndOffsets;
use crate::settings::TrigCondition;
use crate::{Defaults, HasHeaderField, OtToolsIoError};
use itertools::Itertools;
use ot_tools_io_derive::{
    ArrayDefaults, AsMutDerive, AsRefDerive, BoxedArrayDefaults, IsDefaultCheck,
};
use serde::{Deserialize, Serialize};
use serde_big_array::BigArray;
use std::array::from_fn;

/// Header array for a MIDI track section in binary data files: `TRAC`
const AUDIO_TRACK_HEADER: [u8; 4] = [0x54, 0x52, 0x41, 0x43];

/// A Trig's parameter locks on the Playback/Machine page for an Audio Track.
#[derive(
    Copy,
    Clone,
    Debug,
    Eq,
    Hash,
    Ord,
    PartialEq,
    PartialOrd,
    Serialize,
    Deserialize,
    AsMutDerive,
    AsRefDerive,
)]
pub struct AudioTrackParameterLockPlayback {
    pub param1: u8,
    pub param2: u8,
    pub param3: u8,
    pub param4: u8,
    pub param5: u8,
    pub param6: u8,
}

impl Default for AudioTrackParameterLockPlayback {
    fn default() -> Self {
        Self {
            param1: 255,
            param2: 255,
            param3: 255,
            param4: 255,
            param5: 255,
            param6: 255,
        }
    }
}

/// A single trig's parameter locks on an Audio Track.
#[derive(
    Copy,
    Clone,
    Debug,
    Eq,
    Hash,
    Ord,
    PartialEq,
    PartialOrd,
    Serialize,
    Deserialize,
    AsMutDerive,
    AsRefDerive,
    ArrayDefaults,
    BoxedArrayDefaults,
    IsDefaultCheck,
)]
pub struct AudioTrackParameterLocks {
    pub machine: AudioTrackParameterLockPlayback,
    pub lfo: LfoParamsValues,
    pub amp: AudioTrackAmpParamsValues,
    pub fx1: AudioTrackFxParamsValues,
    pub fx2: AudioTrackFxParamsValues,
    /// P-Lock to change an audio track's flex machine sample slot assignment per trig
    pub flex_slot_id: u8,
    /// P-Lock to change an audio track's static machine sample slot assignment per trig
    pub static_slot_id: u8,
}

impl Default for AudioTrackParameterLocks {
    fn default() -> Self {
        // 255 -> disabled

        // NOTE: the `part.rs` `default` methods for each of these type has
        // fields all set to the correct defaults for the TRACK view, not p-lock
        // trigS. So don't try and use the type's `default` method here as you
        // will end up with a bunch of p-locks on trigs for all the default
        // values. (Although maybe that's a desired feature for some workflows).

        // Yes, this comment is duplicated below. It is to make sur you've seen
        // it.
        Self {
            machine: AudioTrackParameterLockPlayback {
                param1: 255,
                param2: 255,
                param3: 255,
                param4: 255,
                param5: 255,
                param6: 255,
            },
            lfo: LfoParamsValues {
                spd1: 255,
                spd2: 255,
                spd3: 255,
                dep1: 255,
                dep2: 255,
                dep3: 255,
            },
            amp: AudioTrackAmpParamsValues {
                atk: 255,
                hold: 255,
                rel: 255,
                vol: 255,
                bal: 255,
                f: 255,
            },
            fx1: AudioTrackFxParamsValues {
                param_1: 255,
                param_2: 255,
                param_3: 255,
                param_4: 255,
                param_5: 255,
                param_6: 255,
            },
            fx2: AudioTrackFxParamsValues {
                param_1: 255,
                param_2: 255,
                param_3: 255,
                param_4: 255,
                param_5: 255,
                param_6: 255,
            },
            static_slot_id: 255,
            flex_slot_id: 255,
        }
    }
}

/// Trig bitmasks array for Audio Tracks.
/// Can be converted into an array of booleans using the `get_track_trigs_from_bitmasks` function.
///
/// Trig bitmask arrays have bitmasks stored in this order, which is slightly confusing (pay attention to the difference with 7 + 8!):
/// 1. 1st half of the 4th page
/// 2. 2nd half of the 4th page
/// 3. 1st half of the 3rd page
/// 4. 2nd half of the 3rd page
/// 5. 1st half of the 2nd page
/// 6. 2nd half of the 2nd page
/// 7. 2nd half of the 1st page
/// 8. 1st half of the 1st page
///
/// ### Bitmask values for trig positions
/// With single trigs in a half-page
/// ```text
/// positions
/// 1 2 3 4 5 6 7 8 | mask value
/// ----------------|-----------
/// - - - - - - - - | 0
/// x - - - - - - - | 1
/// - x - - - - - - | 2
/// - - x - - - - - | 4
/// - - - x - - - - | 8
/// - - - - x - - - | 16
/// - - - - - x - - | 32
/// - - - - - - x - | 64
/// - - - - - - - x | 128
/// ```
///
/// When there are multiple trigs in a half-page, the individual position values are summed together:
///
/// ```text
/// 1 2 3 4 5 6 7 8 | mask value
/// ----------------|-----------
/// x x - - - - - - | 1 + 2 = 3
/// x x x x - - - - | 1 + 2 + 4 + 8 = 15
/// ```
/// ### Fuller diagram of mask values
///
/// ```text
/// positions
/// 1 2 3 4 5 6 7 8 | mask value
/// ----------------|-----------
/// x - - - - - - - | 1
/// - x - - - - - - | 2
/// x x - - - - - - | 3
/// - - x - - - - - | 4
/// x - x - - - - - | 5
/// - x x - - - - - | 6
/// x x x - - - - - | 7
/// - - - x - - - - | 8
/// x - - x - - - - | 9
/// - x - x - - - - | 10
/// x x - x - - - - | 11
/// - - x x - - - - | 12
/// x - x x - - - - | 13
/// - x x x - - - - | 14
/// x x x x - - - - | 15
/// ................|....
/// x x x x x x - - | 63
/// ................|....
/// - - - - - - - x | 128
/// ................|....
/// - x - x - x - x | 170
/// ................|....
/// - - - - x x x x | 240
/// ................|....
/// x x x x x x x x | 255
/// ```
///
#[derive(
    Copy,
    Clone,
    Debug,
    Eq,
    Hash,
    Ord,
    PartialEq,
    PartialOrd,
    Serialize,
    Deserialize,
    AsMutDerive,
    AsRefDerive,
)]
pub struct AudioTrackTrigMasks {
    /// Trigger Trig masks -- indicate which Trigger Trigs are active.
    /// Base track Trig masks are stored backwards, meaning
    /// the first 8 Trig positions are the last bytes in this section.
    pub trigger: Tracks<u8>,

    /// Envelope Trig masks -- indicate which Envelope Trigs are active.
    /// See the description of the `trig_trig_masks` field for an
    /// explanation of how the masking works.
    pub trigless: Tracks<u8>,

    /// Parameter-Lock Trig masks -- indicate which Parameter-Lock Trigs are active.
    /// See the description of the `trig_trig_masks` field for an
    /// explanation of how the masking works.
    pub plock: Tracks<u8>,

    /// Hold Trig masks -- indicate which Hold Trigs are active.
    /// See the description of the `trig_trig_masks` field for an
    /// explanation of how the masking works.
    pub oneshot: Tracks<u8>,

    /// Recorder Trig masks -- indicate which Recorder Trigs are active.
    /// These seem to function differently to the main Track Trig masks.
    /// Filling up Recorder Trigs on a Pattern results in a 32 length array
    /// instead of 8 length.
    /// Possible that the Trig type is stored in this array as well.
    #[serde(with = "BigArray")]
    pub recorder: [u8; 32],

    /// Swing trigs Trig masks.
    pub swing: Tracks<u8>,

    /// Parameter Slide trigs Trig masks.
    pub slide: Tracks<u8>,
}

impl Default for AudioTrackTrigMasks {
    fn default() -> Self {
        Self {
            trigger: Tracks::new(from_fn(|_| 0)),
            trigless: Tracks::new(from_fn(|_| 0)),
            plock: Tracks::new(from_fn(|_| 0)),
            oneshot: Tracks::new(from_fn(|_| 0)),
            recorder: from_fn(|_| 0),
            swing: Tracks::new(from_fn(|_| 170)),
            slide: Tracks::new(from_fn(|_| 0)),
        }
    }
}
/// Track trigs assigned on an Audio Track within a Pattern
///
/// No `Copy` trait on this type as the `plocks` field is the [`AudioTrackParameterLocks`] type,
/// which does not implement `Copy`
#[derive(
    Clone,
    Debug,
    Eq,
    Hash,
    Ord,
    PartialEq,
    PartialOrd,
    Serialize,
    Deserialize,
    AsMutDerive,
    AsRefDerive,
    BoxedArrayDefaults,
)]
pub struct AudioTrackTrigs {
    /// Header data section
    ///
    /// example data:
    /// ```text
    /// TRAC
    /// 54 52 41 43
    /// ```
    #[serde(with = "BigArray")]
    pub header: [u8; 4],

    /// Unknown data.
    #[serde(with = "BigArray")]
    pub unknown_1: [u8; 4],

    /// The zero indexed track number
    pub track_id: u8,

    /// Trig masks contain the Trig step locations for different trig types
    pub trig_masks: AudioTrackTrigMasks,

    /// The scale of this Audio Track in Per Track Pattern mode.
    pub scale_per_track_mode: TrackPerTrackModeScale,

    /// Amount of swing when a Swing Trig is active for the Track.
    /// Maximum is `30` (`80` on device), minimum is `0` (`50` on device).
    pub swing_amount: u8,

    /// Pattern settings for this Audio Track
    pub pattern_settings: TrackPatternSettings,

    /// Unknown data.
    pub unknown_2: u8,

    /// Parameter-Lock data for all Trigs.
    // note -- stack overflow if tring to use #[serde(with = "BigArray")]
    pub plocks: Trigs<AudioTrackParameterLocks>,

    /// What the hell is this field?!?!
    /// It **has to** be something to do with trigs, but i have no idea what it could be.
    pub unknown_3: Trigs<u8>,

    /// Trig Offsets, Trig Counts and Trig Conditions.
    /// See the documentation for [`TrigRepeatsConditionsAndOffsets`] for a detailed explainer on
    /// how this field works before attempting to use it!
    pub trig_offsets_repeats_conditions: Trigs<TrigRepeatsConditionsAndOffsets>,
}

// todo: duplicated for MidiTrackTrigs -- make this generic?!
impl AudioTrackTrigs {
    pub fn track_id(&self) -> Option<TrackId> {
        TrackId::try_from(self.track_id).ok()
    }

    // shorthand accessor method because the `trig_masks` name could be confusing
    pub fn trigs(self) -> AudioTrackTrigMasks {
        self.trig_masks
    }

    // shorthand accessor method because the `trig_masks` name could be confusing
    pub fn trigs_ref(&self) -> &AudioTrackTrigMasks {
        &self.trig_masks
    }

    // shorthand accessor method because the `trig_masks` name could be confusing
    pub fn trigs_mut(&mut self) -> &mut AudioTrackTrigMasks {
        &mut self.trig_masks
    }

    pub fn swing_amount(&self) -> u8 {
        self.swing_amount + 50
    }

    #[allow(dead_code)]
    fn trig_offsets(&self) -> [u8; 64] {
        todo!()
    }

    #[allow(dead_code)]
    fn trig_counts(&self) -> [u8; 64] {
        todo!()
    }

    // todo: unwraps!
    pub fn trig_conditions(&self) -> Trigs<TrigCondition> {
        let trigs = self
            .trig_offsets_repeats_conditions
            .iter()
            .map(|x| x.condition)
            // note: `rem_euclid` is applied during `try_from` method call to handle wrap around for
            // the interleaved offsets data
            .map(|x| TrigCondition::try_from(x).unwrap())
            .collect_array()
            .unwrap();

        Trigs::new(trigs)
    }
}

impl Default for AudioTrackTrigs {
    fn default() -> Self {
        Self {
            header: AUDIO_TRACK_HEADER,
            unknown_1: from_fn(|_| 0),
            track_id: 0,
            trig_masks: AudioTrackTrigMasks::default(),
            scale_per_track_mode: TrackPerTrackModeScale::default(),
            swing_amount: 0,
            pattern_settings: TrackPatternSettings::default(),
            unknown_2: 0,
            plocks: Trigs::<AudioTrackParameterLocks>::default(),
            unknown_3: Trigs::<u8>::new(from_fn(|_| 0)),
            trig_offsets_repeats_conditions: Trigs::<TrigRepeatsConditionsAndOffsets>::default(),
        }
    }
}

// need to implement manually to handle track_id field
impl<const N: usize> Defaults<[Self; N]> for AudioTrackTrigs {
    fn defaults() -> [Self; N]
    where
        Self: Default,
    {
        from_fn(|i| Self {
            track_id: i as u8,
            ..Default::default()
        })
    }
}

#[cfg(test)]
mod audio_track_trigs_defaults {
    use super::AudioTrackTrigs;
    use crate::Defaults;

    fn defs() -> [AudioTrackTrigs; 8] {
        AudioTrackTrigs::defaults()
    }

    #[test]
    fn ok_track_ids() -> Result<(), ()> {
        for i in 0..8 {
            println!("Track: {} Track ID: {i}", i + 1);
            assert_eq!(defs()[i].track_id, i as u8);
        }
        Ok(())
    }
}

impl HasHeaderField for AudioTrackTrigs {
    fn check_header(&self) -> Result<bool, OtToolsIoError> {
        Ok(self.header == AUDIO_TRACK_HEADER)
    }
}

#[cfg(test)]
mod audio_track_trigs_header {
    use super::AudioTrackTrigs;
    use crate::{
        test_utils::get_blank_proj_dirpath, BankFile, HasHeaderField, OctatrackFileIO,
        OtToolsIoError,
    };
    #[test]
    fn file_read_valid() -> Result<(), OtToolsIoError> {
        let path = get_blank_proj_dirpath().join("bank01.work");
        let x = BankFile::from_data_file(&path)?.patterns[0]
            .clone()
            .audio_track_trigs;
        assert!(x[0].check_header()?);
        Ok(())
    }

    #[test]
    fn file_read_invalid() -> Result<(), OtToolsIoError> {
        let path = get_blank_proj_dirpath().join("bank01.work");
        let x = BankFile::from_data_file(&path)?.patterns[0]
            .clone()
            .audio_track_trigs;
        let mut trigs = x[0].clone();
        trigs.header[0] = 254;
        trigs.header[1] = 254;
        trigs.header[2] = 254;
        trigs.header[3] = 254;
        assert!(!trigs.check_header()?);
        Ok(())
    }

    #[test]
    fn default_valid() -> Result<(), OtToolsIoError> {
        let trigs = AudioTrackTrigs::default();
        assert!(trigs.check_header()?);
        Ok(())
    }

    #[test]
    fn default_invalid() -> Result<(), OtToolsIoError> {
        let mut trigs = AudioTrackTrigs::default();
        trigs.header[0] = 0x01;
        trigs.header[1] = 0x01;
        trigs.header[2] = 0x50;
        assert!(!trigs.check_header()?);
        Ok(())
    }
}