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}