mirror of
https://github.com/italicsjenga/agb.git
synced 2024-12-23 00:01:34 +11:00
Get the desktop player actually playing music
This commit is contained in:
parent
c863b94f49
commit
3a755107aa
|
@ -298,12 +298,7 @@ impl<M: Mixer> TrackerChannel<M> {
|
|||
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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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<dyn Error>> {
|
||||
let args: Vec<String> = 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<Module, Box<dyn Error>> {
|
||||
let file_content = fs::read(xm_path)?;
|
||||
Ok(XmModule::load(&file_content)?.to_module())
|
||||
}
|
||||
|
|
205
tracker/desktop-player/src/mixer.rs
Normal file
205
tracker/desktop-player/src/mixer.rs
Normal file
|
@ -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<SoundChannel>; 8],
|
||||
indices: [Wrapping<usize>; 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<i16, 4> = right_amount.change_base();
|
||||
let left_amount: Num<i16, 4> = left_amount.change_base();
|
||||
|
||||
let channel_len = Num::<u32, 8>::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<u32, 8>,
|
||||
should_loop: bool,
|
||||
restart_point: Num<u32, 8>,
|
||||
|
||||
is_playing: bool,
|
||||
playback_speed: Num<u32, 8>,
|
||||
volume: Num<i16, 8>,
|
||||
|
||||
panning: Num<i16, 8>, // 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<usize>);
|
||||
|
||||
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<Num<i16, 8>>) -> &mut Self {
|
||||
self.volume = value.into();
|
||||
self
|
||||
}
|
||||
|
||||
fn restart_point(&mut self, value: impl Into<Num<u32, 8>>) -> &mut Self {
|
||||
self.restart_point = value.into();
|
||||
self
|
||||
}
|
||||
|
||||
fn playback(&mut self, playback_speed: impl Into<Num<u32, 8>>) -> &mut Self {
|
||||
self.playback_speed = playback_speed.into();
|
||||
self
|
||||
}
|
||||
|
||||
fn panning(&mut self, panning: impl Into<Num<i16, 8>>) -> &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<Self::ChannelId> {
|
||||
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
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue