//! cpal audio output stream: reads from shared preview and instrument playback state. use std::sync::Arc; use audiofiles_browser::instrument::render_voices; use audiofiles_browser::preview::PreviewPlayback; use audiofiles_browser::state::SharedState; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use cpal::Stream; use parking_lot::Mutex; use tracing::instrument; use thiserror::Error; /// Errors from audio output stream setup. #[derive(Error, Debug)] pub enum AudioError { #[error("no output audio device found")] NoDevice, #[error("default output config: {0}")] DefaultConfig(#[from] cpal::DefaultStreamConfigError), #[error("unsupported sample format: {0:?}")] UnsupportedFormat(cpal::SampleFormat), #[error("build stream: {0}")] BuildStream(#[from] cpal::BuildStreamError), #[error("stream play: {0}")] Play(#[from] cpal::PlayStreamError), } /// Build and start a cpal output stream that reads from the shared preview state. /// Returns `(stream, device_sample_rate, device_name)` — the stream handle must /// be kept alive. `device_name` is whatever cpal reports for the default /// output device; it's surfaced in the footer for diagnostic visibility. #[instrument(skip_all)] pub fn start_output_stream(shared: Arc) -> Result<(Stream, u32, String), AudioError> { let host = cpal::default_host(); let device = host .default_output_device() .ok_or(AudioError::NoDevice)?; let device_name = device .description() .map(|d| d.name().to_string()) .unwrap_or_else(|_| "default".to_string()); let config = device.default_output_config()?; let channels = config.channels() as usize; let device_sample_rate = config.sample_rate(); let stream = match config.sample_format() { cpal::SampleFormat::F32 => build_stream::( &device, &config.into(), shared, channels, device_sample_rate, ), cpal::SampleFormat::I16 => build_stream::( &device, &config.into(), shared, channels, device_sample_rate, ), cpal::SampleFormat::U16 => build_stream::( &device, &config.into(), shared, channels, device_sample_rate, ), fmt => Err(AudioError::UnsupportedFormat(fmt)), }?; stream.play()?; Ok((stream, device_sample_rate, device_name)) } fn build_stream>( device: &cpal::Device, config: &cpal::StreamConfig, shared: Arc, channels: usize, device_sample_rate: u32, ) -> Result { let mut mix_buf: Vec = Vec::new(); let stream = device .build_output_stream( config, move |data: &mut [T], _: &cpal::OutputCallbackInfo| { let num_samples = data.len(); // Resize mix buffer if needed (no per-callback allocation after first call) if mix_buf.len() < num_samples { mix_buf.resize(num_samples, 0.0); } let buf = &mut mix_buf[..num_samples]; // Zero the mix buffer for s in buf.iter_mut() { *s = 0.0; } // Fill preview audio fill_preview(&shared.preview, buf, channels, device_sample_rate); // Fill instrument audio (additive) if let Some(mut inst) = shared.instrument.try_lock() { render_voices(&mut inst, buf, channels, device_sample_rate); } // Convert f32 mix → output format with clamp for (out, &mix) in data.iter_mut().zip(buf.iter()) { *out = T::from_sample(mix.clamp(-1.0, 1.0)); } }, |err| { tracing::error!("audio stream error: {err}"); }, None, )?; Ok(stream) } /// Fill an f32 buffer from the preview playback state. /// /// Uses fractional position advancement (`file_rate / device_rate` per output frame) /// with linear interpolation for correct-speed playback at any sample rate. pub(crate) fn fill_preview( playback: &Mutex, buf: &mut [f32], channels: usize, device_sample_rate: u32, ) { let Some(mut guard) = playback.try_lock() else { return; // GUI thread holds lock — leave buffer unchanged (already zeroed) }; if !guard.playing || device_sample_rate == 0 { return; } let Some(ref preview_buf) = guard.buffer else { return; }; // For streaming, use the smaller of decoded_frames and actual data length // to prevent OOB if decoded_frames is updated before data is fully appended. let total_frames = if guard.streaming { guard.decoded_frames.min(preview_buf.data.len() / 2) } else { preview_buf.data.len() / 2 }; let rate_ratio = preview_buf.sample_rate as f64 / device_sample_rate as f64; let loop_enabled = guard.loop_enabled; let mut pos_frac = guard.position_frac; let num_frames = buf.len() / channels; for frame in 0..num_frames { let pos_int = pos_frac as usize; if pos_int >= total_frames { if guard.streaming { // Still decoding — stop filling but keep playing guard.position_frac = pos_frac; return; } if loop_enabled && total_frames > 0 { pos_frac %= total_frames as f64; } else { guard.playing = false; guard.position_frac = 0.0; return; } } let pos_int = pos_frac as usize; let frac = (pos_frac - pos_int as f64) as f32; let l0 = preview_buf.data[pos_int * 2]; let r0 = preview_buf.data[pos_int * 2 + 1]; let next = (pos_int + 1).min(total_frames.saturating_sub(1)); let l1 = preview_buf.data[next * 2]; let r1 = preview_buf.data[next * 2 + 1]; let left = l0 + (l1 - l0) * frac; let right = r0 + (r1 - r0) * frac; let base = frame * channels; if channels >= 2 { buf[base] += left; buf[base + 1] += right; // Extra channels stay zero (already zeroed) } else if channels == 1 { buf[base] += (left + right) * 0.5; } pos_frac += rate_ratio; } guard.position_frac = pos_frac; } /// Fill a typed cpal output buffer from the preview playback state (test helper). #[cfg(test)] fn fill_cpal_output>( playback: &Mutex, data: &mut [T], channels: usize, device_sample_rate: u32, ) { let mut buf = vec![0.0f32; data.len()]; fill_preview(playback, &mut buf, channels, device_sample_rate); for (out, &mix) in data.iter_mut().zip(buf.iter()) { *out = T::from_sample(mix); } // For backward compat: if preview stopped, silence the rest. // fill_preview will have left the tail at 0.0 already, which converts to silence. } #[cfg(test)] mod tests { use super::*; use audiofiles_browser::preview::PreviewBuffer; fn make_playback(data: Vec, sample_rate: u32, playing: bool) -> Mutex { Mutex::new(PreviewPlayback { buffer: Some(PreviewBuffer { data, channels: 2, sample_rate, }), position_frac: 0.0, playing, loop_enabled: false, streaming: false, decoded_frames: 0, total_frames_estimate: None, }) } #[test] fn fill_cpal_stereo_f32() { let playback = make_playback(vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6], 44100, true); let mut data = vec![0.0f32; 6]; fill_cpal_output(&playback, &mut data, 2, 44100); assert_eq!(data, vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6]); } #[test] fn fill_cpal_mono_f32() { let playback = make_playback(vec![0.4, 0.6, 0.2, 0.8], 44100, true); let mut data = vec![0.0f32; 2]; fill_cpal_output(&playback, &mut data, 1, 44100); assert_eq!(data[0], (0.4 + 0.6) * 0.5); assert_eq!(data[1], (0.2 + 0.8) * 0.5); } #[test] fn fill_cpal_stops_at_end() { let playback = make_playback(vec![0.1, 0.2, 0.3, 0.4], 44100, true); let mut data = vec![9.0f32; 8]; fill_cpal_output(&playback, &mut data, 2, 44100); assert_eq!(data[0], 0.1); assert_eq!(data[1], 0.2); assert_eq!(data[2], 0.3); assert_eq!(data[3], 0.4); assert_eq!(data[4], 0.0); assert_eq!(data[5], 0.0); assert_eq!(data[6], 0.0); assert_eq!(data[7], 0.0); let guard = playback.lock(); assert!(!guard.playing); assert_eq!(guard.position_frac, 0.0); } #[test] fn fill_cpal_not_playing_outputs_silence() { let playback = make_playback(vec![0.5, 0.5], 44100, false); let mut data = vec![1.0f32; 4]; fill_cpal_output(&playback, &mut data, 2, 44100); assert!(data.iter().all(|&s| s == 0.0)); } #[test] fn fill_cpal_same_rate_unchanged() { let playback = make_playback(vec![0.1, 0.2, 0.3, 0.4], 48000, true); let mut data = vec![0.0f32; 4]; fill_cpal_output(&playback, &mut data, 2, 48000); assert_eq!(data, vec![0.1, 0.2, 0.3, 0.4]); let guard = playback.lock(); assert!((guard.position_frac - 2.0).abs() < 1e-10); } #[test] fn fill_cpal_resamples_96k_to_48k() { let playback = make_playback( vec![0.0, 0.0, 0.5, 0.5, 1.0, 1.0, 0.5, 0.5], 96000, true, ); let mut data = vec![0.0f32; 4]; fill_cpal_output(&playback, &mut data, 2, 48000); assert!((data[0] - 0.0).abs() < 1e-6); assert!((data[1] - 0.0).abs() < 1e-6); assert!((data[2] - 1.0).abs() < 1e-6); assert!((data[3] - 1.0).abs() < 1e-6); let guard = playback.lock(); assert!((guard.position_frac - 4.0).abs() < 1e-10); } #[test] fn fill_cpal_loop_wraps() { let playback = Mutex::new(PreviewPlayback { buffer: Some(PreviewBuffer { data: vec![0.1, 0.2, 0.3, 0.4], channels: 2, sample_rate: 44100, }), position_frac: 0.0, playing: true, loop_enabled: true, streaming: false, decoded_frames: 0, total_frames_estimate: None, }); let mut data = vec![0.0f32; 8]; fill_cpal_output(&playback, &mut data, 2, 44100); assert_eq!(data[0], 0.1); assert_eq!(data[1], 0.2); assert_eq!(data[2], 0.3); assert_eq!(data[3], 0.4); assert_eq!(data[4], 0.1); assert_eq!(data[5], 0.2); assert_eq!(data[6], 0.3); assert_eq!(data[7], 0.4); let guard = playback.lock(); assert!(guard.playing); } #[test] fn fill_cpal_loop_disabled_stops() { let playback = make_playback(vec![0.1, 0.2, 0.3, 0.4], 44100, true); let mut data = vec![9.0f32; 8]; fill_cpal_output(&playback, &mut data, 2, 44100); assert_eq!(data[0], 0.1); assert_eq!(data[4], 0.0); let guard = playback.lock(); assert!(!guard.playing); } #[test] fn audio_error_display() { let variants: Vec> = vec![ Box::new(AudioError::NoDevice), Box::new(AudioError::UnsupportedFormat(cpal::SampleFormat::U32)), ]; for err in &variants { assert!(!err.to_string().is_empty()); } } #[test] fn fill_preview_additive() { let playback = make_playback(vec![0.1, 0.2, 0.3, 0.4], 44100, true); let mut buf = vec![0.5f32; 4]; fill_preview(&playback, &mut buf, 2, 44100); // 0.5 + 0.1 = 0.6, etc. assert!((buf[0] - 0.6).abs() < 1e-6); assert!((buf[1] - 0.7).abs() < 1e-6); } #[test] fn clamp_prevents_overflow() { // Simulate very loud mixed audio let buf = [1.5f32, -1.5, 0.5, -0.5]; let mut data = [0.0f32; 4]; for (out, &mix) in data.iter_mut().zip(buf.iter()) { *out = mix.clamp(-1.0, 1.0); } assert_eq!(data[0], 1.0); assert_eq!(data[1], -1.0); assert_eq!(data[2], 0.5); assert_eq!(data[3], -0.5); } }