use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MidiMessage {
NoteOff { channel: u8, key: u8, velocity: u8 },
NoteOn { channel: u8, key: u8, velocity: u8 },
PolyPressure { channel: u8, key: u8, pressure: u8 },
ControlChange {
channel: u8,
controller: u8,
value: u8,
},
ProgramChange { channel: u8, program: u8 },
ChannelPressure { channel: u8, pressure: u8 },
PitchBend {
channel: u8,
value: u16,
},
SysEx(Vec<u8>),
Meta(MetaEvent),
MtcQuarterFrame(u8),
SongPosition(u16),
SongSelect(u8),
TuneRequest,
TimingClock,
Start,
Continue,
Stop,
ActiveSensing,
SystemReset,
}
impl MidiMessage {
pub fn note_on(channel: u8, key: u8, velocity: u8) -> Self {
MidiMessage::NoteOn {
channel: channel & 0x0F,
key: key & 0x7F,
velocity: velocity & 0x7F,
}
}
pub fn note_off(channel: u8, key: u8, velocity: u8) -> Self {
MidiMessage::NoteOff {
channel: channel & 0x0F,
key: key & 0x7F,
velocity: velocity & 0x7F,
}
}
pub fn control_change(channel: u8, controller: u8, value: u8) -> Self {
MidiMessage::ControlChange {
channel: channel & 0x0F,
controller: controller & 0x7F,
value: value & 0x7F,
}
}
pub fn program_change(channel: u8, program: u8) -> Self {
MidiMessage::ProgramChange {
channel: channel & 0x0F,
program: program & 0x7F,
}
}
pub fn pitch_bend(channel: u8, value: u16) -> Self {
MidiMessage::PitchBend {
channel: channel & 0x0F,
value: value & 0x3FFF,
}
}
pub fn pitch_bend_signed(channel: u8, value: i16) -> Self {
let unsigned = (value as i32 + 0x2000).clamp(0, 0x3FFF) as u16;
Self::pitch_bend(channel, unsigned)
}
pub fn make_sustain(channel: u8, value: u8) -> Self {
Self::control_change(channel, 64, value)
}
pub fn make_sustain_on(channel: u8) -> Self {
Self::make_sustain(channel, 127)
}
pub fn make_sustain_off(channel: u8) -> Self {
Self::make_sustain(channel, 0)
}
pub fn make_soft(channel: u8, value: u8) -> Self {
Self::control_change(channel, 67, value)
}
pub fn make_soft_on(channel: u8) -> Self {
Self::make_soft(channel, 127)
}
pub fn make_soft_off(channel: u8) -> Self {
Self::make_soft(channel, 0)
}
pub fn make_patch_change(channel: u8, program: u8) -> Self {
Self::program_change(channel, program)
}
pub fn make_timbre(channel: u8, program: u8) -> Self {
Self::program_change(channel, program)
}
pub fn channel(&self) -> Option<u8> {
match self {
MidiMessage::NoteOff { channel, .. }
| MidiMessage::NoteOn { channel, .. }
| MidiMessage::PolyPressure { channel, .. }
| MidiMessage::ControlChange { channel, .. }
| MidiMessage::ProgramChange { channel, .. }
| MidiMessage::ChannelPressure { channel, .. }
| MidiMessage::PitchBend { channel, .. } => Some(*channel),
_ => None,
}
}
pub fn is_note_on(&self) -> bool {
matches!(self, MidiMessage::NoteOn { velocity, .. } if *velocity > 0)
}
pub fn is_note_off(&self) -> bool {
matches!(
self,
MidiMessage::NoteOff { .. } | MidiMessage::NoteOn { velocity: 0, .. }
)
}
pub fn is_channel_message(&self) -> bool {
self.channel().is_some()
}
pub fn is_system_message(&self) -> bool {
matches!(
self,
MidiMessage::SysEx(_)
| MidiMessage::MtcQuarterFrame(_)
| MidiMessage::SongPosition(_)
| MidiMessage::SongSelect(_)
| MidiMessage::TuneRequest
| MidiMessage::TimingClock
| MidiMessage::Start
| MidiMessage::Continue
| MidiMessage::Stop
| MidiMessage::ActiveSensing
| MidiMessage::SystemReset
)
}
pub fn is_meta(&self) -> bool {
matches!(self, MidiMessage::Meta(_))
}
pub fn is_note(&self) -> bool {
matches!(
self,
MidiMessage::NoteOn { .. } | MidiMessage::NoteOff { .. }
)
}
pub fn is_controller(&self) -> bool {
matches!(self, MidiMessage::ControlChange { .. })
}
pub fn get_controller_number(&self) -> Option<u8> {
match self {
MidiMessage::ControlChange { controller, .. } => Some(*controller),
_ => None,
}
}
pub fn get_controller_value(&self) -> Option<u8> {
match self {
MidiMessage::ControlChange { value, .. } => Some(*value),
_ => None,
}
}
pub fn is_sustain(&self) -> bool {
self.get_controller_number() == Some(64)
}
pub fn is_sustain_on(&self) -> bool {
self.is_sustain() && self.get_controller_value().unwrap_or(0) >= 64
}
pub fn is_sustain_off(&self) -> bool {
self.is_sustain() && self.get_controller_value().unwrap_or(0) < 64
}
pub fn is_soft(&self) -> bool {
self.get_controller_number() == Some(67)
}
pub fn is_soft_on(&self) -> bool {
self.is_soft() && self.get_controller_value().unwrap_or(0) >= 64
}
pub fn is_soft_off(&self) -> bool {
self.is_soft() && self.get_controller_value().unwrap_or(0) < 64
}
pub fn is_patch_change(&self) -> bool {
matches!(self, MidiMessage::ProgramChange { .. })
}
pub fn is_timbre(&self) -> bool {
self.is_patch_change()
}
pub fn is_pressure(&self) -> bool {
matches!(
self,
MidiMessage::PolyPressure { .. } | MidiMessage::ChannelPressure { .. }
)
}
pub fn is_aftertouch(&self) -> bool {
self.is_pressure()
}
pub fn is_pitchbend(&self) -> bool {
matches!(self, MidiMessage::PitchBend { .. })
}
pub fn status_byte(&self) -> Option<u8> {
match self {
MidiMessage::NoteOff { channel, .. } => Some(0x80 | channel),
MidiMessage::NoteOn { channel, .. } => Some(0x90 | channel),
MidiMessage::PolyPressure { channel, .. } => Some(0xA0 | channel),
MidiMessage::ControlChange { channel, .. } => Some(0xB0 | channel),
MidiMessage::ProgramChange { channel, .. } => Some(0xC0 | channel),
MidiMessage::ChannelPressure { channel, .. } => Some(0xD0 | channel),
MidiMessage::PitchBend { channel, .. } => Some(0xE0 | channel),
MidiMessage::SysEx(_) => Some(0xF0),
MidiMessage::MtcQuarterFrame(_) => Some(0xF1),
MidiMessage::SongPosition(_) => Some(0xF2),
MidiMessage::SongSelect(_) => Some(0xF3),
MidiMessage::TuneRequest => Some(0xF6),
MidiMessage::TimingClock => Some(0xF8),
MidiMessage::Start => Some(0xFA),
MidiMessage::Continue => Some(0xFB),
MidiMessage::Stop => Some(0xFC),
MidiMessage::ActiveSensing => Some(0xFE),
MidiMessage::SystemReset => Some(0xFF),
MidiMessage::Meta(_) => Some(0xFF), }
}
pub fn to_bytes(&self) -> Vec<u8> {
match self {
MidiMessage::NoteOff {
channel,
key,
velocity,
} => vec![0x80 | channel, *key, *velocity],
MidiMessage::NoteOn {
channel,
key,
velocity,
} => vec![0x90 | channel, *key, *velocity],
MidiMessage::PolyPressure {
channel,
key,
pressure,
} => vec![0xA0 | channel, *key, *pressure],
MidiMessage::ControlChange {
channel,
controller,
value,
} => vec![0xB0 | channel, *controller, *value],
MidiMessage::ProgramChange { channel, program } => vec![0xC0 | channel, *program],
MidiMessage::ChannelPressure { channel, pressure } => vec![0xD0 | channel, *pressure],
MidiMessage::PitchBend { channel, value } => {
vec![0xE0 | channel, (*value & 0x7F) as u8, (*value >> 7) as u8]
}
MidiMessage::SysEx(data) => {
let mut bytes = vec![0xF0];
bytes.extend(data);
bytes.push(0xF7);
bytes
}
MidiMessage::MtcQuarterFrame(data) => vec![0xF1, *data],
MidiMessage::SongPosition(pos) => {
vec![0xF2, (*pos & 0x7F) as u8, (*pos >> 7) as u8]
}
MidiMessage::SongSelect(song) => vec![0xF3, *song],
MidiMessage::TuneRequest => vec![0xF6],
MidiMessage::TimingClock => vec![0xF8],
MidiMessage::Start => vec![0xFA],
MidiMessage::Continue => vec![0xFB],
MidiMessage::Stop => vec![0xFC],
MidiMessage::ActiveSensing => vec![0xFE],
MidiMessage::SystemReset => vec![0xFF],
MidiMessage::Meta(meta) => meta.to_bytes(),
}
}
pub fn from_bytes(data: &[u8]) -> Option<(MidiMessage, usize)> {
if data.is_empty() {
return None;
}
let status = data[0];
let channel = status & 0x0F;
match status & 0xF0 {
0x80 if data.len() >= 3 => Some((
MidiMessage::NoteOff {
channel,
key: data[1],
velocity: data[2],
},
3,
)),
0x90 if data.len() >= 3 => Some((
MidiMessage::NoteOn {
channel,
key: data[1],
velocity: data[2],
},
3,
)),
0xA0 if data.len() >= 3 => Some((
MidiMessage::PolyPressure {
channel,
key: data[1],
pressure: data[2],
},
3,
)),
0xB0 if data.len() >= 3 => Some((
MidiMessage::ControlChange {
channel,
controller: data[1],
value: data[2],
},
3,
)),
0xC0 if data.len() >= 2 => Some((
MidiMessage::ProgramChange {
channel,
program: data[1],
},
2,
)),
0xD0 if data.len() >= 2 => Some((
MidiMessage::ChannelPressure {
channel,
pressure: data[1],
},
2,
)),
0xE0 if data.len() >= 3 => Some((
MidiMessage::PitchBend {
channel,
value: (data[1] as u16) | ((data[2] as u16) << 7),
},
3,
)),
0xF0 => match status {
0xF0 => {
data.iter()
.position(|&b| b == 0xF7)
.map(|end| (MidiMessage::SysEx(data[1..end].to_vec()), end + 1))
}
0xF1 if data.len() >= 2 => Some((MidiMessage::MtcQuarterFrame(data[1]), 2)),
0xF2 if data.len() >= 3 => Some((
MidiMessage::SongPosition((data[1] as u16) | ((data[2] as u16) << 7)),
3,
)),
0xF3 if data.len() >= 2 => Some((MidiMessage::SongSelect(data[1]), 2)),
0xF6 => Some((MidiMessage::TuneRequest, 1)),
0xF8 => Some((MidiMessage::TimingClock, 1)),
0xFA => Some((MidiMessage::Start, 1)),
0xFB => Some((MidiMessage::Continue, 1)),
0xFC => Some((MidiMessage::Stop, 1)),
0xFE => Some((MidiMessage::ActiveSensing, 1)),
0xFF => {
if data.len() >= 2 && data[1] < 0x80 {
MetaEvent::from_bytes(&data[1..])
.map(|(meta, len)| (MidiMessage::Meta(meta), len + 1))
} else {
Some((MidiMessage::SystemReset, 1))
}
}
_ => None,
},
_ => None,
}
}
}
pub fn frequency_to_semitones(frequency_hz: f64) -> f64 {
69.0 + 12.0 * (frequency_hz / 440.0).log2()
}
impl fmt::Display for MidiMessage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MidiMessage::NoteOff {
channel,
key,
velocity,
} => {
write!(f, "NoteOff(ch={}, key={}, vel={})", channel, key, velocity)
}
MidiMessage::NoteOn {
channel,
key,
velocity,
} => {
write!(f, "NoteOn(ch={}, key={}, vel={})", channel, key, velocity)
}
MidiMessage::ControlChange {
channel,
controller,
value,
} => write!(f, "CC(ch={}, cc={}, val={})", channel, controller, value),
MidiMessage::ProgramChange { channel, program } => {
write!(f, "PC(ch={}, prog={})", channel, program)
}
MidiMessage::PitchBend { channel, value } => {
write!(f, "PitchBend(ch={}, val={})", channel, value)
}
MidiMessage::Meta(meta) => write!(f, "{}", meta),
_ => write!(f, "{:?}", self),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MetaEvent {
SequenceNumber(u16),
Text(String),
Copyright(String),
TrackName(String),
InstrumentName(String),
Lyric(String),
Marker(String),
CuePoint(String),
ProgramName(String),
DeviceName(String),
ChannelPrefix(u8),
MidiPort(u8),
EndOfTrack,
Tempo(u32),
SmpteOffset {
hours: u8,
minutes: u8,
seconds: u8,
frames: u8,
subframes: u8,
},
TimeSignature {
numerator: u8,
denominator_power: u8, clocks_per_click: u8,
notated_32nd_per_quarter: u8,
},
KeySignature {
sharps_flats: i8,
minor: bool,
},
SequencerSpecific(Vec<u8>),
Unknown { type_: u8, data: Vec<u8> },
}
impl MetaEvent {
pub fn tempo_from_bpm(bpm: f64) -> Self {
let microseconds = (60_000_000.0 / bpm).round() as u32;
MetaEvent::Tempo(microseconds)
}
pub fn tempo_to_bpm(&self) -> Option<f64> {
if let MetaEvent::Tempo(us) = self {
Some(60_000_000.0 / *us as f64)
} else {
None
}
}
pub fn tempo_microseconds(&self) -> Option<u32> {
match self {
MetaEvent::Tempo(us) => Some(*us),
_ => None,
}
}
pub fn tempo_seconds(&self) -> Option<f64> {
self.tempo_microseconds().map(|us| us as f64 / 1_000_000.0)
}
pub fn tempo_ticks_per_second(&self, ticks_per_quarter: u16) -> Option<f64> {
self.tempo_seconds()
.map(|spq| ticks_per_quarter as f64 / spq)
}
pub fn tempo_seconds_per_tick(&self, ticks_per_quarter: u16) -> Option<f64> {
self.tempo_seconds()
.map(|spq| spq / ticks_per_quarter as f64)
}
pub fn time_signature(numerator: u8, denominator: u8) -> Self {
let denominator_power = (denominator as f64).log2() as u8;
MetaEvent::TimeSignature {
numerator,
denominator_power,
clocks_per_click: 24,
notated_32nd_per_quarter: 8,
}
}
pub fn compound_time_signature(numerator: u8, denominator: u8) -> Self {
let denominator_power = (denominator as f64).log2() as u8;
MetaEvent::TimeSignature {
numerator,
denominator_power,
clocks_per_click: 36,
notated_32nd_per_quarter: 8,
}
}
pub fn time_signature_denominator(&self) -> Option<u8> {
if let MetaEvent::TimeSignature {
denominator_power, ..
} = self
{
Some(1 << denominator_power)
} else {
None
}
}
pub fn key_signature(sharps: i8, minor: bool) -> Self {
MetaEvent::KeySignature {
sharps_flats: sharps,
minor,
}
}
pub fn type_byte(&self) -> u8 {
match self {
MetaEvent::SequenceNumber(_) => 0x00,
MetaEvent::Text(_) => 0x01,
MetaEvent::Copyright(_) => 0x02,
MetaEvent::TrackName(_) => 0x03,
MetaEvent::InstrumentName(_) => 0x04,
MetaEvent::Lyric(_) => 0x05,
MetaEvent::Marker(_) => 0x06,
MetaEvent::CuePoint(_) => 0x07,
MetaEvent::ProgramName(_) => 0x08,
MetaEvent::DeviceName(_) => 0x09,
MetaEvent::ChannelPrefix(_) => 0x20,
MetaEvent::MidiPort(_) => 0x21,
MetaEvent::EndOfTrack => 0x2F,
MetaEvent::Tempo(_) => 0x51,
MetaEvent::SmpteOffset { .. } => 0x54,
MetaEvent::TimeSignature { .. } => 0x58,
MetaEvent::KeySignature { .. } => 0x59,
MetaEvent::SequencerSpecific(_) => 0x7F,
MetaEvent::Unknown { type_, .. } => *type_,
}
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = vec![0xFF, self.type_byte()];
let data: Vec<u8> = match self {
MetaEvent::SequenceNumber(n) => vec![(*n >> 8) as u8, *n as u8],
MetaEvent::Text(s)
| MetaEvent::Copyright(s)
| MetaEvent::TrackName(s)
| MetaEvent::InstrumentName(s)
| MetaEvent::Lyric(s)
| MetaEvent::Marker(s)
| MetaEvent::CuePoint(s)
| MetaEvent::ProgramName(s)
| MetaEvent::DeviceName(s) => s.as_bytes().to_vec(),
MetaEvent::ChannelPrefix(ch) => vec![*ch],
MetaEvent::MidiPort(port) => vec![*port],
MetaEvent::EndOfTrack => vec![],
MetaEvent::Tempo(us) => vec![(*us >> 16) as u8, (*us >> 8) as u8, *us as u8],
MetaEvent::SmpteOffset {
hours,
minutes,
seconds,
frames,
subframes,
} => vec![*hours, *minutes, *seconds, *frames, *subframes],
MetaEvent::TimeSignature {
numerator,
denominator_power,
clocks_per_click,
notated_32nd_per_quarter,
} => vec![
*numerator,
*denominator_power,
*clocks_per_click,
*notated_32nd_per_quarter,
],
MetaEvent::KeySignature {
sharps_flats,
minor,
} => vec![*sharps_flats as u8, if *minor { 1 } else { 0 }],
MetaEvent::SequencerSpecific(data) | MetaEvent::Unknown { data, .. } => data.clone(),
};
bytes.extend(Self::encode_varlen(data.len() as u32));
bytes.extend(data);
bytes
}
pub fn from_bytes(data: &[u8]) -> Option<(MetaEvent, usize)> {
if data.is_empty() {
return None;
}
let type_byte = data[0];
let (length, varlen_size) = Self::decode_varlen(&data[1..])?;
let data_start = 1 + varlen_size;
let data_end = data_start + length as usize;
if data.len() < data_end {
return None;
}
let event_data = &data[data_start..data_end];
let event = match type_byte {
0x00 if event_data.len() >= 2 => {
MetaEvent::SequenceNumber(((event_data[0] as u16) << 8) | event_data[1] as u16)
}
0x01 => MetaEvent::Text(String::from_utf8_lossy(event_data).to_string()),
0x02 => MetaEvent::Copyright(String::from_utf8_lossy(event_data).to_string()),
0x03 => MetaEvent::TrackName(String::from_utf8_lossy(event_data).to_string()),
0x04 => MetaEvent::InstrumentName(String::from_utf8_lossy(event_data).to_string()),
0x05 => MetaEvent::Lyric(String::from_utf8_lossy(event_data).to_string()),
0x06 => MetaEvent::Marker(String::from_utf8_lossy(event_data).to_string()),
0x07 => MetaEvent::CuePoint(String::from_utf8_lossy(event_data).to_string()),
0x08 => MetaEvent::ProgramName(String::from_utf8_lossy(event_data).to_string()),
0x09 => MetaEvent::DeviceName(String::from_utf8_lossy(event_data).to_string()),
0x20 if !event_data.is_empty() => MetaEvent::ChannelPrefix(event_data[0]),
0x21 if !event_data.is_empty() => MetaEvent::MidiPort(event_data[0]),
0x2F => MetaEvent::EndOfTrack,
0x51 if event_data.len() >= 3 => MetaEvent::Tempo(
((event_data[0] as u32) << 16)
| ((event_data[1] as u32) << 8)
| event_data[2] as u32,
),
0x54 if event_data.len() >= 5 => MetaEvent::SmpteOffset {
hours: event_data[0],
minutes: event_data[1],
seconds: event_data[2],
frames: event_data[3],
subframes: event_data[4],
},
0x58 if event_data.len() >= 4 => MetaEvent::TimeSignature {
numerator: event_data[0],
denominator_power: event_data[1],
clocks_per_click: event_data[2],
notated_32nd_per_quarter: event_data[3],
},
0x59 if event_data.len() >= 2 => MetaEvent::KeySignature {
sharps_flats: event_data[0] as i8,
minor: event_data[1] != 0,
},
0x7F => MetaEvent::SequencerSpecific(event_data.to_vec()),
_ => MetaEvent::Unknown {
type_: type_byte,
data: event_data.to_vec(),
},
};
Some((event, data_end))
}
fn encode_varlen(mut value: u32) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.push((value & 0x7F) as u8);
value >>= 7;
while value > 0 {
bytes.push(((value & 0x7F) | 0x80) as u8);
value >>= 7;
}
bytes.reverse();
bytes
}
fn decode_varlen(data: &[u8]) -> Option<(u32, usize)> {
let mut value: u32 = 0;
let mut bytes_read = 0;
for &byte in data.iter().take(4) {
bytes_read += 1;
value = (value << 7) | (byte & 0x7F) as u32;
if byte & 0x80 == 0 {
return Some((value, bytes_read));
}
}
None
}
}
impl fmt::Display for MetaEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MetaEvent::Tempo(us) => {
let bpm = 60_000_000.0 / *us as f64;
write!(f, "Tempo({:.2} BPM)", bpm)
}
MetaEvent::TimeSignature {
numerator,
denominator_power,
..
} => {
let denom = 1 << denominator_power;
write!(f, "TimeSignature({}/{})", numerator, denom)
}
MetaEvent::KeySignature {
sharps_flats,
minor,
} => {
let mode = if *minor { "minor" } else { "major" };
write!(f, "KeySignature({} {})", sharps_flats, mode)
}
MetaEvent::TrackName(name) => write!(f, "TrackName({})", name),
MetaEvent::EndOfTrack => write!(f, "EndOfTrack"),
_ => write!(f, "{:?}", self),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_note_on_off() {
let on = MidiMessage::note_on(0, 60, 100);
assert!(on.is_note_on());
assert!(!on.is_note_off());
assert_eq!(on.channel(), Some(0));
let off = MidiMessage::note_off(0, 60, 0);
assert!(off.is_note_off());
assert!(!off.is_note_on());
}
#[test]
fn test_message_to_bytes() {
let on = MidiMessage::note_on(0, 60, 100);
let bytes = on.to_bytes();
assert_eq!(bytes, vec![0x90, 60, 100]);
let (parsed, _) = MidiMessage::from_bytes(&bytes).unwrap();
assert_eq!(parsed, on);
}
#[test]
fn test_pitch_bend() {
let center = MidiMessage::pitch_bend(0, 0x2000);
assert_eq!(center.channel(), Some(0));
if let MidiMessage::PitchBend { value, .. } = center {
assert_eq!(value, 0x2000);
}
let signed = MidiMessage::pitch_bend_signed(0, 0);
if let MidiMessage::PitchBend { value, .. } = signed {
assert_eq!(value, 0x2000);
}
}
#[test]
fn test_tempo_meta() {
let tempo = MetaEvent::tempo_from_bpm(120.0);
if let MetaEvent::Tempo(us) = tempo {
assert_eq!(us, 500_000);
}
assert!((tempo.tempo_to_bpm().unwrap() - 120.0).abs() < 0.01);
}
#[test]
fn test_time_signature_meta() {
let ts = MetaEvent::time_signature(4, 4);
assert_eq!(ts.time_signature_denominator(), Some(4));
let ts = MetaEvent::time_signature(6, 8);
assert_eq!(ts.time_signature_denominator(), Some(8));
}
#[test]
fn test_controller_classification_and_constructors() {
let sustain_on = MidiMessage::make_sustain_on(0);
assert!(sustain_on.is_controller());
assert!(sustain_on.is_sustain());
assert!(sustain_on.is_sustain_on());
assert!(!sustain_on.is_sustain_off());
assert_eq!(sustain_on.get_controller_number(), Some(64));
let sustain_off = MidiMessage::make_sustain_off(0);
assert!(sustain_off.is_sustain_off());
assert!(!sustain_off.is_sustain_on());
let soft_on = MidiMessage::make_soft_on(1);
assert!(soft_on.is_soft());
assert!(soft_on.is_soft_on());
let patch = MidiMessage::make_patch_change(0, 40);
assert!(patch.is_patch_change());
assert!(patch.is_timbre());
assert!(MidiMessage::note_on(0, 60, 100).is_note());
assert!(MidiMessage::note_off(0, 60, 0).is_note());
assert!(MidiMessage::pitch_bend(0, 0x2000).is_pitchbend());
}
#[test]
fn test_tempo_primitives() {
let tempo = MetaEvent::tempo_from_bpm(120.0);
assert_eq!(tempo.tempo_microseconds(), Some(500_000));
assert!((tempo.tempo_seconds().unwrap() - 0.5).abs() < 1e-9);
assert!((tempo.tempo_ticks_per_second(480).unwrap() - 960.0).abs() < 1e-6);
assert!((tempo.tempo_seconds_per_tick(480).unwrap() - 0.5 / 480.0).abs() < 1e-9);
}
#[test]
fn test_frequency_to_semitones() {
assert!((frequency_to_semitones(440.0) - 69.0).abs() < 1e-9);
assert!((frequency_to_semitones(880.0) - 81.0).abs() < 1e-9);
}
#[test]
fn test_meta_event_roundtrip() {
let events = vec![
MetaEvent::TrackName("Test Track".to_string()),
MetaEvent::Tempo(500_000),
MetaEvent::time_signature(4, 4),
MetaEvent::key_signature(0, false),
MetaEvent::EndOfTrack,
];
for event in events {
let bytes = event.to_bytes();
let (parsed, _) = MetaEvent::from_bytes(&bytes[1..]).unwrap();
assert_eq!(parsed, event);
}
}
}