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