pub use truce_utils::midi::*;
use crate::events::EventBody;
#[must_use]
pub fn decode_short_message(status: u8, data1: u8, data2: u8) -> Option<EventBody> {
let channel = status & 0x0F;
let d1 = data1 & 0x7F;
let d2 = data2 & 0x7F;
match status & 0xF0 {
0x90 if d2 > 0 => Some(EventBody::NoteOn {
group: 0,
channel,
note: d1,
velocity: d2,
}),
0x90 => Some(EventBody::NoteOff {
group: 0,
channel,
note: d1,
velocity: 0,
}),
0x80 => Some(EventBody::NoteOff {
group: 0,
channel,
note: d1,
velocity: d2,
}),
0xA0 => Some(EventBody::Aftertouch {
group: 0,
channel,
note: d1,
pressure: d2,
}),
0xB0 => Some(EventBody::ControlChange {
group: 0,
channel,
cc: d1,
value: d2,
}),
0xC0 => Some(EventBody::ProgramChange {
group: 0,
channel,
program: d1,
}),
0xD0 => Some(EventBody::ChannelPressure {
group: 0,
channel,
pressure: d1,
}),
0xE0 => Some(EventBody::PitchBend {
group: 0,
channel,
value: pitch_bend_from_bytes(d1, d2),
}),
_ => None,
}
}
#[must_use]
pub fn parse_midi1(group: u8, bytes: &[u8]) -> Option<EventBody> {
if bytes.is_empty() {
return None;
}
let status = bytes[0];
let (data1, data2) = match status & 0xF0 {
0xC0 | 0xD0 if bytes.len() >= 2 => (bytes[1], 0),
0x80..=0xB0 | 0xE0 if bytes.len() >= 3 => (bytes[1], bytes[2]),
_ => return None,
};
let mut event = decode_short_message(status, data1, data2)?;
rewrite_group(&mut event, group);
Some(event)
}
fn rewrite_group(event: &mut EventBody, new_group: u8) {
match event {
EventBody::NoteOn { group, .. }
| EventBody::NoteOff { group, .. }
| EventBody::Aftertouch { group, .. }
| EventBody::ChannelPressure { group, .. }
| EventBody::ControlChange { group, .. }
| EventBody::PitchBend { group, .. }
| EventBody::ProgramChange { group, .. } => *group = new_group,
_ => {}
}
}
#[must_use]
pub fn event_to_midi1(event: &EventBody) -> Option<(usize, [u8; 3])> {
match event {
EventBody::NoteOn {
channel,
note,
velocity,
..
} => Some((3, [0x90 | (channel & 0x0F), *note, *velocity])),
EventBody::NoteOff {
channel,
note,
velocity,
..
} => Some((3, [0x80 | (channel & 0x0F), *note, *velocity])),
EventBody::Aftertouch {
channel,
note,
pressure,
..
} => Some((3, [0xA0 | (channel & 0x0F), *note, *pressure])),
EventBody::ControlChange {
channel, cc, value, ..
} => Some((3, [0xB0 | (channel & 0x0F), *cc, *value])),
EventBody::PitchBend { channel, value, .. } => {
let (lsb, msb) = pitch_bend_to_bytes(*value);
Some((3, [0xE0 | (channel & 0x0F), lsb, msb]))
}
EventBody::ChannelPressure {
channel, pressure, ..
} => Some((2, [0xD0 | (channel & 0x0F), *pressure, 0])),
EventBody::ProgramChange {
channel, program, ..
} => Some((2, [0xC0 | (channel & 0x0F), *program, 0])),
_ => None,
}
}
#[must_use]
#[allow(clippy::cast_possible_truncation)]
pub fn downconvert_to_midi1(body: &EventBody) -> Option<EventBody> {
let hi7_16 = |v: u16| (v >> 9) as u8;
let hi7_32 = |v: u32| (v >> 25) as u8;
let hi14_32 = |v: u32| (v >> 18) as u16 & 0x3FFF;
Some(match *body {
EventBody::NoteOn2 {
group,
channel,
note,
velocity,
..
} => EventBody::NoteOn {
group,
channel,
note,
velocity: hi7_16(velocity).max(1),
},
EventBody::NoteOff2 {
group,
channel,
note,
velocity,
..
} => EventBody::NoteOff {
group,
channel,
note,
velocity: hi7_16(velocity),
},
EventBody::PolyPressure2 {
group,
channel,
note,
pressure,
} => EventBody::Aftertouch {
group,
channel,
note,
pressure: hi7_32(pressure),
},
EventBody::PerNoteCC {
registered: false, ..
} => return None,
EventBody::ControlChange2 { cc, .. } | EventBody::PerNoteCC { cc, .. } if cc >= 128 => {
return None;
}
EventBody::ControlChange2 {
group,
channel,
cc,
value,
}
| EventBody::PerNoteCC {
group,
channel,
cc,
value,
..
} => EventBody::ControlChange {
group,
channel,
cc,
value: hi7_32(value),
},
EventBody::ChannelPressure2 {
group,
channel,
pressure,
} => EventBody::ChannelPressure {
group,
channel,
pressure: hi7_32(pressure),
},
EventBody::PitchBend2 {
group,
channel,
value,
}
| EventBody::PerNotePitchBend {
group,
channel,
value,
..
} => EventBody::PitchBend {
group,
channel,
value: hi14_32(value),
},
EventBody::ProgramChange2 {
group,
channel,
program,
..
} => EventBody::ProgramChange {
group,
channel,
program,
},
_ => return None,
})
}
fn upscale(src: u32, src_bits: u32, dst_bits: u32) -> u32 {
let scale_bits = dst_bits - src_bits;
let shifted = src << scale_bits;
let center = 1u32 << (src_bits - 1);
if src <= center {
return shifted;
}
let repeat_bits = src_bits - 1;
let mut repeat = src & ((1 << repeat_bits) - 1);
if scale_bits > repeat_bits {
repeat <<= scale_bits - repeat_bits;
} else {
repeat >>= repeat_bits - scale_bits;
}
let mut out = shifted;
while repeat != 0 {
out |= repeat;
repeat >>= repeat_bits;
}
out
}
#[must_use]
pub fn upscale_7_to_16(v: u8) -> u16 {
#[allow(clippy::cast_possible_truncation)]
let out = upscale(u32::from(v), 7, 16) as u16;
out
}
#[must_use]
pub fn upscale_7_to_32(v: u8) -> u32 {
upscale(u32::from(v), 7, 32)
}
#[must_use]
pub fn upscale_14_to_32(v: u16) -> u32 {
upscale(u32::from(v), 14, 32)
}
#[must_use]
pub fn upconvert_to_midi2(body: &EventBody) -> Option<EventBody> {
Some(match *body {
EventBody::NoteOn {
group,
channel,
note,
velocity: 0,
} => EventBody::NoteOff2 {
group,
channel,
note,
velocity: 0x8000,
attribute_type: 0,
attribute: 0,
},
EventBody::NoteOn {
group,
channel,
note,
velocity,
} => EventBody::NoteOn2 {
group,
channel,
note,
velocity: upscale_7_to_16(velocity),
attribute_type: 0,
attribute: 0,
},
EventBody::NoteOff {
group,
channel,
note,
velocity,
} => EventBody::NoteOff2 {
group,
channel,
note,
velocity: upscale_7_to_16(velocity),
attribute_type: 0,
attribute: 0,
},
EventBody::Aftertouch {
group,
channel,
note,
pressure,
} => EventBody::PolyPressure2 {
group,
channel,
note,
pressure: upscale_7_to_32(pressure),
},
EventBody::ControlChange {
group,
channel,
cc,
value,
} => EventBody::ControlChange2 {
group,
channel,
cc,
value: upscale_7_to_32(value),
},
EventBody::ChannelPressure {
group,
channel,
pressure,
} => EventBody::ChannelPressure2 {
group,
channel,
pressure: upscale_7_to_32(pressure),
},
EventBody::PitchBend {
group,
channel,
value,
} => EventBody::PitchBend2 {
group,
channel,
value: upscale_14_to_32(value),
},
EventBody::ProgramChange {
group,
channel,
program,
} => EventBody::ProgramChange2 {
group,
channel,
program,
bank: None,
},
_ => return None,
})
}
#[must_use]
pub fn route_midi_port(port: u8, count: u8) -> u8 {
if port < count { port } else { 0 }
}
const PER_NOTE_BEND_CENTER: f64 = 2_147_483_648.0;
pub const PER_NOTE_TUNING_SEMITONES: f64 = 48.0;
pub const PER_NOTE_VOLUME_MAX_GAIN: f64 = 4.0;
#[must_use]
pub fn per_note_bend_semitones(v: u32) -> f64 {
((f64::from(v) - PER_NOTE_BEND_CENTER) / PER_NOTE_BEND_CENTER) * PER_NOTE_TUNING_SEMITONES
}
#[must_use]
pub fn per_note_bend_from_semitones(semis: f64) -> u32 {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let v = (PER_NOTE_BEND_CENTER + (semis / PER_NOTE_TUNING_SEMITONES) * PER_NOTE_BEND_CENTER)
.clamp(0.0, f64::from(u32::MAX)) as u32;
v
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_note_on() {
let bytes = [0x90, 60, 100];
let event = parse_midi1(0, &bytes).unwrap();
let (len, back) = event_to_midi1(&event).unwrap();
assert_eq!(len, 3);
assert_eq!(back, [0x90, 60, 100]);
}
#[test]
fn downconvert_note_on_2_keeps_high_bits() {
let out = downconvert_to_midi1(&EventBody::NoteOn2 {
group: 2,
channel: 3,
note: 60,
velocity: 0xFFFF,
attribute_type: 0,
attribute: 0,
});
assert!(matches!(
out,
Some(EventBody::NoteOn {
group: 2,
channel: 3,
note: 60,
velocity: 127,
})
));
}
#[test]
fn downconvert_keeps_live_note_off_the_note_off_boundary() {
for velocity2 in [0u16, 1] {
let Some(EventBody::NoteOn { velocity, .. }) =
downconvert_to_midi1(&EventBody::NoteOn2 {
group: 0,
channel: 0,
note: 60,
velocity: velocity2,
attribute_type: 0,
attribute: 0,
})
else {
panic!("expected NoteOn");
};
assert_eq!(velocity, 1);
}
}
#[test]
fn downconvert_per_note_collapses_to_channel_cc() {
let out = downconvert_to_midi1(&EventBody::PerNoteCC {
group: 0,
channel: 4,
note: 60,
cc: 74,
value: u32::MAX,
registered: true,
});
assert!(matches!(
out,
Some(EventBody::ControlChange {
channel: 4,
cc: 74,
value: 127,
..
})
));
}
#[test]
fn downconvert_drops_out_of_range_per_note_cc() {
for cc in [128u8, 200, 255] {
assert!(
downconvert_to_midi1(&EventBody::PerNoteCC {
group: 0,
channel: 3,
note: 60,
cc,
value: u32::MAX,
registered: true,
})
.is_none(),
"per-note CC index {cc} must drop, not emit a corrupt data byte"
);
}
assert!(matches!(
downconvert_to_midi1(&EventBody::PerNoteCC {
group: 0,
channel: 3,
note: 60,
cc: 127,
value: 0,
registered: true,
}),
Some(EventBody::ControlChange { cc: 127, .. })
));
}
#[test]
fn downconvert_returns_none_for_1_0_and_non_cv() {
assert!(
downconvert_to_midi1(&EventBody::NoteOn {
group: 0,
channel: 0,
note: 60,
velocity: 100,
})
.is_none()
);
assert!(downconvert_to_midi1(&EventBody::ParamChange { id: 0, value: 0.0 }).is_none());
}
#[test]
fn round_trip_pitch_bend_center() {
let bytes = [0xE0, 0x00, 0x40]; let event = parse_midi1(0, &bytes).unwrap();
if let EventBody::PitchBend { value, .. } = event {
assert_eq!(value, 8192);
} else {
panic!("expected PitchBend");
}
}
#[test]
fn note_on_zero_velocity_is_note_off() {
let bytes = [0x90, 60, 0];
let event = parse_midi1(0, &bytes).unwrap();
assert!(matches!(event, EventBody::NoteOff { .. }));
}
#[test]
fn channel_masked_on_encode() {
let event = EventBody::NoteOn {
group: 0,
channel: 64, note: 60,
velocity: 100,
};
let (_len, bytes) = event_to_midi1(&event).unwrap();
assert_eq!(bytes[0], 0x90);
}
#[test]
fn group_propagated_through_parse() {
let event = parse_midi1(7, &[0x90, 60, 100]).unwrap();
if let EventBody::NoteOn { group, .. } = event {
assert_eq!(group, 7);
} else {
panic!("expected NoteOn");
}
}
#[test]
fn decode_program_change() {
let event = decode_short_message(0xC3, 42, 0).unwrap();
if let EventBody::ProgramChange {
channel, program, ..
} = event
{
assert_eq!(channel, 3);
assert_eq!(program, 42);
} else {
panic!("expected ProgramChange, got {event:?}");
}
}
#[test]
fn decode_channel_pressure() {
let event = decode_short_message(0xD5, 96, 0).unwrap();
if let EventBody::ChannelPressure {
channel, pressure, ..
} = event
{
assert_eq!(channel, 5);
assert_eq!(pressure, 96);
} else {
panic!("expected ChannelPressure, got {event:?}");
}
}
#[test]
fn decode_short_message_unknown_status_returns_none() {
assert!(decode_short_message(0xF0, 0, 0).is_none());
assert!(decode_short_message(0xF8, 0, 0).is_none());
}
#[test]
fn decode_short_message_strips_data_high_bit() {
let event = decode_short_message(0xB0, 0xFF, 0xFF).unwrap();
if let EventBody::ControlChange { cc, value, .. } = event {
assert_eq!(cc, 0x7F);
assert_eq!(value, 0x7F);
} else {
panic!("expected ControlChange, got {event:?}");
}
}
#[test]
fn upconvert_upscales_center_and_max_exactly() {
let up = |velocity| match upconvert_to_midi2(&EventBody::NoteOn {
group: 0,
channel: 0,
note: 60,
velocity,
}) {
Some(EventBody::NoteOn2 { velocity, .. }) => velocity,
other => panic!("expected NoteOn2, got {other:?}"),
};
assert_eq!(up(127), u16::MAX);
assert_eq!(up(64), 0x8000);
assert_eq!(up(1), 1 << 9);
match upconvert_to_midi2(&EventBody::PitchBend {
group: 0,
channel: 0,
value: 0x3FFF,
}) {
Some(EventBody::PitchBend2 { value, .. }) => assert_eq!(value, u32::MAX),
other => panic!("expected PitchBend2, got {other:?}"),
}
match upconvert_to_midi2(&EventBody::PitchBend {
group: 0,
channel: 0,
value: 0x2000,
}) {
Some(EventBody::PitchBend2 { value, .. }) => assert_eq!(value, 0x8000_0000),
other => panic!("expected PitchBend2, got {other:?}"),
}
}
#[test]
fn upconvert_round_trips_through_downconvert() {
let bodies = [
EventBody::NoteOn {
group: 0,
channel: 3,
note: 60,
velocity: 100,
},
EventBody::NoteOff {
group: 0,
channel: 3,
note: 60,
velocity: 40,
},
EventBody::ControlChange {
group: 0,
channel: 1,
cc: 74,
value: 99,
},
EventBody::PitchBend {
group: 0,
channel: 0,
value: 12345,
},
EventBody::ChannelPressure {
group: 0,
channel: 9,
pressure: 77,
},
EventBody::Aftertouch {
group: 0,
channel: 2,
note: 61,
pressure: 5,
},
];
for body in bodies {
let wide = upconvert_to_midi2(&body).expect("channel voice widens");
assert_eq!(downconvert_to_midi1(&wide), Some(body));
}
}
#[test]
fn upconvert_maps_velocity_zero_note_on_to_note_off() {
match upconvert_to_midi2(&EventBody::NoteOn {
group: 0,
channel: 0,
note: 60,
velocity: 0,
}) {
Some(EventBody::NoteOff2 { velocity, .. }) => assert_eq!(velocity, 0x8000),
other => panic!("expected NoteOff2, got {other:?}"),
}
assert!(
upconvert_to_midi2(&EventBody::NoteOn2 {
group: 0,
channel: 0,
note: 60,
velocity: 1,
attribute_type: 0,
attribute: 0,
})
.is_none()
);
}
#[test]
fn out_of_range_port_routes_to_zero() {
assert_eq!(route_midi_port(1, 2), 1); assert_eq!(route_midi_port(2, 2), 0); assert_eq!(route_midi_port(0, 0), 0); }
#[test]
fn upscale_preserves_center_and_endpoints() {
assert_eq!(upscale_14_to_32(0x2000), 0x8000_0000); assert_eq!(upscale_14_to_32(0x3FFF), u32::MAX);
assert_eq!(upscale_14_to_32(0), 0);
assert_eq!(upscale_7_to_16(64), 0x8000);
assert_eq!(upscale_7_to_32(127), u32::MAX);
}
#[test]
fn assignable_per_note_cc_has_no_midi1_form() {
let assignable = EventBody::PerNoteCC {
group: 0,
channel: 0,
note: 60,
cc: 7,
value: u32::MAX,
registered: false,
};
assert_eq!(downconvert_to_midi1(&assignable), None);
let registered = EventBody::PerNoteCC {
group: 0,
channel: 0,
note: 60,
cc: 7,
value: u32::MAX,
registered: true,
};
assert!(matches!(
downconvert_to_midi1(®istered),
Some(EventBody::ControlChange { cc: 7, .. })
));
}
#[test]
fn per_note_bend_semitone_round_trip() {
assert!(per_note_bend_semitones(0x8000_0000).abs() < 1e-9);
assert_eq!(per_note_bend_from_semitones(0.0), 0x8000_0000);
assert_eq!(per_note_bend_from_semitones(-PER_NOTE_TUNING_SEMITONES), 0);
assert_eq!(
per_note_bend_from_semitones(PER_NOTE_TUNING_SEMITONES),
u32::MAX
);
assert!((per_note_bend_semitones(u32::MAX) - PER_NOTE_TUNING_SEMITONES).abs() < 1e-6);
assert_eq!(per_note_bend_from_semitones(-120.0), 0);
assert_eq!(per_note_bend_from_semitones(120.0), u32::MAX);
let wire = per_note_bend_from_semitones(12.0);
assert!((per_note_bend_semitones(wire) - 12.0).abs() < 1e-6);
}
}