Skip to main content

rustriff_lib/services/
device_service.rs

1use crate::domain::dto::audio_device_dto::AudioDeviceDto;
2use cpal::traits::DeviceTrait;
3use cpal::traits::HostTrait;
4use cpal::Host;
5use tracing::error;
6
7/// Service for managing audio device enumeration and lookup.
8///
9/// `DeviceService` wraps a CPAL [`Host`] and provides convenient methods to:
10/// - List available input and output devices
11/// - Look up devices by their ID
12/// - Convert device information into [`AudioDeviceDto`] for frontend consumption
13pub struct DeviceService {
14    host: Host,
15}
16
17impl DeviceService {
18    /// Creates a new `DeviceService` with the given CPAL host.
19    ///
20    /// # Arguments
21    ///
22    /// * `host` - A CPAL [`Host`] instance.
23    pub fn new(host: Host) -> Self {
24        Self { host }
25    }
26
27    /// Retrieves a list of all available input devices.
28    ///
29    /// Queries the CPAL host for input devices, converts them to [`AudioDeviceDto`],
30    /// and returns them. If device enumeration fails, an empty list is returned
31    /// and an error is added to the logs.
32    ///
33    /// # Returns
34    ///
35    /// A [`Vec`] of [`AudioDeviceDto`] representing available input devices.
36    pub fn get_input_devices(&self) -> Vec<AudioDeviceDto> {
37        match self.host.input_devices() {
38            Ok(devices) => devices
39                .filter_map(|device| {
40                    let desc = device.description().ok()?;
41                    let name = desc.name().to_string();
42                    let device_id = device.id().ok()?;
43                    let id = format!("{:?}", device_id);
44                    let device_config = device.default_input_config().ok()?;
45                    let default_sample_rate = device_config.sample_rate();
46
47                    Some(AudioDeviceDto {
48                        id,
49                        name,
50                        sample_rate: default_sample_rate,
51                    })
52                })
53                .collect(),
54            Err(e) => {
55                error!("Failed to get input devices: {}", e);
56                vec![]
57            }
58        }
59    }
60
61    /// Retrieves a list of all available output devices.
62    ///
63    /// Queries the CPAL host for output devices, converts them to [`AudioDeviceDto`],
64    /// and returns them. If device enumeration fails, an empty list is returned
65    /// and an error is printed to stderr.
66    ///
67    /// # Returns
68    ///
69    /// A [`Vec`] of [`AudioDeviceDto`] representing available output devices.
70    pub fn get_output_devices(&self) -> Vec<AudioDeviceDto> {
71        match self.host.output_devices() {
72            Ok(devices) => devices
73                .filter_map(|device| {
74                    let desc = device.description().ok()?;
75                    let name = desc.name().to_string();
76                    let device_id = device.id().ok()?;
77                    let id = format!("{:?}", device_id);
78                    let device_config = device.default_output_config().ok()?;
79                    let default_sample_rate = device_config.sample_rate();
80
81                    Some(AudioDeviceDto {
82                        id,
83                        name,
84                        sample_rate: default_sample_rate,
85                    })
86                })
87                .collect(),
88            Err(e) => {
89                eprintln!("Failed to get output devices: {}", e);
90                vec![]
91            }
92        }
93    }
94
95    /// Finds an input device by its string ID.
96    ///
97    /// Searches through the host's input devices for one whose debug-formatted
98    /// ID matches the given string.
99    ///
100    /// # Arguments
101    ///
102    /// * `id` - The device ID string to search for (debug-formatted CPAL device ID).
103    ///
104    /// # Returns
105    ///
106    /// `Some(device)` if a matching input device is found, `None` otherwise.
107    pub fn find_input_device_by_id(&self, id: &str) -> Option<cpal::Device> {
108        let devices = self.host.input_devices().ok()?;
109
110        for device in devices {
111            let device_id = device.id().ok()?;
112            if format!("{:?}", device_id) == id {
113                return Some(device);
114            }
115        }
116
117        None
118    }
119
120    /// Finds an output device by its string ID.
121    ///
122    /// Searches through the host's output devices for one whose debug-formatted
123    /// ID matches the given string.
124    ///
125    /// # Arguments
126    ///
127    /// * `id` - The device ID string to search for (debug-formatted CPAL device ID).
128    ///
129    /// # Returns
130    ///
131    /// `Some(device)` if a matching output device is found, `None` otherwise.
132    pub fn find_output_device_by_id(&self, id: &str) -> Option<cpal::Device> {
133        let devices = self.host.output_devices().ok()?;
134
135        for device in devices {
136            let device_id = device.id().ok()?;
137            if format!("{:?}", device_id) == id {
138                return Some(device);
139            }
140        }
141
142        None
143    }
144}