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:
Afonso Jorge Ramos committed 2026-03-20 00:45:03 +01:00
1 parent 352d589276
commit 5ed549d46b
4 files changed
+150 -13

No files matched your search

+11 -6
View File
@@ -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};
+120 -4
View File
@@ -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
}
}
+17 -3
View File
@@ -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 }
+2
View File
@@ -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()