feat(macos): gate Linux-only audio backends with cfg
Add #[cfg(target_os = "linux")] to pipewire_backend and pulse_backend modules. Add CpalDefaultBackend for non-Linux platforms using CPAL default host (CoreAudio on macOS). Gate reset_pipewire_clock() calls.
This commit is contained in:
4 files changed
+150
-13
No files matched your search
@@ -7,10 +7,13 @@
|
||||
use alsa::pcm::{Access, Format, HwParams, PCM};
|
||||
#[cfg(target_os = "linux")]
|
||||
use alsa::{Direction, ValueOr};
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::sync::atomic::AtomicBool;
|
||||
#[cfg(target_os = "linux")]
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
/// Direct ALSA PCM stream for hw: devices
|
||||
#[cfg(target_os = "linux")]
|
||||
pub struct AlsaDirectStream {
|
||||
pcm: Arc<Mutex<PCM>>,
|
||||
#[allow(dead_code)]
|
||||
@@ -21,9 +24,16 @@ pub struct AlsaDirectStream {
|
||||
device_id: String,
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub struct AlsaDirectStream {
|
||||
sample_rate: u32,
|
||||
channels: u16,
|
||||
device_id: String,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
impl AlsaDirectStream {
|
||||
/// Create new ALSA direct stream
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn new(device_id: &str, sample_rate: u32, channels: u16) -> Result<Self, String> {
|
||||
log::info!(
|
||||
"[ALSA Direct] Opening device: {} ({}Hz, {}ch)",
|
||||
@@ -127,7 +137,6 @@ impl AlsaDirectStream {
|
||||
}
|
||||
|
||||
/// Write audio samples to ALSA (auto-converts from i16 based on detected format)
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn write(&self, samples_i16: &[i16]) -> Result<(), String> {
|
||||
let pcm = self.pcm.lock().unwrap();
|
||||
let frames = samples_i16.len() / self.channels as usize;
|
||||
@@ -296,7 +305,6 @@ impl AlsaDirectStream {
|
||||
///
|
||||
/// f32 has 24 bits of significand, so 24-bit audio is preserved losslessly.
|
||||
/// This is the primary write path for the f32 pipeline.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn write_f32(&self, samples_f32: &[f32]) -> Result<(), String> {
|
||||
let pcm = self.pcm.lock().unwrap();
|
||||
let frames = samples_f32.len() / self.channels as usize;
|
||||
@@ -469,7 +477,6 @@ impl AlsaDirectStream {
|
||||
}
|
||||
|
||||
/// Drain and stop playback
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn drain(&self) -> Result<(), String> {
|
||||
log::info!("[ALSA Direct] Draining PCM");
|
||||
let pcm = self.pcm.lock().unwrap();
|
||||
@@ -478,7 +485,6 @@ impl AlsaDirectStream {
|
||||
}
|
||||
|
||||
/// Stop PCM immediately (prepare for next playback)
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn stop(&self) -> Result<(), String> {
|
||||
log::info!("[ALSA Direct] Stopping PCM");
|
||||
let pcm = self.pcm.lock().unwrap();
|
||||
@@ -510,7 +516,6 @@ impl AlsaDirectStream {
|
||||
/// - Mixer API fails
|
||||
///
|
||||
/// NOTE: Failure doesn't break playback, just means volume can't be controlled.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn set_hardware_volume(&self, volume: f32) -> Result<(), String> {
|
||||
use alsa::mixer::SelemChannelId::*;
|
||||
use alsa::mixer::{Mixer, SelemId};
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
//! allowing users to choose their preferred audio stack.
|
||||
|
||||
use rodio::MixerDeviceSink;
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
use rodio::{
|
||||
cpal::traits::{DeviceTrait, HostTrait},
|
||||
DeviceSinkBuilder,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Supported audio backends
|
||||
@@ -266,8 +271,15 @@ impl BackendManager {
|
||||
pub fn create_backend(backend_type: AudioBackendType) -> BackendResult<Box<dyn AudioBackend>> {
|
||||
match backend_type {
|
||||
AudioBackendType::PipeWire => {
|
||||
let backend = crate::pipewire_backend::PipeWireBackend::new()?;
|
||||
Ok(Box::new(backend))
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let backend = crate::pipewire_backend::PipeWireBackend::new()?;
|
||||
Ok(Box::new(backend))
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
Ok(Box::new(CpalDefaultBackend::new()?))
|
||||
}
|
||||
}
|
||||
AudioBackendType::Alsa => {
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -281,8 +293,15 @@ impl BackendManager {
|
||||
}
|
||||
}
|
||||
AudioBackendType::Pulse => {
|
||||
let backend = crate::pulse_backend::PulseBackend::new()?;
|
||||
Ok(Box::new(backend))
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let backend = crate::pulse_backend::PulseBackend::new()?;
|
||||
Ok(Box::new(backend))
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
Err("PulseAudio backend only available on Linux".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -309,3 +328,100 @@ impl BackendManager {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// CPAL default backend for non-Linux platforms (macOS CoreAudio, Windows WASAPI).
|
||||
/// Uses the system default audio device via CPAL without any platform-specific commands.
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub struct CpalDefaultBackend {
|
||||
host: rodio::cpal::Host,
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
impl CpalDefaultBackend {
|
||||
pub fn new() -> BackendResult<Self> {
|
||||
Ok(Self {
|
||||
host: rodio::cpal::default_host(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
impl AudioBackend for CpalDefaultBackend {
|
||||
fn backend_type(&self) -> AudioBackendType {
|
||||
AudioBackendType::PipeWire // Reuse the enum variant for non-Linux default
|
||||
}
|
||||
|
||||
fn enumerate_devices(&self) -> BackendResult<Vec<AudioDevice>> {
|
||||
let default_device = self
|
||||
.host
|
||||
.default_output_device()
|
||||
.ok_or_else(|| "No default output device found".to_string())?;
|
||||
|
||||
let default_name = default_device
|
||||
.description()
|
||||
.map(|desc| desc.name().to_string())
|
||||
.unwrap_or_else(|_| "Default Output".to_string());
|
||||
|
||||
let mut devices = Vec::new();
|
||||
for device in self
|
||||
.host
|
||||
.output_devices()
|
||||
.map_err(|e| format!("Failed to enumerate output devices: {}", e))?
|
||||
{
|
||||
let name = device
|
||||
.description()
|
||||
.map(|desc| desc.name().to_string())
|
||||
.unwrap_or_else(|_| "Unknown Device".to_string());
|
||||
let is_default = name == default_name;
|
||||
devices.push(AudioDevice {
|
||||
id: name.clone(),
|
||||
name,
|
||||
description: None,
|
||||
is_default,
|
||||
max_sample_rate: None,
|
||||
supported_sample_rates: None,
|
||||
device_bus: None,
|
||||
is_hardware: false,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(devices)
|
||||
}
|
||||
|
||||
fn create_output_stream(&self, config: &BackendConfig) -> BackendResult<MixerDeviceSink> {
|
||||
let device = if let Some(ref device_id) = config.device_id {
|
||||
self.host
|
||||
.output_devices()
|
||||
.map_err(|e| format!("Failed to enumerate devices: {}", e))?
|
||||
.find(|d| {
|
||||
d.description()
|
||||
.map(|desc| desc.name() == device_id.as_str())
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.ok_or_else(|| format!("Device '{}' not found", device_id))?
|
||||
} else {
|
||||
self.host
|
||||
.default_output_device()
|
||||
.ok_or_else(|| "No default output device found".to_string())?
|
||||
};
|
||||
|
||||
let mixer_sink = DeviceSinkBuilder::from_device(device)
|
||||
.map_err(|e| format!("Failed to create device sink builder: {}", e))?
|
||||
.open_stream()
|
||||
.map_err(|e| format!("Failed to create output stream: {}", e))?;
|
||||
|
||||
Ok(mixer_sink)
|
||||
}
|
||||
|
||||
fn is_available(&self) -> bool {
|
||||
self.host.default_output_device().is_some()
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"System Audio - Default audio output"
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -26,22 +26,26 @@
|
||||
//! └───────────────┘
|
||||
//! ```
|
||||
|
||||
pub mod backend;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod pipewire_backend;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod alsa_backend;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod pulse_backend;
|
||||
pub mod alsa_direct;
|
||||
pub mod analysis;
|
||||
pub mod analyzer_tap;
|
||||
pub mod backend;
|
||||
pub mod diagnostic;
|
||||
pub mod dynamic_amplify;
|
||||
pub mod loudness;
|
||||
pub mod loudness_analyzer;
|
||||
pub mod loudness_cache;
|
||||
pub mod pipewire_backend;
|
||||
pub mod pulse_backend;
|
||||
pub mod settings;
|
||||
pub mod visualizer;
|
||||
|
||||
// Re-export commonly used types
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use alsa_backend::{
|
||||
device_supports_sample_rate, get_device_supported_rates, normalize_device_id_to_stable,
|
||||
resolve_stable_to_current_hw,
|
||||
@@ -60,3 +64,13 @@ pub use loudness_analyzer::LoudnessAnalyzer;
|
||||
pub use loudness_cache::LoudnessCache;
|
||||
pub use settings::AudioSettings;
|
||||
pub use visualizer::{RingBuffer, TappedSource, VisualizerTap};
|
||||
|
||||
// Stub implementations for non-Linux platforms
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub fn normalize_device_id_to_stable(_id: &str) -> String { _id.to_string() }
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub fn resolve_stable_to_current_hw(_stable: &str) -> Option<String> { None }
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub fn device_supports_sample_rate(_device_id: &str, _sample_rate: u32) -> Option<bool> { None }
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub fn get_device_supported_rates(_device_id: &str) -> Option<Vec<u32>> { None }
|
||||
@@ -2003,6 +2003,7 @@ impl Player {
|
||||
drop(stream_opt.take());
|
||||
*pause_suspend_deadline = None;
|
||||
// Reset PipeWire clock if bit-perfect was active
|
||||
#[cfg(target_os = "linux")]
|
||||
if thread_settings
|
||||
.lock()
|
||||
.ok()
|
||||
@@ -2454,6 +2455,7 @@ impl Player {
|
||||
drop(stream_opt.take());
|
||||
pause_suspend_deadline = None;
|
||||
// Reset PipeWire clock if bit-perfect was active
|
||||
#[cfg(target_os = "linux")]
|
||||
if thread_settings
|
||||
.lock()
|
||||
.ok()
|
||||
|
||||
Reference in new issue
Block a user