use core::fmt;
use core::iter;
#[cfg(bela_device)]
use core::ptr::{self, NonNull};
use core::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
#[cfg(bela_device)]
use std::ffi::CString;
use std::sync::Arc;
use std::thread::{self, ThreadId};
use crate::context::{CallbackContext, SetupContext};
use crate::error::Error;
use crate::task::{AuxiliaryTask, Priority};
#[must_use]
pub fn midi_ports() -> Vec<String> {
ports()
}
#[cfg(bela_device)]
fn ports() -> Vec<String> {
let needed = unsafe { bela_sys::bela_midi_list_ports(ptr::null_mut(), 0) } as usize;
let mut buffer = vec![0u8; needed];
let needed_now = unsafe {
bela_sys::bela_midi_list_ports(
buffer.as_mut_ptr().cast(),
u32::try_from(needed).unwrap_or(u32::MAX),
)
} as usize;
buffer.truncate(needed_now.min(needed));
buffer
.split(|byte| *byte == 0)
.filter(|name| !name.is_empty())
.map(|name| String::from_utf8_lossy(name).into_owned())
.collect()
}
#[cfg(not(bela_device))]
#[allow(
clippy::missing_const_for_fn,
reason = "mirrors the device signature, which allocates"
)]
fn ports() -> Vec<String> {
Vec::new()
}
#[derive(Debug)]
pub struct MidiInput {
#[cfg(bela_device)]
raw: NonNull<bela_sys::BelaMidi>,
}
unsafe impl Send for MidiInput {}
unsafe impl Sync for MidiInput {}
impl MidiInput {
#[cfg(bela_device)]
pub fn open(port: &str) -> Result<Self, Error> {
let name = CString::new(port).map_err(|_| Error::MidiPortName)?;
let raw = NonNull::new(unsafe { bela_sys::bela_midi_new() }).ok_or(Error::MidiCreate)?;
let input = Self { raw };
let opened = unsafe { bela_sys::bela_midi_read_from(input.raw.as_ptr(), name.as_ptr()) };
if opened < 0 {
return Err(Error::MidiOpen(opened));
}
Ok(input)
}
#[cfg(not(bela_device))]
#[allow(
clippy::missing_const_for_fn,
reason = "mirrors the device signature, which is not const"
)]
pub fn open(_port: &str) -> Result<Self, Error> {
Err(Error::MidiUnavailable)
}
#[must_use]
#[allow(
clippy::missing_const_for_fn,
reason = "const only off-device, where there is no ring to read"
)]
pub fn available(&self) -> usize {
self.available_raw()
}
#[cfg(bela_device)]
fn available_raw(&self) -> usize {
let available = unsafe { bela_sys::bela_midi_available_messages(self.raw.as_ptr()) };
usize::try_from(available).unwrap_or(0)
}
#[cfg(not(bela_device))]
#[allow(
clippy::unused_self,
reason = "mirrors the device signature; there is no port to have read from"
)]
const fn available_raw(&self) -> usize {
0
}
pub fn read(&mut self) -> Option<MidiMessage> {
loop {
let (bytes, len) = self.read_raw()?;
if let Some(message) = MidiMessage::from_bytes(&bytes[..len]) {
return Some(message);
}
}
}
pub fn messages(&mut self) -> impl Iterator<Item = MidiMessage> + '_ {
iter::from_fn(move || self.read())
}
#[cfg(bela_device)]
fn read_raw(&mut self) -> Option<([u8; bela_sys::BELA_MIDI_MESSAGE_MAX], usize)> {
let mut bytes = [0u8; bela_sys::BELA_MIDI_MESSAGE_MAX];
let written =
unsafe { bela_sys::bela_midi_get_message(self.raw.as_ptr(), bytes.as_mut_ptr()) };
let len = usize::try_from(written)
.unwrap_or(0)
.min(bela_sys::BELA_MIDI_MESSAGE_MAX);
(len > 0).then_some((bytes, len))
}
#[cfg(not(bela_device))]
#[allow(
clippy::unused_self,
clippy::needless_pass_by_ref_mut,
reason = "mirrors the device signature, where &mut self is what keeps the ring to one reader"
)]
const fn read_raw(&mut self) -> Option<([u8; bela_sys::BELA_MIDI_MESSAGE_MAX], usize)> {
None
}
}
impl Drop for MidiInput {
fn drop(&mut self) {
#[cfg(bela_device)]
unsafe {
bela_sys::bela_midi_delete(self.raw.as_ptr());
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum MidiMessage {
NoteOff {
channel: MidiChannel,
note: Note,
velocity: Velocity,
},
NoteOn {
channel: MidiChannel,
note: Note,
velocity: Velocity,
},
KeyPressure {
channel: MidiChannel,
note: Note,
pressure: Pressure,
},
ControlChange {
channel: MidiChannel,
controller: Controller,
value: ControlValue,
},
ProgramChange {
channel: MidiChannel,
program: Program,
},
ChannelPressure {
channel: MidiChannel,
pressure: Pressure,
},
PitchBend {
channel: MidiChannel,
bend: PitchBend,
},
Clock,
Start,
Continue,
Stop,
ActiveSensing,
Reset,
}
const NOTE_OFF: u8 = 0x80;
const NOTE_ON: u8 = 0x90;
const KEY_PRESSURE: u8 = 0xA0;
const CONTROL_CHANGE: u8 = 0xB0;
const PROGRAM_CHANGE: u8 = 0xC0;
const CHANNEL_PRESSURE: u8 = 0xD0;
const PITCH_BEND: u8 = 0xE0;
const SYSTEM: u8 = 0xF0;
const CLOCK: u8 = 0xF8;
const START: u8 = 0xFA;
const CONTINUE: u8 = 0xFB;
const STOP: u8 = 0xFC;
const ACTIVE_SENSING: u8 = 0xFE;
const RESET: u8 = 0xFF;
impl MidiMessage {
fn from_bytes(bytes: &[u8]) -> Option<Self> {
let status = *bytes.first()?;
let channel = MidiChannel::from_bits(status);
let first = bytes.get(1).copied();
let second = bytes.get(2).copied();
Some(match status & 0xF0 {
NOTE_OFF => Self::NoteOff {
channel,
note: Note::from_bits(first?),
velocity: Velocity::from_bits(second?),
},
NOTE_ON => Self::NoteOn {
channel,
note: Note::from_bits(first?),
velocity: Velocity::from_bits(second?),
},
KEY_PRESSURE => Self::KeyPressure {
channel,
note: Note::from_bits(first?),
pressure: Pressure::from_bits(second?),
},
CONTROL_CHANGE => Self::ControlChange {
channel,
controller: Controller::from_bits(first?),
value: ControlValue::from_bits(second?),
},
PROGRAM_CHANGE => Self::ProgramChange {
channel,
program: Program::from_bits(first?),
},
CHANNEL_PRESSURE => Self::ChannelPressure {
channel,
pressure: Pressure::from_bits(first?),
},
PITCH_BEND => Self::PitchBend {
channel,
bend: PitchBend::from_bits(first?, second?),
},
SYSTEM => match status {
CLOCK => Self::Clock,
START => Self::Start,
CONTINUE => Self::Continue,
STOP => Self::Stop,
ACTIVE_SENSING => Self::ActiveSensing,
RESET => Self::Reset,
_ => return None,
},
_ => return None,
})
}
const fn to_bytes(self) -> ([u8; MESSAGE_MAX], usize) {
const fn status(kind: u8, channel: MidiChannel) -> u8 {
kind | channel.get()
}
match self {
Self::NoteOff {
channel,
note,
velocity,
} => ([status(NOTE_OFF, channel), note.get(), velocity.get()], 3),
Self::NoteOn {
channel,
note,
velocity,
} => ([status(NOTE_ON, channel), note.get(), velocity.get()], 3),
Self::KeyPressure {
channel,
note,
pressure,
} => (
[status(KEY_PRESSURE, channel), note.get(), pressure.get()],
3,
),
Self::ControlChange {
channel,
controller,
value,
} => (
[
status(CONTROL_CHANGE, channel),
controller.get(),
value.get(),
],
3,
),
Self::ProgramChange { channel, program } => {
([status(PROGRAM_CHANGE, channel), program.get(), 0], 2)
}
Self::ChannelPressure { channel, pressure } => {
([status(CHANNEL_PRESSURE, channel), pressure.get(), 0], 2)
}
Self::PitchBend { channel, bend } => {
let (low, high) = bend.to_bits();
([status(PITCH_BEND, channel), low, high], 3)
}
Self::Clock => ([CLOCK, 0, 0], 1),
Self::Start => ([START, 0, 0], 1),
Self::Continue => ([CONTINUE, 0, 0], 1),
Self::Stop => ([STOP, 0, 0], 1),
Self::ActiveSensing => ([ACTIVE_SENSING, 0, 0], 1),
Self::Reset => ([RESET, 0, 0], 1),
}
}
#[must_use]
pub const fn channel(self) -> Option<MidiChannel> {
match self {
Self::NoteOff { channel, .. }
| Self::NoteOn { channel, .. }
| Self::KeyPressure { channel, .. }
| Self::ControlChange { channel, .. }
| Self::ProgramChange { channel, .. }
| Self::ChannelPressure { channel, .. }
| Self::PitchBend { channel, .. } => Some(channel),
Self::Clock
| Self::Start
| Self::Continue
| Self::Stop
| Self::ActiveSensing
| Self::Reset => None,
}
}
}
macro_rules! seven_bit {
($(
$(#[$meta:meta])*
$name:ident, $what:literal;
)*) => {$(
$(#[$meta])*
///
/// A seven-bit value: 0 to 127, which is what one MIDI data
/// byte carries.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct $name(u8);
impl $name {
pub const MIN: Self = Self(0);
pub const MAX: Self = Self(127);
#[doc = concat!("A ", $what, ", or [`None`] above 127.")]
#[must_use]
pub const fn new(value: u8) -> Option<Self> {
if value > Self::MAX.0 {
return None;
}
Some(Self(value))
}
#[must_use]
pub const fn get(self) -> u8 {
self.0
}
const fn from_bits(bits: u8) -> Self {
Self(bits & 0x7F)
}
}
impl From<$name> for u8 {
fn from(value: $name) -> Self {
value.0
}
}
impl TryFrom<u8> for $name {
type Error = Error;
#[doc = concat!("A ", $what, ", or [`Error::MidiValue`] above 127.")]
fn try_from(value: u8) -> Result<Self, Self::Error> {
Self::new(value).ok_or_else(|| Error::MidiValue {
value: u16::from(value),
max: u16::from(Self::MAX.0),
kind: $what,
})
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
)*};
}
seven_bit! {
Note, "note number";
Velocity, "velocity";
Controller, "controller number";
ControlValue, "controller value";
Program, "program number";
Pressure, "pressure";
}
impl Note {
pub const MIDDLE_C: Self = Self(60);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct MidiChannel(u8);
impl MidiChannel {
pub const MIN: Self = Self(0);
pub const MAX: Self = Self(15);
#[must_use]
pub const fn new(channel: u8) -> Option<Self> {
if channel > Self::MAX.0 {
return None;
}
Some(Self(channel))
}
#[must_use]
pub const fn get(self) -> u8 {
self.0
}
const fn from_bits(status: u8) -> Self {
Self(status & 0x0F)
}
}
impl From<MidiChannel> for u8 {
fn from(channel: MidiChannel) -> Self {
channel.0
}
}
impl TryFrom<u8> for MidiChannel {
type Error = Error;
fn try_from(channel: u8) -> Result<Self, Self::Error> {
Self::new(channel).ok_or_else(|| Error::MidiValue {
value: u16::from(channel),
max: u16::from(Self::MAX.0),
kind: "channel",
})
}
}
impl fmt::Display for MidiChannel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct PitchBend(u16);
impl PitchBend {
pub const MIN: Self = Self(0);
pub const MAX: Self = Self(16383);
pub const CENTRE: Self = Self(8192);
#[must_use]
pub const fn new(bend: u16) -> Option<Self> {
if bend > Self::MAX.0 {
return None;
}
Some(Self(bend))
}
#[must_use]
pub const fn get(self) -> u16 {
self.0
}
#[must_use]
#[allow(
clippy::cast_possible_wrap,
clippy::cast_possible_truncation,
reason = "the difference of two 14-bit values is in -8192..=8191"
)]
pub const fn offset(self) -> i16 {
self.0 as i16 - Self::CENTRE.0 as i16
}
const fn from_bits(low: u8, high: u8) -> Self {
Self(((high as u16 & 0x7F) << 7) | (low as u16 & 0x7F))
}
#[allow(
clippy::cast_possible_truncation,
reason = "each half is masked to seven bits"
)]
const fn to_bits(self) -> (u8, u8) {
((self.0 & 0x7F) as u8, (self.0 >> 7) as u8)
}
}
impl Default for PitchBend {
fn default() -> Self {
Self::CENTRE
}
}
impl From<PitchBend> for u16 {
fn from(bend: PitchBend) -> Self {
bend.0
}
}
impl TryFrom<u16> for PitchBend {
type Error = Error;
fn try_from(bend: u16) -> Result<Self, Self::Error> {
Self::new(bend).ok_or(Error::MidiValue {
value: bend,
max: Self::MAX.0,
kind: "pitch bend",
})
}
}
impl fmt::Display for PitchBend {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
#[derive(Debug)]
pub struct MidiOutput {
shared: Arc<Shared>,
task: Arc<AuxiliaryTask>,
untaken: Vec<bool>,
owner: ThreadId,
buffer: Vec<u8>,
}
impl MidiOutput {
pub fn open(port: &str, context: &SetupContext, capacity: usize) -> Result<Self, Error> {
let handle = MidiHandle::open(port)?;
let output = Self::assemble(handle, context.thread_count(), capacity)?;
output.task.schedule(context);
Ok(output)
}
fn assemble(handle: MidiHandle, threads: usize, capacity: usize) -> Result<Self, Error> {
let shared = Self::shared(handle, threads, capacity);
let weak = Arc::downgrade(&shared);
let mut scratch = Vec::with_capacity(shared.drain_bytes());
let name = format!(
"{DRAIN_TASK_NAME}-{}",
NEXT_DRAIN.fetch_add(1, Ordering::Relaxed)
);
let task = AuxiliaryTask::new(&name, DRAIN_PRIORITY, move || {
if let Some(shared) = weak.upgrade() {
shared.drain(&mut scratch);
}
})?;
Ok(Self::with_task(shared, task))
}
fn shared(handle: MidiHandle, threads: usize, capacity: usize) -> Arc<Shared> {
Arc::new(Shared {
midi: handle,
queues: (0..threads.max(1)).map(|_| Queue::new(capacity)).collect(),
draining: AtomicBool::new(false),
})
}
fn with_task(shared: Arc<Shared>, task: AuxiliaryTask) -> Self {
Self {
untaken: vec![true; shared.queues.len()],
buffer: Vec::with_capacity(shared.drain_bytes() + MESSAGE_MAX),
shared,
task: Arc::new(task),
owner: thread::current().id(),
}
}
pub fn take_sender(&mut self, thread: usize) -> Option<MidiSender> {
let untaken = self.untaken.get_mut(thread)?;
if !*untaken {
return None;
}
*untaken = false;
Some(MidiSender {
shared: Arc::clone(&self.shared),
task: Arc::clone(&self.task),
thread,
})
}
#[must_use]
pub fn capacity(&self) -> usize {
self.shared.queues[0].capacity()
}
pub fn send(&mut self, message: MidiMessage) -> Result<(), Error> {
if thread::current().id() != self.owner {
return Err(Error::MidiThread);
}
let (bytes, len) = message.to_bytes();
self.shared.drain_and_write(&mut self.buffer, &bytes[..len]);
Ok(())
}
pub fn flush(&mut self) -> Result<(), Error> {
if thread::current().id() != self.owner {
return Err(Error::MidiThread);
}
self.shared.drain_and_write(&mut self.buffer, &[]);
Ok(())
}
}
const DRAIN_TASK_NAME: &str = "bela-rs-midi-out";
static NEXT_DRAIN: AtomicUsize = AtomicUsize::new(0);
const DRAIN_PRIORITY: Priority = Priority::new(50).expect("50 is within Bela's priority range");
#[derive(Debug)]
pub struct MidiSender {
shared: Arc<Shared>,
task: Arc<AuxiliaryTask>,
thread: usize,
}
impl MidiSender {
pub fn send(
&mut self,
context: &impl CallbackContext,
message: MidiMessage,
) -> Result<(), Error> {
let (bytes, len) = message.to_bytes();
let queue = &self.shared.queues[self.thread];
queue.push(Slot::pack(bytes, len))?;
self.task.schedule(context);
Ok(())
}
#[must_use]
pub const fn thread(&self) -> usize {
self.thread
}
}
#[derive(Debug)]
struct Shared {
#[cfg_attr(
not(bela_device),
allow(dead_code, reason = "only the device build writes through it")
)]
midi: MidiHandle,
queues: Box<[Queue]>,
draining: AtomicBool,
}
impl Shared {
fn drain_bytes(&self) -> usize {
self.queues.iter().map(Queue::capacity).sum::<usize>() * MESSAGE_MAX
}
fn is_empty(&self) -> bool {
self.queues.iter().all(Queue::is_empty)
}
fn try_take(&self) -> bool {
!self.draining.swap(true, Ordering::Acquire)
}
fn release(&self) {
self.draining.store(false, Ordering::Release);
}
fn drain_taken(&self, buffer: &mut Vec<u8>, tail: &[u8]) {
buffer.clear();
self.collect(buffer);
buffer.extend_from_slice(tail);
if !buffer.is_empty() {
self.write(buffer);
}
}
fn collect(&self, buffer: &mut Vec<u8>) {
for queue in &self.queues {
for _ in 0..queue.capacity() {
let Some(slot) = queue.pop() else { break };
let (bytes, len) = slot.unpack();
buffer.extend_from_slice(&bytes[..len]);
}
}
}
fn drain(&self, buffer: &mut Vec<u8>) {
while self.try_take() {
self.drain_taken(buffer, &[]);
self.release();
if self.is_empty() {
return;
}
}
}
fn drain_and_write(&self, buffer: &mut Vec<u8>, tail: &[u8]) {
let mut tail = tail;
loop {
while !self.try_take() {
thread::yield_now();
}
self.drain_taken(buffer, tail);
tail = &[];
self.release();
if self.is_empty() {
return;
}
}
}
#[cfg(bela_device)]
fn write(&self, bytes: &[u8]) {
let length = u32::try_from(bytes.len()).unwrap_or(u32::MAX);
let _ = unsafe {
bela_sys::bela_midi_write_output(self.midi.raw.as_ptr(), bytes.as_ptr(), length)
};
}
#[cfg(not(bela_device))]
#[allow(
clippy::unused_self,
clippy::missing_const_for_fn,
reason = "mirrors the device signature; unreachable because no port can be opened"
)]
fn write(&self, _bytes: &[u8]) {}
}
#[derive(Debug)]
struct MidiHandle {
#[cfg(bela_device)]
raw: NonNull<bela_sys::BelaMidi>,
}
impl MidiHandle {
#[cfg(bela_device)]
fn open(port: &str) -> Result<Self, Error> {
let name = CString::new(port).map_err(|_| Error::MidiPortName)?;
let raw = NonNull::new(unsafe { bela_sys::bela_midi_new() }).ok_or(Error::MidiCreate)?;
let handle = Self { raw };
let opened = unsafe { bela_sys::bela_midi_write_to(handle.raw.as_ptr(), name.as_ptr()) };
if opened < 0 {
return Err(Error::MidiOpen(opened));
}
Ok(handle)
}
#[cfg(not(bela_device))]
#[allow(
clippy::missing_const_for_fn,
reason = "mirrors the device signature, which opens a device"
)]
fn open(_port: &str) -> Result<Self, Error> {
Err(Error::MidiUnavailable)
}
}
unsafe impl Send for MidiHandle {}
unsafe impl Sync for MidiHandle {}
impl Drop for MidiHandle {
fn drop(&mut self) {
#[cfg(bela_device)]
unsafe {
bela_sys::bela_midi_delete(self.raw.as_ptr());
}
}
}
const MESSAGE_MAX: usize = 3;
const _: () = assert!(
MESSAGE_MAX == bela_sys::BELA_MIDI_MESSAGE_MAX,
"what a message takes on the wire and what the shim writes have to agree"
);
const _: () = assert!(
MESSAGE_MAX == 3,
"a Slot holds three bytes and a length in one word, and the shifts in `pack` assume it"
);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Slot(u32);
#[allow(
clippy::cast_possible_truncation,
reason = "the length is clamped to 3, and each byte read back is one octet of the word"
)]
impl Slot {
fn pack(bytes: [u8; MESSAGE_MAX], len: usize) -> Self {
let mut packed = (len.min(MESSAGE_MAX) as u32) << 24;
for (index, byte) in bytes.iter().enumerate() {
packed |= u32::from(*byte) << (16 - index * 8);
}
Self(packed)
}
fn unpack(self) -> ([u8; MESSAGE_MAX], usize) {
let len = (self.0 >> 24) as usize;
let bytes = [(self.0 >> 16) as u8, (self.0 >> 8) as u8, self.0 as u8];
(bytes, len.min(MESSAGE_MAX))
}
}
#[derive(Debug)]
struct Queue {
slots: Box<[AtomicU32]>,
write: AtomicUsize,
read: AtomicUsize,
}
impl Queue {
fn new(capacity: usize) -> Self {
Self {
slots: (0..capacity.max(1)).map(|_| AtomicU32::new(0)).collect(),
write: AtomicUsize::new(0),
read: AtomicUsize::new(0),
}
}
fn capacity(&self) -> usize {
self.slots.len()
}
fn is_empty(&self) -> bool {
self.read.load(Ordering::Relaxed) == self.write.load(Ordering::Acquire)
}
fn push(&self, slot: Slot) -> Result<(), Error> {
let write = self.write.load(Ordering::Relaxed);
if write - self.read.load(Ordering::Acquire) == self.slots.len() {
return Err(Error::MidiQueueFull);
}
self.slots[write % self.slots.len()].store(slot.0, Ordering::Relaxed);
self.write.store(write + 1, Ordering::Release);
Ok(())
}
fn pop(&self) -> Option<Slot> {
let read = self.read.load(Ordering::Relaxed);
if read == self.write.load(Ordering::Acquire) {
return None;
}
let slot = Slot(self.slots[read % self.slots.len()].load(Ordering::Relaxed));
self.read.store(read + 1, Ordering::Release);
Some(slot)
}
}
#[cfg(test)]
mod tests {
#[cfg(not(bela_device))]
use core::time::Duration;
#[cfg(not(bela_device))]
use crate::context::tests::Fixture;
#[cfg(not(bela_device))]
use crate::task::test_handle;
use super::*;
#[test]
fn a_note_on_carries_channel_note_and_velocity() {
let message = MidiMessage::from_bytes(&[0x93, 60, 100]).expect("a note on should parse");
assert_eq!(
message,
MidiMessage::NoteOn {
channel: MidiChannel::new(3).unwrap(),
note: Note::new(60).unwrap(),
velocity: Velocity::new(100).unwrap(),
}
);
}
#[test]
fn a_note_on_with_velocity_zero_stays_a_note_on() {
let message = MidiMessage::from_bytes(&[0x90, 60, 0]).expect("a note on should parse");
assert!(
matches!(message, MidiMessage::NoteOn { velocity, .. } if velocity.get() == 0),
"expected a note on with velocity 0, got {message:?}"
);
}
#[test]
fn a_pitch_bend_reads_low_byte_first() {
let message =
MidiMessage::from_bytes(&[0xE0, 0x00, 0x40]).expect("a pitch bend should parse");
assert_eq!(
message,
MidiMessage::PitchBend {
channel: MidiChannel::new(0).unwrap(),
bend: PitchBend::CENTRE,
},
"0x00 0x40 is 8192, the centre"
);
}
#[test]
fn every_channel_message_parses_whole() {
let channel = MidiChannel::MAX;
let note = Note::new(60).unwrap();
let expected = [
(
[0x8F, 60, 0].as_slice(),
MidiMessage::NoteOff {
channel,
note,
velocity: Velocity::new(0).unwrap(),
},
),
(
[0x9F, 60, 1].as_slice(),
MidiMessage::NoteOn {
channel,
note,
velocity: Velocity::new(1).unwrap(),
},
),
(
[0xAF, 60, 1].as_slice(),
MidiMessage::KeyPressure {
channel,
note,
pressure: Pressure::new(1).unwrap(),
},
),
(
[0xBF, 7, 100].as_slice(),
MidiMessage::ControlChange {
channel,
controller: Controller::new(7).unwrap(),
value: ControlValue::new(100).unwrap(),
},
),
(
[0xCF, 5].as_slice(),
MidiMessage::ProgramChange {
channel,
program: Program::new(5).unwrap(),
},
),
(
[0xDF, 50].as_slice(),
MidiMessage::ChannelPressure {
channel,
pressure: Pressure::new(50).unwrap(),
},
),
(
[0xEF, 0, 0x40].as_slice(),
MidiMessage::PitchBend {
channel,
bend: PitchBend::CENTRE,
},
),
];
for (bytes, message) in expected {
let parsed = MidiMessage::from_bytes(bytes).expect("should parse");
assert_eq!(parsed, message, "{bytes:02x?} parsed as something else");
assert_eq!(
parsed.channel(),
Some(channel),
"{message:?} lost its channel"
);
}
}
#[test]
fn system_real_time_messages_have_no_channel() {
let expected = [
(0xF8, MidiMessage::Clock),
(0xFA, MidiMessage::Start),
(0xFB, MidiMessage::Continue),
(0xFC, MidiMessage::Stop),
(0xFE, MidiMessage::ActiveSensing),
(0xFF, MidiMessage::Reset),
];
for (status, message) in expected {
let parsed = MidiMessage::from_bytes(&[status]).expect("should parse");
assert_eq!(parsed, message, "status {status:#04x}");
assert_eq!(parsed.channel(), None, "{message:?} should have no channel");
}
}
#[test]
fn the_undefined_system_messages_are_skipped() {
for status in [0xF9u8, 0xFD] {
assert_eq!(
MidiMessage::from_bytes(&[status]),
None,
"{status:#04x} is undefined and has nothing to report"
);
}
}
#[test]
fn a_truncated_message_is_not_half_a_message() {
assert_eq!(MidiMessage::from_bytes(&[]), None, "no status byte");
assert_eq!(MidiMessage::from_bytes(&[0x90]), None, "no note number");
}
#[test]
fn seven_bit_values_reject_the_eighth_bit() {
assert_eq!(Note::new(127).map(Note::get), Some(127));
assert_eq!(Note::new(128), None, "128 needs eight bits");
assert_eq!(Velocity::new(255), None);
assert_eq!(Controller::MAX.get(), 127);
}
#[test]
fn a_value_converts_both_ways() {
assert_eq!(Note::try_from(60), Ok(Note::new(60).unwrap()));
assert_eq!(u8::from(Note::new(60).unwrap()), 60);
assert_eq!(
Velocity::try_from(128),
Err(Error::MidiValue {
value: 128,
max: 127,
kind: "velocity"
})
);
assert_eq!(
MidiChannel::try_from(16),
Err(Error::MidiValue {
value: 16,
max: 15,
kind: "channel"
})
);
assert_eq!(PitchBend::try_from(8192), Ok(PitchBend::CENTRE));
assert_eq!(
PitchBend::try_from(16384),
Err(Error::MidiValue {
value: 16384,
max: 16383,
kind: "pitch bend"
})
);
}
#[test]
fn two_values_in_the_same_range_fail_differently() {
let note = Note::try_from(200).unwrap_err();
let velocity = Velocity::try_from(200).unwrap_err();
assert_ne!(note, velocity, "the same bytes, and not the same error");
assert!(
note.to_string().contains("note number"),
"expected the type in the message, got: {note}"
);
}
#[test]
fn a_channel_is_one_of_sixteen() {
assert_eq!(MidiChannel::new(15).map(MidiChannel::get), Some(15));
assert_eq!(MidiChannel::new(16), None, "there are sixteen channels");
}
#[test]
fn a_pitch_bend_measures_from_the_centre() {
assert_eq!(PitchBend::CENTRE.offset(), 0);
assert_eq!(PitchBend::new(0).map(PitchBend::offset), Some(-8192));
assert_eq!(PitchBend::MAX.offset(), 8191);
assert_eq!(PitchBend::new(16384), None, "14 bits is 16383");
assert_eq!(PitchBend::default(), PitchBend::CENTRE);
}
#[test]
#[cfg(not(bela_device))]
fn the_iterator_ends_when_the_ring_is_empty() {
let mut input = MidiInput {};
assert_eq!(input.messages().count(), 0);
}
#[test]
fn a_message_survives_the_round_trip_to_bytes() {
let messages = [
MidiMessage::NoteOff {
channel: MidiChannel::MAX,
note: Note::MIDDLE_C,
velocity: Velocity::MIN,
},
MidiMessage::NoteOn {
channel: MidiChannel::MIN,
note: Note::MAX,
velocity: Velocity::MAX,
},
MidiMessage::KeyPressure {
channel: MidiChannel::new(9).unwrap(),
note: Note::MIDDLE_C,
pressure: Pressure::new(64).unwrap(),
},
MidiMessage::ControlChange {
channel: MidiChannel::new(1).unwrap(),
controller: Controller::new(7).unwrap(),
value: ControlValue::new(100).unwrap(),
},
MidiMessage::ProgramChange {
channel: MidiChannel::new(2).unwrap(),
program: Program::new(5).unwrap(),
},
MidiMessage::ChannelPressure {
channel: MidiChannel::new(3).unwrap(),
pressure: Pressure::new(80).unwrap(),
},
MidiMessage::PitchBend {
channel: MidiChannel::new(4).unwrap(),
bend: PitchBend::CENTRE,
},
MidiMessage::Clock,
MidiMessage::Start,
MidiMessage::Continue,
MidiMessage::Stop,
MidiMessage::ActiveSensing,
MidiMessage::Reset,
];
for message in messages {
let (bytes, len) = message.to_bytes();
assert_eq!(
MidiMessage::from_bytes(&bytes[..len]),
Some(message),
"{message:?} came back as something else"
);
}
}
#[test]
fn a_pitch_bend_goes_out_low_byte_first() {
let (bytes, len) = MidiMessage::PitchBend {
channel: MidiChannel::MIN,
bend: PitchBend::CENTRE,
}
.to_bytes();
assert_eq!(
(&bytes[..len], len),
([0xE0, 0x00, 0x40].as_slice(), 3),
"8192 is 0x00 0x40 on the wire"
);
}
#[test]
fn a_slot_carries_the_message_and_its_length() {
for (bytes, len) in [([0x90, 60, 100], 3), ([0xC0, 5, 0], 2), ([0xF8, 0, 0], 1)] {
let slot = Slot::pack(bytes, len);
let (back, back_len) = slot.unpack();
assert_eq!(back_len, len, "length changed");
assert_eq!(&back[..len], &bytes[..len], "bytes changed");
}
}
#[test]
fn a_queue_returns_messages_in_order() {
let queue = Queue::new(4);
for note in 0..4u8 {
queue
.push(Slot::pack([0x90, note, 100], 3))
.expect("the queue has room for four");
}
for note in 0..4u8 {
let (bytes, len) = queue.pop().expect("four were pushed").unpack();
assert_eq!((&bytes[..len], len), ([0x90, note, 100].as_slice(), 3));
}
assert!(queue.pop().is_none(), "and then it is empty");
}
#[test]
fn a_full_queue_refuses_rather_than_overwrites() {
let queue = Queue::new(2);
assert_eq!(queue.capacity(), 2, "capacity is what was asked for");
queue.push(Slot::pack([0x90, 1, 1], 3)).expect("first");
queue.push(Slot::pack([0x90, 2, 2], 3)).expect("second");
assert_eq!(
queue.push(Slot::pack([0x90, 3, 3], 3)),
Err(Error::MidiQueueFull),
"a third does not fit"
);
let (bytes, _) = queue.pop().expect("first is still there").unpack();
assert_eq!(bytes[1], 1);
queue
.push(Slot::pack([0x90, 3, 3], 3))
.expect("and now there is room again");
}
#[test]
fn a_queue_wraps_around_its_slots() {
let queue = Queue::new(2);
for round in 0..6u8 {
queue.push(Slot::pack([0x90, round, 64], 3)).expect("room");
let (bytes, _) = queue.pop().expect("just pushed").unpack();
assert_eq!(bytes[1], round, "round {round} came back wrong");
}
}
#[test]
fn a_queue_survives_a_writer_and_a_reader_at_once() {
use std::thread;
const MESSAGES: u8 = 100;
let queue = Arc::new(Queue::new(4));
let writer = Arc::clone(&queue);
let sender = thread::spawn(move || {
let mut note = 0;
while note < MESSAGES {
if writer.push(Slot::pack([0x90, note, 64], 3)).is_ok() {
note += 1;
}
}
});
let mut received = Vec::new();
while received.len() < usize::from(MESSAGES) {
if let Some(slot) = queue.pop() {
let (bytes, _) = slot.unpack();
received.push(bytes[1]);
}
}
sender.join().expect("the writer should not panic");
assert_eq!(
received,
(0..MESSAGES).collect::<Vec<_>>(),
"every message once, in order"
);
}
#[cfg(not(bela_device))]
fn output(threads: usize, capacity: usize) -> MidiOutput {
let handle = MidiHandle {};
let shared = MidiOutput::shared(handle, threads, capacity);
MidiOutput::with_task(shared, test_handle())
}
#[cfg(not(bela_device))]
fn note(number: u8) -> Slot {
Slot::pack([0x90, number, 64], 3)
}
#[test]
#[cfg(not(bela_device))]
fn a_drain_takes_the_queues_in_thread_order() {
let output = output(3, 4);
output.shared.queues[2].push(note(2)).unwrap();
output.shared.queues[0].push(note(0)).unwrap();
output.shared.queues[1].push(note(1)).unwrap();
let mut buffer = Vec::new();
output.shared.collect(&mut buffer);
assert_eq!(
buffer,
vec![0x90, 0, 64, 0x90, 1, 64, 0x90, 2, 64],
"thread 0's messages, then thread 1's, then thread 2's"
);
assert!(output.shared.is_empty(), "and nothing is left");
}
#[test]
#[cfg(not(bela_device))]
fn a_second_drain_takes_nothing_while_one_is_running() {
let output = output(1, 4);
output.shared.queues[0].push(note(60)).unwrap();
assert!(output.shared.try_take(), "the drain is free to take");
let mut buffer = Vec::new();
output.shared.drain(&mut buffer);
assert!(
!output.shared.is_empty(),
"the message belongs to whoever holds the drain"
);
output.shared.release();
output.shared.drain(&mut buffer);
assert!(output.shared.is_empty(), "and is taken once it is free");
}
#[test]
#[cfg(not(bela_device))]
fn a_drain_keeps_going_while_a_thread_is_still_pushing() {
const MESSAGES: u8 = 200;
let output = output(1, 4);
let queue = Arc::clone(&output.shared);
let pushing = thread::spawn(move || {
let mut sent = 0;
while sent < MESSAGES {
if queue.queues[0].push(note(sent)).is_ok() {
sent += 1;
}
}
});
let mut buffer = Vec::new();
let mut taken = 0;
while taken < usize::from(MESSAGES) {
output.shared.drain_taken(&mut buffer, &[]);
taken += buffer.len() / 3;
assert!(
buffer.len() <= output.capacity() * MESSAGE_MAX,
"one pass took {} bytes, more than the declared budget",
buffer.len()
);
}
pushing.join().expect("the writer should not panic");
output.shared.drain(&mut buffer);
assert!(output.shared.is_empty(), "the drain ends with nothing left");
}
#[test]
#[cfg(not(bela_device))]
fn a_sender_is_handed_out_once() {
let mut output = output(2, 4);
assert!(output.take_sender(0).is_some(), "thread 0's, once");
assert!(
output.take_sender(0).is_none(),
"and not again: one sender is one writer"
);
assert!(output.take_sender(1).is_some(), "thread 1 has its own");
assert!(
output.take_sender(2).is_none(),
"and a thread that does not render has none"
);
}
#[test]
#[cfg(not(bela_device))]
fn a_queue_that_could_hold_nothing_holds_one() {
assert_eq!(output(1, 4).capacity(), 4, "what was asked for");
assert_eq!(
output(1, 0).capacity(),
1,
"a queue that can hold nothing is an output that can send nothing"
);
}
#[test]
#[cfg(not(bela_device))]
fn a_sender_queues_until_the_budget_is_spent() {
let mut fixture = Fixture::new();
let mut output = output(1, 2);
let mut sender = output.take_sender(0).expect("thread 0's sender");
let first = MidiMessage::NoteOn {
channel: MidiChannel::MIN,
note: Note::MIDDLE_C,
velocity: Velocity::MAX,
};
let second = MidiMessage::Stop;
assert_eq!(sender.send(fixture.render(0), first), Ok(()));
assert_eq!(sender.send(fixture.render(0), second), Ok(()));
assert_eq!(
sender.send(fixture.render(0), MidiMessage::Clock),
Err(Error::MidiQueueFull),
"two is what this output was opened for"
);
let mut buffer = Vec::new();
output.shared.collect(&mut buffer);
assert_eq!(
buffer,
vec![0x90, 60, 127, 0xFC],
"both messages, in the order they were sent, and not the refused one"
);
assert_eq!(sender.send(fixture.render(0), MidiMessage::Clock), Ok(()));
}
#[test]
#[cfg(not(bela_device))]
fn a_flush_empties_the_queues() {
let mut fixture = Fixture::new();
let mut output = output(1, 4);
let mut sender = output.take_sender(0).expect("thread 0's sender");
sender
.send(fixture.render(0), MidiMessage::Clock)
.expect("room for one");
assert_eq!(output.flush(), Ok(()), "on the thread that opened it");
assert!(
output.shared.is_empty(),
"a flush takes what the task would have"
);
}
#[test]
#[cfg(not(bela_device))]
fn a_flush_waits_for_a_drain_it_cannot_take() {
let mut output = output(1, 4);
let shared = Arc::clone(&output.shared);
let released = Arc::new(AtomicBool::new(false));
let published = Arc::clone(&released);
assert!(shared.try_take(), "take the drain out from under it");
let holder = thread::spawn(move || {
thread::sleep(Duration::from_millis(50));
published.store(true, Ordering::Release);
shared.release();
});
output.flush().expect("on the owning thread");
assert!(
released.load(Ordering::Acquire),
"the flush returned while another drain still held it"
);
holder.join().expect("the holder should not panic");
}
#[test]
#[cfg(not(bela_device))]
fn a_queue_set_is_never_empty() {
let output = output(0, 4);
assert_eq!(output.shared.queues.len(), 1);
assert_eq!(output.capacity(), 4, "and it is a usable queue");
}
#[test]
#[cfg(not(bela_device))]
fn a_send_lands_behind_what_was_queued() {
let output = output(2, 4);
output.shared.queues[0].push(note(1)).unwrap();
output.shared.queues[1].push(note(2)).unwrap();
let mut buffer = Vec::new();
let (tail, len) = MidiMessage::Stop.to_bytes();
output.shared.drain_taken(&mut buffer, &tail[..len]);
assert_eq!(
buffer,
vec![0x90, 1, 64, 0x90, 2, 64, 0xFC],
"both queues, then the message this call is for"
);
}
#[test]
#[cfg(not(bela_device))]
fn only_the_thread_that_opened_the_port_may_send() {
let mut output = output(1, 4);
let elsewhere = thread::spawn(move || {
let sent = output.send(MidiMessage::Stop);
let flushed = output.flush();
(sent, flushed)
});
let (sent, flushed) = elsewhere.join().expect("the thread should not panic");
assert_eq!(sent, Err(Error::MidiThread), "send from another thread");
assert_eq!(flushed, Err(Error::MidiThread), "and flush");
}
#[test]
#[cfg(not(bela_device))]
fn no_port_can_be_opened_for_output_off_device() {
let mut fixture = Fixture::with_threads(2);
assert_eq!(
MidiOutput::open("hw:0,0,0", fixture.setup(), 8).unwrap_err(),
Error::MidiUnavailable,
"off-device there is no libbelaextra to open a port with"
);
}
#[test]
#[cfg(not(bela_device))]
fn the_drain_task_is_the_second_thing_a_host_build_cannot_have() {
assert_eq!(
MidiOutput::assemble(MidiHandle {}, 2, 8).unwrap_err(),
Error::TaskUnavailable,
"off-device there is no audio system to create the drain task in"
);
}
#[test]
fn a_missing_library_and_a_refused_object_do_not_read_alike() {
let unavailable = Error::MidiUnavailable.to_string();
let refused = Error::MidiCreate.to_string();
assert_ne!(unavailable, refused);
assert!(
unavailable.contains("libbelaextra"),
"a build with no library should name the library: {unavailable}"
);
assert!(
!refused.contains("libbelaextra"),
"a board refusing an object should not read like a missing library: {refused}"
);
}
#[test]
#[cfg(not(bela_device))]
fn no_port_can_be_opened_off_device() {
assert_eq!(
MidiInput::open("hw:0,0,0").unwrap_err(),
Error::MidiUnavailable,
"off-device there is no libbelaextra to open a port with"
);
assert!(
midi_ports().is_empty(),
"and nothing to list ports with either"
);
}
}