diff --git a/.github/workflows/platforms.yml b/.github/workflows/platforms.yml index f609655c2..1ae102312 100644 --- a/.github/workflows/platforms.yml +++ b/.github/workflows/platforms.yml @@ -34,10 +34,10 @@ env: MSRV_WASM: "1.85" MSRV_WINDOWS: "1.85" - PACKAGES_LINUX: libasound2-dev libjack-jackd2-dev libdbus-1-dev libpipewire-0.3-dev + PACKAGES_LINUX: libasound2-dev libdbus-1-dev libjack-jackd2-dev libpipewire-0.3-dev # System packages required by the docs.rs feature set and available on docs.rs build environment. - PACKAGES_DOCS_RS: libasound2-dev libjack-jackd2-dev libdbus-1-dev + PACKAGES_DOCS_RS: libasound2-dev libdbus-1-dev libjack-jackd2-dev ANDROID_COMPILE_SDK: "30" ANDROID_BUILD_TOOLS: "30.0.3" @@ -180,6 +180,41 @@ jobs: - name: Check PipeWire on Debian Bookworm run: cargo check --features pipewire --workspace --verbose + # MIPS (Tier 3 target, requires nightly + build-std; no native 64-bit atomics) + mipsel: + runs-on: ubuntu-latest + container: + image: buildpack-deps:bookworm + env: + TARGET: mipsel-unknown-linux-gnu + PKG_CONFIG_ALLOW_CROSS: "1" + PKG_CONFIG_PATH: /usr/lib/mipsel-linux-gnu/pkgconfig/ + CARGO_TARGET_MIPSEL_UNKNOWN_LINUX_GNU_LINKER: mipsel-linux-gnu-gcc + steps: + - uses: actions/checkout@v5 + + - name: Install mipsel cross-toolchain and audio libs + run: | + dpkg --add-architecture mipsel + apt-get update -qq + apt-get install -y --no-install-recommends libclang-dev crossbuild-essential-mipsel $(for p in ${{ env.PACKAGES_LINUX }}; do echo "$p:mipsel"; done) + + - name: Install Rust nightly (for build-std) + uses: dtolnay/rust-toolchain@nightly + with: + components: rust-src + + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + with: + key: mipsel + + - name: Check examples (default features) + run: cargo +nightly check --examples --workspace --verbose --target ${{ env.TARGET }} -Z build-std + + - name: Check examples (all features) + run: cargo +nightly check --examples --all-features --workspace --verbose --target ${{ env.TARGET }} -Z build-std + # Windows (x86_64 and i686) windows: strategy: @@ -577,18 +612,19 @@ jobs: publish-cpal: if: github.event_name == 'release' && startsWith(github.ref, 'refs/tags/v') needs: + - android + - docs-cpal + - ios - linux - linux-armv7 - - pipewire-bookworm - - windows - macos - - android - - ios + - mipsel + - pipewire-bookworm - tvos - - wasm-bindgen - wasm-audioworklet + - wasm-bindgen - wasm-wasip1 - - docs-cpal + - windows runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 diff --git a/CHANGELOG.md b/CHANGELOG.md index 150eb3f18..e4cd95b6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **JACK**: Channel enumeration is no longer capped at the physical system port count. - **PipeWire**: Fix streams starting audio before `play()` is called. - **PipeWire**: Streams for a specific device no longer auto-reroute if it disappears. +- **PipeWire**: Build on 32-bit targets without native 64-bit atomics. - **PulseAudio**: `NoData` errors are no longer misreported as buffer xruns. +- **PulseAudio**: Build on 32-bit targets without native 64-bit atomics. - **visionOS**: The CoreAudio backend now builds. - **WASAPI**: Default device changes no longer report `DeviceChanged`, which wrongly implied the stream had rerouted automatically. - **WASAPI**: Reported buffer sizes are no longer off by one frame. diff --git a/Cargo.toml b/Cargo.toml index 9b2f6e878..20cf51100 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,7 +74,7 @@ pipewire = ["dep:pipewire"] # Provides audio I/O support on Linux and some BSDs using the PulseAudio sound server # Requires: PulseAudio server and client libraries installed on the system # Platform: Linux, DragonFly BSD, FreeBSD, NetBSD -pulseaudio = ["dep:pulseaudio", "dep:futures"] +pulseaudio = ["dep:pulseaudio", "dep:futures", "dep:portable-atomic"] # WebAssembly backend using wasm-bindgen # Enables the Web Audio API backend for browser-based audio @@ -91,6 +91,11 @@ wasm-bindgen = [ [dependencies] dasp_sample = "0.11" +# Polyfills 64-bit atomics on targets without native support (e.g. mips, powerpc, +# riscv32); used by the pulseaudio backend's lock-free latency tracking. +[target.'cfg(not(target_has_atomic = "64"))'.dependencies] +portable-atomic = { version = "1", optional = true } + [dev-dependencies] anyhow = "1.0" hound = "3.5" diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index a74cd8d90..22cc57d97 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -4,7 +4,7 @@ use std::{ hash::{Hash, Hasher}, rc::Rc, sync::{ - atomic::{AtomicBool, AtomicU64, Ordering}, + atomic::{AtomicBool, AtomicU32, Ordering}, mpsc, Arc, }, thread, @@ -369,10 +369,10 @@ impl DeviceTrait for Device { let device = self.clone(); let wait_timeout = timeout.unwrap_or(Duration::from_secs(2)); let initial_quantum = match config.buffer_size { - BufferSize::Fixed(n) => n as u64, - BufferSize::Default => self.quantum as u64, + BufferSize::Fixed(n) => n, + BufferSize::Default => self.quantum, }; - let last_quantum = Arc::new(AtomicU64::new(initial_quantum)); + let last_quantum = Arc::new(AtomicU32::new(initial_quantum)); let last_quantum_clone = last_quantum.clone(); // Keep `capture` monotonic: pw_time delay() grows when another client joins // needing a larger buffer, which can pull `capture` backward. @@ -547,10 +547,10 @@ impl DeviceTrait for Device { let device = self.clone(); let wait_timeout = timeout.unwrap_or(Duration::from_secs(2)); let initial_quantum = match config.buffer_size { - BufferSize::Fixed(n) => n as u64, - BufferSize::Default => self.quantum as u64, + BufferSize::Fixed(n) => n, + BufferSize::Default => self.quantum, }; - let last_quantum = Arc::new(AtomicU64::new(initial_quantum)); + let last_quantum = Arc::new(AtomicU32::new(initial_quantum)); let last_quantum_clone = last_quantum.clone(); // Keep `playback` monotonic: pw_time delay() shrinks when other clients that needed // a larger buffer leave the graph, which can pull `playback` backward. diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index 92692aad1..b0d829cd7 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -2,7 +2,7 @@ use std::{ cell::RefCell, rc::Rc, sync::{ - atomic::{AtomicBool, AtomicU64, Ordering}, + atomic::{AtomicBool, AtomicU32, Ordering}, Arc, Mutex, }, thread::JoinHandle, @@ -81,7 +81,7 @@ pub(super) enum StreamCommand { pub struct Stream { handle: Option>, controller: pw::channel::Sender, - last_quantum: Arc, + last_quantum: Arc, start: Instant, latch: Latch, } @@ -90,7 +90,7 @@ impl Stream { pub(super) fn new( handle: JoinHandle<()>, controller: pw::channel::Sender, - last_quantum: Arc, + last_quantum: Arc, start: Instant, latch: Latch, ) -> Self { @@ -224,7 +224,7 @@ pub struct UserData { error_callback: ErrorCallbackArc, sample_format: SampleFormat, format: AudioInfoRaw, - last_quantum: Arc, + last_quantum: Arc, start: Instant, is_default_device: bool, has_connected: bool, @@ -311,14 +311,14 @@ where fn publish_data_in(&mut self, stream: &pw::stream::Stream, frames: usize, data: &Data) { #[cfg(feature = "realtime")] { - let prev = self.last_quantum.swap(frames as u64, Ordering::Relaxed); - if !self.rt_promoted || frames as u64 != prev { + let prev = self.last_quantum.swap(frames as u32, Ordering::Relaxed); + if !self.rt_promoted || frames as u32 != prev { self.promote_realtime(frames as FrameCount); } } #[cfg(not(feature = "realtime"))] - self.last_quantum.store(frames as u64, Ordering::Relaxed); + self.last_quantum.store(frames as u32, Ordering::Relaxed); let (callback, capture) = match pw_stream_time(stream) { Some(t) => { @@ -352,14 +352,14 @@ where fn publish_data_out(&mut self, stream: &pw::stream::Stream, frames: usize, data: &mut Data) { #[cfg(feature = "realtime")] { - let prev = self.last_quantum.swap(frames as u64, Ordering::Relaxed); - if !self.rt_promoted || frames as u64 != prev { + let prev = self.last_quantum.swap(frames as u32, Ordering::Relaxed); + if !self.rt_promoted || frames as u32 != prev { self.promote_realtime(frames as FrameCount); } } #[cfg(not(feature = "realtime"))] - self.last_quantum.store(frames as u64, Ordering::Relaxed); + self.last_quantum.store(frames as u32, Ordering::Relaxed); let (callback, playback) = match pw_stream_time(stream) { Some(t) => { @@ -539,7 +539,7 @@ pub struct ConnectParams { pub config: StreamConfig, pub properties: PropertiesBox, pub sample_format: SampleFormat, - pub last_quantum: Arc, + pub last_quantum: Arc, pub start: Instant, pub connect_automatically: bool, pub is_default_device: bool, diff --git a/src/host/pulseaudio/stream.rs b/src/host/pulseaudio/stream.rs index d7c0c2e49..2f4f97c6f 100644 --- a/src/host/pulseaudio/stream.rs +++ b/src/host/pulseaudio/stream.rs @@ -1,11 +1,16 @@ use std::{ sync::{ - atomic::{AtomicBool, AtomicU64, Ordering}, + atomic::{AtomicBool, Ordering}, Arc, Condvar, Mutex, }, time::{Duration, Instant}, }; +#[cfg(not(target_has_atomic = "64"))] +use portable_atomic::AtomicU64; +#[cfg(target_has_atomic = "64")] +use std::sync::atomic::AtomicU64; + use futures::executor::block_on; use futures::FutureExt as _; use pulseaudio::{protocol, AsPlaybackSource};