v3.4 refinements: WASAPI drift correction, startup-dialog sequencing, quick-switch polish, docs
Builds on the v3.4 freeze (dd70613) with the fixes and tuning from live testing,
plus the v3.4 documentation pass.
Audio (receiver):
- Per-device WASAPI drift correction in MultiOutputPlayout. A pull-side resampler
(mirroring SessionPlayout's proven corrector) holds each output device's buffer at
a fixed low depth, cancelling the slow clock drift that made WASAPI peers "lag
apart" over long sessions. Feed-forward clock-ratio measurement plus a gentle
depth-restoring term; the first measurement window is discarded because WASAPI
start-up priming poisons it. ASIO already self-corrected; this brings WASAPI level.
- Output device buffer requested at 5 ms (WASAPI clamps it up to the device's minimum
period, ~10 ms) instead of 15 ms, since the corrector keeps it fed — a free saving.
UI / accessibility (MainForm, Program):
- Startup notices (what's-new About box, Realtek warning) now run one at a time via a
single sequence instead of separate BeginInvokes, so they no longer stack into
nested modals that couldn't be closed. The loading splash is skipped for a
tray-bound quick switch.
- Quick profile switch keeps RemSound in the tray if it was there, and plays the
switch cue immediately on click.
- Profile-switch cue now plays on click for every switch path (recent menu, quick
switch, File > Open) and no longer on a fresh start into the first profile. It was
also previously dead on the rebuilt form (pendingProfile was nulled first).
- Realtek ASIO toggle's accessible name now reads "Enable"/"Disable" to match the
visible text, instead of "Toggle" (screen reader read the wrong word).
Docs (plain English):
- RELEASE_NOTES.md: v3.4 entry.
- About dialog: v3.4 "what's new".
- readme.html (the canonical bundled manual): quick switch, the hotkey read-outs, the
new profile-menu-open cue, Realtek auto-detect/disable, and the config-folder path.
- MANUAL.md regenerated from readme.html via sync-manual.py so the two stay in sync.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
dd70613017
commit
946e6f4be4
@@ -20,6 +20,41 @@ internal sealed class AboutDialog : Form
|
||||
/// updates" path.</summary>
|
||||
private const string ReleaseNotes =
|
||||
"""
|
||||
RemSound v3.4
|
||||
|
||||
Quick profile switch: a new global hotkey pops up a
|
||||
list of all your profiles from anywhere — even when
|
||||
RemSound is in the system tray. Arrow to one, press
|
||||
Enter, and it switches straight away. It marks the
|
||||
profile you're on, plays a sound as the list opens
|
||||
and again when you switch, and stays in the tray if
|
||||
that's where it was. Unset by default; give it a key
|
||||
under Options → Keyboard shortcuts.
|
||||
|
||||
A safety net for the Realtek ASIO driver, which leaks
|
||||
Windows resources and can make audio unstable: RemSound
|
||||
now spots it on startup and offers, just once, to
|
||||
disable it. Re-enable or disable it any time from the
|
||||
Options menu.
|
||||
|
||||
Your screen reader now reads out global hotkeys when
|
||||
you move over the menu item or control they're tied to,
|
||||
so you can learn your shortcuts just by arrowing around
|
||||
— no need to open the shortcuts dialog.
|
||||
|
||||
Smoother long sessions on WASAPI: two machines' sound
|
||||
clocks drift apart by a hair over time, which slowly
|
||||
added delay. RemSound now corrects that continuously,
|
||||
so a WASAPI link stays as tight after three hours as it
|
||||
was at the start. (ASIO already kept itself in step.)
|
||||
|
||||
Also: a new "profile menu open" cue; the profile-switch
|
||||
cue now plays the instant you switch, and no longer on
|
||||
a fresh start; faster reaction when you plug or unplug a
|
||||
device; and your settings now tuck into a "config"
|
||||
folder, moved there automatically the first time you
|
||||
run this version.
|
||||
|
||||
RemSound v3.3
|
||||
|
||||
Your audio is now encrypted, end to end, so you no
|
||||
|
||||
@@ -1180,17 +1180,10 @@ public sealed class MainForm : Form
|
||||
// blank-template case (no pendingProfile) we schedule it here.
|
||||
if (pendingProfile is null) ScheduleBaselineCapture();
|
||||
ApplyPendingProfileToControls();
|
||||
// Profile-switch cue (2026-05-28): fires once after the profile finishes loading
|
||||
// into the UI. Covers BOTH startup (user picks a profile from the picker) and
|
||||
// mid-session switch (user picks a different profile from the menu — Program.Main
|
||||
// re-creates MainForm under the new profile). Skipped when the user is on the
|
||||
// blank template, where there's no profile to announce. Honours the per-profile
|
||||
// EnableProfileSwitchCue flag set in Preferences.
|
||||
if (pendingProfile is not null
|
||||
&& settings.LoadEnableProfileSwitchCue())
|
||||
{
|
||||
profileSwitchSound?.Play();
|
||||
}
|
||||
// The profile-switch cue is played ON CLICK by the switch entry points (Recent menu,
|
||||
// quick switch, File open) — NOT here. A fresh launch into the first profile must stay
|
||||
// silent: hearing the switch cue and then the connect cue at startup is confusing
|
||||
// (Ed, 2026-06-08). So the rebuilt form never replays it.
|
||||
// Show/hide the Update vs Save-as buttons based on whether we're on a loaded
|
||||
// profile or the blank template.
|
||||
UpdateProfileButtonsVisibility();
|
||||
@@ -1210,7 +1203,9 @@ public sealed class MainForm : Form
|
||||
// some virtual-machine drivers throw a redraw exception). The pending-profile
|
||||
// apply path above is unaffected — settings/devices/peers are already wired
|
||||
// up before we hide the window.
|
||||
if (AppConfig.Load().StartMinimised)
|
||||
var minimizeThisInstance = AppConfig.Load().StartMinimised || startNextInstanceMinimized;
|
||||
startNextInstanceMinimized = false;
|
||||
if (minimizeThisInstance)
|
||||
{
|
||||
BeginInvoke(() => trayController.Minimize());
|
||||
}
|
||||
@@ -1260,14 +1255,14 @@ public sealed class MainForm : Form
|
||||
});
|
||||
}
|
||||
|
||||
// If the user opted in, show the About box once on the first launch after an update
|
||||
// installed, so they see what's new. BeginInvoke so it opens after Shown completes.
|
||||
BeginInvoke(new Action(MaybeShowWhatsNewAfterUpdate));
|
||||
|
||||
// Offer once to disable a handle-leaking Realtek ASIO driver if one is installed.
|
||||
// BeginInvoke so the TaskDialog opens after Shown completes (and after the what's-new
|
||||
// box, if that fired).
|
||||
BeginInvoke(new Action(MaybeWarnAboutRealtekAsio));
|
||||
// Post-launch notices, shown ONE AT A TIME via a single BeginInvoke that runs them in
|
||||
// sequence — NOT one BeginInvoke per notice. Separate BeginInvokes NEST: the second
|
||||
// dialog opens inside the first's modal message loop, the two stack on top of each
|
||||
// other, and that nesting tangles their modal state so the boxes stop closing cleanly
|
||||
// (the bug where the what's-new About box wouldn't close after the Realtek warning).
|
||||
// RunStartupNotices shows each notice, waits for the user to close it, THEN shows the
|
||||
// next — every one modal to the main window, never nested.
|
||||
BeginInvoke(new Action(RunStartupNotices));
|
||||
};
|
||||
|
||||
statusTimer.Start();
|
||||
@@ -1292,6 +1287,21 @@ public sealed class MainForm : Form
|
||||
deviceRefreshTimer.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the post-launch notices one at a time — each ShowDialog blocks until the user closes it,
|
||||
/// so the next never opens on top of a still-open one. Order: the what's-new About box (after an
|
||||
/// update), then the Realtek-ASIO compatibility warning. The config-migration notice is handled
|
||||
/// separately in Program.Main (shown before the profile picker), so it's already outside this
|
||||
/// sequence and can't stack with these.
|
||||
/// </summary>
|
||||
private void RunStartupNotices()
|
||||
{
|
||||
if (IsDisposed) return;
|
||||
MaybeShowWhatsNewAfterUpdate();
|
||||
if (IsDisposed) return;
|
||||
MaybeWarnAboutRealtekAsio();
|
||||
}
|
||||
|
||||
/// <summary>If the user opted in (<see cref="AppConfig.ShowWhatsNewAfterUpdate"/>) and the
|
||||
/// running version changed since the last launch we recorded, open the About box once so
|
||||
/// they see what changed in the update just installed. Always records the current version
|
||||
@@ -1573,7 +1583,9 @@ public sealed class MainForm : Form
|
||||
ToolStripMenuItem? realtekToggle = null;
|
||||
if (realtekAsioDriverNames.Count > 0)
|
||||
{
|
||||
realtekToggle = new ToolStripMenuItem { AccessibleName = "Toggle Realtek ASIO driver in RemSound" };
|
||||
// AccessibleName is set (alongside Text) by UpdateRealtekAsioMenuItemText so the screen
|
||||
// reader hears "Enable"/"Disable", matching what's shown — never "Toggle".
|
||||
realtekToggle = new ToolStripMenuItem();
|
||||
realtekToggle.Click += (_, _) => ToggleRealtekAsio();
|
||||
realtekAsioToggleItem = realtekToggle;
|
||||
UpdateRealtekAsioMenuItemText();
|
||||
@@ -1689,6 +1701,12 @@ public sealed class MainForm : Form
|
||||
}
|
||||
}
|
||||
|
||||
// Carried across the close-and-relaunch profile switch (static so the NEXT MainForm instance,
|
||||
// built by Program.Main after this one closes, can read it). A switch done while RemSound was
|
||||
// in the tray should land back in the tray; internal so Program.Main can also skip the
|
||||
// "loading audio driver" splash in that case.
|
||||
internal static bool startNextInstanceMinimized;
|
||||
|
||||
/// <summary>Switch to the profile at <paramref name="path"/> via the same close-and-relaunch
|
||||
/// flow OpenProfileFromPicker uses. The active profile gets pushed to the front of the
|
||||
/// recents list by the next MainForm constructor when it sees the loaded path.</summary>
|
||||
@@ -1709,8 +1727,20 @@ public sealed class MainForm : Form
|
||||
}
|
||||
var title = Path.GetFileNameWithoutExtension(path);
|
||||
if (string.IsNullOrEmpty(title)) return;
|
||||
// Play the switch cue NOW, on click, for immediate feedback — CuePlayer.Play is fire-and-
|
||||
// forget on its own thread + device, so it survives the form rebuild that follows. Covers
|
||||
// BOTH the Recent-profiles menu and the quick-switch popup (both route through here). The
|
||||
// rebuilt form deliberately does NOT replay it, so startup into the first profile is silent.
|
||||
if (settings.LoadEnableProfileSwitchCue())
|
||||
{
|
||||
profileSwitchSound?.Play();
|
||||
}
|
||||
NextProfilePathToLoad = path;
|
||||
NextProfileTitleToLoad = title;
|
||||
// If the switch was triggered while the window was minimised / in the tray (the quick-
|
||||
// switch hotkey can fire from anywhere), keep the rebuilt instance in the tray too rather
|
||||
// than popping the window up in front of whatever the user is doing.
|
||||
startNextInstanceMinimized = !Visible || WindowState == FormWindowState.Minimized;
|
||||
AppendLogEntry($"profile switch via Recent profiles: \"{title}\" from {path}");
|
||||
Close();
|
||||
}
|
||||
@@ -1748,8 +1778,8 @@ public sealed class MainForm : Form
|
||||
var chosen = QuickProfileSwitchDialog.Show(entries);
|
||||
if (!string.IsNullOrEmpty(chosen))
|
||||
{
|
||||
// No-ops if it's already the current profile; otherwise reloads into the chosen one,
|
||||
// which plays the profile-switch cue on the relaunch.
|
||||
// SwitchToRecentProfile plays the switch cue on click, keeps the window in the
|
||||
// tray if it was there, and no-ops if the chosen profile is already current.
|
||||
SwitchToRecentProfile(chosen);
|
||||
}
|
||||
}
|
||||
@@ -1897,6 +1927,11 @@ public sealed class MainForm : Form
|
||||
var picked = Path.GetFileNameWithoutExtension(pickedPath);
|
||||
if (string.IsNullOrEmpty(picked)) return;
|
||||
if (string.Equals(pickedPath, currentProfilePath, StringComparison.OrdinalIgnoreCase)) return; // already loaded
|
||||
// Switch cue on click (same rationale as SwitchToRecentProfile).
|
||||
if (settings.LoadEnableProfileSwitchCue())
|
||||
{
|
||||
profileSwitchSound?.Play();
|
||||
}
|
||||
// Always pass the full path through. Program.cs deserialises directly from this
|
||||
// path, so profiles saved outside the active BaseDirectory still load correctly.
|
||||
NextProfilePathToLoad = pickedPath;
|
||||
@@ -3856,9 +3891,14 @@ public sealed class MainForm : Form
|
||||
{
|
||||
if (realtekAsioToggleItem is null || realtekAsioDriverNames.Count == 0) return;
|
||||
var anyDisabled = realtekAsioDriverNames.Exists(d => disabledAsioDrivers.Contains(d));
|
||||
// Set BOTH the visible Text (with the mnemonic) and the AccessibleName (no mnemonic) to the
|
||||
// same Enable/Disable wording, so the screen reader reads exactly what's shown — never "Toggle".
|
||||
realtekAsioToggleItem.Text = anyDisabled
|
||||
? "&Enable Realtek ASIO driver in RemSound"
|
||||
: "&Disable Realtek ASIO driver in RemSound";
|
||||
realtekAsioToggleItem.AccessibleName = anyDisabled
|
||||
? "Enable Realtek ASIO driver in RemSound"
|
||||
: "Disable Realtek ASIO driver in RemSound";
|
||||
}
|
||||
|
||||
private void RemoveDisabledDriverFromPicker(string driver)
|
||||
|
||||
@@ -186,7 +186,11 @@ internal static class Program
|
||||
// MainForm construction. Show a "Loading audio driver" splash — on its own
|
||||
// thread, so it stays painted while this thread is busy — so startup doesn't
|
||||
// look hung. No-op for WASAPI-only profiles (construction is near-instant).
|
||||
var splash = AsioLoadingSplash.StartIfNeeded(profile);
|
||||
// Skip the loading splash when this rebuild is a quick-profile-switch that's
|
||||
// staying in the tray — popping a splash up in front of the user's current app
|
||||
// defeats the point of keeping RemSound minimised, and the switch cue already
|
||||
// gave them feedback. Normal launches and visible switches still show it.
|
||||
var splash = MainForm.startNextInstanceMinimized ? null : AsioLoadingSplash.StartIfNeeded(profile);
|
||||
using var form = new MainForm(store, profile, title, nextPath);
|
||||
// Expose the live window to the single-instance activation callback (a second
|
||||
// copy choosing "switch to the running copy" signals us to surface this form).
|
||||
|
||||
Reference in New Issue
Block a user