Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/cpal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -320,8 +320,6 @@ jobs:
fail-fast: false
matrix:
include:
- windows-version: "0.59.0"
- windows-version: "0.60.0"
- windows-version: "0.61.3"
# Skip 0.62.x since we already test current version in test-native
name: test-windows-v${{ matrix.windows-version }}
Expand Down Expand Up @@ -349,6 +347,12 @@ jobs:
# Use cargo update --precise to lock the windows crate to a specific version
cargo update --precise ${{ matrix.windows-version }} windows

# Remove and re-add `windows-core` to force Cargo to re-resolve it
# I'd prefer a more surgical approach, but things get complicated once multiple `windows-core`
# versions are in play
cargo rm --target='cfg(target_os = "windows")' windows-core
cargo add --target='cfg(target_os = "windows")' windows-core@*

# Verify the version was locked correctly
echo "Locked windows crate version:"
cargo tree | grep "windows v" || echo "Windows crate not found in dependency tree"
Expand Down
18 changes: 17 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,20 @@ edition = "2021"
rust-version = "1.70"

[features]
default = ["wasapi-virtual-default-devices"]

asio = [
"asio-sys",
"num-traits",
] # Only available on Windows. See README for setup instructions.

# Enable virtual default devices for WASAPI, so that audio will be
# automatically rerouted when the default input or output device is changed.
#
# Note that this only works on Windows 8 and above. It is turned on by default,
# but consider turning it off if you are supporting an older version of Windows.
wasapi-virtual-default-devices = []

# Deprecated, the `oboe` backend has been removed
oboe-shared-stdcxx = []

