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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **AudioWorklet**: Fix stale output when the data callback grows Wasm memory.
- **JACK**: Channel enumeration is capped at the physical system port count again.
- **WASAPI**: Device enumeration no longer panics if the COM enumerator fails to initialize.
- **WASAPI**: Revert "Default device changes no longer report `DeviceChanged`" as it was misinformed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this should be a bit more succinct: just document observable behavior, no need to pony up about being wrong or right.

Default device streams **do** reroute automatically.

## [0.18.2] - 2026-08-16

Expand Down
32 changes: 9 additions & 23 deletions src/host/wasapi/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ fn get_current_default(flow: Audio::EDataFlow) -> Option<Audio::IMMDevice> {
super::device::current_default_endpoint(flow)
}

/// Fires a Windows auto-reset event when the system default audio device changes.
/// Fires a Windows auto-reset event when the system default audio device changes, allowing
/// the stream run loop to deliver `ErrorKind::DeviceChanged` to the caller.
pub(crate) struct DefaultDeviceMonitor {
enumerator: Audio::IMMDeviceEnumerator,
client: Audio::IMMNotificationClient,
event: Foundation::HANDLE,
pub(crate) flow: Audio::EDataFlow,
pub(crate) pending_device_changed: Arc<AtomicBool>,
}

Expand Down Expand Up @@ -79,7 +79,6 @@ impl DefaultDeviceMonitor {
enumerator,
client,
event,
flow,
pending_device_changed,
})
}
Expand Down Expand Up @@ -211,8 +210,9 @@ pub struct Stream {
// QueryPerformanceFrequency result, cached at construction (constant for the system lifetime).
qpc_frequency: u64,

// Present for default-device streams. Dropped after the run thread joins, ensuring the
// HANDLE is not waited on when it is closed.
// Present for default-device streams; fires `ErrorKind::DeviceChanged` when the system
// default changes. Dropped after the run thread joins, ensuring the HANDLE is not
// waited on when it is closed.
_default_device_monitor: Option<DefaultDeviceMonitor>,

// Latch that ensures no callbacks fire before the caller receives the `Stream` handle.
Expand Down Expand Up @@ -252,12 +252,10 @@ struct RunContext {

commands: Receiver<Command>,

// Set by a device-change notification callback when SetEvent fails.
// Set by a device-change notification callback when SetEvent fails. The audio loop delivers
// DeviceChanged on its next iteration.
pending_device_changed: Option<Arc<AtomicBool>>,

// Set when this stream tracks the default device, rather than a pinned one.
default_device_flow: Option<Audio::EDataFlow>,

// Owned here so the worker thread closes it on exit in a self-join case.
pending_scheduled_event: Foundation::HANDLE,
}
Expand Down Expand Up @@ -342,13 +340,11 @@ impl Stream {
let pending_device_changed = default_device_monitor
.as_ref()
.map(|m| m.pending_device_changed.clone());
let default_device_flow = default_device_monitor.as_ref().map(|m| m.flow);
let run_context = RunContext {
handles,
stream: stream_inner,
commands: rx,
pending_device_changed,
default_device_flow,
pending_scheduled_event,
};

Expand Down Expand Up @@ -414,13 +410,11 @@ impl Stream {
let pending_device_changed = default_device_monitor
.as_ref()
.map(|m| m.pending_device_changed.clone());
let default_device_flow = default_device_monitor.as_ref().map(|m| m.flow);
let run_context = RunContext {
handles,
stream: stream_inner,
commands: rx,
pending_device_changed,
default_device_flow,
pending_scheduled_event,
};

Expand Down Expand Up @@ -727,14 +721,6 @@ fn boost_current_thread_priority(
}
}

// WASAPI never rebinds the IAudioClient, so report what's actually true instead of DeviceChanged.
fn default_device_change_error(flow: Option<Audio::EDataFlow>) -> Error {
match flow.and_then(get_current_default) {
None => ErrorKind::DeviceNotAvailable.into(),
Some(_) => ErrorKind::StreamInvalidated.into(),
}
}

fn process_commands_and_await_signal(
run_context: &mut RunContext,
error_callback: &ErrorCallbackArc,
Expand All @@ -753,7 +739,7 @@ fn process_commands_and_await_signal(
if flag.swap(false, Ordering::Relaxed) {
emit_error(
error_callback,
default_device_change_error(run_context.default_device_flow),
Error::with_message(ErrorKind::DeviceChanged, "Default audio device changed"),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This seems to throw out more than #1339 needs. StreamInvalidated is wrong when a replacement device exists, but default_device_change_error's other branch serves a different purpose:

OnDeviceStateChanged and OnDeviceRemoved only SetEvent when get_current_default(self.flow).is_none() (see the comment above them), so they only ever wake this code up for the no-replacement case. After this revert that case gets reported as DeviceChanged too, which contradicts its definition: "the stream remains active and no rebuild is required." There's nothing to route to there, so that isn't true.

I think the fix should be smaller than a full revert: keep default_device_change_error, just fix the one arm that was actually wrong:

fn default_device_change_error(flow: Option<Audio::EDataFlow>) -> Error {
    match flow.and_then(get_current_default) {
        None => ErrorKind::DeviceNotAvailable.into(),
        Some(_) => ErrorKind::DeviceChanged.into(), // was StreamInvalidated
    }
}

);
}
}
Expand All @@ -774,7 +760,7 @@ fn process_commands_and_await_signal(
if handle_idx >= 2 {
emit_error(
error_callback,
default_device_change_error(run_context.default_device_flow),
Error::with_message(ErrorKind::DeviceChanged, "Default audio device changed"),
);
return ControlFlow::Continue(false);
}
Expand Down