1use cpal::{
4 traits::{DeviceTrait, HostTrait, StreamTrait},
5 ChannelCount, Device, Host, SampleFormat, SampleRate, Stream, StreamConfig, StreamError,
6};
7use hound::{WavReader, WavSpec};
8use std::collections::VecDeque;
9use std::path::Path;
10use std::sync::{Arc, Mutex};
11use std::time::Duration;
12use voirs_sdk::{Result, VoirsError};
13
14#[derive(Debug, Clone)]
16pub struct AudioData {
17 pub samples: Vec<i16>,
19 pub sample_rate: u32,
21 pub channels: u16,
23}
24
25impl AudioData {
26 pub fn duration(&self) -> f32 {
28 self.samples.len() as f32 / (self.sample_rate as f32 * self.channels as f32)
29 }
30
31 pub fn to_f32_samples(&self) -> Vec<f32> {
33 self.samples
34 .iter()
35 .map(|&s| s as f32 / i16::MAX as f32)
36 .collect()
37 }
38}
39
40#[derive(Debug, Clone)]
42pub struct PlaybackConfig {
43 pub sample_rate: u32,
45 pub channels: u16,
47 pub buffer_size: u32,
49 pub device_name: Option<String>,
51 pub volume: f32,
53}
54
55impl Default for PlaybackConfig {
56 fn default() -> Self {
57 Self {
58 sample_rate: 22050,
59 channels: 1,
60 buffer_size: 1024,
61 device_name: None,
62 volume: 1.0,
63 }
64 }
65}
66
67#[derive(Debug, Clone)]
69pub struct AudioDevice {
70 pub name: String,
71 pub is_default: bool,
72 pub max_output_channels: u16,
73 pub sample_rates: Vec<u32>,
74 pub supported_formats: Vec<SampleFormat>,
75}
76
77impl AudioDevice {
78 pub fn supports_config(&self, config: &PlaybackConfig) -> bool {
80 self.max_output_channels >= config.channels
81 && self.sample_rates.contains(&config.sample_rate)
82 }
83}
84
85#[derive(Debug, Clone)]
87pub struct QueueItem {
88 pub id: String,
89 pub audio_data: AudioData,
90 pub metadata: std::collections::HashMap<String, String>,
91}
92
93#[derive(Debug)]
95pub struct PlaybackQueue {
96 items: Arc<Mutex<VecDeque<QueueItem>>>,
97 current_playing: Arc<Mutex<Option<String>>>,
98}
99
100impl PlaybackQueue {
101 pub fn new() -> Self {
103 Self {
104 items: Arc::new(Mutex::new(VecDeque::new())),
105 current_playing: Arc::new(Mutex::new(None)),
106 }
107 }
108
109 pub fn enqueue(&self, item: QueueItem) -> Result<()> {
111 let mut items = self
112 .items
113 .lock()
114 .map_err(|_| VoirsError::device_error("audio_queue", "Failed to lock queue mutex"))?;
115 items.push_back(item);
116 Ok(())
117 }
118
119 pub fn dequeue(&self) -> Result<Option<QueueItem>> {
121 let mut items = self
122 .items
123 .lock()
124 .map_err(|_| VoirsError::device_error("audio_queue", "Failed to lock queue mutex"))?;
125 Ok(items.pop_front())
126 }
127
128 pub fn len(&self) -> Result<usize> {
130 let items = self
131 .items
132 .lock()
133 .map_err(|_| VoirsError::device_error("audio_queue", "Failed to lock queue mutex"))?;
134 Ok(items.len())
135 }
136
137 pub fn is_empty(&self) -> Result<bool> {
139 Ok(self.len()? == 0)
140 }
141
142 pub fn clear(&self) -> Result<()> {
144 let mut items = self
145 .items
146 .lock()
147 .map_err(|_| VoirsError::device_error("audio_queue", "Failed to lock queue mutex"))?;
148 items.clear();
149 Ok(())
150 }
151
152 pub fn set_current_playing(&self, id: Option<String>) -> Result<()> {
154 let mut current = self.current_playing.lock().map_err(|_| {
155 VoirsError::device_error("audio_queue", "Failed to lock current_playing mutex")
156 })?;
157 *current = id;
158 Ok(())
159 }
160
161 pub fn get_current_playing(&self) -> Result<Option<String>> {
163 let current = self.current_playing.lock().map_err(|_| {
164 VoirsError::device_error("audio_queue", "Failed to lock current_playing mutex")
165 })?;
166 Ok(current.clone())
167 }
168}
169
170impl Default for PlaybackQueue {
171 fn default() -> Self {
172 Self::new()
173 }
174}
175
176pub struct AudioPlayer {
178 config: PlaybackConfig,
179 device: Device,
180 host: Host,
181 stream: Option<Stream>,
182 queue: PlaybackQueue,
183 is_playing: Arc<Mutex<bool>>,
184}
185
186impl AudioPlayer {
187 pub fn new(config: PlaybackConfig) -> Result<Self> {
189 let host = cpal::default_host();
190 let device = if let Some(device_name) = &config.device_name {
191 Self::find_device_by_name(&host, device_name)?.ok_or_else(|| {
192 VoirsError::device_error(
193 "audio_device",
194 format!("Audio device '{}' not found", device_name),
195 )
196 })?
197 } else {
198 host.default_output_device().ok_or_else(|| {
199 VoirsError::device_error("audio_device", "No default audio output device found")
200 })?
201 };
202
203 Ok(Self {
204 config,
205 device,
206 host,
207 stream: None,
208 queue: PlaybackQueue::new(),
209 is_playing: Arc::new(Mutex::new(false)),
210 })
211 }
212
213 pub fn get_output_devices() -> Result<Vec<AudioDevice>> {
215 let host = cpal::default_host();
216 let mut devices = Vec::new();
217
218 let default_device = host.default_output_device();
219 let default_device_name = default_device.as_ref().and_then(|d| d.name().ok());
220
221 for device in host.output_devices().map_err(|e| {
222 VoirsError::device_error(
223 "audio_device",
224 format!("Failed to enumerate devices: {}", e),
225 )
226 })? {
227 if let Ok(name) = device.name() {
228 let is_default = default_device_name
229 .as_ref()
230 .map(|default| default == &name)
231 .unwrap_or(false);
232
233 let supported_configs = device.supported_output_configs().map_err(|e| {
235 VoirsError::device_error(
236 "audio_device",
237 format!("Failed to get device configs: {}", e),
238 )
239 })?;
240
241 let mut sample_rates = Vec::new();
242 let mut supported_formats = Vec::new();
243 let mut max_channels = 0;
244
245 for config in supported_configs {
246 max_channels = max_channels.max(config.channels());
247 sample_rates.push(config.min_sample_rate().0);
248 sample_rates.push(config.max_sample_rate().0);
249 supported_formats.push(config.sample_format());
250 }
251
252 sample_rates.sort_unstable();
254 sample_rates.dedup();
255 supported_formats.sort_by(|a, b| format!("{:?}", a).cmp(&format!("{:?}", b)));
256 supported_formats.dedup();
257
258 devices.push(AudioDevice {
259 name,
260 is_default,
261 max_output_channels: max_channels,
262 sample_rates,
263 supported_formats,
264 });
265 }
266 }
267
268 Ok(devices)
269 }
270
271 fn find_device_by_name(host: &Host, device_name: &str) -> Result<Option<Device>> {
273 for device in host.output_devices().map_err(|e| {
274 VoirsError::device_error(
275 "audio_device",
276 format!("Failed to enumerate devices: {}", e),
277 )
278 })? {
279 if let Ok(name) = device.name() {
280 if name == device_name {
281 return Ok(Some(device));
282 }
283 }
284 }
285 Ok(None)
286 }
287
288 pub async fn play(&mut self, audio_data: &AudioData) -> Result<()> {
290 let item = QueueItem {
291 id: format!("direct_{}", chrono::Utc::now().timestamp_millis()),
292 audio_data: audio_data.clone(),
293 metadata: std::collections::HashMap::new(),
294 };
295
296 self.queue.enqueue(item)?;
297 self.start_playback().await
298 }
299
300 pub async fn play_file<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
302 let audio_data = self.load_audio_file(path.as_ref())?;
303 self.play(&audio_data).await
304 }
305
306 pub fn enqueue(&self, audio_data: AudioData, id: Option<String>) -> Result<()> {
308 let item = QueueItem {
309 id: id.unwrap_or_else(|| format!("queued_{}", chrono::Utc::now().timestamp_millis())),
310 audio_data,
311 metadata: std::collections::HashMap::new(),
312 };
313
314 self.queue.enqueue(item)
315 }
316
317 pub async fn start_playback(&mut self) -> Result<()> {
319 if self.is_playing()? {
320 return Ok(());
321 }
322
323 self.set_playing(true)?;
324
325 let stream_config = StreamConfig {
326 channels: self.config.channels as ChannelCount,
327 sample_rate: SampleRate(self.config.sample_rate),
328 buffer_size: cpal::BufferSize::Fixed(self.config.buffer_size),
329 };
330
331 let queue = self.queue.items.clone();
332 let volume = self.config.volume;
333 let is_playing = self.is_playing.clone();
334 let current_playing = self.queue.current_playing.clone();
335
336 let mut current_audio: Option<Vec<f32>> = None;
337 let mut audio_position = 0;
338
339 let stream = self
340 .device
341 .build_output_stream(
342 &stream_config,
343 move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
344 for sample in data.iter_mut() {
346 *sample = 0.0;
347 }
348
349 if current_audio.is_none()
351 || audio_position >= current_audio.as_ref().unwrap().len()
352 {
353 if let Ok(mut queue_guard) = queue.lock() {
355 if let Some(item) = queue_guard.pop_front() {
356 if let Ok(mut current_guard) = current_playing.lock() {
358 *current_guard = Some(item.id.clone());
359 }
360
361 current_audio = Some(
363 item.audio_data
364 .samples
365 .iter()
366 .map(|&s| s as f32 / i16::MAX as f32)
367 .collect(),
368 );
369 audio_position = 0;
370 } else {
371 if let Ok(mut playing_guard) = is_playing.lock() {
373 *playing_guard = false;
374 }
375 if let Ok(mut current_guard) = current_playing.lock() {
376 *current_guard = None;
377 }
378 return;
379 }
380 }
381 }
382
383 if let Some(ref audio) = current_audio {
385 let samples_to_copy = (data.len()).min(audio.len() - audio_position);
386
387 for i in 0..samples_to_copy {
388 data[i] = audio[audio_position + i] * volume;
389 }
390
391 audio_position += samples_to_copy;
392 }
393 },
394 move |err| {
395 tracing::error!("Audio stream error: {}", err);
396 },
397 None, )
399 .map_err(|e| {
400 VoirsError::device_error(
401 "audio_device",
402 format!("Failed to build output stream: {}", e),
403 )
404 })?;
405
406 stream.play().map_err(|e| {
407 VoirsError::device_error("audio_device", format!("Failed to start stream: {}", e))
408 })?;
409
410 self.stream = Some(stream);
411 Ok(())
412 }
413
414 pub fn stop(&mut self) -> Result<()> {
416 self.set_playing(false)?;
417 self.queue.set_current_playing(None)?;
418
419 if let Some(stream) = self.stream.take() {
420 stream.pause().map_err(|e| {
421 VoirsError::device_error("audio_device", format!("Failed to stop stream: {}", e))
422 })?;
423 }
424
425 Ok(())
426 }
427
428 pub fn pause(&self) -> Result<()> {
430 if let Some(stream) = &self.stream {
431 stream.pause().map_err(|e| {
432 VoirsError::device_error("audio_device", format!("Failed to pause stream: {}", e))
433 })?;
434 }
435 self.set_playing(false)
436 }
437
438 pub fn resume(&self) -> Result<()> {
440 if let Some(stream) = &self.stream {
441 stream.play().map_err(|e| {
442 VoirsError::device_error("audio_device", format!("Failed to resume stream: {}", e))
443 })?;
444 }
445 self.set_playing(true)
446 }
447
448 pub fn is_playing(&self) -> Result<bool> {
450 let playing = self.is_playing.lock().map_err(|_| {
451 VoirsError::device_error("audio_player", "Failed to lock is_playing mutex")
452 })?;
453 Ok(*playing)
454 }
455
456 fn set_playing(&self, playing: bool) -> Result<()> {
458 let mut state = self.is_playing.lock().map_err(|_| {
459 VoirsError::device_error("audio_player", "Failed to lock is_playing mutex")
460 })?;
461 *state = playing;
462 Ok(())
463 }
464
465 pub fn set_volume(&mut self, volume: f32) -> Result<()> {
467 if volume < 0.0 || volume > 1.0 {
468 return Err(VoirsError::config_error(
469 "Volume must be between 0.0 and 1.0",
470 ));
471 }
472 self.config.volume = volume;
473 Ok(())
474 }
475
476 pub fn get_volume(&self) -> f32 {
478 self.config.volume
479 }
480
481 pub fn queue(&self) -> &PlaybackQueue {
483 &self.queue
484 }
485
486 fn load_audio_file(&self, path: &Path) -> Result<AudioData> {
488 let mut reader = WavReader::open(path).map_err(|e| {
489 VoirsError::device_error("audio_device", format!("Failed to open audio file: {}", e))
490 })?;
491
492 let spec = reader.spec();
493 let samples: std::result::Result<Vec<i16>, hound::Error> =
494 reader.samples::<i16>().collect();
495 let samples = samples.map_err(|e| {
496 VoirsError::device_error(
497 "audio_device",
498 format!("Failed to read audio samples: {}", e),
499 )
500 })?;
501
502 Ok(AudioData {
503 samples,
504 sample_rate: spec.sample_rate,
505 channels: spec.channels,
506 })
507 }
508}
509
510pub fn play_audio_file_simple<P: AsRef<Path>>(path: P) -> Result<()> {
512 use std::process::Command;
513
514 let path = path.as_ref();
515
516 #[cfg(target_os = "macos")]
517 let (command, args) = ("afplay", vec![path.to_str().unwrap()]);
518
519 #[cfg(target_os = "linux")]
520 let (command, args) = {
521 if Command::new("aplay").arg("--version").output().is_ok() {
522 ("aplay", vec![path.to_str().unwrap()])
523 } else if Command::new("paplay").arg("--version").output().is_ok() {
524 ("paplay", vec![path.to_str().unwrap()])
525 } else {
526 return Err(VoirsError::config_error(
527 "No audio player found. Install 'alsa-utils' (aplay) or 'pulseaudio-utils' (paplay)."
528 .to_string(),
529 ));
530 }
531 };
532
533 #[cfg(target_os = "windows")]
534 let (command, args) = (
535 "powershell",
536 vec![
537 "-c",
538 &format!(
539 "(New-Object Media.SoundPlayer '{}').PlaySync()",
540 path.display()
541 ),
542 ],
543 );
544
545 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
546 {
547 return Err(VoirsError::config_error(
548 "Audio playback not supported on this platform".to_string(),
549 ));
550 }
551
552 let status = Command::new(command).args(&args).status().map_err(|e| {
553 VoirsError::config_error(format!("Failed to play audio with '{}': {}", command, e))
554 })?;
555
556 if !status.success() {
557 return Err(VoirsError::config_error(format!(
558 "Audio player '{}' exited with error",
559 command
560 )));
561 }
562
563 Ok(())
564}
565
566#[cfg(test)]
567mod tests {
568 use super::AudioData;
569 use super::*;
570
571 #[test]
572 fn test_playback_config_default() {
573 let config = PlaybackConfig::default();
574 assert_eq!(config.sample_rate, 22050);
575 assert_eq!(config.channels, 1);
576 assert_eq!(config.volume, 1.0);
577 }
578
579 #[test]
580 fn test_playback_queue() {
581 let queue = PlaybackQueue::new();
582 assert!(queue.is_empty().unwrap());
583
584 let audio_data = AudioData {
585 samples: vec![0, 1, 2, 3],
586 sample_rate: 22050,
587 channels: 1,
588 };
589
590 let item = QueueItem {
591 id: "test".to_string(),
592 audio_data,
593 metadata: std::collections::HashMap::new(),
594 };
595
596 queue.enqueue(item).unwrap();
597 assert_eq!(queue.len().unwrap(), 1);
598 assert!(!queue.is_empty().unwrap());
599
600 let dequeued = queue.dequeue().unwrap().unwrap();
601 assert_eq!(dequeued.id, "test");
602 assert!(queue.is_empty().unwrap());
603 }
604
605 #[tokio::test]
606 #[ignore] async fn test_get_output_devices() {
608 match AudioPlayer::get_output_devices() {
610 Ok(devices) => {
611 if !devices.is_empty() {
613 assert!(devices.iter().any(|d| d.is_default));
614 }
615 }
616 Err(_) => {
617 }
619 }
620 }
621
622 #[tokio::test]
623 async fn test_audio_player_creation() {
624 let config = PlaybackConfig::default();
625
626 match AudioPlayer::new(config) {
628 Ok(player) => {
629 assert_eq!(player.get_volume(), 1.0);
630 assert!(!player.is_playing().unwrap());
631 }
632 Err(_) => {
633 }
635 }
636 }
637
638 #[test]
639 fn test_audio_device_supports_config() {
640 let device = AudioDevice {
641 name: "Test Device".to_string(),
642 is_default: true,
643 max_output_channels: 2,
644 sample_rates: vec![22050, 44100, 48000],
645 supported_formats: vec![SampleFormat::F32],
646 };
647
648 let config = PlaybackConfig {
649 sample_rate: 22050,
650 channels: 1,
651 buffer_size: 1024,
652 device_name: None,
653 volume: 1.0,
654 };
655
656 assert!(device.supports_config(&config));
657
658 let unsupported_config = PlaybackConfig {
659 sample_rate: 96000, channels: 1,
661 buffer_size: 1024,
662 device_name: None,
663 volume: 1.0,
664 };
665
666 assert!(!device.supports_config(&unsupported_config));
667 }
668}