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")); + } +}