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}