1use sim_kernel::{Error, Result, Symbol};
2use sim_lib_stream_audio::{PcmSampleFormat, PcmSpec};
3use sim_lib_stream_host::HostDirection;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum AlsaPcmNameKind {
12 Default,
14 Hw,
16 PlugHw,
18}
19
20#[derive(Clone, Debug, PartialEq, Eq)]
22pub struct AlsaPcmName {
23 raw: String,
24 kind: AlsaPcmNameKind,
25}
26
27#[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 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 pub fn raw(&self) -> &str {
77 &self.raw
78 }
79
80 pub fn kind(&self) -> AlsaPcmNameKind {
82 self.kind
83 }
84
85 pub fn is_default(&self) -> bool {
87 self.kind == AlsaPcmNameKind::Default
88 }
89
90 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 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 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 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 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 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 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 pub fn id(&self) -> &Symbol {
208 &self.id
209 }
210
211 pub fn pcm_name(&self) -> &AlsaPcmName {
213 &self.pcm_name
214 }
215
216 pub fn display_name(&self) -> &str {
218 &self.display_name
219 }
220
221 pub fn direction(&self) -> HostDirection {
223 self.direction
224 }
225
226 pub fn channels(&self) -> usize {
228 self.channels
229 }
230
231 pub fn sample_rate_hz(&self) -> u32 {
233 self.sample_rate_hz
234 }
235
236 pub fn sample_format(&self) -> PcmSampleFormat {
238 self.sample_format
239 }
240
241 pub fn buffer_frames(&self) -> usize {
243 self.buffer_frames
244 }
245
246 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 pub fn is_default(&self) -> bool {
256 self.pcm_name.is_default()
257 }
258
259 pub fn is_compatible_with(&self, requested: HostDirection) -> bool {
263 self.direction == requested || self.direction == HostDirection::Duplex
264 }
265
266 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}