mod audio_out;
mod midi_in;
#[cfg(feature = "repl")]
mod repl;
use moont::{Frame, Synth, cm32l};
use std::process;
use std::sync::mpsc;
#[cfg(feature = "repl")]
fn drain_commands(
rx: &mpsc::Receiver<repl::ReplCommand>,
synth: &mut dyn Synth,
) -> bool {
while let Ok(cmd) = rx.try_recv() {
match cmd {
repl::ReplCommand::Controls(controls) => {
for control in controls {
if let Err(e) = synth.apply_command(control) {
eprintln!("Command failed: {e}");
break;
}
}
}
repl::ReplCommand::Quit => return true,
}
}
false
}
fn drain_midi(
rx: &mpsc::Receiver<midi_in::MidiEvent>,
synth: &mut dyn Synth,
verbose: bool,
) {
while let Ok(ev) = rx.try_recv() {
match ev {
midi_in::MidiEvent::Msg(msg) => {
if verbose {
eprintln!(
"MIDI: {:02X} {:02X} {:02X}",
msg & 0xFF,
(msg >> 8) & 0xFF,
(msg >> 16) & 0xFF,
);
}
synth.play_msg(msg);
}
midi_in::MidiEvent::Sysex(data) => {
if verbose {
eprintln!("SysEx: {} bytes", data.len());
}
synth.play_sysex(&data);
}
}
}
}
const DEFAULT_PERIOD: u64 = 512;
struct Args {
ctrl_path: Option<String>,
pcm_path: Option<String>,
gm: bool,
device: String,
period: u64,
verbose: bool,
list: bool,
}
fn usage(prog: &str) {
if cfg!(feature = "bundle-rom") {
eprintln!("Usage: {prog} [OPTIONS]");
} else {
eprintln!("Usage: {prog} [OPTIONS] --control <ROM> --pcm <ROM>");
}
eprintln!();
eprintln!("Options:");
eprintln!(" -c, --control ROM CM-32L control ROM path");
eprintln!(" -p, --pcm ROM CM-32L PCM ROM path");
eprintln!(" -g, --gm Enable General MIDI translation");
eprintln!(" -d, --device DEV ALSA PCM device (default: \"default\")");
eprintln!(
" -b, --buffer N Period size in frames (default: {DEFAULT_PERIOD})"
);
eprintln!(" -v, --verbose Log MIDI events to stderr");
eprintln!(" -l, --list List MIDI ports and exit");
}
fn parse_args() -> Args {
let raw: Vec<String> = std::env::args().collect();
let prog = &raw[0];
let mut args = Args {
ctrl_path: None,
pcm_path: None,
gm: false,
device: "default".into(),
period: DEFAULT_PERIOD,
verbose: false,
list: false,
};
let mut i = 1;
while i < raw.len() {
match raw[i].as_str() {
"-g" | "--gm" => args.gm = true,
"-v" | "--verbose" => args.verbose = true,
"-l" | "--list" => args.list = true,
"-c" | "--control" => {
i += 1;
if i >= raw.len() {
eprintln!("Error: --control requires an argument");
process::exit(1);
}
args.ctrl_path = Some(raw[i].clone());
}
"-p" | "--pcm" => {
i += 1;
if i >= raw.len() {
eprintln!("Error: --pcm requires an argument");
process::exit(1);
}
args.pcm_path = Some(raw[i].clone());
}
"-d" | "--device" => {
i += 1;
if i >= raw.len() {
eprintln!("Error: --device requires an argument");
process::exit(1);
}
args.device = raw[i].clone();
}
"-b" | "--buffer" => {
i += 1;
if i >= raw.len() {
eprintln!("Error: --buffer requires an argument");
process::exit(1);
}
args.period = raw[i].parse().unwrap_or_else(|_| {
eprintln!("Error: invalid buffer size: {}", raw[i]);
process::exit(1);
});
}
"-h" | "--help" => {
usage(prog);
process::exit(0);
}
s if s.starts_with('-') => {
eprintln!("Error: unknown option: {s}");
usage(prog);
process::exit(1);
}
_ => {
eprintln!("Error: unexpected argument: {}", raw[i]);
usage(prog);
process::exit(1);
}
}
i += 1;
}
if args.list {
return args;
}
let have_ctrl = args.ctrl_path.is_some();
let have_pcm = args.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
}
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 main() {
let args = parse_args();
if args.list {
midi_in::list_ports();
return;
}
let rom = load_rom(&args);
let mut synth: Box<dyn Synth> = if args.gm {
eprintln!("GM translation enabled");
Box::new(cm32l::GmDevice::new(rom))
} else {
Box::new(cm32l::Device::new(rom))
};
let audio = audio_out::AudioOut::open(&args.device, args.period);
let (midi_tx, midi_rx) = mpsc::sync_channel::<midi_in::MidiEvent>(1024);
let _midi_thread = midi_in::open_port(midi_tx);
#[cfg(feature = "repl")]
let (cmd_tx, cmd_rx) = mpsc::sync_channel::<repl::ReplCommand>(128);
#[cfg(feature = "repl")]
let repl_thread = repl::spawn_repl(cmd_tx);
eprintln!("Running (Ctrl-C to stop)...");
let period = args.period as usize;
let mut frames = vec![Frame(0, 0); period];
let mut pcm_buf = vec![0i16; period * 2];
loop {
#[cfg(feature = "repl")]
if drain_commands(&cmd_rx, synth.as_mut()) {
break;
}
drain_midi(&midi_rx, synth.as_mut(), args.verbose);
synth.render(&mut frames);
for (i, Frame(l, r)) in frames.iter().enumerate() {
pcm_buf[i * 2] = *l;
pcm_buf[i * 2 + 1] = *r;
}
audio.writei(&pcm_buf);
}
#[cfg(feature = "repl")]
{
drop(midi_rx);
drop(cmd_rx);
if let Err(e) = repl_thread.join() {
eprintln!("REPL thread join error: {:?}", e);
}
}
}