#![cfg(all(target_os = "freebsd", feature = "oss"))]
use std::ffi::CString;
use std::io;
use std::sync::mpsc;
use std::time::Duration;
use audio_core_bsd::AudioFrame;
use crate::backend::{AudioBackend, OutputSink};
use crate::device::{DeviceDirection, DeviceInfo, StreamParams};
use crate::error::{IoError, Result};
use crate::sample_conv;
#[allow(dead_code)]
const SNDCTL_DSP_RESET: libc::c_ulong = 0x2000_5000;
const SNDCTL_DSP_SPEED: libc::c_ulong = 0xc004_5002;
const SNDCTL_DSP_SETFMT: libc::c_ulong = 0xc004_5005;
const SNDCTL_DSP_CHANNELS: libc::c_ulong = 0xc004_5006;
const SNDCTL_DSP_GETFMTS: libc::c_ulong = 0x4004_500b;
const SNDCTL_DSP_SETFRAGMENT: libc::c_ulong = 0xc004_500a;
#[allow(dead_code)]
const SNDCTL_DSP_GETOSPACE: libc::c_ulong = 0x4010_500c;
const AFMT_FLOAT: i32 = 0x1000_0000;
const AFMT_S32_LE: i32 = 0x0000_1000;
const AFMT_S16_LE: i32 = 0x0000_0010;
#[allow(dead_code)]
#[repr(C)]
#[derive(Default, Clone, Copy, Debug)]
struct AudioBufInfo {
fragments: i32,
fragstotal: i32,
fragsize: i32,
bytes: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OssFormat {
Float,
S32Le,
S16Le,
}
impl OssFormat {
const fn afmt(self) -> i32 {
match self {
OssFormat::Float => AFMT_FLOAT,
OssFormat::S32Le => AFMT_S32_LE,
OssFormat::S16Le => AFMT_S16_LE,
}
}
const fn bytes_per_sample(self) -> usize {
match self {
OssFormat::Float | OssFormat::S32Le => 4,
OssFormat::S16Le => 2,
}
}
}
#[must_use]
fn negotiate_format(supported_mask: i32) -> Option<OssFormat> {
for (bit, fmt) in [
(AFMT_FLOAT, OssFormat::Float),
(AFMT_S32_LE, OssFormat::S32Le),
(AFMT_S16_LE, OssFormat::S16Le),
] {
if supported_mask & bit == bit {
return Some(fmt);
}
}
None
}
#[must_use]
fn encode_fragment(num_frags: i32, frag_log2: i32) -> i32 {
let n = num_frags.clamp(1, 0x7FFF);
let s = frag_log2.clamp(4, 0xFFFF);
(n << 0x10) | (s & 0xFFFF)
}
#[allow(clippy::cast_precision_loss)]
#[must_use]
pub fn fragment_latency_ms(
num_frags: i32,
frag_log2: i32,
channels: u16,
bytes_per_sample: usize,
sample_rate: u32,
) -> f64 {
let ch = channels.max(1) as usize;
let bps = bytes_per_sample.max(1);
let bytes_per_frag = f64::from(1_u32 << u32::try_from(frag_log2.max(0)).unwrap_or(0));
let frames_per_frag = bytes_per_frag / (ch * bps) as f64;
let total_frames = f64::from(num_frags) * frames_per_frag;
total_frames / f64::from(sample_rate) * 1000.0
}
unsafe fn dsp_ioctl_ptr<T>(fd: libc::c_int, req: libc::c_ulong, arg: *mut T) -> Result<()> {
let rc = libc::ioctl(fd, req, arg.cast::<libc::c_void>());
if rc < 0 {
Err(io::Error::last_os_error().into())
} else {
Ok(())
}
}
unsafe fn dsp_ioctl_int(fd: libc::c_int, req: libc::c_ulong, mut value: i32) -> Result<i32> {
let rc = libc::ioctl(fd, req, core::ptr::addr_of_mut!(value));
if rc < 0 {
Err(io::Error::last_os_error().into())
} else {
Ok(value)
}
}
pub struct OssBackend;
impl OssBackend {
#[must_use]
pub const fn new() -> Self {
Self
}
const DEFAULT_DEV: &'static str = "/dev/dsp";
}
impl Default for OssBackend {
fn default() -> Self {
Self::new()
}
}
impl AudioBackend for OssBackend {
fn enumerate_devices(&self) -> Vec<DeviceInfo> {
let mut devs = Vec::new();
devs.push(DeviceInfo::new(
Self::DEFAULT_DEV,
DeviceDirection::Duplex,
2,
vec![44_100, 48_000, 96_000],
true,
));
for unit in 1..=8 {
let path = format!("/dev/dsp{unit}");
if std::path::Path::new(&path).exists() {
devs.push(DeviceInfo::new(
path,
DeviceDirection::Duplex,
2,
vec![44_100, 48_000, 96_000],
false,
));
}
}
devs
}
fn default_output(&self) -> Option<DeviceInfo> {
Some(DeviceInfo::new(
Self::DEFAULT_DEV,
DeviceDirection::Output,
2,
vec![44_100, 48_000, 96_000],
true,
))
}
fn default_input(&self) -> Option<DeviceInfo> {
None
}
fn open_output(&self, dev: &str, params: StreamParams) -> Result<Box<dyn OutputSink>> {
params.validate()?;
let cdev = CString::new(dev).map_err(|e| IoError::DeviceNotFound(format!("{dev}: {e}")))?;
let fd = unsafe { libc::open(cdev.as_ptr(), libc::O_WRONLY | libc::O_CLOEXEC) };
if fd < 0 {
let e = io::Error::last_os_error();
return match e.raw_os_error() {
Some(libc::ENOENT | libc::ENXIO) => Err(IoError::DeviceNotFound(dev.into())),
_ => Err(IoError::Backend(format!("open {dev}: {e}"))),
};
}
let fmt = configure_output(fd, params)?;
let (producer, consumer) = rtrb::RingBuffer::<f32>::new(RING_CAPACITY);
let (drop_tx, drop_rx) = mpsc::channel::<()>();
let (ready_tx, ready_rx) = mpsc::channel::<Result<()>>();
let channels = params.channels;
let sample_rate = params.sample_rate;
std::thread::Builder::new()
.name("audio-io-oss-output".into())
.spawn(move || {
run_output_thread(fd, consumer, channels, sample_rate, fmt, ready_tx, drop_rx);
})
.map_err(|e| IoError::Backend(format!("spawn oss output thread: {e}")))?;
ready_rx
.recv()
.map_err(|e| IoError::StreamSetup(format!("oss output thread panicked: {e}")))??;
Ok(Box::new(OssSink {
producer,
scratch: Vec::with_capacity(4 * channels as usize),
channels,
drop_tx: Some(drop_tx),
}))
}
fn open_input(
&self,
_dev: &str,
_params: StreamParams,
) -> Result<Box<dyn crate::backend::InputSource>> {
Err(IoError::UnsupportedConfig(
"OSS capture (InputSource) is not yet implemented".into(),
))
}
}
const RING_CAPACITY: usize = 1 << 16;
fn configure_output(fd: libc::c_int, params: StreamParams) -> Result<OssFormat> {
let mut fmts = 0_i32;
unsafe { dsp_ioctl_ptr(fd, SNDCTL_DSP_GETFMTS, core::ptr::addr_of_mut!(fmts)) }?;
let chosen = negotiate_format(fmts)
.ok_or_else(|| IoError::UnsupportedConfig("no supported OSS sample format".into()))?;
let readback = unsafe { dsp_ioctl_int(fd, SNDCTL_DSP_SETFMT, chosen.afmt()) }?;
if readback != chosen.afmt() {
return Err(IoError::UnsupportedConfig(format!(
"driver rejected format {chosen:?} (readback 0x{readback:x})"
)));
}
let mut chans = i32::from(params.channels);
let got_chans = unsafe { dsp_ioctl_int(fd, SNDCTL_DSP_CHANNELS, chans) }?;
if got_chans != chans {
if !(1..=8).contains(&got_chans) {
return Err(IoError::UnsupportedConfig(format!(
"driver returned {got_chans} channels (requested {chans})"
)));
}
chans = got_chans;
}
let rate = i32::try_from(params.sample_rate).unwrap_or(i32::MAX);
let got_rate = unsafe { dsp_ioctl_int(fd, SNDCTL_DSP_SPEED, rate) }?;
if got_rate > 0 && (got_rate - rate).abs() > rate / 100 {
return Err(IoError::UnsupportedConfig(format!(
"driver returned {got_rate} Hz (requested {rate})"
)));
}
let ch_us = usize::try_from(chans).unwrap_or(0);
let frag_log2 = compute_fragment_log2(ch_us, chosen.bytes_per_sample(), ¶ms);
let frag_arg = encode_fragment(4, frag_log2);
let _ = unsafe { dsp_ioctl_int(fd, SNDCTL_DSP_SETFRAGMENT, frag_arg) };
Ok(chosen)
}
#[allow(clippy::needless_pass_by_value)]
fn compute_fragment_log2(channels: usize, bytes_per_sample: usize, params: &StreamParams) -> i32 {
let target_frames = match params.buffer_size {
crate::device::BufferSize::Fixed(n) => n.max(64),
crate::device::BufferSize::Default => 256,
};
let target_bytes = target_frames * channels.max(1) * bytes_per_sample.max(1);
let mut log2 = 4_i32;
while (1 << log2) < target_bytes && log2 < 16 {
log2 += 1;
}
log2
}
#[allow(clippy::needless_pass_by_value)]
fn run_output_thread(
fd: libc::c_int,
mut consumer: rtrb::Consumer<f32>,
channels: u16,
_sample_rate: u32,
fmt: OssFormat,
ready_tx: mpsc::Sender<Result<()>>,
drop_rx: mpsc::Receiver<()>,
) {
let _ = ready_tx.send(Ok(()));
let ch = channels.max(1) as usize;
let bps = fmt.bytes_per_sample();
let mut f32_buf: Vec<f32> = Vec::with_capacity(1024);
let mut pcm_bytes: Vec<u8> = Vec::with_capacity(4096);
while let Err(mpsc::TryRecvError::Empty) = drop_rx.try_recv() {
f32_buf.clear();
while let Ok(s) = consumer.pop() {
f32_buf.push(s);
}
if f32_buf.is_empty() {
std::thread::sleep(Duration::from_millis(1));
continue;
}
let n = (f32_buf.len() / ch) * ch;
if n == 0 {
continue;
}
let needed = n * bps;
if pcm_bytes.len() < needed {
pcm_bytes.resize(needed, 0);
}
match fmt {
OssFormat::Float => {
for (i, &v) in f32_buf.iter().take(n).enumerate() {
pcm_bytes[i * 4..i * 4 + 4].copy_from_slice(&v.to_le_bytes());
}
}
OssFormat::S32Le => {
let mut tmp = vec![0_i32; n];
sample_conv::f32_interleaved_to_s32(&f32_buf[..n], &mut tmp);
sample_conv::s32_interleaved_to_le_bytes(&tmp, &mut pcm_bytes[..needed]);
}
OssFormat::S16Le => {
let mut tmp = vec![0_i16; n];
sample_conv::f32_interleaved_to_s16(&f32_buf[..n], &mut tmp);
sample_conv::s16_interleaved_to_le_bytes(&tmp, &mut pcm_bytes[..needed]);
}
}
let mut off = 0;
while off < needed {
let wr = unsafe {
libc::write(
fd,
pcm_bytes[off..].as_ptr().cast::<libc::c_void>(),
needed - off,
)
};
if wr < 0 {
let e = io::Error::last_os_error();
if e.kind() == io::ErrorKind::WouldBlock {
break;
}
break;
}
off += usize::try_from(wr).unwrap_or(0);
if wr == 0 {
break;
}
}
}
unsafe { libc::close(fd) };
}
pub struct OssSink {
producer: rtrb::Producer<f32>,
scratch: Vec<f32>,
channels: u16,
drop_tx: Option<mpsc::Sender<()>>,
}
impl OutputSink for OssSink {
fn write(&mut self, frame: &AudioFrame) -> Result<()> {
let n = frame.num_frames();
let ch = (self.channels.min(frame.channels)) as usize;
if ch == 0 {
return Ok(());
}
self.scratch.clear();
self.scratch.reserve(n * ch);
for i in 0..n {
for c in 0..ch {
let v = frame.channel_slice(c).get(i).copied().unwrap_or(0.0);
self.scratch.push(v);
}
}
for &s in &self.scratch {
if self.producer.push(s).is_err() {
return Err(IoError::StreamSetup("oss ring full (back-pressure)".into()));
}
}
Ok(())
}
}
impl Drop for OssSink {
fn drop(&mut self) {
self.drop_tx.take();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn negotiate_prefers_float_then_s32_then_s16() {
assert_eq!(
negotiate_format(AFMT_FLOAT | AFMT_S32_LE | AFMT_S16_LE),
Some(OssFormat::Float)
);
assert_eq!(
negotiate_format(AFMT_S32_LE | AFMT_S16_LE),
Some(OssFormat::S32Le)
);
assert_eq!(negotiate_format(AFMT_S16_LE), Some(OssFormat::S16Le));
assert_eq!(negotiate_format(0), None);
}
#[test]
fn format_afmt_and_width() {
assert_eq!(OssFormat::Float.afmt(), AFMT_FLOAT);
assert_eq!(OssFormat::S32Le.afmt(), AFMT_S32_LE);
assert_eq!(OssFormat::S16Le.afmt(), AFMT_S16_LE);
assert_eq!(OssFormat::S16Le.bytes_per_sample(), 2);
assert_eq!(OssFormat::S32Le.bytes_per_sample(), 4);
}
#[test]
fn encode_fragment_packs_count_and_log2() {
let arg = encode_fragment(4, 10);
assert_eq!(arg, (4 << 0x10) | 0xA);
}
#[test]
fn encode_fragment_clamps_extremes() {
let arg = encode_fragment(0, 2);
assert_eq!(arg, (1 << 0x10) | 0x4);
}
#[test]
fn fragment_latency_matches_known_case() {
let ms = fragment_latency_ms(4, 10, 2, 2, 48_000);
assert!((ms - 21.33).abs() < 0.1, "got {ms} ms");
}
#[test]
fn fragment_latency_doubles_with_fragment_count() {
let one = fragment_latency_ms(2, 10, 2, 2, 48_000);
let two = fragment_latency_ms(4, 10, 2, 2, 48_000);
assert!((two - 2.0 * one).abs() < 0.01);
}
#[test]
fn oss_backend_enumerates_default_device() {
let b = OssBackend::new();
let devs = b.enumerate_devices();
assert!(devs.iter().any(|d| d.name == "/dev/dsp" && d.is_default));
}
#[test]
fn oss_backend_has_default_output() {
let b = OssBackend::new();
let dev = b.default_output().unwrap();
assert_eq!(dev.name, "/dev/dsp");
}
#[test]
fn oss_backend_open_output_unknown_device_is_not_found() {
let b = OssBackend::new();
let err = b
.open_output("/dev/dsp_nonexistent_unit", StreamParams::pcm_48k_stereo())
.err()
.unwrap();
assert!(err.to_string().contains("device not found") || err.to_string().contains("open"));
}
#[test]
fn open_input_reports_unsupported() {
let b = OssBackend::new();
let err = b
.open_input("/dev/dsp", StreamParams::pcm_48k_mono())
.err()
.unwrap();
assert!(err.to_string().contains("not yet implemented"));
}
#[test]
fn open_output_validates_params() {
let b = OssBackend::new();
let err = b
.open_output("/dev/dsp", StreamParams::pcm_48k_stereo().with_channels(0))
.err()
.unwrap();
assert!(err.to_string().contains("invalid channel count"));
}
#[test]
fn shutdown_signal_disconnects_on_drop() {
let (drop_tx, drop_rx) = mpsc::channel::<()>();
assert_eq!(drop_rx.try_recv(), Err(mpsc::TryRecvError::Empty));
drop(drop_tx);
assert_eq!(drop_rx.try_recv(), Err(mpsc::TryRecvError::Disconnected));
}
}