use crate::error::TalkError;
struct PlaybackState {
samples: Vec<f32>,
position: usize,
paused: bool,
}
pub struct AudioPlayer {
state: std::sync::Arc<std::sync::Mutex<PlaybackState>>,
device_sample_rate: u32,
_stream: cpal::Stream,
}
impl AudioPlayer {
pub fn new() -> Result<Self, TalkError> {
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
let host = cpal::default_host();
let device = host
.default_output_device()
.ok_or_else(|| TalkError::Audio("no default audio output device".to_string()))?;
let config = device
.default_output_config()
.map_err(|e| TalkError::Audio(format!("output config: {}", e)))?;
let device_sample_rate = config.sample_rate().0;
let channels = config.channels() as usize;
let state = std::sync::Arc::new(std::sync::Mutex::new(PlaybackState {
samples: Vec::new(),
position: 0,
paused: false,
}));
let state_cb = std::sync::Arc::clone(&state);
let stream = device
.build_output_stream(
&cpal::StreamConfig {
channels: config.channels(),
sample_rate: config.sample_rate(),
buffer_size: cpal::BufferSize::Default,
},
move |output: &mut [f32], _: &cpal::OutputCallbackInfo| {
if let Ok(mut guard) = state_cb.try_lock() {
if guard.paused {
for s in output.iter_mut() {
*s = 0.0;
}
return;
}
let frames = output.len() / channels;
for frame_idx in 0..frames {
let sample = if guard.position < guard.samples.len() {
let s = guard.samples[guard.position];
guard.position += 1;
s
} else {
0.0
};
for ch in 0..channels {
output[frame_idx * channels + ch] = sample;
}
}
} else {
for s in output.iter_mut() {
*s = 0.0;
}
}
},
|err| log::error!("audio output error: {}", err),
None,
)
.map_err(|e| TalkError::Audio(format!("output stream: {}", e)))?;
stream
.play()
.map_err(|e| TalkError::Audio(format!("start output stream: {}", e)))?;
Ok(Self {
state,
device_sample_rate,
_stream: stream,
})
}
pub fn device_sample_rate(&self) -> u32 {
self.device_sample_rate
}
pub fn load_f32(&self, samples: Vec<f32>) {
if let Ok(mut guard) = self.state.lock() {
guard.samples = samples;
guard.position = 0;
guard.paused = false;
}
}
pub fn play_pcm_blocking(&self, pcm: &[i16], sample_rate: u32) -> Result<(), TalkError> {
if pcm.is_empty() {
return Ok(());
}
let mono: Vec<f32> = pcm.iter().map(|&s| s as f32 / 32768.0).collect();
let resampled = resample_linear_mono(&mono, sample_rate, self.device_sample_rate);
self.load_f32(resampled);
let poll = std::time::Duration::from_millis(20);
loop {
std::thread::sleep(poll);
let done = self
.state
.lock()
.map(|g| g.position >= g.samples.len() || g.samples.is_empty())
.unwrap_or(true);
if done {
break;
}
}
std::thread::sleep(std::time::Duration::from_millis(120));
self.stop();
Ok(())
}
pub fn stop(&self) {
if let Ok(mut guard) = self.state.lock() {
guard.samples.clear();
guard.position = 0;
guard.paused = false;
}
}
pub fn is_finished(&self) -> bool {
self.state
.lock()
.map(|g| g.samples.is_empty() || g.position >= g.samples.len())
.unwrap_or(true)
}
pub fn progress(&self) -> f64 {
self.state
.lock()
.map(|g| {
if g.samples.is_empty() {
0.0
} else {
g.position as f64 / g.samples.len() as f64
}
})
.unwrap_or(0.0)
}
pub fn seek(&self, fraction: f64) {
let fraction = fraction.clamp(0.0, 1.0);
if let Ok(mut guard) = self.state.lock() {
if !guard.samples.is_empty() {
guard.position = (fraction * guard.samples.len() as f64) as usize;
}
}
}
pub fn pause(&self) {
if let Ok(mut guard) = self.state.lock() {
guard.paused = true;
}
}
pub fn resume(&self) {
if let Ok(mut guard) = self.state.lock() {
guard.paused = false;
}
}
pub fn is_paused(&self) -> bool {
self.state.lock().map(|g| g.paused).unwrap_or(false)
}
pub fn has_audio(&self) -> bool {
self.state
.lock()
.map(|g| !g.samples.is_empty())
.unwrap_or(false)
}
pub fn duration_secs(&self) -> f64 {
self.state
.lock()
.map(|g| {
if g.samples.is_empty() {
0.0
} else {
g.samples.len() as f64 / self.device_sample_rate as f64
}
})
.unwrap_or(0.0)
}
}
fn resample_linear_mono(mono: &[f32], src_rate: u32, target_rate: u32) -> Vec<f32> {
if src_rate == 0 || src_rate == target_rate || mono.is_empty() {
return mono.to_vec();
}
let ratio = target_rate as f64 / src_rate as f64;
let new_len = (mono.len() as f64 * ratio).ceil() as usize;
let mut out = Vec::with_capacity(new_len);
for i in 0..new_len {
let src = i as f64 / ratio;
let idx = src.floor() as usize;
let frac = (src - idx as f64) as f32;
let s0 = mono.get(idx).copied().unwrap_or(0.0);
let s1 = mono.get(idx + 1).copied().unwrap_or(s0);
out.push(s0 + (s1 - s0) * frac);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resample_same_rate_is_identity() {
let samples = vec![0.0f32, 0.5, -0.5, 1.0];
let out = resample_linear_mono(&samples, 24_000, 24_000);
assert_eq!(out, samples);
}
#[test]
fn resample_empty_is_empty() {
assert!(resample_linear_mono(&[], 24_000, 48_000).is_empty());
}
#[test]
fn resample_upsample_doubles_length_approx() {
let samples = vec![0.0f32, 1.0, 0.0, -1.0];
let out = resample_linear_mono(&samples, 24_000, 48_000);
assert!(out.len() >= samples.len() * 2 - 1);
}
#[test]
fn resample_zero_src_rate_is_identity() {
let samples = vec![0.1f32, 0.2];
let out = resample_linear_mono(&samples, 0, 48_000);
assert_eq!(out, samples);
}
}