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
@@ -24,9 +24,9 @@ public sealed class ProfileStore
|
||||
public ProfileStore()
|
||||
{
|
||||
var machineFolder = SanitiseFsName(Environment.MachineName);
|
||||
// 2026-06-07: profiles live under config\profiles\<machine>\ (was <exe>\profiles\<machine>\).
|
||||
// 2026-06-10: profiles live under "user settings and logs"\profiles\<machine>\.
|
||||
// AppConfig.MigrateLegacyLayoutIfNeeded moves any pre-existing profiles here at startup.
|
||||
baseDir = Path.Combine(AppContext.BaseDirectory, "config", "profiles", machineFolder);
|
||||
baseDir = Path.Combine(AppConfig.ProfilesBaseDirectory, machineFolder);
|
||||
try { Directory.CreateDirectory(baseDir); }
|
||||
catch { /* permissions; List/Save will surface this when actually used */ }
|
||||
}
|
||||
@@ -128,7 +128,26 @@ public sealed class ProfileStore
|
||||
Directory.CreateDirectory(baseDir);
|
||||
var path = PathFor(profile.Title);
|
||||
var json = JsonSerializer.Serialize(profile, new JsonSerializerOptions { WriteIndented = true });
|
||||
File.WriteAllText(path, json);
|
||||
WriteFileAtomic(path, json);
|
||||
}
|
||||
|
||||
/// <summary>Write text crash-safely: write a sibling temp file, then atomically move it over the
|
||||
/// target. A crash, power-loss, or the updater force-closing mid-write then leaves either the old
|
||||
/// file or the complete new one — never a truncated file that the catch-all loaders would
|
||||
/// silently read as a blank profile.</summary>
|
||||
private static void WriteFileAtomic(string path, string contents)
|
||||
{
|
||||
var tmp = path + ".tmp";
|
||||
try
|
||||
{
|
||||
File.WriteAllText(tmp, contents);
|
||||
File.Move(tmp, path, overwrite: true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
try { if (File.Exists(tmp)) File.Delete(tmp); } catch { /* ignore */ }
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Deletes the profile by title. Returns true if a file was removed,
|
||||
@@ -171,7 +190,7 @@ public sealed class ProfileStore
|
||||
if (profile is null) return false;
|
||||
profile.Title = newTitle;
|
||||
var json = JsonSerializer.Serialize(profile, new JsonSerializerOptions { WriteIndented = true });
|
||||
File.WriteAllText(newPath, json);
|
||||
WriteFileAtomic(newPath, json);
|
||||
if (!string.Equals(oldPath, newPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
File.Delete(oldPath);
|
||||
|
||||
Reference in New Issue
Block a user