use std::{ collections::HashMap, io::Cursor, rc::Rc, sync::mpsc::{Receiver, SyncSender, sync_channel}, }; use rodio::Source; use wgui::{assets::AssetProvider, sound::WguiSoundType}; use std::io::Read; pub use rodio; enum AudioThreadMessage { PlaySample(AudioSample), } pub struct AudioSystem { receiver: Option>, sender: SyncSender, } #[derive(Clone)] pub struct AudioSample { buffer: rodio::buffer::SamplesBuffer, } pub struct SamplePlayer { samples: HashMap, } fn audio_thread(receiver: Receiver) { log::debug!("audio_thread starting"); let Ok(mut stream) = rodio::DeviceSinkBuilder::open_default_sink() else { log::error!("Failed to open audio stream. Audio will not work."); return; }; stream.log_on_drop(false); while let Ok(msg) = receiver.recv() { match msg { AudioThreadMessage::PlaySample(audio_sample) => { stream.mixer().add(audio_sample.buffer.clone()); } } } log::debug!("audio_thread exiting"); } fn get_sample_name_from_wgui_sound_type(sound: WguiSoundType) -> &'static str { match sound { WguiSoundType::ButtonMouseEnter => "wgui_mouse_enter", WguiSoundType::ButtonPress => "wgui_button_press", WguiSoundType::ButtonRelease => "wgui_button_release", WguiSoundType::CheckboxCheck => "wgui_checkbox_check", WguiSoundType::CheckboxUncheck => "wgui_checkbox_uncheck", } } impl SamplePlayer { pub fn new() -> Self { Self { samples: HashMap::new(), } } pub fn register_sample(&mut self, sample_name: &str, sample: AudioSample) -> anyhow::Result<()> { // load only once if self.samples.contains_key(sample_name) { log::debug!("Audio sample '{sample_name}' already exists."); return Ok(()); } log::debug!("Registering audio sample '{sample_name}'"); self.samples.insert(String::from(sample_name), sample); Ok(()) } pub fn register_wgui_samples(&mut self, assets: &mut dyn AssetProvider) -> anyhow::Result<()> { let mut load = |sound: WguiSoundType| -> anyhow::Result<()> { let sample_name = get_sample_name_from_wgui_sound_type(sound); let path = &format!("sound/{}.mp3", sample_name); // try loading a custom sound; if one doesn't exist (or it failed to load), use the built-in asset let sound_bytes = match AudioSample::try_bytes_from_config(path) { Ok(bytes) => bytes, Err(_) => assets.load_from_path(path)?.into(), }; self.register_sample(sample_name, AudioSample::from_mp3(&sound_bytes)?)?; Ok(()) }; load(WguiSoundType::ButtonPress)?; load(WguiSoundType::ButtonRelease)?; load(WguiSoundType::ButtonMouseEnter)?; load(WguiSoundType::CheckboxCheck)?; load(WguiSoundType::CheckboxUncheck)?; Ok(()) } pub fn play_sample(&mut self, system: &mut AudioSystem, sample_name: &str) { let Some(sample) = self.samples.get(sample_name) else { log::error!("Failed to play sample by name '{}'", sample_name); return; }; system.play_sample(sample); } pub fn play_wgui_samples(&mut self, system: &mut AudioSystem, samples: Vec) { for sample in samples { self.play_sample(system, get_sample_name_from_wgui_sound_type(sample)); } } } impl Default for SamplePlayer { fn default() -> Self { Self::new() } } impl AudioSystem { pub fn new() -> Self { let (sender, receiver) = sync_channel::(8); Self { receiver: Some(receiver), sender, } } fn lazy_start_thread(&mut self) { let Some(receiver) = self.receiver.take() else { return; // already started }; std::thread::spawn(move || audio_thread(receiver)); } pub fn play_sample(&mut self, sample: &AudioSample) -> Option<()> { self.lazy_start_thread(); let _dont_care = self.sender.try_send(AudioThreadMessage::PlaySample(sample.clone())); Some(()) } } impl Default for AudioSystem { fn default() -> Self { Self::new() } } impl AudioSample { pub fn from_mp3(encoded_bin: &[u8]) -> anyhow::Result { // SAFETY: this is safe // rodio requires us to provide 'static data to decode it // we are casting &T into &'static T just to prevent unnecessary memory copy into Vec. // `encoded_bin` data will be always valid, because we are dropping `decoder` in this scope afterwards. // Compliant and slower version would be: Cursor::new(encoded_bin.to_vec()) let cursor = unsafe { Cursor::new(std::mem::transmute::<&[u8], &'static [u8]>(encoded_bin)) }; let decoder = rodio::Decoder::new_mp3(cursor)?; Ok(Self { buffer: rodio::buffer::SamplesBuffer::new( decoder.channels(), decoder.sample_rate(), decoder.collect::>(), ), }) } pub fn try_bytes_from_config(path: &str) -> anyhow::Result> { let real_path = crate::config_io::get_config_root().join(path); let mut file = std::fs::File::open(&real_path) .inspect_err(|e| log::debug!("Could not open file '{}': {e:?}", real_path.display()))?; let mut file_buffer = vec![]; file .read_to_end(&mut file_buffer) .inspect_err(|e| log::debug!("Could not read file '{}': {e:?}", real_path.display()))?; Ok(file_buffer.into()) } pub fn bytes_from_config_or_default(path: &str, default: &'static [u8]) -> Rc<[u8]> { match AudioSample::try_bytes_from_config(path) { Ok(value) => value, Err(_) => { log::trace!("File '{}' not found, using default.", path); default.into() } } } }