use moont::smf;
use moont::{Frame, Synth, cm32l};
use std::fs::File;
use std::io::prelude::*;
use std::process;
const SAMPLE_RATE: u32 = 32000;
const CHANNELS: u16 = 2;
const BITS_PER_SAMPLE: u16 = 16;
const TAIL_SECONDS: u32 = 2;
const BUFFER_FRAMES: usize = 8192;
struct Args {
midi_path: String,
ctrl_path: Option<String>,
pcm_path: Option<String>,
output_path: String,
}
fn usage(prog: &str) {
if cfg!(feature = "bundle-rom") {
eprintln!("Usage: {prog} [OPTIONS] <input.mid> <output.wav>");
} else {
eprintln!(
"Usage: {prog} [OPTIONS] --control <ROM> --pcm <ROM> <input.mid> <output.wav>"
);
}
eprintln!();
eprintln!("Options:");
eprintln!(" -c, --control ROM CM-32L control ROM path");
eprintln!(" -p, --pcm ROM CM-32L PCM ROM path");
}
fn parse_args() -> Args {
let raw: Vec<String> = std::env::args().collect();
let prog = &raw[0];
let mut ctrl_path = None;
let mut pcm_path = None;
let mut positional = Vec::new();
let mut i = 1;
while i < raw.len() {
match raw[i].as_str() {
"-c" | "--control" => {
i += 1;
if i >= raw.len() {
eprintln!("Error: --control requires an argument");
process::exit(1);
}
ctrl_path = Some(raw[i].clone());
}
"-p" | "--pcm" => {
i += 1;
if i >= raw.len() {
eprintln!("Error: --pcm requires an argument");
process::exit(1);
}
pcm_path = Some(raw[i].clone());
}
"-h" | "--help" => {
usage(prog);
process::exit(0);
}
s if s.starts_with('-') => {
eprintln!("Error: unknown option: {s}");
usage(prog);
process::exit(1);
}
_ => {
positional.push(raw[i].clone());
}
}
i += 1;
}
if positional.len() != 2 {
usage(prog);
process::exit(1);
}
let have_ctrl = ctrl_path.is_some();
let have_pcm = pcm_path.is_some();
if have_ctrl != have_pcm {
eprintln!("Error: --control and --pcm must both be provided");
process::exit(1);
}
if !have_ctrl && !cfg!(feature = "bundle-rom") {
usage(prog);
process::exit(1);
}
Args {
midi_path: positional[0].clone(),
ctrl_path,
pcm_path,
output_path: positional[1].clone(),
}
}
fn load_rom_files(ctrl_path: &str, pcm_path: &str) -> cm32l::Rom {
let ctrl = std::fs::read(ctrl_path).unwrap_or_else(|e| {
eprintln!("Failed to read control ROM: {e}");
process::exit(1);
});
let pcm = std::fs::read(pcm_path).unwrap_or_else(|e| {
eprintln!("Failed to read PCM ROM: {e}");
process::exit(1);
});
cm32l::Rom::new(&ctrl, &pcm).unwrap_or_else(|e| {
eprintln!("Invalid ROM: {e}");
process::exit(1);
})
}
fn load_rom(args: &Args) -> cm32l::Rom {
match (&args.ctrl_path, &args.pcm_path) {
(Some(ctrl), Some(pcm)) => load_rom_files(ctrl, pcm),
#[cfg(feature = "bundle-rom")]
_ => cm32l::Rom::bundled(),
#[cfg(not(feature = "bundle-rom"))]
_ => unreachable!(),
}
}
fn load_midi(path: &str) -> Vec<smf::Event> {
let data = std::fs::read(path).unwrap_or_else(|e| {
eprintln!("Failed to read MIDI file: {e}");
process::exit(1);
});
smf::parse(&data).unwrap_or_else(|e| {
eprintln!("Failed to parse MIDI file: {e}");
process::exit(1);
})
}
fn total_frames(events: &[smf::Event]) -> u32 {
let last = events.last().map(|e| e.time()).unwrap_or(0);
last + TAIL_SECONDS * SAMPLE_RATE
}
fn feed_events(
synth: &mut cm32l::Device,
events: &[smf::Event],
ei: &mut usize,
deadline: u32,
) {
while *ei < events.len() && events[*ei].time() <= deadline {
match &events[*ei] {
smf::Event::Msg { time, msg } => {
synth.play_msg_at(*msg, *time);
}
smf::Event::Sysex { time, data } => {
synth.play_sysex_at(data, *time);
}
_ => {}
}
*ei += 1;
}
}
fn write_wav(
path: &str,
synth: &mut cm32l::Device,
events: &[smf::Event],
frames: u32,
) {
let mut f = File::create(path).unwrap_or_else(|e| {
eprintln!("Failed to create output file: {e}");
process::exit(1);
});
let block_align = CHANNELS * BITS_PER_SAMPLE / 8;
let byte_rate = SAMPLE_RATE * block_align as u32;
let data_size = frames * block_align as u32;
f.write_all(b"RIFF").unwrap();
f.write_all(&(data_size + 36).to_le_bytes()).unwrap();
f.write_all(b"WAVE").unwrap();
f.write_all(b"fmt ").unwrap();
f.write_all(&16u32.to_le_bytes()).unwrap();
f.write_all(&1u16.to_le_bytes()).unwrap(); f.write_all(&CHANNELS.to_le_bytes()).unwrap();
f.write_all(&SAMPLE_RATE.to_le_bytes()).unwrap();
f.write_all(&byte_rate.to_le_bytes()).unwrap();
f.write_all(&block_align.to_le_bytes()).unwrap();
f.write_all(&BITS_PER_SAMPLE.to_le_bytes()).unwrap();
f.write_all(b"data").unwrap();
f.write_all(&data_size.to_le_bytes()).unwrap();
let mut buf = vec![Frame(0, 0); BUFFER_FRAMES];
let mut pcm = vec![0u8; BUFFER_FRAMES * 4];
let mut ei = 0;
let mut rem = frames as usize;
while rem > 0 {
let n = rem.min(BUFFER_FRAMES);
let current = synth.current_time();
feed_events(synth, events, &mut ei, current + n as u32);
let out = &mut buf[..n];
synth.render(out);
let bytes = &mut pcm[..n * 4];
for (chunk, Frame(left, right)) in
bytes.chunks_exact_mut(4).zip(out.iter())
{
let [a, b] = left.to_le_bytes();
let [c, d] = right.to_le_bytes();
chunk[0] = a;
chunk[1] = b;
chunk[2] = c;
chunk[3] = d;
}
f.write_all(&bytes[..n * 4]).unwrap();
rem -= n;
}
}
fn main() {
let args = parse_args();
let events = load_midi(&args.midi_path);
eprintln!("Loaded {} MIDI events", events.len());
let rom = load_rom(&args);
let mut synth = cm32l::Device::new(rom);
let frames = total_frames(&events);
let seconds = frames / SAMPLE_RATE;
eprintln!("Rendering {seconds}s ({frames} frames)...");
write_wav(&args.output_path, &mut synth, &events, frames);
eprintln!("Wrote {}", args.output_path);
}