From 3524ede480d0ff4d6aee257d253287546d504fec Mon Sep 17 00:00:00 2001 From: gramps Date: Fri, 30 Oct 2026 12:46:57 -0700 Subject: [PATCH] =?UTF-8?q?feat(dock):=20the=20MIC=20bay=20=E2=80=94=20dB?= =?UTF-8?q?=20fader,=20measured=20ladder,=20mute,=20picker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unit 5. The dock's first working bay: - 24-segment ladder fed by `MicMeterFraction` — the MEASURED display level (already scaled by the volume and zeroed when muted), never the gain setting. - Fader in dB via `MicVolumeDb` (−60 … +6, 0 = unity) with a mono-weight readout (`MicVolumeDbLabel`), so "−6.0 dB" is what the creator reads instead of "0.50". - Mute button whose dot uses the existing MicStatusBrush; clicking the device name opens the mic picker, so the label stays the control (the KISS rule: one place to change the mic). The preview's own mic row stays for now — it is removed in the final unit, after the dock bays are complete, so nothing is lost if a bay misbehaves. Guard: MicBayTests — the bay binds the dB face and the measured level (not the gain), the fader range IS the scale's range, the VM surface exists, and mute still zeroes the ladder. --- Controls/AudioDock.xaml | 47 +++++++++++++++++++++++++++---- Controls/AudioDock.xaml.cs | 6 ++++ ViewModels/MainViewModel.Audio.cs | 26 +++++++++++++++++ ytLive.Tests/MicBayTests.cs | 37 ++++++++++++++++++++++++ 4 files changed, 111 insertions(+), 5 deletions(-) create mode 100644 ytLive.Tests/MicBayTests.cs 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); + } +}