Add NoteEvent conversions to and from MIDI SysEx
JACK already supports this because otherwise things wouldn't compile, but support still needs to be added for CLAP and VST3.
This commit is contained in:
parent
1e8bdb9d8e
commit
68d68c0bc3
3 changed files with 191 additions and 131 deletions
259
src/midi.rs
259
src/midi.rs
|
@ -318,6 +318,18 @@ pub enum NoteEvent<S: SysExMessage> {
|
|||
MidiSysEx { timing: u32, message: S },
|
||||
}
|
||||
|
||||
/// The result of converting a `NoteEvent<S>` to MIDI. This is a bit weirder than it would have to
|
||||
/// be because it's not possible to use associated constants in type definitions.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MidiResult {
|
||||
/// A basic three byte MIDI event.
|
||||
Basic([u8; 3]),
|
||||
/// A SysEx event. The message was written to the buffer provided to [`NoteEvent::as_midi()`].
|
||||
/// The `usize` value indicates the message's actual length, including headers and end of SysEx
|
||||
/// byte.
|
||||
SysEx(usize),
|
||||
}
|
||||
|
||||
impl<S: SysExMessage> NoteEvent<S> {
|
||||
/// Returns the sample within the current buffer this event belongs to.
|
||||
pub fn timing(&self) -> u32 {
|
||||
|
@ -367,78 +379,109 @@ impl<S: SysExMessage> NoteEvent<S> {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: `[u8; 3]` doesn't work anymore with SysEx. We can wrap this in an
|
||||
// `enum MidiBuffer<P> { simple: [u8; 3], sysex: P::SysExMessage::Buffer }`.
|
||||
/// Parse MIDI into a [`NoteEvent`]. Supports both basic three bytes messages as well as SysEx.
|
||||
/// Will return `Err(event_type)` if the parsing failed.
|
||||
pub fn from_midi(timing: u32, midi_data: &[u8]) -> Result<Self, u8> {
|
||||
let status_byte = midi_data.first().copied().unwrap_or_default();
|
||||
let event_type = status_byte & midi::EVENT_TYPE_MASK;
|
||||
|
||||
/// Parse MIDI into a [`NoteEvent`]. Will return `Err(event_type)` if the parsing failed.
|
||||
pub fn from_midi(timing: u32, midi_data: [u8; 3]) -> Result<Self, u8> {
|
||||
// TODO: Maybe add special handling for 14-bit CCs and RPN messages at some
|
||||
// point, right now the plugin has to figure it out for itself
|
||||
let event_type = midi_data[0] & midi::EVENT_TYPE_MASK;
|
||||
let channel = midi_data[0] & midi::MIDI_CHANNEL_MASK;
|
||||
match event_type {
|
||||
// You thought this was a note on? Think again! This is a cleverly disguised note off
|
||||
// event straight from the 80s when Baud rate was still a limiting factor!
|
||||
midi::NOTE_ON if midi_data[2] == 0 => Ok(NoteEvent::NoteOff {
|
||||
timing,
|
||||
voice_id: None,
|
||||
channel,
|
||||
note: midi_data[1],
|
||||
// Few things use release velocity. Just having this be zero here is fine, right?
|
||||
velocity: 0.0,
|
||||
}),
|
||||
midi::NOTE_ON => Ok(NoteEvent::NoteOn {
|
||||
timing,
|
||||
voice_id: None,
|
||||
channel,
|
||||
note: midi_data[1],
|
||||
velocity: midi_data[2] as f32 / 127.0,
|
||||
}),
|
||||
midi::NOTE_OFF => Ok(NoteEvent::NoteOff {
|
||||
timing,
|
||||
voice_id: None,
|
||||
channel,
|
||||
note: midi_data[1],
|
||||
velocity: midi_data[2] as f32 / 127.0,
|
||||
}),
|
||||
midi::POLYPHONIC_KEY_PRESSURE => Ok(NoteEvent::PolyPressure {
|
||||
timing,
|
||||
voice_id: None,
|
||||
channel,
|
||||
note: midi_data[1],
|
||||
pressure: midi_data[2] as f32 / 127.0,
|
||||
}),
|
||||
midi::CHANNEL_KEY_PRESSURE => Ok(NoteEvent::MidiChannelPressure {
|
||||
timing,
|
||||
channel,
|
||||
pressure: midi_data[1] as f32 / 127.0,
|
||||
}),
|
||||
midi::PITCH_BEND_CHANGE => Ok(NoteEvent::MidiPitchBend {
|
||||
timing,
|
||||
channel,
|
||||
value: (midi_data[1] as u16 + ((midi_data[2] as u16) << 7)) as f32
|
||||
/ ((1 << 14) - 1) as f32,
|
||||
}),
|
||||
midi::CONTROL_CHANGE => Ok(NoteEvent::MidiCC {
|
||||
timing,
|
||||
channel,
|
||||
cc: midi_data[1],
|
||||
value: midi_data[2] as f32 / 127.0,
|
||||
}),
|
||||
midi::PROGRAM_CHANGE => Ok(NoteEvent::MidiProgramChange {
|
||||
timing,
|
||||
channel,
|
||||
program: midi_data[1],
|
||||
}),
|
||||
// TODO: SysEx
|
||||
n => Err(n),
|
||||
if midi_data.len() >= 3 {
|
||||
// TODO: Maybe add special handling for 14-bit CCs and RPN messages at some
|
||||
// point, right now the plugin has to figure it out for itself
|
||||
let channel = status_byte & midi::MIDI_CHANNEL_MASK;
|
||||
match event_type {
|
||||
// You thought this was a note on? Think again! This is a cleverly disguised note off
|
||||
// event straight from the 80s when Baud rate was still a limiting factor!
|
||||
midi::NOTE_ON if midi_data[2] == 0 => {
|
||||
return Ok(NoteEvent::NoteOff {
|
||||
timing,
|
||||
voice_id: None,
|
||||
channel,
|
||||
note: midi_data[1],
|
||||
// Few things use release velocity. Just having this be zero here is fine, right?
|
||||
velocity: 0.0,
|
||||
});
|
||||
}
|
||||
midi::NOTE_ON => {
|
||||
return Ok(NoteEvent::NoteOn {
|
||||
timing,
|
||||
voice_id: None,
|
||||
channel,
|
||||
note: midi_data[1],
|
||||
velocity: midi_data[2] as f32 / 127.0,
|
||||
});
|
||||
}
|
||||
midi::NOTE_OFF => {
|
||||
return Ok(NoteEvent::NoteOff {
|
||||
timing,
|
||||
voice_id: None,
|
||||
channel,
|
||||
note: midi_data[1],
|
||||
velocity: midi_data[2] as f32 / 127.0,
|
||||
});
|
||||
}
|
||||
midi::POLYPHONIC_KEY_PRESSURE => {
|
||||
return Ok(NoteEvent::PolyPressure {
|
||||
timing,
|
||||
voice_id: None,
|
||||
channel,
|
||||
note: midi_data[1],
|
||||
pressure: midi_data[2] as f32 / 127.0,
|
||||
});
|
||||
}
|
||||
midi::CHANNEL_KEY_PRESSURE => {
|
||||
return Ok(NoteEvent::MidiChannelPressure {
|
||||
timing,
|
||||
channel,
|
||||
pressure: midi_data[1] as f32 / 127.0,
|
||||
});
|
||||
}
|
||||
midi::PITCH_BEND_CHANGE => {
|
||||
return Ok(NoteEvent::MidiPitchBend {
|
||||
timing,
|
||||
channel,
|
||||
value: (midi_data[1] as u16 + ((midi_data[2] as u16) << 7)) as f32
|
||||
/ ((1 << 14) - 1) as f32,
|
||||
});
|
||||
}
|
||||
midi::CONTROL_CHANGE => {
|
||||
return Ok(NoteEvent::MidiCC {
|
||||
timing,
|
||||
channel,
|
||||
cc: midi_data[1],
|
||||
value: midi_data[2] as f32 / 127.0,
|
||||
});
|
||||
}
|
||||
midi::PROGRAM_CHANGE => {
|
||||
return Ok(NoteEvent::MidiProgramChange {
|
||||
timing,
|
||||
channel,
|
||||
program: midi_data[1],
|
||||
});
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
// Every other message is parsed as SysEx, even if they don't have the `0xf0` status byte.
|
||||
// This allows the `SysExMessage` trait to have a bit more flexibility if needed. Regular
|
||||
// note event parsing however still has higher priority.
|
||||
match S::from_buffer(midi_data) {
|
||||
Some(message) => Ok(NoteEvent::MidiSysEx { timing, message }),
|
||||
None => Err(event_type),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a MIDI message from this note event. Return `None` if this even does not have a
|
||||
/// Create a MIDI message from this note event. Returns `None` if this even does not have a
|
||||
/// direct MIDI equivalent. `PolyPressure` will be converted to polyphonic key pressure, but the
|
||||
/// other polyphonic note expression types will not be converted to MIDI CC messages.
|
||||
pub fn as_midi(self) -> Option<[u8; 3]> {
|
||||
///
|
||||
/// The `sysex_buffer` is an `[u8; N]` buffer with a length depending on SysEx message type `S`.
|
||||
/// If this event contained SysEx data, then the result is written to the buffer and the
|
||||
/// message's length in bytes is returned. This weird approach is needed because it's not
|
||||
/// possible to use associated constants in types. Otherwise the buffer could be stored in
|
||||
/// `MidiResult`.
|
||||
pub fn as_midi(self, sysex_buffer: &mut S::Buffer) -> Option<MidiResult> {
|
||||
match self {
|
||||
NoteEvent::NoteOn {
|
||||
timing: _,
|
||||
|
@ -446,42 +489,42 @@ impl<S: SysExMessage> NoteEvent<S> {
|
|||
channel,
|
||||
note,
|
||||
velocity,
|
||||
} => Some([
|
||||
} => Some(MidiResult::Basic([
|
||||
midi::NOTE_ON | channel,
|
||||
note,
|
||||
(velocity * 127.0).round().clamp(0.0, 127.0) as u8,
|
||||
]),
|
||||
])),
|
||||
NoteEvent::NoteOff {
|
||||
timing: _,
|
||||
voice_id: _,
|
||||
channel,
|
||||
note,
|
||||
velocity,
|
||||
} => Some([
|
||||
} => Some(MidiResult::Basic([
|
||||
midi::NOTE_OFF | channel,
|
||||
note,
|
||||
(velocity * 127.0).round().clamp(0.0, 127.0) as u8,
|
||||
]),
|
||||
])),
|
||||
NoteEvent::PolyPressure {
|
||||
timing: _,
|
||||
voice_id: _,
|
||||
channel,
|
||||
note,
|
||||
pressure,
|
||||
} => Some([
|
||||
} => Some(MidiResult::Basic([
|
||||
midi::POLYPHONIC_KEY_PRESSURE | channel,
|
||||
note,
|
||||
(pressure * 127.0).round().clamp(0.0, 127.0) as u8,
|
||||
]),
|
||||
])),
|
||||
NoteEvent::MidiChannelPressure {
|
||||
timing: _,
|
||||
channel,
|
||||
pressure,
|
||||
} => Some([
|
||||
} => Some(MidiResult::Basic([
|
||||
midi::CHANNEL_KEY_PRESSURE | channel,
|
||||
(pressure * 127.0).round().clamp(0.0, 127.0) as u8,
|
||||
0,
|
||||
]),
|
||||
])),
|
||||
NoteEvent::MidiPitchBend {
|
||||
timing: _,
|
||||
channel,
|
||||
|
@ -492,27 +535,36 @@ impl<S: SysExMessage> NoteEvent<S> {
|
|||
.round()
|
||||
.clamp(0.0, PITCH_BEND_RANGE) as u16;
|
||||
|
||||
Some([
|
||||
Some(MidiResult::Basic([
|
||||
midi::PITCH_BEND_CHANGE | channel,
|
||||
(midi_value & ((1 << 7) - 1)) as u8,
|
||||
(midi_value >> 7) as u8,
|
||||
])
|
||||
]))
|
||||
}
|
||||
NoteEvent::MidiCC {
|
||||
timing: _,
|
||||
channel,
|
||||
cc,
|
||||
value,
|
||||
} => Some([
|
||||
} => Some(MidiResult::Basic([
|
||||
midi::CONTROL_CHANGE | channel,
|
||||
cc,
|
||||
(value * 127.0).round().clamp(0.0, 127.0) as u8,
|
||||
]),
|
||||
])),
|
||||
NoteEvent::MidiProgramChange {
|
||||
timing: _,
|
||||
channel,
|
||||
program,
|
||||
} => Some([midi::PROGRAM_CHANGE | channel, program, 0]),
|
||||
} => Some(MidiResult::Basic([
|
||||
midi::PROGRAM_CHANGE | channel,
|
||||
program,
|
||||
0,
|
||||
])),
|
||||
// `message` is serialized and written to `sysex_buffer`, and the result contains the
|
||||
// message's actual length
|
||||
NoteEvent::MidiSysEx { timing: _, message } => {
|
||||
Some(MidiResult::SysEx(message.to_buffer(sysex_buffer)))
|
||||
}
|
||||
NoteEvent::Choke { .. }
|
||||
| NoteEvent::VoiceTerminated { .. }
|
||||
| NoteEvent::PolyModulation { .. }
|
||||
|
@ -523,8 +575,6 @@ impl<S: SysExMessage> NoteEvent<S> {
|
|||
| NoteEvent::PolyVibrato { .. }
|
||||
| NoteEvent::PolyExpression { .. }
|
||||
| NoteEvent::PolyBrightness { .. } => None,
|
||||
// TODO: These functions need to handle both simple and longer messages as documented above
|
||||
NoteEvent::MidiSysEx { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -560,6 +610,16 @@ mod tests {
|
|||
|
||||
const TIMING: u32 = 5;
|
||||
|
||||
/// Converts an event to and from MIDI. Panics if any part of the conversion fails.
|
||||
fn roundtrip_basic_event(event: NoteEvent<()>) -> NoteEvent<()> {
|
||||
let midi_data = match event.as_midi(&mut Default::default()).unwrap() {
|
||||
MidiResult::Basic(midi_data) => midi_data,
|
||||
MidiResult::SysEx(_) => panic!("Unexpected SysEx result"),
|
||||
};
|
||||
|
||||
NoteEvent::from_midi(TIMING, &midi_data).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_note_on_midi_conversion() {
|
||||
let event = NoteEvent::<()>::NoteOn {
|
||||
|
@ -571,10 +631,7 @@ mod tests {
|
|||
velocity: 0.6929134,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
NoteEvent::from_midi(TIMING, event.as_midi().unwrap()).unwrap(),
|
||||
event
|
||||
);
|
||||
assert_eq!(roundtrip_basic_event(event), event);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -587,10 +644,7 @@ mod tests {
|
|||
velocity: 0.6929134,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
NoteEvent::from_midi(TIMING, event.as_midi().unwrap()).unwrap(),
|
||||
event
|
||||
);
|
||||
assert_eq!(roundtrip_basic_event(event), event);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -603,10 +657,7 @@ mod tests {
|
|||
pressure: 0.6929134,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
NoteEvent::from_midi(TIMING, event.as_midi().unwrap()).unwrap(),
|
||||
event
|
||||
);
|
||||
assert_eq!(roundtrip_basic_event(event), event);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -617,10 +668,7 @@ mod tests {
|
|||
pressure: 0.6929134,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
NoteEvent::from_midi(TIMING, event.as_midi().unwrap()).unwrap(),
|
||||
event
|
||||
);
|
||||
assert_eq!(roundtrip_basic_event(event), event);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -631,10 +679,7 @@ mod tests {
|
|||
value: 0.6929134,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
NoteEvent::from_midi(TIMING, event.as_midi().unwrap()).unwrap(),
|
||||
event
|
||||
);
|
||||
assert_eq!(roundtrip_basic_event(event), event);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -646,10 +691,7 @@ mod tests {
|
|||
value: 0.6929134,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
NoteEvent::from_midi(TIMING, event.as_midi().unwrap()).unwrap(),
|
||||
event
|
||||
);
|
||||
assert_eq!(roundtrip_basic_event(event), event);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -660,11 +702,8 @@ mod tests {
|
|||
program: 42,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
NoteEvent::from_midi(TIMING, event.as_midi().unwrap()).unwrap(),
|
||||
event
|
||||
);
|
||||
assert_eq!(roundtrip_basic_event(event), event);
|
||||
}
|
||||
|
||||
// TODO: SysEx conversion
|
||||
// TODO: Test SysEx conversion
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
//! Traits for working with MIDI SysEx data.
|
||||
|
||||
use std::borrow::{Borrow, BorrowMut};
|
||||
use std::fmt::Debug;
|
||||
|
||||
/// A type that can be converted to and from byte buffers containing MIDI SysEx messages.
|
||||
|
@ -14,35 +15,36 @@ use std::fmt::Debug;
|
|||
/// For example, the message to turn general MIDI mode on is `[0xf0, 0x7e, 0x7f, 0x09, 0x01, 0xf7]`,
|
||||
/// and has a length of 6 bytes. Note that this includes the `0xf0` start byte and `0xf7` end byte.
|
||||
pub trait SysExMessage: Debug + Clone + PartialEq + Send + Sync {
|
||||
/// The maximum SysEx message size, in bytes. This covers the full message, see the trait's
|
||||
/// docstring for more information.
|
||||
const MAX_BUFFER_SIZE: usize;
|
||||
/// The byte array buffer the messages are read from and serialized to. Should be a `[u8; N]`,
|
||||
/// where `N` is the maximum supported message length in bytes. This covers the full message,
|
||||
/// see the trait's docstring for more information.
|
||||
///
|
||||
/// Ideally this could just be a const generic but Rust doesn't let you use those as array
|
||||
/// lengths just yet.
|
||||
///
|
||||
/// <https://github.com/rust-lang/rust/issues/60551>
|
||||
type Buffer: Default + Borrow<[u8]> + BorrowMut<[u8]>;
|
||||
|
||||
/// Read a SysEx message from `buffer` and convert it to this message type if supported. This
|
||||
/// covers the full message, see the trait's docstring for more information. `buffer`'s length
|
||||
/// matches the received message. It is not padded to `MAX_BUFFER_SIZE` bytes.
|
||||
/// matches the received message. It is not padded to match [`Buffer`][Self::Buffer].
|
||||
fn from_buffer(buffer: &[u8]) -> Option<Self>;
|
||||
|
||||
/// Serialize this message object as a SysEx message in `buffer`, returning the message's length
|
||||
/// in bytes. This should contain the full message including headers and the EOX byte, see the
|
||||
/// trait's docstring for more information.
|
||||
///
|
||||
/// `buffer` is a `[u8; Self::MAX_BUFFER_SIZE]`, but Rust currently doesn't allow using
|
||||
/// associated constants in method types:
|
||||
///
|
||||
/// <https://github.com/rust-lang/rust/issues/60551>
|
||||
fn to_buffer(self, buffer: &mut [u8]) -> usize;
|
||||
fn to_buffer(self, buffer: &mut Self::Buffer) -> usize;
|
||||
}
|
||||
|
||||
/// A default implementation plugins that don't need SysEx support can use.
|
||||
impl SysExMessage for () {
|
||||
const MAX_BUFFER_SIZE: usize = 0;
|
||||
type Buffer = [u8; 0];
|
||||
|
||||
fn from_buffer(_buffer: &[u8]) -> Option<Self> {
|
||||
None
|
||||
}
|
||||
|
||||
fn to_buffer(self, _buffer: &mut [u8]) -> usize {
|
||||
fn to_buffer(self, _buffer: &mut [u8; 0]) -> usize {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use std::borrow::Borrow;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
@ -12,7 +13,7 @@ use super::super::config::WrapperConfig;
|
|||
use super::Backend;
|
||||
use crate::buffer::Buffer;
|
||||
use crate::context::process::Transport;
|
||||
use crate::midi::{MidiConfig, NoteEvent, PluginNoteEvent};
|
||||
use crate::midi::{MidiConfig, MidiResult, NoteEvent, PluginNoteEvent};
|
||||
use crate::plugin::Plugin;
|
||||
|
||||
/// Uses JACK audio and MIDI.
|
||||
|
@ -129,14 +130,13 @@ impl<P: Plugin> Backend<P> for Jack {
|
|||
let mut midi_data = [0u8; 3];
|
||||
midi_data[..midi.bytes.len()].copy_from_slice(midi.bytes);
|
||||
|
||||
NoteEvent::from_midi(midi.time, midi_data).ok()
|
||||
NoteEvent::from_midi(midi.time, &midi_data).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// TODO: Support SysEx
|
||||
output_events.clear();
|
||||
if cb(&mut buffer, transport, &input_events, &mut output_events) {
|
||||
if let Some(midi_output) = &midi_output {
|
||||
|
@ -144,12 +144,31 @@ impl<P: Plugin> Backend<P> for Jack {
|
|||
let mut midi_writer = midi_output.writer(ps);
|
||||
for event in output_events.drain(..) {
|
||||
let timing = event.timing();
|
||||
if let Some(midi_data) = event.as_midi() {
|
||||
let write_result = midi_writer.write(&jack::RawMidi {
|
||||
time: timing,
|
||||
bytes: &midi_data,
|
||||
});
|
||||
nih_debug_assert!(write_result.is_ok(), "The MIDI buffer is full");
|
||||
|
||||
let mut sysex_buffer = Default::default();
|
||||
match event.as_midi(&mut sysex_buffer) {
|
||||
Some(MidiResult::Basic(midi_data)) => {
|
||||
let write_result = midi_writer.write(&jack::RawMidi {
|
||||
time: timing,
|
||||
bytes: &midi_data,
|
||||
});
|
||||
|
||||
nih_debug_assert!(write_result.is_ok(), "The MIDI buffer is full");
|
||||
}
|
||||
Some(MidiResult::SysEx(length)) => {
|
||||
// This feels a bit like gymnastics, but if the event was a SysEx
|
||||
// event then `sysex_buffer` now contains the full message plus
|
||||
// possibly some padding at the end
|
||||
let sysex_buffer = sysex_buffer.borrow();
|
||||
nih_debug_assert!(length <= sysex_buffer.len());
|
||||
let write_result = midi_writer.write(&jack::RawMidi {
|
||||
time: timing,
|
||||
bytes: &sysex_buffer[..length],
|
||||
});
|
||||
|
||||
nih_debug_assert!(write_result.is_ok(), "The MIDI buffer is full");
|
||||
}
|
||||
None => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue