From a1d8e761eae39a181b7197a17388aa9284eb87e3 Mon Sep 17 00:00:00 2001 From: gramps Date: Fri, 30 Oct 2026 13:12:10 -0700 Subject: [PATCH] =?UTF-8?q?feat(dock):=20the=20GAME=20bay=20=E2=80=94=20dB?= =?UTF-8?q?=20fader,=20measured=20ladder,=20mute,=20real=20duck=20badge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Controls/AudioDock.xaml | 50 ++++++++++++++++++++++++++++--- Services/Audio/AudioMixer.cs | 4 +++ ViewModels/MainViewModel.Audio.cs | 39 ++++++++++++++++++++++++ ytLive.Tests/GameBayTests.cs | 33 ++++++++++++++++++++ 4 files changed, 122 insertions(+), 4 deletions(-) create mode 100644 ytLive.Tests/GameBayTests.cs 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")); + } +}