Refactor audio output

This commit is contained in:
pjht 2024-01-27 17:24:59 -06:00
parent 1ff2990f2e
commit fb262cf3b1
Signed by: pjht
GPG Key ID: CA239FC6934E6F3A
2 changed files with 263 additions and 164 deletions

251
src/audio.rs Normal file
View File

@ -0,0 +1,251 @@
use std::{
sync::mpsc::{self, Receiver, Sender, TryRecvError},
thread,
time::{Duration, Instant},
};
use log::trace;
use soloud::{AudioExt, Handle as VoiceHandle, LoadExt, Soloud, Wav};
#[derive(Clone, Debug)]
pub enum AudioMessage {
PlaySwitchClick,
FanOn,
FanOff,
}
impl AudioMessage {
/// Returns `true` if the audio message is [`FanOff`].
///
/// [`FanOff`]: AudioMessage::FanOff
#[must_use]
pub fn is_fan_off(&self) -> bool {
matches!(self, Self::FanOff)
}
}
pub struct AudioThread {
fan_startup: Wav,
fan_loop: Wav,
fan_shutdown: Wav,
sw_click: Wav,
sl: Soloud,
receiver: Receiver<AudioMessage>,
fan_state: FanState,
}
enum FanState {
Off,
Starting {
start_time: Instant,
sound_offset: Duration,
},
On,
Stopping {
start_time: Instant,
sound_offset: Duration,
handle: VoiceHandle,
},
}
impl FanState {
/// Returns `true` if the fan state is [`Off`].
///
/// [`Off`]: FanState::Off
#[must_use]
fn is_off(&self) -> bool {
matches!(self, Self::Off)
}
/// Returns `true` if the fan state is [`Starting`].
///
/// [`Starting`]: FanState::Starting
#[must_use]
fn is_starting(&self) -> bool {
matches!(self, Self::Starting { .. })
}
/// Returns `true` if the fan state is [`On`].
///
/// [`On`]: FanState::On
#[must_use]
fn is_on(&self) -> bool {
matches!(self, Self::On)
}
/// Returns `true` if the fan state is [`Stopping`].
///
/// [`Stopping`]: FanState::Stopping
#[must_use]
fn is_stopping(&self) -> bool {
matches!(self, Self::Stopping { .. })
}
}
impl AudioThread {
pub fn init() -> Sender<AudioMessage> {
let sl = Soloud::default().unwrap();
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let fan_startup = {
let mut fan_startup = Wav::default();
fan_startup
.load("/home/pjht/projects/altair_emu/resources/fan1.wav")
.unwrap();
fan_startup
};
let fan_loop = {
let mut fan_loop = Wav::default();
fan_loop
.load("/home/pjht/projects/altair_emu/resources/fan2.wav")
.unwrap();
fan_loop
};
let fan_shutdown = {
let mut fan_shutdown = Wav::default();
fan_shutdown
.load("/home/pjht/projects/altair_emu/resources/fan3.wav")
.unwrap();
fan_shutdown
};
let sw_click = {
let mut sw_click = Wav::default();
sw_click
.load("/home/pjht/projects/altair_emu/resources/click2.wav")
.unwrap();
sw_click
};
let slf: AudioThread = Self {
fan_startup,
fan_loop,
fan_shutdown,
sw_click,
sl,
receiver: rx,
fan_state: FanState::Off,
};
slf.run();
});
tx
}
fn run(mut self) {
loop {
let Ok(msg) = self.receiver.recv() else {
break;
};
self.handle_msg(&msg);
}
}
fn handle_msg(&mut self, msg: &AudioMessage) {
self.mark_fan_off_if_stopped();
match msg {
AudioMessage::PlaySwitchClick => {
self.sl.play(&self.sw_click);
}
AudioMessage::FanOn => {
if self.fan_state.is_starting() || self.fan_state.is_on() {
return;
}
trace!(target: "altair_emu::fan_thread", "Start fan");
let fan_startup_handle = if let FanState::Stopping {
start_time,
sound_offset,
handle,
} = self.fan_state
{
self.sl.stop(handle);
let fan_shutdown_elapsed =
start_time.elapsed().as_secs_f64() + sound_offset.as_secs_f64();
let fan_startup_handle = self.sl.play(&self.fan_startup);
self.sl
.seek(
fan_startup_handle,
self.fan_shutdown.length() - fan_shutdown_elapsed,
)
.unwrap();
self.fan_state = FanState::Starting {
start_time: Instant::now(),
sound_offset: Duration::from_secs_f64(
self.fan_shutdown.length() - fan_shutdown_elapsed,
),
};
fan_startup_handle
} else {
let fan_startup_handle = self.sl.play(&self.fan_startup);
self.fan_state = FanState::Starting {
start_time: Instant::now(),
sound_offset: Duration::ZERO,
};
fan_startup_handle
};
while self.sl.is_valid_voice_handle(fan_startup_handle) {
let msg = match self.receiver.try_recv() {
Ok(msg) => msg,
Err(TryRecvError::Empty) => continue,
Err(_) => return,
};
self.handle_msg(&msg);
if msg.is_fan_off() {
return;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
let handle = self.sl.play(&self.fan_loop);
self.sl.set_looping(handle, true);
self.fan_state = FanState::On;
}
AudioMessage::FanOff => match self.fan_state {
FanState::Off | FanState::Stopping { .. } => return,
FanState::Starting {
start_time,
sound_offset,
} => {
self.sl.stop_all();
let fan_startup_elapsed =
start_time.elapsed().as_secs_f64() + sound_offset.as_secs_f64();
let fan_shutdown_handle = self.sl.play(&self.fan_shutdown);
self.fan_state = FanState::Stopping {
start_time: Instant::now(),
sound_offset: Duration::from_secs_f64(
self.fan_startup.length() - fan_startup_elapsed,
),
handle: fan_shutdown_handle,
};
self.sl
.seek(
fan_shutdown_handle,
self.fan_startup.length() - fan_startup_elapsed,
)
.unwrap();
}
FanState::On => {
self.sl.stop_all();
let fan_shutdown_handle = self.sl.play(&self.fan_shutdown);
self.fan_state = FanState::Stopping {
start_time: Instant::now(),
sound_offset: Duration::ZERO,
handle: fan_shutdown_handle,
};
}
},
}
}
fn mark_fan_off_if_stopped(&mut self) {
if let FanState::Stopping {
start_time,
sound_offset,
..
} = self.fan_state
{
let fan_shutdown_elapsed =
start_time.elapsed().as_secs_f64() + sound_offset.as_secs_f64();
if fan_shutdown_elapsed >= self.fan_shutdown.length() {
self.fan_state = FanState::Off;
}
}
}
}

View File

@ -1,15 +1,12 @@
mod audio;
mod card; mod card;
mod cpu; mod cpu;
mod frontpanel; mod frontpanel;
mod ram; mod ram;
use std::{ use std::{path::Path, sync::mpsc::Sender};
path::Path,
sync::{mpsc::Sender, Arc},
thread,
time::{Duration, Instant},
};
use audio::{AudioMessage, AudioThread};
use cpu::{MemCycle, Status, I8080}; use cpu::{MemCycle, Status, I8080};
use device_query::DeviceState; use device_query::DeviceState;
use eframe::{ use eframe::{
@ -17,162 +14,19 @@ use eframe::{
NativeOptions, NativeOptions,
}; };
use egui_modal::Modal; use egui_modal::Modal;
use log::{debug, trace, warn};
use parking_lot::Mutex;
use rand::RngCore; use rand::RngCore;
use rfd::FileDialog; use rfd::FileDialog;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use soloud::{audio, AudioExt, LoadExt, Soloud};
use crate::frontpanel::{switch, Frontpanel}; use crate::frontpanel::{switch, Frontpanel};
#[derive(Clone, Debug)]
enum AudioMessage {
PlaySwitchClick,
}
fn main() -> Result<(), eframe::Error> { fn main() -> Result<(), eframe::Error> {
env_logger::init(); env_logger::init();
let (main_audio_tx, main_audio_rx) = std::sync::mpsc::channel::<AudioMessage>(); let audio_tx = AudioThread::init();
let (fan_audio_tx, fan_audio_rx) = std::sync::mpsc::channel::<bool>();
let sl = Arc::new(Mutex::new(Soloud::default().unwrap()));
let fan_sl = Arc::clone(&sl);
thread::spawn(move || {
let sl = fan_sl;
let rx = fan_audio_rx;
let fan_startup = {
let mut fan_startup = audio::Wav::default();
fan_startup
.load("/home/pjht/projects/altair_emu/resources/fan1.wav")
.unwrap();
fan_startup
};
let fan_loop = {
let mut fan_loop = audio::Wav::default();
fan_loop
.load("/home/pjht/projects/altair_emu/resources/fan2.wav")
.unwrap();
fan_loop
};
let fan_shutdown = {
let mut fan_shutdown = audio::Wav::default();
fan_shutdown
.load("/home/pjht/projects/altair_emu/resources/fan3.wav")
.unwrap();
fan_shutdown
};
debug!(target: "altair_emu::fan_thread", "Fan startup length: {}s. Fan shutdown length: {}s", fan_startup.length(), fan_shutdown.length());
if fan_startup.length() != fan_shutdown.length() {
warn!(target: "altair_emu::fan_thread", "Fan startup sound and shutdown sounds do not have the same length! ({}s and {}s, respectively.) This may cause audio glitches.", fan_startup.length(), fan_shutdown.length());
}
let mut fan_shutdown_start: Option<(Instant, Duration)> = None;
for msg in &rx {
if msg {
trace!(target: "altair_emu::fan_thread", "Start fan");
let fan_startup_start = if let Some(fan_shutdown_start) = fan_shutdown_start.take()
{
trace!(target: "altair_emu::fan_thread", "Fan shutdown sound playing");
let sl = sl.lock();
sl.stop_all();
let fan_shutdown_elapsed = fan_shutdown_start.0.elapsed().as_secs_f64()
+ fan_shutdown_start.1.as_secs_f64();
if fan_shutdown_elapsed >= fan_shutdown.length() {
trace!(target: "altair_emu::fan_thread", "Shutdown elapsed > shutdown length, not actually playing");
let fan_startup_start = (Instant::now(), Duration::ZERO);
sl.play(&fan_startup);
fan_startup_start
} else {
trace!(target: "altair_emu::fan_thread", "Fan shutdown sound stopped, elapsed time {}s", fan_shutdown_elapsed);
let fan_startup_start = (
Instant::now(),
Duration::from_secs_f64(fan_shutdown.length() - fan_shutdown_elapsed),
);
let fan_startup_handle = sl.play(&fan_startup);
sl.seek(
fan_startup_handle,
fan_shutdown.length() - fan_shutdown_elapsed,
)
.unwrap();
drop(sl);
trace!(target: "altair_emu::fan_thread", "Fan startup playing at offset {}s", fan_shutdown.length() - fan_shutdown_elapsed);
fan_startup_start
}
} else {
trace!(target: "altair_emu::fan_thread", "Fan shutdown sound not playing");
let fan_startup_start = (Instant::now(), Duration::ZERO);
sl.lock().play(&fan_startup);
fan_startup_start
};
let mut msg = None;
while sl.lock().voice_count() > 0 {
if let Ok(r_msg) = rx.try_recv() {
trace!(target: "altair_emu::fan_thread", "Got message {} while waiting for end of startup sound", r_msg);
msg = Some(r_msg);
break;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
if let Some(msg) = msg {
if msg {
panic!();
} else {
trace!(target: "altair_emu::fan_thread", "Stop fan in startup");
let sl = sl.lock();
sl.stop_all();
let fan_startup_elapsed = fan_startup_start.0.elapsed().as_secs_f64()
+ fan_startup_start.1.as_secs_f64();
trace!(target: "altair_emu::fan_thread", "Fan startup sound stopped, elapsed time {}s", fan_startup_elapsed);
fan_shutdown_start = Some((
Instant::now(),
Duration::from_secs_f64(fan_startup.length() - fan_startup_elapsed),
));
let fan_shutdown_handle = sl.play(&fan_shutdown);
sl.seek(
fan_shutdown_handle,
fan_startup.length() - fan_startup_elapsed,
)
.unwrap();
drop(sl);
trace!(target: "altair_emu::fan_thread", "Fan shutdown playing at offset {}s", fan_startup.length() - fan_startup_elapsed);
}
continue;
}
let mut sl = sl.lock();
let handle = sl.play(&fan_loop);
sl.set_looping(handle, true);
drop(sl);
trace!(target: "altair_emu::fan_thread", "Fan loop started, start done");
} else {
trace!(target: "altair_emu::fan_thread", "Stop fan");
sl.lock().stop_all();
trace!(target: "altair_emu::fan_thread", "Fan sound stopped");
fan_shutdown_start = Some((Instant::now(), Duration::ZERO));
sl.lock().play(&fan_shutdown);
trace!(target: "altair_emu::fan_thread", "Fan shutdown sound playing, stop done");
}
}
});
thread::spawn(move || {
let rx = main_audio_rx;
let head_step = {
let mut head_step = audio::Wav::default();
head_step
.load("/home/pjht/projects/altair_emu/resources/click2.wav")
.unwrap();
head_step
};
for msg in &rx {
match msg {
AudioMessage::PlaySwitchClick => {
sl.lock().play(&head_step);
}
}
}
});
eframe::run_native( eframe::run_native(
"Altair 8800 Emulator", "Altair 8800 Emulator",
NativeOptions::default(), NativeOptions::default(),
Box::new(|cc| Box::new(AltairEmulator::new(cc, main_audio_tx, fan_audio_tx))), Box::new(|cc| Box::new(AltairEmulator::new(cc, audio_tx))),
) )
} }
@ -201,8 +55,7 @@ struct AltairEmulator {
mouse_newdown: bool, mouse_newdown: bool,
mouse_olddown: bool, mouse_olddown: bool,
running: bool, running: bool,
main_audio_tx: Sender<AudioMessage>, audio_tx: Sender<AudioMessage>,
fan_audio_tx: Sender<bool>,
options: Options, options: Options,
option_window: Option<OptionWindow>, option_window: Option<OptionWindow>,
device_state: DeviceState, device_state: DeviceState,
@ -211,11 +64,7 @@ struct AltairEmulator {
} }
impl AltairEmulator { impl AltairEmulator {
fn new( fn new(cc: &eframe::CreationContext<'_>, audio_tx: Sender<AudioMessage>) -> Self {
cc: &eframe::CreationContext<'_>,
main_audio_tx: Sender<AudioMessage>,
fan_audio_tx: Sender<bool>,
) -> Self {
let options = if cc.storage.unwrap().get_string("options").is_none() { let options = if cc.storage.unwrap().get_string("options").is_none() {
Options { fan_enabled: true } Options { fan_enabled: true }
} else { } else {
@ -272,8 +121,7 @@ impl AltairEmulator {
mouse_newdown: false, mouse_newdown: false,
mouse_olddown: false, mouse_olddown: false,
running: false, running: false,
main_audio_tx, audio_tx,
fan_audio_tx,
options, options,
option_window: None, option_window: None,
device_state: DeviceState::new(), device_state: DeviceState::new(),
@ -735,15 +583,15 @@ impl eframe::App for AltairEmulator {
// } // }
}); });
let old_fan_enabled = self.options.fan_enabled; // let old_fan_enabled = self.options.fan_enabled;
if let Some(option_window) = self.option_window.as_mut() { if let Some(option_window) = self.option_window.as_mut() {
if option_window.draw(ctx, &mut self.options) { if option_window.draw(ctx, &mut self.options) {
self.option_window = None; self.option_window = None;
} }
} }
if (old_fan_enabled != self.options.fan_enabled) && self.power { // if (old_fan_enabled != self.options.fan_enabled) && self.power {
self.fan_audio_tx.send(self.options.fan_enabled).unwrap(); // self.fan_audio_tx.send(self.options.fan_enabled).unwrap();
} // }
if self.running { if self.running {
ctx.request_repaint(); ctx.request_repaint();
} }