Refactored to allow dummy audio for console frontend

This commit is contained in:
transistor 2023-03-14 20:05:29 -07:00
parent 9be996d2a1
commit e6614f3e15
12 changed files with 195 additions and 65 deletions

2
.gitignore vendored
View File

@ -10,3 +10,5 @@ perf.data.old
binaries/*/*.asm
binaries/*/*.bin
binaries/*/*.smd
emulator/frontends/pixels/dist/

1
Cargo.lock generated
View File

@ -600,6 +600,7 @@ dependencies = [
name = "moa_console"
version = "0.1.0"
dependencies = [
"clap 3.2.22",
"log",
"moa_common",
"moa_computie",

74
docs/discussion.txt Normal file
View File

@ -0,0 +1,74 @@
2021/10/21:
Frontend/Backend Interface
--------------------------
- need a way for the frontend window to be updated with graphics data from a backend device
- also need access to input key and joystick presses; other interfaces can be used for audio, etc
- it would be nice if it was possible to have multiple video output devices in a system
outputting to different windows (ie. one window per video device)
- either the frontend calls a function in the backend to update the window, or the backend
calls an indirect function on the frontend to update the window
- Frontend calling backend:
- Opt 1 - supply a callback and object separately, pass object to callback (no closure)
- older way but should work
- object needs to be Arc in order to share, but doesn't need to be wrapped in a tuple struct
- con: if object is device itself, would still need to use tuple struct wrapper for Addressable
- Opt 2 - supply a trait object with an update method
- frontend would store a Box<dyn Window>
- device would have to make a wrapper: `struct WindowWrapper(Arc<ActualDevice>)` and
impl Window on WindowWrapper, and `struct DeviceWrapper(Arc<ActualDevice>)` and impl
Addressable/Steppable on DeviceWrapper
- pro: in-sync on-demand rendering
- con: lots of complications and indirection
- Opt 3 - supply a common object that devices can update, and that then updates the window
- backend would define a Frame type object which would contain a rendered frame
- system thread would render to the Frame, ui thread would then copy the Frame to window
- con: lots of copying of pixel data
- con: out of sync rendering
- pro: ui can handle any scaling
- Backend calling frontend:
- Opt 4 - host can produce a generic window object that satisfies Window trait
- the device struct which needs the window would have a type parameter for the window
object, and the system thread (step) would call an update function on the generic
window to copy the rendered frame to a ui buffer, which then copies again on ui thread
- window object can only have a buffer, but it can use a native format rather than internal format
- con: out of sync rendering
- Opt 5 - host can produce a Window trait object
- the device would just store the trait object so no need for a type param
- the frontend can't put the native window in the window object so it would need to be a buffer
- con: would need a wrapper if the frontend needs internal access to the common device
- this is
- Opt 3, 4, and 5 would all involve an intermediate buffer, but with Opt 4 or 5, that buffer can be native-compatible
- Opt 3, 4, and 5 are out of sync updating
- Opt 1 and 2 can be in-sync updating if the device object is supplied, but not if it uses an intermediate buffer
- it seems like Opt 4 isn't working because you can't make an existential generic (can't return MiniWindow as W: Window)
- it seems Opt 5 works, but is trying to update the screen waaay too much, and causing the sim to almost never move.
Even with a simple count limit, it seems to pause when it tries updating the screen, probably due to lock contention
- Opt 1 is misleading because you still need a shared object, and it can't be a generic, so it's either a backend-specific
struct or a dyn trait object, so really it's the same as Opt 2, or Opt 3
- it might be possible to have a common data struct that contains 2 frames, one that can only be updated by the sim, and one
that can only be read by in-sync update function, and they are swapped on update (if the writable one isn't locked), so
that the updating is still in sync in the ui thread, but the rendering is happening in another thread
2021/12/07:
Signals, Etc.
-------------
* think more about what kinds of signals are used:
- one setter with one or more passive listeners (bank_register, updated by writing to and used whenever a value is read)
- one or more setters and one listeners (reset, bus_request for the CPUs)
- one one-shot setter (no reset) with one active listener that resets the signal (certain interrupts, including a vsync interrupt)
- what about interrupt controller?

View File

@ -1,17 +1,16 @@
use std::sync::{Arc, Mutex};
use std::collections::VecDeque;
use cpal::{Stream, SampleRate, SampleFormat, StreamConfig, traits::{DeviceTrait, HostTrait, StreamTrait}};
use moa_core::{Clock, warn, error};
use moa_core::Clock;
use moa_core::host::{Audio, ClockedQueue};
const SAMPLE_RATE: usize = 48000;
pub const SAMPLE_RATE: usize = 48000;
#[derive(Clone, Default)]
pub struct AudioFrame {
data: Vec<(f32, f32)>,
pub data: Vec<(f32, f32)>,
}
pub struct AudioSource {
@ -233,6 +232,10 @@ impl AudioOutput {
}))
}
pub fn set_frame_size(&mut self, frame_size: usize) {
self.frame_size = frame_size
}
pub fn add_frame(&mut self, frame: AudioFrame) {
self.output.push_back(frame);
self.sequence_num = self.sequence_num.wrapping_add(1);
@ -253,59 +256,3 @@ impl AudioOutput {
}
}
#[allow(dead_code)]
pub struct CpalAudioOutput {
stream: Stream,
}
impl CpalAudioOutput {
pub fn create_audio_output(output: Arc<Mutex<AudioOutput>>) -> CpalAudioOutput {
let device = cpal::default_host()
.default_output_device()
.expect("No sound output device available");
let config: StreamConfig = device
.supported_output_configs()
.expect("error while querying configs")
.find(|config| config.sample_format() == SampleFormat::F32 && config.channels() == 2)
.expect("no supported config?!")
.with_sample_rate(SampleRate(SAMPLE_RATE as u32))
.into();
let data_callback = move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
let result = if let Ok(mut output) = output.lock() {
output.frame_size = data.len() / 2;
output.pop_next()
} else {
return;
};
if let Some(frame) = result {
let (start, middle, end) = unsafe { frame.data.align_to::<f32>() };
if !start.is_empty() || !end.is_empty() {
warn!("audio: frame wasn't aligned");
}
let length = middle.len().min(data.len());
data[..length].copy_from_slice(&middle[..length]);
} else {
warn!("missed an audio frame");
}
};
let stream = device.build_output_stream(
&config,
data_callback,
move |err| {
error!("ERROR: {:?}", err);
},
).unwrap();
stream.play().unwrap();
CpalAudioOutput {
stream,
}
}
}

View File

@ -0,0 +1,63 @@
use std::sync::{Arc, Mutex};
use cpal::{Stream, SampleRate, SampleFormat, StreamConfig, traits::{DeviceTrait, HostTrait, StreamTrait}};
use moa_core::{warn, error};
use crate::audio::{AudioOutput, SAMPLE_RATE};
#[allow(dead_code)]
pub struct CpalAudioOutput {
stream: Stream,
}
impl CpalAudioOutput {
pub fn create_audio_output(output: Arc<Mutex<AudioOutput>>) -> CpalAudioOutput {
let device = cpal::default_host()
.default_output_device()
.expect("No sound output device available");
let config: StreamConfig = device
.supported_output_configs()
.expect("error while querying configs")
.find(|config| config.sample_format() == SampleFormat::F32 && config.channels() == 2)
.expect("no supported config?!")
.with_sample_rate(SampleRate(SAMPLE_RATE as u32))
.into();
let data_callback = move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
let result = if let Ok(mut output) = output.lock() {
output.set_frame_size(data.len() / 2);
output.pop_next()
} else {
return;
};
if let Some(frame) = result {
let (start, middle, end) = unsafe { frame.data.align_to::<f32>() };
if !start.is_empty() || !end.is_empty() {
warn!("audio: frame wasn't aligned");
}
let length = middle.len().min(data.len());
data[..length].copy_from_slice(&middle[..length]);
} else {
warn!("missed an audio frame");
}
};
let stream = device.build_output_stream(
&config,
data_callback,
move |err| {
error!("ERROR: {:?}", err);
},
).unwrap();
stream.play().unwrap();
CpalAudioOutput {
stream,
}
}
}

View File

@ -2,6 +2,11 @@
#[cfg(feature = "tty")]
pub mod tty;
#[cfg(feature = "audio")]
pub mod audio;
pub use crate::audio::{AudioMixer, AudioSource};
#[cfg(feature = "audio")]
pub mod cpal;
#[cfg(feature = "audio")]
pub use crate::cpal::CpalAudioOutput;

View File

@ -6,6 +6,7 @@ default-run = "moa-computie"
[dependencies]
log = "0.4"
clap = "3.2.20"
simple_logger = "2.3.0"
moa_core = { path = "../../core" }

View File

@ -0,0 +1,23 @@
use clap::{App, Arg};
use moa_console::ConsoleFrontend;
use moa_genesis::{build_genesis, SegaGenesisOptions};
fn main() {
let matches = App::new("Sega Genesis/Mega Drive Emulator")
.arg(Arg::new("ROM")
.help("ROM file to load (must be flat binary)"))
.get_matches();
let mut frontend = ConsoleFrontend;
let mut options = SegaGenesisOptions::default();
if let Some(filename) = matches.value_of("ROM") {
options.rom = filename.to_string();
}
let mut system = build_genesis(&mut frontend, options).unwrap();
system.run_loop();
}

View File

@ -1,6 +1,8 @@
use moa_core::Error;
use moa_core::host::{Host, Tty, WindowUpdater};
use moa_core::host::{Host, Tty, WindowUpdater, ControllerDevice, ControllerUpdater, Audio};
use moa_common::audio::{AudioMixer, AudioSource};
pub struct ConsoleFrontend;
@ -14,5 +16,16 @@ impl Host for ConsoleFrontend {
println!("console: add_window() is not supported from the console; ignoring request...");
Ok(())
}
fn register_controller(&mut self, _device: ControllerDevice, _input: Box<dyn ControllerUpdater>) -> Result<(), Error> {
println!("console: register_controller() is not supported from the console; ignoring request...");
Ok(())
}
fn create_audio_source(&mut self) -> Result<Box<dyn Audio>, Error> {
println!("console: create_audio_source() is not supported from the console; returning dummy device...");
let source = AudioSource::new(AudioMixer::with_default_rate());
Ok(Box::new(source))
}
}

View File

@ -11,7 +11,8 @@ use moa_core::{System, Error};
use moa_core::host::{Host, ControllerUpdater, KeyboardUpdater, KeyEvent, MouseUpdater, MouseState, WindowUpdater, Audio, ControllerDevice};
use moa_core::host::gfx::Frame;
use moa_common::audio::{AudioMixer, AudioSource, CpalAudioOutput};
use moa_common::{AudioMixer, AudioSource};
use moa_common::CpalAudioOutput;
mod keys;
mod controllers;

View File

@ -5,7 +5,7 @@ use moa_core::{System, Error};
use moa_genesis::{SegaGenesisOptions, build_genesis};
fn load_system(host: &mut PixelsFrontend, rom_data: Vec<u8>) -> Result<System, Error> {
let mut options = SegaGenesisOptions::new();
let mut options = SegaGenesisOptions::default();
options.rom_data = Some(rom_data);
build_genesis(host, options)
}

View File

@ -7,7 +7,7 @@ use winit::event_loop::{ControlFlow, EventLoop};
use moa_core::{System, Error};
use moa_core::host::{Host, WindowUpdater, ControllerDevice, ControllerEvent, ControllerUpdater, Audio, DummyAudio};
use moa_core::host::gfx::Frame;
use moa_common::audio::{AudioMixer, AudioSource, CpalAudioOutput};
use moa_common::{AudioMixer, AudioSource, CpalAudioOutput};
use crate::settings;
use crate::create_window;