From 13677c848928606491fc8b90f58087ae24429a08 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Mon, 5 Jan 2026 22:48:42 +0900 Subject: [PATCH 01/85] chore: little process --- Cargo.toml | 1 + src/host/mod.rs | 5 + src/host/pipewire/device.rs | 353 ++++++++++++++++++++++++++++++++++++ src/host/pipewire/mod.rs | 12 ++ 4 files changed, 371 insertions(+) create mode 100644 src/host/pipewire/device.rs create mode 100644 src/host/pipewire/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 33fbaa1eb..1da779d41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,7 @@ custom = [] [dependencies] dasp_sample = "0.11" +pipewire = "0.9.2" [dev-dependencies] anyhow = "1.0" diff --git a/src/host/mod.rs b/src/host/mod.rs index d4e7a3ec9..a740e0eb6 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -7,6 +7,11 @@ pub(crate) mod aaudio; target_os = "netbsd" ))] pub(crate) mod alsa; +#[cfg(all( + any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"), + //feature = "pipewire" +))] +pub(crate) mod pipewire; #[cfg(all(windows, feature = "asio"))] pub(crate) mod asio; #[cfg(all( diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs new file mode 100644 index 000000000..8b6283f49 --- /dev/null +++ b/src/host/pipewire/device.rs @@ -0,0 +1,353 @@ +use std::{cell::RefCell, rc::Rc}; + +use crate::DeviceDirection; +use pipewire::{ + self as pw, + metadata::{Metadata, MetadataListener}, + node::{Node, NodeListener}, + proxy::ProxyT, + spa::utils::result::AsyncSeq, +}; + +pub type Devices = std::vec::IntoIter; + +#[derive(Clone, Debug, Default, Copy)] +pub(crate) enum DeviceType { + #[default] + Node, + DefaultSink, + DefaultInput, + DefaultOutput, +} + +#[derive(Clone, Debug, Default)] +pub struct Device { + id: u32, + node_name: String, + nick_name: String, + description: String, + direction: DeviceDirection, + channels: usize, + limit_quantum: u32, + rate: u32, + allow_rates: Vec, + quantum: u32, + min_quantum: u32, + max_quantum: u32, + device_type: DeviceType, +} + +impl Device { + fn sink_default() -> Self { + Self { + id: 0, + node_name: "sink_default".to_owned(), + nick_name: "sink_default".to_owned(), + description: "default_sink".to_owned(), + direction: DeviceDirection::Input, + channels: 2, + device_type: DeviceType::DefaultSink, + ..Default::default() + } + } + fn input_default() -> Self { + Self { + id: 0, + node_name: "input_default".to_owned(), + nick_name: "input_default".to_owned(), + description: "default_input".to_owned(), + direction: DeviceDirection::Input, + channels: 2, + device_type: DeviceType::DefaultInput, + ..Default::default() + } + } + fn output_default() -> Self { + Self { + id: 0, + node_name: "output_default".to_owned(), + nick_name: "output_default".to_owned(), + description: "default_output".to_owned(), + direction: DeviceDirection::Output, + channels: 2, + device_type: DeviceType::DefaultOutput, + ..Default::default() + } + } +} + +impl Device { + pub fn id(&self) -> u32 { + self.id + } + pub fn name(&self) -> &str { + &self.nick_name + } + pub fn channels(&self) -> usize { + self.channels + } + pub fn direction(&self) -> DeviceDirection { + self.direction + } + pub fn node_name(&self) -> &str { + &self.node_name + } + pub fn description(&self) -> &str { + &self.description + } + pub fn limit_quantam(&self) -> u32 { + self.limit_quantum + } + pub fn min_quantum(&self) -> u32 { + self.min_quantum + } + pub fn max_quantum(&self) -> u32 { + self.max_quantum + } + pub fn quantum(&self) -> u32 { + self.quantum + } + pub fn rate(&self) -> u32 { + self.rate + } + pub fn allow_rates(&self) -> &[u32] { + &self.allow_rates + } +} + +#[derive(Debug, Clone, Default)] +struct Settings { + rate: u32, + allow_rates: Vec, + quantum: u32, + min_quantum: u32, + max_quantum: u32, +} + +#[allow(dead_code)] +enum Request { + Node(NodeListener), + Meta(MetadataListener), +} + +impl From for Request { + fn from(value: NodeListener) -> Self { + Self::Node(value) + } +} + +impl From for Request { + fn from(value: MetadataListener) -> Self { + Self::Meta(value) + } +} + +fn init_roundtrip() -> Option> { + let mainloop = pw::main_loop::MainLoopRc::new(None).ok()?; + let context = pw::context::ContextRc::new(&mainloop, None).ok()?; + let core = context.connect_rc(None).ok()?; + let registry = core.get_registry_rc().ok()?; + + // To comply with Rust's safety rules, we wrap this variable in an `Rc` and a `Cell`. + let devices: Rc>> = Rc::new(RefCell::new(vec![ + Device::sink_default(), + Device::input_default(), + Device::output_default(), + ])); + let requests = Rc::new(RefCell::new(vec![])); + let settings = Rc::new(RefCell::new(Settings::default())); + let loop_clone = mainloop.clone(); + + // Trigger the sync event. The server's answer won't be processed until we start the main loop, + // so we can safely do this before setting up a callback. This lets us avoid using a Cell. + let peddings: Rc>> = Rc::new(RefCell::new(vec![])); + let pending = core.sync(0).expect("sync failed"); + + peddings.borrow_mut().push(pending); + + let _listener_core = core + .add_listener_local() + .done({ + let peddings = peddings.clone(); + move |id, seq| { + if id != pw::core::PW_ID_CORE { + return; + } + let mut peddinglist = peddings.borrow_mut(); + let Some(index) = peddinglist.iter().position(|o_seq| *o_seq == seq) else { + return; + }; + peddinglist.remove(index); + if !peddinglist.is_empty() { + return; + } + loop_clone.quit(); + } + }) + .register(); + let _listener_reg = registry + .add_listener_local() + .global({ + let devices = devices.clone(); + let registry = registry.clone(); + let requests = requests.clone(); + let settings = settings.clone(); + move |global| match global.type_ { + pipewire::types::ObjectType::Metadata => { + if !global.props.is_some_and(|props| { + props + .get("metadata.name") + .is_some_and(|name| name == "settings") + }) { + return; + } + let meta_settings: Metadata = registry.bind(global).unwrap(); + let settings = settings.clone(); + let listener = meta_settings + .add_listener_local() + .property(move |_, key, _, value| { + match (key, value) { + (Some("clock.rate"), Some(rate)) => { + let Ok(rate) = rate.parse() else { + return 0; + }; + settings.borrow_mut().rate = rate; + } + (Some("clock.allowed-rates"), Some(list)) => { + let Some(list) = list.strip_prefix("[") else { + return 0; + }; + let Some(list) = list.strip_suffix("]") else { + return 0; + }; + let list = list.trim(); + let list: Vec<&str> = list.split(' ').collect(); + let mut allow_rates = vec![]; + for rate in list { + let Ok(rate) = rate.parse() else { + return 0; + }; + allow_rates.push(rate); + } + settings.borrow_mut().allow_rates = allow_rates; + } + (Some("clock.quantum"), Some(quantum)) => { + let Ok(quantum) = quantum.parse() else { + return 0; + }; + settings.borrow_mut().quantum = quantum; + } + (Some("clock.min-quantum"), Some(min_quantum)) => { + let Ok(min_quantum) = min_quantum.parse() else { + return 0; + }; + settings.borrow_mut().min_quantum = min_quantum; + } + (Some("clock.max-quantum"), Some(max_quantum)) => { + let Ok(max_quantum) = max_quantum.parse() else { + return 0; + }; + settings.borrow_mut().max_quantum = max_quantum; + } + _ => {} + } + 0 + }) + .register(); + let pending = core.sync(0).expect("sync failed"); + peddings.borrow_mut().push(pending); + requests + .borrow_mut() + .push((meta_settings.upcast(), Request::Meta(listener))); + } + pipewire::types::ObjectType::Node => { + let Some(props) = global.props else { + return; + }; + let Some(media_class) = props.get("media.class") else { + return; + }; + if !matches!(media_class, "Audio/Sink" | "Audio/Source") { + return; + } + + let node: Node = registry.bind(global).expect("should ok"); + + let devices = devices.clone(); + let listener = node + .add_listener_local() + .info(move |info| { + let Some(props) = info.props() else { + return; + }; + let Some(media_class) = props.get("media.class") else { + return; + }; + let direction = match media_class { + "Audio/Sink" => DeviceDirection::Input, + "Audio/Source" => DeviceDirection::Output, + _ => { + return; + } + }; + let id = info.id(); + let node_name = props.get("node.name").unwrap_or("unknown").to_owned(); + let nick_name = props.get("node.nick").unwrap_or("unknown").to_owned(); + let description = props + .get("node.description") + .unwrap_or("unknown") + .to_owned(); + let channels: usize = props + .get("audio.channels") + .and_then(|channels| channels.parse().ok()) + .unwrap_or(2); + let limit_quantum: u32 = props + .get("clock.quantum-limit") + .and_then(|channels| channels.parse().ok()) + .unwrap_or(0); + let device = Device { + id, + node_name, + nick_name, + description, + direction, + channels, + limit_quantum, + ..Default::default() + }; + devices.borrow_mut().push(device); + }) + .register(); + let pending = core.sync(0).expect("sync failed"); + peddings.borrow_mut().push(pending); + requests + .borrow_mut() + .push((node.upcast(), Request::Node(listener))); + } + _ => {} + } + }) + .register(); + + mainloop.run(); + + let mut devices = devices.take(); + let settings = settings.take(); + for device in devices.iter_mut() { + device.rate = settings.rate; + device.allow_rates = settings.allow_rates.clone(); + device.quantum = settings.quantum; + device.min_quantum = settings.min_quantum; + device.max_quantum = settings.max_quantum; + } + Some(devices) +} + +pub fn init_devices() -> Option> { + pw::init(); + let devices = init_roundtrip()?; + unsafe { + pw::deinit(); + } + Some(devices) +} diff --git a/src/host/pipewire/mod.rs b/src/host/pipewire/mod.rs new file mode 100644 index 000000000..19c9b2d02 --- /dev/null +++ b/src/host/pipewire/mod.rs @@ -0,0 +1,12 @@ +use device::{init_devices, Device}; +mod device; + +#[derive(Debug)] +pub struct Host(Vec); + +impl Host { + pub fn new() -> Result { + let devices = init_devices().ok_or(crate::HostUnavailable)?; + Ok(Host(devices)) + } +} From f385d75e57ddf2a8334b6f98c22e64700b2ad2cc Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Mon, 5 Jan 2026 23:28:50 +0900 Subject: [PATCH 02/85] feat: base settings --- Cargo.toml | 5 +- src/host/mod.rs | 2 +- src/host/pipewire/device.rs | 166 +++++++++++++++++++++++++++++++++--- src/host/pipewire/mod.rs | 28 +++++- src/platform/mod.rs | 17 +++- 5 files changed, 202 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1da779d41..e2998b965 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ pulseaudio = ["dep:pulseaudio", "dep:futures"] # Platform: Linux, DragonFly BSD, FreeBSD, NetBSD, macOS, Windows # Note: JACK must be installed separately on all platforms jack = ["dep:jack"] +pipewire = ["dep:pipewire"] # Audio thread priority elevation # Raises the audio callback thread to real-time priority for lower latency and fewer glitches @@ -65,9 +66,10 @@ audioworklet = [ # Platform: All platforms custom = [] +default = ["pipewire"] + [dependencies] dasp_sample = "0.11" -pipewire = "0.9.2" [dev-dependencies] anyhow = "1.0" @@ -104,6 +106,7 @@ audio_thread_priority = { version = "0.34", optional = true } jack = { version = "0.13", optional = true } pulseaudio = { version = "0.3", optional = true } futures = { version = "0.3", optional = true } +pipewire = { version = "0.9.2", optional = true } [target.'cfg(target_vendor = "apple")'.dependencies] mach2 = "0.5" diff --git a/src/host/mod.rs b/src/host/mod.rs index a740e0eb6..de66c395f 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -9,7 +9,7 @@ pub(crate) mod aaudio; pub(crate) mod alsa; #[cfg(all( any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"), - //feature = "pipewire" + feature = "pipewire" ))] pub(crate) mod pipewire; #[cfg(all(windows, feature = "asio"))] diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 8b6283f49..6de9e1c25 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -1,6 +1,11 @@ use std::{cell::RefCell, rc::Rc}; -use crate::DeviceDirection; +use crate::{ + traits::{DeviceTrait, StreamTrait}, + DeviceDirection, SupportedStreamConfigRange, +}; + +use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs}; use pipewire::{ self as pw, metadata::{Metadata, MetadataListener}, @@ -20,6 +25,7 @@ pub(crate) enum DeviceType { DefaultOutput, } +#[allow(unused)] #[derive(Clone, Debug, Default)] pub struct Device { id: u32, @@ -27,7 +33,7 @@ pub struct Device { nick_name: String, description: String, direction: DeviceDirection, - channels: usize, + channels: u16, limit_quantum: u32, rate: u32, allow_rates: Vec, @@ -38,6 +44,9 @@ pub struct Device { } impl Device { + pub(crate) fn device_type(&self) -> DeviceType { + self.device_type + } fn sink_default() -> Self { Self { id: 0, @@ -76,14 +85,149 @@ impl Device { } } -impl Device { - pub fn id(&self) -> u32 { - self.id +// TODO: +pub struct Stream; + +impl StreamTrait for Stream { + fn play(&self) -> Result<(), crate::PlayStreamError> { + todo!() + } + fn pause(&self) -> Result<(), crate::PauseStreamError> { + todo!() + } +} + +impl DeviceTrait for Device { + type Stream = Stream; + type SupportedInputConfigs = SupportedInputConfigs; + type SupportedOutputConfigs = SupportedOutputConfigs; + + fn id(&self) -> Result { + Ok(crate::DeviceId( + crate::HostId::PipeWire, + self.nick_name.clone(), + )) + } + + // TODO: device type + fn description(&self) -> Result { + Ok(crate::DeviceDescriptionBuilder::new(&self.nick_name) + .direction(self.direction()) + .build()) + } + + fn supports_input(&self) -> bool { + matches!( + self.direction, + DeviceDirection::Input | DeviceDirection::Duplex + ) + } + + fn supports_output(&self) -> bool { + matches!( + self.direction, + DeviceDirection::Output | DeviceDirection::Duplex + ) + } + + // TODO: sample_format + fn supported_input_configs( + &self, + ) -> Result { + if !self.supports_input() { + return Err(crate::SupportedStreamConfigsError::DeviceNotAvailable); + } + Ok(vec![SupportedStreamConfigRange { + channels: self.channels, + min_sample_rate: self.rate, + max_sample_rate: self.rate, + buffer_size: crate::SupportedBufferSize::Range { + min: self.min_quantum, + max: self.max_quantum, + }, + sample_format: crate::SampleFormat::I32, + }] + .into_iter()) } - pub fn name(&self) -> &str { - &self.nick_name + fn supported_output_configs( + &self, + ) -> Result { + if !self.supports_output() { + return Err(crate::SupportedStreamConfigsError::DeviceNotAvailable); + } + Ok(vec![SupportedStreamConfigRange { + channels: self.channels, + min_sample_rate: self.rate, + max_sample_rate: self.rate, + buffer_size: crate::SupportedBufferSize::Range { + min: self.min_quantum, + max: self.max_quantum, + }, + sample_format: crate::SampleFormat::I32, + }] + .into_iter()) + } + fn default_input_config( + &self, + ) -> Result { + Ok(crate::SupportedStreamConfig { + channels: self.channels, + sample_format: crate::SampleFormat::I32, + sample_rate: self.rate, + buffer_size: crate::SupportedBufferSize::Range { + min: self.min_quantum, + max: self.max_quantum, + }, + }) } - pub fn channels(&self) -> usize { + + fn default_output_config( + &self, + ) -> Result { + Ok(crate::SupportedStreamConfig { + channels: self.channels, + sample_format: crate::SampleFormat::I32, + sample_rate: self.rate, + buffer_size: crate::SupportedBufferSize::Range { + min: self.min_quantum, + max: self.max_quantum, + }, + }) + } + + fn build_input_stream_raw( + &self, + config: &crate::StreamConfig, + sample_format: crate::SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&crate::Data, &crate::InputCallbackInfo) + Send + 'static, + E: FnMut(crate::StreamError) + Send + 'static, + { + todo!() + } + + fn build_output_stream_raw( + &self, + config: &crate::StreamConfig, + sample_format: crate::SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&mut crate::Data, &crate::OutputCallbackInfo) + Send + 'static, + E: FnMut(crate::StreamError) + Send + 'static, + { + todo!() + } +} + +impl Device { + pub fn channels(&self) -> u16 { self.channels } pub fn direction(&self) -> DeviceDirection { @@ -92,9 +236,7 @@ impl Device { pub fn node_name(&self) -> &str { &self.node_name } - pub fn description(&self) -> &str { - &self.description - } + pub fn limit_quantam(&self) -> u32 { self.limit_quantum } @@ -297,7 +439,7 @@ fn init_roundtrip() -> Option> { .get("node.description") .unwrap_or("unknown") .to_owned(); - let channels: usize = props + let channels = props .get("audio.channels") .and_then(|channels| channels.parse().ok()) .unwrap_or(2); diff --git a/src/host/pipewire/mod.rs b/src/host/pipewire/mod.rs index 19c9b2d02..2e84ce7fc 100644 --- a/src/host/pipewire/mod.rs +++ b/src/host/pipewire/mod.rs @@ -1,4 +1,6 @@ -use device::{init_devices, Device}; +use device::{init_devices, Device, DeviceType, Devices}; + +use crate::traits::HostTrait; mod device; #[derive(Debug)] @@ -10,3 +12,27 @@ impl Host { Ok(Host(devices)) } } + +impl HostTrait for Host { + type Devices = Devices; + type Device = Device; + fn is_available() -> bool { + true + } + fn devices(&self) -> Result { + Ok(self.0.clone().into_iter()) + } + + fn default_input_device(&self) -> Option { + self.0 + .iter() + .find(|device| matches!(device.device_type(), DeviceType::DefaultSink)) + .cloned() + } + fn default_output_device(&self) -> Option { + self.0 + .iter() + .find(|device| matches!(device.device_type(), DeviceType::DefaultOutput)) + .cloned() + } +} diff --git a/src/platform/mod.rs b/src/platform/mod.rs index da00b89fc..bd4eb55c6 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -716,11 +716,26 @@ mod platform_impl { #[cfg(feature = "pulseaudio")] pub use crate::host::pulseaudio::Host as PulseAudioHost; + #[cfg(feature = "pipewire")] + #[cfg_attr( + docsrs, + doc(cfg(all( + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd" + ), + feature = "jack" + ))) + )] + pub use crate::host::pipewire::Host as PipeWireHost; impl_platform_host!( #[cfg(feature = "pulseaudio")] PulseAudio => PulseAudioHost, #[cfg(feature = "jack")] Jack => JackHost, Alsa => AlsaHost, - #[cfg(feature = "custom")] Custom => super::CustomHost + #[cfg(feature = "custom")] Custom => super::CustomHost, + #[cfg(feature = "pipewire")] PipeWire => super::PipeWireHost, ); /// The default host for the current compilation target platform. From 46904a64836509d8b4cce2613d12e00dc71bf5f0 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Tue, 6 Jan 2026 11:33:52 +0900 Subject: [PATCH 03/85] chore: base connect function --- src/host/pipewire/device.rs | 19 +--- src/host/pipewire/mod.rs | 2 +- src/host/pipewire/stream.rs | 196 ++++++++++++++++++++++++++++++++++++ 3 files changed, 201 insertions(+), 16 deletions(-) create mode 100644 src/host/pipewire/stream.rs diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 6de9e1c25..774939f81 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -1,9 +1,6 @@ use std::{cell::RefCell, rc::Rc}; -use crate::{ - traits::{DeviceTrait, StreamTrait}, - DeviceDirection, SupportedStreamConfigRange, -}; +use crate::{traits::DeviceTrait, DeviceDirection, SupportedStreamConfigRange}; use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs}; use pipewire::{ @@ -14,6 +11,8 @@ use pipewire::{ spa::utils::result::AsyncSeq, }; +use super::stream::Stream; + pub type Devices = std::vec::IntoIter; #[derive(Clone, Debug, Default, Copy)] @@ -83,20 +82,10 @@ impl Device { ..Default::default() } } -} - -// TODO: -pub struct Stream; - -impl StreamTrait for Stream { - fn play(&self) -> Result<(), crate::PlayStreamError> { - todo!() - } - fn pause(&self) -> Result<(), crate::PauseStreamError> { + pub(crate) fn pw_properties(&self) -> pw::properties::Properties { todo!() } } - impl DeviceTrait for Device { type Stream = Stream; type SupportedInputConfigs = SupportedInputConfigs; diff --git a/src/host/pipewire/mod.rs b/src/host/pipewire/mod.rs index 2e84ce7fc..02e493db2 100644 --- a/src/host/pipewire/mod.rs +++ b/src/host/pipewire/mod.rs @@ -2,7 +2,7 @@ use device::{init_devices, Device, DeviceType, Devices}; use crate::traits::HostTrait; mod device; - +mod stream; #[derive(Debug)] pub struct Host(Vec); diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs new file mode 100644 index 000000000..bfc9a0d67 --- /dev/null +++ b/src/host/pipewire/stream.rs @@ -0,0 +1,196 @@ +use std::time::Duration; + +use crate::{traits::StreamTrait, InputCallbackInfo, SampleFormat, StreamConfig, StreamError}; +use pipewire::{ + self as pw, + context::ContextRc, + main_loop::MainLoopRc, + spa::{ + param::{ + format::{MediaSubtype, MediaType}, + format_utils, + }, + pod::Pod, + }, + stream::{StreamListener, StreamRc}, +}; + +use crate::Data; +pub struct Stream; + +impl StreamTrait for Stream { + fn play(&self) -> Result<(), crate::PlayStreamError> { + todo!() + } + fn pause(&self) -> Result<(), crate::PauseStreamError> { + todo!() + } +} + +impl From for pw::spa::param::audio::AudioFormat { + fn from(value: SampleFormat) -> Self { + match value { + SampleFormat::I8 => Self::S8, + SampleFormat::U8 => Self::U8, + + #[cfg(target_endian = "little")] + SampleFormat::I16 => Self::S16LE, + #[cfg(target_endian = "big")] + SampleFormat::I16 => Self::S16BE, + #[cfg(target_endian = "little")] + SampleFormat::U16 => Self::U16LE, + #[cfg(target_endian = "big")] + SampleFormat::U16 => Self::U16BE, + + #[cfg(target_endian = "little")] + SampleFormat::I24 => Self::S24LE, + #[cfg(target_endian = "big")] + SampleFormat::I24 => Self::S24BE, + #[cfg(target_endian = "little")] + SampleFormat::U24 => Self::U24LE, + #[cfg(target_endian = "big")] + SampleFormat::U24 => Self::U24BE, + #[cfg(target_endian = "little")] + SampleFormat::I32 => Self::S32LE, + #[cfg(target_endian = "big")] + SampleFormat::I32 => Self::S32BE, + #[cfg(target_endian = "little")] + SampleFormat::U32 => Self::U32LE, + #[cfg(target_endian = "big")] + SampleFormat::U32 => Self::U32BE, + #[cfg(target_endian = "little")] + SampleFormat::F32 => Self::F64BE, + #[cfg(target_endian = "big")] + SampleFormat::F32 => Self::F32BE, + #[cfg(target_endian = "little")] + SampleFormat::F64 => Self::F64LE, + #[cfg(target_endian = "big")] + SampleFormat::F64 => Self::F64BE, + SampleFormat::I64 => Self::Unknown, + SampleFormat::U64 => Self::Unknown, + } + } +} +struct UserData { + data_callback: D, + error_callback: E, + samples_format: SampleFormat, + format: pw::spa::param::audio::AudioInfoRaw, +} +struct StreamData { + mainloop: MainLoopRc, + listener: StreamListener>, + stream: StreamRc, + context: ContextRc, +} + +fn connect_input( + config: &StreamConfig, + properties: pw::properties::PropertiesBox, + samples_format: SampleFormat, + data_callback: D, + error_callback: E, + _timeout: Option, +) -> Result, pw::Error> +where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, +{ + pw::init(); + let mainloop = pw::main_loop::MainLoopRc::new(None)?; + let context = pw::context::ContextRc::new(&mainloop, None)?; + let core = context.connect_rc(None)?; + + let data = UserData { + data_callback, + error_callback, + samples_format, + format: Default::default(), + }; + + let stream = pw::stream::StreamRc::new(core, "cpal-capture", properties)?; + let listener = stream + .add_local_listener_with_user_data(data) + .param_changed(|_, user_data, id, param| { + let Some(param) = param else { + return; + }; + if id != pw::spa::param::ParamType::Format.as_raw() { + return; + } + + let (media_type, media_subtype) = match format_utils::parse_format(param) { + Ok(v) => v, + Err(_) => return, + }; + + // only accept raw audio + if media_type != MediaType::Audio || media_subtype != MediaSubtype::Raw { + return; + } + + // call a helper function to parse the format for us. + user_data + .format + .parse(param) + .expect("Failed to parse param changed to AudioInfoRaw"); + }) + .process(|stream, user_data| match stream.dequeue_buffer() { + None => (user_data.error_callback)(StreamError::BufferUnderrun), + Some(mut buffer) => { + let datas = buffer.datas_mut(); + if datas.is_empty() { + return; + } + let data = &mut datas[0]; + let n_channels = user_data.format.channels(); + let n_samples = data.chunk().size() / user_data.samples_format.sample_size() as u32; + + let Some(samples) = data.data() else { + return; + }; + let data = samples.as_ptr() as *mut (); + let _data = + unsafe { Data::from_parts(data, n_samples as usize, user_data.samples_format) }; + //(user_data.data_callback)(&data,crate::InputCallbackInfo::new(Ins) ) + } + }) + .register()?; + let mut audio_info = pw::spa::param::audio::AudioInfoRaw::new(); + audio_info.set_format(samples_format.into()); + audio_info.set_rate(config.sample_rate); + audio_info.set_channels(config.channels as u32); + + let obj = pw::spa::pod::Object { + type_: pw::spa::utils::SpaTypes::ObjectParamFormat.as_raw(), + id: pw::spa::param::ParamType::EnumFormat.as_raw(), + properties: audio_info.into(), + }; + let values: Vec = pw::spa::pod::serialize::PodSerializer::serialize( + std::io::Cursor::new(Vec::new()), + &pw::spa::pod::Value::Object(obj), + ) + .unwrap() + .0 + .into_inner(); + + let mut params = [Pod::from_bytes(&values).unwrap()]; + + /* Now connect this stream. We ask that our process function is + * called in a realtime thread. */ + stream.connect( + pw::spa::utils::Direction::Input, + None, + pw::stream::StreamFlags::AUTOCONNECT + | pw::stream::StreamFlags::MAP_BUFFERS + | pw::stream::StreamFlags::RT_PROCESS, + &mut params, + )?; + + Ok(StreamData { + mainloop, + listener, + stream, + context, + }) +} From 38d35dfc5d92753e74ed7abc52f7ffc1a6fb44d8 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Tue, 6 Jan 2026 18:59:59 +0900 Subject: [PATCH 04/85] chore: nearly finished --- Cargo.toml | 2 +- src/host/pipewire/device.rs | 159 ++++++++++++++++++++++++++++++++++-- src/host/pipewire/stream.rs | 156 +++++++++++++++++++++++++++++++---- 3 files changed, 291 insertions(+), 26 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e2998b965..c11f6efd8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,7 +106,7 @@ audio_thread_priority = { version = "0.34", optional = true } jack = { version = "0.13", optional = true } pulseaudio = { version = "0.3", optional = true } futures = { version = "0.3", optional = true } -pipewire = { version = "0.9.2", optional = true } +pipewire = { version = "0.9.2", optional = true, features = ["v0_3_44"]} [target.'cfg(target_vendor = "apple")'.dependencies] mach2 = "0.5" diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 774939f81..c7a6c63fa 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -1,5 +1,7 @@ +use std::time::Duration; use std::{cell::RefCell, rc::Rc}; +use crate::host::pipewire::stream::StreamData; use crate::{traits::DeviceTrait, DeviceDirection, SupportedStreamConfigRange}; use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs}; @@ -11,6 +13,8 @@ use pipewire::{ spa::utils::result::AsyncSeq, }; +use std::thread; + use super::stream::Stream; pub type Devices = std::vec::IntoIter; @@ -24,6 +28,13 @@ pub(crate) enum DeviceType { DefaultOutput, } +#[derive(Clone, Debug, Default, Copy)] +pub enum Role { + Sink, + #[default] + Source, +} + #[allow(unused)] #[derive(Clone, Debug, Default)] pub struct Device { @@ -40,6 +51,8 @@ pub struct Device { min_quantum: u32, max_quantum: u32, device_type: DeviceType, + object_id: String, + role: Role, } impl Device { @@ -55,6 +68,7 @@ impl Device { direction: DeviceDirection::Input, channels: 2, device_type: DeviceType::DefaultSink, + role: Role::Sink, ..Default::default() } } @@ -67,6 +81,7 @@ impl Device { direction: DeviceDirection::Input, channels: 2, device_type: DeviceType::DefaultInput, + role: Role::Source, ..Default::default() } } @@ -79,11 +94,31 @@ impl Device { direction: DeviceDirection::Output, channels: 2, device_type: DeviceType::DefaultOutput, + role: Role::Source, ..Default::default() } } - pub(crate) fn pw_properties(&self) -> pw::properties::Properties { - todo!() + pub(crate) fn pw_properties(&self) -> pw::properties::PropertiesBox { + let mut properties = match self.direction { + DeviceDirection::Output => pw::properties::properties! { + *pw::keys::MEDIA_TYPE => "Audio", + *pw::keys::MEDIA_CATEGORY => "Playback", + *pw::keys::MEDIA_ROLE => "Music", + }, + DeviceDirection::Input => pw::properties::properties! { + *pw::keys::MEDIA_TYPE => "Audio", + *pw::keys::MEDIA_CATEGORY => "Capture", + *pw::keys::MEDIA_ROLE => "Music", + }, + _ => unreachable!(), + }; + if matches!(self.role, Role::Sink) { + properties.insert(*pw::keys::STREAM_CAPTURE_SINK, "true"); + } + if matches!(self.device_type, DeviceType::Node) { + properties.insert(*pw::keys::TARGET_OBJECT, self.object_id.to_owned()); + } + properties } } impl DeviceTrait for Device { @@ -196,7 +231,54 @@ impl DeviceTrait for Device { D: FnMut(&crate::Data, &crate::InputCallbackInfo) + Send + 'static, E: FnMut(crate::StreamError) + Send + 'static, { - todo!() + let (pw_play_tx, pw_play_rv) = pw::channel::channel::(); + + let (pw_init_tx, pw_init_rv) = std::sync::mpsc::channel::(); + let device = self.clone(); + let config = config.clone(); + let wait_timeout = timeout.clone().unwrap_or(Duration::from_secs(2)); + let handle = thread::Builder::new() + .name("pw_capture_music_in".to_owned()) + .spawn(move || { + let properties = device.pw_properties(); + let Ok(StreamData { + mainloop, + listener, + stream, + context, + }) = super::stream::connect_input( + &config, + properties, + sample_format, + data_callback, + error_callback, + timeout, + ) + else { + let _ = pw_init_tx.send(false); + return; + }; + let _ = pw_init_tx.send(true); + let stream = stream.clone(); + let _receiver = pw_play_rv.attach(mainloop.loop_(), move |play| { + let _ = stream.set_active(play); + }); + mainloop.run(); + drop(listener); + drop(context); + }) + .unwrap(); + if pw_init_rv + .recv_timeout(wait_timeout) + .ok() + .is_none_or(|re| !re) + { + return Err(crate::BuildStreamError::DeviceNotAvailable); + }; + Ok(Stream { + handle, + controller: pw_play_tx, + }) } fn build_output_stream_raw( @@ -211,7 +293,54 @@ impl DeviceTrait for Device { D: FnMut(&mut crate::Data, &crate::OutputCallbackInfo) + Send + 'static, E: FnMut(crate::StreamError) + Send + 'static, { - todo!() + let (pw_play_tx, pw_play_rv) = pw::channel::channel::(); + + let (pw_init_tx, pw_init_rv) = std::sync::mpsc::channel::(); + let device = self.clone(); + let config = config.clone(); + let wait_timeout = timeout.clone().unwrap_or(Duration::from_secs(2)); + let handle = thread::Builder::new() + .name("pw_capture_music_out".to_owned()) + .spawn(move || { + let properties = device.pw_properties(); + let Ok(StreamData { + mainloop, + listener, + stream, + context, + }) = super::stream::connect_output( + &config, + properties, + sample_format, + data_callback, + error_callback, + timeout, + ) + else { + let _ = pw_init_tx.send(false); + return; + }; + let _ = pw_init_tx.send(true); + let stream = stream.clone(); + let _receiver = pw_play_rv.attach(mainloop.loop_(), move |play| { + let _ = stream.set_active(play); + }); + mainloop.run(); + drop(listener); + drop(context); + }) + .unwrap(); + if pw_init_rv + .recv_timeout(wait_timeout) + .ok() + .is_none_or(|re| !re) + { + return Err(crate::BuildStreamError::DeviceNotAvailable); + }; + Ok(Stream { + handle, + controller: pw_play_tx, + }) } } @@ -414,13 +543,26 @@ fn init_roundtrip() -> Option> { let Some(media_class) = props.get("media.class") else { return; }; - let direction = match media_class { - "Audio/Sink" => DeviceDirection::Input, - "Audio/Source" => DeviceDirection::Output, + let role = match media_class { + "Audio/Sink" => Role::Sink, + "Audio/Source" => Role::Source, _ => { return; } }; + let Some(group) = props.get("port.group") else { + return; + }; + let direction = match group { + "playback" => DeviceDirection::Input, + "capture" => DeviceDirection::Output, + _ => { + return; + } + }; + let Some(object_id) = props.get("object.id") else { + return; + }; let id = info.id(); let node_name = props.get("node.name").unwrap_or("unknown").to_owned(); let nick_name = props.get("node.nick").unwrap_or("unknown").to_owned(); @@ -436,14 +578,17 @@ fn init_roundtrip() -> Option> { .get("clock.quantum-limit") .and_then(|channels| channels.parse().ok()) .unwrap_or(0); + let device = Device { id, node_name, nick_name, description, direction, + role, channels, limit_quantum, + object_id: object_id.to_owned(), ..Default::default() }; devices.borrow_mut().push(device); diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index bfc9a0d67..328925f4d 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -1,6 +1,9 @@ -use std::time::Duration; +use std::{thread::JoinHandle, time::Duration}; -use crate::{traits::StreamTrait, InputCallbackInfo, SampleFormat, StreamConfig, StreamError}; +use crate::{ + traits::StreamTrait, InputCallbackInfo, OutputCallbackInfo, SampleFormat, StreamConfig, + StreamError, +}; use pipewire::{ self as pw, context::ContextRc, @@ -16,14 +19,21 @@ use pipewire::{ }; use crate::Data; -pub struct Stream; + +#[allow(unused)] +pub struct Stream { + pub(crate) handle: JoinHandle<()>, + pub(crate) controller: pw::channel::Sender, +} impl StreamTrait for Stream { fn play(&self) -> Result<(), crate::PlayStreamError> { - todo!() + let _ = self.controller.send(true); + Ok(()) } fn pause(&self) -> Result<(), crate::PauseStreamError> { - todo!() + let _ = self.controller.send(false); + Ok(()) } } @@ -71,23 +81,133 @@ impl From for pw::spa::param::audio::AudioFormat { } } } -struct UserData { + +pub struct UserData { data_callback: D, error_callback: E, - samples_format: SampleFormat, + sample_format: SampleFormat, format: pw::spa::param::audio::AudioInfoRaw, } -struct StreamData { - mainloop: MainLoopRc, - listener: StreamListener>, - stream: StreamRc, - context: ContextRc, +pub struct StreamData { + pub mainloop: MainLoopRc, + pub listener: StreamListener>, + pub stream: StreamRc, + pub context: ContextRc, } +pub fn connect_output( + config: &StreamConfig, + properties: pw::properties::PropertiesBox, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + _timeout: Option, +) -> Result, pw::Error> +where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, +{ + pw::init(); + let mainloop = pw::main_loop::MainLoopRc::new(None)?; + let context = pw::context::ContextRc::new(&mainloop, None)?; + let core = context.connect_rc(None)?; -fn connect_input( + let data = UserData { + data_callback, + error_callback, + sample_format, + format: Default::default(), + }; + + let stream = pw::stream::StreamRc::new(core, "cpal-capture", properties)?; + let listener = stream + .add_local_listener_with_user_data(data) + .param_changed(|_, user_data, id, param| { + let Some(param) = param else { + return; + }; + if id != pw::spa::param::ParamType::Format.as_raw() { + return; + } + + let (media_type, media_subtype) = match format_utils::parse_format(param) { + Ok(v) => v, + Err(_) => return, + }; + + // only accept raw audio + if media_type != MediaType::Audio || media_subtype != MediaSubtype::Raw { + return; + } + + // call a helper function to parse the format for us. + user_data + .format + .parse(param) + .expect("Failed to parse param changed to AudioInfoRaw"); + }) + .process(|stream, user_data| match stream.dequeue_buffer() { + None => (user_data.error_callback)(StreamError::BufferUnderrun), + Some(mut buffer) => { + let datas = buffer.datas_mut(); + if datas.is_empty() { + return; + } + let data = &mut datas[0]; + let n_channels = user_data.format.channels(); + let n_samples = data.chunk().size() / user_data.sample_format.sample_size() as u32; + + let Some(samples) = data.data() else { + return; + }; + let data = samples.as_ptr() as *mut (); + let _data = + unsafe { Data::from_parts(data, n_samples as usize, user_data.sample_format) }; + //(user_data.data_callback)(&data,crate::InputCallbackInfo::new(Ins) ) + } + }) + .register()?; + let mut audio_info = pw::spa::param::audio::AudioInfoRaw::new(); + audio_info.set_format(sample_format.into()); + audio_info.set_rate(config.sample_rate); + audio_info.set_channels(config.channels as u32); + + let obj = pw::spa::pod::Object { + type_: pw::spa::utils::SpaTypes::ObjectParamFormat.as_raw(), + id: pw::spa::param::ParamType::EnumFormat.as_raw(), + properties: audio_info.into(), + }; + let values: Vec = pw::spa::pod::serialize::PodSerializer::serialize( + std::io::Cursor::new(Vec::new()), + &pw::spa::pod::Value::Object(obj), + ) + .unwrap() + .0 + .into_inner(); + + let mut params = [Pod::from_bytes(&values).unwrap()]; + + /* Now connect this stream. We ask that our process function is + * called in a realtime thread. */ + stream.connect( + pw::spa::utils::Direction::Output, + None, + pw::stream::StreamFlags::AUTOCONNECT + | pw::stream::StreamFlags::MAP_BUFFERS + | pw::stream::StreamFlags::RT_PROCESS, + &mut params, + )?; + + Ok(StreamData { + mainloop, + listener, + stream, + context, + }) +} +pub fn connect_input( config: &StreamConfig, properties: pw::properties::PropertiesBox, - samples_format: SampleFormat, + sample_format: SampleFormat, data_callback: D, error_callback: E, _timeout: Option, @@ -104,7 +224,7 @@ where let data = UserData { data_callback, error_callback, - samples_format, + sample_format, format: Default::default(), }; @@ -144,20 +264,20 @@ where } let data = &mut datas[0]; let n_channels = user_data.format.channels(); - let n_samples = data.chunk().size() / user_data.samples_format.sample_size() as u32; + let n_samples = data.chunk().size() / user_data.sample_format.sample_size() as u32; let Some(samples) = data.data() else { return; }; let data = samples.as_ptr() as *mut (); let _data = - unsafe { Data::from_parts(data, n_samples as usize, user_data.samples_format) }; + unsafe { Data::from_parts(data, n_samples as usize, user_data.sample_format) }; //(user_data.data_callback)(&data,crate::InputCallbackInfo::new(Ins) ) } }) .register()?; let mut audio_info = pw::spa::param::audio::AudioInfoRaw::new(); - audio_info.set_format(samples_format.into()); + audio_info.set_format(sample_format.into()); audio_info.set_rate(config.sample_rate); audio_info.set_channels(config.channels as u32); From 2ff6ce6791d90321f16432501cecb27dab5dd9f4 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Tue, 6 Jan 2026 21:08:19 +0900 Subject: [PATCH 05/85] chore: seems working well --- _typos.toml | 5 ++ src/host/pipewire/device.rs | 37 +++++++----- src/host/pipewire/mod.rs | 2 + src/host/pipewire/stream.rs | 113 ++++++++++++++++++++++++++++++++---- src/platform/mod.rs | 2 +- 5 files changed, 130 insertions(+), 29 deletions(-) create mode 100644 _typos.toml diff --git a/_typos.toml b/_typos.toml new file mode 100644 index 000000000..35e0a4db7 --- /dev/null +++ b/_typos.toml @@ -0,0 +1,5 @@ +[files] +extend-exclude = ["**/*.cmake", "**/*.json", "assets_for_test/*"] + +[default.extend-words] +datas = "datas" diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index c7a6c63fa..65a21df4a 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -52,6 +52,7 @@ pub struct Device { max_quantum: u32, device_type: DeviceType, object_id: String, + device_id: String, role: Role, } @@ -112,11 +113,12 @@ impl Device { }, _ => unreachable!(), }; + dbg!(&self); if matches!(self.role, Role::Sink) { properties.insert(*pw::keys::STREAM_CAPTURE_SINK, "true"); } if matches!(self.device_type, DeviceType::Node) { - properties.insert(*pw::keys::TARGET_OBJECT, self.object_id.to_owned()); + properties.insert(*pw::keys::TARGET_OBJECT, self.device_id.to_owned()); } properties } @@ -154,7 +156,6 @@ impl DeviceTrait for Device { ) } - // TODO: sample_format fn supported_input_configs( &self, ) -> Result { @@ -169,7 +170,7 @@ impl DeviceTrait for Device { min: self.min_quantum, max: self.max_quantum, }, - sample_format: crate::SampleFormat::I32, + sample_format: crate::SampleFormat::F32, }] .into_iter()) } @@ -187,7 +188,7 @@ impl DeviceTrait for Device { min: self.min_quantum, max: self.max_quantum, }, - sample_format: crate::SampleFormat::I32, + sample_format: crate::SampleFormat::F32, }] .into_iter()) } @@ -196,7 +197,7 @@ impl DeviceTrait for Device { ) -> Result { Ok(crate::SupportedStreamConfig { channels: self.channels, - sample_format: crate::SampleFormat::I32, + sample_format: crate::SampleFormat::F32, sample_rate: self.rate, buffer_size: crate::SupportedBufferSize::Range { min: self.min_quantum, @@ -210,7 +211,7 @@ impl DeviceTrait for Device { ) -> Result { Ok(crate::SupportedStreamConfig { channels: self.channels, - sample_format: crate::SampleFormat::I32, + sample_format: crate::SampleFormat::F32, sample_rate: self.rate, buffer_size: crate::SupportedBufferSize::Range { min: self.min_quantum, @@ -303,23 +304,27 @@ impl DeviceTrait for Device { .name("pw_capture_music_out".to_owned()) .spawn(move || { let properties = device.pw_properties(); - let Ok(StreamData { + + let StreamData { mainloop, listener, stream, context, - }) = super::stream::connect_output( + } = match super::stream::connect_output( &config, properties, sample_format, data_callback, error_callback, timeout, - ) - else { - let _ = pw_init_tx.send(false); - return; + ) { + Ok(data) => data, + Err(_) => { + let _ = pw_init_tx.send(false); + return; + } }; + let _ = pw_init_tx.send(true); let stream = stream.clone(); let _receiver = pw_play_rv.attach(mainloop.loop_(), move |play| { @@ -563,6 +568,9 @@ fn init_roundtrip() -> Option> { let Some(object_id) = props.get("object.id") else { return; }; + let Some(device_id) = props.get("device.id") else { + return; + }; let id = info.id(); let node_name = props.get("node.name").unwrap_or("unknown").to_owned(); let nick_name = props.get("node.nick").unwrap_or("unknown").to_owned(); @@ -589,6 +597,7 @@ fn init_roundtrip() -> Option> { channels, limit_quantum, object_id: object_id.to_owned(), + device_id: device_id.to_owned(), ..Default::default() }; devices.borrow_mut().push(device); @@ -620,10 +629,6 @@ fn init_roundtrip() -> Option> { } pub fn init_devices() -> Option> { - pw::init(); let devices = init_roundtrip()?; - unsafe { - pw::deinit(); - } Some(devices) } diff --git a/src/host/pipewire/mod.rs b/src/host/pipewire/mod.rs index 02e493db2..65b978371 100644 --- a/src/host/pipewire/mod.rs +++ b/src/host/pipewire/mod.rs @@ -3,11 +3,13 @@ use device::{init_devices, Device, DeviceType, Devices}; use crate::traits::HostTrait; mod device; mod stream; +use pipewire as pw; #[derive(Debug)] pub struct Host(Vec); impl Host { pub fn new() -> Result { + pw::init(); let devices = init_devices().ok_or(crate::HostUnavailable)?; Ok(Host(devices)) } diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index 328925f4d..8b16d18e9 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -1,8 +1,11 @@ -use std::{thread::JoinHandle, time::Duration}; +use std::{ + thread::JoinHandle, + time::{Duration, Instant}, +}; use crate::{ - traits::StreamTrait, InputCallbackInfo, OutputCallbackInfo, SampleFormat, StreamConfig, - StreamError, + traits::StreamTrait, BackendSpecificError, InputCallbackInfo, OutputCallbackInfo, SampleFormat, + StreamConfig, StreamError, }; use pipewire::{ self as pw, @@ -69,7 +72,7 @@ impl From for pw::spa::param::audio::AudioFormat { #[cfg(target_endian = "big")] SampleFormat::U32 => Self::U32BE, #[cfg(target_endian = "little")] - SampleFormat::F32 => Self::F64BE, + SampleFormat::F32 => Self::F32LE, #[cfg(target_endian = "big")] SampleFormat::F32 => Self::F32BE, #[cfg(target_endian = "little")] @@ -87,6 +90,52 @@ pub struct UserData { error_callback: E, sample_format: SampleFormat, format: pw::spa::param::audio::AudioInfoRaw, + created_instance: Instant, +} + +impl UserData +where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, +{ + fn publish_data_in(&mut self, frames: usize, data: &Data) -> Result<(), BackendSpecificError> { + let callback = stream_timestamp_fallback(self.created_instance)?; + let delay_duration = frames_to_duration(frames, self.format.rate()); + let capture = callback + .add(delay_duration) + .ok_or_else(|| BackendSpecificError { + description: "`playback` occurs beyond representation supported by `StreamInstant`" + .to_string(), + })?; + let timestamp = crate::InputStreamTimestamp { callback, capture }; + let info = crate::InputCallbackInfo { timestamp }; + (self.data_callback)(data, &info); + Ok(()) + } +} +impl UserData +where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, +{ + fn publish_data_out( + &mut self, + frames: usize, + data: &mut Data, + ) -> Result<(), BackendSpecificError> { + let callback = stream_timestamp_fallback(self.created_instance)?; + let delay_duration = frames_to_duration(frames, self.format.rate()); + let playback = callback + .add(delay_duration) + .ok_or_else(|| BackendSpecificError { + description: "`playback` occurs beyond representation supported by `StreamInstant`" + .to_string(), + })?; + let timestamp = crate::OutputStreamTimestamp { callback, playback }; + let info = crate::OutputCallbackInfo { timestamp }; + (self.data_callback)(data, &info); + Ok(()) + } } pub struct StreamData { pub mainloop: MainLoopRc, @@ -94,6 +143,30 @@ pub struct StreamData { pub stream: StreamRc, pub context: ContextRc, } + +// Use elapsed duration since stream creation as fallback when hardware timestamps are unavailable. +// +// This ensures positive values that are compatible with our `StreamInstant` representation. +#[inline] +fn stream_timestamp_fallback( + creation: std::time::Instant, +) -> Result { + let now = std::time::Instant::now(); + let duration = now.duration_since(creation); + crate::StreamInstant::from_nanos_i128(duration.as_nanos() as i128).ok_or(BackendSpecificError { + description: "stream duration has exceeded `StreamInstant` representation".to_string(), + }) +} + +// Convert the given duration in frames at the given sample rate to a `std::time::Duration`. +#[inline] +fn frames_to_duration(frames: usize, rate: crate::SampleRate) -> std::time::Duration { + let secsf = frames as f64 / rate as f64; + let secs = secsf as u64; + let nanos = ((secsf - secs as f64) * 1_000_000_000.0) as u32; + std::time::Duration::new(secs, nanos) +} + pub fn connect_output( config: &StreamConfig, properties: pw::properties::PropertiesBox, @@ -111,14 +184,16 @@ where let context = pw::context::ContextRc::new(&mainloop, None)?; let core = context.connect_rc(None)?; + dbg!(&properties); let data = UserData { data_callback, error_callback, sample_format, format: Default::default(), + created_instance: Instant::now(), }; - let stream = pw::stream::StreamRc::new(core, "cpal-capture", properties)?; + let stream = pw::stream::StreamRc::new(core, "cpal-playback", properties)?; let listener = stream .add_local_listener_with_user_data(data) .param_changed(|_, user_data, id, param| { @@ -152,17 +227,27 @@ where if datas.is_empty() { return; } - let data = &mut datas[0]; + let buf_data = &mut datas[0]; let n_channels = user_data.format.channels(); - let n_samples = data.chunk().size() / user_data.sample_format.sample_size() as u32; - let Some(samples) = data.data() else { + let Some(samples) = buf_data.data() else { return; }; + let stride = user_data.sample_format.sample_size() * n_channels as usize; + let frames = samples.len() / stride; + + let n_samples = samples.len() / user_data.sample_format.sample_size(); + let data = samples.as_ptr() as *mut (); - let _data = + let mut data = unsafe { Data::from_parts(data, n_samples as usize, user_data.sample_format) }; - //(user_data.data_callback)(&data,crate::InputCallbackInfo::new(Ins) ) + if let Err(err) = user_data.publish_data_out(frames, &mut data) { + (user_data.error_callback)(StreamError::BackendSpecific { err }); + } + let chunk = buf_data.chunk_mut(); + *chunk.offset_mut() = 0; + *chunk.stride_mut() = stride as i32; + *chunk.size_mut() = frames as u32; } }) .register()?; @@ -226,6 +311,7 @@ where error_callback, sample_format, format: Default::default(), + created_instance: Instant::now(), }; let stream = pw::stream::StreamRc::new(core, "cpal-capture", properties)?; @@ -265,14 +351,17 @@ where let data = &mut datas[0]; let n_channels = user_data.format.channels(); let n_samples = data.chunk().size() / user_data.sample_format.sample_size() as u32; + let frames = n_samples / n_channels; let Some(samples) = data.data() else { return; }; let data = samples.as_ptr() as *mut (); - let _data = + let data = unsafe { Data::from_parts(data, n_samples as usize, user_data.sample_format) }; - //(user_data.data_callback)(&data,crate::InputCallbackInfo::new(Ins) ) + if let Err(err) = user_data.publish_data_in(frames as usize, &data) { + (user_data.error_callback)(StreamError::BackendSpecific { err }); + } } }) .register()?; diff --git a/src/platform/mod.rs b/src/platform/mod.rs index bd4eb55c6..5cd6dd1e5 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -740,7 +740,7 @@ mod platform_impl { /// The default host for the current compilation target platform. pub fn default_host() -> Host { - AlsaHost::new() + PipeWireHost::new() .expect("the default host should always be available") .into() } From 7fe2e0e3166eea8108d945cab11065357333dff4 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Tue, 6 Jan 2026 21:22:54 +0900 Subject: [PATCH 06/85] fix: feature wrong in platform/mod.rs --- src/platform/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 5cd6dd1e5..250c00291 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -726,7 +726,7 @@ mod platform_impl { target_os = "freebsd", target_os = "netbsd" ), - feature = "jack" + feature = "pipewire" ))) )] pub use crate::host::pipewire::Host as PipeWireHost; From fd9527e4b781ea624d7aad4ef93692f55e58c01e Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Tue, 6 Jan 2026 21:24:05 +0900 Subject: [PATCH 07/85] chore: tidy up, clippy, fmt --- Cargo.toml | 2 +- src/host/mod.rs | 10 +++++----- src/host/pipewire/device.rs | 4 ++-- src/host/pipewire/stream.rs | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c11f6efd8..f11a3c8bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ documentation = "https://docs.rs/cpal" license = "Apache-2.0" keywords = ["audio", "sound"] edition = "2021" -rust-version = "1.78" +rust-version = "1.82" [features] # ASIO backend for Windows diff --git a/src/host/mod.rs b/src/host/mod.rs index de66c395f..ad023c77f 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -7,11 +7,6 @@ pub(crate) mod aaudio; target_os = "netbsd" ))] pub(crate) mod alsa; -#[cfg(all( - any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"), - feature = "pipewire" -))] -pub(crate) mod pipewire; #[cfg(all(windows, feature = "asio"))] pub(crate) mod asio; #[cfg(all( @@ -46,6 +41,11 @@ pub(crate) mod jack; feature = "pulseaudio" ))] pub(crate) mod pulseaudio; +#[cfg(all( + any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"), + feature = "pipewire" +))] +pub(crate) mod pipewire; #[cfg(windows)] pub(crate) mod wasapi; #[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 65a21df4a..ed53d0ab1 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -237,7 +237,7 @@ impl DeviceTrait for Device { let (pw_init_tx, pw_init_rv) = std::sync::mpsc::channel::(); let device = self.clone(); let config = config.clone(); - let wait_timeout = timeout.clone().unwrap_or(Duration::from_secs(2)); + let wait_timeout = timeout.unwrap_or(Duration::from_secs(2)); let handle = thread::Builder::new() .name("pw_capture_music_in".to_owned()) .spawn(move || { @@ -299,7 +299,7 @@ impl DeviceTrait for Device { let (pw_init_tx, pw_init_rv) = std::sync::mpsc::channel::(); let device = self.clone(); let config = config.clone(); - let wait_timeout = timeout.clone().unwrap_or(Duration::from_secs(2)); + let wait_timeout = timeout.unwrap_or(Duration::from_secs(2)); let handle = thread::Builder::new() .name("pw_capture_music_out".to_owned()) .spawn(move || { diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index 8b16d18e9..e63b5e02d 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -240,7 +240,7 @@ where let data = samples.as_ptr() as *mut (); let mut data = - unsafe { Data::from_parts(data, n_samples as usize, user_data.sample_format) }; + unsafe { Data::from_parts(data, n_samples, user_data.sample_format) }; if let Err(err) = user_data.publish_data_out(frames, &mut data) { (user_data.error_callback)(StreamError::BackendSpecific { err }); } From 06c290991ae745b4d88ee5d0592b28502999568c Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Tue, 6 Jan 2026 21:40:06 +0900 Subject: [PATCH 08/85] fix: output error size should be frames * stride --- src/host/pipewire/mod.rs | 2 -- src/host/pipewire/stream.rs | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/host/pipewire/mod.rs b/src/host/pipewire/mod.rs index 65b978371..02e493db2 100644 --- a/src/host/pipewire/mod.rs +++ b/src/host/pipewire/mod.rs @@ -3,13 +3,11 @@ use device::{init_devices, Device, DeviceType, Devices}; use crate::traits::HostTrait; mod device; mod stream; -use pipewire as pw; #[derive(Debug)] pub struct Host(Vec); impl Host { pub fn new() -> Result { - pw::init(); let devices = init_devices().ok_or(crate::HostUnavailable)?; Ok(Host(devices)) } diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index e63b5e02d..5e9cb54e2 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -184,7 +184,6 @@ where let context = pw::context::ContextRc::new(&mainloop, None)?; let core = context.connect_rc(None)?; - dbg!(&properties); let data = UserData { data_callback, error_callback, @@ -247,7 +246,7 @@ where let chunk = buf_data.chunk_mut(); *chunk.offset_mut() = 0; *chunk.stride_mut() = stride as i32; - *chunk.size_mut() = frames as u32; + *chunk.size_mut() = (frames * stride) as u32; } }) .register()?; From ef3b8ff434a6da5f02bacc98d3cfc962dd4a4691 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Tue, 6 Jan 2026 22:58:49 +0900 Subject: [PATCH 09/85] feat: sink can do both side --- src/host/pipewire/device.rs | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index ed53d0ab1..ab4ab1552 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -99,8 +99,11 @@ impl Device { ..Default::default() } } - pub(crate) fn pw_properties(&self) -> pw::properties::PropertiesBox { - let mut properties = match self.direction { + pub(crate) fn pw_properties( + &self, + direction: DeviceDirection, + ) -> pw::properties::PropertiesBox { + let mut properties = match direction { DeviceDirection::Output => pw::properties::properties! { *pw::keys::MEDIA_TYPE => "Audio", *pw::keys::MEDIA_CATEGORY => "Playback", @@ -195,6 +198,9 @@ impl DeviceTrait for Device { fn default_input_config( &self, ) -> Result { + if !self.supports_input() { + return Err(crate::DefaultStreamConfigError::StreamTypeNotSupported); + } Ok(crate::SupportedStreamConfig { channels: self.channels, sample_format: crate::SampleFormat::F32, @@ -209,6 +215,9 @@ impl DeviceTrait for Device { fn default_output_config( &self, ) -> Result { + if !self.supports_output() { + return Err(crate::DefaultStreamConfigError::StreamTypeNotSupported); + } Ok(crate::SupportedStreamConfig { channels: self.channels, sample_format: crate::SampleFormat::F32, @@ -241,7 +250,7 @@ impl DeviceTrait for Device { let handle = thread::Builder::new() .name("pw_capture_music_in".to_owned()) .spawn(move || { - let properties = device.pw_properties(); + let properties = device.pw_properties(DeviceDirection::Input); let Ok(StreamData { mainloop, listener, @@ -303,7 +312,7 @@ impl DeviceTrait for Device { let handle = thread::Builder::new() .name("pw_capture_music_out".to_owned()) .spawn(move || { - let properties = device.pw_properties(); + let properties = device.pw_properties(DeviceDirection::Output); let StreamData { mainloop, @@ -558,9 +567,10 @@ fn init_roundtrip() -> Option> { let Some(group) = props.get("port.group") else { return; }; - let direction = match group { - "playback" => DeviceDirection::Input, - "capture" => DeviceDirection::Output, + let direction = match (group, role) { + ("playback", Role::Sink) => DeviceDirection::Duplex, + ("playback", Role::Source) => DeviceDirection::Input, + ("capture", _) => DeviceDirection::Input, _ => { return; } From 448a7dddf6294acf0d5f155ff78a8e2378693dbc Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Tue, 6 Jan 2026 23:04:47 +0900 Subject: [PATCH 10/85] chore: complete supported configs --- src/host/pipewire/device.rs | 52 +++++++++++++++++++++---------------- src/host/pipewire/stream.rs | 12 +++++++++ 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index ab4ab1552..d03f05cfa 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -1,7 +1,7 @@ use std::time::Duration; use std::{cell::RefCell, rc::Rc}; -use crate::host::pipewire::stream::StreamData; +use crate::host::pipewire::stream::{StreamData, SUPPORTED_FORMATS}; use crate::{traits::DeviceTrait, DeviceDirection, SupportedStreamConfigRange}; use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs}; @@ -165,17 +165,20 @@ impl DeviceTrait for Device { if !self.supports_input() { return Err(crate::SupportedStreamConfigsError::DeviceNotAvailable); } - Ok(vec![SupportedStreamConfigRange { - channels: self.channels, - min_sample_rate: self.rate, - max_sample_rate: self.rate, - buffer_size: crate::SupportedBufferSize::Range { - min: self.min_quantum, - max: self.max_quantum, - }, - sample_format: crate::SampleFormat::F32, - }] - .into_iter()) + Ok(SUPPORTED_FORMATS + .iter() + .map(|sample_format| SupportedStreamConfigRange { + channels: self.channels, + min_sample_rate: self.rate, + max_sample_rate: self.rate, + buffer_size: crate::SupportedBufferSize::Range { + min: self.min_quantum, + max: self.max_quantum, + }, + sample_format: *sample_format, + }) + .collect::>() + .into_iter()) } fn supported_output_configs( &self, @@ -183,17 +186,20 @@ impl DeviceTrait for Device { if !self.supports_output() { return Err(crate::SupportedStreamConfigsError::DeviceNotAvailable); } - Ok(vec![SupportedStreamConfigRange { - channels: self.channels, - min_sample_rate: self.rate, - max_sample_rate: self.rate, - buffer_size: crate::SupportedBufferSize::Range { - min: self.min_quantum, - max: self.max_quantum, - }, - sample_format: crate::SampleFormat::F32, - }] - .into_iter()) + Ok(SUPPORTED_FORMATS + .iter() + .map(|sample_format| SupportedStreamConfigRange { + channels: self.channels, + min_sample_rate: self.rate, + max_sample_rate: self.rate, + buffer_size: crate::SupportedBufferSize::Range { + min: self.min_quantum, + max: self.max_quantum, + }, + sample_format: *sample_format, + }) + .collect::>() + .into_iter()) } fn default_input_config( &self, diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index 5e9cb54e2..18af55c5d 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -40,6 +40,18 @@ impl StreamTrait for Stream { } } +pub(crate) const SUPPORTED_FORMATS: &[SampleFormat] = &[ + SampleFormat::I8, + SampleFormat::U8, + SampleFormat::I16, + SampleFormat::U16, + SampleFormat::I24, + SampleFormat::U24, + SampleFormat::I32, + SampleFormat::U32, + SampleFormat::F64, +]; + impl From for pw::spa::param::audio::AudioFormat { fn from(value: SampleFormat) -> Self { match value { From 4bdea8e5aa76abc2c40dd1fdcd22a873177495b0 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Tue, 6 Jan 2026 23:31:18 +0900 Subject: [PATCH 11/85] feat: support show the device type --- src/host/pipewire/device.rs | 55 +++++++++++++++++++++++-------------- src/host/pipewire/mod.rs | 6 ++-- 2 files changed, 37 insertions(+), 24 deletions(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index d03f05cfa..889befcde 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -19,8 +19,9 @@ use super::stream::Stream; pub type Devices = std::vec::IntoIter; +/// This enum record whether it is created by human or just default device #[derive(Clone, Debug, Default, Copy)] -pub(crate) enum DeviceType { +pub(crate) enum ClassType { #[default] Node, DefaultSink, @@ -50,15 +51,16 @@ pub struct Device { quantum: u32, min_quantum: u32, max_quantum: u32, - device_type: DeviceType, + class_type: ClassType, object_id: String, device_id: String, role: Role, + icon_name: String, } impl Device { - pub(crate) fn device_type(&self) -> DeviceType { - self.device_type + pub(crate) fn class_type(&self) -> ClassType { + self.class_type } fn sink_default() -> Self { Self { @@ -66,9 +68,9 @@ impl Device { node_name: "sink_default".to_owned(), nick_name: "sink_default".to_owned(), description: "default_sink".to_owned(), - direction: DeviceDirection::Input, + direction: DeviceDirection::Duplex, channels: 2, - device_type: DeviceType::DefaultSink, + class_type: ClassType::DefaultSink, role: Role::Sink, ..Default::default() } @@ -81,7 +83,7 @@ impl Device { description: "default_input".to_owned(), direction: DeviceDirection::Input, channels: 2, - device_type: DeviceType::DefaultInput, + class_type: ClassType::DefaultInput, role: Role::Source, ..Default::default() } @@ -94,11 +96,20 @@ impl Device { description: "default_output".to_owned(), direction: DeviceDirection::Output, channels: 2, - device_type: DeviceType::DefaultOutput, + class_type: ClassType::DefaultOutput, role: Role::Source, ..Default::default() } } + + fn device_type(&self) -> crate::DeviceType { + match self.icon_name.as_str() { + "audio-headphones" => crate::DeviceType::Headphones, + "audio-input-microphone" => crate::DeviceType::Microphone, + _ => crate::DeviceType::Unknown, + } + } + pub(crate) fn pw_properties( &self, direction: DeviceDirection, @@ -116,11 +127,10 @@ impl Device { }, _ => unreachable!(), }; - dbg!(&self); if matches!(self.role, Role::Sink) { properties.insert(*pw::keys::STREAM_CAPTURE_SINK, "true"); } - if matches!(self.device_type, DeviceType::Node) { + if matches!(self.class_type, ClassType::Node) { properties.insert(*pw::keys::TARGET_OBJECT, self.device_id.to_owned()); } properties @@ -138,10 +148,10 @@ impl DeviceTrait for Device { )) } - // TODO: device type fn description(&self) -> Result { Ok(crate::DeviceDescriptionBuilder::new(&self.nick_name) .direction(self.direction()) + .device_type(self.device_type()) .build()) } @@ -163,7 +173,7 @@ impl DeviceTrait for Device { &self, ) -> Result { if !self.supports_input() { - return Err(crate::SupportedStreamConfigsError::DeviceNotAvailable); + return Ok(vec![].into_iter()); } Ok(SUPPORTED_FORMATS .iter() @@ -184,7 +194,7 @@ impl DeviceTrait for Device { &self, ) -> Result { if !self.supports_output() { - return Err(crate::SupportedStreamConfigsError::DeviceNotAvailable); + return Ok(vec![].into_iter()); } Ok(SUPPORTED_FORMATS .iter() @@ -320,24 +330,22 @@ impl DeviceTrait for Device { .spawn(move || { let properties = device.pw_properties(DeviceDirection::Output); - let StreamData { + let Ok(StreamData { mainloop, listener, stream, context, - } = match super::stream::connect_output( + }) = super::stream::connect_output( &config, properties, sample_format, data_callback, error_callback, timeout, - ) { - Ok(data) => data, - Err(_) => { - let _ = pw_init_tx.send(false); - return; - } + ) + else { + let _ = pw_init_tx.send(false); + return; }; let _ = pw_init_tx.send(true); @@ -602,6 +610,10 @@ fn init_roundtrip() -> Option> { .get("clock.quantum-limit") .and_then(|channels| channels.parse().ok()) .unwrap_or(0); + let icon_name = props + .get("device.icon_name") + .unwrap_or("default") + .to_owned(); let device = Device { id, @@ -612,6 +624,7 @@ fn init_roundtrip() -> Option> { role, channels, limit_quantum, + icon_name, object_id: object_id.to_owned(), device_id: device_id.to_owned(), ..Default::default() diff --git a/src/host/pipewire/mod.rs b/src/host/pipewire/mod.rs index 02e493db2..af7c402a0 100644 --- a/src/host/pipewire/mod.rs +++ b/src/host/pipewire/mod.rs @@ -1,4 +1,4 @@ -use device::{init_devices, Device, DeviceType, Devices}; +use device::{init_devices, ClassType, Device, Devices}; use crate::traits::HostTrait; mod device; @@ -26,13 +26,13 @@ impl HostTrait for Host { fn default_input_device(&self) -> Option { self.0 .iter() - .find(|device| matches!(device.device_type(), DeviceType::DefaultSink)) + .find(|device| matches!(device.class_type(), ClassType::DefaultSink)) .cloned() } fn default_output_device(&self) -> Option { self.0 .iter() - .find(|device| matches!(device.device_type(), DeviceType::DefaultOutput)) + .find(|device| matches!(device.class_type(), ClassType::DefaultOutput)) .cloned() } } From 9ba922304a7e0a51469e2f8dfe029ed6a81adabc Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Tue, 6 Jan 2026 23:32:57 +0900 Subject: [PATCH 12/85] chore: modify the _typos.toml --- _typos.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/_typos.toml b/_typos.toml index 35e0a4db7..02d3ec106 100644 --- a/_typos.toml +++ b/_typos.toml @@ -1,5 +1,2 @@ -[files] -extend-exclude = ["**/*.cmake", "**/*.json", "assets_for_test/*"] - [default.extend-words] datas = "datas" From aed88a547530ee478b959d9476bce7a2302cb75d Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Tue, 6 Jan 2026 23:40:45 +0900 Subject: [PATCH 13/85] chore: reset default backend to alsa --- examples/beep.rs | 46 ++++++++++++++++++++++++++++++++++++++++++ examples/record_wav.rs | 30 ++++++++++++++++++++++++--- src/platform/mod.rs | 2 +- 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/examples/beep.rs b/examples/beep.rs index f11ea6551..e5614f77a 100644 --- a/examples/beep.rs +++ b/examples/beep.rs @@ -31,6 +31,19 @@ struct Opt { /// Use the PulseAudio host. Requires `--features pulseaudio`. #[arg(long, default_value_t = false)] pulseaudio: bool, + /// Use the pipewire host + #[cfg(all( + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd" + ), + feature = "pipewire" + ))] + #[arg(short, long)] + #[allow(dead_code)] + pipewire: bool, } fn main() -> anyhow::Result<()> { @@ -74,6 +87,39 @@ fn main() -> anyhow::Result<()> { } else { cpal::default_host() }; + // Conditionally compile with jack if the feature is specified. + #[cfg(all( + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd" + ), + feature = "pipewire" + ))] + // Manually check for flags. Can be passed through cargo with -- e.g. + // cargo run --release --example beep --features jack -- --jack + let host = if opt.pipewire { + cpal::host_from_id(cpal::available_hosts() + .into_iter() + .find(|id| *id == cpal::HostId::PipeWire) + .expect( + "make sure --features pipewire is specified. only works on OSes where jack is available", + )).expect("jack host unavailable") + } else { + cpal::default_host() + }; + + #[cfg(any( + not(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd" + )), + not(any(feature = "jack", feature = "pipewire")) + ))] + let host = cpal::default_host(); let device = if let Some(device) = opt.device { let id = &device.parse().expect("failed to parse device id"); diff --git a/examples/record_wav.rs b/examples/record_wav.rs index bbe8f86cf..6d3683f7b 100644 --- a/examples/record_wav.rs +++ b/examples/record_wav.rs @@ -33,6 +33,19 @@ struct Opt { #[arg(short, long)] #[allow(dead_code)] jack: bool, + /// Use the pipewire host + #[cfg(all( + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd" + ), + feature = "pipewire" + ))] + #[arg(short, long)] + #[allow(dead_code)] + pipewire: bool, } fn main() -> Result<(), anyhow::Error> { @@ -49,7 +62,7 @@ fn main() -> Result<(), anyhow::Error> { feature = "jack" ))] // Manually check for flags. Can be passed through cargo with -- e.g. - // cargo run --release --example beep --features jack -- --jack + // cargo run --release --example record_wav --features jack -- --jack let host = if opt.jack { cpal::host_from_id(cpal::available_hosts() .into_iter() @@ -60,7 +73,18 @@ fn main() -> Result<(), anyhow::Error> { } else { cpal::default_host() }; - + // Manually check for flags. Can be passed through cargo with -- e.g. + // cargo run --release --example record_wav --features pipewire -- -- pipewire + let host = if opt.pipewire { + cpal::host_from_id(cpal::available_hosts() + .into_iter() + .find(|id| *id == cpal::HostId::PipeWire) + .expect( + "make sure --features pipewire is specified. only works on OSes where jack is available", + )).expect("jack host unavailable") + } else { + cpal::default_host() + }; #[cfg(any( not(any( target_os = "linux", @@ -68,7 +92,7 @@ fn main() -> Result<(), anyhow::Error> { target_os = "freebsd", target_os = "netbsd" )), - not(feature = "jack") + not(any(feature = "jack", feature = "pipewire")) ))] let host = cpal::default_host(); diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 250c00291..2d4715a4b 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -740,7 +740,7 @@ mod platform_impl { /// The default host for the current compilation target platform. pub fn default_host() -> Host { - PipeWireHost::new() + AlsaHost::new() .expect("the default host should always be available") .into() } From b6a17b01c370176105c66aecf4043374b0437fd5 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Tue, 6 Jan 2026 23:42:37 +0900 Subject: [PATCH 14/85] chore: add pipewire dependence --- .github/workflows/platforms.yml | 2 +- .github/workflows/quality.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/platforms.yml b/.github/workflows/platforms.yml index 3354a5551..ad3dcf81a 100644 --- a/.github/workflows/platforms.yml +++ b/.github/workflows/platforms.yml @@ -33,7 +33,7 @@ env: MSRV_WASM: "1.82" MSRV_WINDOWS: "1.82" - PACKAGES_LINUX: libasound2-dev libjack-jackd2-dev libjack-jackd2-0 libdbus-1-dev + PACKAGES_LINUX: libasound2-dev libjack-jackd2-dev libjack-jackd2-0 libdbus-1-dev libpipewire-0.3-dev ANDROID_COMPILE_SDK: "30" ANDROID_BUILD_TOOLS: "30.0.3" diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index cd470bfd0..ba55ac99c 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -92,7 +92,7 @@ jobs: if: runner.os == 'Linux' uses: awalsh128/cache-apt-pkgs-action@latest with: - packages: libasound2-dev libjack-jackd2-dev libjack-jackd2-0 libdbus-1-dev + packages: libasound2-dev libjack-jackd2-dev libjack-jackd2-0 libdbus-1-dev libpipewire-0.3-dev - name: Setup ASIO SDK if: runner.os == 'Windows' @@ -128,7 +128,7 @@ jobs: - name: Cache Linux audio packages uses: awalsh128/cache-apt-pkgs-action@latest with: - packages: libasound2-dev libjack-jackd2-dev libjack-jackd2-0 libdbus-1-dev + packages: libasound2-dev libjack-jackd2-dev libjack-jackd2-0 libdbus-1-dev libpipewire-0.3-dev - name: Install Rust toolchain uses: dtolnay/rust-toolchain@nightly From 272a398985c7ad3e48bb1ac5c7f109caf3b4da93 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Wed, 7 Jan 2026 10:33:01 +0900 Subject: [PATCH 15/85] fix: target object should be the node name --- src/host/pipewire/device.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 889befcde..1411c2850 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -131,7 +131,7 @@ impl Device { properties.insert(*pw::keys::STREAM_CAPTURE_SINK, "true"); } if matches!(self.class_type, ClassType::Node) { - properties.insert(*pw::keys::TARGET_OBJECT, self.device_id.to_owned()); + properties.insert(*pw::keys::TARGET_OBJECT, self.node_name().to_owned()); } properties } From e13b722354d055cdab0be177bc6fc4b19c98cd44 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Wed, 7 Jan 2026 10:37:58 +0900 Subject: [PATCH 16/85] chore: use object_serial instead --- src/host/pipewire/device.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 1411c2850..9d57aa70d 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -56,6 +56,7 @@ pub struct Device { device_id: String, role: Role, icon_name: String, + object_serial: u32, } impl Device { @@ -131,7 +132,7 @@ impl Device { properties.insert(*pw::keys::STREAM_CAPTURE_SINK, "true"); } if matches!(self.class_type, ClassType::Node) { - properties.insert(*pw::keys::TARGET_OBJECT, self.node_name().to_owned()); + properties.insert(*pw::keys::TARGET_OBJECT, self.object_serial.to_string()); } properties } @@ -595,6 +596,12 @@ fn init_roundtrip() -> Option> { let Some(device_id) = props.get("device.id") else { return; }; + let Some(object_serial) = props + .get("object.serial") + .and_then(|serial| serial.parse().ok()) + else { + return; + }; let id = info.id(); let node_name = props.get("node.name").unwrap_or("unknown").to_owned(); let nick_name = props.get("node.nick").unwrap_or("unknown").to_owned(); @@ -627,6 +634,7 @@ fn init_roundtrip() -> Option> { icon_name, object_id: object_id.to_owned(), device_id: device_id.to_owned(), + object_serial, ..Default::default() }; devices.borrow_mut().push(device); From 6f6e4135679d2b71f17ab3bc15cf1e71cc2e585b Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Wed, 7 Jan 2026 10:45:17 +0900 Subject: [PATCH 17/85] chore: capture should be output --- src/host/pipewire/device.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 9d57aa70d..0e73a7fe9 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -585,7 +585,7 @@ fn init_roundtrip() -> Option> { let direction = match (group, role) { ("playback", Role::Sink) => DeviceDirection::Duplex, ("playback", Role::Source) => DeviceDirection::Input, - ("capture", _) => DeviceDirection::Input, + ("capture", _) => DeviceDirection::Output, _ => { return; } From 66ec2bbf1e6c795473aeb7b04dd3e33194875133 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Thu, 8 Jan 2026 18:47:28 +0900 Subject: [PATCH 18/85] chore: remove unused timeout --- src/host/pipewire/device.rs | 2 -- src/host/pipewire/stream.rs | 7 +------ 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 0e73a7fe9..3b795cbb0 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -279,7 +279,6 @@ impl DeviceTrait for Device { sample_format, data_callback, error_callback, - timeout, ) else { let _ = pw_init_tx.send(false); @@ -342,7 +341,6 @@ impl DeviceTrait for Device { sample_format, data_callback, error_callback, - timeout, ) else { let _ = pw_init_tx.send(false); diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index 18af55c5d..bcccefabd 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -1,7 +1,4 @@ -use std::{ - thread::JoinHandle, - time::{Duration, Instant}, -}; +use std::{thread::JoinHandle, time::Instant}; use crate::{ traits::StreamTrait, BackendSpecificError, InputCallbackInfo, OutputCallbackInfo, SampleFormat, @@ -185,7 +182,6 @@ pub fn connect_output( sample_format: SampleFormat, data_callback: D, error_callback: E, - _timeout: Option, ) -> Result, pw::Error> where D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, @@ -306,7 +302,6 @@ pub fn connect_input( sample_format: SampleFormat, data_callback: D, error_callback: E, - _timeout: Option, ) -> Result, pw::Error> where D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, From a5871e91804679afb595a95485d5c60940483e10 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Sat, 10 Jan 2026 07:22:13 +0900 Subject: [PATCH 19/85] fix: test pipewire with rust 1.85 --- .github/workflows/platforms.yml | 17 +++++++++++------ Cargo.toml | 2 +- examples/record_wav.rs | 11 +++++++++++ 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/.github/workflows/platforms.yml b/.github/workflows/platforms.yml index ad3dcf81a..fe299f6a1 100644 --- a/.github/workflows/platforms.yml +++ b/.github/workflows/platforms.yml @@ -24,12 +24,12 @@ on: env: # MSRV varies by backend due to platform-specific dependencies - MSRV_AAUDIO: "1.82" - MSRV_ALSA: "1.82" - MSRV_COREAUDIO: "1.80" + MSRV_AAUDIO: "1.85" + MSRV_ALSA: "1.85" + MSRV_COREAUDIO: "1.82" MSRV_JACK: "1.82" MSRV_PULSEAUDIO: "1.88" - MSRV_WASIP1: "1.78" + MSRV_WASIP1: "1.82" MSRV_WASM: "1.82" MSRV_WINDOWS: "1.82" @@ -67,13 +67,13 @@ jobs: jack-msrv: ${{ env.MSRV_JACK }} pulseaudio-msrv: ${{ env.MSRV_PULSEAUDIO }} - - name: Install Rust MSRV (${{ env.MSRV_ALSA }}) + - name: Install Rust MSRV (${{ env.MSRV_PIPEWIRE }}) uses: dtolnay/rust-toolchain@master with: toolchain: ${{ env.MSRV_ALSA }} - name: Install Rust MSRV (${{ steps.msrv.outputs.all-features }}) - if: steps.msrv.outputs.all-features != env.MSRV_ALSA + if: steps.msrv.outputs.all-features != env.MSRV_PIPEWIRE uses: dtolnay/rust-toolchain@master with: toolchain: ${{ steps.msrv.outputs.all-features }} @@ -109,6 +109,11 @@ jobs: steps: - uses: actions/checkout@v5 + - name: Cache Linux audio packages + uses: awalsh128/cache-apt-pkgs-action@latest + with: + packages: ${{ env.PACKAGES_LINUX }} + - name: Determine MSRV for all-features id: msrv uses: ./.github/actions/determine-msrv diff --git a/Cargo.toml b/Cargo.toml index f11a3c8bf..cf7e63c95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,7 +66,7 @@ audioworklet = [ # Platform: All platforms custom = [] -default = ["pipewire"] +default = [] [dependencies] dasp_sample = "0.11" diff --git a/examples/record_wav.rs b/examples/record_wav.rs index 6d3683f7b..940862a2b 100644 --- a/examples/record_wav.rs +++ b/examples/record_wav.rs @@ -73,6 +73,17 @@ fn main() -> Result<(), anyhow::Error> { } else { cpal::default_host() }; + + // Conditionally compile with jack if the feature is specified. + #[cfg(all( + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd" + ), + feature = "pipewire" + ))] // Manually check for flags. Can be passed through cargo with -- e.g. // cargo run --release --example record_wav --features pipewire -- -- pipewire let host = if opt.pipewire { From 87bd324009ff4f5408f71dc98b64750b74dce9e9 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Sat, 10 Jan 2026 08:19:14 +0900 Subject: [PATCH 20/85] chore: keep rust version --- .github/workflows/platforms.yml | 2 +- Cargo.toml | 2 +- src/host/pipewire/device.rs | 12 ++---------- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/.github/workflows/platforms.yml b/.github/workflows/platforms.yml index fe299f6a1..3c733bba5 100644 --- a/.github/workflows/platforms.yml +++ b/.github/workflows/platforms.yml @@ -26,7 +26,7 @@ env: # MSRV varies by backend due to platform-specific dependencies MSRV_AAUDIO: "1.85" MSRV_ALSA: "1.85" - MSRV_COREAUDIO: "1.82" + MSRV_COREAUDIO: "1.80" MSRV_JACK: "1.82" MSRV_PULSEAUDIO: "1.88" MSRV_WASIP1: "1.82" diff --git a/Cargo.toml b/Cargo.toml index cf7e63c95..3157c80dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ documentation = "https://docs.rs/cpal" license = "Apache-2.0" keywords = ["audio", "sound"] edition = "2021" -rust-version = "1.82" +rust-version = "1.78" [features] # ASIO backend for Windows diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 3b795cbb0..c7a89124a 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -294,11 +294,7 @@ impl DeviceTrait for Device { drop(context); }) .unwrap(); - if pw_init_rv - .recv_timeout(wait_timeout) - .ok() - .is_none_or(|re| !re) - { + if pw_init_rv.recv_timeout(wait_timeout).unwrap_or(false) { return Err(crate::BuildStreamError::DeviceNotAvailable); }; Ok(Stream { @@ -357,11 +353,7 @@ impl DeviceTrait for Device { drop(context); }) .unwrap(); - if pw_init_rv - .recv_timeout(wait_timeout) - .ok() - .is_none_or(|re| !re) - { + if pw_init_rv.recv_timeout(wait_timeout).unwrap_or(false) { return Err(crate::BuildStreamError::DeviceNotAvailable); }; Ok(Stream { From 754002b08accb2aa716db375169f470556a21132 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Sun, 11 Jan 2026 01:01:51 +0900 Subject: [PATCH 21/85] fix: ci --- .github/workflows/platforms.yml | 5 ----- Cross.toml | 5 ++--- Dockerfile | 3 ++- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/.github/workflows/platforms.yml b/.github/workflows/platforms.yml index 3c733bba5..682e686ff 100644 --- a/.github/workflows/platforms.yml +++ b/.github/workflows/platforms.yml @@ -109,11 +109,6 @@ jobs: steps: - uses: actions/checkout@v5 - - name: Cache Linux audio packages - uses: awalsh128/cache-apt-pkgs-action@latest - with: - packages: ${{ env.PACKAGES_LINUX }} - - name: Determine MSRV for all-features id: msrv uses: ./.github/actions/determine-msrv diff --git a/Cross.toml b/Cross.toml index 6d0ad81d1..57b252d5c 100644 --- a/Cross.toml +++ b/Cross.toml @@ -1,7 +1,6 @@ [target.armv7-unknown-linux-gnueabihf] dockerfile = "Dockerfile" +build-args = { CROSS_BASE_IMAGE = "ubuntu:24.04" } [target.armv7-unknown-linux-gnueabihf.env] -passthrough = [ - "RUSTFLAGS", -] +passthrough = ["RUSTFLAGS"] diff --git a/Dockerfile b/Dockerfile index 8e56a2efd..5ecc260d8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,4 +7,5 @@ ENV PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/ RUN dpkg --add-architecture armhf && \ apt-get update && \ apt-get install libasound2-dev:armhf -y && \ - apt-get install libjack-jackd2-dev:armhf libjack-jackd2-0:armhf -y \ + apt-get install libjack-jackd2-dev:armhf libjack-jackd2-0:armhf -y && \ + apt-get install libpipewire-0.3-dev:armhf -y \ From 3a86e7604aa83dbaf91e4e61be711dea78d588fa Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Sun, 11 Jan 2026 18:49:24 +0900 Subject: [PATCH 22/85] fix: ci problem because cross-rs is based on ubuntu20.04, so it does not contains pipewire --- .github/workflows/platforms.yml | 8 ++++---- Cross.toml | 1 - Dockerfile | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/platforms.yml b/.github/workflows/platforms.yml index 682e686ff..ede0670a8 100644 --- a/.github/workflows/platforms.yml +++ b/.github/workflows/platforms.yml @@ -96,10 +96,10 @@ jobs: run: cargo +${{ env.MSRV_ALSA }} check --examples --no-default-features --workspace --verbose - name: Run tests (all features) - run: cargo +${{ steps.msrv.outputs.all-features }} test --all-features --workspace --verbose + run: cargo +${{ steps.msrv.outputs.all-features }} test --features=jack --workspace --verbose - name: Check examples (all features) - run: cargo +${{ steps.msrv.outputs.all-features }} check --examples --all-features --workspace --verbose + run: cargo +${{ steps.msrv.outputs.all-features }} check --examples --features=jack --workspace --verbose # Linux ARMv7 (cross-compilation) linux-armv7: @@ -153,10 +153,10 @@ jobs: run: cross +${{ env.MSRV_ALSA }} test --no-default-features --workspace --verbose --target ${{ env.TARGET }} - name: Run tests (all features) - run: cross +${{ steps.msrv.outputs.all-features }} test --all-features --workspace --verbose --target ${{ env.TARGET }} + run: cross +${{ steps.msrv.outputs.all-features }} test --features=jack --workspace --verbose --target ${{ env.TARGET }} - name: Check examples (all features) - run: cross +${{ steps.msrv.outputs.all-features }} test --all-features --workspace --verbose --target ${{ env.TARGET }} + run: cross +${{ steps.msrv.outputs.all-features }} test --features=jack --workspace --verbose --target ${{ env.TARGET }} # Windows (x86_64 and i686) windows: diff --git a/Cross.toml b/Cross.toml index 57b252d5c..09b92e9ea 100644 --- a/Cross.toml +++ b/Cross.toml @@ -1,6 +1,5 @@ [target.armv7-unknown-linux-gnueabihf] dockerfile = "Dockerfile" -build-args = { CROSS_BASE_IMAGE = "ubuntu:24.04" } [target.armv7-unknown-linux-gnueabihf.env] passthrough = ["RUSTFLAGS"] diff --git a/Dockerfile b/Dockerfile index 5ecc260d8..5405f1417 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,5 +7,5 @@ ENV PKG_CONFIG_PATH=/usr/lib/arm-linux-gnueabihf/pkgconfig/ RUN dpkg --add-architecture armhf && \ apt-get update && \ apt-get install libasound2-dev:armhf -y && \ - apt-get install libjack-jackd2-dev:armhf libjack-jackd2-0:armhf -y && \ - apt-get install libpipewire-0.3-dev:armhf -y \ + apt-get install libjack-jackd2-dev:armhf libjack-jackd2-0:armhf -y +# TODO: now the cross-rs is based on ubuntu:20.04, so it does not contain pipewire-0.3-dev From db9454186c74a6d4957fd2643c6bffa445124135 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Sun, 11 Jan 2026 18:56:03 +0900 Subject: [PATCH 23/85] fix: remove the unexisted variable --- .github/workflows/platforms.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/platforms.yml b/.github/workflows/platforms.yml index ede0670a8..04660f40e 100644 --- a/.github/workflows/platforms.yml +++ b/.github/workflows/platforms.yml @@ -67,13 +67,13 @@ jobs: jack-msrv: ${{ env.MSRV_JACK }} pulseaudio-msrv: ${{ env.MSRV_PULSEAUDIO }} - - name: Install Rust MSRV (${{ env.MSRV_PIPEWIRE }}) + - name: Install Rust MSRV (${{ env.MSRV_ALSA }}) uses: dtolnay/rust-toolchain@master with: toolchain: ${{ env.MSRV_ALSA }} - name: Install Rust MSRV (${{ steps.msrv.outputs.all-features }}) - if: steps.msrv.outputs.all-features != env.MSRV_PIPEWIRE + if: steps.msrv.outputs.all-features != env.MSRV_ALSA uses: dtolnay/rust-toolchain@master with: toolchain: ${{ steps.msrv.outputs.all-features }} From 54d69da7792f63c03b47eff85bb7a5a178ab1362 Mon Sep 17 00:00:00 2001 From: loxoron218 Date: Tue, 20 Jan 2026 23:05:42 +0100 Subject: [PATCH 24/85] feat(pipewire): add support for I64, U64, and F32 sample formats --- src/host/pipewire/stream.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index bcccefabd..b99aeede5 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -46,6 +46,9 @@ pub(crate) const SUPPORTED_FORMATS: &[SampleFormat] = &[ SampleFormat::U24, SampleFormat::I32, SampleFormat::U32, + SampleFormat::I64, + SampleFormat::U64, + SampleFormat::F32, SampleFormat::F64, ]; From e7d01be0a76e43211e46b7e16ed813613d492426 Mon Sep 17 00:00:00 2001 From: loxoron218 Date: Wed, 21 Jan 2026 01:54:32 +0100 Subject: [PATCH 25/85] feat(pipewire): support multiple sample rates in device configuration --- src/host/pipewire/device.rs | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index c7a89124a..df80c20e6 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -176,17 +176,26 @@ impl DeviceTrait for Device { if !self.supports_input() { return Ok(vec![].into_iter()); } - Ok(SUPPORTED_FORMATS + let rates = if self.allow_rates.is_empty() { + vec![self.rate] + } else { + self.allow_rates.clone() + }; + Ok(rates .iter() - .map(|sample_format| SupportedStreamConfigRange { + .flat_map(|&rate| { + SUPPORTED_FORMATS + .iter() + .map(move |sample_format| SupportedStreamConfigRange { channels: self.channels, - min_sample_rate: self.rate, - max_sample_rate: self.rate, + min_sample_rate: rate, + max_sample_rate: rate, buffer_size: crate::SupportedBufferSize::Range { min: self.min_quantum, max: self.max_quantum, }, sample_format: *sample_format, + }) }) .collect::>() .into_iter()) @@ -197,17 +206,26 @@ impl DeviceTrait for Device { if !self.supports_output() { return Ok(vec![].into_iter()); } - Ok(SUPPORTED_FORMATS + let rates = if self.allow_rates.is_empty() { + vec![self.rate] + } else { + self.allow_rates.clone() + }; + Ok(rates .iter() - .map(|sample_format| SupportedStreamConfigRange { + .flat_map(|&rate| { + SUPPORTED_FORMATS + .iter() + .map(move |sample_format| SupportedStreamConfigRange { channels: self.channels, - min_sample_rate: self.rate, - max_sample_rate: self.rate, + min_sample_rate: rate, + max_sample_rate: rate, buffer_size: crate::SupportedBufferSize::Range { min: self.min_quantum, max: self.max_quantum, }, sample_format: *sample_format, + }) }) .collect::>() .into_iter()) From a9e4e40f75ecd73dfe6cd10616b6a7633e924b66 Mon Sep 17 00:00:00 2001 From: loxoron218 Date: Wed, 21 Jan 2026 12:25:09 +0100 Subject: [PATCH 26/85] fix(pipewire): improve error handling for stream initialization timeout Replace boolean check with match expression to properly handle both timeout expiration and channel receive errors, returning a more accurate StreamConfigNotSupported error instead of DeviceNotAvailable. --- src/host/pipewire/device.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index df80c20e6..efac1c1a4 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -312,13 +312,13 @@ impl DeviceTrait for Device { drop(context); }) .unwrap(); - if pw_init_rv.recv_timeout(wait_timeout).unwrap_or(false) { - return Err(crate::BuildStreamError::DeviceNotAvailable); - }; - Ok(Stream { + match pw_init_rv.recv_timeout(wait_timeout) { + Ok(true) => Ok(Stream { handle, controller: pw_play_tx, - }) + }), + Ok(false) | Err(_) => Err(crate::BuildStreamError::StreamConfigNotSupported), + } } fn build_output_stream_raw( @@ -371,15 +371,15 @@ impl DeviceTrait for Device { drop(context); }) .unwrap(); - if pw_init_rv.recv_timeout(wait_timeout).unwrap_or(false) { - return Err(crate::BuildStreamError::DeviceNotAvailable); - }; - Ok(Stream { + match pw_init_rv.recv_timeout(wait_timeout) { + Ok(true) => Ok(Stream { handle, controller: pw_play_tx, - }) - } + }), + Ok(false) | Err(_) => Err(crate::BuildStreamError::StreamConfigNotSupported), } + } + } impl Device { pub fn channels(&self) -> u16 { From 5277348e69a267f519263088bc5aa6572747faf2 Mon Sep 17 00:00:00 2001 From: loxoron218 Date: Wed, 21 Jan 2026 12:25:45 +0100 Subject: [PATCH 27/85] fix(pipewire): normalize channel list parsing and improve code formatting --- src/host/pipewire/device.rs | 42 ++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index efac1c1a4..6e5fe229c 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -187,14 +187,14 @@ impl DeviceTrait for Device { SUPPORTED_FORMATS .iter() .map(move |sample_format| SupportedStreamConfigRange { - channels: self.channels, + channels: self.channels, min_sample_rate: rate, max_sample_rate: rate, - buffer_size: crate::SupportedBufferSize::Range { - min: self.min_quantum, - max: self.max_quantum, - }, - sample_format: *sample_format, + buffer_size: crate::SupportedBufferSize::Range { + min: self.min_quantum, + max: self.max_quantum, + }, + sample_format: *sample_format, }) }) .collect::>() @@ -217,14 +217,14 @@ impl DeviceTrait for Device { SUPPORTED_FORMATS .iter() .map(move |sample_format| SupportedStreamConfigRange { - channels: self.channels, + channels: self.channels, min_sample_rate: rate, max_sample_rate: rate, - buffer_size: crate::SupportedBufferSize::Range { - min: self.min_quantum, - max: self.max_quantum, - }, - sample_format: *sample_format, + buffer_size: crate::SupportedBufferSize::Range { + min: self.min_quantum, + max: self.max_quantum, + }, + sample_format: *sample_format, }) }) .collect::>() @@ -314,8 +314,8 @@ impl DeviceTrait for Device { .unwrap(); match pw_init_rv.recv_timeout(wait_timeout) { Ok(true) => Ok(Stream { - handle, - controller: pw_play_tx, + handle, + controller: pw_play_tx, }), Ok(false) | Err(_) => Err(crate::BuildStreamError::StreamConfigNotSupported), } @@ -373,13 +373,13 @@ impl DeviceTrait for Device { .unwrap(); match pw_init_rv.recv_timeout(wait_timeout) { Ok(true) => Ok(Stream { - handle, - controller: pw_play_tx, + handle, + controller: pw_play_tx, }), Ok(false) | Err(_) => Err(crate::BuildStreamError::StreamConfigNotSupported), -} - } } + } +} impl Device { pub fn channels(&self) -> u16 { @@ -518,7 +518,11 @@ fn init_roundtrip() -> Option> { return 0; }; let list = list.trim(); - let list: Vec<&str> = list.split(' ').collect(); + let list_normalized = list.replace(',', " "); + let list: Vec<&str> = list_normalized + .split(' ') + .filter(|s| !s.is_empty()) + .collect(); let mut allow_rates = vec![]; for rate in list { let Ok(rate) = rate.parse() else { From 2e7c9a73fe397726a411bba4551656c0086ac38d Mon Sep 17 00:00:00 2001 From: Frando Date: Tue, 10 Feb 2026 14:05:06 +0100 Subject: [PATCH 28/85] fix: support bluetooth devices --- src/host/pipewire/device.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 6e5fe229c..2c03efe87 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -598,9 +598,10 @@ fn init_roundtrip() -> Option> { ("playback", Role::Sink) => DeviceDirection::Duplex, ("playback", Role::Source) => DeviceDirection::Input, ("capture", _) => DeviceDirection::Output, - _ => { - return; - } + // Bluetooth and other non-ALSA devices use generic port group + // names like "stream.0" — derive direction from media.class + (_, Role::Sink) => DeviceDirection::Output, + (_, Role::Source) => DeviceDirection::Input, }; let Some(object_id) = props.get("object.id") else { return; @@ -616,11 +617,14 @@ fn init_roundtrip() -> Option> { }; let id = info.id(); let node_name = props.get("node.name").unwrap_or("unknown").to_owned(); - let nick_name = props.get("node.nick").unwrap_or("unknown").to_owned(); let description = props .get("node.description") .unwrap_or("unknown") .to_owned(); + let nick_name = props + .get("node.nick") + .unwrap_or_else(|| description.as_str()) + .to_owned(); let channels = props .get("audio.channels") .and_then(|channels| channels.parse().ok()) From 24327a43f184f33580699617d632e94c89389a98 Mon Sep 17 00:00:00 2001 From: Frando Date: Tue, 10 Feb 2026 14:19:30 +0100 Subject: [PATCH 29/85] feat(pipewire): use stable node.name as device ID and enrich device descriptions Device IDs now use node.name (e.g. alsa_output.pci-..., bluez_output.80_99_...) which is unique and stable across restarts, instead of nick_name which can have duplicates. Device descriptions now include interface_type (Bluetooth, PCI, USB), address (BT MAC or ALSA path), driver (factory.name), and improved device_type mapping (Speaker, Headset). --- src/host/pipewire/device.rs | 41 ++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 2c03efe87..f4aa69cc5 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -57,6 +57,9 @@ pub struct Device { role: Role, icon_name: String, object_serial: u32, + interface_type: crate::InterfaceType, + address: Option, + driver: Option, } impl Device { @@ -106,7 +109,9 @@ impl Device { fn device_type(&self) -> crate::DeviceType { match self.icon_name.as_str() { "audio-headphones" => crate::DeviceType::Headphones, + "audio-headset" => crate::DeviceType::Headset, "audio-input-microphone" => crate::DeviceType::Microphone, + "audio-speakers" => crate::DeviceType::Speaker, _ => crate::DeviceType::Unknown, } } @@ -145,15 +150,22 @@ impl DeviceTrait for Device { fn id(&self) -> Result { Ok(crate::DeviceId( crate::HostId::PipeWire, - self.nick_name.clone(), + self.node_name.clone(), )) } fn description(&self) -> Result { - Ok(crate::DeviceDescriptionBuilder::new(&self.nick_name) + let mut builder = crate::DeviceDescriptionBuilder::new(&self.nick_name) .direction(self.direction()) .device_type(self.device_type()) - .build()) + .interface_type(self.interface_type); + if let Some(ref address) = self.address { + builder = builder.address(address); + } + if let Some(ref driver) = self.driver { + builder = builder.driver(driver); + } + Ok(builder.build()) } fn supports_input(&self) -> bool { @@ -638,6 +650,26 @@ fn init_roundtrip() -> Option> { .unwrap_or("default") .to_owned(); + let interface_type = match props.get("device.api") { + Some("bluez5") => crate::InterfaceType::Bluetooth, + _ => match props.get("device.bus") { + Some("pci") => crate::InterfaceType::Pci, + Some("usb") => crate::InterfaceType::Usb, + Some("firewire") => crate::InterfaceType::FireWire, + Some("thunderbolt") => crate::InterfaceType::Thunderbolt, + _ => crate::InterfaceType::Unknown, + }, + }; + + let address = props + .get("api.bluez5.address") + .or_else(|| props.get("api.alsa.path")) + .map(|s| s.to_owned()); + + let driver = props + .get("factory.name") + .map(|s| s.to_owned()); + let device = Device { id, node_name, @@ -651,6 +683,9 @@ fn init_roundtrip() -> Option> { object_id: object_id.to_owned(), device_id: device_id.to_owned(), object_serial, + interface_type, + address, + driver, ..Default::default() }; devices.borrow_mut().push(device); From e35cc437c83e5d3bb72b6bc1a82ba112e6cae9ce Mon Sep 17 00:00:00 2001 From: Frando Date: Wed, 11 Feb 2026 12:20:53 +0100 Subject: [PATCH 30/85] fixup device properties --- src/host/pipewire/device.rs | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index f4aa69cc5..162f836cc 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -2,6 +2,7 @@ use std::time::Duration; use std::{cell::RefCell, rc::Rc}; use crate::host::pipewire::stream::{StreamData, SUPPORTED_FORMATS}; +use crate::InterfaceType; use crate::{traits::DeviceTrait, DeviceDirection, SupportedStreamConfigRange}; use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs}; @@ -57,7 +58,7 @@ pub struct Device { role: Role, icon_name: String, object_serial: u32, - interface_type: crate::InterfaceType, + interface_type: InterfaceType, address: Option, driver: Option, } @@ -112,6 +113,7 @@ impl Device { "audio-headset" => crate::DeviceType::Headset, "audio-input-microphone" => crate::DeviceType::Microphone, "audio-speakers" => crate::DeviceType::Speaker, + "video-display" => crate::DeviceType::Speaker, _ => crate::DeviceType::Unknown, } } @@ -159,12 +161,15 @@ impl DeviceTrait for Device { .direction(self.direction()) .device_type(self.device_type()) .interface_type(self.interface_type); - if let Some(ref address) = self.address { + if let Some(address) = self.address.as_ref() { builder = builder.address(address); } - if let Some(ref driver) = self.driver { + if let Some(driver) = self.driver.as_ref() { builder = builder.driver(driver); } + if !self.description.is_empty() && self.description != self.nick_name { + builder = builder.add_extended_line(&self.description); + } Ok(builder.build()) } @@ -608,8 +613,8 @@ fn init_roundtrip() -> Option> { }; let direction = match (group, role) { ("playback", Role::Sink) => DeviceDirection::Duplex, - ("playback", Role::Source) => DeviceDirection::Input, - ("capture", _) => DeviceDirection::Output, + ("playback", Role::Source) => DeviceDirection::Output, + ("capture", _) => DeviceDirection::Input, // Bluetooth and other non-ALSA devices use generic port group // names like "stream.0" — derive direction from media.class (_, Role::Sink) => DeviceDirection::Output, @@ -651,13 +656,13 @@ fn init_roundtrip() -> Option> { .to_owned(); let interface_type = match props.get("device.api") { - Some("bluez5") => crate::InterfaceType::Bluetooth, + Some("bluez5") => InterfaceType::Bluetooth, _ => match props.get("device.bus") { - Some("pci") => crate::InterfaceType::Pci, - Some("usb") => crate::InterfaceType::Usb, - Some("firewire") => crate::InterfaceType::FireWire, - Some("thunderbolt") => crate::InterfaceType::Thunderbolt, - _ => crate::InterfaceType::Unknown, + Some("pci") => InterfaceType::Pci, + Some("usb") => InterfaceType::Usb, + Some("firewire") => InterfaceType::FireWire, + Some("thunderbolt") => InterfaceType::Thunderbolt, + _ => InterfaceType::Unknown, }, }; @@ -666,9 +671,7 @@ fn init_roundtrip() -> Option> { .or_else(|| props.get("api.alsa.path")) .map(|s| s.to_owned()); - let driver = props - .get("factory.name") - .map(|s| s.to_owned()); + let driver = props.get("factory.name").map(|s| s.to_owned()); let device = Device { id, From 0823f9ee41e6e7ff5966fa0c9b99e8e2dbaa0b44 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Wed, 11 Feb 2026 21:28:30 +0900 Subject: [PATCH 31/85] chore: clippy fix --- src/host/pipewire/device.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 162f836cc..e05bca26d 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -640,7 +640,7 @@ fn init_roundtrip() -> Option> { .to_owned(); let nick_name = props .get("node.nick") - .unwrap_or_else(|| description.as_str()) + .unwrap_or(description.as_str()) .to_owned(); let channels = props .get("audio.channels") From 8ac6c7477b85e4b941d28f7552d0d0619d0ffb32 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Wed, 11 Feb 2026 21:29:47 +0900 Subject: [PATCH 32/85] chore: follow upstream changes --- src/host/pipewire/stream.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index b99aeede5..4d59b431f 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -91,8 +91,8 @@ impl From for pw::spa::param::audio::AudioFormat { SampleFormat::F64 => Self::F64LE, #[cfg(target_endian = "big")] SampleFormat::F64 => Self::F64BE, - SampleFormat::I64 => Self::Unknown, - SampleFormat::U64 => Self::Unknown, + // TODO: maybe we also need to support others + _ => Self::Unknown, } } } From b66f059c335456c775f4c9427f16eff24959815e Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Thu, 12 Feb 2026 22:32:38 +0900 Subject: [PATCH 33/85] chore: clippy fix for examples --- examples/beep.rs | 14 +++++++++----- examples/record_wav.rs | 3 ++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/examples/beep.rs b/examples/beep.rs index e5614f77a..1aa31de67 100644 --- a/examples/beep.rs +++ b/examples/beep.rs @@ -56,11 +56,15 @@ fn main() -> anyhow::Result<()> { #[allow(unused_mut, unused_assignments)] let mut pulseaudio_host_id = Err(HostUnavailable); - #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd" + #[cfg(all( + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd" + ), + feature = "jack", + not(feature = "pipewire") ))] { #[cfg(feature = "jack")] diff --git a/examples/record_wav.rs b/examples/record_wav.rs index 940862a2b..0dea95c88 100644 --- a/examples/record_wav.rs +++ b/examples/record_wav.rs @@ -59,7 +59,8 @@ fn main() -> Result<(), anyhow::Error> { target_os = "freebsd", target_os = "netbsd" ), - feature = "jack" + feature = "jack", + not(feature = "pipewire") ))] // Manually check for flags. Can be passed through cargo with -- e.g. // cargo run --release --example record_wav --features jack -- --jack From 66e1a5aa7b6a7dfdf116e45fffe72b13956198f0 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Sun, 15 Feb 2026 09:17:23 +0900 Subject: [PATCH 34/85] fix: stream dropped, pipewire still works --- src/host/pipewire/device.rs | 24 +++++++++++++++++------- src/host/pipewire/stream.rs | 18 +++++++++++++++--- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index e05bca26d..0af80a11a 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -1,7 +1,7 @@ use std::time::Duration; use std::{cell::RefCell, rc::Rc}; -use crate::host::pipewire::stream::{StreamData, SUPPORTED_FORMATS}; +use crate::host::pipewire::stream::{StreamCommand, StreamData, SUPPORTED_FORMATS}; use crate::InterfaceType; use crate::{traits::DeviceTrait, DeviceDirection, SupportedStreamConfigRange}; @@ -293,7 +293,7 @@ impl DeviceTrait for Device { D: FnMut(&crate::Data, &crate::InputCallbackInfo) + Send + 'static, E: FnMut(crate::StreamError) + Send + 'static, { - let (pw_play_tx, pw_play_rv) = pw::channel::channel::(); + let (pw_play_tx, pw_play_rv) = pw::channel::channel::(); let (pw_init_tx, pw_init_rv) = std::sync::mpsc::channel::(); let device = self.clone(); @@ -321,8 +321,13 @@ impl DeviceTrait for Device { }; let _ = pw_init_tx.send(true); let stream = stream.clone(); - let _receiver = pw_play_rv.attach(mainloop.loop_(), move |play| { - let _ = stream.set_active(play); + let _receiver = pw_play_rv.attach(mainloop.loop_(), move |play| match play { + StreamCommand::Toggle(state) => { + let _ = stream.set_active(state); + } + StreamCommand::Stop => { + let _ = stream.disconnect(); + } }); mainloop.run(); drop(listener); @@ -350,7 +355,7 @@ impl DeviceTrait for Device { D: FnMut(&mut crate::Data, &crate::OutputCallbackInfo) + Send + 'static, E: FnMut(crate::StreamError) + Send + 'static, { - let (pw_play_tx, pw_play_rv) = pw::channel::channel::(); + let (pw_play_tx, pw_play_rv) = pw::channel::channel::(); let (pw_init_tx, pw_init_rv) = std::sync::mpsc::channel::(); let device = self.clone(); @@ -380,8 +385,13 @@ impl DeviceTrait for Device { let _ = pw_init_tx.send(true); let stream = stream.clone(); - let _receiver = pw_play_rv.attach(mainloop.loop_(), move |play| { - let _ = stream.set_active(play); + let _receiver = pw_play_rv.attach(mainloop.loop_(), move |play| match play { + StreamCommand::Toggle(state) => { + let _ = stream.set_active(state); + } + StreamCommand::Stop => { + let _ = stream.disconnect(); + } }); mainloop.run(); drop(listener); diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index 4d59b431f..8c7cfc3d5 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -20,19 +20,31 @@ use pipewire::{ use crate::Data; +#[derive(Debug, Clone, Copy)] +pub enum StreamCommand { + Toggle(bool), + Stop, +} + #[allow(unused)] pub struct Stream { pub(crate) handle: JoinHandle<()>, - pub(crate) controller: pw::channel::Sender, + pub(crate) controller: pw::channel::Sender, +} + +impl Drop for Stream { + fn drop(&mut self) { + let _ = self.controller.send(StreamCommand::Stop); + } } impl StreamTrait for Stream { fn play(&self) -> Result<(), crate::PlayStreamError> { - let _ = self.controller.send(true); + let _ = self.controller.send(StreamCommand::Toggle(true)); Ok(()) } fn pause(&self) -> Result<(), crate::PauseStreamError> { - let _ = self.controller.send(false); + let _ = self.controller.send(StreamCommand::Toggle(false)); Ok(()) } } From 73f7f924a46392840594bc93991b58e048e58471 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Mon, 16 Feb 2026 11:42:51 +0900 Subject: [PATCH 35/85] fix: thread won't stop --- src/host/pipewire/device.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 0af80a11a..7758d1e33 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -321,12 +321,14 @@ impl DeviceTrait for Device { }; let _ = pw_init_tx.send(true); let stream = stream.clone(); + let mainloop_rc1 = mainloop.clone(); let _receiver = pw_play_rv.attach(mainloop.loop_(), move |play| match play { StreamCommand::Toggle(state) => { let _ = stream.set_active(state); } StreamCommand::Stop => { let _ = stream.disconnect(); + mainloop_rc1.quit(); } }); mainloop.run(); @@ -385,12 +387,14 @@ impl DeviceTrait for Device { let _ = pw_init_tx.send(true); let stream = stream.clone(); + let mainloop_rc1 = mainloop.clone(); let _receiver = pw_play_rv.attach(mainloop.loop_(), move |play| match play { StreamCommand::Toggle(state) => { let _ = stream.set_active(state); } StreamCommand::Stop => { let _ = stream.disconnect(); + mainloop_rc1.quit(); } }); mainloop.run(); From 1c7ba88a4d4367c5643ff87907be5b394259f843 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Mon, 16 Feb 2026 11:51:41 +0900 Subject: [PATCH 36/85] fix: support support audio rate --- src/host/pipewire/device.rs | 8 ++++++-- src/lib.rs | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 7758d1e33..dce896181 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -121,6 +121,7 @@ impl Device { pub(crate) fn pw_properties( &self, direction: DeviceDirection, + config: &crate::StreamConfig, ) -> pw::properties::PropertiesBox { let mut properties = match direction { DeviceDirection::Output => pw::properties::properties! { @@ -141,6 +142,9 @@ impl Device { if matches!(self.class_type, ClassType::Node) { properties.insert(*pw::keys::TARGET_OBJECT, self.object_serial.to_string()); } + if let crate::BufferSize::Fixed(buffer_size) = config.buffer_size { + properties.insert(*pw::keys::AUDIO_RATE, buffer_size.to_string()); + } properties } } @@ -302,7 +306,7 @@ impl DeviceTrait for Device { let handle = thread::Builder::new() .name("pw_capture_music_in".to_owned()) .spawn(move || { - let properties = device.pw_properties(DeviceDirection::Input); + let properties = device.pw_properties(DeviceDirection::Input, &config); let Ok(StreamData { mainloop, listener, @@ -366,7 +370,7 @@ impl DeviceTrait for Device { let handle = thread::Builder::new() .name("pw_capture_music_out".to_owned()) .spawn(move || { - let properties = device.pw_properties(DeviceDirection::Output); + let properties = device.pw_properties(DeviceDirection::Output, &config); let Ok(StreamData { mainloop, diff --git a/src/lib.rs b/src/lib.rs index 5252671ff..6875b9702 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -393,7 +393,7 @@ impl wasm_bindgen::convert::FromWasmAbi for BufferSize { ), wasm_bindgen )] -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Copy)] pub struct StreamConfig { pub channels: ChannelCount, pub sample_rate: SampleRate, From 58342ea5d32aa974f1ca3cf271e225ed345addaa Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Mon, 16 Feb 2026 11:58:13 +0900 Subject: [PATCH 37/85] fix: do not make StreamConfig Copyable Yes, it should be marked as Copy, but it should in another pr --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 6875b9702..5252671ff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -393,7 +393,7 @@ impl wasm_bindgen::convert::FromWasmAbi for BufferSize { ), wasm_bindgen )] -#[derive(Clone, Debug, Eq, PartialEq, Copy)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct StreamConfig { pub channels: ChannelCount, pub sample_rate: SampleRate, From 58ee7ff7ef066d3a93d32569d8a9d7675aa36123 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Tue, 17 Feb 2026 10:31:13 +0900 Subject: [PATCH 38/85] fix: not set rate but the quantum I do not know if it is right. At least I should not set the rate --- src/host/pipewire/device.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index dce896181..71c0671e2 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -143,7 +143,7 @@ impl Device { properties.insert(*pw::keys::TARGET_OBJECT, self.object_serial.to_string()); } if let crate::BufferSize::Fixed(buffer_size) = config.buffer_size { - properties.insert(*pw::keys::AUDIO_RATE, buffer_size.to_string()); + properties.insert(*pw::keys::NODE_LOCK_QUANTUM, buffer_size.to_string()); } properties } From 39c22e9a2bf2387425305081bd47f752928190b6 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Tue, 17 Feb 2026 21:19:55 +0900 Subject: [PATCH 39/85] fix: buffer_size not work use "node.force-quantum" to force the buffer size --- src/host/pipewire/device.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 71c0671e2..02f24fe35 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -143,7 +143,11 @@ impl Device { properties.insert(*pw::keys::TARGET_OBJECT, self.object_serial.to_string()); } if let crate::BufferSize::Fixed(buffer_size) = config.buffer_size { - properties.insert(*pw::keys::NODE_LOCK_QUANTUM, buffer_size.to_string()); + // NOTE: https://docs.pipewire.org/group__pw__keys.html#ga7e12dd6c9dec4a345c8a2a62f138b5bf + // TODO: not in pw keys yet. later when it is included, should be changed to that one + // in source + // + properties.insert("node.force-quantum", buffer_size.to_string()); } properties } From 852c607922871c8e24fad707b879d3c922979dad Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Tue, 17 Feb 2026 21:46:35 +0900 Subject: [PATCH 40/85] chore: use NODE_FORCE_QUANTUM instead open the feature of v0_3_45 --- Cargo.toml | 2 +- src/host/pipewire/device.rs | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3157c80dc..15c22c7c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,7 +106,7 @@ audio_thread_priority = { version = "0.34", optional = true } jack = { version = "0.13", optional = true } pulseaudio = { version = "0.3", optional = true } futures = { version = "0.3", optional = true } -pipewire = { version = "0.9.2", optional = true, features = ["v0_3_44"]} +pipewire = { version = "0.9.2", optional = true, features = ["v0_3_45"]} [target.'cfg(target_vendor = "apple")'.dependencies] mach2 = "0.5" diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 02f24fe35..25c18fd87 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -143,11 +143,7 @@ impl Device { properties.insert(*pw::keys::TARGET_OBJECT, self.object_serial.to_string()); } if let crate::BufferSize::Fixed(buffer_size) = config.buffer_size { - // NOTE: https://docs.pipewire.org/group__pw__keys.html#ga7e12dd6c9dec4a345c8a2a62f138b5bf - // TODO: not in pw keys yet. later when it is included, should be changed to that one - // in source - // - properties.insert("node.force-quantum", buffer_size.to_string()); + properties.insert(*pw::keys::NODE_FORCE_QUANTUM, buffer_size.to_string()); } properties } From 7599358f81af163a1c7d6e8e025c96986b06272d Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Fri, 20 Feb 2026 10:56:34 +0900 Subject: [PATCH 41/85] chore: adjust the examples --- examples/beep.rs | 60 +++++++++++++----------------------------------- 1 file changed, 16 insertions(+), 44 deletions(-) diff --git a/examples/beep.rs b/examples/beep.rs index 1aa31de67..8da0a9d74 100644 --- a/examples/beep.rs +++ b/examples/beep.rs @@ -55,17 +55,14 @@ fn main() -> anyhow::Result<()> { let mut jack_host_id = Err(HostUnavailable); #[allow(unused_mut, unused_assignments)] let mut pulseaudio_host_id = Err(HostUnavailable); - - #[cfg(all( - any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd" - ), - feature = "jack", - not(feature = "pipewire") - ))] + #[allow(unused_mut, unused_assignments)] + let mut pipewire_host_id = Err(HostUnavailable); + #[cfg(all(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd" + ),))] { #[cfg(feature = "jack")] { @@ -76,6 +73,10 @@ fn main() -> anyhow::Result<()> { { pulseaudio_host_id = Ok(cpal::HostId::PulseAudio); } + #[cfg(feature = "pipewire")] + { + pipewire_host_id = Ok(cpal::HostId::PipeWire); + } } // Manually check for flags. Can be passed through cargo with -- e.g. @@ -88,42 +89,13 @@ fn main() -> anyhow::Result<()> { pulseaudio_host_id .and_then(cpal::host_from_id) .expect("make sure `--features pulseaudio` is specified, and the platform is supported") + } else if opt.pipewire { + pipewire_host_id + .and_then(cpal::host_from_id) + .expect("make sure `--features pipewire` is specified, and the platform is supported") } else { cpal::default_host() }; - // Conditionally compile with jack if the feature is specified. - #[cfg(all( - any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd" - ), - feature = "pipewire" - ))] - // Manually check for flags. Can be passed through cargo with -- e.g. - // cargo run --release --example beep --features jack -- --jack - let host = if opt.pipewire { - cpal::host_from_id(cpal::available_hosts() - .into_iter() - .find(|id| *id == cpal::HostId::PipeWire) - .expect( - "make sure --features pipewire is specified. only works on OSes where jack is available", - )).expect("jack host unavailable") - } else { - cpal::default_host() - }; - - #[cfg(any( - not(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd" - )), - not(any(feature = "jack", feature = "pipewire")) - ))] - let host = cpal::default_host(); let device = if let Some(device) = opt.device { let id = &device.parse().expect("failed to parse device id"); From 49e88fc8f0c255f89c839c5e6d6d419d1b5016af Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Fri, 20 Feb 2026 10:58:03 +0900 Subject: [PATCH 42/85] chore: do fmt --- src/host/mod.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/host/mod.rs b/src/host/mod.rs index ad023c77f..982d24351 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -31,6 +31,11 @@ pub(crate) mod emscripten; ) ))] pub(crate) mod jack; +#[cfg(all( + any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"), + feature = "pipewire" +))] +pub(crate) mod pipewire; #[cfg(all( any( target_os = "linux", @@ -41,11 +46,6 @@ pub(crate) mod jack; feature = "pulseaudio" ))] pub(crate) mod pulseaudio; -#[cfg(all( - any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"), - feature = "pipewire" -))] -pub(crate) mod pipewire; #[cfg(windows)] pub(crate) mod wasapi; #[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] From ce129e5cb614886831e05dd5d8b9fd8bc0ac4510 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Fri, 20 Feb 2026 11:00:56 +0900 Subject: [PATCH 43/85] fix: unit test --- examples/beep.rs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/examples/beep.rs b/examples/beep.rs index 8da0a9d74..a613ab380 100644 --- a/examples/beep.rs +++ b/examples/beep.rs @@ -32,17 +32,8 @@ struct Opt { #[arg(long, default_value_t = false)] pulseaudio: bool, /// Use the pipewire host - #[cfg(all( - any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd" - ), - feature = "pipewire" - ))] - #[arg(short, long)] - #[allow(dead_code)] + + #[arg(long, default_value_t = false)] pipewire: bool, } From 08234c3f7f27ec2179cdb408dcbb568f70ccc928 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Sun, 22 Feb 2026 11:04:37 +0900 Subject: [PATCH 44/85] chore: do as suggested, use the type alias, join thread and etc Have not solved all suggestions yet --- Cargo.toml | 4 +- examples/record_wav.rs | 2 +- src/host/mod.rs | 7 +++- src/host/pipewire/device.rs | 79 ++++++++++++++++++------------------- src/host/pipewire/mod.rs | 6 +-- src/host/pipewire/stream.rs | 7 ++-- 6 files changed, 53 insertions(+), 52 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 15c22c7c9..682b778e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,8 +66,6 @@ audioworklet = [ # Platform: All platforms custom = [] -default = [] - [dependencies] dasp_sample = "0.11" @@ -106,7 +104,7 @@ audio_thread_priority = { version = "0.34", optional = true } jack = { version = "0.13", optional = true } pulseaudio = { version = "0.3", optional = true } futures = { version = "0.3", optional = true } -pipewire = { version = "0.9.2", optional = true, features = ["v0_3_45"]} +pipewire = { version = "0.9", optional = true, features = ["v0_3_45"]} [target.'cfg(target_vendor = "apple")'.dependencies] mach2 = "0.5" diff --git a/examples/record_wav.rs b/examples/record_wav.rs index 0dea95c88..5ed2d2f27 100644 --- a/examples/record_wav.rs +++ b/examples/record_wav.rs @@ -75,7 +75,7 @@ fn main() -> Result<(), anyhow::Error> { cpal::default_host() }; - // Conditionally compile with jack if the feature is specified. + // Conditionally compile with pipewire if the feature is specified. #[cfg(all( any( target_os = "linux", diff --git a/src/host/mod.rs b/src/host/mod.rs index 982d24351..7801b7d41 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -32,7 +32,12 @@ pub(crate) mod emscripten; ))] pub(crate) mod jack; #[cfg(all( - any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"), + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + ), feature = "pipewire" ))] pub(crate) mod pipewire; diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 25c18fd87..6dffdaef4 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -2,8 +2,8 @@ use std::time::Duration; use std::{cell::RefCell, rc::Rc}; use crate::host::pipewire::stream::{StreamCommand, StreamData, SUPPORTED_FORMATS}; -use crate::InterfaceType; use crate::{traits::DeviceTrait, DeviceDirection, SupportedStreamConfigRange}; +use crate::{ChannelCount, FrameCount, InterfaceType, SampleRate}; use crate::iter::{SupportedInputConfigs, SupportedOutputConfigs}; use pipewire::{ @@ -20,9 +20,9 @@ use super::stream::Stream; pub type Devices = std::vec::IntoIter; -/// This enum record whether it is created by human or just default device +// This enum record whether it is created by human or just default device #[derive(Clone, Debug, Default, Copy)] -pub(crate) enum ClassType { +pub(crate) enum Class { #[default] Node, DefaultSink, @@ -37,7 +37,7 @@ pub enum Role { Source, } -#[allow(unused)] +#[allow(dead_code)] #[derive(Clone, Debug, Default)] pub struct Device { id: u32, @@ -45,14 +45,14 @@ pub struct Device { nick_name: String, description: String, direction: DeviceDirection, - channels: u16, - limit_quantum: u32, - rate: u32, - allow_rates: Vec, + channels: ChannelCount, + limit_quantum: FrameCount, + rate: SampleRate, + allow_rates: Vec, quantum: u32, - min_quantum: u32, - max_quantum: u32, - class_type: ClassType, + min_quantum: FrameCount, + max_quantum: FrameCount, + class: Class, object_id: String, device_id: String, role: Role, @@ -64,8 +64,8 @@ pub struct Device { } impl Device { - pub(crate) fn class_type(&self) -> ClassType { - self.class_type + pub(crate) fn class_type(&self) -> Class { + self.class } fn sink_default() -> Self { Self { @@ -75,7 +75,7 @@ impl Device { description: "default_sink".to_owned(), direction: DeviceDirection::Duplex, channels: 2, - class_type: ClassType::DefaultSink, + class: Class::DefaultSink, role: Role::Sink, ..Default::default() } @@ -88,7 +88,7 @@ impl Device { description: "default_input".to_owned(), direction: DeviceDirection::Input, channels: 2, - class_type: ClassType::DefaultInput, + class: Class::DefaultInput, role: Role::Source, ..Default::default() } @@ -101,7 +101,7 @@ impl Device { description: "default_output".to_owned(), direction: DeviceDirection::Output, channels: 2, - class_type: ClassType::DefaultOutput, + class: Class::DefaultOutput, role: Role::Source, ..Default::default() } @@ -113,7 +113,6 @@ impl Device { "audio-headset" => crate::DeviceType::Headset, "audio-input-microphone" => crate::DeviceType::Microphone, "audio-speakers" => crate::DeviceType::Speaker, - "video-display" => crate::DeviceType::Speaker, _ => crate::DeviceType::Unknown, } } @@ -127,19 +126,17 @@ impl Device { DeviceDirection::Output => pw::properties::properties! { *pw::keys::MEDIA_TYPE => "Audio", *pw::keys::MEDIA_CATEGORY => "Playback", - *pw::keys::MEDIA_ROLE => "Music", }, DeviceDirection::Input => pw::properties::properties! { *pw::keys::MEDIA_TYPE => "Audio", *pw::keys::MEDIA_CATEGORY => "Capture", - *pw::keys::MEDIA_ROLE => "Music", }, _ => unreachable!(), }; if matches!(self.role, Role::Sink) { properties.insert(*pw::keys::STREAM_CAPTURE_SINK, "true"); } - if matches!(self.class_type, ClassType::Node) { + if matches!(self.class, Class::Node) { properties.insert(*pw::keys::TARGET_OBJECT, self.object_serial.to_string()); } if let crate::BufferSize::Fixed(buffer_size) = config.buffer_size { @@ -304,7 +301,7 @@ impl DeviceTrait for Device { let config = config.clone(); let wait_timeout = timeout.unwrap_or(Duration::from_secs(2)); let handle = thread::Builder::new() - .name("pw_capture_music_in".to_owned()) + .name("pw_in".to_owned()) .spawn(move || { let properties = device.pw_properties(DeviceDirection::Input, &config); let Ok(StreamData { @@ -342,7 +339,7 @@ impl DeviceTrait for Device { .unwrap(); match pw_init_rv.recv_timeout(wait_timeout) { Ok(true) => Ok(Stream { - handle, + handle: Some(handle), controller: pw_play_tx, }), Ok(false) | Err(_) => Err(crate::BuildStreamError::StreamConfigNotSupported), @@ -368,7 +365,7 @@ impl DeviceTrait for Device { let config = config.clone(); let wait_timeout = timeout.unwrap_or(Duration::from_secs(2)); let handle = thread::Builder::new() - .name("pw_capture_music_out".to_owned()) + .name("pw_out".to_owned()) .spawn(move || { let properties = device.pw_properties(DeviceDirection::Output, &config); @@ -408,7 +405,7 @@ impl DeviceTrait for Device { .unwrap(); match pw_init_rv.recv_timeout(wait_timeout) { Ok(true) => Ok(Stream { - handle, + handle: Some(handle), controller: pw_play_tx, }), Ok(false) | Err(_) => Err(crate::BuildStreamError::StreamConfigNotSupported), @@ -417,7 +414,7 @@ impl DeviceTrait for Device { } impl Device { - pub fn channels(&self) -> u16 { + pub fn channels(&self) -> ChannelCount { self.channels } pub fn direction(&self) -> DeviceDirection { @@ -427,33 +424,33 @@ impl Device { &self.node_name } - pub fn limit_quantam(&self) -> u32 { + pub fn limit_quantum(&self) -> FrameCount { self.limit_quantum } - pub fn min_quantum(&self) -> u32 { + pub fn min_quantum(&self) -> FrameCount { self.min_quantum } - pub fn max_quantum(&self) -> u32 { + pub fn max_quantum(&self) -> FrameCount { self.max_quantum } - pub fn quantum(&self) -> u32 { + pub fn quantum(&self) -> FrameCount { self.quantum } - pub fn rate(&self) -> u32 { + pub fn rate(&self) -> SampleRate { self.rate } - pub fn allow_rates(&self) -> &[u32] { + pub fn allow_rates(&self) -> &[FrameCount] { &self.allow_rates } } #[derive(Debug, Clone, Default)] struct Settings { - rate: u32, - allow_rates: Vec, - quantum: u32, - min_quantum: u32, - max_quantum: u32, + rate: SampleRate, + allow_rates: Vec, + quantum: FrameCount, + min_quantum: FrameCount, + max_quantum: FrameCount, } #[allow(dead_code)] @@ -492,15 +489,15 @@ fn init_roundtrip() -> Option> { // Trigger the sync event. The server's answer won't be processed until we start the main loop, // so we can safely do this before setting up a callback. This lets us avoid using a Cell. - let peddings: Rc>> = Rc::new(RefCell::new(vec![])); + let pending_events: Rc>> = Rc::new(RefCell::new(vec![])); let pending = core.sync(0).expect("sync failed"); - peddings.borrow_mut().push(pending); + pending_events.borrow_mut().push(pending); let _listener_core = core .add_listener_local() .done({ - let peddings = peddings.clone(); + let peddings = pending_events.clone(); move |id, seq| { if id != pw::core::PW_ID_CORE { return; @@ -591,7 +588,7 @@ fn init_roundtrip() -> Option> { }) .register(); let pending = core.sync(0).expect("sync failed"); - peddings.borrow_mut().push(pending); + pending_events.borrow_mut().push(pending); requests .borrow_mut() .push((meta_settings.upcast(), Request::Meta(listener))); @@ -713,7 +710,7 @@ fn init_roundtrip() -> Option> { }) .register(); let pending = core.sync(0).expect("sync failed"); - peddings.borrow_mut().push(pending); + pending_events.borrow_mut().push(pending); requests .borrow_mut() .push((node.upcast(), Request::Node(listener))); diff --git a/src/host/pipewire/mod.rs b/src/host/pipewire/mod.rs index af7c402a0..316b9622c 100644 --- a/src/host/pipewire/mod.rs +++ b/src/host/pipewire/mod.rs @@ -1,4 +1,4 @@ -use device::{init_devices, ClassType, Device, Devices}; +use device::{init_devices, Class, Device, Devices}; use crate::traits::HostTrait; mod device; @@ -26,13 +26,13 @@ impl HostTrait for Host { fn default_input_device(&self) -> Option { self.0 .iter() - .find(|device| matches!(device.class_type(), ClassType::DefaultSink)) + .find(|device| matches!(device.class_type(), Class::DefaultSink)) .cloned() } fn default_output_device(&self) -> Option { self.0 .iter() - .find(|device| matches!(device.class_type(), ClassType::DefaultOutput)) + .find(|device| matches!(device.class_type(), Class::DefaultOutput)) .cloned() } } diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index 8c7cfc3d5..0064717ed 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -28,13 +28,14 @@ pub enum StreamCommand { #[allow(unused)] pub struct Stream { - pub(crate) handle: JoinHandle<()>, + pub(crate) handle: Option>, pub(crate) controller: pw::channel::Sender, } impl Drop for Stream { fn drop(&mut self) { let _ = self.controller.send(StreamCommand::Stop); + let _ = self.handle.take().map(|handle| handle.join()); } } @@ -260,7 +261,7 @@ where let n_samples = samples.len() / user_data.sample_format.sample_size(); - let data = samples.as_ptr() as *mut (); + let data = samples.as_mut_ptr() as *mut (); let mut data = unsafe { Data::from_parts(data, n_samples, user_data.sample_format) }; if let Err(err) = user_data.publish_data_out(frames, &mut data) { @@ -377,7 +378,7 @@ where let Some(samples) = data.data() else { return; }; - let data = samples.as_ptr() as *mut (); + let data = samples.as_mut_ptr() as *mut (); let data = unsafe { Data::from_parts(data, n_samples as usize, user_data.sample_format) }; if let Err(err) = user_data.publish_data_in(frames as usize, &data) { From 31e3955cabb8210f96b54c0ac8838c0a3cd6eeab Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Sun, 22 Feb 2026 12:22:15 +0900 Subject: [PATCH 45/85] chore: comment RT_PROCESS maybe add an option later --- src/host/pipewire/stream.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index 0064717ed..89aff0287 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -294,14 +294,13 @@ where let mut params = [Pod::from_bytes(&values).unwrap()]; + // TODO: what about RT_PROCESS? /* Now connect this stream. We ask that our process function is * called in a realtime thread. */ stream.connect( pw::spa::utils::Direction::Output, None, - pw::stream::StreamFlags::AUTOCONNECT - | pw::stream::StreamFlags::MAP_BUFFERS - | pw::stream::StreamFlags::RT_PROCESS, + pw::stream::StreamFlags::AUTOCONNECT | pw::stream::StreamFlags::MAP_BUFFERS, &mut params, )?; @@ -407,14 +406,13 @@ where let mut params = [Pod::from_bytes(&values).unwrap()]; + // TODO: what about RT_PROCESS? /* Now connect this stream. We ask that our process function is * called in a realtime thread. */ stream.connect( pw::spa::utils::Direction::Input, None, - pw::stream::StreamFlags::AUTOCONNECT - | pw::stream::StreamFlags::MAP_BUFFERS - | pw::stream::StreamFlags::RT_PROCESS, + pw::stream::StreamFlags::AUTOCONNECT | pw::stream::StreamFlags::MAP_BUFFERS, &mut params, )?; From 7f7f854f03a03256a4164b57363f23674fba63a9 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Sun, 22 Feb 2026 12:35:32 +0900 Subject: [PATCH 46/85] chore: better error handle --- src/host/pipewire/device.rs | 38 ++++++++++++++++++++++++++++++------- src/host/pipewire/stream.rs | 16 ++++++++++++++-- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 6dffdaef4..56a57af67 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -336,13 +336,22 @@ impl DeviceTrait for Device { drop(listener); drop(context); }) - .unwrap(); + .map_err(|e| crate::BuildStreamError::BackendSpecific { + err: crate::BackendSpecificError { + description: format!("failed to create thread: {e}"), + }, + })?; match pw_init_rv.recv_timeout(wait_timeout) { Ok(true) => Ok(Stream { handle: Some(handle), controller: pw_play_tx, }), - Ok(false) | Err(_) => Err(crate::BuildStreamError::StreamConfigNotSupported), + Ok(false) => Err(crate::BuildStreamError::StreamConfigNotSupported), + Err(_) => Err(crate::BuildStreamError::BackendSpecific { + err: crate::BackendSpecificError { + description: "pipewire timeout".to_owned(), + }, + }), } } @@ -402,13 +411,22 @@ impl DeviceTrait for Device { drop(listener); drop(context); }) - .unwrap(); + .map_err(|e| crate::BuildStreamError::BackendSpecific { + err: crate::BackendSpecificError { + description: format!("failed to create thread: {e}"), + }, + })?; match pw_init_rv.recv_timeout(wait_timeout) { Ok(true) => Ok(Stream { handle: Some(handle), controller: pw_play_tx, }), - Ok(false) | Err(_) => Err(crate::BuildStreamError::StreamConfigNotSupported), + Ok(false) => Err(crate::BuildStreamError::StreamConfigNotSupported), + Err(_) => Err(crate::BuildStreamError::BackendSpecific { + err: crate::BackendSpecificError { + description: "pipewire timeout".to_owned(), + }, + }), } } } @@ -490,7 +508,7 @@ fn init_roundtrip() -> Option> { // Trigger the sync event. The server's answer won't be processed until we start the main loop, // so we can safely do this before setting up a callback. This lets us avoid using a Cell. let pending_events: Rc>> = Rc::new(RefCell::new(vec![])); - let pending = core.sync(0).expect("sync failed"); + let pending = core.sync(0).ok()?; pending_events.borrow_mut().push(pending); @@ -587,7 +605,10 @@ fn init_roundtrip() -> Option> { 0 }) .register(); - let pending = core.sync(0).expect("sync failed"); + let Ok(pending) = core.sync(0) else { + // TODO: maybe we should add a log? + return; + }; pending_events.borrow_mut().push(pending); requests .borrow_mut() @@ -709,7 +730,10 @@ fn init_roundtrip() -> Option> { devices.borrow_mut().push(device); }) .register(); - let pending = core.sync(0).expect("sync failed"); + let Ok(pending) = core.sync(0) else { + // TODO: maybe we should add a log? + return; + }; pending_events.borrow_mut().push(pending); requests .borrow_mut() diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index 89aff0287..7af84731a 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -41,11 +41,23 @@ impl Drop for Stream { impl StreamTrait for Stream { fn play(&self) -> Result<(), crate::PlayStreamError> { - let _ = self.controller.send(StreamCommand::Toggle(true)); + self.controller + .send(StreamCommand::Toggle(true)) + .map_err(|_| crate::PlayStreamError::BackendSpecific { + err: BackendSpecificError { + description: "Cannot send message".to_owned(), + }, + })?; Ok(()) } fn pause(&self) -> Result<(), crate::PauseStreamError> { - let _ = self.controller.send(StreamCommand::Toggle(false)); + self.controller + .send(StreamCommand::Toggle(false)) + .map_err(|_| crate::PauseStreamError::BackendSpecific { + err: BackendSpecificError { + description: "Cannot send message".to_owned(), + }, + })?; Ok(()) } } From 48af57ef11745d4e1803930e1ad41c85c0e34ca5 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Sun, 22 Feb 2026 13:05:05 +0900 Subject: [PATCH 47/85] chore: do error callback if the rate or channels does not fit the input --- src/host/pipewire/stream.rs | 53 ++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index 7af84731a..6617fef50 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -227,11 +227,12 @@ where format: Default::default(), created_instance: Instant::now(), }; - + let channels = config.channels as _; + let rate = config.sample_rate as _; let stream = pw::stream::StreamRc::new(core, "cpal-playback", properties)?; let listener = stream .add_local_listener_with_user_data(data) - .param_changed(|_, user_data, id, param| { + .param_changed(move|_, user_data, id, param| { let Some(param) = param else { return; }; @@ -248,12 +249,20 @@ where if media_type != MediaType::Audio || media_subtype != MediaSubtype::Raw { return; } - // call a helper function to parse the format for us. - user_data - .format - .parse(param) - .expect("Failed to parse param changed to AudioInfoRaw"); + // When the format update, we check the format first, in case it does not fit what we + // set + if user_data.format.parse(param).is_ok() { + let current_channels = user_data.format.channels(); + let current_rate = user_data.format.rate(); + if current_channels != channels || rate != current_rate { + (user_data.error_callback)(StreamError::BackendSpecific { + err: BackendSpecificError { + description: format!("channels or rate is not fit, current channels: {current_channels}, current rate: {current_rate}"), + }, + }); + } + } }) .process(|stream, user_data| match stream.dequeue_buffer() { None => (user_data.error_callback)(StreamError::BufferUnderrun), @@ -288,8 +297,8 @@ where .register()?; let mut audio_info = pw::spa::param::audio::AudioInfoRaw::new(); audio_info.set_format(sample_format.into()); - audio_info.set_rate(config.sample_rate); - audio_info.set_channels(config.channels as u32); + audio_info.set_rate(rate); + audio_info.set_channels(channels); let obj = pw::spa::pod::Object { type_: pw::spa::utils::SpaTypes::ObjectParamFormat.as_raw(), @@ -347,10 +356,13 @@ where created_instance: Instant::now(), }; + let channels = config.channels as _; + let rate = config.sample_rate as _; + let stream = pw::stream::StreamRc::new(core, "cpal-capture", properties)?; let listener = stream .add_local_listener_with_user_data(data) - .param_changed(|_, user_data, id, param| { + .param_changed(move |_, user_data, id, param| { let Some(param) = param else { return; }; @@ -369,10 +381,19 @@ where } // call a helper function to parse the format for us. - user_data - .format - .parse(param) - .expect("Failed to parse param changed to AudioInfoRaw"); + // When the format update, we check the format first, in case it does not fit what we + // set + if user_data.format.parse(param).is_ok() { + let current_channels = user_data.format.channels(); + let current_rate = user_data.format.rate(); + if current_channels != channels || rate != current_rate { + (user_data.error_callback)(StreamError::BackendSpecific { + err: BackendSpecificError { + description: format!("channels or rate is not fit, current channels: {current_channels}, current rate: {current_rate}"), + }, + }); + } + } }) .process(|stream, user_data| match stream.dequeue_buffer() { None => (user_data.error_callback)(StreamError::BufferUnderrun), @@ -400,8 +421,8 @@ where .register()?; let mut audio_info = pw::spa::param::audio::AudioInfoRaw::new(); audio_info.set_format(sample_format.into()); - audio_info.set_rate(config.sample_rate); - audio_info.set_channels(config.channels as u32); + audio_info.set_rate(rate); + audio_info.set_channels(channels); let obj = pw::spa::pod::Object { type_: pw::spa::utils::SpaTypes::ObjectParamFormat.as_raw(), From b2acf3a9506b600d4f2468fbe6e0b77c30c660c0 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Sun, 22 Feb 2026 13:07:39 +0900 Subject: [PATCH 48/85] chore: rename class_type to class --- src/host/pipewire/device.rs | 2 +- src/host/pipewire/mod.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 56a57af67..fa2b8dc4b 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -64,7 +64,7 @@ pub struct Device { } impl Device { - pub(crate) fn class_type(&self) -> Class { + pub(crate) fn class(&self) -> Class { self.class } fn sink_default() -> Self { diff --git a/src/host/pipewire/mod.rs b/src/host/pipewire/mod.rs index 316b9622c..e1ee6fce2 100644 --- a/src/host/pipewire/mod.rs +++ b/src/host/pipewire/mod.rs @@ -26,13 +26,13 @@ impl HostTrait for Host { fn default_input_device(&self) -> Option { self.0 .iter() - .find(|device| matches!(device.class_type(), Class::DefaultSink)) + .find(|device| matches!(device.class(), Class::DefaultSink)) .cloned() } fn default_output_device(&self) -> Option { self.0 .iter() - .find(|device| matches!(device.class_type(), Class::DefaultOutput)) + .find(|device| matches!(device.class(), Class::DefaultOutput)) .cloned() } } From aa0aeb8f23efe9591fc1a87b40f5299a2e9a9bcd Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Sun, 22 Feb 2026 13:55:28 +0900 Subject: [PATCH 49/85] chore: use keys in pipewire crate --- src/host/pipewire/device.rs | 28 ++++++++++++++++------------ src/host/pipewire/mod.rs | 2 ++ src/host/pipewire/stream.rs | 1 - src/host/pipewire/utils.rs | 19 +++++++++++++++++++ 4 files changed, 37 insertions(+), 13 deletions(-) create mode 100644 src/host/pipewire/utils.rs diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index fa2b8dc4b..46a457f3f 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -2,6 +2,7 @@ use std::time::Duration; use std::{cell::RefCell, rc::Rc}; use crate::host::pipewire::stream::{StreamCommand, StreamData, SUPPORTED_FORMATS}; +use crate::host::pipewire::utils::METADATA_NAME; use crate::{traits::DeviceTrait, DeviceDirection, SupportedStreamConfigRange}; use crate::{ChannelCount, FrameCount, InterfaceType, SampleRate}; @@ -543,7 +544,7 @@ fn init_roundtrip() -> Option> { pipewire::types::ObjectType::Metadata => { if !global.props.is_some_and(|props| { props - .get("metadata.name") + .get(*METADATA_NAME) .is_some_and(|name| name == "settings") }) { return; @@ -618,7 +619,7 @@ fn init_roundtrip() -> Option> { let Some(props) = global.props else { return; }; - let Some(media_class) = props.get("media.class") else { + let Some(media_class) = props.get(*pw::keys::MEDIA_CLASS) else { return; }; if !matches!(media_class, "Audio/Sink" | "Audio/Source") { @@ -634,7 +635,7 @@ fn init_roundtrip() -> Option> { let Some(props) = info.props() else { return; }; - let Some(media_class) = props.get("media.class") else { + let Some(media_class) = props.get(*pw::keys::MEDIA_CLASS) else { return; }; let role = match media_class { @@ -656,30 +657,33 @@ fn init_roundtrip() -> Option> { (_, Role::Sink) => DeviceDirection::Output, (_, Role::Source) => DeviceDirection::Input, }; - let Some(object_id) = props.get("object.id") else { + let Some(object_id) = props.get(*pw::keys::OBJECT_ID) else { return; }; - let Some(device_id) = props.get("device.id") else { + let Some(device_id) = props.get(*pw::keys::DEVICE_ID) else { return; }; let Some(object_serial) = props - .get("object.serial") + .get(*pw::keys::OBJECT_SERIAL) .and_then(|serial| serial.parse().ok()) else { return; }; let id = info.id(); - let node_name = props.get("node.name").unwrap_or("unknown").to_owned(); + let node_name = props + .get(*pw::keys::NODE_NAME) + .unwrap_or("unknown") + .to_owned(); let description = props - .get("node.description") + .get(*pw::keys::NODE_DESCRIPTION) .unwrap_or("unknown") .to_owned(); let nick_name = props - .get("node.nick") + .get(*pw::keys::NODE_NICK) .unwrap_or(description.as_str()) .to_owned(); let channels = props - .get("audio.channels") + .get(*pw::keys::AUDIO_CHANNELS) .and_then(|channels| channels.parse().ok()) .unwrap_or(2); let limit_quantum: u32 = props @@ -691,7 +695,7 @@ fn init_roundtrip() -> Option> { .unwrap_or("default") .to_owned(); - let interface_type = match props.get("device.api") { + let interface_type = match props.get(*pw::keys::DEVICE_API) { Some("bluez5") => InterfaceType::Bluetooth, _ => match props.get("device.bus") { Some("pci") => InterfaceType::Pci, @@ -707,7 +711,7 @@ fn init_roundtrip() -> Option> { .or_else(|| props.get("api.alsa.path")) .map(|s| s.to_owned()); - let driver = props.get("factory.name").map(|s| s.to_owned()); + let driver = props.get(*pw::keys::FACTORY_NAME).map(|s| s.to_owned()); let device = Device { id, diff --git a/src/host/pipewire/mod.rs b/src/host/pipewire/mod.rs index e1ee6fce2..8871dc5a7 100644 --- a/src/host/pipewire/mod.rs +++ b/src/host/pipewire/mod.rs @@ -3,6 +3,8 @@ use device::{init_devices, Class, Device, Devices}; use crate::traits::HostTrait; mod device; mod stream; +mod utils; + #[derive(Debug)] pub struct Host(Vec); diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index 6617fef50..81d3f59fc 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -26,7 +26,6 @@ pub enum StreamCommand { Stop, } -#[allow(unused)] pub struct Stream { pub(crate) handle: Option>, pub(crate) controller: pw::channel::Sender, diff --git a/src/host/pipewire/utils.rs b/src/host/pipewire/utils.rs new file mode 100644 index 000000000..3a5ca4bb7 --- /dev/null +++ b/src/host/pipewire/utils.rs @@ -0,0 +1,19 @@ +use pipewire::sys; +use std::ffi::CStr; +use std::sync::LazyLock; + +// unfortunately we have to take two args as concat_idents! is in experimental +macro_rules! key_constant { + ($name:ident, $pw_symbol:ident, #[doc = $doc:expr]) => { + #[doc = $doc] + pub static $name: LazyLock<&'static str> = LazyLock::new(|| unsafe { + CStr::from_bytes_with_nul_unchecked(sys::$pw_symbol) + .to_str() + .unwrap() + }); + }; +} + +key_constant!(METADATA_NAME, PW_KEY_METADATA_NAME, + /// METADATA_NAME +); From ae7d5ea9dbad1d98d85a7ec0791c0e2b86ea4480 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Sun, 22 Feb 2026 14:10:38 +0900 Subject: [PATCH 50/85] chore: use constants and the keys in pipewire --- src/host/pipewire/device.rs | 38 +++++++++++++++++----------------- src/host/pipewire/utils.rs | 41 ++++++++++++++++++++++--------------- 2 files changed, 44 insertions(+), 35 deletions(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 46a457f3f..aa4a8ed01 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -2,7 +2,9 @@ use std::time::Duration; use std::{cell::RefCell, rc::Rc}; use crate::host::pipewire::stream::{StreamCommand, StreamData, SUPPORTED_FORMATS}; -use crate::host::pipewire::utils::METADATA_NAME; +use crate::host::pipewire::utils::{ + audio, clock, group, DEVICE_ICON_NAME, METADATA_NAME, PORT_GROUP, +}; use crate::{traits::DeviceTrait, DeviceDirection, SupportedStreamConfigRange}; use crate::{ChannelCount, FrameCount, InterfaceType, SampleRate}; @@ -544,7 +546,7 @@ fn init_roundtrip() -> Option> { pipewire::types::ObjectType::Metadata => { if !global.props.is_some_and(|props| { props - .get(*METADATA_NAME) + .get(METADATA_NAME) .is_some_and(|name| name == "settings") }) { return; @@ -555,13 +557,13 @@ fn init_roundtrip() -> Option> { .add_listener_local() .property(move |_, key, _, value| { match (key, value) { - (Some("clock.rate"), Some(rate)) => { + (Some(clock::RATE), Some(rate)) => { let Ok(rate) = rate.parse() else { return 0; }; settings.borrow_mut().rate = rate; } - (Some("clock.allowed-rates"), Some(list)) => { + (Some(clock::ALLOWED_RATES), Some(list)) => { let Some(list) = list.strip_prefix("[") else { return 0; }; @@ -583,19 +585,19 @@ fn init_roundtrip() -> Option> { } settings.borrow_mut().allow_rates = allow_rates; } - (Some("clock.quantum"), Some(quantum)) => { + (Some(clock::QUANTUM), Some(quantum)) => { let Ok(quantum) = quantum.parse() else { return 0; }; settings.borrow_mut().quantum = quantum; } - (Some("clock.min-quantum"), Some(min_quantum)) => { + (Some(clock::MIN_QUANTUM), Some(min_quantum)) => { let Ok(min_quantum) = min_quantum.parse() else { return 0; }; settings.borrow_mut().min_quantum = min_quantum; } - (Some("clock.max-quantum"), Some(max_quantum)) => { + (Some(clock::MAX_QUANTUM), Some(max_quantum)) => { let Ok(max_quantum) = max_quantum.parse() else { return 0; }; @@ -622,7 +624,7 @@ fn init_roundtrip() -> Option> { let Some(media_class) = props.get(*pw::keys::MEDIA_CLASS) else { return; }; - if !matches!(media_class, "Audio/Sink" | "Audio/Source") { + if !matches!(media_class, audio::SINK | audio::SOURCE) { return; } @@ -639,19 +641,19 @@ fn init_roundtrip() -> Option> { return; }; let role = match media_class { - "Audio/Sink" => Role::Sink, - "Audio/Source" => Role::Source, + audio::SINK => Role::Sink, + audio::SOURCE => Role::Source, _ => { return; } }; - let Some(group) = props.get("port.group") else { + let Some(group) = props.get(PORT_GROUP) else { return; }; let direction = match (group, role) { - ("playback", Role::Sink) => DeviceDirection::Duplex, - ("playback", Role::Source) => DeviceDirection::Output, - ("capture", _) => DeviceDirection::Input, + (group::PLAY_BACK, Role::Sink) => DeviceDirection::Duplex, + (group::PLAY_BACK, Role::Source) => DeviceDirection::Output, + (group::CAPTURE, _) => DeviceDirection::Input, // Bluetooth and other non-ALSA devices use generic port group // names like "stream.0" — derive direction from media.class (_, Role::Sink) => DeviceDirection::Output, @@ -687,13 +689,11 @@ fn init_roundtrip() -> Option> { .and_then(|channels| channels.parse().ok()) .unwrap_or(2); let limit_quantum: u32 = props - .get("clock.quantum-limit") + .get(clock::QUANTUM_LIMIT) .and_then(|channels| channels.parse().ok()) .unwrap_or(0); - let icon_name = props - .get("device.icon_name") - .unwrap_or("default") - .to_owned(); + let icon_name = + props.get(DEVICE_ICON_NAME).unwrap_or("default").to_owned(); let interface_type = match props.get(*pw::keys::DEVICE_API) { Some("bluez5") => InterfaceType::Bluetooth, diff --git a/src/host/pipewire/utils.rs b/src/host/pipewire/utils.rs index 3a5ca4bb7..c116cd39d 100644 --- a/src/host/pipewire/utils.rs +++ b/src/host/pipewire/utils.rs @@ -1,19 +1,28 @@ -use pipewire::sys; -use std::ffi::CStr; -use std::sync::LazyLock; +pub const METADATA_NAME: &str = "metadata.name"; +pub const PORT_GROUP: &str = "port.group"; -// unfortunately we have to take two args as concat_idents! is in experimental -macro_rules! key_constant { - ($name:ident, $pw_symbol:ident, #[doc = $doc:expr]) => { - #[doc = $doc] - pub static $name: LazyLock<&'static str> = LazyLock::new(|| unsafe { - CStr::from_bytes_with_nul_unchecked(sys::$pw_symbol) - .to_str() - .unwrap() - }); - }; +// NOTE: the icon name contains bluetooth and etc, not icon-name, but icon_name +// I have tried to get the information, and get +// "device.icon-name": "audio-card-analog", +// "device.icon_name": "video-display", +// So seems the `icon_name` is usable +pub const DEVICE_ICON_NAME: &str = "device.icon_name"; + +pub mod clock { + pub const RATE: &str = "clock.rate"; + pub const ALLOWED_RATES: &str = "clock.allowed-rates"; + pub const QUANTUM: &str = "clock.quantum"; + pub const MIN_QUANTUM: &str = "clock.min-quantum"; + pub const MAX_QUANTUM: &str = "clock.max-quantum"; + pub const QUANTUM_LIMIT: &str = "clock.quantum-limit"; +} + +pub mod audio { + pub const SINK: &str = "Audio/Sink"; + pub const SOURCE: &str = "Audio/Source"; } -key_constant!(METADATA_NAME, PW_KEY_METADATA_NAME, - /// METADATA_NAME -); +pub mod group { + pub const PLAY_BACK: &str = "playback"; + pub const CAPTURE: &str = "capture"; +} From 76e1cb2386b3e847b2515f65d375e7e3d0e618e7 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Sun, 22 Feb 2026 18:11:45 +0900 Subject: [PATCH 51/85] chore: try downgrade the compiler version --- .github/workflows/platforms.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/platforms.yml b/.github/workflows/platforms.yml index 04660f40e..27f17ab47 100644 --- a/.github/workflows/platforms.yml +++ b/.github/workflows/platforms.yml @@ -24,12 +24,12 @@ on: env: # MSRV varies by backend due to platform-specific dependencies - MSRV_AAUDIO: "1.85" - MSRV_ALSA: "1.85" + MSRV_AAUDIO: "1.82" + MSRV_ALSA: "1.82" MSRV_COREAUDIO: "1.80" MSRV_JACK: "1.82" MSRV_PULSEAUDIO: "1.88" - MSRV_WASIP1: "1.82" + MSRV_WASIP1: "1.78" MSRV_WASM: "1.82" MSRV_WINDOWS: "1.82" From efd177e285ad88540fa1dca289d9fa2cf4d30f76 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Sun, 22 Feb 2026 22:09:30 +0900 Subject: [PATCH 52/85] chore: set default_input to DefaultInput not DefaultSink --- src/host/pipewire/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/host/pipewire/mod.rs b/src/host/pipewire/mod.rs index 8871dc5a7..de98f3672 100644 --- a/src/host/pipewire/mod.rs +++ b/src/host/pipewire/mod.rs @@ -28,7 +28,7 @@ impl HostTrait for Host { fn default_input_device(&self) -> Option { self.0 .iter() - .find(|device| matches!(device.class(), Class::DefaultSink)) + .find(|device| matches!(device.class(), Class::DefaultInput)) .cloned() } fn default_output_device(&self) -> Option { From 330e5b8f8b9f1e6b5459c2026521a9568434c290 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Sun, 22 Feb 2026 22:44:06 +0900 Subject: [PATCH 53/85] chore: handle state change in pipewire --- src/host/pipewire/stream.rs | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index 81d3f59fc..601d64ddd 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -15,7 +15,7 @@ use pipewire::{ }, pod::Pod, }, - stream::{StreamListener, StreamRc}, + stream::{StreamListener, StreamRc, StreamState}, }; use crate::Data; @@ -128,7 +128,25 @@ pub struct UserData { format: pw::spa::param::audio::AudioInfoRaw, created_instance: Instant, } - +impl UserData +where + E: FnMut(StreamError) + Send + 'static, +{ + fn state_changed(&mut self, new: StreamState) { + match new { + pipewire::stream::StreamState::Error(e) => { + (self.error_callback)(StreamError::BackendSpecific { + err: BackendSpecificError { description: e }, + }) + } + // TODO: maybe we need to log information when every new state comes? + pipewire::stream::StreamState::Paused => {} + pipewire::stream::StreamState::Streaming => {} + pipewire::stream::StreamState::Connecting => {} + pipewire::stream::StreamState::Unconnected => {} + } + } +} impl UserData where D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, @@ -263,6 +281,9 @@ where } } }) + .state_changed(|_stream, user_data, _old, new| { + user_data.state_changed(new); + }) .process(|stream, user_data| match stream.dequeue_buffer() { None => (user_data.error_callback)(StreamError::BufferUnderrun), Some(mut buffer) => { @@ -394,6 +415,9 @@ where } } }) + .state_changed(|_stream, user_data, _old, new| { + user_data.state_changed(new); + }) .process(|stream, user_data| match stream.dequeue_buffer() { None => (user_data.error_callback)(StreamError::BufferUnderrun), Some(mut buffer) => { From 95360e52ac4726e5040340e524b0d16e4819ec85 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Sun, 22 Feb 2026 22:48:02 +0900 Subject: [PATCH 54/85] fix: typos pedding and peddinglist they were typos --- src/host/pipewire/device.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index aa4a8ed01..fa8c717f6 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -518,17 +518,17 @@ fn init_roundtrip() -> Option> { let _listener_core = core .add_listener_local() .done({ - let peddings = pending_events.clone(); + let pending_events = pending_events.clone(); move |id, seq| { if id != pw::core::PW_ID_CORE { return; } - let mut peddinglist = peddings.borrow_mut(); - let Some(index) = peddinglist.iter().position(|o_seq| *o_seq == seq) else { + let mut pendinglist = pending_events.borrow_mut(); + let Some(index) = pendinglist.iter().position(|o_seq| *o_seq == seq) else { return; }; - peddinglist.remove(index); - if !peddinglist.is_empty() { + pendinglist.remove(index); + if !pendinglist.is_empty() { return; } loop_clone.quit(); From 72d17823c014361e4429a34b03f32a2ebe875a0e Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Sun, 22 Feb 2026 22:53:41 +0900 Subject: [PATCH 55/85] fix: allow_rates return wrong type it should return &[SampleRate] --- src/host/pipewire/device.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index fa8c717f6..cd8f6f61a 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -460,7 +460,7 @@ impl Device { pub fn rate(&self) -> SampleRate { self.rate } - pub fn allow_rates(&self) -> &[FrameCount] { + pub fn allow_rates(&self) -> &[SampleRate] { &self.allow_rates } } From b22d907ba9b563cee62612363ddacbe986ad542f Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Sun, 22 Feb 2026 23:03:54 +0900 Subject: [PATCH 56/85] chore: add pipewire available check --- src/host/pipewire/device.rs | 1 + src/host/pipewire/mod.rs | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index cd8f6f61a..cb127050c 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -493,6 +493,7 @@ impl From for Request { } fn init_roundtrip() -> Option> { + pw::init(); let mainloop = pw::main_loop::MainLoopRc::new(None).ok()?; let context = pw::context::ContextRc::new(&mainloop, None).ok()?; let core = context.connect_rc(None).ok()?; diff --git a/src/host/pipewire/mod.rs b/src/host/pipewire/mod.rs index de98f3672..c5f6d65a1 100644 --- a/src/host/pipewire/mod.rs +++ b/src/host/pipewire/mod.rs @@ -1,10 +1,17 @@ use device::{init_devices, Class, Device, Devices}; +use pipewire as pw; use crate::traits::HostTrait; mod device; mod stream; mod utils; +// just init the pipewire the check if it is available +fn pipewire_available() -> bool { + pw::init(); + pw::main_loop::MainLoopRc::new(None).is_ok() +} + #[derive(Debug)] pub struct Host(Vec); @@ -19,7 +26,7 @@ impl HostTrait for Host { type Devices = Devices; type Device = Device; fn is_available() -> bool { - true + pipewire_available() } fn devices(&self) -> Result { Ok(self.0.clone().into_iter()) From 3967b47f79ee1680c697a6e39a7866422c4ff363 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Sun, 22 Feb 2026 23:22:49 +0900 Subject: [PATCH 57/85] fix: connect_output does not listen to the FixedBuffer We solve it after set the feature of pipewire to v0_3_49 and with the new function of request --- Cargo.toml | 2 +- src/host/pipewire/stream.rs | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 682b778e5..712ec4ea8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,7 +104,7 @@ audio_thread_priority = { version = "0.34", optional = true } jack = { version = "0.13", optional = true } pulseaudio = { version = "0.3", optional = true } futures = { version = "0.3", optional = true } -pipewire = { version = "0.9", optional = true, features = ["v0_3_45"]} +pipewire = { version = "0.9", optional = true, features = ["v0_3_49"]} [target.'cfg(target_vendor = "apple")'.dependencies] mach2 = "0.5" diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index 601d64ddd..6909273f3 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -287,6 +287,8 @@ where .process(|stream, user_data| match stream.dequeue_buffer() { None => (user_data.error_callback)(StreamError::BufferUnderrun), Some(mut buffer) => { + // Read the requested frame count before mutably borrowing datas_mut(). + let requested = buffer.requested() as usize; let datas = buffer.datas_mut(); if datas.is_empty() { return; @@ -294,13 +296,17 @@ where let buf_data = &mut datas[0]; let n_channels = user_data.format.channels(); + let stride = user_data.sample_format.sample_size() * n_channels as usize; + // frames = samples / channels or frames = data_len / stride + // Honor the frame count PipeWire requests this cycle, capped by the + // mapped buffer capacity to guard against any mismatch. + let frames = requested.min(buf_data.as_raw().maxsize as usize / stride); let Some(samples) = buf_data.data() else { return; }; - let stride = user_data.sample_format.sample_size() * n_channels as usize; - let frames = samples.len() / stride; - let n_samples = samples.len() / user_data.sample_format.sample_size(); + // samples = frames * channels or samples = data_len / sample_size + let n_samples = frames * n_channels as usize; let data = samples.as_mut_ptr() as *mut (); let mut data = From 0a75f5af0a66334b51f4eed4ad79caa70e1a20a2 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Tue, 24 Feb 2026 09:45:52 +0900 Subject: [PATCH 58/85] chore: remove unwrap and use expect for some known results and modify the message of expect --- src/host/pipewire/device.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index cb127050c..589d765e1 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -552,7 +552,9 @@ fn init_roundtrip() -> Option> { }) { return; } - let meta_settings: Metadata = registry.bind(global).unwrap(); + let meta_settings: Metadata = registry + .bind(global) + .expect("settings is checked, and should exists"); let settings = settings.clone(); let listener = meta_settings .add_listener_local() @@ -629,7 +631,9 @@ fn init_roundtrip() -> Option> { return; } - let node: Node = registry.bind(global).expect("should ok"); + let node: Node = registry + .bind(global) + .expect("global is checked and should exists"); let devices = devices.clone(); let listener = node From 21955f29b5d3ecfd314f9f562e0cf93065cc6c3b Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Tue, 24 Feb 2026 09:47:12 +0900 Subject: [PATCH 59/85] chore: try revert the changes in platforms.yml --- .github/workflows/platforms.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/platforms.yml b/.github/workflows/platforms.yml index 27f17ab47..9c54e9561 100644 --- a/.github/workflows/platforms.yml +++ b/.github/workflows/platforms.yml @@ -96,10 +96,10 @@ jobs: run: cargo +${{ env.MSRV_ALSA }} check --examples --no-default-features --workspace --verbose - name: Run tests (all features) - run: cargo +${{ steps.msrv.outputs.all-features }} test --features=jack --workspace --verbose + run: cargo +${{ steps.msrv.outputs.all-features }} test --all-features --workspace --verbose - name: Check examples (all features) - run: cargo +${{ steps.msrv.outputs.all-features }} check --examples --features=jack --workspace --verbose + run: cargo +${{ steps.msrv.outputs.all-features }} check --examples --all-features --workspace --verbose # Linux ARMv7 (cross-compilation) linux-armv7: From 4be348f10386455f2c6ffccaa2409fedd271a3e0 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Tue, 24 Feb 2026 09:59:19 +0900 Subject: [PATCH 60/85] fix: message related to pipewire should be pipewire not jack --- examples/record_wav.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/record_wav.rs b/examples/record_wav.rs index 5ed2d2f27..b86d925ff 100644 --- a/examples/record_wav.rs +++ b/examples/record_wav.rs @@ -92,8 +92,8 @@ fn main() -> Result<(), anyhow::Error> { .into_iter() .find(|id| *id == cpal::HostId::PipeWire) .expect( - "make sure --features pipewire is specified. only works on OSes where jack is available", - )).expect("jack host unavailable") + "make sure --features pipewire is specified. only works on OSes where pipewire is available", + )).expect("pipewire host unavailable") } else { cpal::default_host() }; From 3508e4f645e792356234b42d8c23a9e7a018d78e Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Tue, 24 Feb 2026 10:18:47 +0900 Subject: [PATCH 61/85] chore: split the logic of parse rates to function and add unit tests --- src/host/pipewire/device.rs | 57 ++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 589d765e1..b2d8d6439 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -567,25 +567,10 @@ fn init_roundtrip() -> Option> { settings.borrow_mut().rate = rate; } (Some(clock::ALLOWED_RATES), Some(list)) => { - let Some(list) = list.strip_prefix("[") else { + let Some(allow_rates) = parse_allow_rates(list) else { return 0; }; - let Some(list) = list.strip_suffix("]") else { - return 0; - }; - let list = list.trim(); - let list_normalized = list.replace(',', " "); - let list: Vec<&str> = list_normalized - .split(' ') - .filter(|s| !s.is_empty()) - .collect(); - let mut allow_rates = vec![]; - for rate in list { - let Ok(rate) = rate.parse() else { - return 0; - }; - allow_rates.push(rate); - } + settings.borrow_mut().allow_rates = allow_rates; } (Some(clock::QUANTUM), Some(quantum)) => { @@ -771,3 +756,41 @@ pub fn init_devices() -> Option> { let devices = init_roundtrip()?; Some(devices) } + +fn parse_allow_rates(list: &str) -> Option> { + let list: Vec<&str> = list + .trim() + .strip_prefix("[")? + .strip_suffix("]")? + .split(' ') + .flat_map(|s| s.split(',')) + .filter(|s| !s.is_empty()) + .collect(); + let mut allow_rates = vec![]; + for rate in list { + let rate = rate.parse().ok()?; + allow_rates.push(rate); + } + Some(allow_rates) +} + +#[cfg(test)] +mod test { + use super::parse_allow_rates; + #[test] + fn rate_parse() { + // In documents, the rates are separated by space + let rate_str = r#" [ 44100 48000 88200 96000 176400 192000 ] "#; + let rates = parse_allow_rates(rate_str).unwrap(); + assert_eq!(rates, vec![44100, 48000, 88200, 96000, 176400, 192000]); + // ',' is also allowed + let rate_str = r#" [ 44100, 48000, 88200, 96000 ,176400 ,192000 ] "#; + let rates = parse_allow_rates(rate_str).unwrap(); + assert_eq!(rates, vec![44100, 48000, 88200, 96000, 176400, 192000]); + assert_eq!(rates, vec![44100, 48000, 88200, 96000, 176400, 192000]); + // We only use [] to define the list + let rate_str = r#" { 44100, 48000, 88200, 96000 ,176400 ,192000 } "#; + let rates = parse_allow_rates(rate_str); + assert_eq!(rates, None); + } +} From efb9dd705566aa6d1ca44b7ad6df714246250917 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Wed, 25 Feb 2026 10:02:11 +0900 Subject: [PATCH 62/85] chore: remove limit_quantum it is meaningful, but not used in this repo --- src/host/pipewire/device.rs | 10 +--------- src/host/pipewire/utils.rs | 1 - 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index b2d8d6439..037a4b48e 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -49,7 +49,6 @@ pub struct Device { description: String, direction: DeviceDirection, channels: ChannelCount, - limit_quantum: FrameCount, rate: SampleRate, allow_rates: Vec, quantum: u32, @@ -445,9 +444,6 @@ impl Device { &self.node_name } - pub fn limit_quantum(&self) -> FrameCount { - self.limit_quantum - } pub fn min_quantum(&self) -> FrameCount { self.min_quantum } @@ -678,10 +674,7 @@ fn init_roundtrip() -> Option> { .get(*pw::keys::AUDIO_CHANNELS) .and_then(|channels| channels.parse().ok()) .unwrap_or(2); - let limit_quantum: u32 = props - .get(clock::QUANTUM_LIMIT) - .and_then(|channels| channels.parse().ok()) - .unwrap_or(0); + let icon_name = props.get(DEVICE_ICON_NAME).unwrap_or("default").to_owned(); @@ -711,7 +704,6 @@ fn init_roundtrip() -> Option> { direction, role, channels, - limit_quantum, icon_name, object_id: object_id.to_owned(), device_id: device_id.to_owned(), diff --git a/src/host/pipewire/utils.rs b/src/host/pipewire/utils.rs index c116cd39d..1c3f416d2 100644 --- a/src/host/pipewire/utils.rs +++ b/src/host/pipewire/utils.rs @@ -14,7 +14,6 @@ pub mod clock { pub const QUANTUM: &str = "clock.quantum"; pub const MIN_QUANTUM: &str = "clock.min-quantum"; pub const MAX_QUANTUM: &str = "clock.max-quantum"; - pub const QUANTUM_LIMIT: &str = "clock.quantum-limit"; } pub mod audio { From cce2ea7fe1049df1ad11640a787fbebcee28e94d Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Wed, 25 Feb 2026 10:05:14 +0900 Subject: [PATCH 63/85] chore: rename init_roundtrip to init_devices no use to keep an unnecessary function --- src/host/pipewire/device.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 037a4b48e..ea94846c6 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -488,7 +488,7 @@ impl From for Request { } } -fn init_roundtrip() -> Option> { +pub fn init_devices() -> Option> { pw::init(); let mainloop = pw::main_loop::MainLoopRc::new(None).ok()?; let context = pw::context::ContextRc::new(&mainloop, None).ok()?; @@ -744,11 +744,6 @@ fn init_roundtrip() -> Option> { Some(devices) } -pub fn init_devices() -> Option> { - let devices = init_roundtrip()?; - Some(devices) -} - fn parse_allow_rates(list: &str) -> Option> { let list: Vec<&str> = list .trim() From b81da7286326ef3f0cc092e4a94f75cba0e1ad6f Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Wed, 25 Feb 2026 10:07:36 +0900 Subject: [PATCH 64/85] chore: just check the pipewire service to confirm if it support pipewire as @roderickvd suggested --- src/host/pipewire/mod.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/host/pipewire/mod.rs b/src/host/pipewire/mod.rs index c5f6d65a1..c29c86c54 100644 --- a/src/host/pipewire/mod.rs +++ b/src/host/pipewire/mod.rs @@ -1,15 +1,16 @@ -use device::{init_devices, Class, Device, Devices}; -use pipewire as pw; - use crate::traits::HostTrait; +use device::{init_devices, Class, Device, Devices}; mod device; mod stream; mod utils; // just init the pipewire the check if it is available +#[inline] fn pipewire_available() -> bool { - pw::init(); - pw::main_loop::MainLoopRc::new(None).is_ok() + let dir = std::env::var("PIPEWIRE_RUNTIME_DIR") + .or_else(|_| std::env::var("XDG_RUNTIME_DIR")) + .unwrap_or_default(); + std::path::Path::new(&dir).join("pipewire-0").exists() } #[derive(Debug)] From 7a57d62c1072ab828f86a7a1664761a0e17c96e2 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Wed, 25 Feb 2026 10:22:58 +0900 Subject: [PATCH 65/85] feat: support Stream/Output/Audio and Stream/Input/Audio "stream.0" --- src/host/pipewire/device.rs | 27 +++++++++++++++++++-------- src/host/pipewire/utils.rs | 3 +++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index ea94846c6..77acb8922 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -38,6 +38,9 @@ pub enum Role { Sink, #[default] Source, + Duplex, + StreamOutput, + StreamInput, } #[allow(dead_code)] @@ -56,7 +59,6 @@ pub struct Device { max_quantum: FrameCount, class: Class, object_id: String, - device_id: String, role: Role, icon_name: String, object_serial: u32, @@ -608,7 +610,14 @@ pub fn init_devices() -> Option> { let Some(media_class) = props.get(*pw::keys::MEDIA_CLASS) else { return; }; - if !matches!(media_class, audio::SINK | audio::SOURCE) { + if !matches!( + media_class, + audio::SINK + | audio::SOURCE + | audio::DUPLEX + | audio::STREAM_INPUT + | audio::STREAM_OUTPUT + ) { return; } @@ -629,6 +638,9 @@ pub fn init_devices() -> Option> { let role = match media_class { audio::SINK => Role::Sink, audio::SOURCE => Role::Source, + audio::DUPLEX => Role::Duplex, + audio::STREAM_OUTPUT => Role::StreamOutput, + audio::STREAM_INPUT => Role::StreamInput, _ => { return; } @@ -640,17 +652,17 @@ pub fn init_devices() -> Option> { (group::PLAY_BACK, Role::Sink) => DeviceDirection::Duplex, (group::PLAY_BACK, Role::Source) => DeviceDirection::Output, (group::CAPTURE, _) => DeviceDirection::Input, - // Bluetooth and other non-ALSA devices use generic port group - // names like "stream.0" — derive direction from media.class (_, Role::Sink) => DeviceDirection::Output, (_, Role::Source) => DeviceDirection::Input, + (_, Role::Duplex) => DeviceDirection::Duplex, + // Bluetooth and other non-ALSA devices use generic port group + // names like "stream.0" — derive direction from media.class + (_, Role::StreamOutput) => DeviceDirection::Output, + (_, Role::StreamInput) => DeviceDirection::Input, }; let Some(object_id) = props.get(*pw::keys::OBJECT_ID) else { return; }; - let Some(device_id) = props.get(*pw::keys::DEVICE_ID) else { - return; - }; let Some(object_serial) = props .get(*pw::keys::OBJECT_SERIAL) .and_then(|serial| serial.parse().ok()) @@ -706,7 +718,6 @@ pub fn init_devices() -> Option> { channels, icon_name, object_id: object_id.to_owned(), - device_id: device_id.to_owned(), object_serial, interface_type, address, diff --git a/src/host/pipewire/utils.rs b/src/host/pipewire/utils.rs index 1c3f416d2..12bea593b 100644 --- a/src/host/pipewire/utils.rs +++ b/src/host/pipewire/utils.rs @@ -19,6 +19,9 @@ pub mod clock { pub mod audio { pub const SINK: &str = "Audio/Sink"; pub const SOURCE: &str = "Audio/Source"; + pub const DUPLEX: &str = "Audio/Duplex"; + pub const STREAM_OUTPUT: &str = "Stream/Output/Audio"; + pub const STREAM_INPUT: &str = "Stream/Input/Audio"; } pub mod group { From 9c0181516f23e2775b048b93e1d2b271b4024e65 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Wed, 25 Feb 2026 10:27:37 +0900 Subject: [PATCH 66/85] chore: update the comment in stream.rs, about the format change PipeWire does support U64 and I64, but libspa doesn't yet. so I add a todo and a note --- src/host/pipewire/stream.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index 6909273f3..0a19bfb55 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -115,7 +115,8 @@ impl From for pw::spa::param::audio::AudioFormat { SampleFormat::F64 => Self::F64LE, #[cfg(target_endian = "big")] SampleFormat::F64 => Self::F64BE, - // TODO: maybe we also need to support others + // NOTE: Seems PipeWire does support U64 and I64, but libspa doesn't yet. + // TODO: Maybe add the support in the future _ => Self::Unknown, } } From 17252d3ddc1aa806621b587331a7875b50acb1a8 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Wed, 25 Feb 2026 10:28:43 +0900 Subject: [PATCH 67/85] chore: remove blank in examples/beep.rs --- examples/beep.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/beep.rs b/examples/beep.rs index a613ab380..7e9273a9d 100644 --- a/examples/beep.rs +++ b/examples/beep.rs @@ -31,8 +31,8 @@ struct Opt { /// Use the PulseAudio host. Requires `--features pulseaudio`. #[arg(long, default_value_t = false)] pulseaudio: bool, - /// Use the pipewire host + /// Use the pipewire host #[arg(long, default_value_t = false)] pipewire: bool, } From c1dcc4c16ecae41400f48fe171b58550888cb043 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Wed, 25 Feb 2026 10:33:08 +0900 Subject: [PATCH 68/85] chore: sync the written way in beep.rs make the flag pipewire, beep and pulseaudio enabled the same time --- examples/record_wav.rs | 124 +++++++++++++++++------------------------ 1 file changed, 50 insertions(+), 74 deletions(-) diff --git a/examples/record_wav.rs b/examples/record_wav.rs index b86d925ff..4876da9de 100644 --- a/examples/record_wav.rs +++ b/examples/record_wav.rs @@ -4,7 +4,7 @@ use clap::Parser; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; -use cpal::{FromSample, Sample}; +use cpal::{FromSample, HostUnavailable, Sample}; use std::fs::File; use std::io::BufWriter; use std::sync::{Arc, Mutex}; @@ -20,93 +20,69 @@ struct Opt { #[arg(long, default_value_t = 3)] duration: u64, - /// Use the JACK host - #[cfg(all( - any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd" - ), - feature = "jack" - ))] - #[arg(short, long)] - #[allow(dead_code)] + /// Use the JACK host. Requires `--features jack`. + #[arg(long, default_value_t = false)] jack: bool, + + /// Use the PulseAudio host. Requires `--features pulseaudio`. + #[arg(long, default_value_t = false)] + pulseaudio: bool, + /// Use the pipewire host - #[cfg(all( - any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd" - ), - feature = "pipewire" - ))] - #[arg(short, long)] - #[allow(dead_code)] + #[arg(long, default_value_t = false)] pipewire: bool, } fn main() -> Result<(), anyhow::Error> { let opt = Opt::parse(); - // Conditionally compile with jack if the feature is specified. - #[cfg(all( - any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd" - ), - feature = "jack", - not(feature = "pipewire") - ))] + // Jack/PulseAudio support must be enabled at compile time, and is + // only available on some platforms. + #[allow(unused_mut, unused_assignments)] + let mut jack_host_id = Err(HostUnavailable); + #[allow(unused_mut, unused_assignments)] + let mut pulseaudio_host_id = Err(HostUnavailable); + #[allow(unused_mut, unused_assignments)] + let mut pipewire_host_id = Err(HostUnavailable); + #[cfg(all(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd" + ),))] + { + #[cfg(feature = "jack")] + { + jack_host_id = Ok(cpal::HostId::Jack); + } + + #[cfg(feature = "pulseaudio")] + { + pulseaudio_host_id = Ok(cpal::HostId::PulseAudio); + } + #[cfg(feature = "pipewire")] + { + pipewire_host_id = Ok(cpal::HostId::PipeWire); + } + } + // Manually check for flags. Can be passed through cargo with -- e.g. // cargo run --release --example record_wav --features jack -- --jack let host = if opt.jack { - cpal::host_from_id(cpal::available_hosts() - .into_iter() - .find(|id| *id == cpal::HostId::Jack) - .expect( - "make sure --features jack is specified. only works on OSes where jack is available", - )).expect("jack host unavailable") - } else { - cpal::default_host() - }; - - // Conditionally compile with pipewire if the feature is specified. - #[cfg(all( - any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd" - ), - feature = "pipewire" - ))] - // Manually check for flags. Can be passed through cargo with -- e.g. - // cargo run --release --example record_wav --features pipewire -- -- pipewire - let host = if opt.pipewire { - cpal::host_from_id(cpal::available_hosts() - .into_iter() - .find(|id| *id == cpal::HostId::PipeWire) - .expect( - "make sure --features pipewire is specified. only works on OSes where pipewire is available", - )).expect("pipewire host unavailable") + jack_host_id + .and_then(cpal::host_from_id) + .expect("make sure `--features jack` is specified, and the platform is supported") + } else if opt.pulseaudio { + pulseaudio_host_id + .and_then(cpal::host_from_id) + .expect("make sure `--features pulseaudio` is specified, and the platform is supported") + } else if opt.pipewire { + pipewire_host_id + .and_then(cpal::host_from_id) + .expect("make sure `--features pipewire` is specified, and the platform is supported") } else { cpal::default_host() }; - #[cfg(any( - not(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd" - )), - not(any(feature = "jack", feature = "pipewire")) - ))] - let host = cpal::default_host(); // Set up the input device and stream with the default input config. let device = if let Some(device) = opt.device { From 7075e374060ca0d2a6b756f9c222b7a1d220f044 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Wed, 25 Feb 2026 10:35:06 +0900 Subject: [PATCH 69/85] chore: sync the comment of pipewire in the commands in examples --- examples/beep.rs | 2 +- examples/record_wav.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/beep.rs b/examples/beep.rs index 7e9273a9d..71f5a7600 100644 --- a/examples/beep.rs +++ b/examples/beep.rs @@ -32,7 +32,7 @@ struct Opt { #[arg(long, default_value_t = false)] pulseaudio: bool, - /// Use the pipewire host + /// Use the pipewire host. Requires `--feature pipewire` #[arg(long, default_value_t = false)] pipewire: bool, } diff --git a/examples/record_wav.rs b/examples/record_wav.rs index 4876da9de..8e1f08682 100644 --- a/examples/record_wav.rs +++ b/examples/record_wav.rs @@ -28,7 +28,7 @@ struct Opt { #[arg(long, default_value_t = false)] pulseaudio: bool, - /// Use the pipewire host + /// Use the pipewire host. Requires `--feature pipewire` #[arg(long, default_value_t = false)] pipewire: bool, } From 1bc535800e0842bde0ef1b4a8f004ab9f3a9f071 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Wed, 25 Feb 2026 10:42:36 +0900 Subject: [PATCH 70/85] chore: make the ci same logic with pulseaudio and cross --all-features also with pulseaudio --- .github/actions/determine-msrv/action.yml | 10 +++++++++- .github/workflows/platforms.yml | 6 ++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/actions/determine-msrv/action.yml b/.github/actions/determine-msrv/action.yml index 3815607a6..9f7c82373 100644 --- a/.github/actions/determine-msrv/action.yml +++ b/.github/actions/determine-msrv/action.yml @@ -12,6 +12,10 @@ inputs: description: The MSRV for PulseAudio backend (optional, Linux only) required: false default: '' + pipewire-msrv: + description: The MSRV for Pipewire backend (optional, Linux only) + required: false + default: '' outputs: all-features: @@ -28,11 +32,15 @@ runs: PLATFORM_MSRV="${{ inputs.platform-msrv }}" JACK_MSRV="${{ inputs.jack-msrv }}" PULSEAUDIO_MSRV="${{ inputs.pulseaudio-msrv }}" + PIPEWIRE_MSRV="${{ inputs.pulseaudio-msrv }}" # Use sort -V to find the maximum version VERSIONS="$PLATFORM_MSRV $JACK_MSRV" if [ -n "$PULSEAUDIO_MSRV" ]; then VERSIONS="$VERSIONS $PULSEAUDIO_MSRV" fi + if [ -n "$PIPEWIRE_MSRV" ]; then + VERSIONS="$VERSIONS $PIPEWIRE_MSRV" + fi MAX_MSRV=$(printf '%s\n' $VERSIONS | sort -V | tail -n1) echo "all-features=$MAX_MSRV" >> $GITHUB_OUTPUT - echo "Platform MSRV: $PLATFORM_MSRV, JACK MSRV: $JACK_MSRV, PulseAudio MSRV: $PULSEAUDIO_MSRV, Using for --all-features: $MAX_MSRV" + echo "Platform MSRV: $PLATFORM_MSRV, JACK MSRV: $JACK_MSRV, PulseAudio MSRV: $PULSEAUDIO_MSRV, PIPEWIRE_MSRV: $PIPEWIRE_MSRV, Using for --all-features: $MAX_MSRV" diff --git a/.github/workflows/platforms.yml b/.github/workflows/platforms.yml index 9c54e9561..da3447d05 100644 --- a/.github/workflows/platforms.yml +++ b/.github/workflows/platforms.yml @@ -29,6 +29,7 @@ env: MSRV_COREAUDIO: "1.80" MSRV_JACK: "1.82" MSRV_PULSEAUDIO: "1.88" + MSRV_PIPEWIRE: "1.82" MSRV_WASIP1: "1.78" MSRV_WASM: "1.82" MSRV_WINDOWS: "1.82" @@ -66,6 +67,7 @@ jobs: platform-msrv: ${{ env.MSRV_ALSA }} jack-msrv: ${{ env.MSRV_JACK }} pulseaudio-msrv: ${{ env.MSRV_PULSEAUDIO }} + pipewire-msrv: ${{ env.MSRV_PIPEWIRE }} - name: Install Rust MSRV (${{ env.MSRV_ALSA }}) uses: dtolnay/rust-toolchain@master @@ -153,10 +155,10 @@ jobs: run: cross +${{ env.MSRV_ALSA }} test --no-default-features --workspace --verbose --target ${{ env.TARGET }} - name: Run tests (all features) - run: cross +${{ steps.msrv.outputs.all-features }} test --features=jack --workspace --verbose --target ${{ env.TARGET }} + run: cross +${{ steps.msrv.outputs.all-features }} test --features=jack,pulseaudio --workspace --verbose --target ${{ env.TARGET }} - name: Check examples (all features) - run: cross +${{ steps.msrv.outputs.all-features }} test --features=jack --workspace --verbose --target ${{ env.TARGET }} + run: cross +${{ steps.msrv.outputs.all-features }} test --features=jack,pulseaudio --workspace --verbose --target ${{ env.TARGET }} # Windows (x86_64 and i686) windows: From ddbed8c1fd28476abb1bd08534dc27665dfdd6ec Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Wed, 25 Feb 2026 10:44:27 +0900 Subject: [PATCH 71/85] chore: remove the file added by typos-lsp it should not be here --- _typos.toml | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 _typos.toml diff --git a/_typos.toml b/_typos.toml deleted file mode 100644 index 02d3ec106..000000000 --- a/_typos.toml +++ /dev/null @@ -1,2 +0,0 @@ -[default.extend-words] -datas = "datas" From 70ddfab96bd1674e0f18b940c6c2026732392f7b Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Wed, 25 Feb 2026 10:59:58 +0900 Subject: [PATCH 72/85] chore: not commit allow(dead_code) to whole struct and add some documents --- src/host/pipewire/device.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 77acb8922..8ef6a5d07 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -43,9 +43,9 @@ pub enum Role { StreamInput, } -#[allow(dead_code)] #[derive(Clone, Debug, Default)] pub struct Device { + #[allow(dead_code)] id: u32, node_name: String, nick_name: String, @@ -58,6 +58,7 @@ pub struct Device { min_quantum: FrameCount, max_quantum: FrameCount, class: Class, + #[allow(dead_code)] object_id: String, role: Role, icon_name: String, @@ -472,6 +473,7 @@ struct Settings { max_quantum: FrameCount, } +// NOTE: it is just used to keep the lifetime #[allow(dead_code)] enum Request { Node(NodeListener), From 6f5d1c0ff5e83a53b24e30b76b6b653067d07acc Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Wed, 25 Feb 2026 11:02:00 +0900 Subject: [PATCH 73/85] chore: rename all rv to rx --- src/host/pipewire/device.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 8ef6a5d07..1844dca0b 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -299,9 +299,9 @@ impl DeviceTrait for Device { D: FnMut(&crate::Data, &crate::InputCallbackInfo) + Send + 'static, E: FnMut(crate::StreamError) + Send + 'static, { - let (pw_play_tx, pw_play_rv) = pw::channel::channel::(); + let (pw_play_tx, pw_play_rx) = pw::channel::channel::(); - let (pw_init_tx, pw_init_rv) = std::sync::mpsc::channel::(); + let (pw_init_tx, pw_init_rx) = std::sync::mpsc::channel::(); let device = self.clone(); let config = config.clone(); let wait_timeout = timeout.unwrap_or(Duration::from_secs(2)); @@ -328,7 +328,7 @@ impl DeviceTrait for Device { let _ = pw_init_tx.send(true); let stream = stream.clone(); let mainloop_rc1 = mainloop.clone(); - let _receiver = pw_play_rv.attach(mainloop.loop_(), move |play| match play { + let _receiver = pw_play_rx.attach(mainloop.loop_(), move |play| match play { StreamCommand::Toggle(state) => { let _ = stream.set_active(state); } @@ -346,7 +346,7 @@ impl DeviceTrait for Device { description: format!("failed to create thread: {e}"), }, })?; - match pw_init_rv.recv_timeout(wait_timeout) { + match pw_init_rx.recv_timeout(wait_timeout) { Ok(true) => Ok(Stream { handle: Some(handle), controller: pw_play_tx, @@ -372,9 +372,9 @@ impl DeviceTrait for Device { D: FnMut(&mut crate::Data, &crate::OutputCallbackInfo) + Send + 'static, E: FnMut(crate::StreamError) + Send + 'static, { - let (pw_play_tx, pw_play_rv) = pw::channel::channel::(); + let (pw_play_tx, pw_play_rx) = pw::channel::channel::(); - let (pw_init_tx, pw_init_rv) = std::sync::mpsc::channel::(); + let (pw_init_tx, pw_init_rx) = std::sync::mpsc::channel::(); let device = self.clone(); let config = config.clone(); let wait_timeout = timeout.unwrap_or(Duration::from_secs(2)); @@ -403,7 +403,7 @@ impl DeviceTrait for Device { let _ = pw_init_tx.send(true); let stream = stream.clone(); let mainloop_rc1 = mainloop.clone(); - let _receiver = pw_play_rv.attach(mainloop.loop_(), move |play| match play { + let _receiver = pw_play_rx.attach(mainloop.loop_(), move |play| match play { StreamCommand::Toggle(state) => { let _ = stream.set_active(state); } @@ -421,7 +421,7 @@ impl DeviceTrait for Device { description: format!("failed to create thread: {e}"), }, })?; - match pw_init_rv.recv_timeout(wait_timeout) { + match pw_init_rx.recv_timeout(wait_timeout) { Ok(true) => Ok(Stream { handle: Some(handle), controller: pw_play_tx, From c1ee2893814914e71a9d507fa794c5838ba3f3ac Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Thu, 26 Feb 2026 09:46:28 +0900 Subject: [PATCH 74/85] chore: remove useless code we do not need object_id and node_id in this place and no need for functions channels, node_name and direction --- src/host/pipewire/device.rs | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 1844dca0b..9986d80ce 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -45,8 +45,6 @@ pub enum Role { #[derive(Clone, Debug, Default)] pub struct Device { - #[allow(dead_code)] - id: u32, node_name: String, nick_name: String, description: String, @@ -58,8 +56,6 @@ pub struct Device { min_quantum: FrameCount, max_quantum: FrameCount, class: Class, - #[allow(dead_code)] - object_id: String, role: Role, icon_name: String, object_serial: u32, @@ -74,7 +70,6 @@ impl Device { } fn sink_default() -> Self { Self { - id: 0, node_name: "sink_default".to_owned(), nick_name: "sink_default".to_owned(), description: "default_sink".to_owned(), @@ -87,7 +82,6 @@ impl Device { } fn input_default() -> Self { Self { - id: 0, node_name: "input_default".to_owned(), nick_name: "input_default".to_owned(), description: "default_input".to_owned(), @@ -100,7 +94,6 @@ impl Device { } fn output_default() -> Self { Self { - id: 0, node_name: "output_default".to_owned(), nick_name: "output_default".to_owned(), description: "default_output".to_owned(), @@ -164,7 +157,7 @@ impl DeviceTrait for Device { fn description(&self) -> Result { let mut builder = crate::DeviceDescriptionBuilder::new(&self.nick_name) - .direction(self.direction()) + .direction(self.direction) .device_type(self.device_type()) .interface_type(self.interface_type); if let Some(address) = self.address.as_ref() { @@ -437,22 +430,10 @@ impl DeviceTrait for Device { } impl Device { - pub fn channels(&self) -> ChannelCount { - self.channels - } - pub fn direction(&self) -> DeviceDirection { - self.direction - } pub fn node_name(&self) -> &str { &self.node_name } - pub fn min_quantum(&self) -> FrameCount { - self.min_quantum - } - pub fn max_quantum(&self) -> FrameCount { - self.max_quantum - } pub fn quantum(&self) -> FrameCount { self.quantum } @@ -662,16 +643,12 @@ pub fn init_devices() -> Option> { (_, Role::StreamOutput) => DeviceDirection::Output, (_, Role::StreamInput) => DeviceDirection::Input, }; - let Some(object_id) = props.get(*pw::keys::OBJECT_ID) else { - return; - }; let Some(object_serial) = props .get(*pw::keys::OBJECT_SERIAL) .and_then(|serial| serial.parse().ok()) else { return; }; - let id = info.id(); let node_name = props .get(*pw::keys::NODE_NAME) .unwrap_or("unknown") @@ -711,7 +688,6 @@ pub fn init_devices() -> Option> { let driver = props.get(*pw::keys::FACTORY_NAME).map(|s| s.to_owned()); let device = Device { - id, node_name, nick_name, description, @@ -719,7 +695,6 @@ pub fn init_devices() -> Option> { role, channels, icon_name, - object_id: object_id.to_owned(), object_serial, interface_type, address, From da111e4475d0e1466c6020cb964606a168428805 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Thu, 26 Feb 2026 09:48:48 +0900 Subject: [PATCH 75/85] chore: clippy fix for examples --- examples/beep.rs | 4 ++-- examples/record_wav.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/beep.rs b/examples/beep.rs index 71f5a7600..9080bf5b4 100644 --- a/examples/beep.rs +++ b/examples/beep.rs @@ -48,12 +48,12 @@ fn main() -> anyhow::Result<()> { let mut pulseaudio_host_id = Err(HostUnavailable); #[allow(unused_mut, unused_assignments)] let mut pipewire_host_id = Err(HostUnavailable); - #[cfg(all(any( + #[cfg(any( target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd" - ),))] + ))] { #[cfg(feature = "jack")] { diff --git a/examples/record_wav.rs b/examples/record_wav.rs index 8e1f08682..268addba2 100644 --- a/examples/record_wav.rs +++ b/examples/record_wav.rs @@ -44,12 +44,12 @@ fn main() -> Result<(), anyhow::Error> { let mut pulseaudio_host_id = Err(HostUnavailable); #[allow(unused_mut, unused_assignments)] let mut pipewire_host_id = Err(HostUnavailable); - #[cfg(all(any( + #[cfg(any( target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd" - ),))] + ))] { #[cfg(feature = "jack")] { From 0ba60e9c74e23d5797efe7954802496d21620a1f Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Thu, 26 Feb 2026 09:50:48 +0900 Subject: [PATCH 76/85] chore:fix typos in examples and ci, format in Cargo.toml and add documents --- .github/actions/determine-msrv/action.yml | 2 +- Cargo.toml | 7 ++++++- examples/beep.rs | 2 +- examples/record_wav.rs | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/actions/determine-msrv/action.yml b/.github/actions/determine-msrv/action.yml index 9f7c82373..41449fc87 100644 --- a/.github/actions/determine-msrv/action.yml +++ b/.github/actions/determine-msrv/action.yml @@ -32,7 +32,7 @@ runs: PLATFORM_MSRV="${{ inputs.platform-msrv }}" JACK_MSRV="${{ inputs.jack-msrv }}" PULSEAUDIO_MSRV="${{ inputs.pulseaudio-msrv }}" - PIPEWIRE_MSRV="${{ inputs.pulseaudio-msrv }}" + PIPEWIRE_MSRV="${{ inputs.pipewire-msrv }}" # Use sort -V to find the maximum version VERSIONS="$PLATFORM_MSRV $JACK_MSRV" if [ -n "$PULSEAUDIO_MSRV" ]; then diff --git a/Cargo.toml b/Cargo.toml index 712ec4ea8..017eaa093 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,11 @@ pulseaudio = ["dep:pulseaudio", "dep:futures"] # Platform: Linux, DragonFly BSD, FreeBSD, NetBSD, macOS, Windows # Note: JACK must be installed separately on all platforms jack = ["dep:jack"] + +# PipeWire backend +# Provides audio I/O on Linux and some BSDs via the PipeWire multimedia server +# Requires: PipeWire server and client libraries installed on the system +# Platform: Linux, DragonFly BSD, FreeBSD, NetBSD pipewire = ["dep:pipewire"] # Audio thread priority elevation @@ -104,7 +109,7 @@ audio_thread_priority = { version = "0.34", optional = true } jack = { version = "0.13", optional = true } pulseaudio = { version = "0.3", optional = true } futures = { version = "0.3", optional = true } -pipewire = { version = "0.9", optional = true, features = ["v0_3_49"]} +pipewire = { version = "0.9", optional = true, features = ["v0_3_49"] } [target.'cfg(target_vendor = "apple")'.dependencies] mach2 = "0.5" diff --git a/examples/beep.rs b/examples/beep.rs index 9080bf5b4..74b7d5a36 100644 --- a/examples/beep.rs +++ b/examples/beep.rs @@ -32,7 +32,7 @@ struct Opt { #[arg(long, default_value_t = false)] pulseaudio: bool, - /// Use the pipewire host. Requires `--feature pipewire` + /// Use the Pipewire host. Requires `--features pipewire` #[arg(long, default_value_t = false)] pipewire: bool, } diff --git a/examples/record_wav.rs b/examples/record_wav.rs index 268addba2..02f8530c2 100644 --- a/examples/record_wav.rs +++ b/examples/record_wav.rs @@ -28,7 +28,7 @@ struct Opt { #[arg(long, default_value_t = false)] pulseaudio: bool, - /// Use the pipewire host. Requires `--feature pipewire` + /// Use the Pipewire host. Requires `--features pipewire` #[arg(long, default_value_t = false)] pipewire: bool, } From de85918c9983bbb1346dfb55fe6b26d545e8e8ef Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Thu, 26 Feb 2026 09:58:00 +0900 Subject: [PATCH 77/85] chore: add pipewire information to CHANGELOG and README add documents add CHANGELOG --- CHANGELOG.md | 1 + README.md | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b09200a95..4ebbe6596 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `DeviceBusy` error variant to `SupportedStreamConfigsError`, `DefaultStreamConfigError`, and `BuildStreamError` for retryable device access errors (EBUSY, EAGAIN). - **PulseAudio**: New host for Linux and some BSDs using the PulseAudio API. +- **Pipewire**: New host for Linux and some BSDs using the Pipewire API. ### Changed diff --git a/README.md b/README.md index 63d967b7e..c6a9a7130 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,17 @@ Enables the PulseAudio backend. PulseAudio is a sound server commonly used on Li - **Usage:** See the [beep example](examples/beep.rs) for selecting the PulseAudio host at runtime. +### `pipewire` + +**Platform:** Linux, DragonFly BSD, FreeBSD, NetBSD + +Enables the Pipewire backend. Pipewire is a media server commonly used on Linux desktops. + +**Requirements:** +- Pipewire server and client libraries must be installed on the system +- +**Usage:** See the [beep example](examples/beep.rs) for selecting the Pipewire host at runtime. + ### `wasm-bindgen` **Platform:** WebAssembly (wasm32-unknown-unknown) @@ -198,6 +209,7 @@ If you receive errors about no default input or output device: - **Linux/ALSA:** Ensure your user is in the `audio` group and that ALSA is properly configured - **Linux/PulseAudio:** Check that PulseAudio is running: `pulseaudio --check` +- **Linux/Pipewire:** Check that Pipewire is running: `systemd --user status pipewire` - **Windows:** Verify your audio device is enabled in Sound Settings - **macOS:** Check System Preferences > Sound for available devices - **Mobile (iOS/Android):** Ensure your app has microphone/audio permissions @@ -236,6 +248,8 @@ For platform-specific features, enable the relevant features: ```bash cargo run --example beep --features asio # Windows ASIO cargo run --example beep --features jack # JACK backend +cargo run --example beep --features pulseaudio # PulseAudio backend +cargo run --example beep --features pipewire # Pipewire backend ``` ## Contributing From 042788a6cd14010acc63d1bf484d71c6346a9158 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Sat, 28 Feb 2026 12:09:35 +0900 Subject: [PATCH 78/85] chore: remove all expect In case that something happens to pipewire, we cannot use expect --- src/host/pipewire/device.rs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 9986d80ce..48f658273 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -533,9 +533,15 @@ pub fn init_devices() -> Option> { }) { return; } - let meta_settings: Metadata = registry - .bind(global) - .expect("settings is checked, and should exists"); + let meta_settings: Metadata = match registry.bind(global) { + Ok(meta_settings) => meta_settings, + Err(_) => { + // TODO: do something about this error + // Though it is already checked, but maybe something happened with + // pipewire? + return; + } + }; let settings = settings.clone(); let listener = meta_settings .add_listener_local() @@ -604,9 +610,15 @@ pub fn init_devices() -> Option> { return; } - let node: Node = registry - .bind(global) - .expect("global is checked and should exists"); + let node: Node = match registry.bind(global) { + Ok(node) => node, + Err(_) => { + // TODO: do something about this error + // Though it is already checked, but maybe something happened with + // pipewire? + return; + } + }; let devices = devices.clone(); let listener = node From 28fcfccb7bfbb1d0b76090b95cee37b0032d3ff0 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Sat, 28 Feb 2026 12:13:01 +0900 Subject: [PATCH 79/85] chore: stop stream when input params do not match --- src/host/pipewire/stream.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index 0a19bfb55..9991d4615 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -250,7 +250,7 @@ where let stream = pw::stream::StreamRc::new(core, "cpal-playback", properties)?; let listener = stream .add_local_listener_with_user_data(data) - .param_changed(move|_, user_data, id, param| { + .param_changed(move|stream, user_data, id, param| { let Some(param) = param else { return; }; @@ -279,7 +279,16 @@ where description: format!("channels or rate is not fit, current channels: {current_channels}, current rate: {current_rate}"), }, }); + // if the channels and rate do not match, we stop the stream + if let Err(e) = stream.set_active(false) { + (user_data.error_callback)(StreamError::BackendSpecific { + err: BackendSpecificError { + description: format!("failed to stop the stream, reason: {e}"), + }, + }); + } } + } }) .state_changed(|_stream, user_data, _old, new| { From 65a9e005d0192222fa10a02522e9986b58084c0e Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Sat, 28 Feb 2026 12:13:44 +0900 Subject: [PATCH 80/85] chore: remove useless comment --- src/host/pipewire/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/host/pipewire/mod.rs b/src/host/pipewire/mod.rs index c29c86c54..67fdf0455 100644 --- a/src/host/pipewire/mod.rs +++ b/src/host/pipewire/mod.rs @@ -4,7 +4,6 @@ mod device; mod stream; mod utils; -// just init the pipewire the check if it is available #[inline] fn pipewire_available() -> bool { let dir = std::env::var("PIPEWIRE_RUNTIME_DIR") From 616ee50023605524895f6bc6f61c218f729774f5 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Sat, 28 Feb 2026 12:15:09 +0900 Subject: [PATCH 81/85] chore: remove useless function --- src/host/pipewire/device.rs | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 48f658273..163e690a0 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -52,7 +52,7 @@ pub struct Device { channels: ChannelCount, rate: SampleRate, allow_rates: Vec, - quantum: u32, + quantum: FrameCount, min_quantum: FrameCount, max_quantum: FrameCount, class: Class, @@ -429,22 +429,6 @@ impl DeviceTrait for Device { } } -impl Device { - pub fn node_name(&self) -> &str { - &self.node_name - } - - pub fn quantum(&self) -> FrameCount { - self.quantum - } - pub fn rate(&self) -> SampleRate { - self.rate - } - pub fn allow_rates(&self) -> &[SampleRate] { - &self.allow_rates - } -} - #[derive(Debug, Clone, Default)] struct Settings { rate: SampleRate, From 5ae8f23b32a55a8269e3f72d8f4bb274608c46a6 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Sat, 28 Feb 2026 12:28:52 +0900 Subject: [PATCH 82/85] chore: use timestamp of pipewire --- Cargo.toml | 2 +- src/host/pipewire/stream.rs | 39 ++++++++++++++++++++++++++++++++----- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 017eaa093..624d34d29 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -109,7 +109,7 @@ audio_thread_priority = { version = "0.34", optional = true } jack = { version = "0.13", optional = true } pulseaudio = { version = "0.3", optional = true } futures = { version = "0.3", optional = true } -pipewire = { version = "0.9", optional = true, features = ["v0_3_49"] } +pipewire = { version = "0.9", optional = true, features = ["v0_3_53"] } [target.'cfg(target_vendor = "apple")'.dependencies] mach2 = "0.5" diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index 9991d4615..287dc5a6c 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -148,13 +148,40 @@ where } } } + +/// Returns a hardware timestamp for the current graph cycle, or `None` if +/// the driver has not started yet or the rate is unavailable. +fn pw_stream_time(stream: &pw::stream::Stream) -> Option { + use pw::sys as pw_sys; + use std::mem; + let mut t: pw_sys::pw_time = unsafe { mem::zeroed() }; + let rc = unsafe { + pw_sys::pw_stream_get_time_n( + stream.as_raw_ptr(), + &mut t, + mem::size_of::(), + ) + }; + if rc != 0 || t.now == 0 || t.rate.denom == 0 { + return None; + } + debug_assert_eq!(t.rate.num, 1, "unexpected pw_time rate.num"); + Some(crate::StreamInstant::from_nanos(t.now)) +} + impl UserData where D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, E: FnMut(StreamError) + Send + 'static, { - fn publish_data_in(&mut self, frames: usize, data: &Data) -> Result<(), BackendSpecificError> { - let callback = stream_timestamp_fallback(self.created_instance)?; + fn publish_data_in( + &mut self, + stream: &pw::stream::Stream, + frames: usize, + data: &Data, + ) -> Result<(), BackendSpecificError> { + let callback = + pw_stream_time(stream).unwrap_or(stream_timestamp_fallback(self.created_instance)?); let delay_duration = frames_to_duration(frames, self.format.rate()); let capture = callback .add(delay_duration) @@ -175,10 +202,12 @@ where { fn publish_data_out( &mut self, + stream: &pw::stream::Stream, frames: usize, data: &mut Data, ) -> Result<(), BackendSpecificError> { - let callback = stream_timestamp_fallback(self.created_instance)?; + let callback = + pw_stream_time(stream).unwrap_or(stream_timestamp_fallback(self.created_instance)?); let delay_duration = frames_to_duration(frames, self.format.rate()); let playback = callback .add(delay_duration) @@ -321,7 +350,7 @@ where let data = samples.as_mut_ptr() as *mut (); let mut data = unsafe { Data::from_parts(data, n_samples, user_data.sample_format) }; - if let Err(err) = user_data.publish_data_out(frames, &mut data) { + if let Err(err) = user_data.publish_data_out(stream, frames, &mut data) { (user_data.error_callback)(StreamError::BackendSpecific { err }); } let chunk = buf_data.chunk_mut(); @@ -452,7 +481,7 @@ where let data = samples.as_mut_ptr() as *mut (); let data = unsafe { Data::from_parts(data, n_samples as usize, user_data.sample_format) }; - if let Err(err) = user_data.publish_data_in(frames as usize, &data) { + if let Err(err) = user_data.publish_data_in(stream, frames as usize, &data) { (user_data.error_callback)(StreamError::BackendSpecific { err }); } } From b6093891094f240f94d90c6f0030795054e99fc3 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Sun, 1 Mar 2026 13:22:13 +0900 Subject: [PATCH 83/85] chore: reset samples to "zero" when doing output --- src/host/pipewire/stream.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index 287dc5a6c..10bfe05a3 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -344,6 +344,9 @@ where return; }; + // set buffers to zero + samples.fill(crate::Sample::EQUILIBRIUM); + // samples = frames * channels or samples = data_len / sample_size let n_samples = frames * n_channels as usize; From fe87df3e7a14cbde084f9c5b8db0f2b2c239c922 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Mon, 2 Mar 2026 08:29:17 +0900 Subject: [PATCH 84/85] chore: modify the stream.rs as suggested use the timestamp in pw_src, typos and etc --- src/host/pipewire/stream.rs | 74 ++++++++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 21 deletions(-) diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index 10bfe05a3..a8f151765 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -2,7 +2,7 @@ use std::{thread::JoinHandle, time::Instant}; use crate::{ traits::StreamTrait, BackendSpecificError, InputCallbackInfo, OutputCallbackInfo, SampleFormat, - StreamConfig, StreamError, + StreamConfig, StreamError, StreamInstant, }; use pipewire::{ self as pw, @@ -149,9 +149,19 @@ where } } +/// Hardware timestamp from a PipeWire graph cycle. +struct PwTime { + /// CLOCK_MONOTONIC nanoseconds, stamped at the start of the graph cycle. + now_ns: i64, + /// Pipeline delay converted to nanoseconds. + /// For output: how far ahead of the driver our next sample will be played. + /// For input: how long ago the data in the buffer was captured. + delay_ns: i64, +} + /// Returns a hardware timestamp for the current graph cycle, or `None` if /// the driver has not started yet or the rate is unavailable. -fn pw_stream_time(stream: &pw::stream::Stream) -> Option { +fn pw_stream_time(stream: &pw::stream::Stream) -> Option<(StreamInstant, PwTime)> { use pw::sys as pw_sys; use std::mem; let mut t: pw_sys::pw_time = unsafe { mem::zeroed() }; @@ -166,7 +176,15 @@ fn pw_stream_time(stream: &pw::stream::Stream) -> Option { return None; } debug_assert_eq!(t.rate.num, 1, "unexpected pw_time rate.num"); - Some(crate::StreamInstant::from_nanos(t.now)) + let delay_ns = t.delay * 1_000_000_000i64 / t.rate.denom as i64; + let callback = crate::StreamInstant::from_nanos(t.now); + Some(( + callback, + PwTime { + now_ns: t.now, + delay_ns, + }, + )) } impl UserData @@ -180,15 +198,22 @@ where frames: usize, data: &Data, ) -> Result<(), BackendSpecificError> { - let callback = - pw_stream_time(stream).unwrap_or(stream_timestamp_fallback(self.created_instance)?); - let delay_duration = frames_to_duration(frames, self.format.rate()); - let capture = callback - .add(delay_duration) - .ok_or_else(|| BackendSpecificError { - description: "`playback` occurs beyond representation supported by `StreamInstant`" - .to_string(), - })?; + let (callback, capture) = match pw_stream_time(stream) { + Some((cb, PwTime { now_ns, delay_ns })) => { + (cb, crate::StreamInstant::from_nanos(now_ns - delay_ns)) + } + None => { + let cb = stream_timestamp_fallback(self.created_instance)?; + let pl = cb + .sub(frames_to_duration(frames, self.format.rate())) + .ok_or_else(|| BackendSpecificError { + description: + "`capture` occurs beyond representation supported by `StreamInstant`" + .to_string(), + })?; + (cb, pl) + } + }; let timestamp = crate::InputStreamTimestamp { callback, capture }; let info = crate::InputCallbackInfo { timestamp }; (self.data_callback)(data, &info); @@ -206,15 +231,22 @@ where frames: usize, data: &mut Data, ) -> Result<(), BackendSpecificError> { - let callback = - pw_stream_time(stream).unwrap_or(stream_timestamp_fallback(self.created_instance)?); - let delay_duration = frames_to_duration(frames, self.format.rate()); - let playback = callback - .add(delay_duration) - .ok_or_else(|| BackendSpecificError { - description: "`playback` occurs beyond representation supported by `StreamInstant`" - .to_string(), - })?; + let (callback, playback) = match pw_stream_time(stream) { + Some((cb, PwTime { now_ns, delay_ns })) => { + (cb, crate::StreamInstant::from_nanos(now_ns + delay_ns)) + } + None => { + let cb = stream_timestamp_fallback(self.created_instance)?; + let pl = cb + .add(frames_to_duration(frames, self.format.rate())) + .ok_or_else(|| BackendSpecificError { + description: + "`playback` occurs beyond representation supported by `StreamInstant`" + .to_string(), + })?; + (cb, pl) + } + }; let timestamp = crate::OutputStreamTimestamp { callback, playback }; let info = crate::OutputCallbackInfo { timestamp }; (self.data_callback)(data, &info); From 6838462de55a838a480a8307ea3a9d0e4a164910 Mon Sep 17 00:00:00 2001 From: ShootingStarDragons Date: Mon, 2 Mar 2026 08:31:19 +0900 Subject: [PATCH 85/85] chore: move fill_with_equilibrium to src/host/mod.rs and use it use fill_with_equilibrium() to fill equilibrium in output capture --- src/host/alsa/mod.rs | 60 +++---------------------------------- src/host/mod.rs | 56 ++++++++++++++++++++++++++++++++++ src/host/pipewire/stream.rs | 6 ++-- 3 files changed, 63 insertions(+), 59 deletions(-) diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index 71d738633..ba1989625 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -20,14 +20,15 @@ use self::alsa::poll::Descriptors; pub use self::enumerate::Devices; use crate::{ + host::fill_with_equilibrium, iter::{SupportedInputConfigs, SupportedOutputConfigs}, traits::{DeviceTrait, HostTrait, StreamTrait}, BackendSpecificError, BufferSize, BuildStreamError, ChannelCount, Data, DefaultStreamConfigError, DeviceDescription, DeviceDescriptionBuilder, DeviceDirection, DeviceId, DeviceIdError, DeviceNameError, DevicesError, FrameCount, InputCallbackInfo, - OutputCallbackInfo, PauseStreamError, PlayStreamError, Sample, SampleFormat, SampleRate, - StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, - SupportedStreamConfigRange, SupportedStreamConfigsError, I24, U24, + OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, + StreamError, SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, + SupportedStreamConfigsError, }; mod enumerate; @@ -1239,59 +1240,6 @@ fn hw_params_buffer_size_min_max(hw_params: &alsa::pcm::HwParams) -> (FrameCount (min_buf, max_buf) } -// Fill a buffer with equilibrium values for any sample format. -// Works with any buffer size, even if not perfectly aligned to sample boundaries. -fn fill_with_equilibrium(buffer: &mut [u8], sample_format: SampleFormat) { - macro_rules! fill_typed { - ($sample_type:ty) => {{ - let sample_size = std::mem::size_of::<$sample_type>(); - - assert_eq!( - buffer.len() % sample_size, - 0, - "Buffer size must be aligned to sample size for format {:?}", - sample_format - ); - - let num_samples = buffer.len() / sample_size; - let equilibrium = <$sample_type as Sample>::EQUILIBRIUM; - - // Safety: We verified the buffer size is correctly aligned for the sample type - let samples = unsafe { - std::slice::from_raw_parts_mut( - buffer.as_mut_ptr() as *mut $sample_type, - num_samples, - ) - }; - - for sample in samples { - *sample = equilibrium; - } - }}; - } - const DSD_SILENCE_BYTE: u8 = 0x69; - - match sample_format { - SampleFormat::I8 => fill_typed!(i8), - SampleFormat::I16 => fill_typed!(i16), - SampleFormat::I24 => fill_typed!(I24), - SampleFormat::I32 => fill_typed!(i32), - // SampleFormat::I48 => fill_typed!(I48), - SampleFormat::I64 => fill_typed!(i64), - SampleFormat::U8 => fill_typed!(u8), - SampleFormat::U16 => fill_typed!(u16), - SampleFormat::U24 => fill_typed!(U24), - SampleFormat::U32 => fill_typed!(u32), - // SampleFormat::U48 => fill_typed!(U48), - SampleFormat::U64 => fill_typed!(u64), - SampleFormat::F32 => fill_typed!(f32), - SampleFormat::F64 => fill_typed!(f64), - SampleFormat::DsdU8 | SampleFormat::DsdU16 | SampleFormat::DsdU32 => { - buffer.fill(DSD_SILENCE_BYTE) - } - } -} - fn init_hw_params<'a>( pcm_handle: &'a alsa::pcm::PCM, config: &StreamConfig, diff --git a/src/host/mod.rs b/src/host/mod.rs index 7801b7d41..39100722d 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -1,3 +1,5 @@ +use crate::{Sample, SampleFormat, I24, U24}; + #[cfg(target_os = "android")] pub(crate) mod aaudio; #[cfg(any( @@ -71,3 +73,57 @@ pub(crate) mod custom; all(target_arch = "wasm32", feature = "wasm-bindgen"), )))] pub(crate) mod null; + +// Fill a buffer with equilibrium values for any sample format. +// Works with any buffer size, even if not perfectly aligned to sample boundaries. +#[allow(unused)] +pub(crate) fn fill_with_equilibrium(buffer: &mut [u8], sample_format: SampleFormat) { + macro_rules! fill_typed { + ($sample_type:ty) => {{ + let sample_size = std::mem::size_of::<$sample_type>(); + + assert_eq!( + buffer.len() % sample_size, + 0, + "Buffer size must be aligned to sample size for format {:?}", + sample_format + ); + + let num_samples = buffer.len() / sample_size; + let equilibrium = <$sample_type as Sample>::EQUILIBRIUM; + + // Safety: We verified the buffer size is correctly aligned for the sample type + let samples = unsafe { + std::slice::from_raw_parts_mut( + buffer.as_mut_ptr() as *mut $sample_type, + num_samples, + ) + }; + + for sample in samples { + *sample = equilibrium; + } + }}; + } + const DSD_SILENCE_BYTE: u8 = 0x69; + + match sample_format { + SampleFormat::I8 => fill_typed!(i8), + SampleFormat::I16 => fill_typed!(i16), + SampleFormat::I24 => fill_typed!(I24), + SampleFormat::I32 => fill_typed!(i32), + // SampleFormat::I48 => fill_typed!(I48), + SampleFormat::I64 => fill_typed!(i64), + SampleFormat::U8 => fill_typed!(u8), + SampleFormat::U16 => fill_typed!(u16), + SampleFormat::U24 => fill_typed!(U24), + SampleFormat::U32 => fill_typed!(u32), + // SampleFormat::U48 => fill_typed!(U48), + SampleFormat::U64 => fill_typed!(u64), + SampleFormat::F32 => fill_typed!(f32), + SampleFormat::F64 => fill_typed!(f64), + SampleFormat::DsdU8 | SampleFormat::DsdU16 | SampleFormat::DsdU32 => { + buffer.fill(DSD_SILENCE_BYTE) + } + } +} diff --git a/src/host/pipewire/stream.rs b/src/host/pipewire/stream.rs index a8f151765..8def5bd2f 100644 --- a/src/host/pipewire/stream.rs +++ b/src/host/pipewire/stream.rs @@ -1,8 +1,8 @@ use std::{thread::JoinHandle, time::Instant}; use crate::{ - traits::StreamTrait, BackendSpecificError, InputCallbackInfo, OutputCallbackInfo, SampleFormat, - StreamConfig, StreamError, StreamInstant, + host::fill_with_equilibrium, traits::StreamTrait, BackendSpecificError, InputCallbackInfo, + OutputCallbackInfo, SampleFormat, StreamConfig, StreamError, StreamInstant, }; use pipewire::{ self as pw, @@ -377,7 +377,7 @@ where }; // set buffers to zero - samples.fill(crate::Sample::EQUILIBRIUM); + fill_with_equilibrium(samples, user_data.sample_format); // samples = frames * channels or samples = data_len / sample_size let n_samples = frames * n_channels as usize;