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:
co-authored by
Claude Opus 4.8
parent
946e6f4be4
commit
4dbe9a47a0
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user