Skip to main content

sim_lib_stream_alsa/
model.rs

1use sim_kernel::{Error, Result, Symbol};
2use sim_lib_stream_audio::{PcmSampleFormat, PcmSpec};
3use sim_lib_stream_host::HostDirection;
4
5/// Supported ALSA PCM name family.
6///
7/// Distinguishes the three PCM naming forms this adapter accepts when parsing
8/// device strings: the routed `default` device, raw `hw:*` hardware access, and
9/// the format-converting `plughw:*` plugin layer.
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum AlsaPcmNameKind {
12    /// The ALSA `default` PCM, a routed fallback device.
13    Default,
14    /// A raw `hw:*` device addressed by card or `card,device`.
15    Hw,
16    /// A `plughw:*` device that adds automatic format and rate conversion.
17    PlugHw,
18}
19
20/// Parsed ALSA PCM name accepted by this adapter.
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub struct AlsaPcmName {
23    raw: String,
24    kind: AlsaPcmNameKind,
25}
26
27/// SIM-visible ALSA PCM device metadata.
28#[derive(Clone, Debug, PartialEq, Eq)]
29pub struct AlsaPcmDevice {
30    id: Symbol,
31    pcm_name: AlsaPcmName,
32    display_name: String,
33    direction: HostDirection,
34    channels: usize,
35    sample_rate_hz: u32,
36    sample_format: PcmSampleFormat,
37    buffer_frames: usize,
38}
39
40impl AlsaPcmName {
41    /// Parses an ALSA PCM name, accepting `default`, `hw:*`, and `plughw:*`.
42    ///
43    /// The `hw:*` and `plughw:*` tails must be non-empty and whitespace-free.
44    /// Any other name is rejected with an `Error::Eval`.
45    ///
46    /// # Examples
47    ///
48    /// ```
49    /// use sim_lib_stream_alsa::{AlsaPcmName, AlsaPcmNameKind};
50    ///
51    /// let name = AlsaPcmName::parse("plughw:1,0").unwrap();
52    /// assert_eq!(name.kind(), AlsaPcmNameKind::PlugHw);
53    /// assert_eq!(name.raw(), "plughw:1,0");
54    /// assert!(!name.is_default());
55    /// assert!(AlsaPcmName::parse("oss:0").is_err());
56    /// ```
57    pub fn parse(raw: impl Into<String>) -> Result<Self> {
58        let raw = raw.into();
59        let kind = if raw == "default" {
60            AlsaPcmNameKind::Default
61        } else if let Some(rest) = raw.strip_prefix("hw:") {
62            validate_pcm_tail(rest, "hw")?;
63            AlsaPcmNameKind::Hw
64        } else if let Some(rest) = raw.strip_prefix("plughw:") {
65            validate_pcm_tail(rest, "plughw")?;
66            AlsaPcmNameKind::PlugHw
67        } else {
68            return Err(Error::Eval(format!(
69                "unsupported ALSA PCM name {raw}; expected default, hw:*, or plughw:*"
70            )));
71        };
72        Ok(Self { raw, kind })
73    }
74
75    /// Returns the original PCM name string as parsed.
76    pub fn raw(&self) -> &str {
77        &self.raw
78    }
79
80    /// Returns the parsed name family.
81    pub fn kind(&self) -> AlsaPcmNameKind {
82        self.kind
83    }
84
85    /// Returns `true` when this is the ALSA `default` PCM.
86    pub fn is_default(&self) -> bool {
87        self.kind == AlsaPcmNameKind::Default
88    }
89
90    /// Builds the SIM device `Symbol` for this PCM name in the given
91    /// `direction`, of the form `alsa/<raw>/<role>` where the role suffix is
92    /// `capture`, `playback`, or `duplex`.
93    pub fn device_symbol(&self, direction: HostDirection) -> Symbol {
94        let suffix = match direction {
95            HostDirection::Input => "capture",
96            HostDirection::Output => "playback",
97            HostDirection::Duplex => "duplex",
98        };
99        Symbol::new(format!("alsa/{}/{suffix}", self.raw))
100    }
101}
102
103impl AlsaPcmDevice {
104    /// Builds an output (playback) PCM device with the F32 sample format.
105    ///
106    /// # Examples
107    ///
108    /// ```
109    /// use sim_lib_stream_alsa::AlsaPcmDevice;
110    ///
111    /// let device = AlsaPcmDevice::playback("hw:0,0", "Card 0", 2, 48_000).unwrap();
112    /// assert_eq!(device.channels(), 2);
113    /// assert_eq!(device.sample_rate_hz(), 48_000);
114    /// assert!(!device.is_default());
115    /// ```
116    pub fn playback(
117        pcm_name: impl Into<String>,
118        display_name: impl Into<String>,
119        channels: usize,
120        sample_rate_hz: u32,
121    ) -> Result<Self> {
122        Self::new(
123            AlsaPcmName::parse(pcm_name)?,
124            display_name,
125            HostDirection::Output,
126            channels,
127            sample_rate_hz,
128            PcmSampleFormat::F32,
129        )
130    }
131
132    /// Builds an input (capture) PCM device with the F32 sample format.
133    pub fn capture(
134        pcm_name: impl Into<String>,
135        display_name: impl Into<String>,
136        channels: usize,
137        sample_rate_hz: u32,
138    ) -> Result<Self> {
139        Self::new(
140            AlsaPcmName::parse(pcm_name)?,
141            display_name,
142            HostDirection::Input,
143            channels,
144            sample_rate_hz,
145            PcmSampleFormat::F32,
146        )
147    }
148
149    /// Builds the routed `default` PCM device for playback.
150    pub fn default_playback(channels: usize, sample_rate_hz: u32) -> Result<Self> {
151        Self::playback("default", "ALSA Default Playback", channels, sample_rate_hz)
152    }
153
154    /// Builds the routed `default` PCM device for capture.
155    pub fn default_capture(channels: usize, sample_rate_hz: u32) -> Result<Self> {
156        Self::capture("default", "ALSA Default Capture", channels, sample_rate_hz)
157    }
158
159    /// Builds a PCM device from explicit parts, defaulting the buffer to 256
160    /// frames.
161    ///
162    /// Returns an `Error::Eval` when `channels` or `sample_rate_hz` is zero.
163    /// The device `Symbol` id is derived from the name and `direction`.
164    pub fn new(
165        pcm_name: AlsaPcmName,
166        display_name: impl Into<String>,
167        direction: HostDirection,
168        channels: usize,
169        sample_rate_hz: u32,
170        sample_format: PcmSampleFormat,
171    ) -> Result<Self> {
172        if channels == 0 {
173            return Err(Error::Eval(
174                "ALSA PCM channel count must be greater than zero".to_owned(),
175            ));
176        }
177        if sample_rate_hz == 0 {
178            return Err(Error::Eval(
179                "ALSA PCM sample rate must be greater than zero".to_owned(),
180            ));
181        }
182        Ok(Self {
183            id: pcm_name.device_symbol(direction),
184            pcm_name,
185            display_name: display_name.into(),
186            direction,
187            channels,
188            sample_rate_hz,
189            sample_format,
190            buffer_frames: 256,
191        })
192    }
193
194    /// Returns a copy of this device with the buffer size set to
195    /// `buffer_frames`, which must be greater than zero.
196    pub fn with_buffer_frames(mut self, buffer_frames: usize) -> Result<Self> {
197        if buffer_frames == 0 {
198            return Err(Error::Eval(
199                "ALSA PCM buffer frame count must be greater than zero".to_owned(),
200            ));
201        }
202        self.buffer_frames = buffer_frames;
203        Ok(self)
204    }
205
206    /// Returns the device's stable SIM identity symbol.
207    pub fn id(&self) -> &Symbol {
208        &self.id
209    }
210
211    /// Returns the parsed ALSA PCM name.
212    pub fn pcm_name(&self) -> &AlsaPcmName {
213        &self.pcm_name
214    }
215
216    /// Returns the human-readable display name.
217    pub fn display_name(&self) -> &str {
218        &self.display_name
219    }
220
221    /// Returns the device direction (input, output, or duplex).
222    pub fn direction(&self) -> HostDirection {
223        self.direction
224    }
225
226    /// Returns the channel count.
227    pub fn channels(&self) -> usize {
228        self.channels
229    }
230
231    /// Returns the sample rate in hertz.
232    pub fn sample_rate_hz(&self) -> u32 {
233        self.sample_rate_hz
234    }
235
236    /// Returns the PCM sample format.
237    pub fn sample_format(&self) -> PcmSampleFormat {
238        self.sample_format
239    }
240
241    /// Returns the configured buffer size in frames.
242    pub fn buffer_frames(&self) -> usize {
243        self.buffer_frames
244    }
245
246    /// Builds the PCM spec (channels, rate, format) for this device.
247    pub fn spec(&self) -> Result<PcmSpec> {
248        match self.sample_format {
249            PcmSampleFormat::I16 => PcmSpec::i16(self.channels, self.sample_rate_hz),
250            PcmSampleFormat::F32 => PcmSpec::f32(self.channels, self.sample_rate_hz),
251        }
252    }
253
254    /// Returns `true` when this device wraps the ALSA `default` PCM.
255    pub fn is_default(&self) -> bool {
256        self.pcm_name.is_default()
257    }
258
259    /// Returns `true` when the device can serve the `requested` direction.
260    ///
261    /// A duplex device satisfies any requested direction.
262    pub fn is_compatible_with(&self, requested: HostDirection) -> bool {
263        self.direction == requested || self.direction == HostDirection::Duplex
264    }
265
266    /// Returns the device's port symbol, the device id with a `/port` suffix.
267    pub fn port_symbol(&self) -> Symbol {
268        Symbol::new(format!("{}/port", self.id))
269    }
270}
271
272fn validate_pcm_tail(tail: &str, family: &str) -> Result<()> {
273    if tail.is_empty() {
274        return Err(Error::Eval(format!(
275            "ALSA {family}: name must include a card or card,device tail"
276        )));
277    }
278    if tail.chars().any(char::is_whitespace) {
279        return Err(Error::Eval(format!(
280            "ALSA {family}: name must not contain whitespace"
281        )));
282    }
283    Ok(())
284}