Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# Unreleased

- ALSA(process_output): Pass `silent=true` to `PCM.try_recover`, so it doesn't write to stderr.
- ALSA: Fix buffer and period size selection by rounding to supported values. Actual buffer size may be different from the requested size or may be a device-specified default size. Additionally sets ALSA "periods" to 2 (previously 4). (error 22)
- CoreAudio: `Device::supported_configs` now returns a single element containing the available sample rate range when all elements have the same `mMinimum` and `mMaximum` values (which is the most common case).
- CoreAudio: Detect default audio device lazily when building a stream, instead of during device enumeration.
- ALSA: Fix buffer and period size by selecting the closest supported values.
- ALSA: Change ALSA periods from 4 to 2.
- ALSA: Change card enumeration to work like `aplay -L` does.
- CoreAudio: Change `Device::supported_configs` to return a single element containing the available sample rate range when all elements have the same `mMinimum` and `mMaximum` values.
- CoreAudio: Change default audio device detection to be lazy when building a stream, instead of during device enumeration.
- iOS: Fix example by properly activating audio session.
- WASAPI: Expose IMMDevice from WASAPI host Device.

Expand Down
128 changes: 60 additions & 68 deletions src/host/alsa/enumerate.rs
Original file line number Diff line number Diff line change
@@ -1,102 +1,69 @@
use super::alsa;
use super::{Device, DeviceHandles};
use std::{
collections::HashSet,
sync::{Arc, Mutex},
};

use super::{
alsa, {Device, DeviceHandles},
};
use crate::{BackendSpecificError, DevicesError};
use std::sync::{Arc, Mutex};

/// ALSA's implementation for `Devices`.
pub struct Devices {
builtin_pos: usize,
card_iter: alsa::card::Iter,
hint_iter: alsa::device_name::HintIter,
enumerated_pcm_ids: HashSet<String>,
}

impl Devices {
pub fn new() -> Result<Self, DevicesError> {
Ok(Devices {
builtin_pos: 0,
card_iter: alsa::card::Iter::new(),
})
// Enumerate ALL devices from ALSA hints (same as aplay -L)
alsa::device_name::HintIter::new_str(None, "pcm")
.map(|hint_iter| Self {
hint_iter,
enumerated_pcm_ids: HashSet::new(),
})
.map_err(DevicesError::from)
}
}

unsafe impl Send for Devices {}
unsafe impl Sync for Devices {}

const BUILTINS: [&str; 5] = ["default", "pipewire", "pulse", "jack", "oss"];

