diff --git a/Controls/AudioDock.xaml b/Controls/AudioDock.xaml
index 1a71989..e36c80a 100644
--- a/Controls/AudioDock.xaml
+++ b/Controls/AudioDock.xaml
@@ -66,10 +66,52 @@
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Services/Audio/AudioMixer.cs b/Services/Audio/AudioMixer.cs
index 9dcbfcd..da6b99d 100644
--- a/Services/Audio/AudioMixer.cs
+++ b/Services/Audio/AudioMixer.cs
@@ -44,6 +44,10 @@ public sealed class AudioMixer : IDisposable
private readonly AudioRingBuffer _alertBuffer;
private readonly VoiceFilterChain _voiceChain;
private readonly AutoDucker _ducker;
+
+ /// The ducker's CURRENT gain (1 = not ducking, 0.25 = the −12 dB duck). Read by the
+ /// dock's badge: a duck indicator that is not the real value is decoration.
+ public float DuckGain => _ducker.CurrentGain;
private readonly MasterLimiter _masterLimiter = new();
private TinyResampler? _micResampler;
private TinyResampler? _loopbackResampler;
diff --git a/ViewModels/MainViewModel.Audio.cs b/ViewModels/MainViewModel.Audio.cs
index 6deee38..5203913 100644
--- a/ViewModels/MainViewModel.Audio.cs
+++ b/ViewModels/MainViewModel.Audio.cs
@@ -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;
+ /// Ladder input: a 0…1 display fraction of what was measured.
+ public double GameMeterFraction => GameMeterLevel;
+
+ /// The game fader's dB face (−60 … +6). Same rule as the mic: the model keeps linear
+ /// gain; the dock shows decibels.
+ 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";
+
+ /// True while the mic is ducking the game/desktop bed (the mixer's own value).
+ public bool IsGameDucking { get; private set; }
+
+ /// "−12.0 dB" — the badge text, computed from the same gain the mixer applied.
+ 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";
+ /// Badge state follows the mix, not a timer of its own: the level tick is already the
+ /// moment the ducker has just run.
+ 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)
diff --git a/ytLive.Tests/GameBayTests.cs b/ytLive.Tests/GameBayTests.cs
new file mode 100644
index 0000000..240b647
--- /dev/null
+++ b/ytLive.Tests/GameBayTests.cs
@@ -0,0 +1,33 @@
+using Xunit;
+using ytLive.Helpers;
+using ytLive.Services.Audio;
+using ytLive.ViewModels;
+
+namespace ytLive.Tests;
+
+/// The GAME bay's badge must carry the gain the mixer actually applied — a duck indicator
+/// that is not the real value is decoration, and this creator cannot hear whether it happened.
+///
+public class GameBayTests
+{
+ [Fact]
+ public void The_game_bay_binds_the_db_fader_and_a_real_duck_readout()
+ {
+ var dock = SourceAssert.Text("Controls", "AudioDock.xaml");
+ Assert.Contains("Value=\"{Binding GameVolumeDb, Mode=TwoWay}\"", dock);
+ Assert.Contains("Level=\"{Binding GameMeterFraction}\"", dock);
+ Assert.Contains("Text=\"{Binding GameDuckLabel}\"", dock);
+ Assert.Contains("Visibility=\"{Binding IsGameDucking, Converter={StaticResource BoolToVis}}\"", dock);
+
+ foreach (var name in new[] { "GameVolumeDb", "GameVolumeDbLabel", "GameMeterFraction", "IsGameDucking", "GameDuckLabel" })
+ Assert.NotNull(typeof(MainViewModel).GetProperty(name));
+
+ // The badge's number is the mixer's ducker, not a copy: the duck is −12.04 dB by design,
+ // and the label is produced from the live gain through the same dB scale as the faders.
+ Assert.Equal(-12.04, GainDb.ToDb(AutoDucker.DuckGain), 1);
+ Assert.Equal("-12.0 DUCK", GainDb.Label(GainDb.ToDb(AutoDucker.DuckGain)) + " DUCK");
+
+ // And the mixer exposes that gain rather than hiding it.
+ Assert.NotNull(typeof(AudioMixer).GetProperty("DuckGain"));
+ }
+}