#![allow(dead_code)]
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use laser_dac::{ChunkRequest, LaserPoint};
use log::{debug, error, info};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use std::thread;
const RING_BUFFER_FRAMES: usize = 96000;
static AUDIO_STATE: OnceLock<Arc<AudioState>> = OnceLock::new();
static AUDIO_INITIALIZED: OnceLock<bool> = OnceLock::new();
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AudioMode {
Mono,
Stereo,
}
#[derive(Debug, Clone)]
pub struct AudioConfig {
pub intensity: u16,
pub color: (u16, u16, u16),
pub blank_threshold: f32,
pub extra_delay_ms: f32,
}
impl Default for AudioConfig {
fn default() -> Self {
Self {
intensity: 65535,
color: (0, 65535, 65535), blank_threshold: 0.01, extra_delay_ms: 10.0, }
}
}
struct AudioSnapshot {
sample_clock: u64,
write_pos: usize,
sample_rate: u32,
mode: AudioMode,
}
struct AudioState {
ring_left: Mutex<Vec<f32>>,
ring_right: Mutex<Vec<f32>>,
write_pos: AtomicU64,
sample_clock: AtomicU64,
sample_rate: u32,
channels: usize,
mode: AudioMode,
}
impl AudioState {
fn new(channels: usize, sample_rate: u32) -> Self {
let mode = if channels >= 2 {
AudioMode::Stereo
} else {
AudioMode::Mono
};
info!(
"Audio mode: {:?} ({} channels, {} Hz)",
mode, channels, sample_rate
);
Self {
ring_left: Mutex::new(vec![0.0; RING_BUFFER_FRAMES]),
ring_right: Mutex::new(vec![0.0; RING_BUFFER_FRAMES]),
write_pos: AtomicU64::new(0),
sample_clock: AtomicU64::new(0),
sample_rate,
channels,
mode,
}
}
fn update_samples(&self, new_samples: &[f32]) {
let frame_count = new_samples.len() / self.channels.max(1);
let mut left = self.ring_left.lock().unwrap();
let mut right = self.ring_right.lock().unwrap();
let mut write_pos = self.write_pos.load(Ordering::Relaxed) as usize;
for i in 0..frame_count {
let l = new_samples[i * self.channels];
let r = if self.channels >= 2 {
new_samples[i * self.channels + 1]
} else {
l
};
left[write_pos] = l;
right[write_pos] = r;
write_pos = (write_pos + 1) % RING_BUFFER_FRAMES;
}
self.write_pos.store(write_pos as u64, Ordering::Release);
self.sample_clock
.fetch_add(frame_count as u64, Ordering::Release);
}
fn snapshot(&self) -> AudioSnapshot {
AudioSnapshot {
sample_clock: self.sample_clock.load(Ordering::Acquire),
write_pos: self.write_pos.load(Ordering::Acquire) as usize,
sample_rate: self.sample_rate,
mode: self.mode,
}
}
fn read_samples(
&self,
start_frame: u64,
count: usize,
points_to_frames: f64,
) -> Vec<(f32, f32)> {
if count == 0 {
return Vec::new();
}
let snap = self.snapshot();
let end_frame_f = start_frame as f64 + (count - 1) as f64 * points_to_frames + 1.0;
let frames_needed = (end_frame_f.ceil() as u64).saturating_sub(start_frame) + 1;
let frames_needed = (frames_needed as usize).min(RING_BUFFER_FRAMES);
let (left_copy, right_copy) = {
let left = self.ring_left.lock().unwrap();
let right = self.ring_right.lock().unwrap();
let mut left_copy = Vec::with_capacity(frames_needed);
let mut right_copy = Vec::with_capacity(frames_needed);
for offset in 0..frames_needed {
let frame_idx = start_frame + offset as u64;
let (l, r) = Self::read_frame_from_slice(&left, &right, frame_idx, &snap);
left_copy.push(l);
right_copy.push(r);
}
(left_copy, right_copy)
};
let mut samples = Vec::with_capacity(count);
for i in 0..count {
let frame_f = i as f64 * points_to_frames;
let idx = frame_f.floor() as usize;
let frac = (frame_f - frame_f.floor()) as f32;
let idx0 = idx.min(left_copy.len().saturating_sub(1));
let idx1 = (idx + 1).min(left_copy.len().saturating_sub(1));
let l = left_copy[idx0] + (left_copy[idx1] - left_copy[idx0]) * frac;
let r = right_copy[idx0] + (right_copy[idx1] - right_copy[idx0]) * frac;
samples.push((l, r));
}
samples
}
fn read_frame_from_slice(
left: &[f32],
right: &[f32],
frame_idx: u64,
snap: &AudioSnapshot,
) -> (f32, f32) {
if frame_idx >= snap.sample_clock {
let idx = if snap.write_pos == 0 {
RING_BUFFER_FRAMES - 1
} else {
snap.write_pos - 1
};
return (left[idx], right[idx]);
}
let frames_back = snap.sample_clock - frame_idx;
if frames_back > RING_BUFFER_FRAMES as u64 {
return (left[snap.write_pos], right[snap.write_pos]);
}
let idx = (snap.write_pos + RING_BUFFER_FRAMES
- (frames_back as usize % RING_BUFFER_FRAMES))
% RING_BUFFER_FRAMES;
(left[idx], right[idx])
}
}
pub fn ensure_audio_initialized() -> bool {
if let Some(&initialized) = AUDIO_INITIALIZED.get() {
return initialized;
}
info!("Initializing audio capture...");
let host = cpal::default_host();
info!("Using audio host: {:?}", host.id());
let input_device = match host.default_input_device() {
Some(d) => d,
None => {
error!("No audio input device available");
let _ = AUDIO_INITIALIZED.set(false);
return false;
}
};
info!("Using input device: {:?}", input_device.name());
let config = match input_device.default_input_config() {
Ok(c) => c,
Err(e) => {
error!("Failed to get default input config: {}", e);
let _ = AUDIO_INITIALIZED.set(false);
return false;
}
};
info!(
"Audio config: {} channels, {} Hz, {:?}",
config.channels(),
config.sample_rate().0,
config.sample_format()
);
let channels = config.channels() as usize;
let sample_rate = config.sample_rate().0;
let audio_state = Arc::new(AudioState::new(channels, sample_rate));
let _ = AUDIO_STATE.set(Arc::clone(&audio_state));
let sample_format = config.sample_format();
let stream_config: cpal::StreamConfig = config.into();
thread::spawn(move || {
let err_fn = |err| error!("Audio stream error: {}", err);
let stream = match sample_format {
cpal::SampleFormat::F32 => {
let state = Arc::clone(&audio_state);
input_device.build_input_stream(
&stream_config,
move |data: &[f32], _: &cpal::InputCallbackInfo| {
state.update_samples(data);
},
err_fn,
None,
)
}
cpal::SampleFormat::I16 => {
let state = Arc::clone(&audio_state);
input_device.build_input_stream(
&stream_config,
move |data: &[i16], _: &cpal::InputCallbackInfo| {
let float_data: Vec<f32> =
data.iter().map(|&s| s as f32 / i16::MAX as f32).collect();
state.update_samples(&float_data);
},
err_fn,
None,
)
}
cpal::SampleFormat::U16 => {
let state = Arc::clone(&audio_state);
input_device.build_input_stream(
&stream_config,
move |data: &[u16], _: &cpal::InputCallbackInfo| {
let float_data: Vec<f32> = data
.iter()
.map(|&s| (s as f32 / u16::MAX as f32) * 2.0 - 1.0)
.collect();
state.update_samples(&float_data);
},
err_fn,
None,
)
}
format => {
error!("Unsupported sample format: {:?}", format);
return;
}
};
match stream {
Ok(s) => {
if let Err(e) = s.play() {
error!("Failed to start audio stream: {}", e);
return;
}
info!("Audio stream started");
loop {
thread::park();
}
}
Err(e) => {
error!("Failed to build audio stream: {}", e);
}
}
});
thread::sleep(std::time::Duration::from_millis(100));
let _ = AUDIO_INITIALIZED.set(true);
true
}
#[allow(dead_code)]
pub fn get_audio_sample_rate() -> Option<u32> {
Some(AUDIO_STATE.get()?.sample_rate)
}
#[allow(dead_code)]
pub fn get_audio_mode() -> Option<AudioMode> {
Some(AUDIO_STATE.get()?.mode)
}
pub fn fill_audio_points(
req: &ChunkRequest,
buffer: &mut [LaserPoint],
n_points: usize,
config: &AudioConfig,
) {
if n_points == 0 {
return;
}
if !ensure_audio_initialized() {
for item in buffer.iter_mut().take(n_points) {
*item = LaserPoint::blanked(0.0, 0.0);
}
return;
}
let state = match AUDIO_STATE.get() {
Some(s) => s,
None => {
for item in buffer.iter_mut().take(n_points) {
*item = LaserPoint::blanked(0.0, 0.0);
}
return;
}
};
let snap = state.snapshot();
let pps = req.pps;
let points_to_frames = snap.sample_rate as f64 / pps as f64;
let extra_delay_frames = (config.extra_delay_ms / 1000.0 * snap.sample_rate as f32) as u64;
let chunk_frames = (n_points as f64 * points_to_frames) as u64;
let audio_start_frame = snap
.sample_clock
.saturating_sub(extra_delay_frames)
.saturating_sub(chunk_frames);
let samples = state.read_samples(audio_start_frame, n_points, points_to_frames);
if config.blank_threshold > 0.0 {
let check_count = (n_points / 10).max(10).min(n_points);
let max_amp = samples
.iter()
.step_by(n_points / check_count)
.map(|(l, r)| l.abs().max(r.abs()))
.fold(0.0f32, f32::max);
if max_amp < config.blank_threshold {
debug!("Audio: silence detected (max_amp={:.4}), blanking", max_amp);
for item in buffer.iter_mut().take(n_points) {
*item = LaserPoint::blanked(0.0, 0.0);
}
return;
}
}
let (cr, cg, cb) = config.color;
match snap.mode {
AudioMode::Stereo => {
for (i, (l, r)) in samples.iter().enumerate().take(n_points) {
buffer[i] = LaserPoint::new(
l.clamp(-1.0, 1.0),
r.clamp(-1.0, 1.0),
cr,
cg,
cb,
config.intensity,
);
}
}
AudioMode::Mono => {
for (i, (l, _r)) in samples.iter().enumerate().take(n_points) {
let t = i as f32 / (n_points - 1).max(1) as f32;
let x = t * 2.0 - 1.0;
buffer[i] = LaserPoint::new(x, l.clamp(-1.0, 1.0), cr, cg, cb, config.intensity);
}
}
}
debug!(
"Audio: {} points, mode={:?}, audio_frame={}, delay={}ms",
n_points, snap.mode, audio_start_frame, config.extra_delay_ms
);
}