use sim_kernel::{Error, Result, Symbol};
use sim_lib_stream_audio::{PcmSampleFormat, PcmSpec};
use sim_lib_stream_host::HostDirection;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AlsaPcmNameKind {
Default,
Hw,
PlugHw,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AlsaPcmName {
raw: String,
kind: AlsaPcmNameKind,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AlsaPcmDevice {
id: Symbol,
pcm_name: AlsaPcmName,
display_name: String,
direction: HostDirection,
channels: usize,
sample_rate_hz: u32,
sample_format: PcmSampleFormat,
buffer_frames: usize,
}
impl AlsaPcmName {
pub fn parse(raw: impl Into<String>) -> Result<Self> {
let raw = raw.into();
let kind = if raw == "default" {
AlsaPcmNameKind::Default
} else if let Some(rest) = raw.strip_prefix("hw:") {
validate_pcm_tail(rest, "hw")?;
AlsaPcmNameKind::Hw
} else if let Some(rest) = raw.strip_prefix("plughw:") {
validate_pcm_tail(rest, "plughw")?;
AlsaPcmNameKind::PlugHw
} else {
return Err(Error::Eval(format!(
"unsupported ALSA PCM name {raw}; expected default, hw:*, or plughw:*"
)));
};
Ok(Self { raw, kind })
}
pub fn raw(&self) -> &str {
&self.raw
}
pub fn kind(&self) -> AlsaPcmNameKind {
self.kind
}
pub fn is_default(&self) -> bool {
self.kind == AlsaPcmNameKind::Default
}
pub fn device_symbol(&self, direction: HostDirection) -> Symbol {
let suffix = match direction {
HostDirection::Input => "capture",
HostDirection::Output => "playback",
HostDirection::Duplex => "duplex",
};
Symbol::new(format!("alsa/{}/{suffix}", self.raw))
}
}
impl AlsaPcmDevice {
pub fn playback(
pcm_name: impl Into<String>,
display_name: impl Into<String>,
channels: usize,
sample_rate_hz: u32,
) -> Result<Self> {
Self::new(
AlsaPcmName::parse(pcm_name)?,
display_name,
HostDirection::Output,
channels,
sample_rate_hz,
PcmSampleFormat::F32,
)
}
pub fn capture(
pcm_name: impl Into<String>,
display_name: impl Into<String>,
channels: usize,
sample_rate_hz: u32,
) -> Result<Self> {
Self::new(
AlsaPcmName::parse(pcm_name)?,
display_name,
HostDirection::Input,
channels,
sample_rate_hz,
PcmSampleFormat::F32,
)
}
pub fn default_playback(channels: usize, sample_rate_hz: u32) -> Result<Self> {
Self::playback("default", "ALSA Default Playback", channels, sample_rate_hz)
}
pub fn default_capture(channels: usize, sample_rate_hz: u32) -> Result<Self> {
Self::capture("default", "ALSA Default Capture", channels, sample_rate_hz)
}
pub fn new(
pcm_name: AlsaPcmName,
display_name: impl Into<String>,
direction: HostDirection,
channels: usize,
sample_rate_hz: u32,
sample_format: PcmSampleFormat,
) -> Result<Self> {
if channels == 0 {
return Err(Error::Eval(
"ALSA PCM channel count must be greater than zero".to_owned(),
));
}
if sample_rate_hz == 0 {
return Err(Error::Eval(
"ALSA PCM sample rate must be greater than zero".to_owned(),
));
}
Ok(Self {
id: pcm_name.device_symbol(direction),
pcm_name,
display_name: display_name.into(),
direction,
channels,
sample_rate_hz,
sample_format,
buffer_frames: 256,
})
}
pub fn with_buffer_frames(mut self, buffer_frames: usize) -> Result<Self> {
if buffer_frames == 0 {
return Err(Error::Eval(
"ALSA PCM buffer frame count must be greater than zero".to_owned(),
));
}
self.buffer_frames = buffer_frames;
Ok(self)
}
pub fn id(&self) -> &Symbol {
&self.id
}
pub fn pcm_name(&self) -> &AlsaPcmName {
&self.pcm_name
}
pub fn display_name(&self) -> &str {
&self.display_name
}
pub fn direction(&self) -> HostDirection {
self.direction
}
pub fn channels(&self) -> usize {
self.channels
}
pub fn sample_rate_hz(&self) -> u32 {
self.sample_rate_hz
}
pub fn sample_format(&self) -> PcmSampleFormat {
self.sample_format
}
pub fn buffer_frames(&self) -> usize {
self.buffer_frames
}
pub fn spec(&self) -> Result<PcmSpec> {
match self.sample_format {
PcmSampleFormat::I16 => PcmSpec::i16(self.channels, self.sample_rate_hz),
PcmSampleFormat::F32 => PcmSpec::f32(self.channels, self.sample_rate_hz),
}
}
pub fn is_default(&self) -> bool {
self.pcm_name.is_default()
}
pub fn is_compatible_with(&self, requested: HostDirection) -> bool {
self.direction == requested || self.direction == HostDirection::Duplex
}
pub fn port_symbol(&self) -> Symbol {
Symbol::new(format!("{}/port", self.id))
}
}
fn validate_pcm_tail(tail: &str, family: &str) -> Result<()> {
if tail.is_empty() {
return Err(Error::Eval(format!(
"ALSA {family}: name must include a card or card,device tail"
)));
}
if tail.chars().any(char::is_whitespace) {
return Err(Error::Eval(format!(
"ALSA {family}: name must not contain whitespace"
)));
}
Ok(())
}