feat(dock): the GAME bay — dB fader, measured ladder, mute, real duck badge

Unit 6. Mirrors the mic bay for the desktop/game bed, plus the badge that matters to a creator who
cannot hear the duck: it shows while the ducker is actually attenuating, and it prints the gain the
mixer applied (−12.0 DUCK by design).

- `AudioMixer.DuckGain` exposes `AutoDucker.CurrentGain` instead of leaving it internal.
- VM: `GameVolumeDb` / `GameVolumeDbLabel` (fader face), `GameMeterFraction` (measured level),
  `IsGameDucking` + `GameDuckLabel` refreshed on the level tick — the same moment the ducker runs,
  so the badge cannot lag behind the mix.
- The bay: header chip + source, ladder, mute dot (uses the meter band, so mute reads at a glance),
  dB readout, fader.

Guard: GameBayTests — the dock binds all four, `AutoDucker.DuckGain` is −12.04 dB and the label is
produced through the same dB scale as the faders, and the mixer property exists so the badge cannot
become a hardcoded number.
This commit is contained in:
2026-10-30 13:12:10 -07:00
parent 3524ede480
commit a1d8e761ea
4 changed files with 122 additions and 4 deletions
+39
View File
@@ -54,6 +54,8 @@ public partial class MainViewModel
OnPropertyChanged(nameof(MeterFillWidth));
OnPropertyChanged(nameof(MeterBrush));
OnPropertyChanged(nameof(MicMeterFraction));
UpdateDuckState();
OnPropertyChanged(nameof(MicMeterFraction));
OnPropertyChanged(nameof(MicVolumeDb));
OnPropertyChanged(nameof(MicVolumeDbLabel));
}
@@ -307,6 +309,32 @@ public partial class MainViewModel
public double GameMeterFillWidth => GameMeterLevel * 288;
/// <summary>Ladder input: a 0…1 display fraction of what was measured.</summary>
public double GameMeterFraction => GameMeterLevel;
/// <summary>The game fader's dB face (−60 … +6). Same rule as the mic: the model keeps linear
/// gain; the dock shows decibels.</summary>
public double GameVolumeDb
{
get => Helpers.GainDb.ToDb(GameAudioVolume);
set
{
var gain = Helpers.GainDb.FromDb(value);
if (Math.Abs(gain - GameAudioVolume) < 1e-6) return;
GameAudioVolume = gain;
OnPropertyChanged(nameof(GameVolumeDb));
OnPropertyChanged(nameof(GameVolumeDbLabel));
}
}
public string GameVolumeDbLabel => Helpers.GainDb.Label(GameVolumeDb) + " dB";
/// <summary>True while the mic is ducking the game/desktop bed (the mixer's own value).</summary>
public bool IsGameDucking { get; private set; }
/// <summary>"−12.0 dB" — the badge text, computed from the same gain the mixer applied.</summary>
public string GameDuckLabel => Helpers.GainDb.Label(Helpers.GainDb.ToDb(_audioMixer.DuckGain)) + " DUCK";
public string GameMeterBrush => GameMeterLevel switch
{
< 0.6 => ThemeBrush.Hex("Brush.Meter.Nominal"),
@@ -386,6 +414,17 @@ public partial class MainViewModel
public string GameMuteText => GameMuted ? "Unmute" : "Mute";
/// <summary>Badge state follows the mix, not a timer of its own: the level tick is already the
/// moment the ducker has just run.</summary>
private void UpdateDuckState()
{
var ducking = _audioMixer.DuckGain < 0.99f;
if (ducking == IsGameDucking) return;
IsGameDucking = ducking;
OnPropertyChanged(nameof(IsGameDucking));
OnPropertyChanged(nameof(GameDuckLabel));
}
private void ToggleGameMute()
{
if (GameMuted)