Compare commits
No commits in common. "1d4c6c9e88f7b777e0380fd67ddc2292b02e95c6" and "1ff4fda55bdc8cdb394ff94aa56028599b40a3b4" have entirely different histories.
1d4c6c9e88
...
1ff4fda55b
|
|
@ -424,9 +424,6 @@ impl ApplicationHandler<UserEvent> for Application {
|
||||||
{
|
{
|
||||||
self.controllers.handle_key_event(&event);
|
self.controllers.handle_key_event(&event);
|
||||||
}
|
}
|
||||||
WindowEvent::DroppedFile(path) => {
|
|
||||||
self.app.handle_dropped_file(viewport_id, path);
|
|
||||||
}
|
|
||||||
WindowEvent::Focused(new_focused) => {
|
WindowEvent::Focused(new_focused) => {
|
||||||
self.focused = new_focused.then_some(viewport_id);
|
self.focused = new_focused.then_some(viewport_id);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
173
src/audio.rs
173
src/audio.rs
|
|
@ -1,16 +1,12 @@
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Result, bail};
|
||||||
use audioadapter_buffers::direct::InterleavedSlice;
|
use audioadapter_buffers::direct::InterleavedSlice;
|
||||||
use cpal::{
|
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||||
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;
|
||||||
|
|
||||||
struct CpalAudio {
|
pub struct Audio {
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
stream: cpal::Stream,
|
stream: cpal::Stream,
|
||||||
sampler: rubato::Async<f32>,
|
sampler: rubato::Async<f32>,
|
||||||
|
|
@ -21,31 +17,19 @@ struct CpalAudio {
|
||||||
|
|
||||||
const VB_FREQUENCY: usize = 41700;
|
const VB_FREQUENCY: usize = 41700;
|
||||||
|
|
||||||
impl CpalAudio {
|
impl Audio {
|
||||||
fn init() -> Result<Self> {
|
pub fn init() -> Result<Self> {
|
||||||
let host = cpal::default_host();
|
let host = cpal::default_host();
|
||||||
let device = host
|
let Some(device) = host.default_output_device() else {
|
||||||
.default_output_device()
|
bail!("No output device available");
|
||||||
.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| {
|
||||||
let supported_config = device
|
c.channels() == 2 && c.sample_format().is_float() && c.contains_rate(default_rate)
|
||||||
.supported_output_configs()?
|
}) else {
|
||||||
.filter(|c| {
|
bail!("No suitable output config available");
|
||||||
c.contains_rate(default_rate)
|
};
|
||||||
&& matches!(
|
let mut config = config.with_sample_rate(default_rate).config();
|
||||||
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(
|
||||||
|
|
@ -58,23 +42,6 @@ impl CpalAudio {
|
||||||
)?;
|
)?;
|
||||||
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) =
|
||||||
|
|
@ -112,60 +79,7 @@ impl CpalAudio {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new<S: SizedSample + FromSample<f32>>(
|
pub fn update(&mut self, mut samples: &[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());
|
||||||
|
|
@ -203,64 +117,9 @@ impl CpalAudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_speed(&mut self, speed: f64) -> Result<()> {
|
pub 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>,
|
||||||
) -> Self {
|
) -> Result<Self> {
|
||||||
Self {
|
Ok(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<()> {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
use std::{
|
use std::{
|
||||||
fmt::Write as _,
|
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
sync::{Arc, Weak},
|
sync::{Arc, Weak},
|
||||||
};
|
};
|
||||||
|
|
@ -50,23 +49,6 @@ pub struct FileDialogBuilder {
|
||||||
|
|
||||||
impl FileDialogBuilder {
|
impl FileDialogBuilder {
|
||||||
pub fn add_filter(self, name: impl Into<String>, extensions: &[impl ToString]) -> Self {
|
pub fn add_filter(self, name: impl Into<String>, extensions: &[impl ToString]) -> Self {
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
let exts = extensions
|
|
||||||
.iter()
|
|
||||||
.map(|ext| {
|
|
||||||
let mut normalized = String::new();
|
|
||||||
for char in ext.to_string().chars() {
|
|
||||||
if char.is_ascii_lowercase() {
|
|
||||||
let _ = write!(&mut normalized, "[{char}{}]", char.to_ascii_uppercase());
|
|
||||||
} else {
|
|
||||||
normalized.push(char);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
normalized
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
let extensions = &exts;
|
|
||||||
Self {
|
Self {
|
||||||
dialog: self.dialog.add_filter(name, extensions),
|
dialog: self.dialog.add_filter(name, extensions),
|
||||||
..self
|
..self
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{path::PathBuf, sync::Arc};
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub use about::AboutWindow;
|
pub use about::AboutWindow;
|
||||||
use egui::{Ui, ViewportBuilder};
|
use egui::{Ui, ViewportBuilder};
|
||||||
|
|
@ -47,10 +47,6 @@ pub trait AppWindow {
|
||||||
let _ = event;
|
let _ = event;
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
fn handle_dropped_file(&mut self, path: PathBuf) -> bool {
|
|
||||||
let _ = path;
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct InitArgs<'a> {
|
pub struct InitArgs<'a> {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
use std::{
|
use std::{
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut},
|
||||||
path::PathBuf,
|
|
||||||
sync::{Arc, Mutex, atomic::AtomicBool, mpsc},
|
sync::{Arc, Mutex, atomic::AtomicBool, mpsc},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
@ -215,10 +214,6 @@ impl GameWindow {
|
||||||
self.handle_event(viewport_id, |window| window.handle_gamepad_event(event))
|
self.handle_event(viewport_id, |window| window.handle_gamepad_event(event))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_dropped_file(&mut self, viewport_id: ViewportId, path: PathBuf) -> bool {
|
|
||||||
self.handle_event(viewport_id, |window| window.handle_dropped_file(path))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_event<F, R>(&mut self, viewport_id: ViewportId, cb: F) -> R
|
fn handle_event<F, R>(&mut self, viewport_id: ViewportId, cb: F) -> R
|
||||||
where
|
where
|
||||||
F: FnOnce(&mut dyn AppWindow) -> R,
|
F: FnOnce(&mut dyn AppWindow) -> R,
|
||||||
|
|
@ -770,12 +765,6 @@ impl AppWindow for GameWindow {
|
||||||
}
|
}
|
||||||
self.file_picker.set_window(args.window);
|
self.file_picker.set_window(args.window);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_dropped_file(&mut self, path: PathBuf) -> bool {
|
|
||||||
self.client
|
|
||||||
.send_command(EmulatorCommand::LoadGame(self.sim_id, path));
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for GameWindow {
|
impl Drop for GameWindow {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue