Refactor audio output
This commit is contained in:
parent
1ff2990f2e
commit
fb262cf3b1
251
src/audio.rs
Normal file
251
src/audio.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
176
src/main.rs
176
src/main.rs
@ -1,15 +1,12 @@
|
||||
mod audio;
|
||||
mod card;
|
||||
mod cpu;
|
||||
mod frontpanel;
|
||||
mod ram;
|
||||
|
||||
use std::{
|
||||
path::Path,
|
||||
sync::{mpsc::Sender, Arc},
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::{path::Path, sync::mpsc::Sender};
|
||||
|
||||
use audio::{AudioMessage, AudioThread};
|
||||
use cpu::{MemCycle, Status, I8080};
|
||||
use device_query::DeviceState;
|
||||
use eframe::{
|
||||
@ -17,162 +14,19 @@ use eframe::{
|
||||
NativeOptions,
|
||||
};
|
||||
use egui_modal::Modal;
|
||||
use log::{debug, trace, warn};
|
||||
use parking_lot::Mutex;
|
||||
use rand::RngCore;
|
||||
use rfd::FileDialog;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use soloud::{audio, AudioExt, LoadExt, Soloud};
|
||||
|
||||
use crate::frontpanel::{switch, Frontpanel};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum AudioMessage {
|
||||
PlaySwitchClick,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), eframe::Error> {
|
||||
env_logger::init();
|
||||
let (main_audio_tx, main_audio_rx) = std::sync::mpsc::channel::<AudioMessage>();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let audio_tx = AudioThread::init();
|
||||
eframe::run_native(
|
||||
"Altair 8800 Emulator",
|
||||
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_olddown: bool,
|
||||
running: bool,
|
||||
main_audio_tx: Sender<AudioMessage>,
|
||||
fan_audio_tx: Sender<bool>,
|
||||
audio_tx: Sender<AudioMessage>,
|
||||
options: Options,
|
||||
option_window: Option<OptionWindow>,
|
||||
device_state: DeviceState,
|
||||
@ -211,11 +64,7 @@ struct AltairEmulator {
|
||||
}
|
||||
|
||||
impl AltairEmulator {
|
||||
fn new(
|
||||
cc: &eframe::CreationContext<'_>,
|
||||
main_audio_tx: Sender<AudioMessage>,
|
||||
fan_audio_tx: Sender<bool>,
|
||||
) -> Self {
|
||||
fn new(cc: &eframe::CreationContext<'_>, audio_tx: Sender<AudioMessage>) -> Self {
|
||||
let options = if cc.storage.unwrap().get_string("options").is_none() {
|
||||
Options { fan_enabled: true }
|
||||
} else {
|
||||
@ -272,8 +121,7 @@ impl AltairEmulator {
|
||||
mouse_newdown: false,
|
||||
mouse_olddown: false,
|
||||
running: false,
|
||||
main_audio_tx,
|
||||
fan_audio_tx,
|
||||
audio_tx,
|
||||
options,
|
||||
option_window: None,
|
||||
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 option_window.draw(ctx, &mut self.options) {
|
||||
self.option_window = None;
|
||||
}
|
||||
}
|
||||
if (old_fan_enabled != self.options.fan_enabled) && self.power {
|
||||
self.fan_audio_tx.send(self.options.fan_enabled).unwrap();
|
||||
}
|
||||
// if (old_fan_enabled != self.options.fan_enabled) && self.power {
|
||||
// self.fan_audio_tx.send(self.options.fan_enabled).unwrap();
|
||||
// }
|
||||
if self.running {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user