From a3cbf4dd5dc5db7c2bc7039708e57aa96ea63f9e Mon Sep 17 00:00:00 2001 From: Matthew Donoughe Date: Sun, 17 Feb 2019 17:27:23 -0500 Subject: [PATCH] add watch_with_volume API --- CHANGELOG.md | 6 ++ src/com/event.rs | 16 ++-- src/lib.rs | 85 +++++++++++++++++++- src/main.rs | 2 +- src/media/event.rs | 121 ++++++++++++++++++++++++++++ src/{media.rs => media/mod.rs} | 142 ++++++++++++++++++++++++++++++++- src/soundcore/core.rs | 8 +- src/soundcore/event.rs | 2 +- src/soundcore/mod.rs | 1 + 9 files changed, 371 insertions(+), 12 deletions(-) create mode 100644 src/media/event.rs rename src/{media.rs => media/mod.rs} (75%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 457fc99..7914f9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] +### Added +- There is now a `watch_with_volume` method on the API which allows API users to observe both changes to SoundBlaster settings and changes to the Windows volume settings at the same time without needing to run two threads. + +### Changed +- The output of the `watch` command is now different due to using the `watch_with_volume` API. + ## [3.0.0] - 2019-01-14 This release unfortunately renames the `-f` command line parameter to `-i` to allow for a new `-f` to specify the file format. diff --git a/src/com/event.rs b/src/com/event.rs index a6e5af2..c9e19ae 100644 --- a/src/com/event.rs +++ b/src/com/event.rs @@ -1,8 +1,8 @@ -use futures::{Async, Stream}; use futures::executor::{self, Notify, NotifyHandle, Spawn}; +use futures::{Async, Stream}; -use std::ptr; use std::mem; +use std::ptr; use std::sync::{Arc, Mutex}; use winapi::um::combaseapi::{CoWaitForMultipleObjects, CWMO_DISPATCH_CALLS}; @@ -10,7 +10,7 @@ use winapi::um::handleapi::CloseHandle; use winapi::um::synchapi::{CreateEventW, SetEvent}; use winapi::um::winbase::INFINITE; -use crate::hresult::{check}; +use crate::hresult::check; struct ComUnparkState { handles: Vec, @@ -119,7 +119,10 @@ pub struct ComEventIterator { inner: Spawn, } -impl ComEventIterator where S: Stream { +impl ComEventIterator +where + S: Stream, +{ pub fn new(stream: S) -> Self { let park = ComUnpark::new(); let id = park.allocate_id(); @@ -131,7 +134,10 @@ impl ComEventIterator where S: Stream { } } -impl Iterator for ComEventIterator where S: Stream { +impl Iterator for ComEventIterator +where + S: Stream, +{ type Item = Result; fn next(&mut self) -> Option { diff --git a/src/lib.rs b/src/lib.rs index 1fb8d40..af61d57 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,6 +21,9 @@ pub mod media; pub mod soundcore; mod winapiext; +use futures::stream::Fuse; +use futures::{Async, Poll, Stream}; + use indexmap::IndexMap; use std::collections::BTreeSet; @@ -30,9 +33,11 @@ use std::fmt; use slog::Logger; -use crate::media::{DeviceEnumerator, Endpoint}; +use crate::com::event::ComEventIterator; +use crate::media::{DeviceEnumerator, Endpoint, VolumeEvents, VolumeNotification}; use crate::soundcore::{ - SoundCore, SoundCoreEventIterator, SoundCoreFeature, SoundCoreParamValue, SoundCoreParameter, + SoundCore, SoundCoreEvent, SoundCoreEventIterator, SoundCoreEvents, SoundCoreFeature, + SoundCoreParamValue, SoundCoreParameter, }; pub use crate::hresult::Win32Error; @@ -257,6 +262,82 @@ pub fn watch( Ok(core.events()?) } +/// Either a SoundCoreEvent or a VolumeNotification. +#[derive(Debug)] +pub enum SoundCoreOrVolumeEvent { + /// A SoundCoreEvent. + SoundCore(SoundCoreEvent), + /// A VolumeNotification. + Volume(VolumeNotification), +} + +struct SoundCoreAndVolumeEvents { + sound_core: Fuse, + volume: Fuse, +} + +impl Stream for SoundCoreAndVolumeEvents { + type Item = SoundCoreOrVolumeEvent; + type Error = Win32Error; + + fn poll(&mut self) -> Poll, Self::Error> { + if let Async::Ready(Some(item)) = self.sound_core.poll()? { + return Ok(Async::Ready(Some(SoundCoreOrVolumeEvent::SoundCore(item)))); + } + if let Async::Ready(Some(item)) = self.volume.poll().unwrap() { + return Ok(Async::Ready(Some(SoundCoreOrVolumeEvent::Volume(item)))); + } + Ok(Async::NotReady) + } +} + +/// Iterates over volume change events and also events produced through the +/// SoundCore API. +/// +/// This iterator will block until the next event is available. +pub struct SoundCoreAndVolumeEventIterator { + inner: ComEventIterator, +} + +impl Iterator for SoundCoreAndVolumeEventIterator { + type Item = Result; + + fn next(&mut self) -> Option { + self.inner.next() + } +} + +/// Gets the sequence of events for a device. +/// +/// If `device_id` is None, the system default output device will be used. +/// +/// # Examples +/// +/// ``` +/// for event in watch_with_volume(logger.clone(), None) { +/// println!("{:?}", event); +/// } +/// ``` +pub fn watch_with_volume( + logger: &Logger, + device_id: Option<&OsStr>, +) -> Result> { + let endpoint = get_endpoint(logger.clone(), device_id)?; + let id = endpoint.id()?; + let clsid = endpoint.clsid()?; + let core = SoundCore::for_device(&clsid, &id, logger.clone())?; + + let core_events = core.event_stream()?; + let volume_events = endpoint.event_stream()?; + + Ok(SoundCoreAndVolumeEventIterator { + inner: ComEventIterator::new(SoundCoreAndVolumeEvents { + sound_core: core_events.fuse(), + volume: volume_events.fuse(), + }), + }) +} + #[derive(Debug)] struct UnsupportedValueError { feature: String, diff --git a/src/main.rs b/src/main.rs index 13cb497..09a28e5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -708,7 +708,7 @@ fn set(logger: &Logger, matches: &ArgMatches) -> Result<(), Box> { } fn watch(logger: &Logger, matches: &ArgMatches) -> Result<(), Box> { - for event in sbz_switch::watch(logger, matches.value_of_os("device"))? { + for event in sbz_switch::watch_with_volume(logger, matches.value_of_os("device"))? { println!("{:?}", event); } Ok(()) diff --git a/src/media/event.rs b/src/media/event.rs new file mode 100644 index 0000000..2ca77d8 --- /dev/null +++ b/src/media/event.rs @@ -0,0 +1,121 @@ +use futures::task::AtomicTask; +use futures::{Async, Poll, Stream}; + +use std::clone::Clone; +use std::fmt; +use std::sync::{mpsc, Arc}; + +use winapi::shared::guiddef::GUID; +use winapi::shared::winerror::E_ABORT; +use winapi::um::endpointvolume::{IAudioEndpointVolume, IAudioEndpointVolumeCallback}; + +use super::AudioEndpointVolumeCallback; +use crate::com::ComObject; +use crate::hresult::{check, Win32Error}; + +/// Describes a volume change event. +/// +/// [Official documentation](https://docs.microsoft.com/en-us/windows/desktop/api/endpointvolume/ns-endpointvolume-audio_volume_notification_data) +pub struct VolumeNotification { + /// The ID that was provided when changing the volume. + pub event_context: GUID, + /// Is the endpoint now muted? + pub is_muted: bool, + /// The new volume level of the endpoint. + pub volume: f32, +} + +impl fmt::Debug for VolumeNotification { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fmt.debug_struct("VolumeNotification") + .field( + "event_context", + &format_args!( + "{{{:08X}-{:04X}-{:04X}-{:02X}{:02X}-{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}}}", + self.event_context.Data1, + self.event_context.Data2, + self.event_context.Data3, + self.event_context.Data4[0], + self.event_context.Data4[1], + self.event_context.Data4[2], + self.event_context.Data4[3], + self.event_context.Data4[4], + self.event_context.Data4[5], + self.event_context.Data4[6], + self.event_context.Data4[7] + ), + ) + .field("is_muted", &self.is_muted) + .field("volume", &self.volume) + .finish() + } +} + +pub(crate) struct VolumeEvents { + volume: ComObject, + events: mpsc::Receiver, + task: Arc, + callback: ComObject, +} + +impl VolumeEvents { + pub fn new(volume: ComObject) -> Result { + let task = Arc::new(AtomicTask::new()); + let (tx, rx) = mpsc::channel(); + + let tx_task = task.clone(); + unsafe { + let callback = AudioEndpointVolumeCallback::wrap(move |e| { + match tx.send(VolumeNotification { + event_context: e.guidEventContext, + is_muted: e.bMuted != 0, + volume: e.fMasterVolume, + }) { + Ok(()) => { + tx_task.notify(); + Ok(()) + } + Err(_) => Err(Win32Error::new(E_ABORT)), + } + }); + + let result = check((*volume).RegisterControlChangeNotify(callback)); + if let Err(error) = result { + (*callback).Release(); + return Err(error); + } + + Ok(Self { + volume, + events: rx, + task, + callback: ComObject::take(callback), + }) + } + } +} + +impl Stream for VolumeEvents { + type Item = VolumeNotification; + type Error = (); + + fn poll(&mut self) -> Poll, Self::Error> { + self.task.register(); + match self.events.try_recv() { + Ok(e) => Ok(Async::Ready(Some(e))), + Err(mpsc::TryRecvError::Empty) => Ok(Async::NotReady), + Err(mpsc::TryRecvError::Disconnected) => Ok(Async::Ready(None)), + } + } +} + +impl Drop for VolumeEvents { + fn drop(&mut self) { + unsafe { + check( + (*self.volume).UnregisterControlChangeNotify(&*self.callback as *const _ as *mut _), + ) + .unwrap(); + } + } +} diff --git a/src/media.rs b/src/media/mod.rs similarity index 75% rename from src/media.rs rename to src/media/mod.rs index bf5ef62..e37cb46 100644 --- a/src/media.rs +++ b/src/media/mod.rs @@ -2,6 +2,9 @@ #![allow(unknown_lints)] +mod event; + +use std::alloc; use std::error::Error; use std::ffi::{OsStr, OsString}; use std::fmt; @@ -10,24 +13,36 @@ use std::mem; use std::os::windows::ffi::{OsStrExt, OsStringExt}; use std::ptr; use std::slice; +use std::sync::atomic::{self, AtomicUsize, Ordering}; use regex::Regex; use slog::Logger; +use winapi::ctypes::c_void; use winapi::shared::guiddef::GUID; +use winapi::shared::guiddef::{IsEqualIID, REFIID}; +use winapi::shared::minwindef::ULONG; +use winapi::shared::ntdef::HRESULT; use winapi::shared::winerror::NTE_NOT_FOUND; +use winapi::shared::winerror::{E_INVALIDARG, E_NOINTERFACE}; use winapi::shared::wtypes::{PROPERTYKEY, VARTYPE}; use winapi::um::combaseapi::CLSCTX_ALL; use winapi::um::combaseapi::{CoCreateInstance, CoTaskMemFree, PropVariantClear}; use winapi::um::coml2api::STGM_READ; -use winapi::um::endpointvolume::IAudioEndpointVolume; +use winapi::um::endpointvolume::{ + IAudioEndpointVolume, IAudioEndpointVolumeCallback, IAudioEndpointVolumeCallbackVtbl, + AUDIO_VOLUME_NOTIFICATION_DATA, +}; use winapi::um::mmdeviceapi::{ eConsole, eRender, CLSID_MMDeviceEnumerator, IMMDevice, IMMDeviceEnumerator, DEVICE_STATE_ACTIVE, }; use winapi::um::propidl::PROPVARIANT; use winapi::um::propsys::IPropertyStore; +use winapi::um::unknwnbase::{IUnknown, IUnknownVtbl}; use winapi::Interface; +pub(crate) use self::event::VolumeEvents; +pub use self::event::VolumeNotification; use crate::com::{ComObject, ComScope}; use crate::hresult::{check, Win32Error}; use crate::lazy::Lazy; @@ -221,6 +236,9 @@ impl Endpoint { Ok(volume) } } + pub(crate) fn event_stream(&self) -> Result { + Ok(VolumeEvents::new(self.volume()?)?) + } } /// Describes an error that occurred while retrieving a property from a device. @@ -362,3 +380,125 @@ impl DeviceEnumerator { } } } + +#[repr(C)] +struct AudioEndpointVolumeCallback { + lp_vtbl: *mut IAudioEndpointVolumeCallbackVtbl, + vtbl: IAudioEndpointVolumeCallbackVtbl, + refs: AtomicUsize, + callback: C, +} + +impl AudioEndpointVolumeCallback +where + C: Send + 'static + FnMut(&AUDIO_VOLUME_NOTIFICATION_DATA) -> Result<(), Win32Error>, +{ + /// Wraps a function in an `IAudioEndpointVolumeCallback`. + pub unsafe fn wrap(callback: C) -> *mut IAudioEndpointVolumeCallback { + let mut value = Box::new(AudioEndpointVolumeCallback:: { + lp_vtbl: ptr::null_mut(), + vtbl: IAudioEndpointVolumeCallbackVtbl { + parent: IUnknownVtbl { + QueryInterface: callback_query_interface::, + AddRef: callback_add_ref::, + Release: callback_release::, + }, + OnNotify: callback_on_notify::, + }, + refs: AtomicUsize::new(1), + callback, + }); + value.lp_vtbl = &mut value.vtbl as *mut _; + Box::into_raw(value) as *mut _ + } +} + +// ensures `this` is an instance of the expected type +unsafe fn validate(this: *mut I) -> Result<*mut AudioEndpointVolumeCallback, Win32Error> +where + I: Interface, +{ + let this = this as *mut IUnknown; + if this.is_null() + || (*this).lpVtbl.is_null() + || (*(*this).lpVtbl).QueryInterface as usize != callback_query_interface:: as usize + { + Err(Win32Error::new(E_INVALIDARG)) + } else { + Ok(this as *mut AudioEndpointVolumeCallback) + } +} + +// converts a `Result` to an `HRESULT` so `?` can be used +unsafe fn uncheck(result: E) -> HRESULT +where + E: FnOnce() -> Result, +{ + match result() { + Ok(result) => result, + Err(Win32Error { code, .. }) => code, + } +} + +unsafe extern "system" fn callback_query_interface( + this: *mut IUnknown, + iid: REFIID, + object: *mut *mut c_void, +) -> HRESULT { + uncheck(|| { + let this = validate::<_, C>(this)?; + let iid = iid.as_ref().unwrap(); + if IsEqualIID(iid, &IUnknown::uuidof()) + || IsEqualIID(iid, &IAudioEndpointVolumeCallback::uuidof()) + { + (*this).refs.fetch_add(1, Ordering::Relaxed); + *object = this as *mut c_void; + Ok(0) + } else { + *object = ptr::null_mut(); + Err(Win32Error::new(E_NOINTERFACE)) + } + }) +} + +unsafe extern "system" fn callback_add_ref(this: *mut IUnknown) -> ULONG { + match validate::<_, C>(this) { + Ok(this) => { + let count = (*this).refs.fetch_add(1, Ordering::Relaxed) + 1; + count as ULONG + } + Err(_) => 1, + } +} + +unsafe extern "system" fn callback_release(this: *mut IUnknown) -> ULONG { + match validate::<_, C>(this) { + Ok(this) => { + let count = (*this).refs.fetch_sub(1, Ordering::Release) - 1; + if count == 0 { + atomic::fence(Ordering::Acquire); + ptr::drop_in_place(this); + alloc::dealloc( + this as *mut u8, + alloc::Layout::for_value(this.as_ref().unwrap()), + ); + } + count as ULONG + } + Err(_) => 1, + } +} + +unsafe extern "system" fn callback_on_notify( + this: *mut IAudioEndpointVolumeCallback, + notify: *mut AUDIO_VOLUME_NOTIFICATION_DATA, +) -> HRESULT +where + C: FnMut(&AUDIO_VOLUME_NOTIFICATION_DATA) -> Result<(), Win32Error>, +{ + uncheck(|| { + let this = validate::<_, C>(this)?; + ((*this).callback)(&*notify)?; + Ok(0) + }) +} diff --git a/src/soundcore/core.rs b/src/soundcore/core.rs index 5922e7c..4cda26f 100644 --- a/src/soundcore/core.rs +++ b/src/soundcore/core.rs @@ -90,17 +90,21 @@ impl SoundCore { /// multiple event handlers and then unregistering only one of them. Probably /// this is okay if done with multiple `SoundCore` instances. pub fn events(&self) -> Result { + Ok(SoundCoreEventIterator::new(self.event_stream()?)) + } + + pub(crate) fn event_stream(&self) -> Result { unsafe { let mut event_notify: *mut IEventNotify = mem::uninitialized(); check(self.sound_core.QueryInterface( &IEventNotify::uuidof(), &mut event_notify as *mut *mut _ as *mut _, ))?; - Ok(SoundCoreEventIterator::new(SoundCoreEvents::new( + Ok(SoundCoreEvents::new( ComObject::take(event_notify), self.sound_core.clone(), self.logger.clone(), - )?)) + )?) } } } diff --git a/src/soundcore/event.rs b/src/soundcore/event.rs index ec16332..66ae76d 100644 --- a/src/soundcore/event.rs +++ b/src/soundcore/event.rs @@ -140,7 +140,7 @@ pub struct SoundCoreEventIterator { impl SoundCoreEventIterator { pub(crate) fn new(stream: SoundCoreEvents) -> Self { SoundCoreEventIterator { - inner: ComEventIterator::new(stream) + inner: ComEventIterator::new(stream), } } } diff --git a/src/soundcore/mod.rs b/src/soundcore/mod.rs index 04966ae..00a4c56 100644 --- a/src/soundcore/mod.rs +++ b/src/soundcore/mod.rs @@ -17,6 +17,7 @@ mod parameter_iterator; pub use self::consts::PKEY_SOUNDCORECTL_CLSID; pub use self::core::SoundCore; pub use self::error::SoundCoreError; +pub(crate) use self::event::SoundCoreEvents; pub use self::event::{SoundCoreEvent, SoundCoreEventIterator}; pub use self::feature::SoundCoreFeature; pub use self::feature_iterator::SoundCoreFeatureIterator;