//! Communication with devices over the MIDI Tuning Standard.
//!
//! References:
//! - [Sysex messages](https://www.midi.org/specifications-old/item/table-4-universal-system-exclusive-messages)
//! - [MIDI Tuning Standard](https://musescore.org/sites/musescore.org/files/2018-06/midituning.pdf)
use std::{collections::HashSet, fmt::Debug, iter};
use crate::{
key::PianoKey,
midi::{ChannelMessage, ChannelMessageType},
note::NoteLetter,
pitch::{Pitch, Pitched, Ratio},
tuning::KeyboardMapping,
};
// Universal System Exclusive Messages
// f0 7e <payload> f7 Non-Real Time
// f0 7f <payload> f7 Real Time
const SYSEX_START: u8 = 0xf0;
const SYSEX_NON_RT: u8 = 0x7e;
const SYSEX_RT: u8 = 0x7f;
const SYSEX_END: u8 = 0xf7;
// MIDI Tuning Standard
// 08 02 Single Note Tuning Change
// 08 07 Single Note Tuning Change with Bank Select
// 08 08 Scale/Octave Tuning, 1 byte format
// 08 09 Scale/Octave Tuning, 2 byte format
const MIDI_TUNING_STANDARD: u8 = 0x08;
const SINGLE_NOTE_TUNING_CHANGE: u8 = 0x02;
const SINGLE_NOTE_TUNING_CHANGE_WITH_BANK_SELECT: u8 = 0x07;
const SCALE_OCTAVE_TUNING_1_BYTE_FORMAT: u8 = 0x08;
const SCALE_OCTAVE_TUNING_2_BYTE_FORMAT: u8 = 0x09;
const DEVICE_ID_BROADCAST: u8 = 0x7f;
const U7_MASK: u16 = (1 << 7) - 1;
const U14_UPPER_BOUND_AS_F64: f64 = (1 << 14) as f64;
/// Properties of the generated *Single Note Tuning Change* message.
///
/// # Examples
///
/// ```
/// # use tune::mts::SingleNoteTuningChange;
/// # use tune::mts::SingleNoteTuningChangeMessage;
/// # use tune::mts::SingleNoteTuningChangeOptions;
/// # use tune::note::NoteLetter;
/// # use tune::pitch::Pitch;
/// let a4 = NoteLetter::A.in_octave(4).as_piano_key();
/// let target_pitch = Pitch::from_hz(445.0);
///
/// let tuning_change = SingleNoteTuningChange { key: a4, target_pitch };
///
/// // Use default options
/// let options = SingleNoteTuningChangeOptions::default();
///
/// let tuning_message = SingleNoteTuningChangeMessage::from_tuning_changes(
/// &options,
/// std::iter::once(tuning_change),
/// )
/// .unwrap();
///
/// assert_eq!(
/// Vec::from_iter(tuning_message.sysex_bytes()),
/// [[0xf0, 0x7f, 0x7f, 0x08, 0x02, // RT Single Note Tuning Change
/// 0, 1, // Tuning program / number of changes
/// 69, 69, 25, 5, // Tuning changes
/// 0xf7]] // Sysex end
/// );
///
/// // Use custom options
/// let options = SingleNoteTuningChangeOptions {
/// realtime: false,
/// device_id: 55,
/// tuning_program: 66,
/// with_bank_select: Some(77),
/// };
///
/// let tuning_message = SingleNoteTuningChangeMessage::from_tuning_changes(
/// &options,
/// std::iter::once(tuning_change),
/// )
/// .unwrap();
///
/// assert_eq!(
/// Vec::from_iter(tuning_message.sysex_bytes()),
/// [[0xf0, 0x7e, 55, 0x08, 0x07, // Non-RT Single Note Tuning Change with Bank Select
/// 77, 66, 1, // Tuning program / tuning bank / number of changes
/// 69, 69, 25, 5, // Tuning changes
/// 0xf7]] // Sysex end
/// );
/// ```
#[derive(Copy, Clone, Debug)]
pub struct SingleNoteTuningChangeOptions {
/// If set to true, generate a realtime SysEx message (defaults to `true`).
pub realtime: bool,
/// Specifies the device ID (defaults to broadcast/0x7f).
pub device_id: u8,
/// Specifies the tuning program to be affected (defaults to 0).
pub tuning_program: u8,
/// If given, generate a *Single Note Tuning Change with Bank Select* message.
pub with_bank_select: Option<u8>,
}
impl Default for SingleNoteTuningChangeOptions {
fn default() -> Self {
Self {
realtime: true,
device_id: DEVICE_ID_BROADCAST,
tuning_program: 0,
with_bank_select: None,
}
}
}
/// Retunes one or multiple MIDI notes using the *Single Note Tuning Change* message format.
#[derive(Clone, Debug)]
pub struct SingleNoteTuningChangeMessage {
sysex_calls: [Option<Vec<u8>>; 2],
out_of_range_notes: Vec<SingleNoteTuningChange>,
}
impl SingleNoteTuningChangeMessage {
/// Creates a [`SingleNoteTuningChangeMessage`] from the provided `tuning` and `keys`.
///
/// # Examples
///
/// ```
/// # use tune::mts::SingleNoteTuningChangeMessage;
/// # use tune::key::PianoKey;
/// # use tune::note::NoteLetter;
/// # use tune::pitch::Ratio;
/// # use tune::scala::KbmRoot;
/// # use tune::scala::Scl;
/// let scl = Scl::builder()
/// .push_ratio(Ratio::octave().divided_into_equal_steps(7))
/// .build()
/// .unwrap();
/// let kbm = KbmRoot::from(NoteLetter::D.in_octave(4)).to_kbm();
///
/// let tuning_message = SingleNoteTuningChangeMessage::from_tuning(
/// &Default::default(),
/// (scl, kbm),
/// (21..109).map(PianoKey::from_midi_number),
/// )
/// .unwrap();
///
/// assert_eq!(tuning_message.sysex_bytes().count(), 1);
/// assert_eq!(tuning_message.out_of_range_notes().len(), 13);
/// ```
pub fn from_tuning(
options: &SingleNoteTuningChangeOptions,
tuning: impl KeyboardMapping<PianoKey>,
keys: impl IntoIterator<Item = PianoKey>,
) -> Result<Self, SingleNoteTuningChangeError> {
let tuning_changes = keys.into_iter().flat_map(|key| {
tuning
.maybe_pitch_of(key)
.map(|target_pitch| SingleNoteTuningChange { key, target_pitch })
});
Self::from_tuning_changes(options, tuning_changes)
}
/// Creates a [`SingleNoteTuningChangeMessage`] from the provided `tuning_changes`.
///
/// # Examples
///
/// ```
/// # use tune::mts::SingleNoteTuningChange;
/// # use tune::mts::SingleNoteTuningChangeMessage;
/// # use tune::note::NoteLetter;
/// # use tune::pitch::Pitch;
/// let key = NoteLetter::A.in_octave(4).as_piano_key();
///
/// let good = SingleNoteTuningChange { key, target_pitch: Pitch::from_hz(445.0) };
/// let too_low = SingleNoteTuningChange { key, target_pitch: Pitch::from_hz(1.0) };
/// let too_high = SingleNoteTuningChange { key, target_pitch: Pitch::from_hz(100000.0) };
///
/// let tuning_message = SingleNoteTuningChangeMessage::from_tuning_changes(
/// &Default::default(), [good, too_low, too_high]
/// )
/// .unwrap();
///
/// assert_eq!(tuning_message.sysex_bytes().count(), 1);
/// assert_eq!(tuning_message.out_of_range_notes(), [too_low, too_high]);
/// ```
pub fn from_tuning_changes(
options: &SingleNoteTuningChangeOptions,
tuning_changes: impl IntoIterator<Item = SingleNoteTuningChange>,
) -> Result<Self, SingleNoteTuningChangeError> {
if options.device_id >= 128 {
return Err(SingleNoteTuningChangeError::DeviceIdOutOfRange);
}
if options.tuning_program >= 128 {
return Err(SingleNoteTuningChangeError::TuningProgramOutOfRange);
}
if options
.with_bank_select
.filter(|&tuning_bank| tuning_bank >= 128)
.is_some()
{
return Err(SingleNoteTuningChangeError::TuningBankNumberOutOfRange);
}
let mut sysex_tuning_list = Vec::new();
let mut num_retuned_notes = 0;
let mut out_of_range_notes = Vec::new();
for tuning_change in tuning_changes {
let approximation = tuning_change.target_pitch.find_in_tuning(());
let mut target_note = approximation.approx_value;
let mut detune_in_u14_resolution =
(approximation.deviation.as_semitones() * U14_UPPER_BOUND_AS_F64).round();
// Make sure that the detune range is [0c..100c] instead of [-50c..50c]
if detune_in_u14_resolution < 0.0 {
target_note = target_note.plus_semitones(-1);
detune_in_u14_resolution += U14_UPPER_BOUND_AS_F64;
}
if let (Some(source), Some(target)) = (
tuning_change.key.checked_midi_number(),
target_note.checked_midi_number(),
) {
let pitch_msb = (detune_in_u14_resolution as u16 >> 7) as u8;
let pitch_lsb = (detune_in_u14_resolution as u16 & U7_MASK) as u8;
sysex_tuning_list.push(source);
sysex_tuning_list.push(target);
sysex_tuning_list.push(pitch_msb);
sysex_tuning_list.push(pitch_lsb);
num_retuned_notes += 1;
} else {
out_of_range_notes.push(tuning_change);
}
if num_retuned_notes > 128 {
return Err(SingleNoteTuningChangeError::TuningChangeListTooLong);
}
}
let create_sysex = |sysex_tuning_list: &[u8]| {
let mut sysex_call = Vec::with_capacity(sysex_tuning_list.len() + 9);
sysex_call.push(SYSEX_START);
sysex_call.push(if options.realtime {
SYSEX_RT
} else {
SYSEX_NON_RT
});
sysex_call.push(options.device_id);
sysex_call.push(MIDI_TUNING_STANDARD);
sysex_call.push(if options.with_bank_select.is_some() {
SINGLE_NOTE_TUNING_CHANGE_WITH_BANK_SELECT
} else {
SINGLE_NOTE_TUNING_CHANGE
});
if let Some(with_bank_select) = options.with_bank_select {
sysex_call.push(with_bank_select);
}
sysex_call.push(options.tuning_program);
sysex_call.push((sysex_tuning_list.len() / 4).try_into().unwrap());
sysex_call.extend(sysex_tuning_list);
sysex_call.push(SYSEX_END);
sysex_call
};
let sysex_calls = if num_retuned_notes == 0 {
[None, None]
} else if num_retuned_notes < 128 {
[Some(create_sysex(&sysex_tuning_list[..])), None]
} else {
[
Some(create_sysex(&sysex_tuning_list[..256])),
Some(create_sysex(&sysex_tuning_list[256..])),
]
};
Ok(SingleNoteTuningChangeMessage {
sysex_calls,
out_of_range_notes,
})
}
/// Returns the tuning message conforming to the MIDI tuning standard.
///
/// If less than 128 notes are retuned the iterator yields a single tuning message.
/// If the number of retuned notes is 128 two messages with a batch of 64 notes are yielded.
/// If the number of retuned notes is 0 no message is yielded.
///
/// # Examples
///
/// ```
/// # use tune::mts::SingleNoteTuningChange;
/// # use tune::mts::SingleNoteTuningChangeMessage;
/// # use tune::key::PianoKey;
/// # use tune::note::Note;
/// # use tune::pitch::Pitched;
/// let create_tuning_message_with_num_changes = |num_changes| {
/// let tuning_changes = (0..num_changes).map(|midi_number| {
/// SingleNoteTuningChange {
/// key: PianoKey::from_midi_number(midi_number),
/// target_pitch: Note::from_midi_number(midi_number).pitch(),
/// }
/// });
///
/// SingleNoteTuningChangeMessage::from_tuning_changes(
/// &Default::default(),
/// tuning_changes,
/// )
/// .unwrap()
/// };
///
/// assert_eq!(create_tuning_message_with_num_changes(0).sysex_bytes().count(), 0);
/// assert_eq!(create_tuning_message_with_num_changes(127).sysex_bytes().count(), 1);
/// assert_eq!(create_tuning_message_with_num_changes(128).sysex_bytes().count(), 2);
/// ```
pub fn sysex_bytes(&self) -> impl Iterator<Item = &[u8]> {
self.sysex_calls.iter().flatten().map(Vec::as_slice)
}
/// Return notes whose target pitch is not representable by the tuning message.
pub fn out_of_range_notes(&self) -> &[SingleNoteTuningChange] {
&self.out_of_range_notes
}
}
/// Tunes the given [`PianoKey`] to the given [`Pitch`].
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct SingleNoteTuningChange {
/// The key to tune.
pub key: PianoKey,
/// The [`Pitch`] that the given key should sound in.
pub target_pitch: Pitch,
}
/// Creating a [`SingleNoteTuningChangeMessage`] failed.
#[derive(Copy, Clone, Debug)]
pub enum SingleNoteTuningChangeError {
/// The tuning change list has more than 128 elements.
///
/// Discarded values are not counted.
///
/// # Example
///
/// ```
/// # use tune::mts::SingleNoteTuningChange;
/// # use tune::mts::SingleNoteTuningChangeError;
/// # use tune::mts::SingleNoteTuningChangeMessage;
/// # use tune::key::PianoKey;
/// # use tune::note::Note;
/// # use tune::pitch::Pitched;
/// let vec_with_128_changes: Vec<_> = (0..128)
/// .map(|midi_number| {
/// SingleNoteTuningChange {
/// key: PianoKey::from_midi_number(midi_number),
/// target_pitch: Note::from_midi_number(midi_number).pitch(),
/// }
/// })
/// .collect();
///
/// let mut vec_with_129_changes = vec_with_128_changes.clone();
/// vec_with_129_changes.push({
/// SingleNoteTuningChange {
/// key: PianoKey::from_midi_number(64),
/// target_pitch: Note::from_midi_number(64).pitch(),
/// }
/// });
///
/// let mut vec_with_discarded_elements = vec_with_128_changes.clone();
/// vec_with_discarded_elements.push({
/// SingleNoteTuningChange {
/// key: PianoKey::from_midi_number(128),
/// target_pitch: Note::from_midi_number(128).pitch(),
/// }
/// });
///
/// assert!(matches!(
/// SingleNoteTuningChangeMessage::from_tuning_changes(
/// &Default::default(),
/// vec_with_128_changes,
/// ),
/// Ok(_)
/// ));
/// assert!(matches!(
/// SingleNoteTuningChangeMessage::from_tuning_changes(
/// &Default::default(),
/// vec_with_129_changes,
/// ),
/// Err(SingleNoteTuningChangeError::TuningChangeListTooLong)
/// ));
/// assert!(matches!(
/// SingleNoteTuningChangeMessage::from_tuning_changes(
/// &Default::default(),
/// vec_with_discarded_elements,
/// ),
/// Ok(_)
/// ));
/// ```
TuningChangeListTooLong,
/// The device ID is greater than 127.
///
/// # Example
///
/// ```
/// # use std::iter;
/// # use tune::mts::SingleNoteTuningChangeError;
/// # use tune::mts::SingleNoteTuningChangeMessage;
/// # use tune::mts::SingleNoteTuningChangeOptions;
/// let create_tuning_message_for_device_id = |device_id| {
/// let options = SingleNoteTuningChangeOptions {
/// device_id,
/// ..Default::default()
/// };
///
/// SingleNoteTuningChangeMessage::from_tuning_changes(&options, iter::empty())
/// };
///
/// assert!(matches!(
/// create_tuning_message_for_device_id(127),
/// Ok(_)
/// ));
/// assert!(matches!(
/// create_tuning_message_for_device_id(128),
/// Err(SingleNoteTuningChangeError::DeviceIdOutOfRange)
/// ));
/// ```
DeviceIdOutOfRange,
/// The tuning program number is greater than 127.
///
/// # Example
///
/// ```
/// # use std::iter;
/// # use tune::mts::SingleNoteTuningChangeError;
/// # use tune::mts::SingleNoteTuningChangeMessage;
/// # use tune::mts::SingleNoteTuningChangeOptions;
/// let create_tuning_message_for_program = |tuning_program| {
/// let options = SingleNoteTuningChangeOptions {
/// tuning_program,
/// ..Default::default()
/// };
///
/// SingleNoteTuningChangeMessage::from_tuning_changes(&options, iter::empty())
/// };
///
/// assert!(matches!(
/// create_tuning_message_for_program(127),
/// Ok(_)
/// ));
/// assert!(matches!(
/// create_tuning_message_for_program(128),
/// Err(SingleNoteTuningChangeError::TuningProgramOutOfRange)
/// ));
/// ```
TuningProgramOutOfRange,
/// The tuning bank number is greater than 127.
///
/// # Example
///
/// ```
/// # use std::iter;
/// # use tune::mts::SingleNoteTuningChangeError;
/// # use tune::mts::SingleNoteTuningChangeMessage;
/// # use tune::mts::SingleNoteTuningChangeOptions;
/// let create_tuning_message_with_bank_select = |tuning_bank| {
/// let options = SingleNoteTuningChangeOptions {
/// with_bank_select: Some(tuning_bank),
/// ..Default::default()
/// };
///
/// SingleNoteTuningChangeMessage::from_tuning_changes(&options, iter::empty())
/// };
///
/// assert!(matches!(
/// create_tuning_message_with_bank_select(127),
/// Ok(_)
/// ));
/// assert!(matches!(
/// create_tuning_message_with_bank_select(128),
/// Err(SingleNoteTuningChangeError::TuningBankNumberOutOfRange)
/// ));
/// ```
TuningBankNumberOutOfRange,
}
/// Properties of the generated *Scale/Octave Tuning* message.
///
/// # Examples
///
/// ```
/// # use std::collections::HashSet;
/// # use tune::mts::ScaleOctaveTuning;
/// # use tune::mts::ScaleOctaveTuningFormat;
/// # use tune::mts::ScaleOctaveTuningMessage;
/// # use tune::mts::ScaleOctaveTuningOptions;
/// # use tune::note::NoteLetter;
/// # use tune::pitch::Ratio;
/// let octave_tuning = ScaleOctaveTuning {
/// c: Ratio::from_cents(10.0),
/// csh: Ratio::from_cents(-200.0), // Will be clamped
/// d: Ratio::from_cents(200.0), // Will be clamped
/// ..Default::default()
/// };
///
/// // Use default options
/// let options = ScaleOctaveTuningOptions::default();
///
/// let tuning_message = ScaleOctaveTuningMessage::from_octave_tuning(
/// &options,
/// &octave_tuning,
/// )
/// .unwrap();
///
/// assert_eq!(
/// tuning_message.sysex_bytes(),
/// [0xf0, 0x7e, 0x7f, 0x08, 0x08, // Non-RT Scale/Octave Tuning (1-Byte)
/// 0b00000011, 0b01111111, 0b01111111, // Channel bits
/// 74, 0, 127, 64, 64, 64, 64, 64, 64, 64, 64, 64, // Tuning changes (C - B)
/// 0xf7] // Sysex end
/// );
///
/// // Use custom options
/// let options = ScaleOctaveTuningOptions {
/// realtime: true,
/// device_id: 55,
/// channels: HashSet::from([0, 3, 6, 9, 12, 15]).into(),
/// format: ScaleOctaveTuningFormat::TwoByte,
/// };
///
/// let tuning_message = ScaleOctaveTuningMessage::from_octave_tuning(
/// &options,
/// &octave_tuning,
/// )
/// .unwrap();
///
/// assert_eq!(
/// tuning_message.sysex_bytes(),
/// [0xf0, 0x7f, 55, 0x08, 0x09, // RT Scale/Octave Tuning (2-Byte)
/// 0b00000010, 0b00100100, 0b01001001, // Channel bits
/// 70, 51, 0, 0, 127, 127, 64, 0, 64, 0, 64, 0, // Tuning changes (C - F)
/// 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, 64, 0, // Tuning changes (F# - B)
/// 0xf7] // Sysex end
/// );
/// ```
#[derive(Clone, Debug)]
pub struct ScaleOctaveTuningOptions {
/// If set to true, generate a realtime SysEx message (defaults to `false`).
pub realtime: bool,
/// Specifies the device ID (defaults to broadcast/0x7f).
pub device_id: u8,
/// Specifies the channels that are affected by the tuning change (defaults to [`Channels::All`]).
pub channels: Channels,
/// Specifies whether to send a 1-byte or 2-byte message (defaults to [`ScaleOctaveTuningFormat::OneByte`]).
pub format: ScaleOctaveTuningFormat,
}
/// 1-byte or 2-byte form of the *Scale/Octave Tuning* message.
///
/// The 1-byte form supports values in the range [-64cents..63cents], the 2-byte form supports values in the range [-100cents..100cents).
#[derive(Copy, Clone, Debug)]
pub enum ScaleOctaveTuningFormat {
OneByte,
TwoByte,
}
impl Default for ScaleOctaveTuningOptions {
fn default() -> Self {
Self {
realtime: false,
channels: Channels::All,
device_id: DEVICE_ID_BROADCAST,
format: ScaleOctaveTuningFormat::OneByte,
}
}
}
/// Retunes MIDI pitch classes within an octave using the *Scale/Octave Tuning* message format.
#[derive(Clone, Debug)]
pub struct ScaleOctaveTuningMessage {
sysex_call: Vec<u8>,
}
impl ScaleOctaveTuningMessage {
/// Creates a [`ScaleOctaveTuningMessage`] from the provided `octave_tunings`.
///
/// # Examples
///
/// ```
/// # use tune::mts::ScaleOctaveTuning;
/// # use tune::mts::ScaleOctaveTuningMessage;
/// # use tune::pitch::Ratio;
/// let octave_tuning = ScaleOctaveTuning {
/// c: Ratio::from_cents(10.0),
/// ..Default::default()
/// };
///
/// let tuning_message = ScaleOctaveTuningMessage::from_octave_tuning(
/// &Default::default(),
/// &octave_tuning,
/// )
/// .unwrap();
///
/// assert_eq!(tuning_message.sysex_bytes().len(), 21);
/// ```
pub fn from_octave_tuning(
options: &ScaleOctaveTuningOptions,
octave_tuning: &ScaleOctaveTuning,
) -> Result<Self, ScaleOctaveTuningError> {
let mut sysex_call = Vec::with_capacity(21);
sysex_call.push(SYSEX_START);
sysex_call.push(if options.realtime {
SYSEX_RT
} else {
SYSEX_NON_RT
});
sysex_call.push(options.device_id);
sysex_call.push(MIDI_TUNING_STANDARD);
sysex_call.push(match options.format {
ScaleOctaveTuningFormat::OneByte => SCALE_OCTAVE_TUNING_1_BYTE_FORMAT,
ScaleOctaveTuningFormat::TwoByte => SCALE_OCTAVE_TUNING_2_BYTE_FORMAT,
});
match &options.channels {
Channels::All => {
sysex_call.push(0b0000_0011); // bits 0 to 1 = channel 15 to 16
sysex_call.push(0b0111_1111); // bits 0 to 6 = channel 8 to 14
sysex_call.push(0b0111_1111); // bits 0 to 6 = channel 1 to 7
}
Channels::Some(channels) => {
let mut encoded_channels = [0; 3];
for &channel in channels {
if channel >= 16 {
return Err(ScaleOctaveTuningError::ChannelOutOfRange);
}
let bit_position = channel % 7;
let row_to_use = channel / 7;
encoded_channels[usize::from(row_to_use)] |= 1 << bit_position;
}
sysex_call.extend(encoded_channels.iter().rev());
}
}
let pitch_bends = [
octave_tuning.c,
octave_tuning.csh,
octave_tuning.d,
octave_tuning.dsh,
octave_tuning.e,
octave_tuning.f,
octave_tuning.fsh,
octave_tuning.g,
octave_tuning.gsh,
octave_tuning.a,
octave_tuning.ash,
octave_tuning.b,
];
match options.format {
ScaleOctaveTuningFormat::OneByte => {
for pitch_bend in pitch_bends {
let value_to_write = (pitch_bend.as_cents() + 64.0).round().clamp(0.0, 127.0);
sysex_call.push(value_to_write as u8);
}
}
ScaleOctaveTuningFormat::TwoByte => {
for pitch_bend in pitch_bends {
let value_to_write = ((pitch_bend.as_semitones() + 1.0) * 8192.0)
.round()
.clamp(0.0, 16383.0) as u16;
sysex_call.push((value_to_write / 128) as u8);
sysex_call.push((value_to_write % 128) as u8);
}
}
}
sysex_call.push(SYSEX_END);
Ok(ScaleOctaveTuningMessage { sysex_call })
}
/// Returns the tuning message conforming to the MIDI tuning standard.
pub fn sysex_bytes(&self) -> &[u8] {
&self.sysex_call
}
}
/// Creating a [`ScaleOctaveTuningMessage`] failed.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ScaleOctaveTuningError {
/// A channel number exceeds the allowed range [0..16).
///
/// # Examples
///
/// ```
/// # use std::collections::HashSet;
/// # use tune::mts::ScaleOctaveTuningError;
/// # use tune::mts::ScaleOctaveTuningMessage;
/// # use tune::mts::ScaleOctaveTuningOptions;
/// // Channels 14 and 15 are valid
/// let options = ScaleOctaveTuningOptions {
/// channels: HashSet::from([14, 15]).into(),
/// ..Default::default()
/// };
///
/// assert!(matches!(
/// ScaleOctaveTuningMessage::from_octave_tuning(&options, &Default::default()),
/// Ok(_)
/// ));
///
/// // Channel 16 is invalid
/// let options = ScaleOctaveTuningOptions {
/// channels: HashSet::from([14, 15, 16]).into(),
/// ..Default::default()
/// };
///
/// assert!(matches!(
/// ScaleOctaveTuningMessage::from_octave_tuning(&options, &Default::default()),
/// Err(ScaleOctaveTuningError::ChannelOutOfRange)
/// ));
/// ```
ChannelOutOfRange,
}
/// The detuning per pitch class within an octave.
#[derive(Clone, Debug, Default)]
pub struct ScaleOctaveTuning {
pub c: Ratio,
pub csh: Ratio,
pub d: Ratio,
pub dsh: Ratio,
pub e: Ratio,
pub f: Ratio,
pub fsh: Ratio,
pub g: Ratio,
pub gsh: Ratio,
pub a: Ratio,
pub ash: Ratio,
pub b: Ratio,
}
impl ScaleOctaveTuning {
pub fn as_mut(&mut self, letter: NoteLetter) -> &mut Ratio {
match letter {
NoteLetter::C => &mut self.c,
NoteLetter::Csh => &mut self.csh,
NoteLetter::D => &mut self.d,
NoteLetter::Dsh => &mut self.dsh,
NoteLetter::E => &mut self.e,
NoteLetter::F => &mut self.f,
NoteLetter::Fsh => &mut self.fsh,
NoteLetter::G => &mut self.g,
NoteLetter::Gsh => &mut self.gsh,
NoteLetter::A => &mut self.a,
NoteLetter::Ash => &mut self.ash,
NoteLetter::B => &mut self.b,
}
}
}
/// Channels to be affected by the *Scale/Octave Tuning* message.
#[derive(Clone, Debug)]
pub enum Channels {
All,
Some(HashSet<u8>),
}
impl From<HashSet<u8>> for Channels {
fn from(channels: HashSet<u8>) -> Self {
Self::Some(channels)
}
}
impl From<u8> for Channels {
fn from(channel: u8) -> Self {
Self::Some(iter::once(channel).collect())
}
}
pub fn channel_fine_tuning(channel: u8, detuning: Ratio) -> Option<[ChannelMessage; 4]> {
const CHANNEL_FINE_TUNING_MSB: u8 = 0x00;
const CHANNEL_FINE_TUNING_LSB: u8 = 0x01;
let (value_msb, value_lsb) = ratio_to_u8s(detuning);
rpn_message_2_byte(
channel,
CHANNEL_FINE_TUNING_MSB,
CHANNEL_FINE_TUNING_LSB,
value_msb,
value_lsb,
)
}
pub fn tuning_program_change(channel: u8, tuning_program: u8) -> Option<[ChannelMessage; 3]> {
const TUNING_PROGRAM_CHANGE_MSB: u8 = 0x00;
const TUNING_PROGRAM_CHANGE_LSB: u8 = 0x03;
rpn_message_1_byte(
channel,
TUNING_PROGRAM_CHANGE_MSB,
TUNING_PROGRAM_CHANGE_LSB,
tuning_program,
)
}
pub fn tuning_bank_change(channel: u8, tuning_bank: u8) -> Option<[ChannelMessage; 3]> {
const TUNING_BANK_CHANGE_MSB: u8 = 0x00;
const TUNING_BANK_CHANGE_LSB: u8 = 0x04;
rpn_message_1_byte(
channel,
TUNING_BANK_CHANGE_MSB,
TUNING_BANK_CHANGE_LSB,
tuning_bank,
)
}
// RPN format reference: https://www.midi.org/specifications-old/item/table-3-control-change-messages-data-bytes-2
const RPN_MSB: u8 = 0x65;
const RPN_LSB: u8 = 0x64;
const DATA_ENTRY_MSB: u8 = 0x06;
const DATA_ENTRY_LSB: u8 = 0x26;
fn rpn_message_1_byte(
channel: u8,
parameter_number_msb: u8,
parameter_number_lsb: u8,
value: u8,
) -> Option<[ChannelMessage; 3]> {
Some([
ChannelMessageType::ControlChange {
controller: RPN_MSB,
value: parameter_number_msb,
}
.in_channel(channel)?,
ChannelMessageType::ControlChange {
controller: RPN_LSB,
value: parameter_number_lsb,
}
.in_channel(channel)?,
ChannelMessageType::ControlChange {
controller: DATA_ENTRY_MSB,
value,
}
.in_channel(channel)?,
])
}
fn rpn_message_2_byte(
channel: u8,
parameter_number_msb: u8,
parameter_number_lsb: u8,
value_msb: u8,
value_lsb: u8,
) -> Option<[ChannelMessage; 4]> {
Some([
ChannelMessageType::ControlChange {
controller: RPN_MSB,
value: parameter_number_msb,
}
.in_channel(channel)?,
ChannelMessageType::ControlChange {
controller: RPN_LSB,
value: parameter_number_lsb,
}
.in_channel(channel)?,
ChannelMessageType::ControlChange {
controller: DATA_ENTRY_MSB,
value: value_msb,
}
.in_channel(channel)?,
ChannelMessageType::ControlChange {
controller: DATA_ENTRY_LSB,
value: value_lsb,
}
.in_channel(channel)?,
])
}
fn ratio_to_u8s(ratio: Ratio) -> (u8, u8) {
let as_u16 = (((ratio.as_semitones() + 1.0) * 13f64.exp2()) as u16).clamp(0, 16383);
((as_u16 / 128) as u8, (as_u16 % 128) as u8)
}
#[cfg(test)]
mod test {
use crate::{
note::{Note, NoteLetter},
scala::{KbmRoot, Scl},
};
use super::*;
#[test]
fn octave_tuning() {
let test_cases: &[(&[_], _, _, _)] = &[
(&[], 0b0000_0000, 0b0000_0000, 0b0000_0000),
(&[0], 0b0000_0000, 0b0000_0000, 0b0000_0001),
(&[6], 0b0000_0000, 0b0000_0000, 0b0100_0000),
(&[7], 0b0000_0000, 0b0000_0001, 0b0000_0000),
(&[13], 0b0000_0000, 0b0100_0000, 0b0000_0000),
(&[14], 0b0000_0001, 0b0000_0000, 0b0000_0000),
(&[15], 0b0000_0010, 0b0000_0000, 0b0000_0000),
(
&[0, 2, 4, 6, 8, 10, 12, 14],
0b0000_0001,
0b0010_1010,
0b0101_0101,
),
(
&[1, 3, 5, 7, 9, 11, 13, 15],
0b0000_0010,
0b0101_0101,
0b0010_1010,
),
];
let octave_tuning = ScaleOctaveTuning {
c: Ratio::from_cents(-61.0),
csh: Ratio::from_cents(-50.0),
d: Ratio::from_cents(-39.0),
dsh: Ratio::from_cents(-28.0),
e: Ratio::from_cents(-17.0),
f: Ratio::from_cents(-6.0),
fsh: Ratio::from_cents(5.0),
g: Ratio::from_cents(16.0),
gsh: Ratio::from_cents(27.0),
a: Ratio::from_cents(38.0),
ash: Ratio::from_cents(49.0),
b: Ratio::from_cents(60.0),
};
for (channels, expected_channel_byte_1, expected_channel_byte_2, expected_channel_byte_3) in
test_cases.iter()
{
let options = ScaleOctaveTuningOptions {
device_id: 77,
channels: Channels::Some(channels.iter().cloned().collect()),
..Default::default()
};
let tuning_message =
ScaleOctaveTuningMessage::from_octave_tuning(&options, &octave_tuning).unwrap();
assert_eq!(
tuning_message.sysex_bytes(),
[
0xf0,
0x7e,
77,
0x08,
0x08,
*expected_channel_byte_1,
*expected_channel_byte_2,
*expected_channel_byte_3,
0x40 - 61,
0x40 - 50,
0x40 - 39,
0x40 - 28,
0x40 - 17,
0x40 - 6,
0x40 + 5,
0x40 + 16,
0x40 + 27,
0x40 + 38,
0x40 + 49,
0x40 + 60,
0xf7
]
);
}
}
#[test]
fn octave_tuning_default_values() {
let tuning_message =
ScaleOctaveTuningMessage::from_octave_tuning(&Default::default(), &Default::default())
.unwrap();
assert_eq!(
tuning_message.sysex_bytes(),
[
0xf0,
0x7e,
0x7f,
0x08,
0x08,
0b0000_0011,
0b0111_1111,
0b0111_1111,
64,
64,
64,
64,
64,
64,
64,
64,
64,
64,
64,
64,
0xf7
]
);
}
#[test]
fn single_note_tuning() {
let scl = Scl::builder()
.push_ratio(Ratio::octave().divided_into_equal_steps(31))
.build()
.unwrap();
let kbm = KbmRoot::from(NoteLetter::D.in_octave(4)).to_kbm();
let tuning = (scl, kbm);
let options = SingleNoteTuningChangeOptions {
device_id: 11,
tuning_program: 22,
..Default::default()
};
let single_message = SingleNoteTuningChangeMessage::from_tuning(
&options,
&tuning,
(0..127).map(PianoKey::from_midi_number),
)
.unwrap();
assert_eq!(
Vec::from_iter(single_message.sysex_bytes()),
[[
0xf0, 0x7f, 11, 0x08, 0x02, 22, 127, 0, 38, 0, 0, 1, 38, 49, 70, 2, 38, 99, 12, 3,
39, 20, 83, 4, 39, 70, 25, 5, 39, 119, 95, 6, 40, 41, 37, 7, 40, 90, 107, 8, 41,
12, 50, 9, 41, 61, 120, 10, 41, 111, 62, 11, 42, 33, 4, 12, 42, 82, 74, 13, 43, 4,
17, 14, 43, 53, 87, 15, 43, 103, 29, 16, 44, 24, 99, 17, 44, 74, 41, 18, 44, 123,
111, 19, 45, 45, 54, 20, 45, 94, 124, 21, 46, 16, 66, 22, 46, 66, 8, 23, 46, 115,
78, 24, 47, 37, 21, 25, 47, 86, 91, 26, 48, 8, 33, 27, 48, 57, 103, 28, 48, 107,
45, 29, 49, 28, 116, 30, 49, 78, 58, 31, 50, 0, 0, 32, 50, 49, 70, 33, 50, 99, 12,
34, 51, 20, 83, 35, 51, 70, 25, 36, 51, 119, 95, 37, 52, 41, 37, 38, 52, 90, 107,
39, 53, 12, 50, 40, 53, 61, 120, 41, 53, 111, 62, 42, 54, 33, 4, 43, 54, 82, 74,
44, 55, 4, 17, 45, 55, 53, 87, 46, 55, 103, 29, 47, 56, 24, 99, 48, 56, 74, 41, 49,
56, 123, 111, 50, 57, 45, 54, 51, 57, 94, 124, 52, 58, 16, 66, 53, 58, 66, 8, 54,
58, 115, 78, 55, 59, 37, 21, 56, 59, 86, 91, 57, 60, 8, 33, 58, 60, 57, 103, 59,
60, 107, 45, 60, 61, 28, 116, 61, 61, 78, 58, 62, 62, 0, 0, 63, 62, 49, 70, 64, 62,
99, 12, 65, 63, 20, 83, 66, 63, 70, 25, 67, 63, 119, 95, 68, 64, 41, 37, 69, 64,
90, 107, 70, 65, 12, 50, 71, 65, 61, 120, 72, 65, 111, 62, 73, 66, 33, 4, 74, 66,
82, 74, 75, 67, 4, 17, 76, 67, 53, 87, 77, 67, 103, 29, 78, 68, 24, 99, 79, 68, 74,
41, 80, 68, 123, 111, 81, 69, 45, 54, 82, 69, 94, 124, 83, 70, 16, 66, 84, 70, 66,
8, 85, 70, 115, 78, 86, 71, 37, 21, 87, 71, 86, 91, 88, 72, 8, 33, 89, 72, 57, 103,
90, 72, 107, 45, 91, 73, 28, 116, 92, 73, 78, 58, 93, 74, 0, 0, 94, 74, 49, 70, 95,
74, 99, 12, 96, 75, 20, 83, 97, 75, 70, 25, 98, 75, 119, 95, 99, 76, 41, 37, 100,
76, 90, 107, 101, 77, 12, 50, 102, 77, 61, 120, 103, 77, 111, 62, 104, 78, 33, 4,
105, 78, 82, 74, 106, 79, 4, 17, 107, 79, 53, 87, 108, 79, 103, 29, 109, 80, 24,
99, 110, 80, 74, 41, 111, 80, 123, 111, 112, 81, 45, 54, 113, 81, 94, 124, 114, 82,
16, 66, 115, 82, 66, 8, 116, 82, 115, 78, 117, 83, 37, 21, 118, 83, 86, 91, 119,
84, 8, 33, 120, 84, 57, 103, 121, 84, 107, 45, 122, 85, 28, 116, 123, 85, 78, 58,
124, 86, 0, 0, 125, 86, 49, 70, 126, 86, 99, 12, 0xf7
]]
);
let options = SingleNoteTuningChangeOptions {
device_id: 33,
tuning_program: 44,
..Default::default()
};
let split_message = SingleNoteTuningChangeMessage::from_tuning(
&options,
&tuning,
(0..128).map(PianoKey::from_midi_number),
)
.unwrap();
assert_eq!(
Vec::from_iter(split_message.sysex_bytes()),
[
[
0xf0, 0x7f, 33, 0x08, 0x02, 44, 64, 0, 38, 0, 0, 1, 38, 49, 70, 2, 38, 99, 12,
3, 39, 20, 83, 4, 39, 70, 25, 5, 39, 119, 95, 6, 40, 41, 37, 7, 40, 90, 107, 8,
41, 12, 50, 9, 41, 61, 120, 10, 41, 111, 62, 11, 42, 33, 4, 12, 42, 82, 74, 13,
43, 4, 17, 14, 43, 53, 87, 15, 43, 103, 29, 16, 44, 24, 99, 17, 44, 74, 41, 18,
44, 123, 111, 19, 45, 45, 54, 20, 45, 94, 124, 21, 46, 16, 66, 22, 46, 66, 8,
23, 46, 115, 78, 24, 47, 37, 21, 25, 47, 86, 91, 26, 48, 8, 33, 27, 48, 57,
103, 28, 48, 107, 45, 29, 49, 28, 116, 30, 49, 78, 58, 31, 50, 0, 0, 32, 50,
49, 70, 33, 50, 99, 12, 34, 51, 20, 83, 35, 51, 70, 25, 36, 51, 119, 95, 37,
52, 41, 37, 38, 52, 90, 107, 39, 53, 12, 50, 40, 53, 61, 120, 41, 53, 111, 62,
42, 54, 33, 4, 43, 54, 82, 74, 44, 55, 4, 17, 45, 55, 53, 87, 46, 55, 103, 29,
47, 56, 24, 99, 48, 56, 74, 41, 49, 56, 123, 111, 50, 57, 45, 54, 51, 57, 94,
124, 52, 58, 16, 66, 53, 58, 66, 8, 54, 58, 115, 78, 55, 59, 37, 21, 56, 59,
86, 91, 57, 60, 8, 33, 58, 60, 57, 103, 59, 60, 107, 45, 60, 61, 28, 116, 61,
61, 78, 58, 62, 62, 0, 0, 63, 62, 49, 70, 0xf7
],
[
0xf0, 0x7f, 33, 0x08, 0x02, 44, 64, 64, 62, 99, 12, 65, 63, 20, 83, 66, 63, 70,
25, 67, 63, 119, 95, 68, 64, 41, 37, 69, 64, 90, 107, 70, 65, 12, 50, 71, 65,
61, 120, 72, 65, 111, 62, 73, 66, 33, 4, 74, 66, 82, 74, 75, 67, 4, 17, 76, 67,
53, 87, 77, 67, 103, 29, 78, 68, 24, 99, 79, 68, 74, 41, 80, 68, 123, 111, 81,
69, 45, 54, 82, 69, 94, 124, 83, 70, 16, 66, 84, 70, 66, 8, 85, 70, 115, 78,
86, 71, 37, 21, 87, 71, 86, 91, 88, 72, 8, 33, 89, 72, 57, 103, 90, 72, 107,
45, 91, 73, 28, 116, 92, 73, 78, 58, 93, 74, 0, 0, 94, 74, 49, 70, 95, 74, 99,
12, 96, 75, 20, 83, 97, 75, 70, 25, 98, 75, 119, 95, 99, 76, 41, 37, 100, 76,
90, 107, 101, 77, 12, 50, 102, 77, 61, 120, 103, 77, 111, 62, 104, 78, 33, 4,
105, 78, 82, 74, 106, 79, 4, 17, 107, 79, 53, 87, 108, 79, 103, 29, 109, 80,
24, 99, 110, 80, 74, 41, 111, 80, 123, 111, 112, 81, 45, 54, 113, 81, 94, 124,
114, 82, 16, 66, 115, 82, 66, 8, 116, 82, 115, 78, 117, 83, 37, 21, 118, 83,
86, 91, 119, 84, 8, 33, 120, 84, 57, 103, 121, 84, 107, 45, 122, 85, 28, 116,
123, 85, 78, 58, 124, 86, 0, 0, 125, 86, 49, 70, 126, 86, 99, 12, 127, 87, 20,
83, 0xf7
]
]
);
}
#[test]
fn single_note_tuning_numerical_correctness() {
let tuning_changes = [
(11, -1.0), // Out of range
(22, -0.00004), // Out of range
(33, -0.00003), // Numerically equivalent to 0
(44, 0.0),
(55, 0.00003), // Numerically equivalent to 0
(66, 0.00004), // Smallest value above 0 => lsb = 1
(77, 31.41592), // Random number => (msb, lsb) = (53, 30)
(11, 62.83185), // Random number => (msb, lsb) = (106, 61)
(22, 68.99996), // Smallest value below 69 => lsb = 127
(33, 68.99997), // Numerically equivalent to 69
(44, 69.0),
(55, 69.00003), // Numerically equivalent to 69
(66, 69.00004), // Smallest value above 69 => lsb = 1
(77, 69.25), // 25% of a semitone => msb = 32
(11, 69.49996), // Smallest value below 69.5 => lsb = 127
(22, 69.49997), // Numerically equivalent to 69.5
(33, 69.5), // 50% of a semitone => msb = 64
(44, 69.50003), // Numerically equivalent to 69.5
(55, 69.50004), // Smallest value above 69.5 => lsb = 1
(66, 69.75), // 75% of a semitone => msb = 96
(77, 127.99996), // Smallest value below 128 => lsb = 127
(1, 127.99997), // Out of range
(11, 129.0), // Out of range
]
.iter()
.map(|&(source, target)| {
let key = PianoKey::from_midi_number(source);
let target_pitch = Note::from_midi_number(0).pitch() * Ratio::from_semitones(target);
SingleNoteTuningChange { key, target_pitch }
});
let tuning_message =
SingleNoteTuningChangeMessage::from_tuning_changes(&Default::default(), tuning_changes)
.unwrap();
assert_eq!(
Vec::from_iter(tuning_message.sysex_bytes()),
[[
0xf0, 0x7f, 0x7f, 0x08, 0x02, 0, 19, 33, 0, 0, 0, 44, 0, 0, 0, 55, 0, 0, 0, 66, 0,
0, 1, 77, 31, 53, 30, 11, 62, 106, 61, 22, 68, 127, 127, 33, 69, 0, 0, 44, 69, 0,
0, 55, 69, 0, 0, 66, 69, 0, 1, 77, 69, 32, 0, 11, 69, 63, 127, 22, 69, 64, 0, 33,
69, 64, 0, 44, 69, 64, 0, 55, 69, 64, 1, 66, 69, 96, 0, 77, 127, 127, 127, 0xf7,
]]
);
assert_eq!(tuning_message.out_of_range_notes().len(), 4);
}
}