diff --git a/Controls/AudioDock.xaml b/Controls/AudioDock.xaml index 98011b8..1a71989 100644 --- a/Controls/AudioDock.xaml +++ b/Controls/AudioDock.xaml @@ -1,6 +1,7 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:controls="clr-namespace:ytLive.Controls"> - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Controls/AudioDock.xaml.cs b/Controls/AudioDock.xaml.cs index d998688..2b12e86 100644 --- a/Controls/AudioDock.xaml.cs +++ b/Controls/AudioDock.xaml.cs @@ -33,6 +33,12 @@ public partial class AudioDock : UserControl Bays.Visibility = collapsed ? Visibility.Collapsed : Visibility.Visible; } + private void MicLabel_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e) + { + if (DataContext is ViewModels.MainViewModel vm && vm.OpenMicPickerCommand.CanExecute(null)) + vm.OpenMicPickerCommand.Execute(null); + } + private void OnLoaded(object sender, RoutedEventArgs e) { _window = Window.GetWindow(this); diff --git a/ViewModels/MainViewModel.Audio.cs b/ViewModels/MainViewModel.Audio.cs index 07b25c2..6deee38 100644 --- a/ViewModels/MainViewModel.Audio.cs +++ b/ViewModels/MainViewModel.Audio.cs @@ -53,6 +53,9 @@ public partial class MainViewModel { OnPropertyChanged(nameof(MeterFillWidth)); OnPropertyChanged(nameof(MeterBrush)); + OnPropertyChanged(nameof(MicMeterFraction)); + OnPropertyChanged(nameof(MicVolumeDb)); + OnPropertyChanged(nameof(MicVolumeDbLabel)); } } } @@ -141,6 +144,29 @@ public partial class MainViewModel } } + /// The fader's face, in dB (−60 … +6, 0 = unity). The model keeps linear gain; this is + /// the same value expressed the way a person reads a mixer — and the only thing the dock binds. + /// + public double MicVolumeDb + { + get => Helpers.GainDb.ToDb(MicVolume); + set + { + var gain = Helpers.GainDb.FromDb(value); + if (Math.Abs(gain - MicVolume) < 1e-6) return; + MicVolume = gain; + OnPropertyChanged(nameof(MicVolumeDb)); + OnPropertyChanged(nameof(MicVolumeDbLabel)); + } + } + + /// "-6.0 dB" beside the fader; "−∞ dB" at the floor. + public string MicVolumeDbLabel => Helpers.GainDb.Label(MicVolumeDb) + " dB"; + + /// The ladder's input: a 0…1 display fraction of what was MEASURED (already scaled by + /// the volume and muted state), never the gain setting. + public double MicMeterFraction => MeterLevel; + /// Read-only: true whenever the volume is 0. Only MicVolume may /// change it, so the slider and the speaker can never disagree. public bool MicMuted => _micMuted; diff --git a/ytLive.Tests/MicBayTests.cs b/ytLive.Tests/MicBayTests.cs new file mode 100644 index 0000000..3a87eec --- /dev/null +++ b/ytLive.Tests/MicBayTests.cs @@ -0,0 +1,37 @@ +using System.Linq; +using Xunit; +using ytLive.ViewModels; + +namespace ytLive.Tests; + +/// The MIC bay is the first bay with real controls, and it is the creator's only audio +/// control — so its wiring deserves a guard that costs nothing to run. +/// +/// The fader speaks dB while the model stores linear gain (that mapping is covered by +/// GainDbTests); what this pins is that the dock actually binds the dB face and the MEASURED level, +/// that the VM exposes both, and that a mute still zeroes the ladder. +public class MicBayTests +{ + [Fact] + public void The_mic_bay_binds_the_db_fader_and_the_measured_level() + { + var dock = SourceAssert.Text("Controls", "AudioDock.xaml"); + Assert.Contains("Value=\"{Binding MicVolumeDb, Mode=TwoWay}\"", dock); + Assert.Contains("Level=\"{Binding MicMeterFraction}\"", dock); + Assert.Contains("Text=\"{Binding MicVolumeDbLabel}\"", dock); + Assert.Contains("Command=\"{Binding ToggleMicMuteCommand}\"", dock); + + // The fader's range is the scale's range, not an arbitrary slider span. + Assert.Contains("Minimum=\"-60\" Maximum=\"6\"", dock); + + // The VM surface exists (no construction: this is a wiring check, not a behaviour one). + var type = typeof(MainViewModel); + foreach (var name in new[] { "MicVolumeDb", "MicVolumeDbLabel", "MicMeterFraction" }) + Assert.NotNull(type.GetProperty(name)); + + // MicMuted is derived from the volume: mute is a fader position, so the ladder must follow. + var audio = SourceAssert.Text("ViewModels", "MainViewModel.Audio.cs"); + Assert.Contains("MicMuted ? 0 :", audio); + Assert.Contains("OnPropertyChanged(nameof(MicMeterFraction));", audio); + } +}