Skip to main content

ot_tools_io/patterns/tracks/
midi.rs

1/*
2SPDX-License-Identifier: GPL-3.0-or-later
3Copyright © 2024 Mike Robeson [dijksterhuis]
4*/
5
6use crate::generics::{Tracks, Trigs};
7use crate::parts::{
8    LfoParamsValues, MidiTrackArpParamsValues, MidiTrackCc1ParamsValues, MidiTrackCc2ParamsValues,
9    MidiTrackMidiParamsValues,
10};
11use crate::patterns::settings::{TrackPatternSettings, TrackPerTrackModeScale};
12use crate::patterns::tracks::TrigRepeatsConditionsAndOffsets;
13use crate::{Defaults, HasHeaderField, OtToolsIoError};
14use itertools::Itertools;
15use ot_tools_io_derive::{ArrayDefaults, AsMutDerive, AsRefDerive, BoxedArrayDefaults};
16use serde::{Deserialize, Serialize};
17use serde_big_array::BigArray;
18use std::array::from_fn;
19
20/// Header array for a MIDI track section in binary data files: `MTRA`
21const MIDI_TRACK_HEADER: [u8; 4] = [0x4d, 0x54, 0x52, 0x41];
22
23/// MIDI Track parameter locks.
24#[derive(
25    Copy,
26    Clone,
27    Debug,
28    Eq,
29    Hash,
30    Ord,
31    PartialEq,
32    PartialOrd,
33    Serialize,
34    Deserialize,
35    AsMutDerive,
36    AsRefDerive,
37    ArrayDefaults,
38    BoxedArrayDefaults,
39)]
40pub struct MidiTrackParameterLocks {
41    pub midi: MidiTrackMidiParamsValues,
42    pub lfo: LfoParamsValues,
43    pub arp: MidiTrackArpParamsValues,
44    pub ctrl1: MidiTrackCc1ParamsValues,
45    pub ctrl2: MidiTrackCc2ParamsValues,
46
47    #[serde(with = "BigArray")]
48    unknown: [u8; 2],
49}
50
51impl Default for MidiTrackParameterLocks {
52    fn default() -> Self {
53        // 255 -> disabled
54
55        // NOTE: the `part.rs` `default` methods for each of these type has
56        // fields all set to the correct defaults for the TRACK view, not p-lock
57        // trigS. So don't try and use the type's `default` method here as you
58        // will end up with a bunch of p-locks on trigs for all the default
59        // values. (Although maybe that's a desired feature for some workflows).
60
61        // Yes, this comment is duplicated above. It is to make sur you've seen
62        // it.
63
64        Self {
65            midi: MidiTrackMidiParamsValues {
66                note: 255,
67                vel: 255,
68                len: 255,
69                not2: 255,
70                not3: 255,
71                not4: 255,
72            },
73            lfo: LfoParamsValues {
74                spd1: 255,
75                spd2: 255,
76                spd3: 255,
77                dep1: 255,
78                dep2: 255,
79                dep3: 255,
80            },
81            arp: MidiTrackArpParamsValues {
82                tran: 255,
83                leg: 255,
84                mode: 255,
85                spd: 255,
86                rnge: 255,
87                nlen: 255,
88            },
89            ctrl1: MidiTrackCc1ParamsValues {
90                pb: 255,
91                at: 255,
92                cc1: 255,
93                cc2: 255,
94                cc3: 255,
95                cc4: 255,
96            },
97            ctrl2: MidiTrackCc2ParamsValues {
98                cc5: 255,
99                cc6: 255,
100                cc7: 255,
101                cc8: 255,
102                cc9: 255,
103                cc10: 255,
104            },
105            unknown: [255, 255],
106        }
107    }
108}
109
110/// MIDI Track Trig masks.
111/// Can be converted into an array of booleans using the `get_track_trigs_from_bitmasks` function.
112/// See `AudioTrackTrigMasks` for more information.
113///
114/// Trig mask arrays have data stored in this order, which is slightly confusing (pay attention to the difference with 7 + 8!):
115/// 1. 1st half of the 4th page
116/// 2. 2nd half of the 4th page
117/// 3. 1st half of the 3rd page
118/// 4. 2nd half of the 3rd page
119/// 5. 1st half of the 2nd page
120/// 6. 2nd half of the 2nd page
121/// 7. 2nd half of the 1st page
122/// 8. 1st half of the 1st page
123#[derive(
124    Copy,
125    Clone,
126    Debug,
127    Eq,
128    Hash,
129    Ord,
130    PartialEq,
131    PartialOrd,
132    Serialize,
133    Deserialize,
134    AsMutDerive,
135    AsRefDerive,
136)]
137pub struct MidiTrackTrigMasks {
138    /// Note Trig masks.
139    pub trigger: Tracks<u8>,
140
141    /// Trigless Trig masks.
142    pub trigless: Tracks<u8>,
143
144    /// Parameter Lock Trig masks.
145    /// Note this only stores data for exclusive parameter lock *trigs* (light green trigs).
146    pub plock: Tracks<u8>,
147
148    /// Swing trigs mask.
149    pub swing: Tracks<u8>,
150
151    /// this is a block of 8, so looks like a trig mask for tracks,
152    /// but I can't think of what it could be.
153    pub unknown: Tracks<u8>,
154}
155
156impl Default for MidiTrackTrigMasks {
157    fn default() -> Self {
158        Self {
159            trigger: Tracks::new(from_fn(|_| 0)),
160            trigless: Tracks::new(from_fn(|_| 0)),
161            plock: Tracks::new(from_fn(|_| 0)),
162            swing: Tracks::new(from_fn(|_| 170)),
163            unknown: Tracks::new(from_fn(|_| 0)),
164        }
165    }
166}
167
168/// Track trigs assigned on an Audio Track within a Pattern
169///
170/// No `Copy` trait as the `plocks` has a `Box<T>` type
171#[derive(
172    Clone,
173    Debug,
174    Eq,
175    Hash,
176    Ord,
177    PartialEq,
178    PartialOrd,
179    Serialize,
180    Deserialize,
181    AsMutDerive,
182    AsRefDerive,
183    BoxedArrayDefaults,
184)]
185pub struct MidiTrackTrigs {
186    /// Header data section
187    ///
188    /// example data:
189    /// ```text
190    /// MTRA
191    /// 4d 54 52 41
192    /// ```
193    #[serde(with = "BigArray")]
194    pub header: [u8; 4],
195
196    /// Unknown data.
197    #[serde(with = "BigArray")]
198    pub unknown_1: [u8; 4],
199
200    /// The zero indexed track number
201    pub track_id: u8,
202
203    /// MIDI Track Trig masks contain the Trig step locations for different trig types
204    pub trig_masks: MidiTrackTrigMasks,
205
206    /// The scale of this MIDI Track in Per Track Pattern mode.
207    pub scale_per_track_mode: TrackPerTrackModeScale,
208
209    /// Amount of swing when a Swing Trig is active for the Track.
210    /// Maximum is `30` (`80` on device), minimum is `0` (`50` on device).
211    pub swing_amount: u8,
212
213    /// Pattern settings for this MIDI Track
214    pub pattern_settings: TrackPatternSettings,
215
216    /// trig properties -- p-locks etc.
217    /// the big `0xff` value block within tracks basically.
218    /// 32 bytes per trig -- 6x parameters for 5x pages plus 2x extra fields at the end.
219    ///
220    /// For audio tracks, the 2x extra fields at the end are for sample locks,
221    /// but there's no such concept for MIDI tracks.
222    /// It seems like Elektron devs reused their data structures for P-Locks on both Audio + MIDI tracks.
223    // note -- stack overflow if trying to use #[serde(with = "BigArray")]
224    pub plocks: Trigs<MidiTrackParameterLocks>,
225
226    /// See the documentation for `AudioTrackTrigs` on how this field works.
227    pub trig_offsets_repeats_conditions: Trigs<TrigRepeatsConditionsAndOffsets>,
228}
229
230impl MidiTrackTrigMasks {
231    const HALF_PAGE_TRIG_BITMASK_VALUES: [u8; 8] = [1, 2, 4, 8, 16, 32, 64, 128];
232
233    /// Given a half-page trig bit mask, get an array of 8x boolean values
234    /// indicating whether each trig in the half-page is active or not
235    // note, should be safe to unwrap the option -- any u8 value is valid here.
236    fn get_halfpage_trigs_from_bitmask_value(bitmask: &u8) -> [bool; 8] {
237        let arr: [bool; 8] = Self::HALF_PAGE_TRIG_BITMASK_VALUES
238            .iter()
239            .map(|x| (bitmask & x) > 0)
240            .collect_array()
241            .unwrap();
242        arr
243    }
244
245    /// Given a half-page trig bit mask, get an array of 8x boolean values
246    /// indicating where each trig in the half-page is active or not
247    // note, should be safe to unwrap the option (see above).
248    pub fn get_track_trigs_from_bitmasks(bitmasks: &[u8; 8]) -> Trigs<bool> {
249        let x = bitmasks
250            .iter()
251            .flat_map(Self::get_halfpage_trigs_from_bitmask_value)
252            .collect_array()
253            .unwrap();
254
255        Trigs::new(x)
256    }
257
258    /// returns a [`Trigs`] type of `bool` values indicating whether there's a trigger trig
259    /// for a specific trig position
260    pub fn trigger(&self) -> Trigs<bool> {
261        Self::get_track_trigs_from_bitmasks(&self.trigger)
262    }
263
264    /// returns a [`Trigs`] type of `bool` values indicating whether there's a trigless trig
265    /// for a specific trig position
266    pub fn trigless(&self) -> Trigs<bool> {
267        Self::get_track_trigs_from_bitmasks(&self.trigless)
268    }
269
270    /// returns a [`Trigs`] type of `bool` values indicating whether there's a plock trig
271    /// for a specific trig position
272    pub fn plock(&self) -> Trigs<bool> {
273        Self::get_track_trigs_from_bitmasks(&self.plock)
274    }
275
276    /// returns a [`Trigs`] type of `bool` values indicating whether there's a swing trig
277    /// for a specific trig position
278    pub fn swing(&self) -> Trigs<bool> {
279        Self::get_track_trigs_from_bitmasks(&self.swing)
280    }
281}
282
283impl Default for MidiTrackTrigs {
284    fn default() -> Self {
285        Self {
286            header: MIDI_TRACK_HEADER,
287            unknown_1: from_fn(|_| 0),
288            track_id: 0,
289            trig_masks: MidiTrackTrigMasks::default(),
290            scale_per_track_mode: TrackPerTrackModeScale::default(),
291            swing_amount: 0,
292            pattern_settings: TrackPatternSettings::default(),
293            plocks: Trigs::default(),
294            trig_offsets_repeats_conditions: Trigs::<TrigRepeatsConditionsAndOffsets>::default(),
295        }
296    }
297}
298
299// needs to be manually implemented
300impl<const N: usize> Defaults<[Self; N]> for MidiTrackTrigs {
301    fn defaults() -> [Self; N]
302    where
303        Self: Default,
304    {
305        from_fn(|i| Self {
306            track_id: i as u8,
307            ..Default::default()
308        })
309    }
310}
311
312#[cfg(test)]
313mod midi_track_trigs_defaults {
314
315    use super::MidiTrackTrigs;
316    use crate::Defaults;
317
318    fn defs() -> [MidiTrackTrigs; 8] {
319        MidiTrackTrigs::defaults()
320    }
321
322    #[test]
323    fn ok_track_ids() -> Result<(), ()> {
324        for i in 0..8 {
325            println!("Track: {} Track ID: {i}", i + 1);
326            assert_eq!(defs()[i].track_id, i as u8);
327        }
328        Ok(())
329    }
330}
331
332impl HasHeaderField for MidiTrackTrigs {
333    fn check_header(&self) -> Result<bool, OtToolsIoError> {
334        Ok(self.header == MIDI_TRACK_HEADER)
335    }
336}
337
338#[cfg(test)]
339mod midi_track_trigs_header {
340    use super::MidiTrackTrigs;
341    use crate::{
342        test_utils::get_blank_proj_dirpath, BankFile, HasHeaderField, OctatrackFileIO,
343        OtToolsIoError,
344    };
345    #[test]
346    fn file_read_valid() -> Result<(), OtToolsIoError> {
347        let path = get_blank_proj_dirpath().join("bank01.work");
348        let x = BankFile::from_data_file(&path)?.patterns[0]
349            .clone()
350            .midi_track_trigs;
351        assert!(x[0].check_header()?);
352        Ok(())
353    }
354
355    #[test]
356    fn file_read_invalid() -> Result<(), OtToolsIoError> {
357        let path = get_blank_proj_dirpath().join("bank01.work");
358        let x = BankFile::from_data_file(&path)?.patterns[0]
359            .clone()
360            .midi_track_trigs;
361        let mut trigs = x[0].clone();
362        trigs.header[0] = 254;
363        trigs.header[1] = 254;
364        trigs.header[2] = 254;
365        trigs.header[3] = 254;
366        assert!(!trigs.check_header()?);
367        Ok(())
368    }
369
370    #[test]
371    fn default_valid() -> Result<(), OtToolsIoError> {
372        let trigs = MidiTrackTrigs::default();
373        assert!(trigs.check_header()?);
374        Ok(())
375    }
376
377    #[test]
378    fn default_invalid() -> Result<(), OtToolsIoError> {
379        let mut trigs = MidiTrackTrigs::default();
380        trigs.header[0] = 0x01;
381        trigs.header[1] = 0x01;
382        trigs.header[2] = 0x50;
383        assert!(!trigs.check_header()?);
384        Ok(())
385    }
386}