Make audio much more defensive

This commit is contained in:
Simon Gellis 2026-08-14 23:42:02 -04:00
parent 1ff4fda55b
commit 644e99c295
No known key found for this signature in database
GPG Key ID: DA576912FED9577B
2 changed files with 162 additions and 21 deletions

View File

@ -1,12 +1,16 @@
use std::time::Duration; use std::time::Duration;
use anyhow::{Result, bail}; use anyhow::{Context, Result, bail};
use audioadapter_buffers::direct::InterleavedSlice; 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 rubato::{Adjustable as _, Resampler};
use tracing::warn; use tracing::warn;
pub struct Audio { struct CpalAudio {
#[allow(unused)] #[allow(unused)]
stream: cpal::Stream, stream: cpal::Stream,
sampler: rubato::Async<f32>, sampler: rubato::Async<f32>,
@ -17,19 +21,31 @@ pub struct Audio {
const VB_FREQUENCY: usize = 41700; const VB_FREQUENCY: usize = 41700;
impl Audio { impl CpalAudio {
pub fn init() -> Result<Self> { fn init() -> Result<Self> {
let host = cpal::default_host(); let host = cpal::default_host();
let Some(device) = host.default_output_device() else { let device = host
bail!("No output device available"); .default_output_device()
}; .context("no output device available")?;
let default_rate = device.default_output_config()?.sample_rate(); 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) let supported_config = device
}) else { .supported_output_configs()?
bail!("No suitable output config available"); .filter(|c| {
}; c.contains_rate(default_rate)
let mut config = config.with_sample_rate(default_rate).config(); && 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 resample_ratio = config.sample_rate as f64 / VB_FREQUENCY as f64;
let chunk_size = (834.0 * resample_ratio) as usize; let chunk_size = (834.0 * resample_ratio) as usize;
let sampler = rubato::Async::new_poly( let sampler = rubato::Async::new_poly(
@ -42,6 +58,23 @@ impl Audio {
)?; )?;
config.buffer_size = cpal::BufferSize::Fixed(sampler.output_frames_max() as u32); 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::<f32>(device, config, sampler),
SampleFormat::F64 => Self::new::<f64>(device, config, sampler),
SampleFormat::I32 => Self::new::<i32>(device, config, sampler),
SampleFormat::I16 => Self::new::<i16>(device, config, sampler),
_ => bail!("unsupported sample format"),
}
}
fn new_f32_stereo(
device: Device,
config: StreamConfig,
sampler: rubato::Async<f32>,
) -> Result<Self> {
let input_buffer = Vec::with_capacity(sampler.nbr_channels() * sampler.input_frames_max()); 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 output_buffer = vec![0.0; sampler.nbr_channels() * sampler.output_frames_max()];
let (sample_sink, mut sample_source) = let (sample_sink, mut sample_source) =
@ -79,7 +112,60 @@ impl Audio {
}) })
} }
pub fn update(&mut self, mut samples: &[f32]) { fn new<S: SizedSample + FromSample<f32>>(
device: Device,
config: StreamConfig,
sampler: rubato::Async<f32>,
) -> Result<Self> {
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 { while self.input_buffer.len() + samples.len() >= self.sampler.input_frames_next() * 2 {
let samples_needed = let samples_needed =
(self.sampler.input_frames_next() * 2).saturating_sub(self.input_buffer.len()); (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 self.sampler
.set_resample_ratio_relative(1.0 / speed, false)?; .set_resample_ratio_relative(1.0 / speed, false)?;
Ok(()) 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),
}
}
}

View File

@ -138,7 +138,7 @@ impl EmulatorBuilder {
self.audio_on, self.audio_on,
self.watch_rom, self.watch_rom,
self.linked, self.linked,
)?; );
if let Some(path) = self.rom { if let Some(path) = self.rom {
emulator.load_cart(SimId::Player1, &path)?; emulator.load_cart(SimId::Player1, &path)?;
} }
@ -178,11 +178,11 @@ impl Emulator {
audio_on: Arc<[AtomicBool; 2]>, audio_on: Arc<[AtomicBool; 2]>,
watch_rom: Arc<[AtomicBool; 2]>, watch_rom: Arc<[AtomicBool; 2]>,
linked: Arc<AtomicBool>, linked: Arc<AtomicBool>,
) -> Result<Self> { ) -> Self {
Ok(Self { Self {
sims: vec![], sims: vec![],
carts: [None, None], carts: [None, None],
audio: Audio::init()?, audio: Audio::init(),
commands, commands,
sim_state, sim_state,
state, state,
@ -198,7 +198,7 @@ impl Emulator {
eye_contents: [vec![0u8; 384 * 224 * 2], vec![0u8; 384 * 224 * 2]], eye_contents: [vec![0u8; 384 * 224 * 2], vec![0u8; 384 * 224 * 2]],
audio_samples: Vec::with_capacity(EXPECTED_FRAME_SIZE), audio_samples: Vec::with_capacity(EXPECTED_FRAME_SIZE),
buffer: vec![], buffer: vec![],
}) }
} }
pub fn reload_cart(&mut self, sim_id: SimId) -> Result<()> { pub fn reload_cart(&mut self, sim_id: SimId) -> Result<()> {