feat(macos): add CoreAudio device probing and sample rate switching
Add coreaudio_direct module using coreaudio-rs safe wrappers for: - Device capability probing (supported sample rates, transport type) - Automatic sample rate switching per-track - Device lookup by name for CoreAudio↔CPAL bridging Uses a fork branch of coreaudio-rs that adds get_available_sample_rates and get_device_transport_type helpers (PR RustAudio/coreaudio-rs#149).
This commit is contained in:
4 files changed
+186
-1
No files matched your search
@@ -39,3 +39,8 @@ dirs = "5"
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
# ALSA for direct hardware access (bit-perfect)
|
||||
alsa = "0.9"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
# CoreAudio for device probing, sample rate switching, and (future) hog mode / exclusive mode
|
||||
# Using fork branch until https://github.com/RustAudio/coreaudio-rs/pull/149 is merged
|
||||
coreaudio-rs = { git = "https://github.com/afonsojramos/coreaudio-rs.git", branch = "feat/device-helpers" }
|
||||
@@ -0,0 +1,165 @@
|
||||
//! CoreAudio direct access for macOS
|
||||
//!
|
||||
//! Provides device capability probing and sample rate switching on macOS
|
||||
//! using the coreaudio-rs safe wrappers.
|
||||
//!
|
||||
//! Phase 1: Device probing + nominal sample rate switching (shared mode)
|
||||
//! Phase 2 (future): Hog mode + integer mode + IO proc for bit-perfect playback
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use coreaudio::audio_unit::macos_helpers;
|
||||
#[cfg(target_os = "macos")]
|
||||
use coreaudio::audio_unit::Scope;
|
||||
|
||||
/// CoreAudio device ID (re-exported so callers don't need objc2_core_audio)
|
||||
#[cfg(target_os = "macos")]
|
||||
pub type AudioDeviceID = u32;
|
||||
|
||||
// CoreAudio transport type constants (FourCC values from AudioHardware.h)
|
||||
#[cfg(target_os = "macos")]
|
||||
mod transport_types {
|
||||
pub const BUILT_IN: u32 = 0x626c746e; // 'bltn'
|
||||
pub const USB: u32 = 0x75736220; // 'usb '
|
||||
pub const BLUETOOTH: u32 = 0x626c7565; // 'blue'
|
||||
pub const BLUETOOTH_LE: u32 = 0x626c6561; // 'blea'
|
||||
pub const HDMI: u32 = 0x68646d69; // 'hdmi'
|
||||
pub const DISPLAY_PORT: u32 = 0x64707274; // 'dprt'
|
||||
pub const THUNDERBOLT: u32 = 0x7468756e; // 'thun'
|
||||
pub const FIREWIRE: u32 = 0x31333934; // '1394'
|
||||
pub const VIRTUAL: u32 = 0x76697274; // 'virt'
|
||||
pub const AGGREGATE: u32 = 0x67727570; // 'grup'
|
||||
}
|
||||
|
||||
/// Common audio sample rates to check against device capabilities
|
||||
#[cfg(target_os = "macos")]
|
||||
const COMMON_SAMPLE_RATES: &[u32] = &[
|
||||
44100, 48000, 88200, 96000, 176400, 192000, 352800, 384000, 705600, 768000,
|
||||
];
|
||||
|
||||
/// Query supported sample rates for a CoreAudio device.
|
||||
/// Returns discrete rates from the device's available nominal sample rate ranges.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn query_supported_sample_rates(device_id: AudioDeviceID) -> Result<Vec<u32>, String> {
|
||||
let ranges = macos_helpers::get_available_sample_rates(device_id)
|
||||
.map_err(|e| format!("Failed to get sample rate ranges: {:?}", e))?;
|
||||
|
||||
let mut rates = Vec::new();
|
||||
for range in &ranges {
|
||||
if (range.mMinimum - range.mMaximum).abs() < 0.5 {
|
||||
// Point value (min == max)
|
||||
rates.push(range.mMinimum as u32);
|
||||
} else {
|
||||
// Continuous range — check which common rates fall within it
|
||||
for &rate in COMMON_SAMPLE_RATES {
|
||||
let rate_f = rate as f64;
|
||||
if rate_f >= range.mMinimum && rate_f <= range.mMaximum {
|
||||
rates.push(rate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rates.sort_unstable();
|
||||
rates.dedup();
|
||||
Ok(rates)
|
||||
}
|
||||
|
||||
/// Set the nominal sample rate of a device.
|
||||
/// Delegates to coreaudio-rs which handles async confirmation with a 2-second timeout.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn set_nominal_sample_rate(device_id: AudioDeviceID, target_rate: u32) -> Result<(), String> {
|
||||
log::info!(
|
||||
"[CoreAudio] Switching sample rate to {}Hz on device {}",
|
||||
target_rate,
|
||||
device_id
|
||||
);
|
||||
|
||||
macos_helpers::set_device_sample_rate(device_id, target_rate as f64)
|
||||
.map_err(|e| format!("Failed to set sample rate to {}Hz: {:?}", target_rate, e))?;
|
||||
|
||||
log::info!("[CoreAudio] Sample rate switched to {}Hz", target_rate);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the default output device ID.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn get_default_output_device() -> Result<AudioDeviceID, String> {
|
||||
macos_helpers::get_default_device_id(false)
|
||||
.ok_or_else(|| "No default output device found".to_string())
|
||||
}
|
||||
|
||||
/// Get all output device IDs.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn get_output_device_ids() -> Result<Vec<AudioDeviceID>, String> {
|
||||
macos_helpers::get_audio_device_ids_for_scope(Scope::Output)
|
||||
.map_err(|e| format!("Failed to enumerate output devices: {:?}", e))
|
||||
}
|
||||
|
||||
/// Get the name of a CoreAudio device.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn get_device_name(device_id: AudioDeviceID) -> Result<String, String> {
|
||||
macos_helpers::get_device_name(device_id)
|
||||
.map_err(|e| format!("Failed to get device name: {:?}", e))
|
||||
}
|
||||
|
||||
/// Find a CoreAudio output device ID by its name.
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn find_device_by_name(name: &str) -> Result<Option<AudioDeviceID>, String> {
|
||||
// get_device_id_from_name: input=false means output device
|
||||
Ok(macos_helpers::get_device_id_from_name(name, false))
|
||||
}
|
||||
|
||||
/// Get the transport type of a device (USB, built-in, Bluetooth, etc.)
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn get_device_transport_type(device_id: AudioDeviceID) -> Option<String> {
|
||||
let transport = macos_helpers::get_device_transport_type(device_id).ok()?;
|
||||
|
||||
let transport_str = if transport == transport_types::BUILT_IN {
|
||||
"built-in"
|
||||
} else if transport == transport_types::USB {
|
||||
"usb"
|
||||
} else if transport == transport_types::BLUETOOTH
|
||||
|| transport == transport_types::BLUETOOTH_LE
|
||||
{
|
||||
"bluetooth"
|
||||
} else if transport == transport_types::HDMI
|
||||
|| transport == transport_types::DISPLAY_PORT
|
||||
{
|
||||
"hdmi"
|
||||
} else if transport == transport_types::THUNDERBOLT {
|
||||
"thunderbolt"
|
||||
} else if transport == transport_types::FIREWIRE {
|
||||
"firewire"
|
||||
} else if transport == transport_types::VIRTUAL {
|
||||
"virtual"
|
||||
} else if transport == transport_types::AGGREGATE {
|
||||
"aggregate"
|
||||
} else {
|
||||
"unknown"
|
||||
};
|
||||
|
||||
Some(transport_str.to_string())
|
||||
}
|
||||
|
||||
// ---- Non-macOS stubs ----
|
||||
|
||||
/// Query supported sample rates (stub for non-macOS)
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub fn query_supported_sample_rates(_device_name: &str) -> Result<Vec<u32>, String> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
/// Get the current nominal sample rate (stub for non-macOS)
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub fn get_nominal_sample_rate_by_name(_device_name: &str) -> Result<u32, String> {
|
||||
Err("CoreAudio is only available on macOS".to_string())
|
||||
}
|
||||
|
||||
/// Set the nominal sample rate (stub for non-macOS)
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub fn set_nominal_sample_rate_by_name(
|
||||
_device_name: &str,
|
||||
_target_rate: u32,
|
||||
) -> Result<(), String> {
|
||||
Err("CoreAudio is only available on macOS".to_string())
|
||||
}
|
||||
@@ -34,6 +34,7 @@ pub mod alsa_backend;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod pulse_backend;
|
||||
pub mod alsa_direct;
|
||||
pub mod coreaudio_direct;
|
||||
pub mod analysis;
|
||||
pub mod analyzer_tap;
|
||||
pub mod diagnostic;
|
||||
|
||||
Generated
+15
-1
@@ -1077,6 +1077,19 @@ dependencies = [
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coreaudio-rs"
|
||||
version = "0.14.0"
|
||||
source = "git+https://github.com/afonsojramos/coreaudio-rs.git?branch=feat%2Fdevice-helpers#ce3fec9bce0b43ece78bb8801c87be9889e5710e"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"libc",
|
||||
"objc2-audio-toolbox",
|
||||
"objc2-core-audio",
|
||||
"objc2-core-audio-types",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpal"
|
||||
version = "0.17.1"
|
||||
@@ -1084,7 +1097,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b1f9c7312f19fc2fa12fd7acaf38de54e8320ba10d1a02dcbe21038def51ccb"
|
||||
dependencies = [
|
||||
"alsa 0.10.0",
|
||||
"coreaudio-rs",
|
||||
"coreaudio-rs 0.13.0",
|
||||
"dasp_sample",
|
||||
"jni",
|
||||
"js-sys",
|
||||
@@ -4496,6 +4509,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"alsa 0.9.1",
|
||||
"async-trait",
|
||||
"coreaudio-rs 0.14.0",
|
||||
"dirs 5.0.1",
|
||||
"ebur128",
|
||||
"log",
|
||||
|
||||
Reference in new issue
Block a user