1use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
4use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
5use std::sync::Arc;
6
7pub struct PlaybackHandle {
46 stream_guard: Option<cpal::Stream>,
49 producer: rtrb::Producer<f32>,
50 flush: Arc<AtomicBool>,
51 device_error: Arc<AtomicBool>,
52 channels: usize,
53 sample_rate: u32,
54 underruns: Arc<AtomicU64>,
55}
56
57impl PlaybackHandle {
58 pub fn producer_mut(&mut self) -> &mut rtrb::Producer<f32> {
62 &mut self.producer
63 }
64
65 pub fn channels(&self) -> usize { self.channels }
66 pub fn sample_rate(&self) -> u32 { self.sample_rate }
67 pub fn flush_flag(&self) -> Arc<AtomicBool> { self.flush.clone() }
68 pub fn device_error_flag(&self) -> Arc<AtomicBool> { self.device_error.clone() }
69 pub fn underrun_counter(&self) -> Arc<AtomicU64> { self.underruns.clone() }
70
71 pub fn is_live(&self) -> bool { self.stream_guard.is_some() }
74
75 pub fn split(self) -> (StreamKeeper, FeederBundle) {
83 let mut me = std::mem::ManuallyDrop::new(self);
86 let stream = me.stream_guard.take();
87 let producer = unsafe { std::ptr::read(&me.producer) };
90 let flush = unsafe { std::ptr::read(&me.flush) };
91 let device_error = unsafe { std::ptr::read(&me.device_error) };
92 let underruns = unsafe { std::ptr::read(&me.underruns) };
93 let channels = me.channels;
94 let sample_rate = me.sample_rate;
95
96 let keeper = StreamKeeper { stream_guard: stream };
97 let bundle = FeederBundle {
98 producer,
99 flush,
100 device_error,
101 channels,
102 sample_rate,
103 underruns,
104 };
105 (keeper, bundle)
106 }
107}
108
109impl Drop for PlaybackHandle {
110 fn drop(&mut self) {
111 if self.stream_guard.is_some() {
112 eprintln!("[forge-audio] PlaybackHandle dropped — audio device closed");
113 }
114 }
115}
116
117pub struct StreamKeeper {
123 stream_guard: Option<cpal::Stream>,
124}
125
126impl Drop for StreamKeeper {
127 fn drop(&mut self) {
128 if self.stream_guard.is_some() {
129 eprintln!("[forge-audio] StreamKeeper dropped — audio device closed");
130 }
131 }
132}
133
134pub struct FeederBundle {
137 pub producer: rtrb::Producer<f32>,
138 pub flush: Arc<AtomicBool>,
139 pub device_error: Arc<AtomicBool>,
140 pub channels: usize,
141 pub sample_rate: u32,
142 pub underruns: Arc<AtomicU64>,
143}
144
145#[derive(Debug, Clone, serde::Serialize)]
147pub struct AudioDeviceInfo {
148 pub name: String,
149 pub api: String,
150 pub sample_rates: Vec<u32>,
151 pub channels: u16,
152}
153
154pub fn list_output_devices() -> Vec<AudioDeviceInfo> {
156 let host = cpal::default_host();
157 let host_name = format!("{:?}", host.id());
158 let devices = match host.output_devices() {
159 Ok(d) => d,
160 Err(e) => {
161 eprintln!("[audio] Cannot enumerate output devices: {}", e);
162 return vec![];
163 }
164 };
165 devices
166 .filter_map(|dev| {
167 let name = dev.name().unwrap_or_else(|_| "Unknown".into());
168 let mut sample_rates = Vec::new();
169 let mut channels = 2u16;
170 if let Ok(configs) = dev.supported_output_configs() {
171 for cfg in configs {
172 channels = channels.max(cfg.channels());
173 let min = cfg.min_sample_rate().0;
174 let max = cfg.max_sample_rate().0;
175 for &sr in &[44100u32, 48000, 88200, 96000, 192000] {
176 if sr >= min && sr <= max && !sample_rates.contains(&sr) {
177 sample_rates.push(sr);
178 }
179 }
180 }
181 }
182 sample_rates.sort();
183 if sample_rates.is_empty() {
184 sample_rates.push(48000);
185 }
186 Some(AudioDeviceInfo {
187 name,
188 api: host_name.clone(),
189 sample_rates,
190 channels,
191 })
192 })
193 .collect()
194}
195
196pub fn start_playback_lockfree(
206 buffer_size: usize,
207) -> Result<PlaybackHandle, String> {
208 start_playback_lockfree_with_error_flag(buffer_size, None, None, None)
209}
210
211pub fn null_playback(buffer_size: usize, sample_rate: u32) -> PlaybackHandle {
214 let (producer, consumer) = rtrb::RingBuffer::<f32>::new(buffer_size);
215 std::mem::forget(consumer);
216 PlaybackHandle {
217 stream_guard: None,
218 producer,
219 flush: Arc::new(AtomicBool::new(false)),
220 device_error: Arc::new(AtomicBool::new(true)),
221 channels: 2,
222 sample_rate,
223 underruns: Arc::new(AtomicU64::new(0)),
224 }
225}
226
227pub fn start_playback_lockfree_with_error_flag(
236 buffer_size: usize,
237 error_flag: Option<Arc<AtomicBool>>,
238 device_name: Option<&str>,
239 cpal_buffer: Option<u32>,
240) -> Result<PlaybackHandle, String> {
241 if std::env::var("DAW_NO_AUDIO").is_ok() {
242 eprintln!("[DAW_NO_AUDIO] *** AUDIO OUTPUT DISABLED *** No cpal device opened.");
243 eprintln!("[DAW_NO_AUDIO] Mixer math + IPC still run; nothing reaches phones/S2/speakers.");
244 let (producer, consumer) = rtrb::RingBuffer::<f32>::new(buffer_size);
245 std::mem::forget(consumer); let flush = Arc::new(AtomicBool::new(false));
247 let device_error = error_flag.unwrap_or_else(|| Arc::new(AtomicBool::new(false)));
248 let underruns = Arc::new(AtomicU64::new(0));
249 return Ok(PlaybackHandle {
250 stream_guard: None,
251 producer,
252 flush,
253 device_error,
254 channels: 2,
255 sample_rate: 48000,
256 underruns,
257 });
258 }
259 let host = cpal::default_host();
260 let device = if let Some(name) = device_name {
261 use cpal::traits::HostTrait;
262 host.output_devices()
263 .map_err(|e| format!("enumerate devices: {}", e))?
264 .find(|d| {
265 use cpal::traits::DeviceTrait;
266 d.name().map_or(false, |n| n == name)
267 })
268 .unwrap_or_else(|| host.default_output_device().expect("No audio output device"))
269 } else {
270 host.default_output_device().ok_or("No audio output device")?
271 };
272 let supported = device.default_output_config().map_err(|e| format!("{}", e))?;
273 let channels = supported.channels() as usize;
274 let device_sample_rate = supported.sample_rate().0;
275 let mut config: cpal::StreamConfig = supported.into();
276 if let Some(bs) = cpal_buffer {
277 config.buffer_size = cpal::BufferSize::Fixed(bs);
278 eprintln!("[audio] Output device: {} channels, {} Hz, buffer {}", channels, device_sample_rate, bs);
279 } else {
280 eprintln!("[audio] Output device: {} channels, {} Hz", channels, device_sample_rate);
281 }
282
283 let (producer, mut consumer) = rtrb::RingBuffer::new(buffer_size);
284 let flush = Arc::new(AtomicBool::new(false));
285 let flush_cb = flush.clone();
286 let device_error = error_flag.unwrap_or_else(|| Arc::new(AtomicBool::new(false)));
287 let device_error_cb = device_error.clone();
288 let underruns = Arc::new(AtomicU64::new(0));
289 let underruns_cb = underruns.clone();
290
291 let stream = device.build_output_stream(
292 &config,
293 move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
294 if flush_cb.swap(false, Ordering::Relaxed) {
296 while consumer.pop().is_ok() {}
297 for sample in data.iter_mut() {
298 *sample = 0.0;
299 }
300 return;
301 }
302 let mut had_underrun = false;
303 for sample in data.iter_mut() {
304 match consumer.pop() {
305 Ok(s) => *sample = s,
306 Err(_) => { *sample = 0.0; had_underrun = true; }
307 }
308 }
309 if had_underrun {
310 underruns_cb.fetch_add(1, Ordering::Relaxed);
311 }
312 },
313 move |err| {
314 eprintln!("[audio] Output error (device disconnect?): {}", err);
315 device_error_cb.store(true, Ordering::Relaxed);
316 },
317 None,
318 ).map_err(|e| format!("Stream: {}", e))?;
319
320 stream.play().map_err(|e| format!("Play: {}", e))?;
321 Ok(PlaybackHandle {
322 stream_guard: Some(stream),
323 producer,
324 flush,
325 device_error,
326 channels,
327 sample_rate: device_sample_rate,
328 underruns,
329 })
330}
331
332pub fn start_headphone_output(
336 buffer_size: usize,
337) -> Option<(cpal::Stream, rtrb::Producer<f32>, usize)> {
338 if std::env::var("DAW_NO_AUDIO").is_ok() {
339 eprintln!("[DAW_NO_AUDIO] Headphone output skipped");
340 return None;
341 }
342 let host = cpal::default_host();
343 let default = host.default_output_device();
344 let default_name = default.as_ref().and_then(|d| d.name().ok()).unwrap_or_default();
345
346 let devices = match host.output_devices() {
348 Ok(d) => d,
349 Err(_) => { eprintln!("[audio] Cannot enumerate output devices"); return None; }
350 };
351
352 let alt_device = devices
353 .filter(|d| {
354 let name = d.name().unwrap_or_default();
355 name != default_name
356 && !name.to_lowercase().contains("nvidia")
357 && !name.to_lowercase().contains("hdmi")
358 && !name.to_lowercase().contains("displayport")
359 && !name.to_lowercase().contains("ultragear")
360 })
361 .next();
362
363 let device = match alt_device {
364 Some(d) => {
365 eprintln!("[audio] Headphone output: {}", d.name().unwrap_or_default());
366 d
367 }
368 None => {
369 eprintln!("[audio] No second output device — headphone mix disabled");
370 return None;
371 }
372 };
373
374 let config = match device.default_output_config() {
375 Ok(c) => c,
376 Err(e) => { eprintln!("[audio] Headphone device config error: {}", e); return None; }
377 };
378 let channels = config.channels() as usize;
379 let (producer, mut consumer) = rtrb::RingBuffer::new(buffer_size);
380
381 let stream = match device.build_output_stream(
382 &config.into(),
383 move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
384 for sample in data.iter_mut() {
385 *sample = consumer.pop().unwrap_or(0.0);
386 }
387 },
388 |err| eprintln!("[audio] Headphone output error: {}", err),
389 None,
390 ) {
391 Ok(s) => s,
392 Err(e) => { eprintln!("[audio] Headphone stream error: {}", e); return None; }
393 };
394
395 if let Err(e) = stream.play() {
396 eprintln!("[audio] Headphone play error: {}", e);
397 return None;
398 }
399
400 Some((stream, producer, channels))
401}
402
403#[cfg(test)]
413mod hard_gate_tests {
414 use super::*;
415 use std::sync::Once;
416
417 static INIT: Once = Once::new();
418
419 fn enter_hard_gate() {
420 INIT.call_once(|| {
423 std::env::set_var("DAW_NO_AUDIO", "1");
424 });
425 }
426
427 #[test]
428 fn handle_under_hard_gate_is_not_live() {
429 enter_hard_gate();
430 let h = start_playback_lockfree(1024).expect("guard mode must succeed");
431 assert!(!h.is_live(), "DAW_NO_AUDIO must produce non-live handle");
432 assert_eq!(h.sample_rate(), 48000);
433 assert_eq!(h.channels(), 2);
434 }
435
436 #[test]
437 fn producer_is_pushable_under_hard_gate() {
438 enter_hard_gate();
439 let mut h = start_playback_lockfree(1024).expect("guard mode must succeed");
440 let _ = h.producer_mut().push(0.5);
444 }
445
446 #[test]
447 fn flags_are_clonable_arcs() {
448 enter_hard_gate();
449 let h = start_playback_lockfree(1024).expect("guard mode must succeed");
450 let flush_a = h.flush_flag();
451 let flush_b = h.flush_flag();
452 flush_a.store(true, Ordering::Relaxed);
453 assert!(flush_b.load(Ordering::Relaxed), "flags must be Arc-clones, not copies");
454 }
455
456 #[test]
457 fn split_produces_send_bundle() {
458 enter_hard_gate();
459 let h = start_playback_lockfree(1024).expect("guard mode must succeed");
460 let (keeper, mut bundle) = h.split();
461 let jh = std::thread::spawn(move || {
463 assert_eq!(bundle.sample_rate, 48000);
464 assert_eq!(bundle.channels, 2);
465 let _ = bundle.producer.push(0.25);
466 bundle.sample_rate
467 });
468 let sr = jh.join().expect("feeder thread joined");
469 assert_eq!(sr, 48000);
470 drop(keeper);
474 }
475
476 #[test]
477 fn drop_under_hard_gate_does_not_panic() {
478 enter_hard_gate();
479 let h = start_playback_lockfree(1024).expect("guard mode must succeed");
480 drop(h);
483 }
484
485 #[test]
486 fn with_error_flag_passthrough_preserves_arc_identity() {
487 enter_hard_gate();
488 let shared = Arc::new(AtomicBool::new(false));
489 let h = start_playback_lockfree_with_error_flag(1024, Some(shared.clone()), None, None)
490 .expect("guard mode must succeed");
491 h.device_error_flag().store(true, Ordering::Relaxed);
494 assert!(shared.load(Ordering::Relaxed),
495 "with_error_flag must reuse the caller's Arc, not clone its contents");
496 }
497}