diff --git a/tracker/agb-tracker/src/lib.rs b/tracker/agb-tracker/src/lib.rs index 3d1cf3d8..b4d952d4 100644 --- a/tracker/agb-tracker/src/lib.rs +++ b/tracker/agb-tracker/src/lib.rs @@ -298,12 +298,7 @@ impl TrackerChannel { channel.stop(); } - let mut new_channel = M::SoundChannel::new(match sample.data { - alloc::borrow::Cow::Borrowed(data) => data, - alloc::borrow::Cow::Owned(_) => { - unimplemented!("Must use borrowed COW data for tracker") - } - }); + let mut new_channel = M::SoundChannel::new(&sample.data); new_channel.volume( (sample.volume.change_base() * global_settings.volume) @@ -528,8 +523,13 @@ fn main(gba: agb::Gba) -> ! { #[cfg(feature = "agb")] impl SoundChannel for agb::sound::mixer::SoundChannel { - fn new(data: &'static [u8]) -> Self { - Self::new(data) + fn new(data: &alloc::borrow::Cow<'static, [u8]>) -> Self { + Self::new(match data { + alloc::borrow::Cow::Borrowed(data) => data, + alloc::borrow::Cow::Owned(_) => { + unimplemented!("Must use borrowed COW data for tracker") + } + }) } fn stop(&mut self) { diff --git a/tracker/agb-tracker/src/mixer.rs b/tracker/agb-tracker/src/mixer.rs index 45871cc4..9054d002 100644 --- a/tracker/agb-tracker/src/mixer.rs +++ b/tracker/agb-tracker/src/mixer.rs @@ -3,7 +3,7 @@ use agb_fixnum::Num; pub trait SoundChannel { - fn new(data: &'static [u8]) -> Self; + fn new(data: &alloc::borrow::Cow<'static, [u8]>) -> Self; fn stop(&mut self); fn pause(&mut self) -> &mut Self; diff --git a/tracker/desktop-player/Cargo.toml b/tracker/desktop-player/Cargo.toml index c1303ea7..cd9f94e0 100644 --- a/tracker/desktop-player/Cargo.toml +++ b/tracker/desktop-player/Cargo.toml @@ -13,3 +13,5 @@ agb_tracker = { version = "0.20.2", path = "../agb-tracker", default-features = agb_fixnum = { version = "0.20.2", path = "../../agb-fixnum" } xmrs = "0.6" + +cpal = "0.15" diff --git a/tracker/desktop-player/src/main.rs b/tracker/desktop-player/src/main.rs index e7a11a96..60b8ed99 100644 --- a/tracker/desktop-player/src/main.rs +++ b/tracker/desktop-player/src/main.rs @@ -1,3 +1,66 @@ -fn main() { - println!("Hello, world!"); +use std::{env, error::Error, fs, path::Path, sync::mpsc}; + +use cpal::{ + traits::{DeviceTrait, HostTrait, StreamTrait}, + SampleFormat, SampleRate, +}; +use mixer::Mixer; +use xmrs::{module::Module, xm::xmmodule::XmModule}; + +mod mixer; + +fn main() -> Result<(), Box> { + let args: Vec = env::args().collect(); + + let file_path = &args[1]; + let module = load_module_from_file(Path::new(file_path))?; + + let track = Box::leak::<'static>(Box::new(agb_xm_core::parse_module(&module))); + + let mut mixer = Mixer::new(); + let mut tracker = agb_tracker::Tracker::new(track); + + let host = cpal::default_host(); + let device = host + .default_output_device() + .expect("Failed to open output device"); + + let mut supported_configs = device.supported_output_configs()?; + let config = supported_configs + .find_map(|config| { + if config.channels() == 2 && config.sample_format() == SampleFormat::F32 { + return config.try_with_sample_rate(SampleRate(32768)); + } + + None + }) + .expect("Could not produce valid config"); + + let (tx, rx) = mpsc::sync_channel(32768 * 3); + + let stream = device.build_output_stream( + &config.into(), + move |data: &mut [f32], _| { + for val in data.iter_mut() { + *val = rx.recv().unwrap(); + } + }, + |err| eprintln!("Error on audio stream {err}"), + None, + )?; + + stream.play()?; + + loop { + tracker.step(&mut mixer); + for (l, r) in mixer.frame() { + tx.send((l as f32) / 128.0)?; + tx.send((r as f32) / 128.0)?; + } + } +} + +fn load_module_from_file(xm_path: &Path) -> Result> { + let file_content = fs::read(xm_path)?; + Ok(XmModule::load(&file_content)?.to_module()) } diff --git a/tracker/desktop-player/src/mixer.rs b/tracker/desktop-player/src/mixer.rs new file mode 100644 index 00000000..e763da88 --- /dev/null +++ b/tracker/desktop-player/src/mixer.rs @@ -0,0 +1,205 @@ +use agb_fixnum::Num; +use std::{borrow::Cow, num::Wrapping}; + +const BUFFER_SIZE: usize = 560; + +#[derive(Default)] +pub struct Mixer { + channels: [Option; 8], + indices: [Wrapping; 8], +} + +impl Mixer { + pub fn new() -> Self { + Self { + channels: Default::default(), + indices: Default::default(), + } + } + + pub fn frame(&mut self) -> Vec<(i8, i8)> { + let channels = + self.channels.iter_mut().flatten().filter(|channel| { + !channel.is_done && channel.volume != 0.into() && channel.is_playing + }); + + let mut buffer = vec![Num::new(0); BUFFER_SIZE * 2]; + + for channel in channels { + let right_amount = ((channel.panning + 1) / 2) * channel.volume; + let left_amount = ((channel.panning + 1) / 2) * channel.volume; + + let right_amount: Num = right_amount.change_base(); + let left_amount: Num = left_amount.change_base(); + + let channel_len = Num::::new(channel.data.len() as u32); + let mut playback_speed = channel.playback_speed; + + while playback_speed >= channel_len - channel.restart_point { + playback_speed -= channel_len; + } + + let restart_subtract = channel_len - channel.restart_point; + + let mut current_pos = channel.pos; + + for i in 0..BUFFER_SIZE { + let val = channel.data[current_pos.floor() as usize] as i8 as i16; + + buffer[2 * i] += left_amount * val; + buffer[2 * i + 1] += right_amount * val; + + current_pos += playback_speed; + + if current_pos >= channel_len { + if channel.should_loop { + current_pos -= restart_subtract; + } else { + channel.is_done = true; + break; + } + } + } + + channel.pos = current_pos; + } + + let mut ret = Vec::with_capacity(BUFFER_SIZE); + for i in 0..BUFFER_SIZE { + let l = buffer[2 * i].floor(); + let r = buffer[2 * i + 1].floor(); + + ret.push(( + l.clamp(i8::MIN as i16, i8::MAX as i16) as i8, + r.clamp(i8::MIN as i16, i8::MAX as i16) as i8, + )); + } + + ret + } +} + +pub struct SoundChannel { + data: Cow<'static, [u8]>, + pos: Num, + should_loop: bool, + restart_point: Num, + + is_playing: bool, + playback_speed: Num, + volume: Num, + + panning: Num, // between -1 and 1 + is_done: bool, +} + +impl std::fmt::Debug for SoundChannel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SoundChannel") + .field("pos", &self.pos) + .field("should_loop", &self.should_loop) + .field("restart_point", &self.restart_point) + .field("is_playing", &self.is_playing) + .field("playback_speed", &self.playback_speed) + .field("volume", &self.volume) + .field("panning", &self.panning) + .field("is_done", &self.is_done) + .finish() + } +} + +impl SoundChannel { + fn new(data: Cow<'static, [u8]>) -> Self { + Self { + data: data.clone(), + + pos: 0.into(), + should_loop: false, + playback_speed: 1.into(), + is_playing: true, + panning: 0.into(), + is_done: false, + volume: 1.into(), + restart_point: 0.into(), + } + } +} + +pub struct SoundChannelId(usize, Wrapping); + +impl agb_tracker::SoundChannel for SoundChannel { + fn new(data: &Cow<'static, [u8]>) -> Self { + Self::new(data.clone()) + } + + fn stop(&mut self) { + self.is_done = true; + } + + fn pause(&mut self) -> &mut Self { + self.is_playing = false; + self + } + + fn resume(&mut self) -> &mut Self { + self.is_playing = true; + self + } + + fn should_loop(&mut self) -> &mut Self { + self.should_loop = true; + self + } + + fn volume(&mut self, value: impl Into>) -> &mut Self { + self.volume = value.into(); + self + } + + fn restart_point(&mut self, value: impl Into>) -> &mut Self { + self.restart_point = value.into(); + self + } + + fn playback(&mut self, playback_speed: impl Into>) -> &mut Self { + self.playback_speed = playback_speed.into(); + self + } + + fn panning(&mut self, panning: impl Into>) -> &mut Self { + self.panning = panning.into(); + self + } +} + +impl agb_tracker::Mixer for Mixer { + type ChannelId = SoundChannelId; + + type SoundChannel = SoundChannel; + + fn channel(&mut self, channel_id: &Self::ChannelId) -> Option<&mut Self::SoundChannel> { + if let Some(channel) = &mut self.channels[channel_id.0] { + if self.indices[channel_id.0] == channel_id.1 && !channel.is_done { + return Some(channel); + } + } + + None + } + + fn play_sound(&mut self, new_channel: Self::SoundChannel) -> Option { + for (i, channel) in self.channels.iter_mut().enumerate() { + if let Some(channel) = channel { + if !channel.is_done { + continue; + } + } + + channel.replace(new_channel); + self.indices[i] += 1; + return Some(SoundChannelId(i, self.indices[i])); + } + + None + } +}