use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
pub struct PlaybackHandle {
stream_guard: Option<cpal::Stream>,
producer: rtrb::Producer<f32>,
flush: Arc<AtomicBool>,
device_error: Arc<AtomicBool>,
channels: usize,
sample_rate: u32,
underruns: Arc<AtomicU64>,
}
impl PlaybackHandle {
pub fn producer_mut(&mut self) -> &mut rtrb::Producer<f32> {
&mut self.producer
}
pub fn channels(&self) -> usize { self.channels }
pub fn sample_rate(&self) -> u32 { self.sample_rate }
pub fn flush_flag(&self) -> Arc<AtomicBool> { self.flush.clone() }
pub fn device_error_flag(&self) -> Arc<AtomicBool> { self.device_error.clone() }
pub fn underrun_counter(&self) -> Arc<AtomicU64> { self.underruns.clone() }
pub fn is_live(&self) -> bool { self.stream_guard.is_some() }
pub fn split(self) -> (StreamKeeper, FeederBundle) {
let mut me = std::mem::ManuallyDrop::new(self);
let stream = me.stream_guard.take();
let producer = unsafe { std::ptr::read(&me.producer) };
let flush = unsafe { std::ptr::read(&me.flush) };
let device_error = unsafe { std::ptr::read(&me.device_error) };
let underruns = unsafe { std::ptr::read(&me.underruns) };
let channels = me.channels;
let sample_rate = me.sample_rate;
let keeper = StreamKeeper { stream_guard: stream };
let bundle = FeederBundle {
producer,
flush,
device_error,
channels,
sample_rate,
underruns,
};
(keeper, bundle)
}
}
impl Drop for PlaybackHandle {
fn drop(&mut self) {
if self.stream_guard.is_some() {
eprintln!("[forge-audio] PlaybackHandle dropped — audio device closed");
}
}
}
pub struct StreamKeeper {
stream_guard: Option<cpal::Stream>,
}
impl Drop for StreamKeeper {
fn drop(&mut self) {
if self.stream_guard.is_some() {
eprintln!("[forge-audio] StreamKeeper dropped — audio device closed");
}
}
}
pub struct FeederBundle {
pub producer: rtrb::Producer<f32>,
pub flush: Arc<AtomicBool>,
pub device_error: Arc<AtomicBool>,
pub channels: usize,
pub sample_rate: u32,
pub underruns: Arc<AtomicU64>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct AudioDeviceInfo {
pub name: String,
pub api: String,
pub sample_rates: Vec<u32>,
pub channels: u16,
}
pub fn list_output_devices() -> Vec<AudioDeviceInfo> {
let host = cpal::default_host();
let host_name = format!("{:?}", host.id());
let devices = match host.output_devices() {
Ok(d) => d,
Err(e) => {
eprintln!("[audio] Cannot enumerate output devices: {}", e);
return vec![];
}
};
devices
.filter_map(|dev| {
let name = dev.name().unwrap_or_else(|_| "Unknown".into());
let mut sample_rates = Vec::new();
let mut channels = 2u16;
if let Ok(configs) = dev.supported_output_configs() {
for cfg in configs {
channels = channels.max(cfg.channels());
let min = cfg.min_sample_rate().0;
let max = cfg.max_sample_rate().0;
for &sr in &[44100u32, 48000, 88200, 96000, 192000] {
if sr >= min && sr <= max && !sample_rates.contains(&sr) {
sample_rates.push(sr);
}
}
}
}
sample_rates.sort();
if sample_rates.is_empty() {
sample_rates.push(48000);
}
Some(AudioDeviceInfo {
name,
api: host_name.clone(),
sample_rates,
channels,
})
})
.collect()
}
pub fn start_playback_lockfree(
buffer_size: usize,
) -> Result<PlaybackHandle, String> {
start_playback_lockfree_with_error_flag(buffer_size, None, None, None)
}
pub fn null_playback(buffer_size: usize, sample_rate: u32) -> PlaybackHandle {
let (producer, consumer) = rtrb::RingBuffer::<f32>::new(buffer_size);
std::mem::forget(consumer);
PlaybackHandle {
stream_guard: None,
producer,
flush: Arc::new(AtomicBool::new(false)),
device_error: Arc::new(AtomicBool::new(true)),
channels: 2,
sample_rate,
underruns: Arc::new(AtomicU64::new(0)),
}
}
pub fn start_playback_lockfree_with_error_flag(
buffer_size: usize,
error_flag: Option<Arc<AtomicBool>>,
device_name: Option<&str>,
cpal_buffer: Option<u32>,
) -> Result<PlaybackHandle, String> {
if std::env::var("DAW_NO_AUDIO").is_ok() {
eprintln!("[DAW_NO_AUDIO] *** AUDIO OUTPUT DISABLED *** No cpal device opened.");
eprintln!("[DAW_NO_AUDIO] Mixer math + IPC still run; nothing reaches phones/S2/speakers.");
let (producer, consumer) = rtrb::RingBuffer::<f32>::new(buffer_size);
std::mem::forget(consumer); let flush = Arc::new(AtomicBool::new(false));
let device_error = error_flag.unwrap_or_else(|| Arc::new(AtomicBool::new(false)));
let underruns = Arc::new(AtomicU64::new(0));
return Ok(PlaybackHandle {
stream_guard: None,
producer,
flush,
device_error,
channels: 2,
sample_rate: 48000,
underruns,
});
}
let host = cpal::default_host();
let device = if let Some(name) = device_name {
use cpal::traits::HostTrait;
host.output_devices()
.map_err(|e| format!("enumerate devices: {}", e))?
.find(|d| {
use cpal::traits::DeviceTrait;
d.name().map_or(false, |n| n == name)
})
.unwrap_or_else(|| host.default_output_device().expect("No audio output device"))
} else {
host.default_output_device().ok_or("No audio output device")?
};
let supported = device.default_output_config().map_err(|e| format!("{}", e))?;
let channels = supported.channels() as usize;
let device_sample_rate = supported.sample_rate().0;
let mut config: cpal::StreamConfig = supported.into();
if let Some(bs) = cpal_buffer {
config.buffer_size = cpal::BufferSize::Fixed(bs);
eprintln!("[audio] Output device: {} channels, {} Hz, buffer {}", channels, device_sample_rate, bs);
} else {
eprintln!("[audio] Output device: {} channels, {} Hz", channels, device_sample_rate);
}
let (producer, mut consumer) = rtrb::RingBuffer::new(buffer_size);
let flush = Arc::new(AtomicBool::new(false));
let flush_cb = flush.clone();
let device_error = error_flag.unwrap_or_else(|| Arc::new(AtomicBool::new(false)));
let device_error_cb = device_error.clone();
let underruns = Arc::new(AtomicU64::new(0));
let underruns_cb = underruns.clone();
let stream = device.build_output_stream(
&config,
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
if flush_cb.swap(false, Ordering::Relaxed) {
while consumer.pop().is_ok() {}
for sample in data.iter_mut() {
*sample = 0.0;
}
return;
}
let mut had_underrun = false;
for sample in data.iter_mut() {
match consumer.pop() {
Ok(s) => *sample = s,
Err(_) => { *sample = 0.0; had_underrun = true; }
}
}
if had_underrun {
underruns_cb.fetch_add(1, Ordering::Relaxed);
}
},
move |err| {
eprintln!("[audio] Output error (device disconnect?): {}", err);
device_error_cb.store(true, Ordering::Relaxed);
},
None,
).map_err(|e| format!("Stream: {}", e))?;
stream.play().map_err(|e| format!("Play: {}", e))?;
Ok(PlaybackHandle {
stream_guard: Some(stream),
producer,
flush,
device_error,
channels,
sample_rate: device_sample_rate,
underruns,
})
}
pub fn start_headphone_output(
buffer_size: usize,
) -> Option<(cpal::Stream, rtrb::Producer<f32>, usize)> {
if std::env::var("DAW_NO_AUDIO").is_ok() {
eprintln!("[DAW_NO_AUDIO] Headphone output skipped");
return None;
}
let host = cpal::default_host();
let default = host.default_output_device();
let default_name = default.as_ref().and_then(|d| d.name().ok()).unwrap_or_default();
let devices = match host.output_devices() {
Ok(d) => d,
Err(_) => { eprintln!("[audio] Cannot enumerate output devices"); return None; }
};
let alt_device = devices
.filter(|d| {
let name = d.name().unwrap_or_default();
name != default_name
&& !name.to_lowercase().contains("nvidia")
&& !name.to_lowercase().contains("hdmi")
&& !name.to_lowercase().contains("displayport")
&& !name.to_lowercase().contains("ultragear")
})
.next();
let device = match alt_device {
Some(d) => {
eprintln!("[audio] Headphone output: {}", d.name().unwrap_or_default());
d
}
None => {
eprintln!("[audio] No second output device — headphone mix disabled");
return None;
}
};
let config = match device.default_output_config() {
Ok(c) => c,
Err(e) => { eprintln!("[audio] Headphone device config error: {}", e); return None; }
};
let channels = config.channels() as usize;
let (producer, mut consumer) = rtrb::RingBuffer::new(buffer_size);
let stream = match device.build_output_stream(
&config.into(),
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
for sample in data.iter_mut() {
*sample = consumer.pop().unwrap_or(0.0);
}
},
|err| eprintln!("[audio] Headphone output error: {}", err),
None,
) {
Ok(s) => s,
Err(e) => { eprintln!("[audio] Headphone stream error: {}", e); return None; }
};
if let Err(e) = stream.play() {
eprintln!("[audio] Headphone play error: {}", e);
return None;
}
Some((stream, producer, channels))
}
#[cfg(test)]
mod hard_gate_tests {
use super::*;
use std::sync::Once;
static INIT: Once = Once::new();
fn enter_hard_gate() {
INIT.call_once(|| {
std::env::set_var("DAW_NO_AUDIO", "1");
});
}
#[test]
fn handle_under_hard_gate_is_not_live() {
enter_hard_gate();
let h = start_playback_lockfree(1024).expect("guard mode must succeed");
assert!(!h.is_live(), "DAW_NO_AUDIO must produce non-live handle");
assert_eq!(h.sample_rate(), 48000);
assert_eq!(h.channels(), 2);
}
#[test]
fn producer_is_pushable_under_hard_gate() {
enter_hard_gate();
let mut h = start_playback_lockfree(1024).expect("guard mode must succeed");
let _ = h.producer_mut().push(0.5);
}
#[test]
fn flags_are_clonable_arcs() {
enter_hard_gate();
let h = start_playback_lockfree(1024).expect("guard mode must succeed");
let flush_a = h.flush_flag();
let flush_b = h.flush_flag();
flush_a.store(true, Ordering::Relaxed);
assert!(flush_b.load(Ordering::Relaxed), "flags must be Arc-clones, not copies");
}
#[test]
fn split_produces_send_bundle() {
enter_hard_gate();
let h = start_playback_lockfree(1024).expect("guard mode must succeed");
let (keeper, mut bundle) = h.split();
let jh = std::thread::spawn(move || {
assert_eq!(bundle.sample_rate, 48000);
assert_eq!(bundle.channels, 2);
let _ = bundle.producer.push(0.25);
bundle.sample_rate
});
let sr = jh.join().expect("feeder thread joined");
assert_eq!(sr, 48000);
drop(keeper);
}
#[test]
fn drop_under_hard_gate_does_not_panic() {
enter_hard_gate();
let h = start_playback_lockfree(1024).expect("guard mode must succeed");
drop(h);
}
#[test]
fn with_error_flag_passthrough_preserves_arc_identity() {
enter_hard_gate();
let shared = Arc::new(AtomicBool::new(false));
let h = start_playback_lockfree_with_error_flag(1024, Some(shared.clone()), None, None)
.expect("guard mode must succeed");
h.device_error_flag().store(true, Ordering::Relaxed);
assert!(shared.load(Ordering::Relaxed),
"with_error_flag must reuse the caller's Arc, not clone its contents");
}
}