Volume/pan/EQ tab overhaul: one master switch, peer checklist, 16-band parametric EQ

Reworks the per-peer shaping tab (held for next release):

 * Renamed the tab to "Volume, pan and EQ for peers"; the Preferences toggle now defaults ON.
 * Collapsed the two master switches (Enable EQ / Enable pan) into ONE: "Enable volume, pan and
   EQ for all peers" (Alt+E). Volume now obeys it too. PeerDspChain.Build takes a single enabled
   flag; Profile.EnableAllPeerShaping replaces the two bools (old ones kept for load-migration).
 * Peer picker is now a CheckedListBox: ticking a peer shapes them (per-peer bypass via new
   PeerShaping.Enabled, default true); the focused row is the one the controls edit. Effective
   shaping = master switch AND that peer's tick. Letter-nav suppressed so keys never toggle a tick.
 * Three EQ modes, renamed: "3 band simple EQ", "12 band advanced graphic EQ", and the new
   "16 band parametric EQ" (PeerEqMode.Parametric16Band).
 * Parametric EQ: up to 16 user bands, each a boost/cut across a start->end range (PeerShaping
   .ParametricBands; ParametricToPeaking maps range -> peaking centre+Q, shared by DSP and curve).
   Add band dialog (spin-or-type, numeric-only, live preview, OK/Escape); Bands list sorted
   bass->treble reading "X Hz to Y Hz, plus/minus N dB"; Delete key / Delete button, multi-select.
   Set peer EQ to default clears the parametric list too.
 * dB now spoken as words ("plus 3 dB" / "minus 6 dB" / "flat") on the graphic sliders and the
   parametric list, since NVDA users typically have punctuation off and never hear a "+".
 * New unbound machine-wide global shortcut "Toggle volume, pan and EQ for all peers" (not stored
   in any profile) via the hotkey controller + settings store.
 * Renamed the Inputs/outputs "Set volume for all received audio" to "Master receive volume".
 * Added EqCurveControl: a purely-visual EQ response graph (not focusable, invisible to NVDA).
 * Full manual sweep (readme.html + regenerated MANUAL.md).

