Skip to main content

sim_lib_stream_alsa/
backend.rs

1use sim_kernel::{Error, Result, Symbol};
2use sim_lib_stream_core::{BufferPolicy, StreamMedia};
3use sim_lib_stream_host::{
4    HostBackend, HostBackendCapability, HostBackendInfo, HostClockInfo, HostDeviceInventory,
5    HostDeviceSpec, HostDirection, HostLatencyInfo, HostOpenStream, HostPortSpec, HostStreamConfig,
6    HostStreamConfigRequest,
7};
8
9use crate::AlsaPcmDevice;
10
11/// Returns the backend identity symbol `stream/host:alsa`.
12pub fn alsa_backend_symbol() -> Symbol {
13    Symbol::qualified("stream/host", "alsa")
14}
15
16/// Returns the transport identity symbol `stream/transport:alsa`.
17pub fn alsa_transport_symbol() -> Symbol {
18    Symbol::qualified("stream/transport", "alsa")
19}
20
21/// ALSA host backend with provider-supplied deterministic PCM devices.
22#[derive(Clone, Debug)]
23pub struct AlsaBackend {
24    info: HostBackendInfo,
25    devices: Vec<AlsaPcmDevice>,
26}
27
28impl Default for AlsaBackend {
29    fn default() -> Self {
30        Self::new(Vec::new())
31    }
32}
33
34impl AlsaBackend {
35    /// Builds a hardware-required backend over the supplied PCM `devices`.
36    ///
37    /// Capabilities are derived from the devices' directions; the backend
38    /// reports that real audio hardware is required.
39    pub fn new(devices: Vec<AlsaPcmDevice>) -> Self {
40        Self {
41            info: HostBackendInfo::new(
42                alsa_backend_symbol(),
43                alsa_transport_symbol(),
44                StreamMedia::Pcm,
45                true,
46            )
47            .with_capabilities(capabilities_for(&devices, true)),
48            devices,
49        }
50    }
51
52    /// Builds an offline backend with a deterministic set of fake PCM devices.
53    ///
54    /// The backend reports that hardware is not required and carries the
55    /// `Offline` and `Fake` capabilities, so it can drive validation without an
56    /// ALSA development package or sound hardware. It exposes a default
57    /// playback, a default capture, a `hw:0,0` playback, and a `plughw:1,0`
58    /// capture device.
59    ///
60    /// # Examples
61    ///
62    /// ```
63    /// use sim_lib_stream_alsa::AlsaBackend;
64    ///
65    /// let backend = AlsaBackend::fake();
66    /// assert_eq!(backend.list_devices().len(), 4);
67    /// assert!(backend.default_playback().is_some());
68    /// assert!(backend.default_capture().is_some());
69    /// ```
70    pub fn fake() -> Self {
71        let devices = vec![
72            AlsaPcmDevice::default_playback(2, 48_000)
73                .expect("valid default playback")
74                .with_buffer_frames(128)
75                .expect("valid default playback buffer"),
76            AlsaPcmDevice::default_capture(2, 48_000)
77                .expect("valid default capture")
78                .with_buffer_frames(128)
79                .expect("valid default capture buffer"),
80            AlsaPcmDevice::playback("hw:0,0", "Fake ALSA hw Playback", 2, 48_000)
81                .expect("valid hw playback"),
82            AlsaPcmDevice::capture("plughw:1,0", "Fake ALSA plughw Capture", 1, 48_000)
83                .expect("valid plughw capture"),
84        ];
85        Self {
86            info: HostBackendInfo::new(
87                alsa_backend_symbol(),
88                alsa_transport_symbol(),
89                StreamMedia::Pcm,
90                false,
91            )
92            .with_capabilities(capabilities_for(&devices, false)),
93            devices,
94        }
95    }
96
97    /// Returns the backend's PCM devices in registration order.
98    pub fn list_devices(&self) -> &[AlsaPcmDevice] {
99        &self.devices
100    }
101
102    /// Returns the first default device that can serve output, if any.
103    pub fn default_playback(&self) -> Option<&AlsaPcmDevice> {
104        self.devices
105            .iter()
106            .find(|device| device.is_default() && device.is_compatible_with(HostDirection::Output))
107    }
108
109    /// Returns the first default device that can serve input, if any.
110    pub fn default_capture(&self) -> Option<&AlsaPcmDevice> {
111        self.devices
112            .iter()
113            .find(|device| device.is_default() && device.is_compatible_with(HostDirection::Input))
114    }
115
116    /// Opens an output stream on the default playback device.
117    ///
118    /// Errors when no default playback device is present, or when the open
119    /// request cannot be satisfied. `capacity` bounds the stream buffer.
120    pub fn open_default_playback(&self, capacity: usize) -> Result<HostOpenStream> {
121        let device = self
122            .default_playback()
123            .ok_or_else(|| Error::Eval("ALSA default playback PCM was not found".to_owned()))?;
124        self.open(request(device, HostDirection::Output, capacity)?)
125    }
126
127    /// Opens an input stream on the default capture device.
128    ///
129    /// Errors when no default capture device is present, or when the open
130    /// request cannot be satisfied. `capacity` bounds the stream buffer.
131    pub fn open_default_capture(&self, capacity: usize) -> Result<HostOpenStream> {
132        let device = self
133            .default_capture()
134            .ok_or_else(|| Error::Eval("ALSA default capture PCM was not found".to_owned()))?;
135        self.open(request(device, HostDirection::Input, capacity)?)
136    }
137
138    fn require_device(
139        &self,
140        device_id: &Symbol,
141        direction: HostDirection,
142    ) -> Result<&AlsaPcmDevice> {
143        let Some(device) = self
144            .devices
145            .iter()
146            .find(|candidate| candidate.id() == device_id)
147        else {
148            return Err(Error::Eval(format!(
149                "ALSA PCM device {device_id} was not found"
150            )));
151        };
152        if !device.is_compatible_with(direction) {
153            return Err(Error::TypeMismatch {
154                expected: "ALSA PCM device with requested direction",
155                found: "ALSA PCM device with another direction",
156            });
157        }
158        Ok(device)
159    }
160}
161
162impl HostBackend for AlsaBackend {
163    fn info(&self) -> &HostBackendInfo {
164        &self.info
165    }
166
167    fn enumerate(&self) -> Result<HostDeviceInventory> {
168        let devices = self
169            .devices
170            .iter()
171            .map(|device| {
172                Ok(HostDeviceSpec::new(
173                    device.id().clone(),
174                    alsa_backend_symbol(),
175                    StreamMedia::Pcm,
176                    device.direction(),
177                    Symbol::qualified("clock", "alsa"),
178                    BufferPolicy::bounded(device.buffer_frames())?,
179                ))
180            })
181            .collect::<Result<Vec<_>>>()?;
182        let ports = self
183            .devices
184            .iter()
185            .map(|device| {
186                HostPortSpec::new(
187                    device.port_symbol(),
188                    device.id().clone(),
189                    alsa_backend_symbol(),
190                    StreamMedia::Pcm,
191                    device.direction(),
192                )
193            })
194            .collect();
195        Ok(HostDeviceInventory::new(alsa_backend_symbol())
196            .with_devices(devices)
197            .with_ports(ports))
198    }
199
200    fn open(&self, request: HostStreamConfigRequest) -> Result<HostOpenStream> {
201        if request.backend() != self.info.id() {
202            return Err(Error::Eval(format!(
203                "ALSA backend cannot open {} requests",
204                request.backend()
205            )));
206        }
207        if request.media() != StreamMedia::Pcm {
208            return Err(Error::TypeMismatch {
209                expected: "PCM stream request",
210                found: "non-PCM stream request",
211            });
212        }
213        let direction = request.direction();
214        let device = self.require_device(request.device(), direction)?;
215        let config = HostStreamConfig::from_request(
216            request,
217            latency_for(direction, device.buffer_frames()),
218            HostClockInfo::new(
219                Symbol::qualified("clock", "alsa"),
220                Some(device.sample_rate_hz()),
221                !self.info.hardware_required(),
222            ),
223        );
224        Ok(HostOpenStream::new(config))
225    }
226}
227
228fn request(
229    device: &AlsaPcmDevice,
230    direction: HostDirection,
231    capacity: usize,
232) -> Result<HostStreamConfigRequest> {
233    Ok(HostStreamConfigRequest::new(
234        alsa_backend_symbol(),
235        device.id().clone(),
236        StreamMedia::Pcm,
237        direction,
238        BufferPolicy::bounded(capacity)?,
239    ))
240}
241
242fn capabilities_for(devices: &[AlsaPcmDevice], fake: bool) -> Vec<HostBackendCapability> {
243    let mut capabilities = Vec::new();
244    if devices
245        .iter()
246        .any(|device| device.is_compatible_with(HostDirection::Output))
247    {
248        capabilities.push(HostBackendCapability::AudioOutput);
249    }
250    if devices
251        .iter()
252        .any(|device| device.is_compatible_with(HostDirection::Input))
253    {
254        capabilities.push(HostBackendCapability::AudioInput);
255    }
256    if devices
257        .iter()
258        .any(|device| device.direction() == HostDirection::Duplex)
259    {
260        capabilities.push(HostBackendCapability::Duplex);
261    }
262    capabilities.push(HostBackendCapability::Reconnect);
263    if fake {
264        capabilities.push(HostBackendCapability::Offline);
265        capabilities.push(HostBackendCapability::Fake);
266    }
267    capabilities
268}
269
270fn latency_for(direction: HostDirection, frames: usize) -> HostLatencyInfo {
271    let frames = frames as u32;
272    match direction {
273        HostDirection::Input => HostLatencyInfo::new(frames, 0),
274        HostDirection::Output => HostLatencyInfo::new(0, frames),
275        HostDirection::Duplex => HostLatencyInfo::new(frames, frames),
276    }
277}