diff --git a/src/audio.rs b/src/audio.rs new file mode 100644 index 0000000..362ea4f --- /dev/null +++ b/src/audio.rs @@ -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, + 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 { + 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; + } + } + } +} diff --git a/src/main.rs b/src/main.rs index 4c9b393..ca78c50 100644 --- a/src/main.rs +++ b/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::(); - let (fan_audio_tx, fan_audio_rx) = std::sync::mpsc::channel::(); - 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, - fan_audio_tx: Sender, + audio_tx: Sender, options: Options, option_window: Option, device_state: DeviceState, @@ -211,11 +64,7 @@ struct AltairEmulator { } impl AltairEmulator { - fn new( - cc: &eframe::CreationContext<'_>, - main_audio_tx: Sender, - fan_audio_tx: Sender, - ) -> Self { + fn new(cc: &eframe::CreationContext<'_>, audio_tx: Sender) -> 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(); }