Refactored to allow dummy audio for console frontend
This commit is contained in:
parent
9be996d2a1
commit
e6614f3e15
|
@ -10,3 +10,5 @@ perf.data.old
|
||||||
binaries/*/*.asm
|
binaries/*/*.asm
|
||||||
binaries/*/*.bin
|
binaries/*/*.bin
|
||||||
binaries/*/*.smd
|
binaries/*/*.smd
|
||||||
|
|
||||||
|
emulator/frontends/pixels/dist/
|
||||||
|
|
|
@ -600,6 +600,7 @@ dependencies = [
|
||||||
name = "moa_console"
|
name = "moa_console"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"clap 3.2.22",
|
||||||
"log",
|
"log",
|
||||||
"moa_common",
|
"moa_common",
|
||||||
"moa_computie",
|
"moa_computie",
|
||||||
|
|
|
@ -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?
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
|
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::collections::VecDeque;
|
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};
|
use moa_core::host::{Audio, ClockedQueue};
|
||||||
|
|
||||||
const SAMPLE_RATE: usize = 48000;
|
|
||||||
|
|
||||||
|
pub const SAMPLE_RATE: usize = 48000;
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct AudioFrame {
|
pub struct AudioFrame {
|
||||||
data: Vec<(f32, f32)>,
|
pub data: Vec<(f32, f32)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AudioSource {
|
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) {
|
pub fn add_frame(&mut self, frame: AudioFrame) {
|
||||||
self.output.push_back(frame);
|
self.output.push_back(frame);
|
||||||
self.sequence_num = self.sequence_num.wrapping_add(1);
|
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,11 @@
|
||||||
#[cfg(feature = "tty")]
|
#[cfg(feature = "tty")]
|
||||||
pub mod tty;
|
pub mod tty;
|
||||||
|
|
||||||
#[cfg(feature = "audio")]
|
|
||||||
pub mod audio;
|
pub mod audio;
|
||||||
|
pub use crate::audio::{AudioMixer, AudioSource};
|
||||||
|
|
||||||
|
#[cfg(feature = "audio")]
|
||||||
|
pub mod cpal;
|
||||||
|
#[cfg(feature = "audio")]
|
||||||
|
pub use crate::cpal::CpalAudioOutput;
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ default-run = "moa-computie"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
clap = "3.2.20"
|
||||||
simple_logger = "2.3.0"
|
simple_logger = "2.3.0"
|
||||||
|
|
||||||
moa_core = { path = "../../core" }
|
moa_core = { path = "../../core" }
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
|
||||||
use moa_core::Error;
|
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;
|
pub struct ConsoleFrontend;
|
||||||
|
|
||||||
|
@ -14,5 +16,16 @@ impl Host for ConsoleFrontend {
|
||||||
println!("console: add_window() is not supported from the console; ignoring request...");
|
println!("console: add_window() is not supported from the console; ignoring request...");
|
||||||
Ok(())
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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::{Host, ControllerUpdater, KeyboardUpdater, KeyEvent, MouseUpdater, MouseState, WindowUpdater, Audio, ControllerDevice};
|
||||||
use moa_core::host::gfx::Frame;
|
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 keys;
|
||||||
mod controllers;
|
mod controllers;
|
||||||
|
|
|
@ -5,7 +5,7 @@ use moa_core::{System, Error};
|
||||||
use moa_genesis::{SegaGenesisOptions, build_genesis};
|
use moa_genesis::{SegaGenesisOptions, build_genesis};
|
||||||
|
|
||||||
fn load_system(host: &mut PixelsFrontend, rom_data: Vec<u8>) -> Result<System, Error> {
|
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);
|
options.rom_data = Some(rom_data);
|
||||||
build_genesis(host, options)
|
build_genesis(host, options)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ use winit::event_loop::{ControlFlow, EventLoop};
|
||||||
use moa_core::{System, Error};
|
use moa_core::{System, Error};
|
||||||
use moa_core::host::{Host, WindowUpdater, ControllerDevice, ControllerEvent, ControllerUpdater, Audio, DummyAudio};
|
use moa_core::host::{Host, WindowUpdater, ControllerDevice, ControllerEvent, ControllerUpdater, Audio, DummyAudio};
|
||||||
use moa_core::host::gfx::Frame;
|
use moa_core::host::gfx::Frame;
|
||||||
use moa_common::audio::{AudioMixer, AudioSource, CpalAudioOutput};
|
use moa_common::{AudioMixer, AudioSource, CpalAudioOutput};
|
||||||
|
|
||||||
use crate::settings;
|
use crate::settings;
|
||||||
use crate::create_window;
|
use crate::create_window;
|
||||||
|
|
Loading…
Reference in New Issue