impl Iterator for Devices {
type Item = Device;

fn next(&mut self) -> Option<Device> {
while self.builtin_pos < BUILTINS.len() {
let pos = self.builtin_pos;
self.builtin_pos += 1;
let name = BUILTINS[pos];

if let Ok(handles) = DeviceHandles::open(name) {
return Some(Device {
name: name.to_string(),
pcm_id: name.to_string(),
handles: Arc::new(Mutex::new(handles)),
});
}
}

loop {
let res = self.card_iter.next()?;
let Ok(card) = res else { continue };

let ctl_id = format!("hw:{}", card.get_index());
let Ok(ctl) = alsa::Ctl::new(&ctl_id, false) else {
continue;
};
let Ok(cardinfo) = ctl.card_info() else {
continue;
};
let Ok(card_name) = cardinfo.get_name() else {
continue;
};

// Using plughw adds the ALSA plug layer, which can do sample type conversion,
// sample rate convertion, ...
// It is convenient, but at the same time not suitable for pro-audio as it hides
// the actual device capabilities and perform audio manipulation under your feet,
// for example sample rate conversion, sample format conversion, adds dummy channels,
// ...
// For now, many hardware only support 24bit / 3 bytes, which isn't yet supported by
// cpal. So we have to enable plughw (unfortunately) for maximum compatibility.
const USE_PLUGHW: bool = true;
let pcm_id = if USE_PLUGHW {
format!("plughw:{}", card.get_index())
} else {
ctl_id
};
if let Ok(handles) = DeviceHandles::open(&pcm_id) {
return Some(Device {
name: card_name.to_string(),
pcm_id: pcm_id.to_string(),
handles: Arc::new(Mutex::new(handles)),
});
let hint = self.hint_iter.next()?;
if let Ok(device) = Device::try_from(hint) {
if self.enumerated_pcm_ids.insert(device.pcm_id.clone()) {
return Some(device);
} else {
// Skip duplicate PCM IDs
continue;
}
}
}
}
}

#[inline]
pub fn default_input_device() -> Option<Device> {
Some(Device {
name: "default".to_owned(),
pcm_id: "default".to_owned(),
handles: Arc::new(Mutex::new(Default::default())),
})
Some(default_device())
}

#[inline]
pub fn default_output_device() -> Option<Device> {
Some(Device {
name: "default".to_owned(),
pcm_id: "default".to_owned(),
Some(default_device())
}

#[inline]
pub fn default_device() -> Device {
Device {
pcm_id: "default".to_string(),
desc: Some("Default Audio Device".to_string()),
handles: Arc::new(Mutex::new(Default::default())),
})
}
}

impl From<alsa::Error> for DevicesError {
Expand All @@ -105,3 +72,28 @@ impl From<alsa::Error> for DevicesError {
err.into()
}
}

impl TryFrom<alsa::device_name::Hint> for Device {
type Error = BackendSpecificError;

fn try_from(hint: alsa::device_name::Hint) -> Result<Self, Self::Error> {
let pcm_id = hint.name.ok_or_else(|| BackendSpecificError {
description: "ALSA hint missing PCM ID".to_string(),
})?;

// Try to open handles during enumeration
let handles = DeviceHandles::open(&pcm_id).unwrap_or_else(|_| {
// If opening fails during enumeration, create default handles
// The actual opening will be attempted when the device is used
DeviceHandles::default()
});

// Include all devices from ALSA hints (matches `aplay -L` behavior)
// Even devices that can't be opened during enumeration are valid for selection
Ok(Self {
pcm_id: pcm_id.to_owned(),
desc: hint.desc,
handles: Arc::new(Mutex::new(handles)),
})
}
}
55 changes: 34 additions & 21 deletions src/host/alsa/mod.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
extern crate alsa;
extern crate libc;

use std::{
cell::Cell,
cmp, fmt,
ops::RangeInclusive,
sync::{Arc, Mutex},
thread::{self, JoinHandle},
time::Duration,
vec::IntoIter as VecIntoIter,
};

use self::alsa::poll::Descriptors;
use crate::traits::{DeviceTrait, HostTrait, StreamTrait};
pub use self::enumerate::{default_input_device, default_output_device, Devices};

use crate::{
traits::{DeviceTrait, HostTrait, StreamTrait},
BackendSpecificError, BufferSize, BuildStreamError, ChannelCount, Data,
DefaultStreamConfigError, DeviceNameError, DevicesError, FrameCount, InputCallbackInfo,
OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig,
StreamError, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange,
SupportedStreamConfigsError,
};
use std::cell::Cell;
use std::cmp;
use std::convert::TryInto;
use std::ops::RangeInclusive;
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>;
Expand Down Expand Up @@ -241,8 +243,8 @@ impl DeviceHandles {

#[derive(Clone)]
pub struct Device {
name: String,
pcm_id: String,
desc: Option<String>,
handles: Arc<Mutex<DeviceHandles>>,
}

Expand Down Expand Up @@ -306,7 +308,7 @@ impl Device {

#[inline]
fn name(&self) -> Result<String, DeviceNameError> {
Ok(self.name.clone())
Ok(self.to_string())
}

fn supported_configs(
Expand Down Expand Up @@ -888,9 +890,8 @@ fn process_output(
}
Ok(result) if result != available_frames => {
let description = format!(
"unexpected number of frames written: expected {}, \
result {} (this should never happen)",
available_frames, result,
"unexpected number of frames written: expected {available_frames}, \
result {result} (this should never happen)"
);
error_callback(BackendSpecificError { description }.into());
continue;
Expand Down Expand Up @@ -940,7 +941,11 @@ fn stream_timestamp(
// 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
let nanos = ts.tv_sec * 1_000_000_000 + ts.tv_nsec;
#[cfg(target_pointer_width = "64")]
return nanos;
#[cfg(not(target_pointer_width = "64"))]
return nanos.into();
}

// Adapted from `timediff` here:
Expand Down Expand Up @@ -1098,8 +1103,7 @@ fn set_hw_params_from_format(
sample_format => {
return Err(BackendSpecificError {
description: format!(
"Sample format '{}' is not supported by this backend",
sample_format
"Sample format '{sample_format}' is not supported by this backend"
),
})
}
Expand All @@ -1123,8 +1127,7 @@ fn set_hw_params_from_format(
sample_format => {
return Err(BackendSpecificError {
description: format!(
"Sample format '{}' is not supported by this backend",
sample_format
"Sample format '{sample_format}' is not supported by this backend"
),
})
}
Expand Down Expand Up @@ -1328,3 +1331,13 @@ impl From<alsa::Error> for StreamError {
err.into()
}
}

impl fmt::Display for Device {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(desc) = &self.desc {
write!(f, "{} ({})", self.pcm_id, desc.replace('\n', ", "))
} else {
write!(f, "{}", self.pcm_id)
}
}
}
Loading