Skip to main content

midi_msg/system_exclusive/
tuning.rs

1use crate::parse_error::*;
2use crate::util::*;
3use alloc::vec::Vec;
4
5/// Change the tunings of one or more notes, either real-time or not.
6/// Used by [`UniversalNonRealTimeMsg`](crate::UniversalNonRealTimeMsg) and [`UniversalRealTimeMsg`](crate::UniversalRealTimeMsg).
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct TuningNoteChange {
9    /// Which tuning program is targeted, 0-127. See [`Parameter::TuningProgramSelect`](crate::Parameter::TuningProgramSelect).
10    pub tuning_program_num: u8,
11    /// Which tuning bank is targeted, 0-127. See [`Parameter::TuningBankSelect`](crate::Parameter::TuningBankSelect).
12    pub tuning_bank_num: Option<u8>,
13    /// At most 127 `(MIDI note number, Option<Tuning>)`` pairs.
14    /// A `None` value represents "No change".
15    pub tunings: Vec<(u8, Option<Tuning>)>,
16}
17
18impl TuningNoteChange {
19    pub(crate) fn extend_midi(&self, v: &mut Vec<u8>) {
20        // The tuning_bank_num is pushed by the caller if needed
21        push_u7(self.tuning_program_num, v);
22        push_u7(self.tunings.len() as u8, v);
23        for (note, tuning) in self.tunings.iter() {
24            push_u7(*note, v);
25            if let Some(tuning) = tuning {
26                tuning.extend_midi(v);
27            } else {
28                // "No change"
29                v.push(0x7F);
30                v.push(0x7F);
31                v.push(0x7F);
32            }
33        }
34    }
35
36    #[allow(dead_code)]
37    pub(crate) fn from_midi(_m: &[u8]) -> Result<(Self, usize), ParseError> {
38        Err(ParseError::NotImplemented("TuningNoteChange"))
39    }
40}
41
42/// Set the tunings of all 128 notes.
43/// Used by [`UniversalNonRealTimeMsg`](crate::UniversalNonRealTimeMsg).
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct KeyBasedTuningDump {
46    /// Which tuning program is targeted, 0-127. See [`Parameter::TuningProgramSelect`](crate::Parameter::TuningProgramSelect).
47    pub tuning_program_num: u8,
48    /// Which tuning bank is targeted, 0-127. See [`Parameter::TuningBankSelect`](crate::Parameter::TuningBankSelect).
49    pub tuning_bank_num: Option<u8>,
50    /// An exactly 16 character name
51    pub name: [u8; 16],
52    /// Should be exactly 128 Tunings with the index of each value = the MIDI note number being tuned.
53    /// Excess values will be ignored. If fewer than 128 values are supplied, equal temperament
54    /// will be applied to the remaining notes.
55    /// A `None` value represents "No change".
56    pub tunings: Vec<Option<Tuning>>,
57}
58
59impl KeyBasedTuningDump {
60    pub(crate) fn extend_midi(&self, v: &mut Vec<u8>) {
61        if let Some(bank_num) = self.tuning_bank_num {
62            v.push(to_u7(bank_num))
63        }
64        push_u7(self.tuning_program_num, v);
65        for ch in self.name.iter() {
66            v.push(*ch);
67        }
68        let mut i = 0;
69        loop {
70            if i >= 128 {
71                break;
72            }
73            if let Some(tuning) = self.tunings.get(i) {
74                if let Some(tuning) = tuning {
75                    tuning.extend_midi(v);
76                } else {
77                    // "No change"
78                    v.push(0x7F);
79                    v.push(0x7F);
80                    v.push(0x7F);
81                }
82            } else {
83                // The equivalent of equal temperament tuning
84                push_u7(i as u8, v);
85                v.push(0);
86                v.push(0);
87            }
88            i += 1;
89        }
90        v.push(0); // Checksum <- Will be written over by `SystemExclusiveMsg.extend_midi`
91    }
92
93    #[allow(dead_code)]
94    pub(crate) fn from_midi(_m: &[u8]) -> Result<(Self, usize), ParseError> {
95        Err(ParseError::NotImplemented("KeyBasedTuningDump"))
96    }
97}
98
99/// Used to represent a tuning by [`TuningNoteChange`] and [`KeyBasedTuningDump`].
100#[derive(Debug, Copy, Clone, PartialEq, Eq)]
101pub struct Tuning {
102    /// The semitone corresponding with the same MIDI note number, 0-127
103    pub semitone: u8,
104    /// Fraction of semitones above the `semitone`, in .0061-cent units.
105    /// 0-16383
106    pub fraction: u16,
107}
108
109impl Tuning {
110    pub fn from_freq(freq: f32) -> Self {
111        if freq < 8.17358 {
112            Self {
113                semitone: 0,
114                fraction: 0,
115            }
116        } else if freq > 13289.73 {
117            Self {
118                semitone: 127,
119                fraction: 16383,
120            }
121        } else {
122            let (semitone, c) = freq_to_midi_note_cents(freq);
123            Self {
124                semitone,
125                fraction: cents_to_u14(c).min(0x3FFE),
126            }
127        }
128    }
129
130    fn extend_midi(&self, v: &mut Vec<u8>) {
131        push_u7(self.semitone, v);
132        let [msb, lsb] = to_u14(self.fraction);
133        v.push(msb); // For some reason this is the opposite order of everything else???
134        v.push(lsb);
135    }
136}
137
138/// Set the tuning of all octaves for a tuning program/bank.
139/// Used by [`UniversalNonRealTimeMsg`](crate::UniversalNonRealTimeMsg).
140///
141/// As defined in MIDI Tuning Updated Specification (CA-020/CA-021/RP-020)
142#[derive(Debug, Copy, Clone, PartialEq, Eq)]
143pub struct ScaleTuningDump1Byte {
144    /// Which tuning program is targeted, 0-127. See [`Parameter::TuningProgramSelect`](crate::Parameter::TuningProgramSelect).
145    pub tuning_program_num: u8,
146    /// Which tuning bank is targeted, 0-127. See [`Parameter::TuningBankSelect`](crate::Parameter::TuningBankSelect).
147    pub tuning_bank_num: u8,
148    /// An exactly 16 character name.
149    pub name: [u8; 16],
150    /// 12 semitones of tuning adjustments repeated over all octaves, starting with C
151    /// Each value represents that number of cents plus the equal temperament tuning,
152    /// from -64 to 63 cents
153    pub tuning: [i8; 12],
154}
155
156impl ScaleTuningDump1Byte {
157    pub(crate) fn extend_midi(&self, v: &mut Vec<u8>) {
158        push_u7(self.tuning_bank_num, v);
159        push_u7(self.tuning_program_num, v);
160        for ch in self.name.iter() {
161            v.push(*ch);
162        }
163
164        for t in self.tuning.iter() {
165            v.push(i_to_u7(*t));
166        }
167
168        v.push(0); // Checksum <- Will be written over by `SystemExclusiveMsg.extend_midi`
169    }
170
171    #[allow(dead_code)]
172    pub(crate) fn from_midi(_m: &[u8]) -> Result<(Self, usize), ParseError> {
173        Err(ParseError::NotImplemented("ScaleTuningDump1Byte"))
174    }
175}
176
177/// Set the high-res tuning of all octaves for a tuning program/bank.
178/// Used by [`UniversalNonRealTimeMsg`](crate::UniversalNonRealTimeMsg).
179///
180/// As defined in MIDI Tuning Updated Specification (CA-020/CA-021/RP-020)
181#[derive(Debug, Copy, Clone, PartialEq, Eq)]
182pub struct ScaleTuningDump2Byte {
183    /// Which tuning program is targeted, 0-127. See [`Parameter::TuningProgramSelect`](crate::Parameter::TuningProgramSelect).
184    pub tuning_program_num: u8,
185    /// Which tuning bank is targeted, 0-127. See [`Parameter::TuningBankSelect`](crate::Parameter::TuningBankSelect).
186    pub tuning_bank_num: u8,
187    /// An exactly 16 character name.
188    pub name: [u8; 16],
189    /// 12 semitones of tuning adjustments repeated over all octaves, starting with C
190    /// Each value represents that fractional number of cents plus the equal temperament tuning,
191    /// from -8192 to 8192 (steps of .012207 cents)
192    pub tuning: [i16; 12],
193}
194
195impl ScaleTuningDump2Byte {
196    pub(crate) fn extend_midi(&self, v: &mut Vec<u8>) {
197        push_u7(self.tuning_bank_num, v);
198        push_u7(self.tuning_program_num, v);
199        for ch in self.name.iter() {
200            v.push(*ch);
201        }
202
203        for t in self.tuning.iter() {
204            let [msb, lsb] = i_to_u14(*t);
205            v.push(lsb);
206            v.push(msb);
207        }
208
209        v.push(0); // Checksum <- Will be written over by `SystemExclusiveMsg.extend_midi`
210    }
211
212    #[allow(dead_code)]
213    pub(crate) fn from_midi(_m: &[u8]) -> Result<(Self, usize), ParseError> {
214        Err(ParseError::NotImplemented("ScaleTuningDump2Byte"))
215    }
216}
217
218/// Set the tuning of all octaves for a set of channels.
219/// Used by [`UniversalNonRealTimeMsg`](crate::UniversalNonRealTimeMsg) and [`UniversalRealTimeMsg`](crate::UniversalRealTimeMsg).
220///
221/// As defined in MIDI Tuning Updated Specification (CA-020/CA-021/RP-020)
222#[derive(Debug, Copy, Clone, PartialEq, Eq)]
223pub struct ScaleTuning1Byte {
224    pub channels: ChannelBitMap,
225    /// 12 semitones of tuning adjustments repeated over all octaves, starting with C
226    /// Each value represents that number of cents plus the equal temperament tuning,
227    /// from -64 to 63 cents
228    pub tuning: [i8; 12],
229}
230
231impl ScaleTuning1Byte {
232    pub(crate) fn extend_midi(&self, v: &mut Vec<u8>) {
233        self.channels.extend_midi(v);
234        for t in self.tuning.iter() {
235            v.push(i_to_u7(*t));
236        }
237    }
238
239    #[allow(dead_code)]
240    pub(crate) fn from_midi(_m: &[u8]) -> Result<(Self, usize), ParseError> {
241        Err(ParseError::NotImplemented("ScaleTuning1Byte"))
242    }
243}
244
245/// Set the high-res tuning of all octaves for a set of channels.
246/// Used by [`UniversalNonRealTimeMsg`](crate::UniversalNonRealTimeMsg) and [`UniversalRealTimeMsg`](crate::UniversalRealTimeMsg).
247///
248/// As defined in MIDI Tuning Updated Specification (CA-020/CA-021/RP-020)
249#[derive(Debug, Copy, Clone, PartialEq, Eq)]
250pub struct ScaleTuning2Byte {
251    pub channels: ChannelBitMap,
252    /// 12 semitones of tuning adjustments repeated over all octaves, starting with C
253    /// Each value represents that fractional number of cents plus the equal temperament tuning,
254    /// from -8192 to 8192 (steps of .012207 cents)
255    pub tuning: [i16; 12],
256}
257
258impl ScaleTuning2Byte {
259    pub(crate) fn extend_midi(&self, v: &mut Vec<u8>) {
260        self.channels.extend_midi(v);
261        for t in self.tuning.iter() {
262            let [msb, lsb] = i_to_u14(*t);
263            v.push(lsb);
264            v.push(msb);
265        }
266    }
267
268    #[allow(dead_code)]
269    pub(crate) fn from_midi(_m: &[u8]) -> Result<(Self, usize), ParseError> {
270        Err(ParseError::NotImplemented("ScaleTuning2Byte"))
271    }
272}
273
274/// The set of channels to apply this tuning message to. Used by [`ScaleTuning1Byte`] and [`ScaleTuning2Byte`].
275#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
276pub struct ChannelBitMap {
277    pub channel_1: bool,
278    pub channel_2: bool,
279    pub channel_3: bool,
280    pub channel_4: bool,
281    pub channel_5: bool,
282    pub channel_6: bool,
283    pub channel_7: bool,
284    pub channel_8: bool,
285    pub channel_9: bool,
286    pub channel_10: bool,
287    pub channel_11: bool,
288    pub channel_12: bool,
289    pub channel_13: bool,
290    pub channel_14: bool,
291    pub channel_15: bool,
292    pub channel_16: bool,
293}
294
295impl ChannelBitMap {
296    /// All channels set
297    pub fn all() -> Self {
298        Self {
299            channel_1: true,
300            channel_2: true,
301            channel_3: true,
302            channel_4: true,
303            channel_5: true,
304            channel_6: true,
305            channel_7: true,
306            channel_8: true,
307            channel_9: true,
308            channel_10: true,
309            channel_11: true,
310            channel_12: true,
311            channel_13: true,
312            channel_14: true,
313            channel_15: true,
314            channel_16: true,
315        }
316    }
317
318    /// No channels set
319    pub fn none() -> Self {
320        Self::default()
321    }
322
323    pub(crate) fn extend_midi(&self, v: &mut Vec<u8>) {
324        let mut byte1: u8 = 0;
325        if self.channel_16 {
326            byte1 += 1 << 1;
327        }
328        if self.channel_15 {
329            byte1 += 1 << 0;
330        }
331        v.push(byte1);
332
333        let mut byte2: u8 = 0;
334        if self.channel_14 {
335            byte2 += 1 << 6;
336        }
337        if self.channel_13 {
338            byte2 += 1 << 5;
339        }
340        if self.channel_12 {
341            byte2 += 1 << 4;
342        }
343        if self.channel_11 {
344            byte2 += 1 << 3;
345        }
346        if self.channel_10 {
347            byte2 += 1 << 2;
348        }
349        if self.channel_9 {
350            byte2 += 1 << 1;
351        }
352        if self.channel_8 {
353            byte2 += 1 << 0;
354        }
355        v.push(byte2);
356
357        let mut byte3: u8 = 0;
358        if self.channel_7 {
359            byte3 += 1 << 6;
360        }
361        if self.channel_6 {
362            byte3 += 1 << 5;
363        }
364        if self.channel_5 {
365            byte3 += 1 << 4;
366        }
367        if self.channel_4 {
368            byte3 += 1 << 3;
369        }
370        if self.channel_3 {
371            byte3 += 1 << 2;
372        }
373        if self.channel_2 {
374            byte3 += 1 << 1;
375        }
376        if self.channel_1 {
377            byte3 += 1 << 0;
378        }
379        v.push(byte3);
380    }
381
382    #[allow(dead_code)]
383    pub(crate) fn from_midi(_m: &[u8]) -> Result<(Self, usize), ParseError> {
384        Err(ParseError::NotImplemented("ChannelBitMap"))
385    }
386}
387
388#[cfg(test)]
389mod tests {
390    use crate::*;
391    use alloc::vec;
392    use bstr::B;
393    use core::convert::TryInto;
394
395    #[test]
396    fn serialize_tuning_note_change() {
397        assert_eq!(
398            MidiMsg::SystemExclusive {
399                msg: SystemExclusiveMsg::UniversalRealTime {
400                    device: DeviceID::AllCall,
401                    msg: UniversalRealTimeMsg::TuningNoteChange(TuningNoteChange {
402                        tuning_program_num: 5,
403                        tuning_bank_num: None,
404                        tunings: vec![
405                            (
406                                1,
407                                Some(Tuning {
408                                    semitone: 1,
409                                    fraction: 255,
410                                }),
411                            ),
412                            (
413                                0x33,
414                                Some(Tuning {
415                                    semitone: 0x33,
416                                    fraction: 511,
417                                }),
418                            ),
419                            (0x45, None),
420                            (0x78, Some(Tuning::from_freq(8_372.063)))
421                        ],
422                    }),
423                },
424            }
425            .to_midi(),
426            &[
427                0xF0, 0x7F, 0x7F, 0x08, 0x02, 0x05, 4, // Number of changes
428                0x01, 0x01, 0x01, 0x7f, // Tuning 1
429                0x33, 0x33, 0x03, 0x7f, // Tuning 2
430                0x45, 0x7f, 0x7f, 0x7f, // Tuning 3 (no change)
431                // 0x78, 0x78, 0x00, 0x01, // Tuning 4, exact
432                0x78, 0x78, 0x00, 0x02, // Tuning 4, micromath approximation
433                0xF7,
434            ]
435        );
436    }
437
438    #[test]
439    fn serialize_tuning_bulk_dump_reply() {
440        let packet_msg = MidiMsg::SystemExclusive {
441            msg: SystemExclusiveMsg::UniversalNonRealTime {
442                device: DeviceID::AllCall,
443                msg: UniversalNonRealTimeMsg::KeyBasedTuningDump(KeyBasedTuningDump {
444                    tuning_program_num: 5,
445                    tuning_bank_num: None,
446                    name: B("A tuning program").try_into().unwrap(), // B creates a &[u8], try_into converts it into an array
447                    tunings: vec![Some(Tuning {
448                        semitone: 1,
449                        fraction: 255,
450                    })],
451                }),
452            },
453        }
454        .to_midi();
455
456        assert_eq!(packet_msg.len(), 408);
457        assert_eq!(
458            &packet_msg[0..7],
459            &[0xF0, 0x7E, 0x7F, 0x08, 0x01, 0x05, b"A"[0]]
460        );
461
462        assert_eq!(
463            &packet_msg[22..31],
464            &[
465                0x01, 0x01, 0x7f, // Provided tuning
466                0x01, 0x00, 0x00, // Default tuning
467                0x02, 0x00, 0x00 // Default tuning
468            ]
469        );
470    }
471}