diff --git a/src/audio.rs b/src/audio.rs index 9507df0..631bad0 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -1,12 +1,16 @@ use std::time::Duration; -use anyhow::{Result, bail}; +use anyhow::{Context, Result, bail}; use audioadapter_buffers::direct::InterleavedSlice; -use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use cpal::{ + Device, FromSample, Sample, SampleFormat, SizedSample, StreamConfig, + traits::{DeviceTrait, HostTrait, StreamTrait}, +}; +use itertools::Itertools; use rubato::{Adjustable as _, Resampler}; use tracing::warn; -pub struct Audio { +struct CpalAudio { #[allow(unused)] stream: cpal::Stream, sampler: rubato::Async, @@ -17,19 +21,31 @@ pub struct Audio { const VB_FREQUENCY: usize = 41700; -impl Audio { - pub fn init() -> Result { +impl CpalAudio { + fn init() -> Result { let host = cpal::default_host(); - let Some(device) = host.default_output_device() else { - bail!("No output device available"); - }; + let device = host + .default_output_device() + .context("no output device available")?; let default_rate = device.default_output_config()?.sample_rate(); - let Some(config) = device.supported_output_configs()?.find(|c| { - c.channels() == 2 && c.sample_format().is_float() && c.contains_rate(default_rate) - }) else { - bail!("No suitable output config available"); - }; - let mut config = config.with_sample_rate(default_rate).config(); + + let supported_config = device + .supported_output_configs()? + .filter(|c| { + c.contains_rate(default_rate) + && matches!( + c.sample_format(), + SampleFormat::F32 + | SampleFormat::F64 + | SampleFormat::I32 + | SampleFormat::I16 + ) + }) + .max_by(|a, b| a.cmp_default_heuristics(b)) + .context("no suitable output config available")? + .with_sample_rate(default_rate); + let mut config = supported_config.config(); + let resample_ratio = config.sample_rate as f64 / VB_FREQUENCY as f64; let chunk_size = (834.0 * resample_ratio) as usize; let sampler = rubato::Async::new_poly( @@ -42,6 +58,23 @@ impl Audio { )?; config.buffer_size = cpal::BufferSize::Fixed(sampler.output_frames_max() as u32); + match supported_config.sample_format() { + SampleFormat::F32 if config.channels == 2 => { + Self::new_f32_stereo(device, config, sampler) + } + SampleFormat::F32 => Self::new::(device, config, sampler), + SampleFormat::F64 => Self::new::(device, config, sampler), + SampleFormat::I32 => Self::new::(device, config, sampler), + SampleFormat::I16 => Self::new::(device, config, sampler), + _ => bail!("unsupported sample format"), + } + } + + fn new_f32_stereo( + device: Device, + config: StreamConfig, + sampler: rubato::Async, + ) -> Result { let input_buffer = Vec::with_capacity(sampler.nbr_channels() * sampler.input_frames_max()); let output_buffer = vec![0.0; sampler.nbr_channels() * sampler.output_frames_max()]; let (sample_sink, mut sample_source) = @@ -79,7 +112,60 @@ impl Audio { }) } - pub fn update(&mut self, mut samples: &[f32]) { + fn new>( + device: Device, + config: StreamConfig, + sampler: rubato::Async, + ) -> Result { + let input_buffer = Vec::with_capacity(sampler.nbr_channels() * sampler.input_frames_max()); + let output_buffer = vec![0.0; sampler.nbr_channels() * sampler.output_frames_max()]; + let (sample_sink, mut sample_source) = + rtrb::RingBuffer::new(sampler.output_frames_max() * 4); + + let channels = config.channels as usize; + + let stream = device.build_output_stream( + config, + move |data: &mut [S], _| { + let requested = data.len() * 2 / channels; + let chunk = match sample_source.read_chunk(requested) { + Ok(c) => c, + Err(rtrb::chunks::ChunkError::TooFewSlots(n)) => { + sample_source.read_chunk(n).unwrap() + } + }; + let (first, second) = chunk.as_slices(); + + let new_samples = first + .iter() + .chain(second) + .map(|s: &f32| (*s).to_sample()) + .chunks(2); + let buffer = data.iter_mut().chunks(channels); + for (dst, samples) in buffer.into_iter().zip(&new_samples) { + // channels beyond the first two are zeroed + let src = samples.chain(std::iter::repeat(S::EQUILIBRIUM)); + for (d, s) in dst.zip(src) { + *d = s; + } + } + + chunk.commit_all(); + }, + move |error| warn!(%error, "stream error"), + None, + )?; + stream.play()?; + Ok(Self { + stream, + sampler, + input_buffer, + output_buffer, + sample_sink, + }) + } + + fn update(&mut self, mut samples: &[f32]) { while self.input_buffer.len() + samples.len() >= self.sampler.input_frames_next() * 2 { let samples_needed = (self.sampler.input_frames_next() * 2).saturating_sub(self.input_buffer.len()); @@ -117,9 +203,64 @@ impl Audio { } } - pub fn set_speed(&mut self, speed: f64) -> Result<()> { + fn set_speed(&mut self, speed: f64) -> Result<()> { self.sampler .set_resample_ratio_relative(1.0 / speed, false)?; Ok(()) } } + +struct NoAudio { + speed: f64, +} + +impl NoAudio { + fn new() -> Self { + Self { speed: 1.0 } + } + + fn update(&mut self, samples: &[f32]) { + let samples = (samples.len() / 2) as f64; + let elapsed = Duration::from_secs_f64(samples / (VB_FREQUENCY as f64 * self.speed)); + std::thread::sleep(elapsed); + } + + fn set_speed(&mut self, speed: f64) -> Result<()> { + self.speed = speed; + Ok(()) + } +} + +#[allow(clippy::large_enum_variant)] +enum AudioInner { + Cpal(CpalAudio), + None(NoAudio), +} + +pub struct Audio(AudioInner); + +impl Audio { + pub fn init() -> Self { + match CpalAudio::init() { + Ok(a) => Self(AudioInner::Cpal(a)), + Err(e) => { + warn!("could not play audio: {e}"); + Self(AudioInner::None(NoAudio::new())) + } + } + } + + pub fn update(&mut self, samples: &[f32]) { + match &mut self.0 { + AudioInner::Cpal(a) => a.update(samples), + AudioInner::None(a) => a.update(samples), + } + } + + pub fn set_speed(&mut self, speed: f64) -> Result<()> { + match &mut self.0 { + AudioInner::Cpal(a) => a.set_speed(speed), + AudioInner::None(a) => a.set_speed(speed), + } + } +} diff --git a/src/emulator.rs b/src/emulator.rs index 4843846..9d44041 100644 --- a/src/emulator.rs +++ b/src/emulator.rs @@ -138,7 +138,7 @@ impl EmulatorBuilder { self.audio_on, self.watch_rom, self.linked, - )?; + ); if let Some(path) = self.rom { emulator.load_cart(SimId::Player1, &path)?; } @@ -178,11 +178,11 @@ impl Emulator { audio_on: Arc<[AtomicBool; 2]>, watch_rom: Arc<[AtomicBool; 2]>, linked: Arc, - ) -> Result { - Ok(Self { + ) -> Self { + Self { sims: vec![], carts: [None, None], - audio: Audio::init()?, + audio: Audio::init(), commands, sim_state, state, @@ -198,7 +198,7 @@ impl Emulator { eye_contents: [vec![0u8; 384 * 224 * 2], vec![0u8; 384 * 224 * 2]], audio_samples: Vec::with_capacity(EXPECTED_FRAME_SIZE), buffer: vec![], - }) + } } pub fn reload_cart(&mut self, sim_id: SimId) -> Result<()> {