// roughly based on http://equalarea.com/paul/alsa-audio.html use crate::{error::Error, PlaySoundParams}; use quad_alsa_sys as sys; use std::sync::mpsc; pub use crate::mixer::Playback; mod consts { pub const DEVICES: &[&str] = &["default\0", "pipewire\0"]; pub const RATE: u32 = 44100; pub const CHANNELS: u32 = 2; pub const PCM_BUFFER_SIZE: ::std::os::raw::c_ulong = 4096; } /// Open and configure the ALSA playback device. /// /// Vendored + patched from quad-snd 0.2.8: every failure returns `None` /// instead of panicking, so machines without a working audio device run the /// game silently rather than crashing at startup. unsafe fn setup_pcm_device() -> Option<*mut sys::snd_pcm_t> { let mut pcm_handle = std::ptr::null_mut(); // Open the PCM device in playback mode if !consts::DEVICES.iter().any(|device| { sys::snd_pcm_open( &mut pcm_handle, device.as_ptr() as _, sys::SND_PCM_STREAM_PLAYBACK, 0, ) >= 0 }) { eprintln!("warn: no audio device found - running silent"); return None; } // Any configuration failure closes the handle and goes silent. macro_rules! fail { ($msg:expr) => {{ eprintln!("warn: audio config failed ({}) - running silent", $msg); sys::snd_pcm_close(pcm_handle); return None; }}; } let mut hw_params: *mut sys::snd_pcm_hw_params_t = std::ptr::null_mut(); sys::snd_pcm_hw_params_malloc(&mut hw_params); sys::snd_pcm_hw_params_any(pcm_handle, hw_params); if sys::snd_pcm_hw_params_set_access(pcm_handle, hw_params, sys::SND_PCM_ACCESS_RW_INTERLEAVED) < 0 { fail!("interleaved mode"); } if sys::snd_pcm_hw_params_set_format(pcm_handle, hw_params, sys::SND_PCM_FORMAT_FLOAT_LE) < 0 { fail!("float format"); } if sys::snd_pcm_hw_params_set_buffer_size(pcm_handle, hw_params, consts::PCM_BUFFER_SIZE) < 0 { fail!("buffer size"); } if sys::snd_pcm_hw_params_set_channels(pcm_handle, hw_params, consts::CHANNELS) < 0 { fail!("channels"); } let mut rate = consts::RATE; if sys::snd_pcm_hw_params_set_rate_near(pcm_handle, hw_params, &mut rate, std::ptr::null_mut()) < 0 { fail!("rate"); } // Write parameters if sys::snd_pcm_hw_params(pcm_handle, hw_params) < 0 { fail!("hardware parameters"); } sys::snd_pcm_hw_params_free(hw_params); // tell ALSA to wake us up whenever AudioContext::PCM_BUFFER_SIZE or more frames // of playback data can be delivered. Also, tell // ALSA that we'll start the device ourselves. let mut sw_params: *mut sys::snd_pcm_sw_params_t = std::ptr::null_mut(); if sys::snd_pcm_sw_params_malloc(&mut sw_params) < 0 { fail!("sw params alloc"); } if sys::snd_pcm_sw_params_current(pcm_handle, sw_params) < 0 { fail!("sw params current"); } if sys::snd_pcm_sw_params_set_start_threshold(pcm_handle, sw_params, 0) < 0 { fail!("start mode"); } if sys::snd_pcm_sw_params(pcm_handle, sw_params) < 0 { fail!("sw params"); } sys::snd_pcm_sw_params_free(sw_params); if sys::snd_pcm_prepare(pcm_handle) < 0 { fail!("prepare"); } Some(pcm_handle) } unsafe fn audio_thread(mut mixer: crate::mixer::Mixer) { let mut buffer: Vec = vec![0.0; consts::PCM_BUFFER_SIZE as usize * 2]; let pcm_handle = match setup_pcm_device() { Some(handle) => handle, None => { // No device: idle forever. Mixer messages queue harmlessly and // every send site already tolerates a missing receiver. loop { std::thread::sleep(std::time::Duration::from_secs(3600)); } } }; loop { // Wait for PCM to be ready for next write (no timeout) if sys::snd_pcm_wait(pcm_handle, -1) < 0 { // Device went away mid-run: stop the thread instead of panicking. eprintln!("warn: audio device not ready - sound disabled"); break; } // // find out how much space is available for playback data // teoretically it should reduce latency - we will fill a minimum amount of // frames just to keep alsa busy and will be able to mix some fresh sounds // it does, but also randmly panics sometimes // let frames_to_deliver = sys::snd_pcm_avail_update(pcm_handle); // println!("{}", frames_to_deliver); // let frames_to_deliver = if frames_to_deliver > consts::PCM_BUFFER_SIZE as _ { // consts::PCM_BUFFER_SIZE as i64 // } else { // frames_to_deliver // }; let frames_to_deliver = consts::PCM_BUFFER_SIZE as i64; // ask mixer to fill the buffer mixer.fill_audio_buffer(&mut buffer, frames_to_deliver as usize); // send filled buffer back to alsa let frames_writen = sys::snd_pcm_writei( pcm_handle, buffer.as_ptr() as *const _, frames_to_deliver as _, ); if frames_writen == -libc::EPIPE as ::std::os::raw::c_long { println!("Underrun occured: -EPIPE, attempting recover"); sys::snd_pcm_recover(pcm_handle, frames_writen as _, 0); } if frames_writen > 0 && frames_writen != frames_to_deliver as _ { println!("Underrun occured: frames_writen != frames_to_deliver, attempting recover"); sys::snd_pcm_recover(pcm_handle, frames_writen as _, 0); } } } pub struct AudioContext { pub(crate) mixer_ctrl: crate::mixer::MixerControl, } impl AudioContext { pub fn new() -> AudioContext { use crate::mixer::Mixer; let (mixer_builder, mixer_ctrl) = Mixer::new(); std::thread::spawn(move || unsafe { audio_thread(mixer_builder.build()); }); AudioContext { mixer_ctrl } } } pub struct Sound { sound_id: u32, } impl Sound { pub fn load(ctx: &AudioContext, data: &[u8]) -> Sound { let sound_id = ctx.mixer_ctrl.load(data); Sound { sound_id } } pub fn play(&self, ctx: &AudioContext, params: PlaySoundParams) -> Playback { ctx.mixer_ctrl.play(self.sound_id, params) } pub fn stop(&self, ctx: &AudioContext) { ctx.mixer_ctrl.stop_all(self.sound_id); } pub fn set_volume(&self, ctx: &AudioContext, volume: f32) { ctx.mixer_ctrl.set_volume_all(self.sound_id, volume); } pub fn delete(&self, ctx: &AudioContext) { ctx.mixer_ctrl.delete(self.sound_id); } }