use audio_core_bsd::{AudioError, SampleFormat};
use crate::Result;
pub use audio_core_bsd::SampleFormat as CoreSampleFormat;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BufferSize {
Fixed(usize),
Default,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StreamParams {
pub sample_rate: u32,
pub channels: u16,
pub sample_format: SampleFormat,
pub buffer_size: BufferSize,
}
impl StreamParams {
#[must_use]
pub fn pcm_48k_stereo() -> Self {
Self {
sample_rate: 48_000,
channels: 2,
sample_format: SampleFormat::F32,
buffer_size: BufferSize::Default,
}
}
#[must_use]
pub fn pcm_48k_mono() -> Self {
Self {
sample_rate: 48_000,
channels: 1,
sample_format: SampleFormat::F32,
buffer_size: BufferSize::Default,
}
}
#[must_use]
pub fn with_sample_rate(mut self, sample_rate: u32) -> Self {
self.sample_rate = sample_rate;
self
}
#[must_use]
pub fn with_channels(mut self, channels: u16) -> Self {
self.channels = channels;
self
}
#[must_use]
pub fn with_sample_format(mut self, sample_format: SampleFormat) -> Self {
self.sample_format = sample_format;
self
}
#[must_use]
pub fn with_fixed_buffer(mut self, frames: usize) -> Self {
self.buffer_size = BufferSize::Fixed(frames);
self
}
pub fn validate(&self) -> Result<()> {
if self.sample_rate == 0 {
return Err(AudioError::InvalidSampleRate(0).into());
}
if self.channels == 0 {
return Err(AudioError::InvalidChannelCount(0).into());
}
if let BufferSize::Fixed(n) = self.buffer_size {
if n == 0 {
return Err(AudioError::BufferTooSmall { needed: 1, have: 0 }.into());
}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DeviceDirection {
Input,
Output,
Duplex,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeviceInfo {
pub name: String,
pub direction: DeviceDirection,
pub channels: u16,
pub sample_rates: Vec<u32>,
pub is_default: bool,
}
impl DeviceInfo {
#[must_use]
pub fn new(
name: impl Into<String>,
direction: DeviceDirection,
channels: u16,
sample_rates: Vec<u32>,
is_default: bool,
) -> Self {
Self {
name: name.into(),
direction,
channels,
sample_rates,
is_default,
}
}
#[must_use]
pub fn supports_rate(&self, rate: u32) -> bool {
self.sample_rates.is_empty() || self.sample_rates.contains(&rate)
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
#[test]
fn pcm_48k_stereo_has_expected_defaults() {
let p = StreamParams::pcm_48k_stereo();
assert_eq!(p.sample_rate, 48_000);
assert_eq!(p.channels, 2);
assert_eq!(p.sample_format, SampleFormat::F32);
assert_eq!(p.buffer_size, BufferSize::Default);
}
#[test]
fn pcm_48k_mono_has_expected_defaults() {
let p = StreamParams::pcm_48k_mono();
assert_eq!(p.channels, 1);
}
#[test]
fn builders_chain_into_custom_params() {
let p = StreamParams::pcm_48k_stereo()
.with_sample_rate(44_100)
.with_channels(6)
.with_sample_format(SampleFormat::I16)
.with_fixed_buffer(256);
assert_eq!(p.sample_rate, 44_100);
assert_eq!(p.channels, 6);
assert_eq!(p.sample_format, SampleFormat::I16);
assert_eq!(p.buffer_size, BufferSize::Fixed(256));
}
#[test]
fn validate_accepts_well_formed_params() {
StreamParams::pcm_48k_stereo().validate().unwrap();
}
#[test]
fn validate_rejects_zero_sample_rate() {
let p = StreamParams::pcm_48k_stereo().with_sample_rate(0);
let err = p.validate().unwrap_err();
assert!(err.to_string().contains("invalid sample rate"));
}
#[test]
fn validate_rejects_zero_channels() {
let p = StreamParams::pcm_48k_stereo().with_channels(0);
let err = p.validate().unwrap_err();
assert!(err.to_string().contains("invalid channel count"));
}
#[test]
fn validate_rejects_zero_fixed_buffer() {
let p = StreamParams::pcm_48k_stereo().with_fixed_buffer(0);
let err = p.validate().unwrap_err();
assert!(err.to_string().contains("buffer too small"));
}
#[test]
fn buffer_size_variants_are_distinct() {
assert_ne!(BufferSize::Default, BufferSize::Fixed(256));
assert_ne!(BufferSize::Fixed(128), BufferSize::Fixed(256));
}
#[test]
fn device_info_new_populates_all_fields() {
let d = DeviceInfo::new(
"/dev/dsp0",
DeviceDirection::Output,
2,
vec![44_100, 48_000],
true,
);
assert_eq!(d.name, "/dev/dsp0");
assert_eq!(d.direction, DeviceDirection::Output);
assert_eq!(d.channels, 2);
assert!(d.is_default);
}
#[test]
fn supports_rate_true_when_listed() {
let d = DeviceInfo::new("d", DeviceDirection::Output, 2, vec![48_000], false);
assert!(d.supports_rate(48_000));
assert!(!d.supports_rate(44_100));
}
#[test]
fn supports_rate_accepts_anything_when_unknown() {
let d = DeviceInfo::new("d", DeviceDirection::Input, 1, vec![], false);
assert!(d.supports_rate(8_000));
assert!(d.supports_rate(192_000));
}
#[test]
fn device_direction_variants_are_distinct() {
assert_ne!(DeviceDirection::Input, DeviceDirection::Output);
assert_ne!(DeviceDirection::Output, DeviceDirection::Duplex);
}
proptest! {
#[test]
fn prop_valid_params_round_trip(
rate in 1u32..=192_000,
channels in 1u16..=32u16,
buf in 1usize..=8192,
) {
let p = StreamParams::pcm_48k_stereo()
.with_sample_rate(rate)
.with_channels(channels)
.with_fixed_buffer(buf);
prop_assert!(p.validate().is_ok());
}
#[test]
fn prop_invalid_params_rejected(channels in 0u16..=32u16) {
let p = StreamParams::pcm_48k_stereo()
.with_sample_rate(0)
.with_channels(channels.max(1));
prop_assert!(p.validate().is_err());
let p2 = StreamParams::pcm_48k_stereo().with_channels(0);
prop_assert!(p2.validate().is_err());
}
}
}