From 9c0298e1eba00f1878579806b5b0dc60a6386175 Mon Sep 17 00:00:00 2001 From: azam Date: Mon, 23 Jun 2025 08:12:45 +0900 Subject: [PATCH 1/2] Implement pipewire output --- Cargo.lock | 161 +++++++++- psst-cli/Cargo.toml | 3 +- psst-core/Cargo.toml | 6 + psst-core/src/audio/output/mod.rs | 4 + psst-core/src/audio/output/pipewire.rs | 416 +++++++++++++++++++++++++ psst-gui/Cargo.toml | 3 +- 6 files changed, 588 insertions(+), 5 deletions(-) create mode 100644 psst-core/src/audio/output/pipewire.rs diff --git a/Cargo.lock b/Cargo.lock index 866b7f3b..854aa628 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,6 +86,16 @@ dependencies = [ "libc", ] +[[package]] +name = "annotate-snippets" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccaf7e9dfbb6ab22c82e473cd1a8a7bd313c19a5b7e40970f3d89ef5a5c9e81e" +dependencies = [ + "unicode-width", + "yansi-term", +] + [[package]] name = "anstream" version = "0.6.18" @@ -453,6 +463,27 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "annotate-snippets", + "bitflags 2.9.0", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.101", +] + [[package]] name = "bindgen" version = "0.70.1" @@ -785,6 +816,15 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f8a2ca5ac02d09563609681103aada9e1777d54fc57a5acd7a41404f9c93b6e" +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cookie" version = "0.14.4" @@ -807,6 +847,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "cookie-factory" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" +dependencies = [ + "futures", +] + [[package]] name = "cookie_store" version = "0.12.0" @@ -910,7 +959,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ce857aa0b77d77287acc1ac3e37a05a8c95a2af3647d23b15f263bdaeb7562b" dependencies = [ - "bindgen", + "bindgen 0.70.1", ] [[package]] @@ -1512,6 +1561,21 @@ dependencies = [ "new_debug_unreachable", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -1519,6 +1583,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -1601,6 +1666,7 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -2332,7 +2398,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.10", + "socket2 0.5.9", "tokio", "tower-service", "tracing", @@ -2854,6 +2920,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "lebe" version = "0.5.2" @@ -2914,6 +2986,34 @@ dependencies = [ "libc", ] +[[package]] +name = "libspa" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65f3a4b81b2a2d8c7f300643676202debd1b7c929dbf5c9bb89402ea11d19810" +dependencies = [ + "bitflags 2.9.0", + "cc", + "convert_case", + "cookie-factory", + "libc", + "libspa-sys", + "nix 0.27.1", + "nom", + "system-deps", +] + +[[package]] +name = "libspa-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf0d9716420364790e85cbb9d3ac2c950bde16a7dd36f3209b7dfdfc4a24d01f" +dependencies = [ + "bindgen 0.69.5", + "cc", + "system-deps", +] + [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -3168,6 +3268,17 @@ dependencies = [ "memoffset 0.7.1", ] +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -3639,6 +3750,34 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pipewire" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08e645ba5c45109106d56610b3ee60eb13a6f2beb8b74f8dc8186cf261788dda" +dependencies = [ + "anyhow", + "bitflags 2.9.0", + "libc", + "libspa", + "libspa-sys", + "nix 0.27.1", + "once_cell", + "pipewire-sys", + "thiserror 1.0.69", +] + +[[package]] +name = "pipewire-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "849e188f90b1dda88fe2bfe1ad31fe5f158af2c98f80fb5d13726c44f3f01112" +dependencies = [ + "bindgen 0.69.5", + "libspa-sys", + "system-deps", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -3857,6 +3996,7 @@ dependencies = [ "oauth2", "once_cell", "parking_lot", + "pipewire", "psst-protocol", "quick-protobuf", "rand 0.9.1", @@ -5543,6 +5683,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "untrusted" version = "0.7.1" @@ -6339,6 +6485,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a67300977d3dc3f8034dae89778f502b6ba20b269527b3223ba59c0cf393bb8a" +[[package]] +name = "yansi-term" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5c30ade05e61656247b2e334a031dfd0cc466fadef865bdcdea8d537951bf1" +dependencies = [ + "winapi", +] + [[package]] name = "yoke" version = "0.7.5" @@ -6387,7 +6542,7 @@ dependencies = [ "futures-sink", "futures-util", "hex", - "nix", + "nix 0.26.4", "once_cell", "ordered-stream", "rand 0.8.5", diff --git a/psst-cli/Cargo.toml b/psst-cli/Cargo.toml index 8e759ca0..73304258 100644 --- a/psst-cli/Cargo.toml +++ b/psst-cli/Cargo.toml @@ -8,9 +8,10 @@ edition = "2021" default = ["cpal"] cpal = ["psst-core/cpal"] cubeb = ["psst-core/cubeb"] +pipewire = ["psst-core/pipewire"] [dependencies] -psst-core = { path = "../psst-core" } +psst-core = { path = "../psst-core", default-features = false } env_logger = "0.11.5" log = "0.4.22" diff --git a/psst-core/Cargo.toml b/psst-core/Cargo.toml index 8f2fd440..0df1dd0b 100644 --- a/psst-core/Cargo.toml +++ b/psst-core/Cargo.toml @@ -4,6 +4,11 @@ version = "0.1.0" authors = ["Jan Pochyla "] edition = "2021" +[features] +default = ["cpal"] +cpal = ["dep:cpal"] +cubeb = ["dep:cubeb"] +pipewire = ["dep:pipewire"] [build-dependencies] gix-config = "0.45.1" @@ -44,6 +49,7 @@ shannon = { version = "0.2.0" } audio_thread_priority = "0.33.0" cpal = { version = "0.15.3", optional = true } cubeb = { git = "https://github.com/mozilla/cubeb-rs", optional = true } +pipewire = { version = "0.8.0", optional = true } libsamplerate = { version = "0.1.0" } rb = { version = "0.4.1" } symphonia = { version = "0.5.4", default-features = false, features = [ diff --git a/psst-core/src/audio/output/mod.rs b/psst-core/src/audio/output/mod.rs index 09d29f89..3d5520a0 100644 --- a/psst-core/src/audio/output/mod.rs +++ b/psst-core/src/audio/output/mod.rs @@ -4,11 +4,15 @@ use crate::audio::source::AudioSource; pub mod cpal; #[cfg(feature = "cubeb")] pub mod cubeb; +#[cfg(feature = "pipewire")] +pub mod pipewire; #[cfg(feature = "cubeb")] pub type DefaultAudioOutput = cubeb::CubebOutput; #[cfg(feature = "cpal")] pub type DefaultAudioOutput = cpal::CpalOutput; +#[cfg(feature = "pipewire")] +pub type DefaultAudioOutput = pipewire::PipewireOutput; pub type DefaultAudioSink = ::Sink; diff --git a/psst-core/src/audio/output/pipewire.rs b/psst-core/src/audio/output/pipewire.rs new file mode 100644 index 00000000..b98650be --- /dev/null +++ b/psst-core/src/audio/output/pipewire.rs @@ -0,0 +1,416 @@ +use std::{ + io::Cursor, + sync::{Arc, RwLock}, + thread::JoinHandle, +}; + +use log::{debug, error, info}; +use pipewire::{ + context::Context, + keys::{ + APP_ICON_NAME, APP_ID, APP_NAME, AUDIO_CHANNELS, MEDIA_CATEGORY, MEDIA_NAME, MEDIA_ROLE, + MEDIA_TYPE, NODE_NAME, + }, + main_loop::MainLoop, + properties::properties, + spa::{ + param::audio::{AudioFormat, AudioInfoRaw, MAX_CHANNELS}, + pod::{ + serialize::{GenError, PodSerializer}, + Object, Pod, Value, + }, + sys::{ + SPA_PARAM_EnumFormat, SPA_PROP_channelVolumes, SPA_TYPE_OBJECT_Format, + SPA_AUDIO_CHANNEL_FL, SPA_AUDIO_CHANNEL_FR, + }, + utils::Direction, + }, + stream::{Stream, StreamFlags, StreamState}, + sys::pw_stream_control, +}; +use symphonia::core::audio; + +use crate::{ + audio::{ + output::{AudioOutput, AudioSink}, + source::{AudioSource, Empty}, + }, + error::Error, +}; + +const DEFAULT_CHANNEL_COUNT: usize = 2; +const DEFAULT_SAMPLE_RATE: u32 = 44_100; + +enum PipewireMsg { + Open, + Close, + Play(Box), + Pause, + Resume, + Stop, + SetVolume(f32), + SetMediaTitle(String), +} + +pub struct PipewireOutput { + mainloop_handle: JoinHandle>, + sink: PipewireSink, +} + +impl PipewireOutput { + pub fn open() -> Result { + info!("opening audio output: pipewire"); + pipewire::init(); + let (mainloop_send, mainloop_recv) = pipewire::channel::channel::(); + let mainloop_handle = std::thread::spawn(move || Self::run(mainloop_recv)); + let sink = PipewireSink { + channel_count: DEFAULT_CHANNEL_COUNT, + sample_rate: DEFAULT_SAMPLE_RATE, + mainloop_send, + }; + Ok(Self { + mainloop_handle, + sink, + }) + } + + fn run(mainloop_recv: pipewire::channel::Receiver) -> Result<(), Error> { + let audio_source = Arc::new(RwLock::new(Box::new(Empty) as Box)); + let audio_is_playing = Arc::new(RwLock::new(false)); + let audio_volume = Arc::new(RwLock::new(0f32)); + let mainloop = MainLoop::new(None)?; + let context = Context::new(&mainloop)?; + let core = context.connect(Some(properties! { + *APP_NAME => "Psst", + *APP_ID => "music.player.psst", + *APP_ICON_NAME => "Psst" + }))?; + let registry = core.get_registry()?; + + let stream = Stream::new( + &core, + "psst", + properties! { + *MEDIA_TYPE => "Audio", + *MEDIA_CATEGORY => "Playback", + *MEDIA_ROLE => "Music", + // *MEDIA_NAME => "artist - title", + *AUDIO_CHANNELS => "2", + *NODE_NAME => "Psst", + *APP_NAME => "Psst", + *APP_ID => "music.player.psst", + *APP_ICON_NAME => "Psst", + }, + )?; + + // let _core_listener = core + // .add_listener_local() + // .info(|_| {}) + // .done({ + // let mainloop = mainloop.clone(); + // move |id, seq| { + // info!("Core sync done for ID: {} seq: {}", id, seq.seq()); + // if id == PW_ID_CORE { + // mainloop.quit(); + // } + // } + // }) + // .register(); + + let listener = stream + .add_local_listener::<()>() + .state_changed({ + move |_stream, _userdata, _old, new| { + debug!("State changed: {_old:?} -> {new:?}"); + match new { + StreamState::Error(x) => { + error!("stream error: {x}"); + } + StreamState::Unconnected => { + debug!("stream unconnected"); + } + StreamState::Connecting => { + debug!("stream connecting"); + } + _ => {} + } + } + }) + .control_info({ + let audio_volume = audio_volume.clone(); + move |_stream, _userdata, id, control_ptr: *const pw_stream_control| { + debug!("control info: id: {id}"); + if id == SPA_PROP_channelVolumes { + debug!("volume set from pipewire control: {:?}", control_ptr); + // TODO: Call player controller to update volume on AppState + unsafe { + let control = *control_ptr; + if control.n_values > 0 { + let values = std::slice::from_raw_parts( + control.values, + control.n_values as usize, + ); + // Ideally we could set volume per channel, but here we are overwriting with the first channel + let mut volume = + audio_volume.write().expect("failed to lock audio_volume"); + for v in values.iter() { + if *v != volume.clone() { + *volume = *v; + break; + } + } + } + } + } + } + }) + .process({ + let audio_source = audio_source.clone(); + let audio_is_playing = audio_is_playing.clone(); + let audio_volume = audio_volume.clone(); + move |stream_ref, _| { + let is_playing = audio_is_playing + .read() + .expect("failed to lock audio_is_playing") + .clone(); + let volume = audio_volume + .read() + .expect("failed to lock audio_volume") + .clone(); + // Why not two channels? + // let stride = size_of::() * DEFAULT_CHANNEL_COUNT; + let stride = size_of::() * 1; + while let Some(mut buffer) = stream_ref.dequeue_buffer() { + for data in buffer.datas_mut() { + let mut written = 0; + + if let Some(d) = data.data() { + let n_samples = d.len() / stride; + let ptr = d.as_mut_ptr() as *mut f32; + let slice = + unsafe { std::slice::from_raw_parts_mut(ptr, n_samples) }; + if is_playing { + written = audio_source + .write() + .expect("failed to lock audio source") + .write(slice); + + // Let pipewire handle the volume scaling. + // let scaled_volume = volume.powf(4.0); + // slice[..written] + // .iter_mut() + // .for_each(|s| *s *= scaled_volume); + + // Mute any remaining samples. + slice[written..].iter_mut().for_each(|s| *s = 0.0); + } + } else { + error!("Buffer data is null or not writable"); + continue; + } + + let chunk = data.chunk_mut(); + *chunk.offset_mut() = 0; + *chunk.stride_mut() = stride as _; + *chunk.size_mut() = (stride * written) as _; + } + } + } + }) + .register()?; + + core.sync(0)?; + + let mut positions = [0; MAX_CHANNELS]; + positions[0] = SPA_AUDIO_CHANNEL_FL; + positions[1] = SPA_AUDIO_CHANNEL_FR; + + let mut audio_info = AudioInfoRaw::new(); + audio_info.set_rate(DEFAULT_SAMPLE_RATE); + audio_info.set_format(AudioFormat::F32LE); + audio_info.set_channels(DEFAULT_CHANNEL_COUNT as u32); + audio_info.set_position(positions); + + let pod_raw = PodSerializer::serialize( + Cursor::new(Vec::new()), + &Value::Object(Object { + type_: SPA_TYPE_OBJECT_Format, + id: SPA_PARAM_EnumFormat, + properties: audio_info.into(), + }), + ) + .map(|data| data.0.into_inner())?; + + let mut params = [Pod::from_bytes(&pod_raw).expect("failed to create pod")]; + + stream.connect( + Direction::Output, + None, + StreamFlags::AUTOCONNECT | StreamFlags::RT_PROCESS | StreamFlags::MAP_BUFFERS, + &mut params, + )?; + + let _receiver = mainloop_recv.attach(mainloop.as_ref(), { + let mainloop = mainloop.clone(); + let audio_source = audio_source.clone(); + let is_playing = audio_is_playing.clone(); + let audio_volume = audio_volume.clone(); + move |msg| { + match msg { + PipewireMsg::Open => { + debug!("PipewireMsg::Open"); + stream.set_active(true).expect("failed to activate stream"); + } + PipewireMsg::Close => { + debug!("PipewireMsg::Close"); + let mut new_is_playing = + is_playing.write().expect("failed to lock is_playing"); + *new_is_playing = false; + stream + .set_active(false) + .expect("failed to deactivate stream"); + mainloop.quit(); + } + PipewireMsg::Play(source) => { + debug!("PipewireMsg::Play"); + debug!( + "PipewireMsg::Play: channel_count: {:?}", + source.channel_count() + ); + debug!("PipewireMsg::Play: sample_rate: {:?}", source.sample_rate()); + + let mut new_source = + audio_source.write().expect("failed to lock audio source"); + *new_source = source; + let mut new_is_playing = + is_playing.write().expect("failed to lock is_playing"); + *new_is_playing = true; + stream.set_active(true).expect("failed to activate stream"); + } + PipewireMsg::Pause => { + debug!("PipewireMsg::Pause"); + let mut new_is_playing = + is_playing.write().expect("failed to lock is_playing"); + *new_is_playing = false; + stream + .set_active(false) + .expect("failed to deactivate stream"); + } + PipewireMsg::Resume => { + debug!("PipewireMsg::Resume"); + let mut new_is_playing = + is_playing.write().expect("failed to lock is_playing"); + *new_is_playing = true; + stream.set_active(true).expect("failed to activate stream"); + } + PipewireMsg::Stop => { + debug!("PipewireMsg::Stop"); + let mut new_is_playing = + is_playing.write().expect("failed to lock is_playing"); + *new_is_playing = false; + stream + .set_active(false) + .expect("failed to deactivate stream"); + } + PipewireMsg::SetVolume(volume) => { + debug!("PipewireMsg::SetVolume: {}", volume); + let values = [volume]; + stream + .set_control(SPA_PROP_channelVolumes, &values) + .expect("failed to set volume"); + let mut new_audio_volume = + audio_volume.write().expect("failed to lock volume"); + *new_audio_volume = volume; + } + PipewireMsg::SetMediaTitle(title) => { + debug!("PipewireMsg::SetMediaTitle: {}", title); + let props = properties! { + *MEDIA_NAME => title.clone(), + }; + unsafe { + pipewire::sys::pw_stream_update_properties( + stream.as_raw_ptr(), + props.dict().as_raw_ptr(), + ); + } + } + }; + } + }); + + info!("mainloop starting"); + mainloop.run(); + info!("mainloop stopped"); + + Ok(()) + } +} + +impl AudioOutput for PipewireOutput { + type Sink = PipewireSink; + + fn sink(&self) -> Self::Sink { + self.sink.clone() + } +} + +#[derive(Clone)] +pub struct PipewireSink { + channel_count: usize, + sample_rate: u32, + mainloop_send: pipewire::channel::Sender, +} + +impl PipewireSink { + fn send(&self, msg: PipewireMsg) { + if self.mainloop_send.send(msg).is_err() { + error!("output stream actor is dead"); + } + } +} + +impl AudioSink for PipewireSink { + fn channel_count(&self) -> usize { + self.channel_count + } + + fn sample_rate(&self) -> u32 { + self.sample_rate + } + + fn set_volume(&self, volume: f32) { + self.send(PipewireMsg::SetVolume(volume)); + } + + fn play(&self, source: impl AudioSource) { + self.send(PipewireMsg::Play(Box::new(source))); + } + + fn pause(&self) { + self.send(PipewireMsg::Pause); + } + + fn resume(&self) { + self.send(PipewireMsg::Resume); + } + + fn stop(&self) { + self.send(PipewireMsg::Stop); + } + + fn close(&self) { + self.send(PipewireMsg::Close); + } +} + +impl From for Error { + fn from(err: GenError) -> Error { + Error::AudioOutputError(Box::new(err)) + } +} + +impl From for Error { + fn from(err: pipewire::Error) -> Error { + Error::AudioOutputError(Box::new(err)) + } +} diff --git a/psst-gui/Cargo.toml b/psst-gui/Cargo.toml index 769d5572..ebe6f16f 100644 --- a/psst-gui/Cargo.toml +++ b/psst-gui/Cargo.toml @@ -11,9 +11,10 @@ repository = "https://github.com/jpochyla/psst" default = ["cpal"] cpal = ["psst-core/cpal"] cubeb = ["psst-core/cubeb"] +pipewire = ["psst-core/pipewire"] [dependencies] -psst-core = { path = "../psst-core" } +psst-core = { path = "../psst-core", default-features = false } # Common crossbeam-channel = { version = "0.5.15" } From 9694987061440b4bc709ab4c433cbffb50e733e9 Mon Sep 17 00:00:00 2001 From: azam Date: Tue, 24 Jun 2025 00:53:53 +0900 Subject: [PATCH 2/2] Cleanup --- psst-core/src/audio/output/pipewire.rs | 66 ++++++-------------------- 1 file changed, 14 insertions(+), 52 deletions(-) diff --git a/psst-core/src/audio/output/pipewire.rs b/psst-core/src/audio/output/pipewire.rs index b98650be..0a10e4b7 100644 --- a/psst-core/src/audio/output/pipewire.rs +++ b/psst-core/src/audio/output/pipewire.rs @@ -1,15 +1,11 @@ -use std::{ - io::Cursor, - sync::{Arc, RwLock}, - thread::JoinHandle, -}; +use std::{io::Cursor, rc::Rc, sync::RwLock, thread::JoinHandle}; use log::{debug, error, info}; use pipewire::{ context::Context, keys::{ APP_ICON_NAME, APP_ID, APP_NAME, AUDIO_CHANNELS, MEDIA_CATEGORY, MEDIA_NAME, MEDIA_ROLE, - MEDIA_TYPE, NODE_NAME, + MEDIA_TYPE, }, main_loop::MainLoop, properties::properties, @@ -28,7 +24,6 @@ use pipewire::{ stream::{Stream, StreamFlags, StreamState}, sys::pw_stream_control, }; -use symphonia::core::audio; use crate::{ audio::{ @@ -53,7 +48,7 @@ enum PipewireMsg { } pub struct PipewireOutput { - mainloop_handle: JoinHandle>, + _mainloop_handle: JoinHandle>, sink: PipewireSink, } @@ -62,22 +57,22 @@ impl PipewireOutput { info!("opening audio output: pipewire"); pipewire::init(); let (mainloop_send, mainloop_recv) = pipewire::channel::channel::(); - let mainloop_handle = std::thread::spawn(move || Self::run(mainloop_recv)); + let _mainloop_handle = std::thread::spawn(move || Self::run(mainloop_recv)); let sink = PipewireSink { channel_count: DEFAULT_CHANNEL_COUNT, sample_rate: DEFAULT_SAMPLE_RATE, mainloop_send, }; Ok(Self { - mainloop_handle, + _mainloop_handle, sink, }) } fn run(mainloop_recv: pipewire::channel::Receiver) -> Result<(), Error> { - let audio_source = Arc::new(RwLock::new(Box::new(Empty) as Box)); - let audio_is_playing = Arc::new(RwLock::new(false)); - let audio_volume = Arc::new(RwLock::new(0f32)); + let audio_source = Rc::new(RwLock::new(Box::new(Empty) as Box)); + let audio_is_playing = Rc::new(RwLock::new(false)); + let audio_volume = Rc::new(RwLock::new(0f32)); let mainloop = MainLoop::new(None)?; let context = Context::new(&mainloop)?; let core = context.connect(Some(properties! { @@ -85,7 +80,6 @@ impl PipewireOutput { *APP_ID => "music.player.psst", *APP_ICON_NAME => "Psst" }))?; - let registry = core.get_registry()?; let stream = Stream::new( &core, @@ -94,34 +88,15 @@ impl PipewireOutput { *MEDIA_TYPE => "Audio", *MEDIA_CATEGORY => "Playback", *MEDIA_ROLE => "Music", - // *MEDIA_NAME => "artist - title", *AUDIO_CHANNELS => "2", - *NODE_NAME => "Psst", - *APP_NAME => "Psst", - *APP_ID => "music.player.psst", - *APP_ICON_NAME => "Psst", }, )?; - // let _core_listener = core - // .add_listener_local() - // .info(|_| {}) - // .done({ - // let mainloop = mainloop.clone(); - // move |id, seq| { - // info!("Core sync done for ID: {} seq: {}", id, seq.seq()); - // if id == PW_ID_CORE { - // mainloop.quit(); - // } - // } - // }) - // .register(); - - let listener = stream + let _stream_listener = stream .add_local_listener::<()>() .state_changed({ move |_stream, _userdata, _old, new| { - debug!("State changed: {_old:?} -> {new:?}"); + debug!("stream state changed: {_old:?} -> {new:?}"); match new { StreamState::Error(x) => { error!("stream error: {x}"); @@ -167,17 +142,12 @@ impl PipewireOutput { .process({ let audio_source = audio_source.clone(); let audio_is_playing = audio_is_playing.clone(); - let audio_volume = audio_volume.clone(); move |stream_ref, _| { let is_playing = audio_is_playing .read() .expect("failed to lock audio_is_playing") .clone(); - let volume = audio_volume - .read() - .expect("failed to lock audio_volume") - .clone(); - // Why not two channels? + // Why not is this not needing two channels? // let stride = size_of::() * DEFAULT_CHANNEL_COUNT; let stride = size_of::() * 1; while let Some(mut buffer) = stream_ref.dequeue_buffer() { @@ -196,10 +166,6 @@ impl PipewireOutput { .write(slice); // Let pipewire handle the volume scaling. - // let scaled_volume = volume.powf(4.0); - // slice[..written] - // .iter_mut() - // .for_each(|s| *s *= scaled_volume); // Mute any remaining samples. slice[written..].iter_mut().for_each(|s| *s = 0.0); @@ -219,8 +185,6 @@ impl PipewireOutput { }) .register()?; - core.sync(0)?; - let mut positions = [0; MAX_CHANNELS]; positions[0] = SPA_AUDIO_CHANNEL_FL; positions[1] = SPA_AUDIO_CHANNEL_FR; @@ -272,13 +236,11 @@ impl PipewireOutput { mainloop.quit(); } PipewireMsg::Play(source) => { - debug!("PipewireMsg::Play"); debug!( - "PipewireMsg::Play: channel_count: {:?}", - source.channel_count() + "PipewireMsg::Play channel_count: {:?} sample_rate: {:?}", + source.channel_count(), + source.sample_rate() ); - debug!("PipewireMsg::Play: sample_rate: {:?}", source.sample_rate()); - let mut new_source = audio_source.write().expect("failed to lock audio source"); *new_source = source;