tune/
mts.rs

1//! Communication with devices over the MIDI Tuning Standard.
2//!
3//! References:
4//! - [Sysex messages](https://www.midi.org/specifications-old/item/table-4-universal-system-exclusive-messages)
5//! - [MIDI Tuning Standard](https://musescore.org/sites/musescore.org/files/2018-06/midituning.pdf)
6
7use std::{collections::HashSet, fmt::Debug, iter};
8
9use crate::{
10    key::PianoKey,
11    midi::{ChannelMessage, ChannelMessageType},
12    note::NoteLetter,
13    pitch::{Pitch, Pitched, Ratio},
14    tuning::KeyboardMapping,
15};
16
17// Universal System Exclusive Messages
18// f0 7e <payload> f7 Non-Real Time
19// f0 7f <payload> f7 Real Time
20
21const SYSEX_START: u8 = 0xf0;
22const SYSEX_NON_RT: u8 = 0x7e;
23const SYSEX_RT: u8 = 0x7f;
24const SYSEX_END: u8 = 0xf7;
25
26// MIDI Tuning Standard
27// 08 02 Single Note Tuning Change
28// 08 07 Single Note Tuning Change with Bank Select
29// 08 08 Scale/Octave Tuning, 1 byte format
30// 08 09 Scale/Octave Tuning, 2 byte format
31
32const MIDI_TUNING_STANDARD: u8 = 0x08;
33
34const SINGLE_NOTE_TUNING_CHANGE: u8 = 0x02;
35const SINGLE_NOTE_TUNING_CHANGE_WITH_BANK_SELECT: u8 = 0x07;
36const SCALE_OCTAVE_TUNING_1_BYTE_FORMAT: u8 = 0x08;
37const SCALE_OCTAVE_TUNING_2_BYTE_FORMAT: u8 = 0x09;
38
39const DEVICE_ID_BROADCAST: u8 = 0x7f;
40
41const U7_MASK: u16 = (1 << 7) - 1;
42const U14_UPPER_BOUND_AS_F64: f64 = (1 << 14) as f64;
43
44/// Properties of the generated *Single Note Tuning Change* message.
45///
46/// # Examples
47///
48/// ```
49/// # use tune::mts::SingleNoteTuningChange;
50/// # use tune::mts::SingleNoteTuningChangeMessage;
51/// # use tune::mts::SingleNoteTuningChangeOptions;
52/// # use tune::note::NoteLetter;
53/// # use tune::pitch::Pitch;
54/// let a4 = NoteLetter::A.in_octave(4).as_piano_key();
55/// let target_pitch = Pitch::from_hz(445.0);
56///
57/// let tuning_change = SingleNoteTuningChange { key: a4, target_pitch };
58///
59/// // Use default options
60/// let options = SingleNoteTuningChangeOptions::default();
61///
62/// let tuning_message = SingleNoteTuningChangeMessage::from_tuning_changes(
63///     &options,
64///     std::iter::once(tuning_change),
65/// )
66/// .unwrap();
67///
68/// assert_eq!(
69///     Vec::from_iter(tuning_message.sysex_bytes()),
70///     [[0xf0, 0x7f, 0x7f, 0x08, 0x02, // RT Single Note Tuning Change
71///       0, 1,                         // Tuning program / number of changes
72///       69, 69, 25, 5,                // Tuning changes
73///       0xf7]]                        // Sysex end
74/// );
75///
76/// // Use custom options
77/// let options = SingleNoteTuningChangeOptions {
78///     realtime: false,
79///     device_id: 55,
80///     tuning_program: 66,
81///     with_bank_select: Some(77),
82/// };
83///
84/// let tuning_message = SingleNoteTuningChangeMessage::from_tuning_changes(
85///     &options,
86///     std::iter::once(tuning_change),
87/// )
88/// .unwrap();
89///
90/// assert_eq!(
91///     Vec::from_iter(tuning_message.sysex_bytes()),
92///     [[0xf0, 0x7e, 55, 0x08, 0x07, // Non-RT Single Note Tuning Change with Bank Select
93///       77, 66, 1,                  // Tuning program / tuning bank / number of changes
94///       69, 69, 25, 5,              // Tuning changes
95///       0xf7]]                      // Sysex end
96/// );
97/// ```
98#[derive(Copy, Clone, Debug)]
99pub struct SingleNoteTuningChangeOptions {
100    /// If set to true, generate a realtime SysEx message (defaults to `true`).
101    pub realtime: bool,
102
103    /// Specifies the device ID (defaults to broadcast/0x7f).
104    pub device_id: u8,
105
106    /// Specifies the tuning program to be affected (defaults to 0).
107    pub tuning_program: u8,
108
109    /// If given, generate a *Single Note Tuning Change with Bank Select* message.
110    pub with_bank_select: Option<u8>,
111}
112
113impl Default for SingleNoteTuningChangeOptions {
114    fn default() -> Self {
115        Self {
116            realtime: true,
117            device_id: DEVICE_ID_BROADCAST,
118            tuning_program: 0,
119            with_bank_select: None,
120        }
121    }
122}
123
124/// Retunes one or multiple MIDI notes using the *Single Note Tuning Change* message format.
125#[derive(Clone, Debug)]
126pub struct SingleNoteTuningChangeMessage {
127    sysex_calls: [Option<Vec<u8>>; 2],
128    out_of_range_notes: Vec<SingleNoteTuningChange>,
129}
130
131impl SingleNoteTuningChangeMessage {
132    /// Creates a [`SingleNoteTuningChangeMessage`] from the provided `tuning` and `keys`.
133    ///
134    /// # Examples
135    ///
136    /// ```
137    /// # use tune::mts::SingleNoteTuningChangeMessage;
138    /// # use tune::key::PianoKey;
139    /// # use tune::note::NoteLetter;
140    /// # use tune::pitch::Ratio;
141    /// # use tune::scala::KbmRoot;
142    /// # use tune::scala::Scl;
143    /// let scl = Scl::builder()
144    ///     .push_ratio(Ratio::octave().divided_into_equal_steps(7))
145    ///     .build()
146    ///     .unwrap();
147    /// let kbm = KbmRoot::from(NoteLetter::D.in_octave(4)).to_kbm();
148    ///
149    /// let tuning_message = SingleNoteTuningChangeMessage::from_tuning(
150    ///     &Default::default(),
151    ///     (scl, kbm),
152    ///     (21..109).map(PianoKey::from_midi_number),
153    /// )
154    /// .unwrap();
155    ///
156    /// assert_eq!(tuning_message.sysex_bytes().count(), 1);
157    /// assert_eq!(tuning_message.out_of_range_notes().len(), 13);
158    /// ```
159    pub fn from_tuning(
160        options: &SingleNoteTuningChangeOptions,
161        tuning: impl KeyboardMapping<PianoKey>,
162        keys: impl IntoIterator<Item = PianoKey>,
163    ) -> Result<Self, SingleNoteTuningChangeError> {
164        let tuning_changes = keys.into_iter().flat_map(|key| {
165            tuning
166                .maybe_pitch_of(key)
167                .map(|target_pitch| SingleNoteTuningChange { key, target_pitch })
168        });
169        Self::from_tuning_changes(options, tuning_changes)
170    }
171
172    /// Creates a [`SingleNoteTuningChangeMessage`] from the provided `tuning_changes`.
173    ///
174    /// # Examples
175    ///
176    /// ```
177    /// # use tune::mts::SingleNoteTuningChange;
178    /// # use tune::mts::SingleNoteTuningChangeMessage;
179    /// # use tune::note::NoteLetter;
180    /// # use tune::pitch::Pitch;
181    /// let key = NoteLetter::A.in_octave(4).as_piano_key();
182    ///
183    /// let good = SingleNoteTuningChange { key, target_pitch: Pitch::from_hz(445.0) };
184    /// let too_low = SingleNoteTuningChange { key, target_pitch: Pitch::from_hz(1.0) };
185    /// let too_high = SingleNoteTuningChange { key, target_pitch: Pitch::from_hz(100000.0) };
186    ///
187    /// let tuning_message = SingleNoteTuningChangeMessage::from_tuning_changes(
188    ///     &Default::default(), [good, too_low, too_high]
189    /// )
190    /// .unwrap();
191    ///
192    /// assert_eq!(tuning_message.sysex_bytes().count(), 1);
193    /// assert_eq!(tuning_message.out_of_range_notes(), [too_low, too_high]);
194    /// ```
195    pub fn from_tuning_changes(
196        options: &SingleNoteTuningChangeOptions,
197        tuning_changes: impl IntoIterator<Item = SingleNoteTuningChange>,
198    ) -> Result<Self, SingleNoteTuningChangeError> {
199        if options.device_id >= 128 {
200            return Err(SingleNoteTuningChangeError::DeviceIdOutOfRange);
201        }
202        if options.tuning_program >= 128 {
203            return Err(SingleNoteTuningChangeError::TuningProgramOutOfRange);
204        }
205        if options
206            .with_bank_select
207            .filter(|&tuning_bank| tuning_bank >= 128)
208            .is_some()
209        {
210            return Err(SingleNoteTuningChangeError::TuningBankNumberOutOfRange);
211        }
212
213        let mut sysex_tuning_list = Vec::new();
214        let mut num_retuned_notes = 0;
215        let mut out_of_range_notes = Vec::new();
216
217        for tuning_change in tuning_changes {
218            let approximation = tuning_change.target_pitch.find_in_tuning(());
219            let mut target_note = approximation.approx_value;
220
221            let mut detune_in_u14_resolution =
222                (approximation.deviation.as_semitones() * U14_UPPER_BOUND_AS_F64).round();
223
224            // Make sure that the detune range is [0c..100c] instead of [-50c..50c]
225            if detune_in_u14_resolution < 0.0 {
226                target_note = target_note.plus_semitones(-1);
227                detune_in_u14_resolution += U14_UPPER_BOUND_AS_F64;
228            }
229
230            if let (Some(source), Some(target)) = (
231                tuning_change.key.checked_midi_number(),
232                target_note.checked_midi_number(),
233            ) {
234                let pitch_msb = (detune_in_u14_resolution as u16 >> 7) as u8;
235                let pitch_lsb = (detune_in_u14_resolution as u16 & U7_MASK) as u8;
236
237                sysex_tuning_list.push(source);
238                sysex_tuning_list.push(target);
239                sysex_tuning_list.push(pitch_msb);
240                sysex_tuning_list.push(pitch_lsb);
241
242                num_retuned_notes += 1;
243            } else {
244                out_of_range_notes.push(tuning_change);
245            }
246
247            if num_retuned_notes > 128 {
248                return Err(SingleNoteTuningChangeError::TuningChangeListTooLong);
249            }
250        }
251
252        let create_sysex = |sysex_tuning_list: &[u8]| {
253            let mut sysex_call = Vec::with_capacity(sysex_tuning_list.len() + 9);
254
255            sysex_call.push(SYSEX_START);
256            sysex_call.push(if options.realtime {
257                SYSEX_RT
258            } else {
259                SYSEX_NON_RT
260            });
261            sysex_call.push(options.device_id);
262            sysex_call.push(MIDI_TUNING_STANDARD);
263            sysex_call.push(if options.with_bank_select.is_some() {
264                SINGLE_NOTE_TUNING_CHANGE_WITH_BANK_SELECT
265            } else {
266                SINGLE_NOTE_TUNING_CHANGE
267            });
268            if let Some(with_bank_select) = options.with_bank_select {
269                sysex_call.push(with_bank_select);
270            }
271            sysex_call.push(options.tuning_program);
272            sysex_call.push((sysex_tuning_list.len() / 4).try_into().unwrap());
273            sysex_call.extend(sysex_tuning_list);
274            sysex_call.push(SYSEX_END);
275
276            sysex_call
277        };
278
279        let sysex_calls = if num_retuned_notes == 0 {
280            [None, None]
281        } else if num_retuned_notes < 128 {
282            [Some(create_sysex(&sysex_tuning_list[..])), None]
283        } else {
284            [
285                Some(create_sysex(&sysex_tuning_list[..256])),
286                Some(create_sysex(&sysex_tuning_list[256..])),
287            ]
288        };
289
290        Ok(SingleNoteTuningChangeMessage {
291            sysex_calls,
292            out_of_range_notes,
293        })
294    }
295
296    /// Returns the tuning message conforming to the MIDI tuning standard.
297    ///
298    /// If less than 128 notes are retuned the iterator yields a single tuning message.
299    /// If the number of retuned notes is 128 two messages with a batch of 64 notes are yielded.
300    /// If the number of retuned notes is 0 no message is yielded.
301    ///
302    /// # Examples
303    ///
304    /// ```
305    /// # use tune::mts::SingleNoteTuningChange;
306    /// # use tune::mts::SingleNoteTuningChangeMessage;
307    /// # use tune::key::PianoKey;
308    /// # use tune::note::Note;
309    /// # use tune::pitch::Pitched;
310    /// let create_tuning_message_with_num_changes = |num_changes| {
311    ///     let tuning_changes = (0..num_changes).map(|midi_number| {
312    ///         SingleNoteTuningChange {
313    ///             key: PianoKey::from_midi_number(midi_number),
314    ///             target_pitch: Note::from_midi_number(midi_number).pitch(),
315    ///         }
316    ///     });
317    ///
318    ///     SingleNoteTuningChangeMessage::from_tuning_changes(
319    ///         &Default::default(),
320    ///         tuning_changes,
321    ///     )
322    ///     .unwrap()
323    /// };
324    ///
325    /// assert_eq!(create_tuning_message_with_num_changes(0).sysex_bytes().count(), 0);
326    /// assert_eq!(create_tuning_message_with_num_changes(127).sysex_bytes().count(), 1);
327    /// assert_eq!(create_tuning_message_with_num_changes(128).sysex_bytes().count(), 2);
328    /// ```
329    pub fn sysex_bytes(&self) -> impl Iterator<Item = &[u8]> {
330        self.sysex_calls.iter().flatten().map(Vec::as_slice)
331    }
332
333    /// Return notes whose target pitch is not representable by the tuning message.
334    pub fn out_of_range_notes(&self) -> &[SingleNoteTuningChange] {
335        &self.out_of_range_notes
336    }
337}
338
339/// Tunes the given [`PianoKey`] to the given [`Pitch`].
340#[derive(Copy, Clone, Debug, PartialEq)]
341pub struct SingleNoteTuningChange {
342    /// The key to tune.
343    pub key: PianoKey,
344
345    /// The [`Pitch`] that the given key should sound in.
346    pub target_pitch: Pitch,
347}
348
349/// Creating a [`SingleNoteTuningChangeMessage`] failed.
350#[derive(Copy, Clone, Debug)]
351pub enum SingleNoteTuningChangeError {
352    /// The tuning change list has more than 128 elements.
353    ///
354    /// Discarded values are not counted.
355    ///
356    /// # Examples
357    ///
358    /// ```
359    /// # use tune::mts::SingleNoteTuningChange;
360    /// # use tune::mts::SingleNoteTuningChangeError;
361    /// # use tune::mts::SingleNoteTuningChangeMessage;
362    /// # use tune::key::PianoKey;
363    /// # use tune::note::Note;
364    /// # use tune::pitch::Pitched;
365    /// let vec_with_128_changes: Vec<_> = (0..128)
366    ///     .map(|midi_number| {
367    ///         SingleNoteTuningChange {
368    ///             key: PianoKey::from_midi_number(midi_number),
369    ///             target_pitch: Note::from_midi_number(midi_number).pitch(),
370    ///         }
371    ///     })
372    ///     .collect();
373    ///
374    /// let mut vec_with_129_changes = vec_with_128_changes.clone();
375    /// vec_with_129_changes.push({
376    ///     SingleNoteTuningChange {
377    ///         key: PianoKey::from_midi_number(64),
378    ///         target_pitch: Note::from_midi_number(64).pitch(),
379    ///     }
380    /// });
381    ///
382    /// let mut vec_with_discarded_elements = vec_with_128_changes.clone();
383    /// vec_with_discarded_elements.push({
384    ///     SingleNoteTuningChange {
385    ///         key: PianoKey::from_midi_number(128),
386    ///         target_pitch: Note::from_midi_number(128).pitch(),
387    ///     }
388    /// });
389    ///
390    /// assert!(matches!(
391    ///     SingleNoteTuningChangeMessage::from_tuning_changes(
392    ///         &Default::default(),
393    ///         vec_with_128_changes,
394    ///     ),
395    ///     Ok(_)
396    /// ));
397    /// assert!(matches!(
398    ///     SingleNoteTuningChangeMessage::from_tuning_changes(
399    ///         &Default::default(),
400    ///         vec_with_129_changes,
401    ///     ),
402    ///     Err(SingleNoteTuningChangeError::TuningChangeListTooLong)
403    /// ));
404    /// assert!(matches!(
405    ///     SingleNoteTuningChangeMessage::from_tuning_changes(
406    ///         &Default::default(),
407    ///         vec_with_discarded_elements,
408    ///     ),
409    ///     Ok(_)
410    /// ));
411    /// ```
412    TuningChangeListTooLong,
413
414    /// The device ID is greater than 127.
415    ///
416    /// # Examples
417    ///
418    /// ```
419    /// # use std::iter;
420    /// # use tune::mts::SingleNoteTuningChangeError;
421    /// # use tune::mts::SingleNoteTuningChangeMessage;
422    /// # use tune::mts::SingleNoteTuningChangeOptions;
423    /// let create_tuning_message_for_device_id = |device_id| {
424    ///     let options = SingleNoteTuningChangeOptions {
425    ///         device_id,
426    ///         ..Default::default()
427    ///     };
428    ///
429    ///     SingleNoteTuningChangeMessage::from_tuning_changes(&options, iter::empty())
430    /// };
431    ///
432    /// assert!(matches!(
433    ///     create_tuning_message_for_device_id(127),
434    ///     Ok(_)
435    /// ));
436    /// assert!(matches!(
437    ///     create_tuning_message_for_device_id(128),
438    ///     Err(SingleNoteTuningChangeError::DeviceIdOutOfRange)
439    /// ));
440    /// ```
441    DeviceIdOutOfRange,
442
443    /// The tuning program number is greater than 127.
444    ///
445    /// # Examples
446    ///
447    /// ```
448    /// # use std::iter;
449    /// # use tune::mts::SingleNoteTuningChangeError;
450    /// # use tune::mts::SingleNoteTuningChangeMessage;
451    /// # use tune::mts::SingleNoteTuningChangeOptions;
452    /// let create_tuning_message_for_program = |tuning_program| {
453    ///     let options = SingleNoteTuningChangeOptions {
454    ///         tuning_program,
455    ///         ..Default::default()
456    ///     };
457    ///
458    ///     SingleNoteTuningChangeMessage::from_tuning_changes(&options, iter::empty())
459    /// };
460    ///
461    /// assert!(matches!(
462    ///     create_tuning_message_for_program(127),
463    ///     Ok(_)
464    /// ));
465    /// assert!(matches!(
466    ///     create_tuning_message_for_program(128),
467    ///     Err(SingleNoteTuningChangeError::TuningProgramOutOfRange)
468    /// ));
469
470    /// ```
471    TuningProgramOutOfRange,
472
473    /// The tuning bank number is greater than 127.
474    ///
475    /// # Examples
476    ///
477    /// ```
478    /// # use std::iter;
479    /// # use tune::mts::SingleNoteTuningChangeError;
480    /// # use tune::mts::SingleNoteTuningChangeMessage;
481    /// # use tune::mts::SingleNoteTuningChangeOptions;
482    /// let create_tuning_message_with_bank_select = |tuning_bank| {
483    ///     let options = SingleNoteTuningChangeOptions {
484    ///         with_bank_select: Some(tuning_bank),
485    ///         ..Default::default()
486    ///     };
487    ///
488    ///     SingleNoteTuningChangeMessage::from_tuning_changes(&options, iter::empty())
489    /// };
490    ///
491    /// assert!(matches!(
492    ///     create_tuning_message_with_bank_select(127),
493    ///     Ok(_)
494    /// ));
495    /// assert!(matches!(
496    ///     create_tuning_message_with_bank_select(128),
497    ///     Err(SingleNoteTuningChangeError::TuningBankNumberOutOfRange)
498    /// ));
499
500    /// ```
501    TuningBankNumberOutOfRange,
502}
503
504/// Properties of the generated *Scale/Octave Tuning* message.
505///
506/// # Examples
507///
508/// ```
509/// # use std::collections::HashSet;
510/// # use tune::mts::ScaleOctaveTuning;
511/// # use tune::mts::ScaleOctaveTuningFormat;
512/// # use tune::mts::ScaleOctaveTuningMessage;
513/// # use tune::mts::ScaleOctaveTuningOptions;
514/// # use tune::note::NoteLetter;
515/// # use tune::pitch::Ratio;
516/// let octave_tuning = ScaleOctaveTuning {
517///     c: Ratio::from_cents(10.0),
518///     csh: Ratio::from_cents(-200.0), // Will be clamped
519///     d: Ratio::from_cents(200.0),    // Will be clamped
520///     ..Default::default()
521/// };
522///
523/// // Use default options
524/// let options = ScaleOctaveTuningOptions::default();
525///
526/// let tuning_message = ScaleOctaveTuningMessage::from_octave_tuning(
527///     &options,
528///     &octave_tuning,
529/// )
530/// .unwrap();
531///
532/// assert_eq!(
533///     tuning_message.sysex_bytes(),
534///     [0xf0, 0x7e, 0x7f, 0x08, 0x08,                   // Non-RT Scale/Octave Tuning (1-Byte)
535///      0b00000011, 0b01111111, 0b01111111,             // Channel bits
536///      74, 0, 127, 64, 64, 64, 64, 64, 64, 64, 64, 64, // Tuning changes (C - B)
537///      0xf7]                                           // Sysex end
538/// );
539///
540/// // Use custom options
541/// let options = ScaleOctaveTuningOptions {
542///     realtime: true,
543///     device_id: 55,
544///     channels: HashSet::from([0, 3, 6, 9, 12, 15]).into(),
545///     format: ScaleOctaveTuningFormat::TwoByte,
546/// };
547///
548/// let tuning_message = ScaleOctaveTuningMessage::from_octave_tuning(
549///     &options,
550///     &octave_tuning,
551/// )
552/// .unwrap();
553///
554/// assert_eq!(
555///     tuning_message.sysex_bytes(),
556///     [0xf0, 0x7f, 55, 0x08, 0x09,                  // RT Scale/Octave Tuning (2-Byte)
557///      0b00000010, 0b00100100, 0b01001001,          // Channel bits
558///      70, 51, 0, 0, 127, 127, 64, 0, 64, 0, 64, 0, // Tuning changes (C - F)
559///      64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0,    // Tuning changes (F# - B)
560///      0xf7]                                        // Sysex end
561/// );
562/// ```
563#[derive(Clone, Debug)]
564pub struct ScaleOctaveTuningOptions {
565    /// If set to true, generate a realtime SysEx message (defaults to `false`).
566    pub realtime: bool,
567
568    /// Specifies the device ID (defaults to broadcast/0x7f).
569    pub device_id: u8,
570
571    /// Specifies the channels that are affected by the tuning change (defaults to [`Channels::All`]).
572    pub channels: Channels,
573
574    /// Specifies whether to send a 1-byte or 2-byte message (defaults to [`ScaleOctaveTuningFormat::OneByte`]).
575    pub format: ScaleOctaveTuningFormat,
576}
577
578/// 1-byte or 2-byte form of the *Scale/Octave Tuning* message.
579///
580/// The 1-byte form supports values in the range [-64cents..63cents], the 2-byte form supports values in the range [-100cents..100cents).
581#[derive(Copy, Clone, Debug)]
582pub enum ScaleOctaveTuningFormat {
583    OneByte,
584    TwoByte,
585}
586
587impl Default for ScaleOctaveTuningOptions {
588    fn default() -> Self {
589        Self {
590            realtime: false,
591            channels: Channels::All,
592            device_id: DEVICE_ID_BROADCAST,
593            format: ScaleOctaveTuningFormat::OneByte,
594        }
595    }
596}
597
598/// Retunes MIDI pitch classes within an octave using the *Scale/Octave Tuning* message format.
599#[derive(Clone, Debug)]
600pub struct ScaleOctaveTuningMessage {
601    sysex_call: Vec<u8>,
602}
603
604impl ScaleOctaveTuningMessage {
605    /// Creates a [`ScaleOctaveTuningMessage`] from the provided `octave_tunings`.
606    ///
607    /// # Examples
608    ///
609    /// ```
610    /// # use tune::mts::ScaleOctaveTuning;
611    /// # use tune::mts::ScaleOctaveTuningMessage;
612    /// # use tune::pitch::Ratio;
613    /// let octave_tuning = ScaleOctaveTuning {
614    ///     c: Ratio::from_cents(10.0),
615    ///     ..Default::default()
616    /// };
617    ///
618    /// let tuning_message = ScaleOctaveTuningMessage::from_octave_tuning(
619    ///     &Default::default(),
620    ///     &octave_tuning,
621    /// )
622    /// .unwrap();
623    ///
624    /// assert_eq!(tuning_message.sysex_bytes().len(), 21);
625    /// ```
626    pub fn from_octave_tuning(
627        options: &ScaleOctaveTuningOptions,
628        octave_tuning: &ScaleOctaveTuning,
629    ) -> Result<Self, ScaleOctaveTuningError> {
630        let mut sysex_call = Vec::with_capacity(21);
631
632        sysex_call.push(SYSEX_START);
633        sysex_call.push(if options.realtime {
634            SYSEX_RT
635        } else {
636            SYSEX_NON_RT
637        });
638        sysex_call.push(options.device_id);
639        sysex_call.push(MIDI_TUNING_STANDARD);
640        sysex_call.push(match options.format {
641            ScaleOctaveTuningFormat::OneByte => SCALE_OCTAVE_TUNING_1_BYTE_FORMAT,
642            ScaleOctaveTuningFormat::TwoByte => SCALE_OCTAVE_TUNING_2_BYTE_FORMAT,
643        });
644
645        match &options.channels {
646            Channels::All => {
647                sysex_call.push(0b0000_0011); // bits 0 to 1 = channel 15 to 16
648                sysex_call.push(0b0111_1111); // bits 0 to 6 = channel 8 to 14
649                sysex_call.push(0b0111_1111); // bits 0 to 6 = channel 1 to 7
650            }
651            Channels::Some(channels) => {
652                let mut encoded_channels = [0; 3];
653
654                for &channel in channels {
655                    if channel >= 16 {
656                        return Err(ScaleOctaveTuningError::ChannelOutOfRange);
657                    }
658                    let bit_position = channel % 7;
659                    let row_to_use = channel / 7;
660                    encoded_channels[usize::from(row_to_use)] |= 1 << bit_position;
661                }
662
663                sysex_call.extend(encoded_channels.iter().rev());
664            }
665        }
666
667        let pitch_bends = [
668            octave_tuning.c,
669            octave_tuning.csh,
670            octave_tuning.d,
671            octave_tuning.dsh,
672            octave_tuning.e,
673            octave_tuning.f,
674            octave_tuning.fsh,
675            octave_tuning.g,
676            octave_tuning.gsh,
677            octave_tuning.a,
678            octave_tuning.ash,
679            octave_tuning.b,
680        ];
681
682        match options.format {
683            ScaleOctaveTuningFormat::OneByte => {
684                for pitch_bend in pitch_bends {
685                    let value_to_write = (pitch_bend.as_cents() + 64.0).round().max(0.0).min(127.0);
686                    sysex_call.push(value_to_write as u8);
687                }
688            }
689            ScaleOctaveTuningFormat::TwoByte => {
690                for pitch_bend in pitch_bends {
691                    let value_to_write = ((pitch_bend.as_semitones() + 1.0) * 8192.0)
692                        .round()
693                        .max(0.0)
694                        .min(16383.0) as u16;
695                    sysex_call.push((value_to_write / 128) as u8);
696                    sysex_call.push((value_to_write % 128) as u8);
697                }
698            }
699        }
700
701        sysex_call.push(SYSEX_END);
702
703        Ok(ScaleOctaveTuningMessage { sysex_call })
704    }
705
706    /// Returns the tuning message conforming to the MIDI tuning standard.
707    pub fn sysex_bytes(&self) -> &[u8] {
708        &self.sysex_call
709    }
710}
711
712/// Creating a [`ScaleOctaveTuningMessage`] failed.
713#[derive(Copy, Clone, Debug, Eq, PartialEq)]
714pub enum ScaleOctaveTuningError {
715    /// A channel number exceeds the allowed range [0..16).
716    ///
717    /// # Examples
718    ///
719    /// ```
720    /// # use std::collections::HashSet;
721    /// # use tune::mts::ScaleOctaveTuningError;
722    /// # use tune::mts::ScaleOctaveTuningMessage;
723    /// # use tune::mts::ScaleOctaveTuningOptions;
724    /// // Channels 14 and 15 are valid
725    /// let options = ScaleOctaveTuningOptions {
726    ///     channels: HashSet::from([14, 15]).into(),
727    ///     ..Default::default()
728    /// };
729    ///
730    /// assert!(matches!(
731    ///     ScaleOctaveTuningMessage::from_octave_tuning(&options, &Default::default()),
732    ///     Ok(_)
733    /// ));
734    ///
735    /// // Channel 16 is invalid
736    /// let options = ScaleOctaveTuningOptions {
737    ///     channels: HashSet::from([14, 15, 16]).into(),
738    ///     ..Default::default()
739    /// };
740    ///
741    /// assert!(matches!(
742    ///     ScaleOctaveTuningMessage::from_octave_tuning(&options, &Default::default()),
743    ///     Err(ScaleOctaveTuningError::ChannelOutOfRange)
744    /// ));
745    /// ```
746    ChannelOutOfRange,
747}
748
749/// The detuning per pitch class within an octave.
750#[derive(Clone, Debug, Default)]
751pub struct ScaleOctaveTuning {
752    pub c: Ratio,
753    pub csh: Ratio,
754    pub d: Ratio,
755    pub dsh: Ratio,
756    pub e: Ratio,
757    pub f: Ratio,
758    pub fsh: Ratio,
759    pub g: Ratio,
760    pub gsh: Ratio,
761    pub a: Ratio,
762    pub ash: Ratio,
763    pub b: Ratio,
764}
765
766impl ScaleOctaveTuning {
767    pub fn as_mut(&mut self, letter: NoteLetter) -> &mut Ratio {
768        match letter {
769            NoteLetter::C => &mut self.c,
770            NoteLetter::Csh => &mut self.csh,
771            NoteLetter::D => &mut self.d,
772            NoteLetter::Dsh => &mut self.dsh,
773            NoteLetter::E => &mut self.e,
774            NoteLetter::F => &mut self.f,
775            NoteLetter::Fsh => &mut self.fsh,
776            NoteLetter::G => &mut self.g,
777            NoteLetter::Gsh => &mut self.gsh,
778            NoteLetter::A => &mut self.a,
779            NoteLetter::Ash => &mut self.ash,
780            NoteLetter::B => &mut self.b,
781        }
782    }
783}
784
785/// Channels to be affected by the *Scale/Octave Tuning* message.
786#[derive(Clone, Debug)]
787pub enum Channels {
788    All,
789    Some(HashSet<u8>),
790}
791
792impl From<HashSet<u8>> for Channels {
793    fn from(channels: HashSet<u8>) -> Self {
794        Self::Some(channels)
795    }
796}
797
798impl From<u8> for Channels {
799    fn from(channel: u8) -> Self {
800        Self::Some(iter::once(channel).collect())
801    }
802}
803
804pub fn channel_fine_tuning(channel: u8, detuning: Ratio) -> Option<[ChannelMessage; 4]> {
805    const CHANNEL_FINE_TUNING_MSB: u8 = 0x00;
806    const CHANNEL_FINE_TUNING_LSB: u8 = 0x01;
807
808    let (value_msb, value_lsb) = ratio_to_u8s(detuning);
809
810    rpn_message_2_byte(
811        channel,
812        CHANNEL_FINE_TUNING_MSB,
813        CHANNEL_FINE_TUNING_LSB,
814        value_msb,
815        value_lsb,
816    )
817}
818
819pub fn tuning_program_change(channel: u8, tuning_program: u8) -> Option<[ChannelMessage; 3]> {
820    const TUNING_PROGRAM_CHANGE_MSB: u8 = 0x00;
821    const TUNING_PROGRAM_CHANGE_LSB: u8 = 0x03;
822
823    rpn_message_1_byte(
824        channel,
825        TUNING_PROGRAM_CHANGE_MSB,
826        TUNING_PROGRAM_CHANGE_LSB,
827        tuning_program,
828    )
829}
830
831pub fn tuning_bank_change(channel: u8, tuning_bank: u8) -> Option<[ChannelMessage; 3]> {
832    const TUNING_BANK_CHANGE_MSB: u8 = 0x00;
833    const TUNING_BANK_CHANGE_LSB: u8 = 0x04;
834
835    rpn_message_1_byte(
836        channel,
837        TUNING_BANK_CHANGE_MSB,
838        TUNING_BANK_CHANGE_LSB,
839        tuning_bank,
840    )
841}
842
843// RPN format reference: https://www.midi.org/specifications-old/item/table-3-control-change-messages-data-bytes-2
844
845const RPN_MSB: u8 = 0x65;
846const RPN_LSB: u8 = 0x64;
847const DATA_ENTRY_MSB: u8 = 0x06;
848const DATA_ENTRY_LSB: u8 = 0x26;
849
850fn rpn_message_1_byte(
851    channel: u8,
852    parameter_number_msb: u8,
853    parameter_number_lsb: u8,
854    value: u8,
855) -> Option<[ChannelMessage; 3]> {
856    Some([
857        ChannelMessageType::ControlChange {
858            controller: RPN_MSB,
859            value: parameter_number_msb,
860        }
861        .in_channel(channel)?,
862        ChannelMessageType::ControlChange {
863            controller: RPN_LSB,
864            value: parameter_number_lsb,
865        }
866        .in_channel(channel)?,
867        ChannelMessageType::ControlChange {
868            controller: DATA_ENTRY_MSB,
869            value,
870        }
871        .in_channel(channel)?,
872    ])
873}
874
875fn rpn_message_2_byte(
876    channel: u8,
877    parameter_number_msb: u8,
878    parameter_number_lsb: u8,
879    value_msb: u8,
880    value_lsb: u8,
881) -> Option<[ChannelMessage; 4]> {
882    Some([
883        ChannelMessageType::ControlChange {
884            controller: RPN_MSB,
885            value: parameter_number_msb,
886        }
887        .in_channel(channel)?,
888        ChannelMessageType::ControlChange {
889            controller: RPN_LSB,
890            value: parameter_number_lsb,
891        }
892        .in_channel(channel)?,
893        ChannelMessageType::ControlChange {
894            controller: DATA_ENTRY_MSB,
895            value: value_msb,
896        }
897        .in_channel(channel)?,
898        ChannelMessageType::ControlChange {
899            controller: DATA_ENTRY_LSB,
900            value: value_lsb,
901        }
902        .in_channel(channel)?,
903    ])
904}
905
906fn ratio_to_u8s(ratio: Ratio) -> (u8, u8) {
907    let as_u16 = (((ratio.as_semitones() + 1.0) * 13f64.exp2()) as u16).min(16383);
908
909    ((as_u16 / 128) as u8, (as_u16 % 128) as u8)
910}
911
912#[cfg(test)]
913mod test {
914    use crate::{
915        note::Note,
916        scala::{KbmRoot, Scl},
917    };
918
919    use super::*;
920
921    #[test]
922    fn octave_tuning() {
923        let test_cases: &[(&[_], _, _, _)] = &[
924            (&[], 0b0000_0000, 0b0000_0000, 0b0000_0000),
925            (&[0], 0b0000_0000, 0b0000_0000, 0b0000_0001),
926            (&[6], 0b0000_0000, 0b0000_0000, 0b0100_0000),
927            (&[7], 0b0000_0000, 0b0000_0001, 0b0000_0000),
928            (&[13], 0b0000_0000, 0b0100_0000, 0b0000_0000),
929            (&[14], 0b0000_0001, 0b0000_0000, 0b0000_0000),
930            (&[15], 0b0000_0010, 0b0000_0000, 0b0000_0000),
931            (
932                &[0, 2, 4, 6, 8, 10, 12, 14],
933                0b0000_0001,
934                0b0010_1010,
935                0b0101_0101,
936            ),
937            (
938                &[1, 3, 5, 7, 9, 11, 13, 15],
939                0b0000_0010,
940                0b0101_0101,
941                0b0010_1010,
942            ),
943        ];
944
945        let octave_tuning = ScaleOctaveTuning {
946            c: Ratio::from_cents(-61.0),
947            csh: Ratio::from_cents(-50.0),
948            d: Ratio::from_cents(-39.0),
949            dsh: Ratio::from_cents(-28.0),
950            e: Ratio::from_cents(-17.0),
951            f: Ratio::from_cents(-6.0),
952            fsh: Ratio::from_cents(5.0),
953            g: Ratio::from_cents(16.0),
954            gsh: Ratio::from_cents(27.0),
955            a: Ratio::from_cents(38.0),
956            ash: Ratio::from_cents(49.0),
957            b: Ratio::from_cents(60.0),
958        };
959
960        for (channels, expected_channel_byte_1, expected_channel_byte_2, expected_channel_byte_3) in
961            test_cases.iter()
962        {
963            let options = ScaleOctaveTuningOptions {
964                device_id: 77,
965                channels: Channels::Some(channels.iter().cloned().collect()),
966                ..Default::default()
967            };
968            let tuning_message =
969                ScaleOctaveTuningMessage::from_octave_tuning(&options, &octave_tuning).unwrap();
970
971            assert_eq!(
972                tuning_message.sysex_bytes(),
973                [
974                    0xf0,
975                    0x7e,
976                    77,
977                    0x08,
978                    0x08,
979                    *expected_channel_byte_1,
980                    *expected_channel_byte_2,
981                    *expected_channel_byte_3,
982                    0x40 - 61,
983                    0x40 - 50,
984                    0x40 - 39,
985                    0x40 - 28,
986                    0x40 - 17,
987                    0x40 - 6,
988                    0x40 + 5,
989                    0x40 + 16,
990                    0x40 + 27,
991                    0x40 + 38,
992                    0x40 + 49,
993                    0x40 + 60,
994                    0xf7
995                ]
996            );
997        }
998    }
999
1000    #[test]
1001    fn octave_tuning_default_values() {
1002        let tuning_message =
1003            ScaleOctaveTuningMessage::from_octave_tuning(&Default::default(), &Default::default())
1004                .unwrap();
1005
1006        assert_eq!(
1007            tuning_message.sysex_bytes(),
1008            [
1009                0xf0,
1010                0x7e,
1011                0x7f,
1012                0x08,
1013                0x08,
1014                0b0000_0011,
1015                0b0111_1111,
1016                0b0111_1111,
1017                64,
1018                64,
1019                64,
1020                64,
1021                64,
1022                64,
1023                64,
1024                64,
1025                64,
1026                64,
1027                64,
1028                64,
1029                0xf7
1030            ]
1031        );
1032    }
1033
1034    #[test]
1035    fn single_note_tuning() {
1036        let scl = Scl::builder()
1037            .push_ratio(Ratio::octave().divided_into_equal_steps(31))
1038            .build()
1039            .unwrap();
1040        let kbm = KbmRoot::from(NoteLetter::D.in_octave(4)).to_kbm();
1041        let tuning = (scl, kbm);
1042
1043        let options = SingleNoteTuningChangeOptions {
1044            device_id: 11,
1045            tuning_program: 22,
1046            ..Default::default()
1047        };
1048        let single_message = SingleNoteTuningChangeMessage::from_tuning(
1049            &options,
1050            &tuning,
1051            (0..127).map(PianoKey::from_midi_number),
1052        )
1053        .unwrap();
1054        assert_eq!(
1055            Vec::from_iter(single_message.sysex_bytes()),
1056            [[
1057                0xf0, 0x7f, 11, 0x08, 0x02, 22, 127, 0, 38, 0, 0, 1, 38, 49, 70, 2, 38, 99, 12, 3,
1058                39, 20, 83, 4, 39, 70, 25, 5, 39, 119, 95, 6, 40, 41, 37, 7, 40, 90, 107, 8, 41,
1059                12, 50, 9, 41, 61, 120, 10, 41, 111, 62, 11, 42, 33, 4, 12, 42, 82, 74, 13, 43, 4,
1060                17, 14, 43, 53, 87, 15, 43, 103, 29, 16, 44, 24, 99, 17, 44, 74, 41, 18, 44, 123,
1061                111, 19, 45, 45, 54, 20, 45, 94, 124, 21, 46, 16, 66, 22, 46, 66, 8, 23, 46, 115,
1062                78, 24, 47, 37, 21, 25, 47, 86, 91, 26, 48, 8, 33, 27, 48, 57, 103, 28, 48, 107,
1063                45, 29, 49, 28, 116, 30, 49, 78, 58, 31, 50, 0, 0, 32, 50, 49, 70, 33, 50, 99, 12,
1064                34, 51, 20, 83, 35, 51, 70, 25, 36, 51, 119, 95, 37, 52, 41, 37, 38, 52, 90, 107,
1065                39, 53, 12, 50, 40, 53, 61, 120, 41, 53, 111, 62, 42, 54, 33, 4, 43, 54, 82, 74,
1066                44, 55, 4, 17, 45, 55, 53, 87, 46, 55, 103, 29, 47, 56, 24, 99, 48, 56, 74, 41, 49,
1067                56, 123, 111, 50, 57, 45, 54, 51, 57, 94, 124, 52, 58, 16, 66, 53, 58, 66, 8, 54,
1068                58, 115, 78, 55, 59, 37, 21, 56, 59, 86, 91, 57, 60, 8, 33, 58, 60, 57, 103, 59,
1069                60, 107, 45, 60, 61, 28, 116, 61, 61, 78, 58, 62, 62, 0, 0, 63, 62, 49, 70, 64, 62,
1070                99, 12, 65, 63, 20, 83, 66, 63, 70, 25, 67, 63, 119, 95, 68, 64, 41, 37, 69, 64,
1071                90, 107, 70, 65, 12, 50, 71, 65, 61, 120, 72, 65, 111, 62, 73, 66, 33, 4, 74, 66,
1072                82, 74, 75, 67, 4, 17, 76, 67, 53, 87, 77, 67, 103, 29, 78, 68, 24, 99, 79, 68, 74,
1073                41, 80, 68, 123, 111, 81, 69, 45, 54, 82, 69, 94, 124, 83, 70, 16, 66, 84, 70, 66,
1074                8, 85, 70, 115, 78, 86, 71, 37, 21, 87, 71, 86, 91, 88, 72, 8, 33, 89, 72, 57, 103,
1075                90, 72, 107, 45, 91, 73, 28, 116, 92, 73, 78, 58, 93, 74, 0, 0, 94, 74, 49, 70, 95,
1076                74, 99, 12, 96, 75, 20, 83, 97, 75, 70, 25, 98, 75, 119, 95, 99, 76, 41, 37, 100,
1077                76, 90, 107, 101, 77, 12, 50, 102, 77, 61, 120, 103, 77, 111, 62, 104, 78, 33, 4,
1078                105, 78, 82, 74, 106, 79, 4, 17, 107, 79, 53, 87, 108, 79, 103, 29, 109, 80, 24,
1079                99, 110, 80, 74, 41, 111, 80, 123, 111, 112, 81, 45, 54, 113, 81, 94, 124, 114, 82,
1080                16, 66, 115, 82, 66, 8, 116, 82, 115, 78, 117, 83, 37, 21, 118, 83, 86, 91, 119,
1081                84, 8, 33, 120, 84, 57, 103, 121, 84, 107, 45, 122, 85, 28, 116, 123, 85, 78, 58,
1082                124, 86, 0, 0, 125, 86, 49, 70, 126, 86, 99, 12, 0xf7
1083            ]]
1084        );
1085
1086        let options = SingleNoteTuningChangeOptions {
1087            device_id: 33,
1088            tuning_program: 44,
1089            ..Default::default()
1090        };
1091
1092        let split_message = SingleNoteTuningChangeMessage::from_tuning(
1093            &options,
1094            &tuning,
1095            (0..128).map(PianoKey::from_midi_number),
1096        )
1097        .unwrap();
1098        assert_eq!(
1099            Vec::from_iter(split_message.sysex_bytes()),
1100            [
1101                [
1102                    0xf0, 0x7f, 33, 0x08, 0x02, 44, 64, 0, 38, 0, 0, 1, 38, 49, 70, 2, 38, 99, 12,
1103                    3, 39, 20, 83, 4, 39, 70, 25, 5, 39, 119, 95, 6, 40, 41, 37, 7, 40, 90, 107, 8,
1104                    41, 12, 50, 9, 41, 61, 120, 10, 41, 111, 62, 11, 42, 33, 4, 12, 42, 82, 74, 13,
1105                    43, 4, 17, 14, 43, 53, 87, 15, 43, 103, 29, 16, 44, 24, 99, 17, 44, 74, 41, 18,
1106                    44, 123, 111, 19, 45, 45, 54, 20, 45, 94, 124, 21, 46, 16, 66, 22, 46, 66, 8,
1107                    23, 46, 115, 78, 24, 47, 37, 21, 25, 47, 86, 91, 26, 48, 8, 33, 27, 48, 57,
1108                    103, 28, 48, 107, 45, 29, 49, 28, 116, 30, 49, 78, 58, 31, 50, 0, 0, 32, 50,
1109                    49, 70, 33, 50, 99, 12, 34, 51, 20, 83, 35, 51, 70, 25, 36, 51, 119, 95, 37,
1110                    52, 41, 37, 38, 52, 90, 107, 39, 53, 12, 50, 40, 53, 61, 120, 41, 53, 111, 62,
1111                    42, 54, 33, 4, 43, 54, 82, 74, 44, 55, 4, 17, 45, 55, 53, 87, 46, 55, 103, 29,
1112                    47, 56, 24, 99, 48, 56, 74, 41, 49, 56, 123, 111, 50, 57, 45, 54, 51, 57, 94,
1113                    124, 52, 58, 16, 66, 53, 58, 66, 8, 54, 58, 115, 78, 55, 59, 37, 21, 56, 59,
1114                    86, 91, 57, 60, 8, 33, 58, 60, 57, 103, 59, 60, 107, 45, 60, 61, 28, 116, 61,
1115                    61, 78, 58, 62, 62, 0, 0, 63, 62, 49, 70, 0xf7
1116                ],
1117                [
1118                    0xf0, 0x7f, 33, 0x08, 0x02, 44, 64, 64, 62, 99, 12, 65, 63, 20, 83, 66, 63, 70,
1119                    25, 67, 63, 119, 95, 68, 64, 41, 37, 69, 64, 90, 107, 70, 65, 12, 50, 71, 65,
1120                    61, 120, 72, 65, 111, 62, 73, 66, 33, 4, 74, 66, 82, 74, 75, 67, 4, 17, 76, 67,
1121                    53, 87, 77, 67, 103, 29, 78, 68, 24, 99, 79, 68, 74, 41, 80, 68, 123, 111, 81,
1122                    69, 45, 54, 82, 69, 94, 124, 83, 70, 16, 66, 84, 70, 66, 8, 85, 70, 115, 78,
1123                    86, 71, 37, 21, 87, 71, 86, 91, 88, 72, 8, 33, 89, 72, 57, 103, 90, 72, 107,
1124                    45, 91, 73, 28, 116, 92, 73, 78, 58, 93, 74, 0, 0, 94, 74, 49, 70, 95, 74, 99,
1125                    12, 96, 75, 20, 83, 97, 75, 70, 25, 98, 75, 119, 95, 99, 76, 41, 37, 100, 76,
1126                    90, 107, 101, 77, 12, 50, 102, 77, 61, 120, 103, 77, 111, 62, 104, 78, 33, 4,
1127                    105, 78, 82, 74, 106, 79, 4, 17, 107, 79, 53, 87, 108, 79, 103, 29, 109, 80,
1128                    24, 99, 110, 80, 74, 41, 111, 80, 123, 111, 112, 81, 45, 54, 113, 81, 94, 124,
1129                    114, 82, 16, 66, 115, 82, 66, 8, 116, 82, 115, 78, 117, 83, 37, 21, 118, 83,
1130                    86, 91, 119, 84, 8, 33, 120, 84, 57, 103, 121, 84, 107, 45, 122, 85, 28, 116,
1131                    123, 85, 78, 58, 124, 86, 0, 0, 125, 86, 49, 70, 126, 86, 99, 12, 127, 87, 20,
1132                    83, 0xf7
1133                ]
1134            ]
1135        );
1136    }
1137
1138    #[test]
1139    fn single_note_tuning_numerical_correctness() {
1140        let tuning_changes = [
1141            (11, -1.0),     // Out of range
1142            (22, -0.00004), // Out of range
1143            (33, -0.00003), // Numerically equivalent to 0
1144            (44, 0.0),
1145            (55, 0.00003),  // Numerically equivalent to 0
1146            (66, 0.00004),  // Smallest value above 0 => lsb = 1
1147            (77, 31.41592), // Random number => (msb, lsb) = (53, 30)
1148            (11, 62.83185), // Random number => (msb, lsb) = (106, 61)
1149            (22, 68.99996), // Smallest value below 69 => lsb = 127
1150            (33, 68.99997), // Numerically equivalent to 69
1151            (44, 69.0),
1152            (55, 69.00003),  // Numerically equivalent to 69
1153            (66, 69.00004),  // Smallest value above 69 => lsb = 1
1154            (77, 69.25),     // 25% of a semitone => msb = 32
1155            (11, 69.49996),  // Smallest value below 69.5 => lsb = 127
1156            (22, 69.49997),  // Numerically equivalent to 69.5
1157            (33, 69.5),      // 50% of a semitone => msb = 64
1158            (44, 69.50003),  // Numerically equivalent to 69.5
1159            (55, 69.50004),  // Smallest value above 69.5 => lsb = 1
1160            (66, 69.75),     // 75% of a semitone => msb = 96
1161            (77, 127.99996), // Smallest value below 128 => lsb = 127
1162            (1, 127.99997),  // Out of range
1163            (11, 129.0),     // Out of range
1164        ]
1165        .iter()
1166        .map(|&(source, target)| {
1167            let key = PianoKey::from_midi_number(source);
1168            let target_pitch = Note::from_midi_number(0).pitch() * Ratio::from_semitones(target);
1169            SingleNoteTuningChange { key, target_pitch }
1170        });
1171
1172        let tuning_message =
1173            SingleNoteTuningChangeMessage::from_tuning_changes(&Default::default(), tuning_changes)
1174                .unwrap();
1175
1176        assert_eq!(
1177            Vec::from_iter(tuning_message.sysex_bytes()),
1178            [[
1179                0xf0, 0x7f, 0x7f, 0x08, 0x02, 0, 19, 33, 0, 0, 0, 44, 0, 0, 0, 55, 0, 0, 0, 66, 0,
1180                0, 1, 77, 31, 53, 30, 11, 62, 106, 61, 22, 68, 127, 127, 33, 69, 0, 0, 44, 69, 0,
1181                0, 55, 69, 0, 0, 66, 69, 0, 1, 77, 69, 32, 0, 11, 69, 63, 127, 22, 69, 64, 0, 33,
1182                69, 64, 0, 44, 69, 64, 0, 55, 69, 64, 1, 66, 69, 96, 0, 77, 127, 127, 127, 0xf7,
1183            ]]
1184        );
1185        assert_eq!(tuning_message.out_of_range_notes().len(), 4);
1186    }
1187}