use clap::Parser;
use console::{Key, Term};
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{mpsc, Arc, Mutex};
use std::thread;
use std::time::Duration;
use xmrs::core::duration::ModuleDuration;
use xmrs::prelude::*;
use xmrsplayer::prelude::*;
fn format_mm_ss(d: Duration) -> String {
let total = d.as_secs();
format!("{:02}:{:02}", total / 60, total % 60)
}
macro_rules! tprintln {
() => {{
use std::io::Write as _;
let _ = std::io::stdout().write_all(b"\r\n");
}};
($($arg:tt)*) => {{
use std::io::Write as _;
print!($($arg)*);
let _ = std::io::stdout().write_all(b"\r\n");
}};
}
fn restore_terminal() {
#[cfg(unix)]
{
let _ = std::process::Command::new("stty").arg("sane").status();
}
}
struct ExitOnSongEnd {
flag: Arc<AtomicBool>,
}
impl PlayerObserver for ExitOnSongEnd {
fn on_row(&mut self, _ctx: &RowContext<'_>) {}
fn on_song_end(&mut self) {
self.flag.store(true, Ordering::Relaxed);
}
}
#[cfg(feature = "import_sid")]
use xmrs::tracker::import::sid::sid_module::SidModule;
#[derive(Parser)]
struct Cli {
#[arg(short = 'f', long, required = true, value_name = "filename")]
filename: Option<String>,
#[arg(short = 's', long, default_value = "0")]
song: usize,
#[arg(short = 'o', long, value_name = "output filename")]
output: Option<String>,
#[arg(short = 'a', long, default_value = "1.0")]
amplification: f32,
#[arg(short = 'c', long, default_value = "0")]
ch: u8,
#[arg(short = 'd', long, default_value = "false")]
debug: bool,
#[arg(short = 'l', long, default_value = "0")]
loops: usize,
#[arg(short = 'p', long, default_value = "0")]
position: usize,
#[arg(short = 'e', long, default_value = "0")]
speed: usize,
#[arg(short = 'D', long, default_value = "false")]
duration: bool,
#[cfg(feature = "import_sid")]
#[arg(short = 'z', long, default_value = "false")]
sid_test_player: bool,
}
#[cfg(feature = "import_sid")]
fn sid_test_player(cli: &Cli) {
let sidmodule = SidModule::get_sid_monty_on_the_run();
let modules = sidmodule.to_modules(false);
let leaked_modules: &'static [Module] = Box::leak(modules.into_boxed_slice());
let module_ref: &'static Module = &leaked_modules[0];
if cli.duration {
let d = module_ref.duration(cli.song);
println!("{}", format_mm_ss(d));
return;
}
play_music(
module_ref,
cli.song,
cli.amplification,
cli.position,
cli.loops,
cli.debug,
cli.ch,
cli.speed,
cli.output.clone(),
);
}
fn main() -> Result<(), std::io::Error> {
let cli = Cli::parse();
println!("--===~ XmRs Player Example ~===--");
println!("(c) 2023-2024 Sébastien Béchet\n");
println!("Because demo scene can't die :)\n");
#[cfg(feature = "import_sid")]
if cli.sid_test_player {
sid_test_player(&cli);
return Ok(());
}
if let Some(filename) = cli.filename {
println!("opening {}", filename);
let contents = std::fs::read(filename.trim())?;
match Module::load(&contents) {
Ok(module) => {
drop(contents); if cli.duration {
let d = module.duration(cli.song);
println!("{}", format_mm_ss(d));
return Ok(());
}
println!("Playing {} !", module.name);
let module = Box::new(module);
let module_ref: &'static Module = Box::leak(module);
play_music(
module_ref,
cli.song,
cli.amplification,
cli.position,
cli.loops,
cli.debug,
cli.ch,
cli.speed,
cli.output.clone(),
);
}
Err(e) => {
println!("{:?}", e);
}
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn play_music(
module: &'static Module,
song: usize,
amplification: f32,
position: usize,
loops: usize,
debug: bool,
ch: u8,
speed: usize,
output: Option<String>,
) {
let host = cpal::default_host();
let device = host
.default_output_device()
.expect("no output device available");
let config = device
.default_output_config()
.expect("failed to get default output config");
let sample_rate = config.sample_rate();
let sample_rate_hz: u32 = sample_rate;
let player = Arc::new(Mutex::new(XmrsPlayer::new(module, sample_rate_hz, song)));
let player_summary = Arc::clone(&player);
let song_ended = Arc::new(AtomicBool::new(false));
{
let mut player_lock = player.lock().unwrap();
player_lock.set_amplification(Amplification::from_raw_q4_12(
((amplification * 4096.0)
.round()
.clamp(i16::MIN as f32, i16::MAX as f32)) as i16,
));
if debug {
tprintln!("Debug on");
tprintln!("Module format: {:?}", module.origin);
player_lock.add_observer(Box::new(DebugObserver::new()));
}
player_lock.add_observer(Box::new(ExitOnSongEnd {
flag: Arc::clone(&song_ended),
}));
if ch != 0 {
player_lock.mute_all(true);
player_lock.set_mute_channel((ch - 1).into(), false);
}
player_lock.set_max_loop_count(loops);
player_lock.goto(position, 0, speed);
}
if let Some(output) = output {
tprintln!("writing {}...", output);
write_wave(player, output.as_str()).unwrap();
} else {
let player_clone = Arc::clone(&player);
let stream = device
.build_output_stream(
&config.config(),
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
let mut player_lock = player_clone.lock().unwrap();
data.iter_mut()
.zip(player_lock.by_ref()) .for_each(|(sample, value)| {
*sample = value as f32 / i16::MAX as f32;
});
},
|_: cpal::StreamError| {},
None,
)
.expect("failed to build output stream");
stream.play().expect("failed to play stream");
let (tx, rx) = mpsc::channel::<Key>();
thread::spawn(move || {
let stdout = Term::stdout();
while let Ok(key) = stdout.read_key() {
if tx.send(key).is_err() {
break;
}
}
});
tprintln!(
"Enter and i keys for info, Space for pause, left or right arrow to move, escape key to exit..."
);
let mut playing = true;
'event_loop: loop {
if song_ended.load(Ordering::Relaxed) {
tprintln!("Song finished.");
break 'event_loop;
}
let character = match rx.recv_timeout(Duration::from_millis(50)) {
Ok(k) => k,
Err(mpsc::RecvTimeoutError::Timeout) => continue,
Err(mpsc::RecvTimeoutError::Disconnected) => break 'event_loop,
};
match character {
Key::Enter => {
let ti = player.lock().unwrap().get_current_table_index();
let p = player.lock().unwrap().get_current_pattern();
tprintln!("current table index:{:02x}, current pattern:{:02x}", ti, p);
}
Key::Escape | Key::Char('q') => {
tprintln!("Have a nice day!");
break 'event_loop;
}
Key::ArrowLeft => {
let i = player.lock().unwrap().get_current_table_index();
if i != 0 {
player.lock().unwrap().goto(i - 1, 0, 0);
}
}
Key::ArrowRight => {
let len = module.song_length(song);
let i = player.lock().unwrap().get_current_table_index();
if i + 1 < len {
player.lock().unwrap().goto(i + 1, 0, 0);
}
}
Key::Char(' ') => {
if playing {
tprintln!("Pause, press space to continue");
player.lock().unwrap().pause(true);
playing = false;
{
let player_lock = player.lock().unwrap();
let ti = player_lock.get_current_table_index();
let p = player_lock.get_current_pattern();
let row = player_lock.get_current_row();
tprintln!("Pattern [{:02X}]={:02X}, Row {:02X}", ti, p, row);
}
} else {
tprintln!("Playing");
player.lock().unwrap().pause(false);
playing = true;
}
}
Key::Char('i') => {
let player_lock = player.lock().unwrap();
tprintln!(
"name:{}\ncomment:{}",
player_lock.module.name,
player_lock.module.comment
);
tprintln!(
"speed={}, generated samples:{}, loop count:{}",
player_lock.get_tempo(),
player_lock.generated_samples(),
player_lock.get_loop_count()
);
let actual_frames = player_lock.generated_samples();
let sr = player_lock.get_sample_rate();
let actual = Duration::from_secs(actual_frames / sr as u64);
let theoretical = module.duration(song);
tprintln!(
"Duration: {} / {}",
format_mm_ss(actual),
format_mm_ss(theoretical),
);
for (i, instr) in player_lock.module.instrument.iter().enumerate() {
if !instr.name.is_empty() {
tprintln!("instrument {:2}: {}", i, instr.name);
}
}
}
_ => {}
}
}
}
let (actual_frames, sample_rate_hz_actual) = {
let lock = player_summary.lock().unwrap();
(lock.generated_samples(), lock.get_sample_rate())
};
let actual = Duration::from_secs(actual_frames / sample_rate_hz_actual as u64);
let theoretical = module.duration(song);
tprintln!(
"Duration: {} / {}",
format_mm_ss(actual),
format_mm_ss(theoretical),
);
restore_terminal();
}
use hound::{SampleFormat, WavSpec, WavWriter};
fn write_wave(
amp: Arc<Mutex<XmrsPlayer>>,
output_file: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let spec = WavSpec {
channels: 2,
sample_rate: 44100,
bits_per_sample: 16,
sample_format: SampleFormat::Int,
};
let mut writer = WavWriter::create(output_file, spec)?;
amp.lock().unwrap().set_max_loop_count(1);
let player_clone = Arc::clone(&);
let mut player_lock = player_clone.lock().unwrap();
for sample in player_lock.by_ref() {
writer.write_sample(sample)?;
}
writer.finalize()?;
Ok(())
}