mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
86704473ed
Improve normalization consistency by separating cache refresh for non-playing tracks, hardening track-id cache lookups, and keeping UI gain state aligned with actual playback. Also reduce startup stalls by preferring non-seekable streaming when format hints are missing and by tuning ALSA/analysis paths to avoid underrun-prone churn.
1165 lines
38 KiB
Rust
1165 lines
38 KiB
Rust
extern crate alsa;
|
|
extern crate libc;
|
|
|
|
use self::alsa::poll::Descriptors;
|
|
use crate::traits::{DeviceTrait, HostTrait, StreamTrait};
|
|
use crate::{
|
|
BackendSpecificError, BufferSize, BuildStreamError, ChannelCount, Data,
|
|
DefaultStreamConfigError, DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo,
|
|
PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError,
|
|
SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange,
|
|
SupportedStreamConfigsError,
|
|
};
|
|
use std::cmp;
|
|
use std::convert::TryInto;
|
|
use std::sync::{Arc, Mutex};
|
|
use std::thread::{self, JoinHandle};
|
|
use std::time::Duration;
|
|
use std::vec::IntoIter as VecIntoIter;
|
|
|
|
pub use self::enumerate::{default_input_device, default_output_device, Devices};
|
|
|
|
pub type SupportedInputConfigs = VecIntoIter<SupportedStreamConfigRange>;
|
|
pub type SupportedOutputConfigs = VecIntoIter<SupportedStreamConfigRange>;
|
|
|
|
mod enumerate;
|
|
|
|
/// The default linux, dragonfly, freebsd and netbsd host type.
|
|
#[derive(Debug)]
|
|
pub struct Host;
|
|
|
|
impl Host {
|
|
pub fn new() -> Result<Self, crate::HostUnavailable> {
|
|
Ok(Host)
|
|
}
|
|
}
|
|
|
|
impl HostTrait for Host {
|
|
type Devices = Devices;
|
|
type Device = Device;
|
|
|
|
fn is_available() -> bool {
|
|
// Assume ALSA is always available on linux/dragonfly/freebsd/netbsd.
|
|
true
|
|
}
|
|
|
|
fn devices(&self) -> Result<Self::Devices, DevicesError> {
|
|
Devices::new()
|
|
}
|
|
|
|
fn default_input_device(&self) -> Option<Self::Device> {
|
|
default_input_device()
|
|
}
|
|
|
|
fn default_output_device(&self) -> Option<Self::Device> {
|
|
default_output_device()
|
|
}
|
|
}
|
|
|
|
impl DeviceTrait for Device {
|
|
type SupportedInputConfigs = SupportedInputConfigs;
|
|
type SupportedOutputConfigs = SupportedOutputConfigs;
|
|
type Stream = Stream;
|
|
|
|
fn name(&self) -> Result<String, DeviceNameError> {
|
|
Device::name(self)
|
|
}
|
|
|
|
fn supported_input_configs(
|
|
&self,
|
|
) -> Result<Self::SupportedInputConfigs, SupportedStreamConfigsError> {
|
|
Device::supported_input_configs(self)
|
|
}
|
|
|
|
fn supported_output_configs(
|
|
&self,
|
|
) -> Result<Self::SupportedOutputConfigs, SupportedStreamConfigsError> {
|
|
Device::supported_output_configs(self)
|
|
}
|
|
|
|
fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
|
Device::default_input_config(self)
|
|
}
|
|
|
|
fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
|
Device::default_output_config(self)
|
|
}
|
|
|
|
fn build_input_stream_raw<D, E>(
|
|
&self,
|
|
conf: &StreamConfig,
|
|
sample_format: SampleFormat,
|
|
data_callback: D,
|
|
error_callback: E,
|
|
timeout: Option<Duration>,
|
|
) -> Result<Self::Stream, BuildStreamError>
|
|
where
|
|
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
|
|
E: FnMut(StreamError) + Send + 'static,
|
|
{
|
|
let stream_inner =
|
|
self.build_stream_inner(conf, sample_format, alsa::Direction::Capture)?;
|
|
let stream = Stream::new_input(
|
|
Arc::new(stream_inner),
|
|
data_callback,
|
|
error_callback,
|
|
timeout,
|
|
);
|
|
Ok(stream)
|
|
}
|
|
|
|
fn build_output_stream_raw<D, E>(
|
|
&self,
|
|
conf: &StreamConfig,
|
|
sample_format: SampleFormat,
|
|
data_callback: D,
|
|
error_callback: E,
|
|
timeout: Option<Duration>,
|
|
) -> Result<Self::Stream, BuildStreamError>
|
|
where
|
|
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
|
|
E: FnMut(StreamError) + Send + 'static,
|
|
{
|
|
let stream_inner =
|
|
self.build_stream_inner(conf, sample_format, alsa::Direction::Playback)?;
|
|
let stream = Stream::new_output(
|
|
Arc::new(stream_inner),
|
|
data_callback,
|
|
error_callback,
|
|
timeout,
|
|
);
|
|
Ok(stream)
|
|
}
|
|
}
|
|
|
|
struct TriggerSender(libc::c_int);
|
|
|
|
struct TriggerReceiver(libc::c_int);
|
|
|
|
impl TriggerSender {
|
|
fn wakeup(&self) {
|
|
let buf = 1u64;
|
|
let ret = unsafe { libc::write(self.0, &buf as *const u64 as *const _, 8) };
|
|
assert_eq!(ret, 8);
|
|
}
|
|
}
|
|
|
|
impl TriggerReceiver {
|
|
fn clear_pipe(&self) {
|
|
let mut out = 0u64;
|
|
let ret = unsafe { libc::read(self.0, &mut out as *mut u64 as *mut _, 8) };
|
|
assert_eq!(ret, 8);
|
|
}
|
|
}
|
|
|
|
fn trigger() -> (TriggerSender, TriggerReceiver) {
|
|
let mut fds = [0, 0];
|
|
match unsafe { libc::pipe(fds.as_mut_ptr()) } {
|
|
0 => (TriggerSender(fds[1]), TriggerReceiver(fds[0])),
|
|
_ => panic!("Could not create pipe"),
|
|
}
|
|
}
|
|
|
|
impl Drop for TriggerSender {
|
|
fn drop(&mut self) {
|
|
unsafe {
|
|
libc::close(self.0);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for TriggerReceiver {
|
|
fn drop(&mut self) {
|
|
unsafe {
|
|
libc::close(self.0);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct DeviceHandles {
|
|
playback: Option<alsa::PCM>,
|
|
capture: Option<alsa::PCM>,
|
|
}
|
|
|
|
impl DeviceHandles {
|
|
/// Create `DeviceHandles` for `name` and try to open a handle for both
|
|
/// directions. Returns `Ok` if either direction is opened successfully.
|
|
fn open(name: &str) -> Result<Self, alsa::Error> {
|
|
let mut handles = Self::default();
|
|
let playback_err = handles.try_open(name, alsa::Direction::Playback).err();
|
|
let capture_err = handles.try_open(name, alsa::Direction::Capture).err();
|
|
if let Some(err) = capture_err.and(playback_err) {
|
|
Err(err)
|
|
} else {
|
|
Ok(handles)
|
|
}
|
|
}
|
|
|
|
/// Get a mutable reference to the `Option` for a specific `stream_type`.
|
|
/// If the `Option` is `None`, the `alsa::PCM` will be opened and placed in
|
|
/// the `Option` before returning. If `handle_mut()` returns `Ok` the contained
|
|
/// `Option` is guaranteed to be `Some(..)`.
|
|
fn try_open(
|
|
&mut self,
|
|
name: &str,
|
|
stream_type: alsa::Direction,
|
|
) -> Result<&mut Option<alsa::PCM>, alsa::Error> {
|
|
let handle = match stream_type {
|
|
alsa::Direction::Playback => &mut self.playback,
|
|
alsa::Direction::Capture => &mut self.capture,
|
|
};
|
|
|
|
if handle.is_none() {
|
|
*handle = Some(alsa::pcm::PCM::new(name, stream_type, true)?);
|
|
}
|
|
|
|
Ok(handle)
|
|
}
|
|
|
|
/// Get a mutable reference to the `alsa::PCM` handle for a specific `stream_type`.
|
|
/// If the handle is not yet opened, it will be opened and stored in `self`.
|
|
fn get_mut(
|
|
&mut self,
|
|
name: &str,
|
|
stream_type: alsa::Direction,
|
|
) -> Result<&mut alsa::PCM, alsa::Error> {
|
|
Ok(self.try_open(name, stream_type)?.as_mut().unwrap())
|
|
}
|
|
|
|
/// Take ownership of the `alsa::PCM` handle for a specific `stream_type`.
|
|
/// If the handle is not yet opened, it will be opened and returned.
|
|
fn take(&mut self, name: &str, stream_type: alsa::Direction) -> Result<alsa::PCM, alsa::Error> {
|
|
Ok(self.try_open(name, stream_type)?.take().unwrap())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct Device {
|
|
name: String,
|
|
handles: Arc<Mutex<DeviceHandles>>,
|
|
}
|
|
|
|
impl Device {
|
|
fn build_stream_inner(
|
|
&self,
|
|
conf: &StreamConfig,
|
|
sample_format: SampleFormat,
|
|
stream_type: alsa::Direction,
|
|
) -> Result<StreamInner, BuildStreamError> {
|
|
let handle_result = self
|
|
.handles
|
|
.lock()
|
|
.unwrap()
|
|
.take(&self.name, stream_type)
|
|
.map_err(|e| (e, e.errno()));
|
|
|
|
let handle = match handle_result {
|
|
Err((_, libc::EBUSY)) => return Err(BuildStreamError::DeviceNotAvailable),
|
|
Err((_, libc::EINVAL)) => return Err(BuildStreamError::InvalidArgument),
|
|
Err((e, _)) => return Err(e.into()),
|
|
Ok(handle) => handle,
|
|
};
|
|
let can_pause = set_hw_params_from_format(&handle, conf, sample_format)?;
|
|
let period_len = set_sw_params_from_format(&handle, conf, stream_type)?;
|
|
|
|
handle.prepare()?;
|
|
|
|
let num_descriptors = handle.count();
|
|
if num_descriptors == 0 {
|
|
let description = "poll descriptor count for stream was 0".to_string();
|
|
let err = BackendSpecificError { description };
|
|
return Err(err.into());
|
|
}
|
|
|
|
// Check to see if we can retrieve valid timestamps from the device.
|
|
// Related: https://bugs.freedesktop.org/show_bug.cgi?id=88503
|
|
let ts = handle.status()?.get_htstamp();
|
|
let creation_instant = match (ts.tv_sec, ts.tv_nsec) {
|
|
(0, 0) => Some(std::time::Instant::now()),
|
|
_ => None,
|
|
};
|
|
|
|
if let alsa::Direction::Capture = stream_type {
|
|
handle.start()?;
|
|
}
|
|
|
|
let stream_inner = StreamInner {
|
|
channel: handle,
|
|
sample_format,
|
|
num_descriptors,
|
|
conf: conf.clone(),
|
|
period_len,
|
|
can_pause,
|
|
creation_instant,
|
|
};
|
|
|
|
Ok(stream_inner)
|
|
}
|
|
|
|
#[inline]
|
|
fn name(&self) -> Result<String, DeviceNameError> {
|
|
Ok(self.name.clone())
|
|
}
|
|
|
|
fn supported_configs(
|
|
&self,
|
|
stream_t: alsa::Direction,
|
|
) -> Result<VecIntoIter<SupportedStreamConfigRange>, SupportedStreamConfigsError> {
|
|
let mut guard = self.handles.lock().unwrap();
|
|
let handle_result = guard
|
|
.get_mut(&self.name, stream_t)
|
|
.map_err(|e| (e, e.errno()));
|
|
|
|
let handle = match handle_result {
|
|
Err((_, libc::ENOENT)) | Err((_, libc::EBUSY)) => {
|
|
return Err(SupportedStreamConfigsError::DeviceNotAvailable)
|
|
}
|
|
Err((_, libc::EINVAL)) => return Err(SupportedStreamConfigsError::InvalidArgument),
|
|
Err((e, _)) => return Err(e.into()),
|
|
Ok(handle) => handle,
|
|
};
|
|
|
|
let hw_params = alsa::pcm::HwParams::any(handle)?;
|
|
|
|
// TODO: check endianness
|
|
const FORMATS: [(SampleFormat, alsa::pcm::Format); 8] = [
|
|
(SampleFormat::I8, alsa::pcm::Format::S8),
|
|
(SampleFormat::U8, alsa::pcm::Format::U8),
|
|
(SampleFormat::I16, alsa::pcm::Format::S16LE),
|
|
//SND_PCM_FORMAT_S16_BE,
|
|
(SampleFormat::U16, alsa::pcm::Format::U16LE),
|
|
//SND_PCM_FORMAT_U16_BE,
|
|
//SND_PCM_FORMAT_S24_LE,
|
|
//SND_PCM_FORMAT_S24_BE,
|
|
//SND_PCM_FORMAT_U24_LE,
|
|
//SND_PCM_FORMAT_U24_BE,
|
|
(SampleFormat::I32, alsa::pcm::Format::S32LE),
|
|
//SND_PCM_FORMAT_S32_BE,
|
|
(SampleFormat::U32, alsa::pcm::Format::U32LE),
|
|
//SND_PCM_FORMAT_U32_BE,
|
|
(SampleFormat::F32, alsa::pcm::Format::FloatLE),
|
|
//SND_PCM_FORMAT_FLOAT_BE,
|
|
(SampleFormat::F64, alsa::pcm::Format::Float64LE),
|
|
//SND_PCM_FORMAT_FLOAT64_BE,
|
|
//SND_PCM_FORMAT_IEC958_SUBFRAME_LE,
|
|
//SND_PCM_FORMAT_IEC958_SUBFRAME_BE,
|
|
//SND_PCM_FORMAT_MU_LAW,
|
|
//SND_PCM_FORMAT_A_LAW,
|
|
//SND_PCM_FORMAT_IMA_ADPCM,
|
|
//SND_PCM_FORMAT_MPEG,
|
|
//SND_PCM_FORMAT_GSM,
|
|
//SND_PCM_FORMAT_SPECIAL,
|
|
//SND_PCM_FORMAT_S24_3LE,
|
|
//SND_PCM_FORMAT_S24_3BE,
|
|
//SND_PCM_FORMAT_U24_3LE,
|
|
//SND_PCM_FORMAT_U24_3BE,
|
|
//SND_PCM_FORMAT_S20_3LE,
|
|
//SND_PCM_FORMAT_S20_3BE,
|
|
//SND_PCM_FORMAT_U20_3LE,
|
|
//SND_PCM_FORMAT_U20_3BE,
|
|
//SND_PCM_FORMAT_S18_3LE,
|
|
//SND_PCM_FORMAT_S18_3BE,
|
|
//SND_PCM_FORMAT_U18_3LE,
|
|
//SND_PCM_FORMAT_U18_3BE,
|
|
];
|
|
|
|
let mut supported_formats = Vec::new();
|
|
for &(sample_format, alsa_format) in FORMATS.iter() {
|
|
if hw_params.test_format(alsa_format).is_ok() {
|
|
supported_formats.push(sample_format);
|
|
}
|
|
}
|
|
|
|
let min_rate = hw_params.get_rate_min()?;
|
|
let max_rate = hw_params.get_rate_max()?;
|
|
|
|
let sample_rates = if min_rate == max_rate || hw_params.test_rate(min_rate + 1).is_ok() {
|
|
vec![(min_rate, max_rate)]
|
|
} else {
|
|
const RATES: [libc::c_uint; 13] = [
|
|
5512, 8000, 11025, 16000, 22050, 32000, 44100, 48000, 64000, 88200, 96000, 176400,
|
|
192000,
|
|
];
|
|
|
|
let mut rates = Vec::new();
|
|
for &rate in RATES.iter() {
|
|
if hw_params.test_rate(rate).is_ok() {
|
|
rates.push((rate, rate));
|
|
}
|
|
}
|
|
|
|
if rates.is_empty() {
|
|
vec![(min_rate, max_rate)]
|
|
} else {
|
|
rates
|
|
}
|
|
};
|
|
|
|
let min_channels = hw_params.get_channels_min()?;
|
|
let max_channels = hw_params.get_channels_max()?;
|
|
|
|
let max_channels = cmp::min(max_channels, 32); // TODO: limiting to 32 channels or too much stuff is returned
|
|
let supported_channels = (min_channels..max_channels + 1)
|
|
.filter_map(|num| {
|
|
if hw_params.test_channels(num).is_ok() {
|
|
Some(num as ChannelCount)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
let min_buffer_size = hw_params.get_buffer_size_min()?;
|
|
let max_buffer_size = hw_params.get_buffer_size_max()?;
|
|
|
|
let buffer_size_range = SupportedBufferSize::Range {
|
|
min: min_buffer_size as u32,
|
|
max: max_buffer_size as u32,
|
|
};
|
|
|
|
let mut output = Vec::with_capacity(
|
|
supported_formats.len() * supported_channels.len() * sample_rates.len(),
|
|
);
|
|
for &sample_format in supported_formats.iter() {
|
|
for &channels in supported_channels.iter() {
|
|
for &(min_rate, max_rate) in sample_rates.iter() {
|
|
output.push(SupportedStreamConfigRange {
|
|
channels,
|
|
min_sample_rate: SampleRate(min_rate as u32),
|
|
max_sample_rate: SampleRate(max_rate as u32),
|
|
buffer_size: buffer_size_range.clone(),
|
|
sample_format,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(output.into_iter())
|
|
}
|
|
|
|
fn supported_input_configs(
|
|
&self,
|
|
) -> Result<SupportedInputConfigs, SupportedStreamConfigsError> {
|
|
self.supported_configs(alsa::Direction::Capture)
|
|
}
|
|
|
|
fn supported_output_configs(
|
|
&self,
|
|
) -> Result<SupportedOutputConfigs, SupportedStreamConfigsError> {
|
|
self.supported_configs(alsa::Direction::Playback)
|
|
}
|
|
|
|
// ALSA does not offer default stream formats, so instead we compare all supported formats by
|
|
// the `SupportedStreamConfigRange::cmp_default_heuristics` order and select the greatest.
|
|
fn default_config(
|
|
&self,
|
|
stream_t: alsa::Direction,
|
|
) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
|
let mut formats: Vec<_> = {
|
|
match self.supported_configs(stream_t) {
|
|
Err(SupportedStreamConfigsError::DeviceNotAvailable) => {
|
|
return Err(DefaultStreamConfigError::DeviceNotAvailable);
|
|
}
|
|
Err(SupportedStreamConfigsError::InvalidArgument) => {
|
|
// this happens sometimes when querying for input and output capabilities, but
|
|
// the device supports only one
|
|
return Err(DefaultStreamConfigError::StreamTypeNotSupported);
|
|
}
|
|
Err(SupportedStreamConfigsError::BackendSpecific { err }) => {
|
|
return Err(err.into());
|
|
}
|
|
Ok(fmts) => fmts.collect(),
|
|
}
|
|
};
|
|
|
|
formats.sort_by(|a, b| a.cmp_default_heuristics(b));
|
|
|
|
match formats.into_iter().last() {
|
|
Some(f) => {
|
|
let min_r = f.min_sample_rate;
|
|
let max_r = f.max_sample_rate;
|
|
let mut format = f.with_max_sample_rate();
|
|
const HZ_44100: SampleRate = SampleRate(44_100);
|
|
if min_r <= HZ_44100 && HZ_44100 <= max_r {
|
|
format.sample_rate = HZ_44100;
|
|
}
|
|
Ok(format)
|
|
}
|
|
None => Err(DefaultStreamConfigError::StreamTypeNotSupported),
|
|
}
|
|
}
|
|
|
|
fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
|
self.default_config(alsa::Direction::Capture)
|
|
}
|
|
|
|
fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
|
|
self.default_config(alsa::Direction::Playback)
|
|
}
|
|
}
|
|
|
|
struct StreamInner {
|
|
// The ALSA channel.
|
|
channel: alsa::pcm::PCM,
|
|
|
|
// When converting between file descriptors and `snd_pcm_t`, this is the number of
|
|
// file descriptors that this `snd_pcm_t` uses.
|
|
num_descriptors: usize,
|
|
|
|
// Format of the samples.
|
|
sample_format: SampleFormat,
|
|
|
|
// The configuration used to open this stream.
|
|
conf: StreamConfig,
|
|
|
|
// Minimum number of samples to put in the buffer.
|
|
period_len: usize,
|
|
|
|
#[allow(dead_code)]
|
|
// Whether or not the hardware supports pausing the stream.
|
|
// TODO: We need an API to expose this. See #197, #284.
|
|
can_pause: bool,
|
|
|
|
// In the case that the device does not return valid timestamps via `get_htstamp`, this field
|
|
// will be `Some` and will contain an `Instant` representing the moment the stream was created.
|
|
//
|
|
// If this field is `Some`, then the stream will use the duration since this instant as a
|
|
// source for timestamps.
|
|
//
|
|
// If this field is `None` then the elapsed duration between `get_trigger_htstamp` and
|
|
// `get_htstamp` is used.
|
|
creation_instant: Option<std::time::Instant>,
|
|
}
|
|
|
|
// Assume that the ALSA library is built with thread safe option.
|
|
unsafe impl Sync for StreamInner {}
|
|
|
|
#[derive(Debug, Eq, PartialEq)]
|
|
enum StreamType {
|
|
Input,
|
|
Output,
|
|
}
|
|
|
|
pub struct Stream {
|
|
/// The high-priority audio processing thread calling callbacks.
|
|
/// Option used for moving out in destructor.
|
|
thread: Option<JoinHandle<()>>,
|
|
|
|
/// Handle to the underlying stream for playback controls.
|
|
inner: Arc<StreamInner>,
|
|
|
|
/// Used to signal to stop processing.
|
|
trigger: TriggerSender,
|
|
}
|
|
|
|
struct StreamWorkerContext {
|
|
descriptors: Vec<libc::pollfd>,
|
|
buffer: Vec<u8>,
|
|
poll_timeout: i32,
|
|
}
|
|
|
|
impl StreamWorkerContext {
|
|
fn new(poll_timeout: &Option<Duration>) -> Self {
|
|
let poll_timeout: i32 = if let Some(d) = poll_timeout {
|
|
d.as_millis().try_into().unwrap()
|
|
} else {
|
|
-1
|
|
};
|
|
|
|
Self {
|
|
descriptors: Vec::new(),
|
|
buffer: Vec::new(),
|
|
poll_timeout,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn input_stream_worker(
|
|
rx: TriggerReceiver,
|
|
stream: &StreamInner,
|
|
data_callback: &mut (dyn FnMut(&Data, &InputCallbackInfo) + Send + 'static),
|
|
error_callback: &mut (dyn FnMut(StreamError) + Send + 'static),
|
|
timeout: Option<Duration>,
|
|
) {
|
|
let mut ctxt = StreamWorkerContext::new(&timeout);
|
|
loop {
|
|
let flow =
|
|
poll_descriptors_and_prepare_buffer(&rx, stream, &mut ctxt).unwrap_or_else(|err| {
|
|
error_callback(err.into());
|
|
PollDescriptorsFlow::Continue
|
|
});
|
|
|
|
match flow {
|
|
PollDescriptorsFlow::Continue => {
|
|
continue;
|
|
}
|
|
PollDescriptorsFlow::XRun => {
|
|
if let Err(err) = stream.channel.prepare() {
|
|
error_callback(err.into());
|
|
}
|
|
continue;
|
|
}
|
|
PollDescriptorsFlow::Return => return,
|
|
PollDescriptorsFlow::Ready {
|
|
status,
|
|
avail_frames: _,
|
|
delay_frames,
|
|
stream_type,
|
|
} => {
|
|
assert_eq!(
|
|
stream_type,
|
|
StreamType::Input,
|
|
"expected input stream, but polling descriptors indicated output",
|
|
);
|
|
if let Err(err) = process_input(
|
|
stream,
|
|
&mut ctxt.buffer,
|
|
status,
|
|
delay_frames,
|
|
data_callback,
|
|
) {
|
|
error_callback(err.into());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn output_stream_worker(
|
|
rx: TriggerReceiver,
|
|
stream: &StreamInner,
|
|
data_callback: &mut (dyn FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static),
|
|
error_callback: &mut (dyn FnMut(StreamError) + Send + 'static),
|
|
timeout: Option<Duration>,
|
|
) {
|
|
let mut ctxt = StreamWorkerContext::new(&timeout);
|
|
loop {
|
|
let flow =
|
|
poll_descriptors_and_prepare_buffer(&rx, stream, &mut ctxt).unwrap_or_else(|err| {
|
|
error_callback(err.into());
|
|
PollDescriptorsFlow::Continue
|
|
});
|
|
|
|
match flow {
|
|
PollDescriptorsFlow::Continue => continue,
|
|
PollDescriptorsFlow::XRun => {
|
|
if let Err(err) = stream.channel.prepare() {
|
|
error_callback(err.into());
|
|
}
|
|
continue;
|
|
}
|
|
PollDescriptorsFlow::Return => return,
|
|
PollDescriptorsFlow::Ready {
|
|
status,
|
|
avail_frames,
|
|
delay_frames,
|
|
stream_type,
|
|
} => {
|
|
assert_eq!(
|
|
stream_type,
|
|
StreamType::Output,
|
|
"expected output stream, but polling descriptors indicated input",
|
|
);
|
|
if let Err(err) = process_output(
|
|
stream,
|
|
&mut ctxt.buffer,
|
|
status,
|
|
avail_frames,
|
|
delay_frames,
|
|
data_callback,
|
|
error_callback,
|
|
) {
|
|
error_callback(err.into());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
enum PollDescriptorsFlow {
|
|
Continue,
|
|
Return,
|
|
Ready {
|
|
stream_type: StreamType,
|
|
status: alsa::pcm::Status,
|
|
avail_frames: usize,
|
|
delay_frames: usize,
|
|
},
|
|
XRun,
|
|
}
|
|
|
|
// This block is shared between both input and output stream worker functions.
|
|
fn poll_descriptors_and_prepare_buffer(
|
|
rx: &TriggerReceiver,
|
|
stream: &StreamInner,
|
|
ctxt: &mut StreamWorkerContext,
|
|
) -> Result<PollDescriptorsFlow, BackendSpecificError> {
|
|
let StreamWorkerContext {
|
|
ref mut descriptors,
|
|
ref mut buffer,
|
|
ref poll_timeout,
|
|
} = *ctxt;
|
|
|
|
descriptors.clear();
|
|
|
|
// Add the self-pipe for signaling termination.
|
|
descriptors.push(libc::pollfd {
|
|
fd: rx.0,
|
|
events: libc::POLLIN,
|
|
revents: 0,
|
|
});
|
|
|
|
// Add ALSA polling fds.
|
|
let len = descriptors.len();
|
|
descriptors.resize(
|
|
stream.num_descriptors + len,
|
|
libc::pollfd {
|
|
fd: 0,
|
|
events: 0,
|
|
revents: 0,
|
|
},
|
|
);
|
|
let filled = stream.channel.fill(&mut descriptors[len..])?;
|
|
debug_assert_eq!(filled, stream.num_descriptors);
|
|
|
|
// Don't timeout, wait forever.
|
|
let res = alsa::poll::poll(descriptors, *poll_timeout)?;
|
|
if res == 0 {
|
|
let description = String::from("`alsa::poll()` spuriously returned");
|
|
return Err(BackendSpecificError { description });
|
|
}
|
|
|
|
if descriptors[0].revents != 0 {
|
|
// The stream has been requested to be destroyed.
|
|
rx.clear_pipe();
|
|
return Ok(PollDescriptorsFlow::Return);
|
|
}
|
|
|
|
let revents = stream.channel.revents(&descriptors[1..])?;
|
|
if revents.contains(alsa::poll::Flags::ERR) {
|
|
let description = String::from("`alsa::poll()` returned POLLERR");
|
|
return Err(BackendSpecificError { description });
|
|
}
|
|
let stream_type = match revents {
|
|
alsa::poll::Flags::OUT => StreamType::Output,
|
|
alsa::poll::Flags::IN => StreamType::Input,
|
|
_ => {
|
|
// Nothing to process, poll again
|
|
return Ok(PollDescriptorsFlow::Continue);
|
|
}
|
|
};
|
|
|
|
let status = stream.channel.status()?;
|
|
let avail_frames = match stream.channel.avail() {
|
|
Err(err) if err.errno() == libc::EPIPE => return Ok(PollDescriptorsFlow::XRun),
|
|
res => res,
|
|
}? as usize;
|
|
let delay_frames = match status.get_delay() {
|
|
// Buffer underrun. TODO: Notify the user.
|
|
d if d < 0 => 0,
|
|
d => d as usize,
|
|
};
|
|
let available_samples = avail_frames * stream.conf.channels as usize;
|
|
|
|
// Only go on if there is at least `stream.period_len` samples.
|
|
if available_samples < stream.period_len {
|
|
return Ok(PollDescriptorsFlow::Continue);
|
|
}
|
|
|
|
// Prepare the data buffer.
|
|
let buffer_size = stream.sample_format.sample_size() * available_samples;
|
|
buffer.resize(buffer_size, 0u8);
|
|
|
|
Ok(PollDescriptorsFlow::Ready {
|
|
stream_type,
|
|
status,
|
|
avail_frames,
|
|
delay_frames,
|
|
})
|
|
}
|
|
|
|
// Read input data from ALSA and deliver it to the user.
|
|
fn process_input(
|
|
stream: &StreamInner,
|
|
buffer: &mut [u8],
|
|
status: alsa::pcm::Status,
|
|
delay_frames: usize,
|
|
data_callback: &mut (dyn FnMut(&Data, &InputCallbackInfo) + Send + 'static),
|
|
) -> Result<(), BackendSpecificError> {
|
|
stream.channel.io_bytes().readi(buffer)?;
|
|
let sample_format = stream.sample_format;
|
|
let data = buffer.as_mut_ptr() as *mut ();
|
|
let len = buffer.len() / sample_format.sample_size();
|
|
let data = unsafe { Data::from_parts(data, len, sample_format) };
|
|
let callback = stream_timestamp(&status, stream.creation_instant)?;
|
|
let delay_duration = frames_to_duration(delay_frames, stream.conf.sample_rate);
|
|
let capture = callback
|
|
.sub(delay_duration)
|
|
.expect("`capture` is earlier than representation supported by `StreamInstant`");
|
|
let timestamp = crate::InputStreamTimestamp { callback, capture };
|
|
let info = crate::InputCallbackInfo { timestamp };
|
|
data_callback(&data, &info);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Request data from the user's function and write it via ALSA.
|
|
//
|
|
// Returns `true`
|
|
fn process_output(
|
|
stream: &StreamInner,
|
|
buffer: &mut [u8],
|
|
status: alsa::pcm::Status,
|
|
available_frames: usize,
|
|
delay_frames: usize,
|
|
data_callback: &mut (dyn FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static),
|
|
error_callback: &mut dyn FnMut(StreamError),
|
|
) -> Result<(), BackendSpecificError> {
|
|
{
|
|
// We're now sure that we're ready to write data.
|
|
let sample_format = stream.sample_format;
|
|
let data = buffer.as_mut_ptr() as *mut ();
|
|
let len = buffer.len() / sample_format.sample_size();
|
|
let mut data = unsafe { Data::from_parts(data, len, sample_format) };
|
|
let callback = stream_timestamp(&status, stream.creation_instant)?;
|
|
let delay_duration = frames_to_duration(delay_frames, stream.conf.sample_rate);
|
|
let playback = callback
|
|
.add(delay_duration)
|
|
.expect("`playback` occurs beyond representation supported by `StreamInstant`");
|
|
let timestamp = crate::OutputStreamTimestamp { callback, playback };
|
|
let info = crate::OutputCallbackInfo { timestamp };
|
|
data_callback(&mut data, &info);
|
|
}
|
|
loop {
|
|
match stream.channel.io_bytes().writei(buffer) {
|
|
Err(err) if err.errno() == libc::EPIPE => {
|
|
// buffer underrun
|
|
// TODO: Notify the user of this.
|
|
let _ = stream.channel.try_recover(err, false);
|
|
}
|
|
Err(err) => {
|
|
error_callback(err.into());
|
|
continue;
|
|
}
|
|
Ok(result) if result != available_frames => {
|
|
let description = format!(
|
|
"unexpected number of frames written: expected {}, \
|
|
result {} (this should never happen)",
|
|
available_frames, result,
|
|
);
|
|
error_callback(BackendSpecificError { description }.into());
|
|
continue;
|
|
}
|
|
_ => {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
// Use the elapsed duration since the start of the stream.
|
|
//
|
|
// This ensures positive values that are compatible with our `StreamInstant` representation.
|
|
fn stream_timestamp(
|
|
status: &alsa::pcm::Status,
|
|
creation_instant: Option<std::time::Instant>,
|
|
) -> Result<crate::StreamInstant, BackendSpecificError> {
|
|
match creation_instant {
|
|
None => {
|
|
let trigger_ts = status.get_trigger_htstamp();
|
|
let ts = status.get_htstamp();
|
|
let nanos = timespec_diff_nanos(ts, trigger_ts);
|
|
if nanos < 0 {
|
|
panic!(
|
|
"get_htstamp `{}.{}` was earlier than get_trigger_htstamp `{}.{}`",
|
|
ts.tv_sec, ts.tv_nsec, trigger_ts.tv_sec, trigger_ts.tv_nsec
|
|
);
|
|
}
|
|
Ok(crate::StreamInstant::from_nanos(nanos))
|
|
}
|
|
Some(creation) => {
|
|
let now = std::time::Instant::now();
|
|
let duration = now.duration_since(creation);
|
|
let instant = crate::StreamInstant::from_nanos_i128(duration.as_nanos() as i128)
|
|
.expect("stream duration has exceeded `StreamInstant` representation");
|
|
Ok(instant)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Adapted from `timestamp2ns` here:
|
|
// https://fossies.org/linux/alsa-lib/test/audio_time.c
|
|
fn timespec_to_nanos(ts: libc::timespec) -> i64 {
|
|
ts.tv_sec as i64 * 1_000_000_000 + ts.tv_nsec as i64
|
|
}
|
|
|
|
// Adapted from `timediff` here:
|
|
// https://fossies.org/linux/alsa-lib/test/audio_time.c
|
|
fn timespec_diff_nanos(a: libc::timespec, b: libc::timespec) -> i64 {
|
|
timespec_to_nanos(a) - timespec_to_nanos(b)
|
|
}
|
|
|
|
// Convert the given duration in frames at the given sample rate to a `std::time::Duration`.
|
|
fn frames_to_duration(frames: usize, rate: crate::SampleRate) -> std::time::Duration {
|
|
let secsf = frames as f64 / rate.0 as f64;
|
|
let secs = secsf as u64;
|
|
let nanos = ((secsf - secs as f64) * 1_000_000_000.0) as u32;
|
|
std::time::Duration::new(secs, nanos)
|
|
}
|
|
|
|
impl Stream {
|
|
fn new_input<D, E>(
|
|
inner: Arc<StreamInner>,
|
|
mut data_callback: D,
|
|
mut error_callback: E,
|
|
timeout: Option<Duration>,
|
|
) -> Stream
|
|
where
|
|
D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
|
|
E: FnMut(StreamError) + Send + 'static,
|
|
{
|
|
let (tx, rx) = trigger();
|
|
// Clone the handle for passing into worker thread.
|
|
let stream = inner.clone();
|
|
let thread = thread::Builder::new()
|
|
.name("cpal_alsa_in".to_owned())
|
|
.spawn(move || {
|
|
input_stream_worker(
|
|
rx,
|
|
&stream,
|
|
&mut data_callback,
|
|
&mut error_callback,
|
|
timeout,
|
|
);
|
|
})
|
|
.unwrap();
|
|
Stream {
|
|
thread: Some(thread),
|
|
inner,
|
|
trigger: tx,
|
|
}
|
|
}
|
|
|
|
fn new_output<D, E>(
|
|
inner: Arc<StreamInner>,
|
|
mut data_callback: D,
|
|
mut error_callback: E,
|
|
timeout: Option<Duration>,
|
|
) -> Stream
|
|
where
|
|
D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
|
|
E: FnMut(StreamError) + Send + 'static,
|
|
{
|
|
let (tx, rx) = trigger();
|
|
// Clone the handle for passing into worker thread.
|
|
let stream = inner.clone();
|
|
let thread = thread::Builder::new()
|
|
.name("cpal_alsa_out".to_owned())
|
|
.spawn(move || {
|
|
output_stream_worker(
|
|
rx,
|
|
&stream,
|
|
&mut data_callback,
|
|
&mut error_callback,
|
|
timeout,
|
|
);
|
|
})
|
|
.unwrap();
|
|
Stream {
|
|
thread: Some(thread),
|
|
inner,
|
|
trigger: tx,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for Stream {
|
|
fn drop(&mut self) {
|
|
self.trigger.wakeup();
|
|
self.thread.take().unwrap().join().unwrap();
|
|
}
|
|
}
|
|
|
|
impl StreamTrait for Stream {
|
|
fn play(&self) -> Result<(), PlayStreamError> {
|
|
self.inner.channel.pause(false).ok();
|
|
Ok(())
|
|
}
|
|
fn pause(&self) -> Result<(), PauseStreamError> {
|
|
self.inner.channel.pause(true).ok();
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn set_hw_params_from_format(
|
|
pcm_handle: &alsa::pcm::PCM,
|
|
config: &StreamConfig,
|
|
sample_format: SampleFormat,
|
|
) -> Result<bool, BackendSpecificError> {
|
|
let hw_params = alsa::pcm::HwParams::any(pcm_handle)?;
|
|
hw_params.set_access(alsa::pcm::Access::RWInterleaved)?;
|
|
|
|
let sample_format = if cfg!(target_endian = "big") {
|
|
match sample_format {
|
|
SampleFormat::I8 => alsa::pcm::Format::S8,
|
|
SampleFormat::I16 => alsa::pcm::Format::S16BE,
|
|
// SampleFormat::I24 => alsa::pcm::Format::S24BE,
|
|
SampleFormat::I32 => alsa::pcm::Format::S32BE,
|
|
// SampleFormat::I48 => alsa::pcm::Format::S48BE,
|
|
// SampleFormat::I64 => alsa::pcm::Format::S64BE,
|
|
SampleFormat::U8 => alsa::pcm::Format::U8,
|
|
SampleFormat::U16 => alsa::pcm::Format::U16BE,
|
|
// SampleFormat::U24 => alsa::pcm::Format::U24BE,
|
|
SampleFormat::U32 => alsa::pcm::Format::U32BE,
|
|
// SampleFormat::U48 => alsa::pcm::Format::U48BE,
|
|
// SampleFormat::U64 => alsa::pcm::Format::U64BE,
|
|
SampleFormat::F32 => alsa::pcm::Format::FloatBE,
|
|
SampleFormat::F64 => alsa::pcm::Format::Float64BE,
|
|
sample_format => {
|
|
return Err(BackendSpecificError {
|
|
description: format!(
|
|
"Sample format '{}' is not supported by this backend",
|
|
sample_format
|
|
),
|
|
})
|
|
}
|
|
}
|
|
} else {
|
|
match sample_format {
|
|
SampleFormat::I8 => alsa::pcm::Format::S8,
|
|
SampleFormat::I16 => alsa::pcm::Format::S16LE,
|
|
// SampleFormat::I24 => alsa::pcm::Format::S24LE,
|
|
SampleFormat::I32 => alsa::pcm::Format::S32LE,
|
|
// SampleFormat::I48 => alsa::pcm::Format::S48LE,
|
|
// SampleFormat::I64 => alsa::pcm::Format::S64LE,
|
|
SampleFormat::U8 => alsa::pcm::Format::U8,
|
|
SampleFormat::U16 => alsa::pcm::Format::U16LE,
|
|
// SampleFormat::U24 => alsa::pcm::Format::U24LE,
|
|
SampleFormat::U32 => alsa::pcm::Format::U32LE,
|
|
// SampleFormat::U48 => alsa::pcm::Format::U48LE,
|
|
// SampleFormat::U64 => alsa::pcm::Format::U64LE,
|
|
SampleFormat::F32 => alsa::pcm::Format::FloatLE,
|
|
SampleFormat::F64 => alsa::pcm::Format::Float64LE,
|
|
sample_format => {
|
|
return Err(BackendSpecificError {
|
|
description: format!(
|
|
"Sample format '{}' is not supported by this backend",
|
|
sample_format
|
|
),
|
|
})
|
|
}
|
|
}
|
|
};
|
|
|
|
hw_params.set_format(sample_format)?;
|
|
hw_params.set_rate(config.sample_rate.0, alsa::ValueOr::Nearest)?;
|
|
hw_params.set_channels(config.channels as u32)?;
|
|
|
|
match config.buffer_size {
|
|
BufferSize::Fixed(v) => {
|
|
hw_params.set_period_size_near((v / 4) as alsa::pcm::Frames, alsa::ValueOr::Nearest)?;
|
|
hw_params.set_buffer_size(v as alsa::pcm::Frames)?;
|
|
}
|
|
BufferSize::Default => {
|
|
// Larger buffer than upstream cpal default: after Symphonia seeks or CPU
|
|
// spikes (analysis, demux) the mixer needs more margin before ALSA underruns.
|
|
// ~40 ms period, ~200 ms total buffer (device may round to nearest supported).
|
|
hw_params.set_period_time_near(40_000, alsa::ValueOr::Nearest)?;
|
|
hw_params.set_buffer_time_near(200_000, alsa::ValueOr::Nearest)?;
|
|
}
|
|
}
|
|
|
|
pcm_handle.hw_params(&hw_params)?;
|
|
|
|
Ok(hw_params.can_pause())
|
|
}
|
|
|
|
fn set_sw_params_from_format(
|
|
pcm_handle: &alsa::pcm::PCM,
|
|
config: &StreamConfig,
|
|
stream_type: alsa::Direction,
|
|
) -> Result<usize, BackendSpecificError> {
|
|
let sw_params = pcm_handle.sw_params_current()?;
|
|
|
|
let period_len = {
|
|
let (buffer, period) = pcm_handle.get_params()?;
|
|
if buffer == 0 {
|
|
return Err(BackendSpecificError {
|
|
description: "initialization resulted in a null buffer".to_string(),
|
|
});
|
|
}
|
|
sw_params.set_avail_min(period as alsa::pcm::Frames)?;
|
|
|
|
let start_threshold = match stream_type {
|
|
alsa::Direction::Playback => buffer - period,
|
|
|
|
// For capture streams, the start threshold is irrelevant and ignored,
|
|
// because build_stream_inner() starts the stream before process_input()
|
|
// reads from it. Set it anyway I guess, since it's better than leaving
|
|
// it at an unspecified default value.
|
|
alsa::Direction::Capture => 1,
|
|
};
|
|
sw_params.set_start_threshold(start_threshold.try_into().unwrap())?;
|
|
|
|
period as usize * config.channels as usize
|
|
};
|
|
|
|
sw_params.set_tstamp_mode(true)?;
|
|
sw_params.set_tstamp_type(alsa::pcm::TstampType::MonotonicRaw)?;
|
|
|
|
// tstamp_type param cannot be changed after the device is opened.
|
|
// The default tstamp_type value on most Linux systems is "monotonic",
|
|
// let's try to use it if setting the tstamp_type fails.
|
|
if pcm_handle.sw_params(&sw_params).is_err() {
|
|
sw_params.set_tstamp_type(alsa::pcm::TstampType::Monotonic)?;
|
|
pcm_handle.sw_params(&sw_params)?;
|
|
}
|
|
|
|
Ok(period_len)
|
|
}
|
|
|
|
impl From<alsa::Error> for BackendSpecificError {
|
|
fn from(err: alsa::Error) -> Self {
|
|
BackendSpecificError {
|
|
description: err.to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<alsa::Error> for BuildStreamError {
|
|
fn from(err: alsa::Error) -> Self {
|
|
let err: BackendSpecificError = err.into();
|
|
err.into()
|
|
}
|
|
}
|
|
|
|
impl From<alsa::Error> for SupportedStreamConfigsError {
|
|
fn from(err: alsa::Error) -> Self {
|
|
let err: BackendSpecificError = err.into();
|
|
err.into()
|
|
}
|
|
}
|
|
|
|
impl From<alsa::Error> for PlayStreamError {
|
|
fn from(err: alsa::Error) -> Self {
|
|
let err: BackendSpecificError = err.into();
|
|
err.into()
|
|
}
|
|
}
|
|
|
|
impl From<alsa::Error> for PauseStreamError {
|
|
fn from(err: alsa::Error) -> Self {
|
|
let err: BackendSpecificError = err.into();
|
|
err.into()
|
|
}
|
|
}
|
|
|
|
impl From<alsa::Error> for StreamError {
|
|
fn from(err: alsa::Error) -> Self {
|
|
let err: BackendSpecificError = err.into();
|
|
err.into()
|
|
}
|
|
}
|