Skip to content

Conversation

philpax
Copy link

@philpax philpax commented Oct 1, 2025

Fixes #740 (mostly). Updated version of #754 that aims to address the issues mentioned in that PR.

Background

When you currently play some audio on the default device using cpal, the default device is fetched once, and then a stream is created from that device to play audio. However, changing the default device will lead to that stream continuing to play on the original device, and removing the device entirely (e.g. unplugging headphones) will result in the stream dying as it no longer has a device to output to.

I spent several hours trying to fix this properly - that is, trying to update the device and rebuild the stream when it changes - and stopped when I realised that a significant amount of the WASAPI stream code would need to be revised to handle a changing device. For those curious, you can see my experimental work here. The goal was to replicate the flow of this Chromium code, which implements this logic.

The Imperfect Fix

However, you don't actually need to do any of that if you don't care about supporting <Windows 8. Windows 8 introduced ActivateAudioInterfaceAsync, and with it, virtual device interfaces. ActivateAudioInterfaceAsync can be used instead of Audio::IMMDevice::Activate to generate a Audio::IAudioClient that will automatically reroute audio for you if the device changes.

This is what the previous PR did, and it works great. On top of that PR, this PR:

  • brings it up to date
  • pushes these changes into a default-on feature
  • makes name use the current default device
  • does some minor cleanup

Ramifications

I've added a default-on feature, wasapi-virtual-default-devices, that adds support for this. Users that need to support older versions of Windows can turn this feature off, and it will behave as the current status quo does. (I'm not actually sure what happens if you have this feature on for an older Windows: in an ideal world, it does nothing, and newer Windows users can enjoy virtual devices. Please let me know if you've tested this.)

One downside of this is that I had to bump the minimum windows version to 0.61. This is because 0.61 removed the previously-explicitly-required implement feature in 0.61, and I can't see any way to conditionally include that feature for older versions of windows. cargo will refuse to resolve the windows dependency with a feature that doesn't exist for the version it's resolving to. I'm happy to drop it back down if anyone can think of a workaround for this.

@philpax philpax force-pushed the audio-device-fix-updated branch from 2517c28 to 0830b3f Compare October 1, 2025 18:44
Copy link
Member

@roderickvd roderickvd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! I don't really see a way around the windows crate version problem. How much of a problem that is, kind of depends on the community... don't know how much it'll hurt to bump the minimum to v0.61. If anyone feels strongly about that, do comment here.

#
# 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 = []
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First, a documentation suggestion:

# Enable virtual default devices for WASAPI. When enabled:
# - Audio automatically reroutes when the default device changes
# - Streams survive device changes (e.g., plugging in headphones)
# - Requires Windows 8 or later
#
# Disable this feature if supporting Windows 7 or earlier.

Second, I wonder if we should invert the feature and rename it to windows-legacy (disabled by default). I'm not so sure if "virtual default devices" clearly communicates its intention. What do you think?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good shout on the docs, will do first.

I think inverting the feature would be more ergonomic, but the Cargo docs suggest that features should always be additive:

A consequence of this is that features should be additive. That is, enabling a feature should not disable functionality, and it should usually be safe to enable any combination of features. A feature should not introduce a SemVer-incompatible change.

I think it's probably fine - I don't see a way for this to cause a conflict with another package, as you'd want this to be controlled at the application level anyway - but I'm not sure. For now, I'll just action the doc change, but I'm open to the inversion if you think it's compatible with the norms of the ecosystem.

Feature name: Hmm, yeah, I'm not sold on it either; it was what first came to mind to cover the concept of activating the output with a virtual default device, but it's not the most self-descriptive thing. Maybe wasapi-default-device-autorouting or something? (I'm using wasapi because it looks like cpal supports ASIO as well, but I'm happy to change to windows if that sounds better)

impl Device {
pub fn name(&self) -> Result<String, DeviceNameError> {
let device = self
.immdevice()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How much of a performance hit does this take when enumerating? Would it make sense to cache it during enumeration or is that a negligible thing?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't checked, but I'd imagine it should be pretty negligible. I did consider caching it, but opted against it because the device could change, and we can't detect that without setting up a IMMNotificationClient (which is a bit tedious, as you can see from master...philpax:cpal:audio-device-fix#diff-ce1e8d650aace89ec56c28e4525e838476cffdbe51fe4447d8181936b5ec6724).

My gut feeling is that this is probably fine for now, and it can be revisited if it ends up being a problem.

"Win32_UI_Shell_PropertiesSystem",
] }
# Explicitly depend on windows-core for use with the `windows::core::implement` macro.
windows-core = "*"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cargo publish will reject wildcard versions, please specify what we need.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... yes, that is a problem. I'll see what I can do; the problem I saw was that the CI would fail to resolve windows-core to the correct version, but that might be fine with the bodge I put in there. Will test.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, this is a problem. cargo will not resolve windows-core to the same version of windows's windows-core if I match the version constraints or use a lax constraint like ^0.61.

I can see three options here:

  1. Restrict the version to a single known version (0.61 or 0.62)
  2. Ask windows-rs to add support for referencing windows::core, instead of windows_core, somehow: Cannot use implement macro microsoft/windows-rs#3568 (comment). Even if this happened, it would have to be another windows-rs release.
  3. Ask a Cargo wizard if there are any solutions for getting *-like behaviour in a way that can still be cargo publish-ed

I'll ask around regarding 3, and ask in that windows-rs issue for future support, but 1 may be necessary in the meantime :S

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Respect Windows output device selection
3 participants