mirror of
https://github.com/transistorfet/moa.git
synced 2025-04-01 07:33:14 +00:00
Refactored to allow dummy audio for console frontend
This commit is contained in:
parent
9be996d2a1
commit
e6614f3e15
2
.gitignore
vendored
2
.gitignore
vendored
@ -10,3 +10,5 @@ perf.data.old
|
||||
binaries/*/*.asm
|
||||
binaries/*/*.bin
|
||||
binaries/*/*.smd
|
||||
|
||||
emulator/frontends/pixels/dist/
|
||||
|
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -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
74
docs/discussion.txt
Normal 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?
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
63
emulator/frontends/common/src/cpal.rs
Normal file
63
emulator/frontends/common/src/cpal.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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" }
|
||||
|
23
emulator/frontends/console/src/bin/moa-console-genesis.rs
Normal file
23
emulator/frontends/console/src/bin/moa-console-genesis.rs
Normal 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();
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user