primitives::midi
MIDI 1.0 messages and real-time-safe event plumbing for the Superb DAW SDK.
Purpose
- [
MidiMessage] — a decoded channel-voice or system message (NoteOn,ControlChange,PitchBend, transport bytes, ...).Copy, heap-free, withto_bytes/from_bytesfor wire encoding. - [
MidiEvent] / [MidiEventBuffer] — a message stamped with a sample offset (frame) within the current process block, stored in a fixed-capacity buffer allocated once so pushing on the audio thread never allocates. - [
TimestampedMessage] / [timing::FrameMapper] — the non-RT-thread equivalent (microsecond timestamp instead of a frame offset) and the converter between the two clocks. inputmodule (featuremidi-io, off by default) — lists hardware MIDI input ports and bridges them into an SPSC queue.
Building a buffer and parsing bytes
use superb::primitives::midi::{MidiChannel, MidiEvent, MidiEventBuffer, MidiMessage};
let channel = MidiChannel::new(0).unwrap();
// Decode a wire message.
let note_on = MidiMessage::from_bytes(&[0x90, 60, 100]).unwrap();
assert_eq!(note_on, MidiMessage::NoteOn { channel, note: 60, velocity: 100 });
// Stamp it with a frame offset and store it in a fixed-capacity, allocated-once buffer.
let mut buffer = MidiEventBuffer::with_capacity(256);
buffer.push(MidiEvent { frame: 0, message: note_on }).unwrap();
buffer
.push_sorted(MidiEvent { frame: 10, message: MidiMessage::note_off(channel, 60, 0).unwrap() })
.unwrap();
// Only the events landing in this sub-block:
for event in buffer.iter_range(0..8) {
println!("{:?}", event);
}
// Encode back to bytes.
let mut wire = [0u8; 3];
let n = note_on.to_bytes(&mut wire);
assert_eq!(&wire[..n], &[0x90, 60, 100]);NoteOn with velocity 0 is normalised to NoteOff by both from_bytes
and the note_on constructor, matching standard MIDI convention.
The midi-io feature and the non-RT -> RT queue pattern
The midi-io feature is not part of default-features, so the crate
(and CI) builds without linking ALSA/CoreMIDI/WinMM. Enable it explicitly:
superb = { version = "0.1", features = ["midi-io"] }Hardware MIDI input runs on midir's own callback thread, which is not
real-time safe (it can allocate, lock, etc.), so it must never touch the
audio thread's state directly. MidiInputBridge::connect sets up the
standard pattern: the midir callback (non-RT producer) parses bytes into a
MidiMessage and pushes a TimestampedMessage into a superb::primitives::realtime::spsc
queue; the audio thread (RT consumer) drains it with Consumer::pop /
Consumer::drain, using timing::FrameMapper to turn each
timestamp_us into a block-relative frame:
use superb::primitives::midi::{input, timing::FrameMapper};
use superb::primitives::SampleRate;
let ports = input::list_input_ports()?;
let (bridge, mut consumer) = input::MidiInputBridge::connect(0, /* queue_capacity */ 256)?;
let mut mapper = FrameMapper::new(SampleRate::HZ_48K);
// In the audio callback, once per block:
mapper.set_block(block_start_us, frames_in_block);
while let Some(msg) = consumer.pop() {
let frame = mapper.frame_for(msg.timestamp_us);
// event_buffer.push(MidiEvent { frame, message: msg.message })...
}
// If the queue filled up (audio thread fell behind), bridge.dropped()
// counts how many messages were discarded rather than blocking the
// callback thread.
println!("dropped so far: {}", bridge.dropped());Messages that fail to parse (SysEx, MTC quarter frames, malformed bytes) are silently ignored by the bridge rather than being queued.
superb::primitives::io::Engine already contains this loop: attach the consumer with
Engine::set_midi_input and every render runs set_block on the engine's
stream clock (or the cpal callback timestamp under device::run_engine)
and frame_for on each message (MidiTiming::Stream, the default). Note
that midir's timestamp_us epoch is midir's own connection clock, not
cpal's stream clock — see docs/primitives/io.md for the consequences.
Limitations
- No SysEx (
0xF0..0xF7) —from_bytesreturns an error. - No MIDI Time Code quarter frames (
0xF1). - No running status — every call to
from_bytesmust start with a status byte; the wire optimisation of omitting repeated status bytes is not implemented. - No Standard MIDI File (SMF) reading or writing — file formats belong
in
primitives::io.
These are deliberately left out rather than half-implemented; the current
scope covers everything the real-time path in primitives::graph and
primitives::mixer needs.