// 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<f32> = 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);
}
}