Build clean; --selftest passes. Deployed to both test folders. Held for next release.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-07-07 23:21:18 +01:00
co-authored by Claude Opus 4.8
parent eaae76d015
commit 033edd776f
102 changed files with 4658 additions and 135 deletions
+10 -5
View File
@@ -68,11 +68,12 @@ public sealed class AppConfig
/// already running quietly". Default false.</summary>
public bool StartMinimised { get; set; }
/// <summary>If true, the main window shows a "Pan and EQ" tab (positioned before "Audio profile")
/// for setting per-peer panning and EQ. Off by default; toggled by "Show pan and EQ tab" on the
/// Preferences General tab. Machine-wide (a UI-visibility preference, like which tabs exist) — the
/// pan/EQ VALUES are saved per profile, in <see cref="Profile.PeerShaping"/>.</summary>
public bool ShowPanEqTab { get; set; }
/// <summary>If true (the default), the main window shows a "Volume, pan and EQ for peers" tab
/// (positioned before "Audio profile") for setting per-peer volume, panning and EQ. Toggled by
/// "Show the volume, pan and EQ for peers tab" on the Preferences General tab. Machine-wide (a
/// UI-visibility preference, like which tabs exist) — the shaping VALUES are saved per profile, in
/// <see cref="Profile.PeerShaping"/>.</summary>
public bool ShowPanEqTab { get; set; } = true;
/// <summary>If true (the default), RemSound plays the startup cue once, right after this
/// copy wins the single-instance takeover and before the profile loads. Machine-wide (not
@@ -173,6 +174,10 @@ public sealed class AppConfig
public HotkeyRecord? SystemMuteToggleHotkey { get; set; }
public HotkeyRecord? QuickProfileSwitchHotkey { get; set; }
public HotkeyRecord? SpeakStatusLineHotkey { get; set; }
/// <summary>Global shortcut that toggles the "Enable volume, pan and EQ for all peers" master
/// switch. Machine-wide and unset by default; NOT stored in any profile (unlike the shaping
/// values). The user binds it in Keyboard shortcuts.</summary>
public HotkeyRecord? ToggleAllPeerShapingHotkey { get; set; }
/// <summary>True once the one-time "your keyboard shortcuts are now shared across profiles" notice
/// has been shown (to an upgrader), or silently marked done on a fresh install that had nothing to
+59 -6
View File
@@ -1,35 +1,64 @@
namespace RemSound.Core;
/// <summary>Which EQ the user is shaping a peer with. The two modes are INDEPENDENT: switching
/// between them keeps each mode's own band gains — nothing is carried over. The active mode is the
/// <summary>Which EQ the user is shaping a peer with. The three modes are INDEPENDENT: switching
/// between them keeps each mode's own settings — nothing is carried over. The active mode is the
/// one that's applied to the sound.</summary>
public enum PeerEqMode
{
/// <summary>3-band tone control (bass / mids / treble). Display name: "3 band simple EQ".</summary>
Simple3Band = 0,
/// <summary>12-band graphic EQ with fixed centre frequencies. Display name: "12 band advanced
/// graphic EQ". (The enum member keeps its historic name; the numeric value is what's persisted.)</summary>
Advanced10Band = 1,
/// <summary>Up to 16 user-defined parametric bands, each a boost/cut across a start→end frequency
/// range. Display name: "16 band parametric EQ".</summary>
Parametric16Band = 2,
}
/// <summary>Per-peer pan + EQ, saved per profile (see <see cref="Profile.PeerShaping"/>). Keyed in
/// <summary>One user-defined parametric EQ band: a boost or cut spread across the range from
/// <see cref="StartHz"/> to <see cref="EndHz"/>. Realised as a peaking filter centred on the
/// geometric mean of the two frequencies, widened to span the range (see
/// <see cref="PeerEqBands.ParametricToPeaking"/>).</summary>
public sealed class ParametricBand
{
public float StartHz { get; set; }
public float EndHz { get; set; }
/// <summary>Gain in dB, -12..+12.</summary>
public float GainDb { get; set; }
public ParametricBand Clone() => new() { StartHz = StartHz, EndHz = EndHz, GainDb = GainDb };
}
/// <summary>Per-peer volume + pan + EQ, saved per profile (see <see cref="Profile.PeerShaping"/>). Keyed in
/// the profile by the same peer-entry string as <see cref="Profile.SelectedConnectedPeers"/>.</summary>
public sealed class PeerShaping
{
/// <summary>Whether this peer is shaped at all. When false the peer passes through untouched even
/// while the profile-wide master switch is on — a per-peer bypass that keeps the settings below.
/// Defaults to true so a freshly-seen peer is covered the moment the master switch is turned on.</summary>
public bool Enabled { get; set; } = true;
/// <summary>-1 (full left) .. 0 (centre) .. +1 (full right).</summary>
public float Pan { get; set; }
/// <summary>Per-peer playback level, 0..1 (1 = 100%, unity). An individual fader for this peer,
/// on top of the global volume. Always applied (no master switch); 100% is transparent.</summary>
/// on top of the global volume. 100% is transparent.</summary>
public float Volume { get; set; } = 1f;
/// <summary>Which EQ mode is currently active for this peer.</summary>
public PeerEqMode EqMode { get; set; } = PeerEqMode.Simple3Band;
/// <summary>Gains in dB (-12..+12) for the 3 simple bands, in the order of <see cref="PeerEqBands.Simple"/>
/// (bass, mids, treble). Kept independently of the advanced bands.</summary>
/// (bass, mids, treble). Kept independently of the other modes.</summary>
public float[] SimpleBandsDb { get; set; } = new float[3];
/// <summary>Gains in dB (-12..+12) for the 12 advanced graphic-EQ bands, in the order of
/// <see cref="PeerEqBands.Advanced"/>. Kept independently of the simple bands.</summary>
/// <see cref="PeerEqBands.Advanced"/>. Kept independently of the other modes.</summary>
public float[] AdvancedBandsDb { get; set; } = new float[12];
/// <summary>The user's parametric bands (up to <see cref="PeerEqBands.ParametricMaxBands"/>). Kept
/// independently of the two graphic modes. Empty = flat.</summary>
public List<ParametricBand> ParametricBands { get; set; } = new();
}
/// <summary>The fixed EQ band layouts. The user sets each band's GAIN; the centre frequencies are
@@ -44,6 +73,13 @@ public static class PeerEqBands
/// <summary>Maximum boost/cut for any band, in dB. Sliders run -12..+12.</summary>
public const float MaxGainDb = 12f;
/// <summary>Most parametric bands a peer can hold.</summary>
public const int ParametricMaxBands = 16;
/// <summary>Lowest / highest frequency a parametric band edge can be set to.</summary>
public const float ParametricMinHz = 20f;
public const float ParametricMaxHz = 20000f;
/// <summary>3-band "tone control": bass (low shelf), mids (peaking), treble (high shelf).</summary>
public static readonly (string Label, double Freq)[] Simple =
[
@@ -69,4 +105,21 @@ public static class PeerEqBands
("8 kHz", 8000.0),
("16 kHz", 16000.0),
];
/// <summary>Converts a parametric band's start/end frequency range into a peaking-filter centre
/// frequency and Q. Centre is the geometric mean of the two edges; Q comes from the range's width
/// in octaves (the standard RBJ bandwidth-to-Q relation). Both the DSP and the on-screen response
/// curve call this so they always agree.</summary>
public static void ParametricToPeaking(float startHz, float endHz, out float centerHz, out float q)
{
startHz = Math.Clamp(startHz, ParametricMinHz, ParametricMaxHz);
endHz = Math.Clamp(endHz, ParametricMinHz, ParametricMaxHz);
if (endHz <= startHz) endHz = MathF.Min(startHz * 1.01f, ParametricMaxHz);
centerHz = MathF.Sqrt(startHz * endHz);
float bwOct = MathF.Log2(endHz / startHz);
if (bwOct < 0.01f) bwOct = 0.01f;
float twoPowBw = MathF.Pow(2f, bwOct);
q = MathF.Sqrt(twoPowBw) / (twoPowBw - 1f);
q = Math.Clamp(q, 0.1f, 12f);
}
}
+8 -5
View File
@@ -183,12 +183,15 @@ public sealed class Profile
/// (the connection simply waits or drops rather than moving). Off by default. (#17)</summary>
public bool LockPeerAddresses { get; set; }
// === Per-peer pan and EQ (jam mixing) ===
/// <summary>Master switch for this profile: apply each connected peer's saved PAN. Off by default.
/// The pan values themselves live in <see cref="PeerShaping"/>.</summary>
public bool EnablePanForPeers { get; set; }
// === Per-peer volume, pan and EQ (jam mixing) ===
/// <summary>Single master switch for this profile: apply each connected peer's saved VOLUME, PAN and
/// EQ. Off by default. The values themselves live in <see cref="PeerShaping"/>; a peer can also be
/// bypassed individually via <see cref="RemSound.Core.PeerShaping.Enabled"/>.</summary>
public bool EnableAllPeerShaping { get; set; }
/// <summary>Master switch for this profile: apply each connected peer's saved EQ. Off by default.</summary>
/// <summary>Legacy separate master switches (pre-collapse). Kept only so older profile JSON still
/// deserialises and migrates: on load, either being true turns the single master switch on.</summary>
public bool EnablePanForPeers { get; set; }
public bool EnableEqForPeers { get; set; }
/// <summary>Per-peer pan + EQ, keyed by the SAME peer-entry string used in
@@ -82,6 +82,10 @@ public sealed class RemSoundSettingsStore
AppConfig.Load().SpeakStatusLineHotkey?.ToHotkeyInfo() ?? HotkeyInfo.Unset;
public void SaveSpeakStatusLineHotkey(HotkeyInfo hotkey) => SaveGlobalHotkey(c => c.SpeakStatusLineHotkey = HotkeyRecord.From(hotkey));
public HotkeyInfo LoadToggleAllPeerShapingHotkey() =>
AppConfig.Load().ToggleAllPeerShapingHotkey?.ToHotkeyInfo() ?? HotkeyInfo.Unset;
public void SaveToggleAllPeerShapingHotkey(HotkeyInfo hotkey) => SaveGlobalHotkey(c => c.ToggleAllPeerShapingHotkey = HotkeyRecord.From(hotkey));
public bool LoadAcceptRemoteVolumeCommands(bool defaultValue = false) =>
Try(() => Load()?.AcceptRemoteVolumeCommands) ?? defaultValue;