pub mod downloader;
pub mod instruments;
pub mod mixer;
pub mod paths;
pub mod scheduler;
pub mod smf;
pub mod tuning;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use oxideav_core::{
AudioFrame, CodecCapabilities, CodecId, CodecInfo, CodecParameters, CodecRegistry, Decoder,
Error, Frame, Packet, Result,
};
use crate::instruments::dls::DlsInstrument;
use crate::instruments::sf2::Sf2Instrument;
use crate::instruments::sfz::SfzInstrument;
use crate::instruments::tone::ToneInstrument;
use crate::instruments::Instrument;
use crate::mixer::Mixer;
use crate::scheduler::Scheduler;
pub const CODEC_ID_STR: &str = "midi";
pub const OUTPUT_SAMPLE_RATE: u32 = 44_100;
pub const FRAME_SAMPLES: usize = 1024;
pub const OUTPUT_CHANNELS: u16 = 2;
pub fn register_codecs(reg: &mut CodecRegistry) {
let caps = CodecCapabilities::audio("midi_synth")
.with_lossy(false)
.with_lossless(true)
.with_intra_only(false)
.with_max_channels(OUTPUT_CHANNELS);
reg.register(
CodecInfo::new(CodecId::new(CODEC_ID_STR))
.capabilities(caps)
.decoder(make_decoder),
);
}
fn make_decoder(_params: &CodecParameters) -> Result<Box<dyn Decoder>> {
Ok(Box::new(MidiDecoder::new(
Arc::new(ToneInstrument::new()),
OUTPUT_SAMPLE_RATE,
)))
}
pub struct MidiDecoder {
codec_id: CodecId,
instrument: Arc<dyn Instrument>,
sample_rate: u32,
scheduler: Option<Scheduler>,
mixer: Mixer,
left: Vec<f32>,
right: Vec<f32>,
next_pts: i64,
drained: bool,
finished: bool,
tail_chunks_remaining: usize,
}
impl MidiDecoder {
pub const TAIL_CHUNK_CAP: usize = 32;
pub fn new(instrument: Arc<dyn Instrument>, sample_rate: u32) -> Self {
Self {
codec_id: CodecId::new(CODEC_ID_STR),
instrument,
sample_rate,
scheduler: None,
mixer: Mixer::new(),
left: vec![0.0; FRAME_SAMPLES],
right: vec![0.0; FRAME_SAMPLES],
next_pts: 0,
drained: false,
finished: false,
tail_chunks_remaining: Self::TAIL_CHUNK_CAP,
}
}
pub fn with_instrument(instrument: Arc<dyn Instrument>) -> Self {
Self::new(instrument, OUTPUT_SAMPLE_RATE)
}
pub fn with_instrument_source(source: InstrumentSource) -> Result<Self> {
let inst = source.load()?;
Ok(Self::new(inst, OUTPUT_SAMPLE_RATE))
}
}
#[derive(Clone, Debug)]
pub enum InstrumentSource {
Sf2(PathBuf),
Sfz(PathBuf),
Dls(PathBuf),
Tone,
}
impl InstrumentSource {
pub fn load(self) -> Result<Arc<dyn Instrument>> {
match self {
InstrumentSource::Sf2(p) => Ok(Arc::new(Sf2Instrument::open(&p)?)),
InstrumentSource::Sfz(p) => Ok(Arc::new(SfzInstrument::open(&p)?)),
InstrumentSource::Dls(p) => Ok(Arc::new(DlsInstrument::open(&p)?)),
InstrumentSource::Tone => Ok(Arc::new(ToneInstrument::new())),
}
}
pub fn sf2(path: impl AsRef<Path>) -> Self {
Self::Sf2(path.as_ref().to_path_buf())
}
pub fn sfz(path: impl AsRef<Path>) -> Self {
Self::Sfz(path.as_ref().to_path_buf())
}
pub fn dls(path: impl AsRef<Path>) -> Self {
Self::Dls(path.as_ref().to_path_buf())
}
}
impl MidiDecoder {
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
pub fn scheduler(&self) -> Option<&Scheduler> {
self.scheduler.as_ref()
}
fn build_audio_frame(&mut self) -> Frame {
let n = self.left.len();
let mut bytes = Vec::with_capacity(n * 2 * 2); for i in 0..n {
let l = (self.left[i].clamp(-1.0, 1.0) * 32_767.0) as i16;
let r = (self.right[i].clamp(-1.0, 1.0) * 32_767.0) as i16;
bytes.extend_from_slice(&l.to_le_bytes());
bytes.extend_from_slice(&r.to_le_bytes());
}
let pts = Some(self.next_pts);
self.next_pts = self.next_pts.saturating_add(n as i64);
Frame::Audio(AudioFrame {
samples: n as u32,
pts,
data: vec![bytes],
})
}
}
impl Decoder for MidiDecoder {
fn codec_id(&self) -> &CodecId {
&self.codec_id
}
fn send_packet(&mut self, packet: &Packet) -> Result<()> {
if packet.data.len() < 4 || &packet.data[0..4] != b"MThd" {
return Err(Error::invalid(
"MIDI: packet does not start with the 'MThd' header chunk",
));
}
let smf = crate::smf::parse(&packet.data)?;
self.scheduler = Some(Scheduler::new(&smf, self.sample_rate));
self.mixer.all_notes_off();
self.next_pts = 0;
self.drained = false;
self.finished = false;
self.tail_chunks_remaining = Self::TAIL_CHUNK_CAP;
Ok(())
}
fn receive_frame(&mut self) -> Result<Frame> {
if self.finished {
return Err(Error::Eof);
}
let scheduler = self.scheduler.as_mut().ok_or(Error::NeedMore)?;
let was_done = scheduler.is_done();
let now_done = scheduler.step(FRAME_SAMPLES, &mut self.mixer, self.instrument.as_ref());
if was_done || now_done {
self.drained = true;
}
let active = self.mixer.mix_stereo(&mut self.left, &mut self.right);
if self.drained {
if active == 0 || self.tail_chunks_remaining == 0 {
self.finished = true;
return Ok(self.build_audio_frame());
}
self.tail_chunks_remaining = self.tail_chunks_remaining.saturating_sub(1);
}
Ok(self.build_audio_frame())
}
fn flush(&mut self) -> Result<()> {
if let Some(s) = self.scheduler.as_mut() {
s.step(u32::MAX as usize, &mut self.mixer, self.instrument.as_ref());
}
Ok(())
}
fn reset(&mut self) -> Result<()> {
self.scheduler = None;
self.mixer.all_notes_off();
self.next_pts = 0;
self.drained = false;
self.finished = false;
self.tail_chunks_remaining = Self::TAIL_CHUNK_CAP;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use oxideav_core::TimeBase;
fn minimal_smf() -> Vec<u8> {
let mut b = vec![];
b.extend_from_slice(b"MThd");
b.extend_from_slice(&6u32.to_be_bytes());
b.extend_from_slice(&0u16.to_be_bytes());
b.extend_from_slice(&1u16.to_be_bytes());
b.extend_from_slice(&96u16.to_be_bytes());
b.extend_from_slice(b"MTrk");
b.extend_from_slice(&4u32.to_be_bytes());
b.extend_from_slice(&[0x00, 0xFF, 0x2F, 0x00]);
b
}
fn five_second_smf() -> Vec<u8> {
let mut blob = Vec::new();
blob.extend_from_slice(b"MThd");
blob.extend_from_slice(&6u32.to_be_bytes());
blob.extend_from_slice(&1u16.to_be_bytes()); blob.extend_from_slice(&3u16.to_be_bytes()); blob.extend_from_slice(&480u16.to_be_bytes());
let mut t1: Vec<u8> = Vec::new();
t1.extend_from_slice(&[0x00, 0xFF, 0x51, 0x03, 0x07, 0xA1, 0x20]);
t1.extend_from_slice(&[0x84, 0x58, 0xFF, 0x51, 0x03, 0x03, 0xD0, 0x90]);
t1.extend_from_slice(&[0x84, 0x58, 0xFF, 0x2F, 0x00]);
push_track(&mut blob, &t1);
let mut t2: Vec<u8> = Vec::new();
t2.extend_from_slice(&[0x00, 0x91, 0x3C, 0x64]);
t2.extend_from_slice(&[0x81, 0x70, 0x81, 0x3C, 0x40]);
t2.extend_from_slice(&[0x00, 0x91, 0x40, 0x64]);
t2.extend_from_slice(&[0x81, 0x70, 0x81, 0x40, 0x40]);
t2.extend_from_slice(&[0x85, 0x50, 0xFF, 0x2F, 0x00]);
push_track(&mut blob, &t2);
let mut t3: Vec<u8> = Vec::new();
t3.extend_from_slice(&[0x00, 0x99, 0x24, 0x64]);
t3.extend_from_slice(&[0x83, 0x60, 0x89, 0x24, 0x40]);
t3.extend_from_slice(&[0x85, 0x50, 0xFF, 0x2F, 0x00]);
push_track(&mut blob, &t3);
blob
}
fn push_track(blob: &mut Vec<u8>, events: &[u8]) {
blob.extend_from_slice(b"MTrk");
blob.extend_from_slice(&(events.len() as u32).to_be_bytes());
blob.extend_from_slice(events);
}
#[test]
fn registers_codec_under_midi_id() {
let mut reg = CodecRegistry::new();
register_codecs(&mut reg);
assert!(reg.has_decoder(&CodecId::new(CODEC_ID_STR)));
}
#[test]
fn decoder_rejects_non_smf_packets() {
let mut reg = CodecRegistry::new();
register_codecs(&mut reg);
let params = CodecParameters::audio(CodecId::new(CODEC_ID_STR));
let mut dec = reg.first_decoder(¶ms).unwrap();
let pkt = Packet::new(0, TimeBase::new(1, 44_100), b"not midi".to_vec());
let err = dec.send_packet(&pkt).unwrap_err();
assert!(matches!(err, Error::InvalidData(_)));
}
#[test]
fn empty_smf_produces_eof_after_initial_chunks() {
let mut reg = CodecRegistry::new();
register_codecs(&mut reg);
let params = CodecParameters::audio(CodecId::new(CODEC_ID_STR));
let mut dec = reg.first_decoder(¶ms).unwrap();
let pkt = Packet::new(0, TimeBase::new(1, 44_100), minimal_smf());
dec.send_packet(&pkt).unwrap();
let _ = dec.receive_frame().expect("initial chunk");
let mut got_eof = false;
for _ in 0..4 {
match dec.receive_frame() {
Err(Error::Eof) => {
got_eof = true;
break;
}
Ok(_) => continue,
Err(other) => panic!("unexpected error {other:?}"),
}
}
assert!(got_eof, "decoder should drain to Eof on an empty SMF");
}
#[test]
fn end_to_end_five_second_smf_produces_pcm() {
let mut dec = MidiDecoder::new(Arc::new(ToneInstrument::new()), OUTPUT_SAMPLE_RATE);
let blob = five_second_smf();
let pkt = Packet::new(0, TimeBase::new(1, 44_100), blob);
dec.send_packet(&pkt).unwrap();
let mut all_samples: Vec<i16> = Vec::new();
let mut frame_count = 0;
for _ in 0..1024 {
match dec.receive_frame() {
Ok(Frame::Audio(af)) => {
assert_eq!(af.samples, FRAME_SAMPLES as u32);
assert_eq!(af.data.len(), 1, "interleaved S16 = single plane");
let bytes = &af.data[0];
assert_eq!(bytes.len(), FRAME_SAMPLES * 4, "stereo S16 = 4 bytes/frame");
for chunk in bytes.chunks_exact(2) {
all_samples.push(i16::from_le_bytes([chunk[0], chunk[1]]));
}
frame_count += 1;
}
Ok(_) => panic!("expected Audio frame"),
Err(Error::Eof) => break,
Err(other) => panic!("unexpected error: {other:?}"),
}
}
let per_channel = all_samples.len() / 2;
assert!(
per_channel >= 30_000,
"expected ≥ 30 k samples (~0.7 s) of audio, got {} samples / channel ({} frames)",
per_channel,
frame_count,
);
let nonzero = all_samples.iter().filter(|s| s.abs() > 16).count();
let nonzero_ratio = nonzero as f64 / all_samples.len() as f64;
assert!(
nonzero_ratio > 0.05,
"audio is mostly silent: {:.2}% non-zero",
nonzero_ratio * 100.0,
);
let peak = all_samples
.iter()
.map(|s| s.unsigned_abs())
.max()
.unwrap_or(0);
assert!(
peak > 327,
"peak {} too quiet — synth is producing near-silent output",
peak,
);
assert!(
peak < 32_767,
"peak {} indicates clipping — mix bus should have headroom",
peak,
);
}
#[test]
fn end_to_end_with_sf2_fixture() {
use crate::instruments::sf2::Sf2Instrument;
let blob = build_looping_sf2_fixture();
let inst = Sf2Instrument::from_bytes("fixture", &blob).expect("parse fixture");
let mut dec = MidiDecoder::new(Arc::new(inst), OUTPUT_SAMPLE_RATE);
let smf = five_second_smf();
let pkt = Packet::new(0, TimeBase::new(1, 44_100), smf);
dec.send_packet(&pkt).unwrap();
let mut all_samples: Vec<i16> = Vec::new();
for _ in 0..1024 {
match dec.receive_frame() {
Ok(Frame::Audio(af)) => {
for chunk in af.data[0].chunks_exact(2) {
all_samples.push(i16::from_le_bytes([chunk[0], chunk[1]]));
}
}
Err(Error::Eof) => break,
Ok(_) => panic!("expected Audio frame"),
Err(other) => panic!("error: {other:?}"),
}
}
assert!(
all_samples.len() / 2 >= 30_000,
"expected ≥ 30 k samples / channel, got {}",
all_samples.len() / 2,
);
let nonzero = all_samples.iter().filter(|s| s.abs() > 16).count();
assert!(
nonzero > all_samples.len() / 20,
"expected ≥ 5 % non-silent samples, got {} / {}",
nonzero,
all_samples.len(),
);
let peak = all_samples
.iter()
.map(|s| s.unsigned_abs())
.max()
.unwrap_or(0);
assert!(peak > 327, "SF2 fixture rendered too quiet (peak {peak})");
}
fn build_looping_sf2_fixture() -> Vec<u8> {
let mut smpl_bytes = Vec::with_capacity(40);
for i in 0i32..20 {
let v = (i * 800 - 8000) as i16;
smpl_bytes.extend_from_slice(&v.to_le_bytes());
}
let mut info = Vec::new();
push_riff(&mut info, b"ifil", &[0x02, 0x00, 0x04, 0x00]); push_riff(&mut info, b"INAM", b"MidiTestBank\0");
let mut info_list = Vec::from(b"INFO" as &[u8]);
info_list.extend_from_slice(&info);
let mut sdta = Vec::new();
push_riff(&mut sdta, b"smpl", &smpl_bytes);
let mut sdta_list = Vec::from(b"sdta" as &[u8]);
sdta_list.extend_from_slice(&sdta);
const GEN_SAMPLE_MODES: u16 = 54;
const GEN_SAMPLE_ID: u16 = 53;
const GEN_INSTRUMENT: u16 = 41;
let phdr = concat_records(&[
phdr_record("Test Preset", 0, 0, 0),
phdr_record("EOP", 0, 0, 1),
]);
let pbag = concat_records(&[bag_record(0, 0), bag_record(1, 0)]);
let pmod = vec![0u8; 10];
let pgen = concat_records(&[gen_record(GEN_INSTRUMENT, 0), gen_record(0, 0)]);
let inst = concat_records(&[inst_record("Test Inst", 0), inst_record("EOI", 2)]);
let ibag = concat_records(&[bag_record(0, 0), bag_record(2, 0)]);
let imod = vec![0u8; 10];
let igen = concat_records(&[
gen_record(GEN_SAMPLE_MODES, 1),
gen_record(GEN_SAMPLE_ID, 0),
gen_record(0, 0),
]);
let shdr = concat_records(&[
shdr_record("RampLoop", 0, 20, 5, 15, 22_050, 60, 0, 0, 1),
shdr_record("EOS", 0, 0, 0, 0, 0, 0, 0, 0, 0),
]);
let mut pdta = Vec::new();
push_riff(&mut pdta, b"phdr", &phdr);
push_riff(&mut pdta, b"pbag", &pbag);
push_riff(&mut pdta, b"pmod", &pmod);
push_riff(&mut pdta, b"pgen", &pgen);
push_riff(&mut pdta, b"inst", &inst);
push_riff(&mut pdta, b"ibag", &ibag);
push_riff(&mut pdta, b"imod", &imod);
push_riff(&mut pdta, b"igen", &igen);
push_riff(&mut pdta, b"shdr", &shdr);
let mut pdta_list = Vec::from(b"pdta" as &[u8]);
pdta_list.extend_from_slice(&pdta);
let mut body = Vec::from(b"sfbk" as &[u8]);
push_riff(&mut body, b"LIST", &info_list);
push_riff(&mut body, b"LIST", &sdta_list);
push_riff(&mut body, b"LIST", &pdta_list);
let mut out = Vec::from(b"RIFF" as &[u8]);
out.extend_from_slice(&(body.len() as u32).to_le_bytes());
out.extend_from_slice(&body);
out
}
fn push_riff(out: &mut Vec<u8>, tag: &[u8; 4], payload: &[u8]) {
out.extend_from_slice(tag);
out.extend_from_slice(&(payload.len() as u32).to_le_bytes());
out.extend_from_slice(payload);
if payload.len() % 2 == 1 {
out.push(0);
}
}
fn concat_records(rs: &[Vec<u8>]) -> Vec<u8> {
let mut out = Vec::new();
for r in rs {
out.extend_from_slice(r);
}
out
}
fn name20(s: &str) -> [u8; 20] {
let mut buf = [0u8; 20];
let bytes = s.as_bytes();
let n = bytes.len().min(19);
buf[..n].copy_from_slice(&bytes[..n]);
buf
}
fn phdr_record(name: &str, program: u16, bank: u16, pbag_start: u16) -> Vec<u8> {
let mut r = vec![0u8; 38];
r[0..20].copy_from_slice(&name20(name));
r[20..22].copy_from_slice(&program.to_le_bytes());
r[22..24].copy_from_slice(&bank.to_le_bytes());
r[24..26].copy_from_slice(&pbag_start.to_le_bytes());
r
}
fn inst_record(name: &str, ibag_start: u16) -> Vec<u8> {
let mut r = vec![0u8; 22];
r[0..20].copy_from_slice(&name20(name));
r[20..22].copy_from_slice(&ibag_start.to_le_bytes());
r
}
fn bag_record(gen_start: u16, mod_start: u16) -> Vec<u8> {
let mut r = vec![0u8; 4];
r[0..2].copy_from_slice(&gen_start.to_le_bytes());
r[2..4].copy_from_slice(&mod_start.to_le_bytes());
r
}
fn gen_record(oper: u16, amount: u16) -> Vec<u8> {
let mut r = vec![0u8; 4];
r[0..2].copy_from_slice(&oper.to_le_bytes());
r[2..4].copy_from_slice(&amount.to_le_bytes());
r
}
#[allow(clippy::too_many_arguments)]
fn shdr_record(
name: &str,
start: u32,
end: u32,
start_loop: u32,
end_loop: u32,
sample_rate: u32,
original_key: u8,
pitch_correction: i8,
sample_link: u16,
sample_type: u16,
) -> Vec<u8> {
let mut r = vec![0u8; 46];
r[0..20].copy_from_slice(&name20(name));
r[20..24].copy_from_slice(&start.to_le_bytes());
r[24..28].copy_from_slice(&end.to_le_bytes());
r[28..32].copy_from_slice(&start_loop.to_le_bytes());
r[32..36].copy_from_slice(&end_loop.to_le_bytes());
r[36..40].copy_from_slice(&sample_rate.to_le_bytes());
r[40] = original_key;
r[41] = pitch_correction as u8;
r[42..44].copy_from_slice(&sample_link.to_le_bytes());
r[44..46].copy_from_slice(&sample_type.to_le_bytes());
r
}
#[test]
fn end_to_end_pitch_bend_event() {
let mut dec = MidiDecoder::new(Arc::new(ToneInstrument::new()), OUTPUT_SAMPLE_RATE);
let blob = pitch_bend_smf();
let pkt = Packet::new(0, TimeBase::new(1, 44_100), blob);
dec.send_packet(&pkt).unwrap();
for _ in 0..64 {
match dec.receive_frame() {
Ok(_) => {}
Err(Error::Eof) => break,
Err(e) => panic!("unexpected: {e:?}"),
}
}
let s = dec.scheduler().unwrap();
assert!(s.is_done(), "scheduler should have drained the bend");
}
fn pitch_bend_smf() -> Vec<u8> {
let mut blob = Vec::new();
blob.extend_from_slice(b"MThd");
blob.extend_from_slice(&6u32.to_be_bytes());
blob.extend_from_slice(&0u16.to_be_bytes());
blob.extend_from_slice(&1u16.to_be_bytes());
blob.extend_from_slice(&480u16.to_be_bytes());
let mut t: Vec<u8> = Vec::new();
t.extend_from_slice(&[0x00, 0xFF, 0x51, 0x03, 0x07, 0xA1, 0x20]);
t.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]);
t.extend_from_slice(&[0x83, 0x60, 0xE0, 0x7F, 0x7F]);
t.extend_from_slice(&[0x83, 0x60, 0x80, 0x3C, 0x40]);
t.extend_from_slice(&[0x81, 0x70, 0xFF, 0x2F, 0x00]);
push_track(&mut blob, &t);
blob
}
#[test]
fn end_to_end_channel_aftertouch_event() {
let mut dec = MidiDecoder::new(Arc::new(ToneInstrument::new()), OUTPUT_SAMPLE_RATE);
let blob = aftertouch_smf();
let pkt = Packet::new(0, TimeBase::new(1, 44_100), blob);
dec.send_packet(&pkt).unwrap();
let mut samples: Vec<i16> = Vec::new();
for _ in 0..64 {
match dec.receive_frame() {
Ok(Frame::Audio(af)) => {
for chunk in af.data[0].chunks_exact(2) {
samples.push(i16::from_le_bytes([chunk[0], chunk[1]]));
}
}
Err(Error::Eof) => break,
Ok(_) => panic!("expected audio"),
Err(e) => panic!("unexpected: {e:?}"),
}
}
assert!(!samples.is_empty(), "no audio rendered");
let nonzero = samples.iter().filter(|s| s.abs() > 16).count();
assert!(
nonzero > samples.len() / 20,
"expected ≥ 5 % non-silent: {} / {}",
nonzero,
samples.len(),
);
}
fn aftertouch_smf() -> Vec<u8> {
let mut blob = Vec::new();
blob.extend_from_slice(b"MThd");
blob.extend_from_slice(&6u32.to_be_bytes());
blob.extend_from_slice(&0u16.to_be_bytes());
blob.extend_from_slice(&1u16.to_be_bytes());
blob.extend_from_slice(&480u16.to_be_bytes());
let mut t: Vec<u8> = Vec::new();
t.extend_from_slice(&[0x00, 0xFF, 0x51, 0x03, 0x07, 0xA1, 0x20]);
t.extend_from_slice(&[0x00, 0x90, 0x3C, 0x64]);
t.extend_from_slice(&[0x81, 0x70, 0xD0, 0x60]);
t.extend_from_slice(&[0x81, 0x70, 0x80, 0x3C, 0x40]);
t.extend_from_slice(&[0x81, 0x70, 0xFF, 0x2F, 0x00]);
push_track(&mut blob, &t);
blob
}
#[test]
fn reset_clears_scheduler_and_voices() {
let mut dec = MidiDecoder::new(Arc::new(ToneInstrument::new()), OUTPUT_SAMPLE_RATE);
let pkt = Packet::new(0, TimeBase::new(1, 44_100), five_second_smf());
dec.send_packet(&pkt).unwrap();
let _ = dec.receive_frame().unwrap();
dec.reset().unwrap();
match dec.receive_frame() {
Err(Error::NeedMore) => {}
other => panic!("expected NeedMore after reset, got {other:?}"),
}
}
}