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 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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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<()> {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue