Make audio much more defensive
This commit is contained in:
parent
1ff4fda55b
commit
644e99c295
173
src/audio.rs
173
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<f32>,
|
||||
|
|
@ -17,19 +21,31 @@ pub struct Audio {
|
|||
|
||||
const VB_FREQUENCY: usize = 41700;
|
||||
|
||||
impl Audio {
|
||||
pub fn init() -> Result<Self> {
|
||||
impl CpalAudio {
|
||||
fn init() -> Result<Self> {
|
||||
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::<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 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<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 {
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<AtomicBool>,
|
||||
) -> Result<Self> {
|
||||
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<()> {
|
||||
|
|
|
|||
Loading…
Reference in New Issue