v3.4: handle-leak fix, Realtek ASIO block, device notifications, quick profile switch, hotkey announcements
Freezes the v3.4 feature set (everything since the v3.3 public release): - Fix the receiver handle leak: the 3-second device-refresh timer reopened the configured ASIO driver every tick, and Realtek's ASIO driver leaks Event+Mutant handles on every open. Cache the ASIO probe per driver so it is opened once. - Realtek ASIO block: detect a Realtek ASIO driver, offer once to disable it, and never touch it again if disabled; Options-menu toggle to reverse. Global config. - Device hot-plug is event-driven (AudioDeviceChangeNotifier) instead of a 3s poll; debounced refresh, falls back to polling if registration fails. - Quick profile switch: new global hotkey opens an NVDA-friendly popup of all profiles (current marked); Enter/click switches; plays a new "profile menu open" cue with Preferences mute + custom-sound. - Announce assigned global hotkeys on the controls/menu items they drive (NVDA reads "press X anywhere"). File > Open already had Ctrl+O. - Held-back changes folded in: config-folder migration, codec-column fix, Tailscale endpoint network-prune, empty-password guard, and the handle-leak diagnostics (ProcessSelfMeter, HandleTypeProbe). Version bumped to 3.4.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
fc451ba3e3
commit
dd70613017
@@ -0,0 +1,47 @@
|
||||
using NAudio.CoreAudioApi;
|
||||
using NAudio.CoreAudioApi.Interfaces;
|
||||
|
||||
namespace RemSound.App;
|
||||
|
||||
/// <summary>
|
||||
/// Fires a callback whenever the set of Windows audio endpoints changes — a device is added,
|
||||
/// removed, changes state (plugged / unplugged / enabled / disabled), or the default device
|
||||
/// changes. This replaces the pre-v3.4 3-second polling of the device lists: instead of
|
||||
/// re-enumerating every tick whether or not anything changed, RemSound re-reads the device lists
|
||||
/// only when Windows actually tells us the device set changed.
|
||||
///
|
||||
/// The callbacks arrive on a COM thread, so the consumer (<see cref="MainForm"/>) marshals to the
|
||||
/// UI thread and debounces — a single hot-plug typically fires several of these in quick
|
||||
/// succession (state-changed + added + default-changed), which collapse into one refresh.
|
||||
///
|
||||
/// Property-value changes (volume nudges, format tweaks) are deliberately ignored: they fire
|
||||
/// constantly and never change the device <em>set</em>, so reacting to them would defeat the
|
||||
/// whole point of going event-driven.
|
||||
/// </summary>
|
||||
internal sealed class AudioDeviceChangeNotifier : IMMNotificationClient, IDisposable
|
||||
{
|
||||
private readonly MMDeviceEnumerator enumerator = new();
|
||||
private readonly Action onChange;
|
||||
private bool registered;
|
||||
|
||||
public AudioDeviceChangeNotifier(Action onChange)
|
||||
{
|
||||
this.onChange = onChange;
|
||||
enumerator.RegisterEndpointNotificationCallback(this);
|
||||
registered = true;
|
||||
}
|
||||
|
||||
public void OnDeviceStateChanged(string deviceId, DeviceState newState) => onChange();
|
||||
public void OnDeviceAdded(string pwstrDeviceId) => onChange();
|
||||
public void OnDeviceRemoved(string deviceId) => onChange();
|
||||
public void OnDefaultDeviceChanged(DataFlow flow, Role role, string defaultDeviceId) => onChange();
|
||||
public void OnPropertyValueChanged(string pwstrDeviceId, PropertyKey key) { /* ignore — too chatty, no set change */ }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
try { if (registered) enumerator.UnregisterEndpointNotificationCallback(this); }
|
||||
catch { /* COM teardown race on shutdown — harmless */ }
|
||||
registered = false;
|
||||
try { enumerator.Dispose(); } catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user