use std::error::Error;
use std::fmt;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, mpsc};
use std::thread;
use std::time::Duration;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::{SampleFormat, Stream, StreamConfig};
pub(crate) trait StereoEngine: Send + 'static {
fn next_stereo(&mut self) -> (f32, f32);
}
#[derive(Debug)]
pub(crate) enum AudioStartError {
NoOutputDevice,
UnsupportedSampleFormat(SampleFormat),
Backend(Box<dyn Error + Send + Sync>),
ThreadExited,
}
impl AudioStartError {
fn backend(error: impl Error + Send + Sync + 'static) -> Self {
Self::Backend(Box::new(error))
}
}
impl fmt::Display for AudioStartError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NoOutputDevice => write!(f, "no default output audio device found"),
Self::UnsupportedSampleFormat(format) => {
write!(f, "unsupported sample format: {format:?}")
}
Self::Backend(error) => write!(f, "{error}"),
Self::ThreadExited => {
write!(f, "audio thread exited before starting the stream")
}
}
}
}
impl Error for AudioStartError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Backend(error) => Some(error.as_ref()),
_ => None,
}
}
}
const DEVICE_POLL_INTERVAL: Duration = Duration::from_secs(1);
pub(crate) struct AudioOutput {
stop: Arc<AtomicBool>,
watcher: Option<thread::JoinHandle<()>>,
}
impl Drop for AudioOutput {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
if let Some(handle) = self.watcher.take() {
let _ = handle.join();
}
}
}
pub(crate) fn start_stream<E>(
app_id: &str,
engine_factory: impl Fn(f32) -> E + Send + 'static,
) -> Result<AudioOutput, AudioStartError>
where
E: StereoEngine,
{
let app_id = app_id.to_string();
let stop = Arc::new(AtomicBool::new(false));
let (init_tx, init_rx) = mpsc::channel();
let watcher = {
let stop = Arc::clone(&stop);
thread::Builder::new()
.name("nooise-audio".into())
.spawn(move || run_audio_thread(app_id, engine_factory, stop, init_tx))
.map_err(AudioStartError::backend)?
};
match init_rx.recv() {
Ok(Ok(())) => {}
Ok(Err(error)) => return Err(error),
Err(_) => return Err(AudioStartError::ThreadExited),
}
Ok(AudioOutput {
stop,
watcher: Some(watcher),
})
}
#[allow(unused_assignments)]
fn run_audio_thread<E>(
app_id: String,
engine_factory: impl Fn(f32) -> E,
stop: Arc<AtomicBool>,
init_tx: mpsc::Sender<Result<(), AudioStartError>>,
) where
E: StereoEngine,
{
let needs_rebuild = Arc::new(AtomicBool::new(false));
let mut _stream = match open_stream(&app_id, &engine_factory, Arc::clone(&needs_rebuild)) {
Ok(stream) => {
let _ = init_tx.send(Ok(()));
stream
}
Err(error) => {
let _ = init_tx.send(Err(error));
return;
}
};
let mut current_device_name = default_device_name();
while !stop.load(Ordering::Relaxed) {
thread::sleep(DEVICE_POLL_INTERVAL);
if stop.load(Ordering::Relaxed) {
break;
}
let device_changed = default_device_name() != current_device_name;
let errored = needs_rebuild.swap(false, Ordering::Relaxed);
if !device_changed && !errored {
continue;
}
match open_stream(&app_id, &engine_factory, Arc::clone(&needs_rebuild)) {
Ok(new_stream) => {
_stream = new_stream;
current_device_name = default_device_name();
}
Err(err) => eprintln!("failed to rebuild audio stream: {err}"),
}
}
}
fn default_device_name() -> Option<String> {
cpal::default_host()
.default_output_device()
.and_then(|device| device.name().ok())
}
fn open_stream<E>(
app_id: &str,
engine_factory: &impl Fn(f32) -> E,
needs_rebuild: Arc<AtomicBool>,
) -> Result<Stream, AudioStartError>
where
E: StereoEngine,
{
let host = cpal::default_host();
let device = host
.default_output_device()
.ok_or(AudioStartError::NoOutputDevice)?;
let supported_config = device
.default_output_config()
.map_err(AudioStartError::backend)?;
let sample_format = supported_config.sample_format();
let stream_config: StreamConfig = supported_config.into();
let sample_rate = stream_config.sample_rate.0 as f32;
println!(
"running {app_id} at {} Hz on {}",
sample_rate as u32,
device.name().map_err(AudioStartError::backend)?
);
let engine = engine_factory(sample_rate);
let stream = match sample_format {
SampleFormat::F32 => build_stream(&device, &stream_config, engine, needs_rebuild, |s| s)?,
SampleFormat::I16 => build_stream(&device, &stream_config, engine, needs_rebuild, to_i16)?,
SampleFormat::U16 => build_stream(&device, &stream_config, engine, needs_rebuild, to_u16)?,
other => return Err(AudioStartError::UnsupportedSampleFormat(other)),
};
stream.play().map_err(AudioStartError::backend)?;
Ok(stream)
}
fn build_stream<E, T, C>(
device: &cpal::Device,
config: &StreamConfig,
engine: E,
needs_rebuild: Arc<AtomicBool>,
convert: C,
) -> Result<Stream, AudioStartError>
where
E: StereoEngine,
T: cpal::SizedSample + Send + 'static,
C: Fn(f32) -> T + Send + 'static,
{
let channels = config.channels as usize;
let mut engine = engine;
device
.build_output_stream(
config,
move |data: &mut [T], _| {
for frame in data.chunks_mut(channels) {
let (left, right) = engine.next_stereo();
write_frame(frame, convert(left), convert(right));
}
},
move |error| audio_error(error, &needs_rebuild),
None,
)
.map_err(AudioStartError::backend)
}
fn write_frame<T: Copy>(frame: &mut [T], left: T, right: T) {
if let Some(sample) = frame.first_mut() {
*sample = left;
}
if frame.len() > 1 {
frame[1] = right;
}
for sample in frame.iter_mut().skip(2) {
*sample = left;
}
}
fn to_i16(sample: f32) -> i16 {
(sample.clamp(-1.0, 1.0) * i16::MAX as f32) as i16
}
fn to_u16(sample: f32) -> u16 {
((sample.clamp(-1.0, 1.0) * 0.5 + 0.5) * u16::MAX as f32) as u16
}
fn audio_error(error: cpal::StreamError, needs_rebuild: &Arc<AtomicBool>) {
eprintln!("audio stream error: {error}");
needs_rebuild.store(true, Ordering::Relaxed);
}