From 15ae48210535358ad90dc85c4bc8809a71627d1d Mon Sep 17 00:00:00 2001 From: Andreas Grois Date: Fri, 4 Jun 2021 21:48:01 +0200 Subject: Pulse now displays volume correctly. Still missing actual functionality beyond a numeric display, and error cases have not been tested, but hey, it's there! --- pulse/src/runnable/mod.rs | 192 +++++++++++++++++------------ pulse/src/runnable/pulse.rs | 290 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 395 insertions(+), 87 deletions(-) (limited to 'pulse/src') diff --git a/pulse/src/runnable/mod.rs b/pulse/src/runnable/mod.rs index 7286c31..352a10a 100644 --- a/pulse/src/runnable/mod.rs +++ b/pulse/src/runnable/mod.rs @@ -31,103 +31,135 @@ impl<'p : 's, 's> PulseVolumeRunnable<'p> { self.to_main.send_update(Err(PluginError::PrintToStdErr(err.to_string()))).expect("Tried to tell main thread that an error occured. Main thread isn't listening any more."); } + fn send_updated_volume_to_main(&self, volume : &pulse::Volume) -> Result<(),PluginCommunicationError> { + self.to_main.send_update(Ok(self.format_volume(volume))) + } + + fn format_volume(&self, volume : &pulse::Volume) -> String { + format!("{}",volume.volume) + } } impl<'p> SwayStatusModuleRunnable for PulseVolumeRunnable<'p> { fn run(&self) { - let pulse = match &self.pulse { - Err(x) => { - self.send_error_to_main(x); - return; - } - Ok(x) => x - }; - let context = match PulseContext::create(pulse) { - Err(x) => { - self.send_error_to_main(x); - return; - } - Ok(x) => x - }; - let mut context_state = PaContextState::Unconnected; - let mut curr_default_sink = None; - let mut curr_volume = None; - let mut sink_we_care_about = match self.config.sink { - crate::config::Sink::Default => { None } - crate::config::Sink::Specific { sink_name } => { - Some(match SinkHandle::try_from(&*sink_name) { - Ok(x) => {x} - Err(e) => { - self.send_error_to_main(e); - return; - } - }) - } - }; - loop { - match context_state { - PaContextState::Unconnected => { - if let crate::config::Sink::Default = self.config.sink { - sink_we_care_about = None; - } - context.connect(); + 'outer : loop { + let pulse = match &self.pulse { + Err(x) => { + self.send_error_to_main(x); + return; + } + Ok(x) => x + }; + let mut scratch = pulse::ContextScratch::default(); + let mut context = match PulseContext::create(pulse, &mut scratch) { + Err(x) => { + self.send_error_to_main(x); + return; + } + Ok(x) => x + }; + let mut curr_default_sink = None; + let mut curr_volume = None; + let mut sink_we_care_about = match &self.config.sink { + crate::config::Sink::Default => { None } + crate::config::Sink::Specific { sink_name } => { + Some(match SinkHandle::try_from(sink_name as &str) { + Ok(x) => {x} + Err(e) => { + self.send_error_to_main(e); + return; + } + }) } - PaContextState::Failed | PaContextState::Terminated => { - match self.from_main.recv_timeout(std::time::Duration::from_secs(1)) { - Ok(x) => { - if let MessagesFromMain::Quit = x { - break; + }; + loop { + match context.get_state() { + PaContextState::Unconnected => { + if let crate::config::Sink::Default = &self.config.sink { + sink_we_care_about = None; + } + if let Err(e) = context.connect_and_set_callbacks() { + self.send_error_to_main(e); + match self.from_main.recv_timeout(std::time::Duration::from_secs(1)) { + Ok(x) => { + if let MessagesFromMain::Quit = x { + break 'outer; + } + } + Err(e) => { + if let RecvTimeoutError::Disconnected = e { + break 'outer; + } + } } } - Err(e) => { - if let RecvTimeoutError::Disconnected = e { - break; + } + PaContextState::Failed | PaContextState::Terminated => { + //context is dead. Wait a second, and start over. + self.to_main.send_update(Err(PluginError::ShowInsteadOfText(String::from("Context died")))).expect("Tried to tell main thread that pulse context died. Main thread isn't listening."); + self.to_main.send_update(Err(PluginError::PrintToStdErr(String::from("Pulseaudio context entered either the Terminated or Failed state. Creating a new context and retrying")))).expect("Tried to tell main thread that pulse context died. Main thread isn't listening."); + match self.from_main.recv_timeout(std::time::Duration::from_secs(1)) { + Ok(x) => { + if let MessagesFromMain::Quit = x { + break 'outer; + } + } + Err(e) => { + if let RecvTimeoutError::Disconnected = e { + break 'outer; + } } } + continue 'outer; } - if let crate::config::Sink::Default = self.config.sink { - sink_we_care_about = None; + PaContextState::Ready => { + if sink_we_care_about.is_none() { + //this may trigger several redundant refreshes, but it _should_ only happen + //during startup, so we don't really care. + context.refresh_default_sink(); + } } - context.connect(); + _ => {} + } + + let iteration_result = context.iterate(&sink_we_care_about); + if let Err(e) = iteration_result { + self.send_error_to_main(e); + break 'outer; } - PaContextState::Ready => { - if sink_we_care_about.is_none() { - //this may trigger several redundant refreshes, but it _should_ only happen - //during startup, so we don't really care. - context.refresh_default_sink(); + + let pulse::IterationResult { default_sink, volume } = iteration_result.unwrap(); + if default_sink.is_some() && default_sink != curr_default_sink { + curr_default_sink = default_sink; + if let crate::config::Sink::Default = self.config.sink { + sink_we_care_about = curr_default_sink.clone(); + if let Some(s) = &sink_we_care_about { + context.refresh_volume(s); + } } } - _ => {} - } - - let Pulse::IterationResult { default_sink, volume, state } = context.iterate(sink_we_care_about); - context_state = state.unwrap_or(context_state); - if default_sink.is_some() && default_sink != curr_default_sink { - curr_default_sink = default_sink; - if let crate::config::Sink::Default = self.config.sink { - sink_we_care_about = curr_default_sink; - context.refresh_volume(); + if volume.is_some() && volume != curr_volume { + curr_volume = volume; + self.send_updated_volume_to_main(curr_volume.as_ref().unwrap()).expect("Tried to inform main thread about volume update. Main thread isn't listening."); } - } - if volume.is_some() && volume != curr_volume { - curr_volume = volume; - self.send_updated_volume_to_main(curr_volume); - } - match self.from_main.try_recv() { - Ok(x) => match x { - MessagesFromMain::Quit => { - break; - } - MessagesFromMain::Refresh => { - context.refresh_volume(); - if let crate::config::Sink::Default = self.config.sink { - context.refresh_default_sink(); + match self.from_main.try_recv() { + Ok(x) => match x { + MessagesFromMain::Quit => { + break 'outer; + } + MessagesFromMain::Refresh => { + if let Some(s) = &sink_we_care_about { + context.refresh_volume(s); + } + if let crate::config::Sink::Default = self.config.sink { + context.refresh_default_sink(); + } } } - } - Err(e) => { - if let TryRecvError::Disconnected = e { - break; + Err(e) => { + if let TryRecvError::Disconnected = e { + break 'outer; + } } } } diff --git a/pulse/src/runnable/pulse.rs b/pulse/src/runnable/pulse.rs index 64a7d33..d92a41f 100644 --- a/pulse/src/runnable/pulse.rs +++ b/pulse/src/runnable/pulse.rs @@ -2,9 +2,8 @@ //! It is the only place in this plugin that explicitly includes unsafe code. use std::sync::{Arc, Weak}; -use std::ffi::{c_void, CString}; -use libc::{c_int, size_t}; -use std::os::raw::c_char; +use std::ffi::{CString, CStr}; +use std::os::raw::{c_int, c_char}; use std::convert::TryFrom; use std::marker::PhantomData; @@ -38,11 +37,12 @@ impl Pulse { pub(super) struct PulseContext<'c> { context : *mut PaContext, //Can actually never be null, the nullptr case is handled by create, which returns an Err instead of a PulseContext then. + scratch : &'c mut ContextScratch, //This scratch space is needed, because user data pointers in pulse need to survive between iterations. Yes, that's insane. No, I can't do anything about it... main : &'c Pulse } impl<'c> PulseContext<'c> { - pub(super) fn create<'m>(pulse : &'m Pulse) -> Result, PulseContextCreationError> { + pub(super) fn create<'m>(pulse : &'m Pulse, scratch : &'m mut ContextScratch) -> Result, PulseContextCreationError> { let api = pulse.main_loop.get_api(); if api.is_null() { Err(PulseContextCreationError::FailedToGetPulseApi) @@ -54,10 +54,102 @@ impl<'c> PulseContext<'c> { Err(PulseContextCreationError::ContextNewFailed) } else { - Ok(PulseContext { context, main : pulse}) + Ok(PulseContext { context, scratch, main : pulse}) } } } + pub(super) fn iterate(&mut self, relevant_sink : &Option) -> Result { + self.scratch.sink_we_care_about = relevant_sink.clone(); + self.scratch.volume = None; + self.scratch.default_sink = None; + self.main.main_loop.iterate()?; + return Ok(IterationResult { + default_sink : self.scratch.default_sink.clone(), + volume : self.scratch.volume.clone() + }); + } + pub(super) fn get_state(&self) -> PaContextState { + unsafe { pa_context_get_state(self.context) } + } + pub(super) fn connect_and_set_callbacks(&mut self) -> Result<(), PulseContextConnectError> { + unsafe { + pa_context_set_state_callback(self.context,Some(Self::on_context_state_change),self.scratch); + let connect_result = pa_context_connect(self.context,std::ptr::null(),PaContextFlags::NoAutoSpawnNoFail,std::ptr::null()); + if connect_result == 0 { + Ok(()) + } + else { + Err(PulseContextConnectError(connect_result)) + } + } + } + + pub(super) fn refresh_default_sink(&mut self) { + unsafe {pa_operation_unref(pa_context_get_server_info(self.context,Some(Self::on_server_info_received),self.scratch)); } + } + + pub(super) fn refresh_volume(&mut self, sink : &SinkHandle) { + unsafe {pa_operation_unref(pa_context_get_sink_info_by_name(self.context,sink.sink.as_ptr(),Some(Self::on_sink_info_received),self.scratch));} + } + + fn on_context_state_change(context : *mut PaContext, scratch : *mut ContextScratch) { + unsafe { + match pa_context_get_state(context) { + PaContextState::Ready => { + pa_context_set_subscribe_callback(context,Some(Self::on_context_event),scratch); + pa_operation_unref(pa_context_subscribe(context,0x80 /* SERVER */ | 0x01 /* SINK */, None, std::ptr::null_mut())); + } + _ => {} + } + } + } + + fn on_context_event(context : *mut PaContext,event_type : c_int,index: u32,scratch : *mut ContextScratch) { + assert!(!context.is_null()); + assert!(!scratch.is_null()); + unsafe { + let facility = 0x000f & event_type; + match facility { + 0x0 /* SINK */ => { + pa_operation_unref(pa_context_get_sink_info_by_index(context,index,Some(Self::on_sink_info_received),scratch)); + } + 0x7 /* SERVER */ => { + pa_operation_unref(pa_context_get_server_info(context,Some(Self::on_server_info_received),scratch)); + } + _ /* should not happen */ => { + assert!(false); + } + } + } + } + + fn on_sink_info_received(_context : *mut PaContext, sink_info : *const PaSinkInfo, _eol : c_int, scratch : *mut ContextScratch) { + if sink_info.is_null() { + return; + } + unsafe { + if let Some(s) = &(*scratch).sink_we_care_about { + if s.sink.as_c_str() == CStr::from_ptr((*sink_info).sink_name) { + let avg_volume = pa_cvolume_avg(&(*sink_info).volume); + const norm : u32 = 0x10000; + let volume = (avg_volume as f32) / (norm as f32); + let balance = pa_cvolume_get_balance(&(*sink_info).volume, &(*sink_info).channel_map); + let muted = (*sink_info).mute != 0; + + (*scratch).volume = Some(Volume {volume, balance, muted}); + } + } + } + } + + fn on_server_info_received(_context : *mut PaContext, server_info : *const PaServerInfo, scratch : *mut ContextScratch) { + if server_info.is_null() { + return; + } + unsafe { + (*scratch).default_sink= Some(SinkHandle::from(CStr::from_ptr((*server_info).default_sink_name))); + } + } } impl<'c> Drop for PulseContext<'c> { @@ -66,6 +158,46 @@ impl<'c> Drop for PulseContext<'c> { } } +#[derive(Debug)] +pub struct PulseContextConnectError(i32); +impl std::fmt::Display for PulseContextConnectError { + fn fmt(&self, f : &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Pulse Context connect error: ")?; + match self.0 { + 0 => { write!(f,"No error occured.") } + 1 => { write!(f,"Access failure") } + 2 => { write!(f,"Unknown command") } + 3 => { write!(f,"Invalid argument") } + 4 => { write!(f,"Entity exists") } + 5 => { write!(f,"No such entity") } + 6 => { write!(f,"Connection refused") } + 7 => { write!(f,"Protocol error") } + 8 => { write!(f,"Timeout") } + 9 => { write!(f,"No authentication key") } + 10 => { write!(f,"Internal error") } + 11 => { write!(f,"Connection terminated") } + 12 => { write!(f,"Entity killed") } + 13 => { write!(f,"Invalid server") } + 14 => { write!(f,"Module initialization failed") } + 15 => { write!(f,"Bad state") } + 16 => { write!(f,"No data") } + 17 => { write!(f,"Incompatible protocol version") } + 18 => { write!(f,"Data too large") } + 19 => { write!(f,"Operation not supported") } + 20 => { write!(f,"The error code was unknown to the client") } + 21 => { write!(f,"Extension does not exist.") } + 22 => { write!(f,"Obsolete functionality.") } + 23 => { write!(f,"Missing implementation.") } + 24 => { write!(f,"The caller forked without calling execve() and tried to reuse the context.") } + 25 => { write!(f,"An IO error happened.") } + 26 => { write!(f,"Device or resource busy.") } + _ => { write!(f,"Unknown error. Not documented at time of writing.") } + } + } +} +impl std::error::Error for PulseContextConnectError {} + + #[derive(Debug)] pub enum PulseContextCreationError { SettingNameFailed, @@ -144,6 +276,15 @@ impl PulseMainLoop { fn get_api(&self) -> *mut PaMainloopApi { unsafe { pa_mainloop_get_api(self.main_loop) } } + fn iterate(&self) -> Result<(),MainLoopIterationError> { + let i = unsafe { pa_mainloop_iterate(self.main_loop, 1, std::ptr::null_mut()) }; + if i >= 0 { + Ok(()) + } + else { + Err(MainLoopIterationError(i)) + } + } } impl Drop for PulseMainLoop { fn drop(&mut self) { @@ -155,6 +296,15 @@ impl Drop for PulseMainLoop { } } +#[derive(Debug)] +pub struct MainLoopIterationError(i32); +impl std::fmt::Display for MainLoopIterationError { + fn fmt(&self, f : &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "Failed to iterate pulseaudio main loop. Error {}", self.0) + } +} +impl std::error::Error for MainLoopIterationError {} + #[derive(Debug)] pub struct MainLoopCreationError {} impl std::fmt::Display for MainLoopCreationError { @@ -164,6 +314,7 @@ impl std::fmt::Display for MainLoopCreationError { } impl std::error::Error for MainLoopCreationError {} +#[derive(Clone, PartialEq, Eq, Debug)] pub(super) struct SinkHandle { sink: CString } @@ -178,15 +329,41 @@ impl TryFrom<&str> for SinkHandle { } } +impl From<&CStr> for SinkHandle { + fn from(s : &CStr) -> SinkHandle { + SinkHandle { sink : CString::from(s) } + } +} + pub(super) struct IterationResult { pub default_sink : Option, pub volume : Option, - pub state : Option } +#[derive(Clone, PartialEq)] pub(super) struct Volume { pub volume : f32, - pub balance : f32 + pub balance : f32, + pub muted : bool +} + +pub(super) struct ContextScratch { + /* output */ + volume : Option, + default_sink : Option, + + /* input */ + sink_we_care_about : Option +} + +impl Default for ContextScratch { + fn default() -> Self { + ContextScratch { + volume : None, + default_sink : None, + sink_we_care_about : None + } + } } #[repr(C)] pub(super) enum PaContextState { @@ -199,21 +376,120 @@ pub(super) struct Volume { Terminated } +#[repr(C)] enum PaContextFlags { + NoFlags = 0, + NoAutoSpawn = 1, + NoFail = 2, + NoAutoSpawnNoFail = 3 +} + +#[repr(C)] struct PaSinkInfo { + sink_name : *const c_char, + index : u32, + description : *const c_char, + sample_spec : PaSampleSpec, + channel_map : PaChannelMap, + owner_module : u32, + volume : PaCVolume, + mute : c_int, + monitor_source : u32, + monitor_source_name : *const c_char, + latency : u64, + driver : *const c_char, + flags : c_int, + proplist : *mut PaPropList, + configured_latency : u64, + base_volume : u32, + state : c_int, + n_volume_steps : u32, + card : u32, + n_ports : u32, + ports : *mut *mut PaSinkPortInfo, + active_port : *mut PaSinkPortInfo, + n_formats : u8, + formats : *mut *mut PaFormatInfo +} + +#[repr(C)] struct PaSampleSpec { + format : c_int, + rate : u32, + channels : u8 +} + +#[repr(C)] struct PaChannelMap { + channels : u8, + map : [c_int; 32] +} + +#[repr(C)] struct PaCVolume { + channels : u8, + values : [u32; 32] +} + +#[repr(C)] struct PaServerInfo { + user_name : *const c_char, + host_name : *const c_char, + server_version : *const c_char, + server_name : *const c_char, + sample_spec : PaSampleSpec, + default_sink_name : *const c_char, + default_source_name : *const c_char, + cookie : u32, + channel_map : PaChannelMap +} + +#[repr(C)] struct PaPropList { _private: [u8; 0]} + +///This actually holds meaningful data, but we don't care about it for now. +#[repr(C)] struct PaSinkPortInfo { _private: [u8; 0]} + +///Similar to PaSinkPortInfo, we could interpret this, but won't need to. +#[repr(C)] struct PaFormatInfo { _private: [u8;0]} + #[repr(C)] struct PaMainloop { _private: [u8; 0] } #[repr(C)] struct PaContext { _private: [u8; 0] } +#[repr(C)] struct PaOperation { _private: [u8; 0] } ///While we could in theory wrap the whole API, there's no need for it. We can treat it as an ///opaque type, because we never call any functions on it. #[repr(C)] struct PaMainloopApi { _private: [u8; 0] } +///If we were to allow auto-spawning, we would need to actually implement this... +#[repr(C)] struct PaSpawnApi { _private: [u8; 0] } + +type PaContextSuccessCb = fn(*mut PaContext, c_int, *mut ContextScratch); +type PaContextSubscribeCb = fn(*mut PaContext,c_int,u32,*mut ContextScratch); +type PaContextStateCb = fn(*mut PaContext, *mut ContextScratch); +type PaSinkInfoCb = fn(*mut PaContext, *const PaSinkInfo, c_int, *mut ContextScratch); +type PaServerInfoCb = fn(*mut PaContext, *const PaServerInfo, *mut ContextScratch); + #[link(name = "pulse")] extern { + #[must_use] fn pa_mainloop_new() -> *mut PaMainloop; fn pa_mainloop_wakeup(_ : *mut PaMainloop); fn pa_mainloop_quit(_ : *mut PaMainloop, retval : c_int); fn pa_mainloop_free(_ : *mut PaMainloop); + fn pa_mainloop_iterate(_ : *mut PaMainloop, block : c_int, return_value : *mut c_int) -> c_int; fn pa_mainloop_get_api(_ : *mut PaMainloop) -> *mut PaMainloopApi; + #[must_use] fn pa_context_new(_ : *mut PaMainloopApi, name :*const c_char) -> *mut PaContext; fn pa_context_unref(_ : *mut PaContext); + fn pa_context_get_state(_ : *mut PaContext) -> PaContextState; + fn pa_context_connect(_: *mut PaContext, server : *const c_char, flags : PaContextFlags, api : *const PaSpawnApi) -> c_int; + fn pa_context_set_state_callback(_: *mut PaContext, callback: Option, scratch : *mut ContextScratch); + fn pa_context_set_subscribe_callback(_: *mut PaContext,callback : Option,scratch : *mut ContextScratch); + #[must_use] + fn pa_context_subscribe(_: *mut PaContext, subscription_mask : c_int, callback : Option, scratch : *mut ContextScratch) -> *mut PaOperation; + fn pa_operation_unref(operation : *mut PaOperation); + + #[must_use] + fn pa_context_get_sink_info_by_index(_: *mut PaContext, sink_index : u32, callback : Option, scratch : *mut ContextScratch) -> *mut PaOperation; + fn pa_context_get_sink_info_by_name(_: *mut PaContext, sink : *const c_char, callback : Option, scratch : *mut ContextScratch) -> *mut PaOperation; + #[must_use] + fn pa_context_get_server_info(_: *mut PaContext, callback : Option, scratch : *mut ContextScratch) -> *mut PaOperation; + + fn pa_cvolume_avg(volume : *const PaCVolume) -> u32; + fn pa_cvolume_get_balance(volume : *const PaCVolume, channel_map : *const PaChannelMap) -> f32; } -- cgit v1.2.3