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:
co-authored by
Claude Opus 4.8
parent
eaae76d015
commit
033edd776f
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user