use crate::audio::io::AudioIO;
use crate::hw::convert_policy;
use nix::libc;
use std::{
fs::File,
os::{fd::AsRawFd, unix::fs::OpenOptionsExt},
sync::{Arc, Mutex},
};
pub use super::midi_hub::MidiHub;
mod audio_core;
mod channel;
mod consts;
mod convert;
mod driver;
mod io_util;
mod ioctl;
mod sync;
pub use self::channel::OSSChannel;
pub use self::consts::*;
pub use self::driver::HwDriver;
pub use self::ioctl::{AudioInfo, BufferInfo, add_to_sync_group, start_sync_group};
pub use crate::hw::options::HwOptions;
use self::audio_core::DoubleBufferedChannel;
use self::convert::*;
use self::io_util::*;
use self::ioctl::*;
use self::sync::{DuplexSync, FrameClock, get_or_create_duplex_sync};
#[cfg(target_endian = "little")]
const AFMT_S16_FOREIGN: u32 = AFMT_S16_BE;
#[cfg(target_endian = "big")]
const AFMT_S16_FOREIGN: u32 = AFMT_S16_LE;
#[cfg(target_endian = "little")]
const AFMT_S24_FOREIGN: u32 = AFMT_S24_BE;
#[cfg(target_endian = "big")]
const AFMT_S24_FOREIGN: u32 = AFMT_S24_LE;
#[cfg(target_endian = "little")]
const AFMT_S32_FOREIGN: u32 = AFMT_S32_BE;
#[cfg(target_endian = "big")]
const AFMT_S32_FOREIGN: u32 = AFMT_S32_LE;
#[derive(Debug)]
pub struct Audio {
dsp: File,
pub channels: Vec<Arc<AudioIO>>,
pub input: bool,
pub output_gain_linear: f32,
pub output_balance: f32,
pub rate: i32,
pub format: u32,
pub chsamples: usize,
buffer: Vec<i32>,
f32_buffer: Vec<f32>,
pub buffer_info: BufferInfo,
frame_size_bytes: usize,
fragment_bytes: usize,
buffer_frames_cached: i64,
caps: i32,
mapped: bool,
map: *mut libc::c_void,
map_progress_bytes: usize,
last_published_balance: i64,
frame_clock: FrameClock,
frame_stamp: i64,
duplex_sync: Arc<Mutex<DuplexSync>>,
channel: DoubleBufferedChannel,
last_underrun_count: i32,
last_overrun_count: i32,
xrun_count: u64,
playing: Arc<std::sync::atomic::AtomicBool>,
was_playing_last_cycle: bool,
stop_fade_remaining_frames: usize,
stop_fade_total_frames: usize,
}
impl Audio {
fn sample_format_candidates(bits: i32) -> Vec<u32> {
fn add_pair(candidates: &mut Vec<u32>, native: u32, foreign: u32) {
candidates.push(native);
candidates.push(foreign);
}
let mut candidates = Vec::with_capacity(7);
match bits {
32 => {
add_pair(&mut candidates, AFMT_S32_NE, AFMT_S32_FOREIGN);
add_pair(&mut candidates, AFMT_S24_NE, AFMT_S24_FOREIGN);
add_pair(&mut candidates, AFMT_S16_NE, AFMT_S16_FOREIGN);
candidates.push(AFMT_S8);
}
24 => {
add_pair(&mut candidates, AFMT_S24_NE, AFMT_S24_FOREIGN);
add_pair(&mut candidates, AFMT_S16_NE, AFMT_S16_FOREIGN);
candidates.push(AFMT_S8);
}
16 => {
add_pair(&mut candidates, AFMT_S16_NE, AFMT_S16_FOREIGN);
candidates.push(AFMT_S8);
}
8 => candidates.push(AFMT_S8),
_ => {
add_pair(&mut candidates, AFMT_S16_NE, AFMT_S16_FOREIGN);
candidates.push(AFMT_S8);
}
}
candidates
}
fn negotiate_sample_format(fd: i32, bits: i32) -> Result<u32, std::io::Error> {
let candidates = Self::sample_format_candidates(bits);
let mut last_errno = None;
let mut last_unsupported = None;
for candidate in candidates {
let mut negotiated = candidate;
let setfmt = unsafe { oss_set_format(fd, &mut negotiated) };
match setfmt {
Ok(_) => {
if supported_sample_format(negotiated) {
return Ok(negotiated);
}
last_unsupported = Some(negotiated);
}
Err(_) => {
last_errno = Some(std::io::Error::last_os_error());
}
}
}
if let Some(format) = last_unsupported {
return Err(std::io::Error::other(format!(
"Unsupported OSS sample format after setfmt fallback chain: {format:#x}"
)));
}
Err(last_errno
.unwrap_or_else(|| std::io::Error::other("OSS setfmt failed for all fallback formats")))
}
fn min_fragment_bytes(frame_size: usize) -> Result<usize, std::io::Error> {
const MAX_FRAGMENT_BYTES: usize = 1 << 16;
if frame_size == 0 {
return Err(std::io::Error::other("OSS frame size is invalid"));
}
if frame_size > MAX_FRAGMENT_BYTES {
return Err(std::io::Error::other(format!(
"OSS frame size {frame_size} exceeds maximum fragment size {MAX_FRAGMENT_BYTES}"
)));
}
Ok(frame_size.next_power_of_two().min(MAX_FRAGMENT_BYTES))
}
fn request_fragment_layout(
fd: i32,
frame_size: usize,
period_frames: usize,
) -> std::io::Result<usize> {
let fragment_bytes = Self::min_fragment_bytes(frame_size)?;
let period_bytes =
Self::period_bytes_for_fragment_grid(frame_size, period_frames, fragment_bytes)?;
let fragments = period_bytes.div_ceil(fragment_bytes).max(1);
let exponent = fragment_bytes.trailing_zeros();
let arg_bits = ((fragments.min(0xffff) as u32) << 16) | exponent;
let mut arg = arg_bits as i32;
if unsafe { oss_set_fragment(fd, &mut arg) }.is_err() {
tracing::warn!(
"OSS: failed to request {} byte fragments: {}",
fragment_bytes,
std::io::Error::last_os_error()
);
}
Ok(fragment_bytes)
}
fn period_bytes_for_fragment_grid(
frame_size: usize,
period_frames: usize,
min_bytes: usize,
) -> std::io::Result<usize> {
let requested_bytes = period_frames
.max(1)
.checked_mul(frame_size)
.ok_or_else(|| std::io::Error::other("OSS period byte size overflow"))?;
let min_bytes = min_bytes.max(frame_size).max(1);
requested_bytes
.max(min_bytes)
.checked_next_power_of_two()
.ok_or_else(|| std::io::Error::other("OSS period byte size overflow"))
}
fn period_frames_for_fragment_grid(
frame_size: usize,
period_frames: usize,
min_bytes: usize,
) -> std::io::Result<usize> {
let bytes = Self::period_bytes_for_fragment_grid(frame_size, period_frames, min_bytes)?;
Ok(bytes.div_ceil(frame_size).max(1))
}
pub fn fd(&self) -> i32 {
self.dsp.as_raw_fd()
}
pub fn start_trigger(&self) -> std::io::Result<()> {
if (self.caps & PCM_CAP_TRIGGER) == 0 {
return Ok(());
}
let trig: i32 = if self.input {
PCM_ENABLE_INPUT
} else {
PCM_ENABLE_OUTPUT
};
unsafe { oss_set_trigger(self.dsp.as_raw_fd(), &trig) }
.map(|_| ())
.map_err(|_| std::io::Error::last_os_error())
}
pub fn stop_trigger(&self) -> std::io::Result<()> {
if (self.caps & PCM_CAP_TRIGGER) == 0 {
return Ok(());
}
let trig: i32 = 0;
unsafe { oss_set_trigger(self.dsp.as_raw_fd(), &trig) }
.map(|_| ())
.map_err(|_| std::io::Error::last_os_error())
}
pub fn new(
path: &str,
sync_key: &str,
rate: i32,
bits: i32,
input: bool,
options: HwOptions,
playing: Arc<std::sync::atomic::AtomicBool>,
) -> Result<Audio, std::io::Error> {
let mut binding = File::options();
let mut flags = libc::O_NONBLOCK;
if input {
flags |= libc::O_RDONLY;
if options.exclusive {
flags |= libc::O_EXCL;
}
binding.read(true).write(false).custom_flags(flags);
} else {
flags |= libc::O_WRONLY;
if options.exclusive {
flags |= libc::O_EXCL;
}
binding.read(false).write(true).custom_flags(flags);
}
let dsp = binding.open(path)?;
let cooked = 0_i32;
unsafe {
let _ = oss_set_cooked(dsp.as_raw_fd(), &cooked);
}
let mut audio_info = AudioInfo::new();
unsafe {
oss_get_info(dsp.as_raw_fd(), &mut audio_info)
.map_err(|_| std::io::Error::last_os_error())?;
}
let mut channels = if audio_info.max_channels > 0 {
audio_info.max_channels
} else {
2_i32
};
let mut effective_rate = rate;
let format = Self::negotiate_sample_format(dsp.as_raw_fd(), bits)?;
unsafe {
oss_set_channels(dsp.as_raw_fd(), &mut channels)
.map_err(|_| std::io::Error::last_os_error())?;
oss_set_speed(dsp.as_raw_fd(), &mut effective_rate)
.map_err(|_| std::io::Error::last_os_error())?;
}
if effective_rate != rate {
return Err(std::io::Error::other(format!(
"OSS device forced sample rate {effective_rate} (requested {rate})"
)));
}
let bytes_per_sample = bytes_per_sample(format)
.ok_or_else(|| std::io::Error::other(format!("Unsupported format: {format:#x}")))?;
let frame_size = (channels as usize) * bytes_per_sample;
let requested_fragment_bytes =
Self::request_fragment_layout(dsp.as_raw_fd(), frame_size, options.period_frames)?;
let mut buffer_info = BufferInfo::new();
unsafe {
if input {
oss_input_buffer_info(dsp.as_raw_fd(), &mut buffer_info)
.map_err(|_| std::io::Error::last_os_error())?;
} else {
oss_output_buffer_info(dsp.as_raw_fd(), &mut buffer_info)
.map_err(|_| std::io::Error::last_os_error())?;
}
}
if buffer_info.fragments < 1 {
buffer_info.fragments = buffer_info.fragstotal;
}
if buffer_info.bytes < 1 {
buffer_info.bytes = buffer_info.fragstotal * buffer_info.fragsize;
}
if buffer_info.bytes < 1 {
return Err(std::io::Error::other("OSS buffer size is invalid"));
}
let mut caps = 0_i32;
unsafe {
oss_get_caps(dsp.as_raw_fd(), &mut caps)
.map_err(|_| std::io::Error::last_os_error())?;
}
let mut sys = OssSysInfo::default();
unsafe {
oss_get_sysinfo(dsp.as_raw_fd(), &mut sys)
.map_err(|_| std::io::Error::last_os_error())?;
}
if (caps & PCM_CAP_MMAP) != 0 {
let ver = cstr_fixed_prefix(&sys.version);
if ver.len() >= 7 && ver.as_bytes()[..7].cmp(b"1302000") == std::cmp::Ordering::Less {
caps &= !PCM_CAP_MMAP;
}
}
let ring_total_frames = if frame_size > 0 && buffer_info.bytes > 0 {
(buffer_info.bytes as usize) / frame_size
} else {
0
};
let frag_frames = if frame_size > 0 && buffer_info.fragsize > 0 {
(buffer_info.fragsize as usize).div_ceil(frame_size)
} else {
0
};
let chsamples = Self::period_frames_for_fragment_grid(
frame_size,
options.period_frames,
buffer_info.fragsize.max(1) as usize,
)?;
tracing::info!(
"OSS {}: requested {} byte fragments, got {} frags x {} bytes ({} frames each), ring {} frames, engine period {} frames",
if input { "capture" } else { "playback" },
requested_fragment_bytes,
buffer_info.fragstotal,
buffer_info.fragsize,
frag_frames,
ring_total_frames,
chsamples,
);
if frag_frames > 0 {
let direction = if input { "capture" } else { "playback" };
if chsamples % frag_frames != 0 {
tracing::info!(
"OSS {}: engine period ({} frames) is not a multiple of OSS fragment size ({} frames)",
direction,
chsamples,
frag_frames,
);
}
}
let buffer_bytes = chsamples * frame_size;
let channel = if input {
DoubleBufferedChannel::new_read(buffer_bytes, chsamples as i64)
} else {
DoubleBufferedChannel::new_write(buffer_bytes, chsamples as i64)
};
let mut map = std::ptr::null_mut();
let mut mapped = false;
if (caps & PCM_CAP_MMAP) != 0 {
let prot = if input {
libc::PROT_READ
} else {
libc::PROT_WRITE
};
let addr = unsafe {
libc::mmap(
std::ptr::null_mut(),
buffer_info.bytes as usize,
prot,
libc::MAP_SHARED,
dsp.as_raw_fd(),
0,
)
};
if addr != libc::MAP_FAILED {
map = addr;
mapped = true;
}
}
let mut io_channels = Vec::with_capacity(channels as usize);
for _ in 0..channels {
io_channels.push(Arc::new(AudioIO::new(chsamples)));
}
let duplex_sync = get_or_create_duplex_sync(sync_key, effective_rate, chsamples);
let mut frame_clock = FrameClock::default();
frame_clock.set_sample_rate(effective_rate as u32);
{
let mut sync = duplex_sync.lock().expect("duplex sync poisoned");
if let Some(zero) = sync.clock_zero {
frame_clock.zero = zero;
} else {
let _ = frame_clock.init_clock(effective_rate as u32);
sync.clock_zero = Some(frame_clock.zero);
}
}
let buffer_frames_cached = (buffer_info.bytes as usize / frame_size) as i64;
let fragment_bytes = buffer_info.fragsize.max(1) as usize;
let mut initial_audio = Audio {
dsp,
channels: io_channels,
input,
output_gain_linear: 1.0,
output_balance: 0.0,
rate: effective_rate,
format,
chsamples,
buffer: vec![0_i32; chsamples * (channels as usize)],
f32_buffer: Vec::new(),
buffer_info,
frame_size_bytes: frame_size,
fragment_bytes,
buffer_frames_cached,
caps,
mapped,
map,
map_progress_bytes: 0,
last_published_balance: i64::MIN,
frame_clock,
frame_stamp: 0,
duplex_sync,
channel,
last_underrun_count: 0,
last_overrun_count: 0,
xrun_count: 0,
playing,
was_playing_last_cycle: false,
stop_fade_remaining_frames: 0,
stop_fade_total_frames: 0,
};
initial_audio.last_underrun_count = initial_audio.get_play_underruns();
initial_audio.last_overrun_count = initial_audio.get_rec_overruns();
Ok(initial_audio)
}
fn frame_size(&self) -> usize {
self.frame_size_bytes
}
pub fn frame_size_bytes(&self) -> usize {
self.frame_size_bytes
}
pub fn sample_bits(&self) -> i32 {
bytes_per_sample(self.format)
.map(|bytes| (bytes * 8) as i32)
.unwrap_or(0)
}
fn buffer_frames(&self) -> i64 {
self.buffer_frames_cached
}
fn io_chunk_bytes(&self) -> usize {
let aligned = self.fragment_bytes - (self.fragment_bytes % self.frame_size());
aligned.max(self.frame_size())
}
fn stepping(&self) -> i64 {
self.frame_clock.stepping()
}
fn map_pointer(&self) -> usize {
if self.buffer_info.bytes <= 0 {
return 0;
}
self.map_progress_bytes % (self.buffer_info.bytes as usize)
}
fn shared_cycle_end_add(&self, delta: i64) -> i64 {
let mut sync = self.duplex_sync.lock().expect("duplex sync poisoned");
sync.cycle_end += delta;
sync.cycle_end
}
fn shared_cycle_end_get(&self) -> i64 {
self.duplex_sync
.lock()
.expect("duplex sync poisoned")
.cycle_end
}
fn publish_balance(&mut self, balance: i64) {
if self.last_published_balance == balance {
return;
}
self.last_published_balance = balance;
let mut sync = self.duplex_sync.lock().expect("duplex sync poisoned");
if self.input {
sync.capture_balance = Some(balance);
} else {
sync.playback_balance = Some(balance);
}
}
fn playback_correction(&self) -> i64 {
if self.input {
return 0;
}
let mut sync = self.duplex_sync.lock().expect("duplex sync poisoned");
match (sync.playback_balance, sync.capture_balance) {
(Some(play), Some(capture)) => sync.correction.correct(play, capture),
_ => 0,
}
}
fn update_map_progress_from_count(&mut self, info: &CountInfo) -> Option<usize> {
if self.buffer_info.bytes <= 0
|| self.buffer_info.fragsize <= 0
|| info.ptr < 0
|| info.blocks < 0
|| (info.ptr as usize) >= self.buffer_info.bytes as usize
|| !(info.ptr as usize).is_multiple_of(self.frame_size())
{
return None;
}
let buf_bytes = self.buffer_info.bytes as usize;
let frag_bytes = self.buffer_info.fragsize as usize;
let ptr = info.ptr as usize;
let mut delta = (ptr + buf_bytes - self.map_pointer()) % buf_bytes;
let max_bytes = ((info.blocks as usize).saturating_add(1))
.saturating_mul(frag_bytes)
.saturating_sub(1);
if max_bytes >= delta {
let mut cycles = max_bytes - delta;
cycles -= cycles % buf_bytes;
delta += cycles;
}
self.map_progress_bytes = self.map_progress_bytes.saturating_add(delta);
Some(delta)
}
fn read_io(&self, dst: &mut [u8], len: usize, count: &mut usize) -> std::io::Result<()> {
read_nonblock(self.dsp.as_raw_fd(), dst, len, self.io_chunk_bytes(), count)
}
fn write_io(&self, src: &mut [u8], len: usize, count: &mut usize) -> std::io::Result<()> {
write_nonblock(self.dsp.as_raw_fd(), src, len, self.io_chunk_bytes(), count)
}
fn read_map(&self, dst: &mut [u8], offset: usize, length: usize) -> usize {
let total = self.buffer_info.bytes.max(0) as usize;
map_read(self.map, self.mapped, total, dst, offset, length)
}
fn write_map(&self, src: Option<&mut [u8]>, offset: usize, length: usize) -> usize {
let total = self.buffer_info.bytes.max(0) as usize;
map_write(self.map, self.mapped, total, src, offset, length)
}
fn queued_samples(&self) -> i32 {
let mut ptr = OssCount::default();
let req = if self.input {
unsafe { oss_current_iptr(self.dsp.as_raw_fd(), &mut ptr) }
} else {
unsafe { oss_current_optr(self.dsp.as_raw_fd(), &mut ptr) }
};
if req.is_ok() { ptr.fifo_samples } else { 0 }
}
fn get_play_underruns(&self) -> i32 {
let mut err = AudioErrInfo::default();
let rc = unsafe { oss_get_error(self.dsp.as_raw_fd(), &mut err) };
if rc.is_ok() { err.play_underruns } else { 0 }
}
fn get_rec_overruns(&self) -> i32 {
let mut err = AudioErrInfo::default();
let rc = unsafe { oss_get_error(self.dsp.as_raw_fd(), &mut err) };
if rc.is_ok() { err.rec_overruns } else { 0 }
}
pub(super) fn detect_xrun_enhanced(&mut self) -> i64 {
let current_underruns = if self.input {
0
} else {
self.get_play_underruns()
};
let current_overruns = if self.input {
self.get_rec_overruns()
} else {
0
};
if (self.last_underrun_count >= 0 || self.last_overrun_count >= 0)
&& (current_underruns > self.last_underrun_count
|| current_overruns > self.last_overrun_count)
{
self.xrun_count += 1;
let delta_underruns = current_underruns - self.last_underrun_count;
let delta_overruns = current_overruns - self.last_overrun_count;
self.last_underrun_count = current_underruns;
self.last_overrun_count = current_overruns;
tracing::warn!(
"OSS hardware xrun detected (#{}, underruns +{}, overruns +{})",
self.xrun_count,
delta_underruns,
delta_overruns
);
return self.chsamples as i64;
}
self.last_underrun_count = current_underruns;
self.last_overrun_count = current_overruns;
0
}
pub fn process(&mut self) {
let num_channels = self.channels.len();
let all_connected = self
.channels
.iter()
.all(crate::hw::ports::has_audio_connections);
if self.input {
let norm_factor = convert_policy::F32_FROM_I32_MAX;
let total_samples = self.chsamples * num_channels;
self.f32_buffer.resize(total_samples, 0.0);
crate::simd::convert_i32_to_f32(
&self.buffer[..total_samples],
&mut self.f32_buffer,
norm_factor,
);
crate::hw::ports::fill_ports_from_interleaved_buffer(
&self.channels,
self.chsamples,
!all_connected,
&self.f32_buffer,
num_channels,
);
} else {
let playing = self.playing.load(std::sync::atomic::Ordering::Relaxed);
if self.was_playing_last_cycle && !playing {
let fade_frames = self.chsamples.max(128);
self.stop_fade_remaining_frames = fade_frames;
self.stop_fade_total_frames = fade_frames;
}
self.was_playing_last_cycle = playing;
let data_i32 = self.buffer.as_mut_slice();
if !playing && self.stop_fade_remaining_frames == 0 {
data_i32.fill(0);
} else {
let scale_factor = convert_policy::F32_TO_I32_MAX;
let output_gain = self.output_gain_linear;
if !all_connected {
data_i32.fill(0);
}
let fade_remaining = self.stop_fade_remaining_frames;
let fade_total = self.stop_fade_total_frames.max(1);
crate::hw::ports::write_interleaved_from_ports(
&self.channels,
self.chsamples,
output_gain,
self.output_balance,
!all_connected,
|ch_idx, frame, sample| {
let target_idx = frame * num_channels + ch_idx;
let fade_gain = if !playing && fade_remaining > 0 {
let progressed = self.chsamples.saturating_sub(fade_remaining) + frame;
(1.0 - (progressed as f32 / fade_total as f32)).clamp(0.0, 1.0)
} else {
1.0
};
data_i32[target_idx] =
(sample.clamp(-1.0, 1.0) * fade_gain * scale_factor) as i32;
},
);
if !playing && self.stop_fade_remaining_frames > 0 {
self.stop_fade_remaining_frames = self
.stop_fade_remaining_frames
.saturating_sub(self.chsamples);
}
}
}
}
pub fn force_silence_now(&mut self) {
if self.input {
return;
}
self.buffer.fill(0);
for ch in &self.channels {
ch.buffer.lock().fill(0.0);
}
self.stop_fade_remaining_frames = 0;
self.stop_fade_total_frames = 0;
self.channel
.reset_buffers(self.frame_stamp, self.frame_size().max(1));
}
}
impl Drop for Audio {
fn drop(&mut self) {
if self.mapped && !self.map.is_null() && self.buffer_info.bytes > 0 {
unsafe {
let _ = libc::munmap(self.map, self.buffer_info.bytes as usize);
}
self.map = std::ptr::null_mut();
}
}
}