Expand All @@ -31,7 +40,12 @@ clap = { version = "4.5", features = ["derive"] }
# versions when bumping to a new release, and only increase the minimum when absolutely necessary.
# When updating this, also update the "windows-version" matrix in the CI workflow.
[target.'cfg(target_os = "windows")'.dependencies]
windows = { version = ">=0.58, <=0.62", features = [
# The `implement` feature was removed in windows-0.61, which means that we can't
# use older versions of the `windows` crate without explicitly activating `implement`
# for them, which will cause problems for >=0.61.
#
# See <https://github.com/microsoft/windows-rs/pull/3333>.
windows = { version = ">=0.61, <=0.62", features = [
"Win32_Media_Audio",
"Win32_Foundation",
"Win32_Devices_Properties",
Expand All @@ -44,6 +58,8 @@ windows = { version = ">=0.58, <=0.62", features = [
"Win32_Media_Multimedia",
"Win32_UI_Shell_PropertiesSystem",
] }
# Explicitly depend on windows-core for use with the `windows::core::implement` macro.
windows-core = "*"
audio_thread_priority = { version = "0.34.0", optional = true }
asio-sys = { version = "0.2", path = "asio-sys", optional = true }
num-traits = { version = "0.2.6", optional = true }
Expand Down
239 changes: 183 additions & 56 deletions src/host/wasapi/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,17 @@ struct IAudioClientWrapper(Audio::IAudioClient);
unsafe impl Send for IAudioClientWrapper {}
unsafe impl Sync for IAudioClientWrapper {}

#[derive(Debug, Clone)]
enum DeviceType {
DefaultOutput,
DefaultInput,
Specific(Audio::IMMDevice),
}

/// An opaque type that identifies an end point.
#[derive(Clone)]
pub struct Device {
device: Audio::IMMDevice,
device: DeviceType,
/// We cache an uninitialized `IAudioClient` so that we can call functions from it without
/// having to create/destroy audio clients all the time.
future_audio_client: Arc<Mutex<Option<IAudioClientWrapper>>>, // TODO: add NonZero around the ptr
Expand Down Expand Up @@ -266,15 +273,70 @@ unsafe fn format_from_waveformatex_ptr(
Some(format)
}

#[cfg(feature = "wasapi-virtual-default-devices")]
unsafe fn activate_audio_interface_sync(
deviceinterfacepath: windows::core::PWSTR,
) -> windows::core::Result<Audio::IAudioClient> {
use windows::core::IUnknown;

#[windows::core::implement(Audio::IActivateAudioInterfaceCompletionHandler)]
struct CompletionHandler(std::sync::mpsc::Sender<windows::core::Result<IUnknown>>);

fn retrieve_result(
operation: &Audio::IActivateAudioInterfaceAsyncOperation,
) -> windows::core::Result<IUnknown> {
let mut result = windows::core::HRESULT::default();
let mut interface: Option<IUnknown> = None;
unsafe {
operation.GetActivateResult(&mut result, &mut interface)?;
}
result.ok()?;
interface.ok_or_else(|| {
windows::core::Error::new(
Audio::AUDCLNT_E_DEVICE_INVALIDATED,
"audio interface could not be retrieved during activation",
)
})
}

impl Audio::IActivateAudioInterfaceCompletionHandler_Impl for CompletionHandler_Impl {
fn ActivateCompleted(
&self,
operation: windows::core::Ref<Audio::IActivateAudioInterfaceAsyncOperation>,
) -> windows::core::Result<()> {
let result = operation.ok().and_then(retrieve_result);
let _ = self.0.send(result);
Ok(())
}
}

let (sender, receiver) = std::sync::mpsc::channel();
let completion: Audio::IActivateAudioInterfaceCompletionHandler =
CompletionHandler(sender).into();
Audio::ActivateAudioInterfaceAsync(
deviceinterfacepath,
&Audio::IAudioClient::IID,
None,
&completion,
)?;
let result = receiver.recv_timeout(Duration::from_secs(2)).unwrap()?;
result.cast()
}

unsafe impl Send for Device {}
unsafe impl Sync for Device {}

impl Device {
pub fn name(&self) -> Result<String, DeviceNameError> {
let device = self
.immdevice()
.ok_or(DeviceNameError::from(BackendSpecificError {
description: "device not found while getting name".to_string(),
}))?;

unsafe {
// Open the device's property store.
let property_store = self
.device
let property_store = device
.OpenPropertyStore(STGM_READ)
.expect("could not open property store");

Expand Down Expand Up @@ -325,13 +387,43 @@ impl Device {
#[inline]
fn from_immdevice(device: Audio::IMMDevice) -> Self {
Device {
device,
device: DeviceType::Specific(device),
future_audio_client: Arc::new(Mutex::new(None)),
}
}

#[inline]
fn default_output() -> Self {
Device {
device: DeviceType::DefaultOutput,
future_audio_client: Arc::new(Mutex::new(None)),
}
}

#[inline]
fn default_input() -> Self {
Device {
device: DeviceType::DefaultInput,
future_audio_client: Arc::new(Mutex::new(None)),
}
}

pub fn immdevice(&self) -> &Audio::IMMDevice {
&self.device
pub fn immdevice(&self) -> Option<Audio::IMMDevice> {
match &self.device {
DeviceType::DefaultOutput => unsafe {
get_enumerator()
.0
.GetDefaultAudioEndpoint(Audio::eRender, Audio::eConsole)
.ok()
},
DeviceType::DefaultInput => unsafe {
get_enumerator()
.0
.GetDefaultAudioEndpoint(Audio::eCapture, Audio::eConsole)
.ok()
},
DeviceType::Specific(device) => Some(device.clone()),
}
}

/// Ensures that `future_audio_client` contains a `Some` and returns a locked mutex to it.
Expand All @@ -343,10 +435,43 @@ impl Device {
return Ok(lock);
}

// When using virtual default devices, we use `ActivateAudioInterfaceAsync` to get
// an `IAudioClient` for the default output or input device. When retrieved this way,
// streams will be automatically rerouted if the default device is changed.
//
// Otherwise, we use `Activate` to get an `IAudioClient` for the current device.

#[cfg(feature = "wasapi-virtual-default-devices")]
let audio_client: Audio::IAudioClient = unsafe {
// can fail if the device has been disconnected since we enumerated it, or if
// the device doesn't support playback for some reason
self.device.Activate(Com::CLSCTX_ALL, None)?
match &self.device {
DeviceType::DefaultOutput => {
let default_audio = Com::StringFromIID(&Audio::DEVINTERFACE_AUDIO_RENDER)?;
let result = activate_audio_interface_sync(default_audio);
Com::CoTaskMemFree(Some(default_audio.as_ptr() as _));
result?
}
DeviceType::DefaultInput => {
let default_audio = Com::StringFromIID(&Audio::DEVINTERFACE_AUDIO_CAPTURE)?;
let result = activate_audio_interface_sync(default_audio);
Com::CoTaskMemFree(Some(default_audio.as_ptr() as _));
result?
}
DeviceType::Specific(device) => {
// can fail if the device has been disconnected since we enumerated it, or if
// the device doesn't support playback for some reason
device.Activate(Com::CLSCTX_ALL, None)?
}
}
};

#[cfg(not(feature = "wasapi-virtual-default-devices"))]
let audio_client = unsafe {
self.immdevice()
.ok_or(windows::core::Error::new(
Audio::AUDCLNT_E_DEVICE_INVALIDATED,
"device not found while getting audio client",
))?
.Activate(Com::CLSCTX_ALL, None)?
};

*lock = Some(IAudioClientWrapper(audio_client));
Expand Down Expand Up @@ -515,8 +640,14 @@ impl Device {
}

pub(crate) fn data_flow(&self) -> Audio::EDataFlow {
let endpoint = Endpoint::from(self.device.clone());
endpoint.data_flow()
match &self.device {
DeviceType::DefaultOutput => Audio::eRender,
DeviceType::DefaultInput => Audio::eCapture,
DeviceType::Specific(device) => {
let endpoint = Endpoint::from(device.clone());
endpoint.data_flow()
}
}
}

pub fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
Expand Down Expand Up @@ -766,40 +897,47 @@ impl Device {
impl PartialEq for Device {
#[inline]
fn eq(&self, other: &Device) -> bool {
// Use case: In order to check whether the default device has changed
// the client code might need to compare the previous default device with the current one.
// The pointer comparison (`self.device == other.device`) don't work there,
// because the pointers are different even when the default device stays the same.
//
// In this code section we're trying to use the GetId method for the device comparison, cf.
// https://docs.microsoft.com/en-us/windows/desktop/api/mmdeviceapi/nf-mmdeviceapi-immdevice-getid
unsafe {
struct IdRAII(windows::core::PWSTR);
/// RAII for device IDs.
impl Drop for IdRAII {
fn drop(&mut self) {
unsafe { Com::CoTaskMemFree(Some(self.0 .0 as *mut _)) }
}
}
// GetId only fails with E_OUTOFMEMORY and if it does, we're probably dead already.
// Plus it won't do to change the device comparison logic unexpectedly.
let id1 = self.device.GetId().expect("cpal: GetId failure");
let id1 = IdRAII(id1);
let id2 = other.device.GetId().expect("cpal: GetId failure");
let id2 = IdRAII(id2);
// 16-bit null-terminated comparison.
let mut offset = 0;
loop {
let w1: u16 = *(id1.0).0.offset(offset);
let w2: u16 = *(id2.0).0.offset(offset);
if w1 == 0 && w2 == 0 {
return true;
}
if w1 != w2 {
return false;
match (&self.device, &other.device) {
(DeviceType::DefaultOutput, DeviceType::DefaultOutput) => true,
(DeviceType::DefaultInput, DeviceType::DefaultInput) => true,
(DeviceType::Specific(dev1), DeviceType::Specific(dev2)) => {
// Use case: In order to check whether the default device has changed
// the client code might need to compare the previous default device with the current one.
// The pointer comparison (`self.device == other.device`) don't work there,
// because the pointers are different even when the default device stays the same.
//
// In this code section we're trying to use the GetId method for the device comparison, cf.
// https://docs.microsoft.com/en-us/windows/desktop/api/mmdeviceapi/nf-mmdeviceapi-immdevice-getid
unsafe {
struct IdRAII(windows::core::PWSTR);
/// RAII for device IDs.
impl Drop for IdRAII {
fn drop(&mut self) {
unsafe { Com::CoTaskMemFree(Some(self.0 .0 as *mut _)) }
}
}
// GetId only fails with E_OUTOFMEMORY and if it does, we're probably dead already.
// Plus it won't do to change the device comparison logic unexpectedly.
let id1 = dev1.GetId().expect("cpal: GetId failure");
let id1 = IdRAII(id1);
let id2 = dev2.GetId().expect("cpal: GetId failure");
let id2 = IdRAII(id2);
// 16-bit null-terminated comparison.
let mut offset = 0;
loop {
let w1: u16 = *(id1.0).0.offset(offset);
let w2: u16 = *(id2.0).0.offset(offset);
if w1 == 0 && w2 == 0 {
return true;
}
if w1 != w2 {
return false;
}
offset += 1;
}
}
offset += 1;
}
_ => false,
}
}
}
Expand Down Expand Up @@ -911,23 +1049,12 @@ impl Iterator for Devices {
}
}

fn default_device(data_flow: Audio::EDataFlow) -> Option<Device> {
unsafe {
let device = get_enumerator()
.0
.GetDefaultAudioEndpoint(data_flow, Audio::eConsole)
.ok()?;
// TODO: check specifically for `E_NOTFOUND`, and panic otherwise
Some(Device::from_immdevice(device))
}
}

pub fn default_input_device() -> Option<Device> {
default_device(Audio::eCapture)
Some(Device::default_input())
}

pub fn default_output_device() -> Option<Device> {
default_device(Audio::eRender)
Some(Device::default_output())
}

/// Get the audio clock used to produce `StreamInstant`s.
Expand Down
Loading