Release v3.5: USB-card recovery, per-card adaptive buffer, one user folder, audit fixes

- Recover an unplugged/replugged output sound card automatically (issue #5):
  detect the dead WASAPI device and remember the receive-output selection so it
  re-ticks and re-opens when the card returns.
- Adaptive per-card WASAPI buffer target sized to each card's pull chunk, held
  stable so it never flits about under CPU/network load.
- Consolidate all per-user data (config, profiles, logs, sounds) into one
  "user settings and logs" folder; migrate every older layout; exclude it from
  the updater so custom cue sounds now survive updates.
- Mic-privacy detector: warn once when a Windows-blocked mic is switched on, or a
  profile loads with one already on.
- All warning/notice dialogs now come to the foreground even when minimised.
- Apply volume + mute on profile load (were saved but not restored).
- Crash-safe (atomic) profile/config saves.
- Fix two resource leaks (push-mode capture MMDevice; UPnP DeviceFound handler).
- Remove dead code (baseline-diff machinery, dead ASIO probes, no-op stubs).
- Docs: readme.html, MANUAL.md, About-box changelog and release notes for v3.5.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-06-10 11:34:17 +01:00
co-authored by Claude Opus 4.8
parent 946e6f4be4
commit 4dbe9a47a0
21 changed files with 681 additions and 305 deletions
-28
View File
@@ -330,32 +330,4 @@ internal sealed class AsioCaptureBackend : ICaptureBackend
// copy + mix loop, or is it encode + sendto".
if (diag) Interlocked.Add(ref cumulativeCaptureTicks, Stopwatch.GetTimestamp() - workStart);
}
/// <summary>Returns the names of all installed ASIO drivers, or an empty list if NAudio
/// can't find any. Exposed for the App's driver picker UI.</summary>
public static IReadOnlyList<string> EnumerateDriverNames()
{
try { return AsioOut.GetDriverNames().ToList(); }
catch { return []; }
}
/// <summary>
/// Briefly opens the named ASIO driver to query its channel counts, then disposes. Single
/// driver instance held for ~50 ms while the COM object reads its channel info — does not
/// claim the device for streaming. Returns (in,out) = (-1,-1) on any failure (driver not
/// installed, busy with another app, etc.). Used by the App to populate channel-pair lists
/// in ASIO mode without holding the driver open between user actions.
/// </summary>
public static (int inputChannels, int outputChannels) ProbeChannelCounts(string driverName)
{
try
{
using var asio = new AsioOut(driverName);
return (asio.DriverInputChannelCount, asio.DriverOutputChannelCount);
}
catch
{
return (-1, -1);
}
}
}
-10
View File
@@ -91,16 +91,6 @@ public static class AsioDeviceProbe
return new AsioDriverProbeResult(-1, -1, [], []);
}
}
/// <summary>
/// Backwards-compatibility shim around <see cref="ProbeDriverInfo"/> for callers that only
/// need channel counts.
/// </summary>
public static (int inputChannels, int outputChannels) ProbeChannelCounts(string driverName)
{
var info = ProbeDriverInfo(driverName);
return (info.InputChannelCount, info.OutputChannelCount);
}
}
public sealed record AsioDriverProbeResult(
@@ -58,6 +58,7 @@ internal sealed class PushModeWasapiBackend : ICaptureBackend
private readonly object gate = new();
private WasapiCapture? capture;
private MMDevice? captureDevice; // the device backing capture/keepAlive; WE own it and must dispose it (NAudio's WasapiCapture never does)
private SilentRenderKeepAlive? keepAlive;
private CaptureSourceSpec? activeSpec;
private string? captureFormatDescription;
@@ -140,6 +141,7 @@ internal sealed class PushModeWasapiBackend : ICaptureBackend
{
using var enumerator = new MMDeviceEnumerator();
var device = enumerator.GetDevice(spec.DeviceId);
captureDevice = device; // hold it for disposal in StopInternal — see field comment
capture = spec.Kind == CaptureKind.Loopback
? new LowLatencyWasapiLoopbackCapture(device, audioBufferMilliseconds: CaptureBufferMs)
@@ -241,6 +243,14 @@ internal sealed class PushModeWasapiBackend : ICaptureBackend
try { keepAlive.Dispose(); } catch { /* ignore */ }
keepAlive = null;
}
// Dispose the device AFTER capture + keepAlive (both hold its COM state). NAudio's
// WasapiCapture keeps no reference to the MMDevice and never disposes it, so without this the
// device's COM/handle state leaks on every start/stop/switch — the WASAPI handle-leak fingerprint.
if (captureDevice is not null)
{
try { captureDevice.Dispose(); } catch { /* ignore */ }
captureDevice = null;
}
resampler = null;
activeSpec = null;
}