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
+92 -17
View File
@@ -24,10 +24,19 @@ internal static class Program
ApplicationConfiguration.Initialize();
// Relocate any pre-2026-06-07 config/profiles into config\ before anything reads them.
// Idempotent and best-effort; also upgrades users coming from an older build. The result
// is shown to the user once (after the single-instance guard) if files actually moved.
// Consolidate every older layout (loose files, or the interim config\ folder) into the single
// "user settings and logs" folder before anything reads config/profiles/logs. Idempotent +
// best-effort; upgrades users from any older build. Shown to the user once if files moved.
var layoutMigration = RemSound.Core.AppConfig.MigrateLegacyLayoutIfNeeded();
// Move the cue sounds into that folder too — seeded from the shipped defaults (see method).
ConsolidateSounds();
// Remove cue WAVs (and their .sfk peak files) left loose in the install ROOT by pre-
// 2026-05-28 builds, where the cues lived next to RemSound.exe before they moved into
// sounds\. A robocopy update copies the new sounds\ tree but uses /E (not /PURGE), so it
// never deletes these orphans — they just linger in the root. Best-effort + idempotent:
// a no-op once they're gone. 2026-06-08.
CleanUpLegacyRootSounds();
// Single-instance guard. RemSound must never run as two copies at once: with the
// auto-updater relaunching the app, a copy that didn't exit cleanly used to leave two
@@ -84,7 +93,7 @@ internal static class Program
// here, after the guard and before the profile picker, so the user reads it once up front.
if (layoutMigration.MovedAnything)
{
ShowLayoutMigrationNotice(layoutMigration);
ShowLayoutMigrationNotice();
}
// Outer loop: lets ProfileManagementDialog change the profiles folder mid-session.
@@ -248,28 +257,94 @@ internal static class Program
}
/// <summary>One-time, Windows-native notice telling the user their config/profiles were moved
/// into the new <c>config\</c> folder. Only called when a real migration happened. TaskDialog
/// into the new "user settings and logs" folder. Only called when a real migration happened. TaskDialog
/// (not a hand-rolled Form) so a screen reader reads the whole message automatically.</summary>
private static void ShowLayoutMigrationNotice(RemSound.Core.AppConfig.LayoutMigrationResult migration)
private static void ShowLayoutMigrationNotice()
{
var moved = new System.Collections.Generic.List<string>();
if (migration.MovedGlobalConfig) moved.Add("- Your settings are now in: config\\global config.json");
if (migration.MovedProfiles) moved.Add("- Your saved profiles are now in: config\\profiles\\");
var page = new TaskDialogPage
{
Caption = "RemSound settings location",
Heading = "Your settings now live in a \"config\" folder",
Text = "To keep the RemSound folder tidy, this update moved your existing settings into a new "
+ "\"config\" folder inside RemSound:\n\n"
+ string.Join("\n", moved)
+ "\n\nNothing was lost and RemSound works exactly as before. You will only see this message once.",
Caption = "RemSound files location",
Heading = "Your RemSound files have moved into one folder",
Text = "To keep the RemSound folder tidy and stop updates from ever touching your own files, "
+ "this update moved everything this machine owns into a single folder inside RemSound "
+ "called \"user settings and logs\":\n\n"
+ "- Your settings (global config)\n"
+ "- Your saved profiles\n"
+ "- Your logs\n"
+ "- Your cue sounds\n\n"
+ "Nothing was lost and RemSound works exactly as before. From now on, RemSound updates "
+ "leave that folder completely untouched. You will only see this message once.",
Icon = TaskDialogIcon.Information,
Buttons = { TaskDialogButton.OK },
DefaultButton = TaskDialogButton.OK,
AllowCancel = true,
};
try { TaskDialog.ShowDialog(page); }
// Give the notice a momentary top-most, foreground owner so it opens FRONT and CENTRE —
// RemSound may have launched straight into the tray (auto-start / start-minimised), and a
// parent-less TaskDialog can otherwise open behind everything where a screen-reader user
// can't read it.
try { ForegroundDialog.Show(owner => TaskDialog.ShowDialog(owner, page)); }
catch { /* a notice must never stop RemSound from starting */ }
}
/// <summary>Delete cue WAVs and their .sfk peak files left loose in the install ROOT by
/// pre-2026-05-28 builds (the cues moved into <c>sounds\</c> then; a robocopy update copies
/// the new tree but never removes the old root copies). Best-effort and idempotent — runs
/// every launch and no-ops once the orphans are gone. Only the known default cue names are
/// touched, never anything else in the folder.</summary>
private static void CleanUpLegacyRootSounds()
{
try
{
var root = AppContext.BaseDirectory;
string[] cueBaseNames =
{
"connect", "disconnect", "record start", "record stop",
"save", "profile", "profile menu open", "update",
};
foreach (var baseName in cueBaseNames)
{
foreach (var fileName in new[] { baseName + ".wav", baseName + ".sfk", baseName + ".wav.sfk" })
{
try
{
var path = Path.Combine(root, fileName);
if (File.Exists(path)) File.Delete(path);
}
catch { /* a locked / unremovable file must never stop startup */ }
}
}
}
catch { /* never let cleanup disturb startup */ }
}
/// <summary>Consolidate the cue WAVs into the per-user sounds folder. The release ships the
/// default cues in <c>&lt;exe&gt;\sounds\</c>; this copies any cue MISSING from the per-user
/// <c>...\user settings and logs\sounds\</c> across (so a fresh install, or a release that adds a
/// new cue, gets seeded) WITHOUT overwriting one already there (so the user's own cue files
/// survive), then removes the shipped folder to keep the install root tidy. The app reads cues
/// only from the per-user folder, which the updater leaves untouched — so a user's custom cue
/// WAVs are no longer clobbered by an update. Best-effort + idempotent. 2026-06-10.</summary>
private static void ConsolidateSounds()
{
try
{
var userSounds = AppConfig.SoundsDirectory;
Directory.CreateDirectory(userSounds);
var shippedSounds = Path.Combine(AppContext.BaseDirectory, "sounds");
if (!Directory.Exists(shippedSounds)) return;
foreach (var src in Directory.GetFiles(shippedSounds))
{
try
{
var dest = Path.Combine(userSounds, Path.GetFileName(src));
if (!File.Exists(dest)) File.Copy(src, dest);
}
catch { /* one unreadable cue mustn't stop the rest */ }
}
try { Directory.Delete(shippedSounds, recursive: true); }
catch { /* leave it if locked — the app reads the per-user copy anyway */ }
}
catch { /* never let cue consolidation disturb startup */ }
}
}