Bump to v1.5.0: cross-lane routing fix, recording double-tap fix, menu reorg
Bug fixes: * BothIndependent recording was double-tapped — when both WASAPI and ASIO outputs were ticked, recordings came out garbled and ~2x the expected duration. AudioRecorder now uses four per-lane rings (sent-wasapi, sent-asio, recv-wasapi, recv-asio) plus a writer- thread mix step that drains min(wasapi, asio) frames and sum-mixes with the soft-tanh limiter. Tap signatures gained a RenderRoute parameter; Mixed maps to the wasapi slot. * A peer announcing on the WASAPI lane was inaudible when the receiver only had an ASIO output ticked (and vice versa). The session opened, samples flowed into the SessionPlayout ring, but ReadForRoute(AsioLane) skipped any session whose Route was WasapiLane so nothing drained the ring. PlayoutEngine now tracks per-lane "is this lane backed by a device" via volatile bools settable through SetLaneActive(RenderRoute, bool), called from CompositeRenderBackend.SetOutputDevices whenever the device split changes. ReadForRoute admits orphan sessions when the other lane is inactive. Menu reorganisation: * New Options menu (Alt+O) holds Recording settings (Alt+S), Keyboard shortcuts (Alt+K, Ctrl+K), Startup behaviour (Alt+T), Preferences (Alt+P, Ctrl+P). Pre-v1.5 these were scattered across File menu, Record menu, and inside the Preferences dialog itself. * Record menu mnemonic moved from Alt+O to Alt+K, rendered as "Record (Alt+K)" so the chord is visible despite K not being a letter in "Record". Alt+R is taken by Receive audio. * File menu — new Recent profiles submenu (Alt+F, R) listing the five most-recently-opened profiles. Press 1..5 inside the submenu to jump to a slot. Missing files are skipped from the menu but kept in storage. * Rename current profile moves to Alt+M (was R), Minimise to tray moves to Alt+N (was M). * Lock to audio clock was Alt+K, now Alt+D. UX additions: * Ctrl+O = Open profile (matches the menu chord). * New global hotkey: Start / Stop recording. Pickable from Options -> Keyboard shortcuts. Unbound by default. Works system-wide. Wire format and audio pipeline unchanged from v1.4 — v1.4 and v1.5 peers interoperate. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
b96ba82378
commit
4915a9afe1
@@ -89,6 +89,33 @@ public sealed class AppConfig
|
||||
/// only once. Null on a fresh install.</summary>
|
||||
public DateTime? LastUpdateCheckUtc { get; set; }
|
||||
|
||||
/// <summary>Most-recently-opened profile paths, newest first, capped at
|
||||
/// <see cref="MaxRecentProfiles"/>. Populated by <see cref="NoteRecentProfile"/> every
|
||||
/// time a profile is loaded, surfaced in the File → Recent profiles submenu. Stored as
|
||||
/// full paths so profiles saved outside the canonical profiles folder are also
|
||||
/// reachable (Save-As to an arbitrary path stays in the recents list).</summary>
|
||||
public List<string> RecentProfiles { get; set; } = new();
|
||||
|
||||
/// <summary>Cap on how many entries we keep in <see cref="RecentProfiles"/>. Five is the
|
||||
/// most that fits comfortably as 1–5 single-digit mnemonics inside a submenu without
|
||||
/// the user needing to read the names to remember which row they want.</summary>
|
||||
public const int MaxRecentProfiles = 5;
|
||||
|
||||
/// <summary>Push a profile path to the front of the recents list. Removes any existing
|
||||
/// entry that matches (case-insensitive) so a recently re-opened profile rises to the
|
||||
/// top instead of being duplicated. Caps the list at <see cref="MaxRecentProfiles"/>.
|
||||
/// Caller must <see cref="Save"/> after mutating.</summary>
|
||||
public void NoteRecentProfile(string? path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path)) return;
|
||||
RecentProfiles.RemoveAll(p => string.Equals(p, path, StringComparison.OrdinalIgnoreCase));
|
||||
RecentProfiles.Insert(0, path);
|
||||
while (RecentProfiles.Count > MaxRecentProfiles)
|
||||
{
|
||||
RecentProfiles.RemoveAt(RecentProfiles.Count - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static string ConfigPath => Path.Combine(AppContext.BaseDirectory, "remsound.config.json");
|
||||
|
||||
/// <summary>Reads the app config from disk. Always returns a non-null instance — a missing
|
||||
|
||||
@@ -131,6 +131,12 @@ public sealed class Profile
|
||||
public HotkeyRecord? TrayHotkey { get; set; }
|
||||
public HotkeyRecord? VolumeUpHotkey { get; set; }
|
||||
public HotkeyRecord? VolumeDownHotkey { get; set; }
|
||||
/// <summary>Global hotkey for start / stop recording. Toggles the same action as the
|
||||
/// Record menu's "Start recording / Stop recording" item and the in-app Ctrl+R, but
|
||||
/// works system-wide (RemSound doesn't need keyboard focus). Default unset — recording
|
||||
/// is uncommon enough that we don't claim a default chord that might clash with the
|
||||
/// user's other tools.</summary>
|
||||
public HotkeyRecord? ToggleRecordingHotkey { get; set; }
|
||||
/// <summary>Hotkey that sends a "raise volume" command to every connected peer that has
|
||||
/// "Accept remote volume commands" enabled. The local volume slider on this machine is
|
||||
/// NOT touched. Use case: I'm NVDA-Remote'd into another machine and want to nudge the
|
||||
|
||||
@@ -64,6 +64,16 @@ public sealed class RemSoundSettingsStore
|
||||
Save(s);
|
||||
}
|
||||
|
||||
public HotkeyInfo LoadToggleRecordingHotkey() =>
|
||||
Try(() => Load()?.ToggleRecordingHotkey?.ToHotkeyInfo()) ?? HotkeyInfo.Unset;
|
||||
|
||||
public void SaveToggleRecordingHotkey(HotkeyInfo hotkey)
|
||||
{
|
||||
var s = Load() ?? new Settings();
|
||||
s.ToggleRecordingHotkey = HotkeySetting.From(hotkey);
|
||||
Save(s);
|
||||
}
|
||||
|
||||
public HotkeyInfo LoadRemoteVolumeUpHotkey() =>
|
||||
Try(() => Load()?.RemoteVolumeUpHotkey?.ToHotkeyInfo()) ?? HotkeyInfo.Unset;
|
||||
|
||||
@@ -466,6 +476,7 @@ public sealed class RemSoundSettingsStore
|
||||
TrayHotkey = profile.TrayHotkey is null ? null : HotkeySettingFromRecord(profile.TrayHotkey),
|
||||
VolumeUpHotkey = profile.VolumeUpHotkey is null ? null : HotkeySettingFromRecord(profile.VolumeUpHotkey),
|
||||
VolumeDownHotkey = profile.VolumeDownHotkey is null ? null : HotkeySettingFromRecord(profile.VolumeDownHotkey),
|
||||
ToggleRecordingHotkey = profile.ToggleRecordingHotkey is null ? null : HotkeySettingFromRecord(profile.ToggleRecordingHotkey),
|
||||
RemoteVolumeUpHotkey = profile.RemoteVolumeUpHotkey is null ? null : HotkeySettingFromRecord(profile.RemoteVolumeUpHotkey),
|
||||
RemoteVolumeDownHotkey = profile.RemoteVolumeDownHotkey is null ? null : HotkeySettingFromRecord(profile.RemoteVolumeDownHotkey),
|
||||
RemoteMuteToggleHotkey = profile.RemoteMuteToggleHotkey is null ? null : HotkeySettingFromRecord(profile.RemoteMuteToggleHotkey),
|
||||
@@ -512,6 +523,7 @@ public sealed class RemSoundSettingsStore
|
||||
profile.TrayHotkey = s.TrayHotkey is null ? null : HotkeyRecordFromSetting(s.TrayHotkey);
|
||||
profile.VolumeUpHotkey = s.VolumeUpHotkey is null ? null : HotkeyRecordFromSetting(s.VolumeUpHotkey);
|
||||
profile.VolumeDownHotkey = s.VolumeDownHotkey is null ? null : HotkeyRecordFromSetting(s.VolumeDownHotkey);
|
||||
profile.ToggleRecordingHotkey = s.ToggleRecordingHotkey is null ? null : HotkeyRecordFromSetting(s.ToggleRecordingHotkey);
|
||||
profile.RemoteVolumeUpHotkey = s.RemoteVolumeUpHotkey is null ? null : HotkeyRecordFromSetting(s.RemoteVolumeUpHotkey);
|
||||
profile.RemoteVolumeDownHotkey = s.RemoteVolumeDownHotkey is null ? null : HotkeyRecordFromSetting(s.RemoteVolumeDownHotkey);
|
||||
profile.RemoteMuteToggleHotkey = s.RemoteMuteToggleHotkey is null ? null : HotkeyRecordFromSetting(s.RemoteMuteToggleHotkey);
|
||||
@@ -570,6 +582,7 @@ public sealed class RemSoundSettingsStore
|
||||
public HotkeySetting? TrayHotkey { get; set; }
|
||||
public HotkeySetting? VolumeUpHotkey { get; set; }
|
||||
public HotkeySetting? VolumeDownHotkey { get; set; }
|
||||
public HotkeySetting? ToggleRecordingHotkey { get; set; }
|
||||
public HotkeySetting? RemoteVolumeUpHotkey { get; set; }
|
||||
public HotkeySetting? RemoteVolumeDownHotkey { get; set; }
|
||||
public HotkeySetting? RemoteMuteToggleHotkey { get; set; }
|
||||
|
||||
Reference in New Issue
Block a user