Skip to main content

voirs_cli/audio/
playback.rs

1//! Cross-platform audio playback implementation.
2
3use 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/// Audio data representation
15#[derive(Debug, Clone)]
16pub struct AudioData {
17    /// Raw audio samples as i16
18    pub samples: Vec<i16>,
19    /// Sample rate in Hz
20    pub sample_rate: u32,
21    /// Number of channels
22    pub channels: u16,
23}
24
25impl AudioData {
26    /// Get duration in seconds
27    pub fn duration(&self) -> f32 {
28        self.samples.len() as f32 / (self.sample_rate as f32 * self.channels as f32)
29    }
30
31    /// Convert to f32 samples
32    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/// Audio playback configuration
41#[derive(Debug, Clone)]
42pub struct PlaybackConfig {
43    /// Target sample rate for playback
44    pub sample_rate: u32,
45    /// Number of audio channels
46    pub channels: u16,
47    /// Audio buffer size in frames
48    pub buffer_size: u32,
49    /// Target audio device (None for default)
50    pub device_name: Option<String>,
51    /// Master volume (0.0 to 1.0)
52    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/// Audio device information
68#[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    /// Check if device supports the given configuration
79    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/// Audio playback queue item
86#[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/// Audio playback queue
94#[derive(Debug)]
95pub struct PlaybackQueue {
96    items: Arc<Mutex<VecDeque<QueueItem>>>,
97    current_playing: Arc<Mutex<Option<String>>>,
98}
99
100impl PlaybackQueue {
101    /// Create a new playback queue
102    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    /// Add audio to the queue
110    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    /// Get the next item from the queue
120    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    /// Get queue length
129    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    /// Check if queue is empty
138    pub fn is_empty(&self) -> Result<bool> {
139        Ok(self.len()? == 0)
140    }
141
142    /// Clear the queue
143    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    /// Set currently playing item
153    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    /// Get currently playing item ID
162    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
176/// Cross-platform audio player
177pub 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    /// Create a new audio player with the given configuration
188    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    /// Get available audio output devices
214    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                // Get device capabilities
234                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                // Remove duplicates and sort
253                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    /// Find device by name
272    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    /// Play audio data immediately
289    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    /// Play audio from file
301    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    /// Add audio to playback queue
307    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    /// Start playback from queue
318    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                    // Fill buffer with audio data
345                    for sample in data.iter_mut() {
346                        *sample = 0.0;
347                    }
348
349                    // Check if we need new audio data
350                    if current_audio.is_none()
351                        || audio_position >= current_audio.as_ref().unwrap().len()
352                    {
353                        // Try to get next item from queue
354                        if let Ok(mut queue_guard) = queue.lock() {
355                            if let Some(item) = queue_guard.pop_front() {
356                                // Set currently playing
357                                if let Ok(mut current_guard) = current_playing.lock() {
358                                    *current_guard = Some(item.id.clone());
359                                }
360
361                                // Convert audio data to f32 samples
362                                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                                // No more audio, stop playback
372                                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                    // Copy audio data to output buffer
384                    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, // No timeout
398            )
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    /// Stop playback
415    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    /// Pause playback
429    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    /// Resume playback
439    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    /// Check if currently playing
449    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    /// Set playing state
457    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    /// Set volume (0.0 to 1.0)
466    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    /// Get current volume
477    pub fn get_volume(&self) -> f32 {
478        self.config.volume
479    }
480
481    /// Get queue reference
482    pub fn queue(&self) -> &PlaybackQueue {
483        &self.queue
484    }
485
486    /// Load audio file
487    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
510/// Simple cross-platform audio file playback using system commands
511pub 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] // Requires actual audio hardware, can segfault in CI/test environments
607    async fn test_get_output_devices() {
608        // This test might fail in CI environments without audio devices
609        match AudioPlayer::get_output_devices() {
610            Ok(devices) => {
611                // If we have devices, at least one should be marked as default
612                if !devices.is_empty() {
613                    assert!(devices.iter().any(|d| d.is_default));
614                }
615            }
616            Err(_) => {
617                // It's okay if no audio devices are available in test environment
618            }
619        }
620    }
621
622    #[tokio::test]
623    async fn test_audio_player_creation() {
624        let config = PlaybackConfig::default();
625
626        // This test might fail in CI environments without audio devices
627        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                // It's okay if no audio devices are available in test environment
634            }
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, // Not supported
660            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}