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
+76
View File
@@ -0,0 +1,76 @@
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace RemSound.App;
/// <summary>
/// Shows a dialog FRONT-AND-CENTRE with keyboard focus, WITHOUT disturbing the main window — it can
/// stay minimised in the tray the whole time. We give the dialog a momentary, top-most, off-taskbar
/// 1×1 owner window at screen centre and force THAT owner to the foreground (the AttachThreadInput
/// dance bypasses Windows' focus-stealing lock), so the modal dialog opens on top with focus and a
/// screen reader lands on it wherever RemSound happens to be sitting. Used for every warning/notice
/// the app raises (mic-privacy, Realtek, About-after-update, config-moved), so a minimised RemSound
/// never leaves a blind user with a dialog dinging away behind everything.
/// </summary>
internal static class ForegroundDialog
{
/// <summary>Run <paramref name="show"/> with a foreground 1×1 owner; returns its result.</summary>
public static T Show<T>(Func<IWin32Window, T> show)
{
var area = Screen.PrimaryScreen?.WorkingArea ?? new Rectangle(0, 0, 800, 600);
using var owner = new Form
{
ShowInTaskbar = false,
FormBorderStyle = FormBorderStyle.None,
StartPosition = FormStartPosition.Manual,
Size = new Size(1, 1),
Location = new Point(area.X + area.Width / 2, area.Y + area.Height / 2),
TopMost = true,
};
owner.Show();
ForceForeground(owner.Handle);
try { return show(owner); }
finally { try { owner.Close(); } catch { /* ignore */ } }
}
/// <summary>Void convenience overload.</summary>
public static void Show(Action<IWin32Window> show) =>
Show<object?>(owner => { show(owner); return null; });
/// <summary>Force <paramref name="hWnd"/> to the foreground even when RemSound isn't the active
/// app. A plain SetForegroundWindow from a background process is refused by Windows; attaching
/// our input queue to the current foreground thread for the call lifts that restriction. The
/// owner is top-most regardless, so this is belt-and-braces for focus.</summary>
private static void ForceForeground(IntPtr hWnd)
{
try
{
var foreThread = GetWindowThreadProcessId(GetForegroundWindow(), out _);
var thisThread = GetCurrentThreadId();
if (foreThread != 0 && foreThread != thisThread)
{
AttachThreadInput(foreThread, thisThread, true);
try
{
BringWindowToTop(hWnd);
SetForegroundWindow(hWnd);
}
finally { AttachThreadInput(foreThread, thisThread, false); }
}
else
{
BringWindowToTop(hWnd);
SetForegroundWindow(hWnd);
}
}
catch { /* best-effort; the owner is top-most anyway */ }
}
[DllImport("user32.dll")] private static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);
[DllImport("user32.dll")] private static extern uint GetCurrentThreadId();
[DllImport("user32.dll")] private static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);
[DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")] private static extern bool BringWindowToTop(IntPtr hWnd);
}