#![forbid(unsafe_code)]
#![no_std]
extern crate alloc;
#[cfg(feature = "std")]
extern crate std;
use alloc::boxed::Box;
use alloc::collections::VecDeque;
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
use core::fmt;
#[cfg(feature = "oxiaudio")]
mod oxiaudio_bridge;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SampleFormat {
F32,
I16,
I24,
I32,
U8,
F64,
}
impl fmt::Display for SampleFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
SampleFormat::F32 => "f32",
SampleFormat::I16 => "i16",
SampleFormat::I24 => "I24",
SampleFormat::I32 => "i32",
SampleFormat::U8 => "u8",
SampleFormat::F64 => "f64",
})
}
}
impl SampleFormat {
#[must_use]
pub const fn byte_size(self) -> usize {
match self {
SampleFormat::U8 => 1,
SampleFormat::I16 => 2,
SampleFormat::I24 => 3,
SampleFormat::F32 | SampleFormat::I32 => 4,
SampleFormat::F64 => 8,
}
}
#[must_use]
pub const fn is_float(self) -> bool {
matches!(self, SampleFormat::F32 | SampleFormat::F64)
}
}
#[must_use]
pub fn pick_preferred_format(
preferred: &[SampleFormat],
supported: &[SampleFormat],
) -> Option<SampleFormat> {
for &fmt in preferred {
if supported.contains(&fmt) {
return Some(fmt);
}
}
supported.first().copied()
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DeviceCapabilities {
pub min_buffer_size: Option<u32>,
pub max_buffer_size: Option<u32>,
pub supported_formats: Vec<SampleFormat>,
pub exclusive_mode: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DeviceInfo {
pub name: String,
pub is_default: bool,
pub sample_rates: Vec<u32>,
pub channel_counts: Vec<u16>,
pub is_input: bool,
pub is_output: bool,
pub capabilities: Option<DeviceCapabilities>,
}
impl fmt::Display for DeviceInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let default_marker = if self.is_default { " [default]" } else { "" };
let io = match (self.is_input, self.is_output) {
(true, true) => " [in+out]",
(true, false) => " [in]",
(false, true) => " [out]",
(false, false) => "",
};
write!(f, "{}{}{}", self.name, default_marker, io)
}
}
impl DeviceInfo {
#[must_use]
pub fn supports_config(&self, config: &StreamConfig) -> bool {
if !self.channel_counts.is_empty() && !self.channel_counts.contains(&config.channels) {
return false;
}
if !self.sample_rates.is_empty() {
let min = self.sample_rates.iter().copied().min().unwrap_or(0);
let max = self.sample_rates.iter().copied().max().unwrap_or(u32::MAX);
if config.sample_rate < min || config.sample_rate > max {
return false;
}
}
true
}
}
pub struct DeviceInfoBuilder {
inner: DeviceInfo,
}
impl DeviceInfo {
pub fn builder(name: impl Into<String>) -> DeviceInfoBuilder {
DeviceInfoBuilder {
inner: DeviceInfo {
name: name.into(),
..Default::default()
},
}
}
}
impl DeviceInfoBuilder {
pub fn default_device(mut self) -> Self {
self.inner.is_default = true;
self
}
pub fn input(mut self, v: bool) -> Self {
self.inner.is_input = v;
self
}
pub fn output(mut self, v: bool) -> Self {
self.inner.is_output = v;
self
}
pub fn sample_rates(mut self, rates: Vec<u32>) -> Self {
self.inner.sample_rates = rates;
self
}
pub fn channel_counts(mut self, counts: Vec<u16>) -> Self {
self.inner.channel_counts = counts;
self
}
pub fn capabilities(mut self, caps: DeviceCapabilities) -> Self {
self.inner.capabilities = Some(caps);
self
}
pub fn build(self) -> DeviceInfo {
self.inner
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct StreamConfig {
pub sample_rate: u32,
pub channels: u16,
pub buffer_size: Option<u32>,
pub sample_format: Option<SampleFormat>,
pub exclusive: bool,
#[cfg_attr(feature = "serde", serde(default))]
pub preferred_formats: Vec<SampleFormat>,
#[cfg_attr(feature = "serde", serde(default))]
pub channel_routing: Option<ChannelRouting>,
#[cfg_attr(feature = "serde", serde(default))]
pub buffer_capacity_secs: Option<f32>,
}
impl StreamConfig {
pub const STEREO_48K: StreamConfig = StreamConfig {
sample_rate: 48_000,
channels: 2,
buffer_size: None,
sample_format: None,
exclusive: false,
preferred_formats: Vec::new(),
channel_routing: None,
buffer_capacity_secs: None,
};
pub const STEREO_44K: StreamConfig = StreamConfig {
sample_rate: 44_100,
channels: 2,
buffer_size: None,
sample_format: None,
exclusive: false,
preferred_formats: Vec::new(),
channel_routing: None,
buffer_capacity_secs: None,
};
pub const MONO_16K: StreamConfig = StreamConfig {
sample_rate: 16_000,
channels: 1,
buffer_size: None,
sample_format: None,
exclusive: false,
preferred_formats: Vec::new(),
channel_routing: None,
buffer_capacity_secs: None,
};
pub fn stereo_48k() -> Self {
Self {
sample_rate: 48_000,
channels: 2,
buffer_size: None,
sample_format: None,
exclusive: false,
preferred_formats: Vec::new(),
channel_routing: None,
buffer_capacity_secs: None,
}
}
pub fn stereo_44k() -> Self {
Self {
sample_rate: 44_100,
channels: 2,
buffer_size: None,
sample_format: None,
exclusive: false,
preferred_formats: Vec::new(),
channel_routing: None,
buffer_capacity_secs: None,
}
}
pub fn mono_16k() -> Self {
Self {
sample_rate: 16_000,
channels: 1,
buffer_size: None,
sample_format: None,
exclusive: false,
preferred_formats: Vec::new(),
channel_routing: None,
buffer_capacity_secs: None,
}
}
pub fn low_latency_stereo_48k() -> Self {
Self {
sample_rate: 48_000,
channels: 2,
buffer_size: Some(256),
sample_format: None,
exclusive: false,
preferred_formats: Vec::new(),
channel_routing: None,
buffer_capacity_secs: None,
}
}
pub fn builder() -> StreamConfigBuilder {
StreamConfigBuilder {
inner: StreamConfig {
sample_rate: 44_100,
channels: 2,
buffer_size: None,
sample_format: None,
exclusive: false,
preferred_formats: Vec::new(),
channel_routing: None,
buffer_capacity_secs: None,
},
}
}
pub fn validate(&self, info: &DeviceInfo) -> Result<(), OxiSoundError> {
if !info.channel_counts.is_empty() && !info.channel_counts.contains(&self.channels) {
return Err(OxiSoundError::UnsupportedConfig(format!(
"channel count {} not supported; device supports: {:?}",
self.channels, info.channel_counts
)));
}
if !info.sample_rates.is_empty() {
let min_rate = info.sample_rates.iter().copied().min().unwrap_or(0);
let max_rate = info.sample_rates.iter().copied().max().unwrap_or(u32::MAX);
if self.sample_rate < min_rate || self.sample_rate > max_rate {
return Err(OxiSoundError::UnsupportedConfig(format!(
"sample rate {} Hz not supported; device range: {}–{} Hz",
self.sample_rate, min_rate, max_rate
)));
}
}
Ok(())
}
}
impl fmt::Display for StreamConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let buf = match self.buffer_size {
Some(n) => format!("{n} frames"),
None => "auto".to_string(),
};
write!(f, "{}Hz {}ch buf={}", self.sample_rate, self.channels, buf)?;
if let Some(fmt) = self.sample_format {
write!(f, " fmt={fmt}")?;
}
if self.exclusive {
f.write_str(" excl")?;
}
if let Some(cap) = self.buffer_capacity_secs {
write!(f, ", capacity={}s", cap)?;
}
Ok(())
}
}
pub struct StreamConfigBuilder {
inner: StreamConfig,
}
impl StreamConfigBuilder {
pub fn sample_rate(mut self, rate: u32) -> Self {
self.inner.sample_rate = rate;
self
}
pub fn channels(mut self, ch: u16) -> Self {
self.inner.channels = ch;
self
}
pub fn buffer_size(mut self, size: u32) -> Self {
self.inner.buffer_size = Some(size);
self
}
pub fn sample_format(mut self, fmt: SampleFormat) -> Self {
self.inner.sample_format = Some(fmt);
self
}
pub fn exclusive(mut self) -> Self {
self.inner.exclusive = true;
self
}
pub fn preferred_formats(mut self, formats: Vec<SampleFormat>) -> Self {
self.inner.preferred_formats = formats;
self
}
pub fn channel_routing(mut self, routing: ChannelRouting) -> Self {
self.inner.channel_routing = Some(routing);
self
}
pub fn buffer_capacity_secs(mut self, secs: f32) -> Self {
self.inner.buffer_capacity_secs = Some(secs);
self
}
pub fn build(self) -> StreamConfig {
self.inner
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NegotiatedConfig {
pub sample_rate: u32,
pub channels: u16,
pub buffer_size: u32,
pub sample_format: SampleFormat,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum HostApi {
CoreAudio,
Wasapi,
Asio,
Alsa,
Jack,
PipeWire,
PulseAudio,
}
impl fmt::Display for HostApi {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let label = match self {
HostApi::CoreAudio => "Core Audio",
HostApi::Wasapi => "WASAPI",
HostApi::Asio => "ASIO",
HostApi::Alsa => "ALSA",
HostApi::Jack => "JACK",
HostApi::PipeWire => "PipeWire",
HostApi::PulseAudio => "PulseAudio",
};
f.write_str(label)
}
}
impl HostApi {
#[must_use]
pub fn is_available(&self) -> bool {
match self {
Self::CoreAudio => cfg!(any(target_os = "macos", target_os = "ios")),
Self::Wasapi => cfg!(target_os = "windows"),
Self::Alsa => cfg!(target_os = "linux"),
Self::Jack => false,
Self::Asio => false,
Self::PulseAudio => cfg!(target_os = "linux"),
Self::PipeWire => cfg!(target_os = "linux"),
}
}
}
#[derive(Debug, Clone, Copy, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct StreamStats {
pub frames_processed: u64,
pub underruns: u64,
pub overruns: u64,
pub latency_frames: u32,
pub cpu_load_percent: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum CallbackPriority {
#[default]
Normal,
Realtime,
}
pub trait AudioDevice: Sized {
fn enumerate() -> Result<Vec<DeviceInfo>, OxiSoundError>;
fn default_output() -> Result<Self, OxiSoundError>;
fn default_input() -> Result<Self, OxiSoundError>;
fn open_output(&self, config: StreamConfig) -> Result<Box<dyn OutputStream>, OxiSoundError>;
fn open_input(&self, config: StreamConfig) -> Result<Box<dyn InputStream>, OxiSoundError>;
fn open_duplex(&self, config: StreamConfig) -> Result<Box<dyn DuplexStream>, OxiSoundError>;
fn negotiate_output(&self, _config: StreamConfig) -> Result<NegotiatedConfig, OxiSoundError> {
Err(OxiSoundError::UnsupportedConfig(
"negotiate_output not implemented for this backend".into(),
))
}
}
pub trait OutputStream: Send {
fn write(&mut self, samples: &[f32]) -> Result<(), OxiSoundError>;
fn stats(&self) -> StreamStats {
StreamStats::default()
}
}
pub trait InputStream: Send {
fn read(&mut self, samples: &mut [f32]) -> Result<usize, OxiSoundError>;
fn stats(&self) -> StreamStats {
StreamStats::default()
}
}
pub trait DuplexStream: Send {
fn write(&mut self, out: &[f32]) -> Result<(), OxiSoundError>;
fn read(&mut self, inp: &mut [f32]) -> Result<usize, OxiSoundError>;
fn stats(&self) -> StreamStats {
StreamStats::default()
}
}
#[cfg(feature = "tokio")]
#[expect(
async_fn_in_trait,
reason = "DeviceWatcher is not used as a trait object; the async fn in trait limitation is acknowledged"
)]
pub trait AsyncOutputStream: Send {
async fn write(&mut self, samples: &[f32]) -> Result<(), OxiSoundError>;
}
#[cfg(feature = "tokio")]
pub trait AsyncInputStream: Send {
fn stream(&mut self) -> impl futures_core::Stream<Item = Vec<f32>> + '_;
}
pub trait DeviceSelector {
fn select(&self, devices: &[DeviceInfo]) -> Option<usize>;
}
pub struct DefaultSelector;
impl DeviceSelector for DefaultSelector {
fn select(&self, devices: &[DeviceInfo]) -> Option<usize> {
if devices.is_empty() {
return None;
}
devices.iter().position(|d| d.is_default).or(Some(0))
}
}
pub struct LatencyOptimalSelector;
impl DeviceSelector for LatencyOptimalSelector {
fn select(&self, devices: &[DeviceInfo]) -> Option<usize> {
if devices.is_empty() { None } else { Some(0) }
}
}
pub struct NameMatchSelector(pub String);
impl DeviceSelector for NameMatchSelector {
fn select(&self, devices: &[DeviceInfo]) -> Option<usize> {
let fragment = self.0.to_lowercase();
devices
.iter()
.position(|d| d.name.to_lowercase().contains(&fragment))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Channel {
FrontLeft,
FrontRight,
Center,
Lfe,
SurroundLeft,
SurroundRight,
BackLeft,
BackRight,
}
impl Channel {
#[must_use]
pub fn standard_index(self) -> usize {
match self {
Channel::FrontLeft => 0,
Channel::FrontRight => 1,
Channel::Center => 2,
Channel::Lfe => 3,
Channel::SurroundLeft => 4,
Channel::SurroundRight => 5,
Channel::BackLeft => 6,
Channel::BackRight => 7,
}
}
}
impl fmt::Display for Channel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Channel::FrontLeft => "FL",
Channel::FrontRight => "FR",
Channel::Center => "C",
Channel::Lfe => "LFE",
Channel::SurroundLeft => "SL",
Channel::SurroundRight => "SR",
Channel::BackLeft => "BL",
Channel::BackRight => "BR",
})
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ChannelRouting(pub Vec<(Channel, usize)>);
impl ChannelRouting {
pub fn stereo() -> Self {
Self(vec![(Channel::FrontLeft, 0), (Channel::FrontRight, 1)])
}
pub fn surround_5_1() -> Self {
Self(vec![
(Channel::FrontLeft, 0),
(Channel::FrontRight, 1),
(Channel::Center, 2),
(Channel::Lfe, 3),
(Channel::SurroundLeft, 4),
(Channel::SurroundRight, 5),
])
}
pub fn surround_7_1() -> Self {
Self(vec![
(Channel::FrontLeft, 0),
(Channel::FrontRight, 1),
(Channel::Center, 2),
(Channel::Lfe, 3),
(Channel::SurroundLeft, 4),
(Channel::SurroundRight, 5),
(Channel::BackLeft, 6),
(Channel::BackRight, 7),
])
}
#[must_use]
pub fn channel_count(&self) -> usize {
self.0.len()
}
pub fn apply_interleaved(&self, buf: &mut [f32], channels: usize) {
if channels == 0 || self.0.is_empty() {
return;
}
let frame_count = buf.len() / channels;
let mut scratch = vec![0.0f32; channels];
for frame_idx in 0..frame_count {
let base = frame_idx * channels;
let frame = &buf[base..base + channels];
scratch[..channels].copy_from_slice(frame);
for &(ref logical, physical) in &self.0 {
let src = logical.standard_index();
if src < channels && physical < channels {
scratch[physical] = frame[src];
}
}
buf[base..base + channels].copy_from_slice(&scratch[..channels]);
}
}
}
impl fmt::Display for ChannelRouting {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s: Vec<String> = self
.0
.iter()
.map(|(ch, idx)| format!("{ch}:{idx}"))
.collect();
f.write_str(&s.join(" "))
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum DeviceEvent {
DeviceAdded(DeviceInfo),
DeviceRemoved(String),
DefaultChanged(DeviceInfo),
}
impl fmt::Display for DeviceEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DeviceEvent::DeviceAdded(d) => write!(f, "device added: {}", d.name),
DeviceEvent::DeviceRemoved(name) => write!(f, "device removed: {name}"),
DeviceEvent::DefaultChanged(d) => write!(f, "default changed: {}", d.name),
}
}
}
pub trait DeviceNotificationCallback: Send {
fn on_device_change(&self, event: DeviceEvent);
}
#[cfg(feature = "tokio")]
pub trait DeviceWatcher: Send {
fn events(&mut self) -> impl futures_core::Stream<Item = DeviceEvent> + '_;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SessionCategory {
Playback,
Record,
PlayAndRecord,
Ambient,
SoloAmbient,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SessionInterruptionEvent {
Began,
Ended { should_resume: bool },
}
pub trait AudioSession: Send {
fn set_category(&self, category: SessionCategory) -> Result<(), OxiSoundError>;
fn set_preferred_sample_rate(&self, rate: u32) -> Result<(), OxiSoundError>;
fn set_preferred_buffer_duration(&self, secs: f64) -> Result<(), OxiSoundError>;
fn on_interruption(&self, event: SessionInterruptionEvent);
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MidiDeviceInfo {
pub name: String,
pub is_input: bool,
pub is_output: bool,
pub port_count: usize,
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MidiMessage {
pub status: u8,
pub data: Vec<u8>,
pub timestamp_micros: u64,
}
impl MidiMessage {
pub fn is_sysex(&self) -> bool {
self.status == 0xF0
}
pub fn sysex_payload(&self) -> Option<&[u8]> {
if self.is_sysex() {
Some(&self.data)
} else {
None
}
}
pub fn new_sysex(payload: &[u8]) -> Self {
Self {
status: 0xF0,
data: payload.to_vec(),
timestamp_micros: 0,
}
}
pub fn to_bytes(&self) -> Vec<u8> {
if self.is_sysex() {
let mut bytes = Vec::with_capacity(self.data.len() + 2);
bytes.push(0xF0);
bytes.extend_from_slice(&self.data);
bytes.push(0xF7);
bytes
} else {
let mut bytes = Vec::with_capacity(self.data.len() + 1);
bytes.push(self.status);
bytes.extend_from_slice(&self.data);
bytes
}
}
}
pub const MIDI_CLOCK: u8 = 0xF8;
pub const MIDI_START: u8 = 0xFA;
pub const MIDI_CONTINUE: u8 = 0xFB;
pub const MIDI_STOP: u8 = 0xFC;
#[derive(Debug, Clone)]
pub struct MidiClock {
tick_timestamps: VecDeque<u64>,
running: bool,
}
impl MidiClock {
pub fn new() -> Self {
Self {
tick_timestamps: VecDeque::with_capacity(25),
running: false,
}
}
pub fn tick(&mut self, timestamp_micros: u64) {
self.running = true;
self.tick_timestamps.push_back(timestamp_micros);
if self.tick_timestamps.len() > 24 {
self.tick_timestamps.pop_front();
}
}
pub fn bpm(&self) -> Option<f64> {
if self.tick_timestamps.len() < 2 {
return None;
}
let first = *self.tick_timestamps.front()?;
let last = *self.tick_timestamps.back()?;
let span_us = last.saturating_sub(first);
if span_us == 0 {
return None;
}
let n_intervals = (self.tick_timestamps.len() - 1) as f64;
let avg_interval_us = span_us as f64 / n_intervals;
Some(60_000_000.0 / (avg_interval_us * 24.0))
}
pub fn is_running(&self) -> bool {
self.running
}
pub fn handle_message(&mut self, msg: &MidiMessage) {
match msg.status {
MIDI_CLOCK => self.tick(msg.timestamp_micros),
MIDI_START => {
self.tick_timestamps.clear();
self.running = true;
}
MIDI_CONTINUE => self.running = true,
MIDI_STOP => self.running = false,
_ => {}
}
}
}
impl Default for MidiClock {
fn default() -> Self {
Self::new()
}
}
pub trait MidiInput: Send {
fn receive(&mut self) -> Result<Option<MidiMessage>, OxiSoundError>;
}
pub trait MidiOutput: Send {
fn send(&mut self, msg: &MidiMessage) -> Result<(), OxiSoundError>;
}
pub trait MidiDevice: Sized {
fn enumerate_midi() -> Result<Vec<MidiDeviceInfo>, OxiSoundError>;
fn open_midi_input(port: usize) -> Result<Box<dyn MidiInput>, OxiSoundError>;
fn open_midi_output(port: usize) -> Result<Box<dyn MidiOutput>, OxiSoundError>;
}
#[derive(Debug, thiserror::Error)]
pub enum OxiSoundError {
#[error("no audio device available")]
NoDevice,
#[error("device error: {0}")]
Device(String),
#[error("stream error: {0}")]
Stream(String),
#[error("configuration not supported: {0}")]
UnsupportedConfig(String),
#[error("device disconnected: {0}")]
Disconnected(String),
#[error("buffer overrun: {0}")]
Overrun(String),
#[error("buffer underrun: {0}")]
Underrun(String),
#[error("hot-plug error: {0}")]
HotPlugError(String),
#[error("permission denied: {0}")]
PermissionDenied(String),
#[error("operation timed out: {0}")]
Timeout(String),
#[error("sample format mismatch: {0}")]
FormatMismatch(String),
#[cfg(feature = "std")]
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("Unsupported: {0}")]
Unsupported(String),
}
impl OxiSoundError {
#[must_use]
pub fn kind(&self) -> &'static str {
match self {
#[cfg(feature = "std")]
Self::Io(_) => "io",
Self::NoDevice => "no-device",
Self::Device(_) => "device",
Self::Stream(_) => "stream",
Self::UnsupportedConfig(_) => "unsupported-config",
Self::Disconnected(_) => "disconnected",
Self::Overrun(_) => "overrun",
Self::Underrun(_) => "underrun",
Self::HotPlugError(_) => "hot-plug-error",
Self::PermissionDenied(_) => "permission-denied",
Self::Timeout(_) => "timeout",
Self::FormatMismatch(_) => "format-mismatch",
Self::Unsupported(_) => "unsupported",
}
}
}
#[cfg(test)]
mod tests;