feat(dock): the MIC bay — dB fader, measured ladder, mute, picker
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.
This commit is contained in:
+42
-5
@@ -1,6 +1,7 @@
|
||||
<UserControl x:Class="ytLive.Controls.AudioDock"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="clr-namespace:ytLive.Controls">
|
||||
<!-- The sound bars. Replaces the empty bottom-bar spacer; collapses to the telemetry strip
|
||||
on short windows (AudioDockLayout decides the height, not this file). -->
|
||||
<Border x:Name="DockRoot" Background="{StaticResource Brush.SurfaceContainer}"
|
||||
@@ -23,10 +24,46 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Border Grid.Column="0" Background="{StaticResource Brush.SurfaceContainerLow}" CornerRadius="{StaticResource Radius.Sm}" Margin="0,0,4,0" Padding="10,6">
|
||||
<StackPanel>
|
||||
<TextBlock Text="MIC" FontSize="10" FontWeight="SemiBold" Foreground="{StaticResource Brush.OnSurfaceVariant}"/>
|
||||
<TextBlock x:Name="MicBayBody" Text="—" FontSize="11" Foreground="{StaticResource Brush.OnSurfaceDisabled}"/>
|
||||
</StackPanel>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Clicking the name opens the mic picker: the label is the control. -->
|
||||
<Grid Grid.Row="0" Cursor="Hand" ToolTip="Change microphone"
|
||||
MouseLeftButtonUp="MicLabel_MouseLeftButtonUp">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border Background="{StaticResource Brush.SurfaceContainerHighest}" CornerRadius="{StaticResource Radius.Sm}" Padding="5,1">
|
||||
<TextBlock Text="MIC 1" FontSize="9" FontWeight="Bold" Foreground="{StaticResource Brush.OnSurfaceVariant}"/>
|
||||
</Border>
|
||||
<TextBlock Grid.Column="1" Margin="6,0,0,0" FontSize="11" VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
Text="{Binding MicSourceName, TargetNullValue='No microphone'}"
|
||||
Foreground="{StaticResource Brush.OnSurface}"/>
|
||||
</Grid>
|
||||
|
||||
<controls:SegmentMeter Grid.Row="1" Height="10" Margin="0,6,0,0" Orientation="Horizontal"
|
||||
Level="{Binding MicMeterFraction}" Segments="24"/>
|
||||
|
||||
<DockPanel Grid.Row="2" Margin="0,6,0,0" VerticalAlignment="Bottom">
|
||||
<Button DockPanel.Dock="Left" Width="24" Height="20" Margin="0,0,6,0"
|
||||
Style="{StaticResource YtButtonSecondary}" Padding="0"
|
||||
Command="{Binding ToggleMicMuteCommand}" ToolTip="{Binding MicMuteText}">
|
||||
<Ellipse Width="9" Height="9" Fill="{Binding MicStatusBrush}"/>
|
||||
</Button>
|
||||
<TextBlock DockPanel.Dock="Right" Width="56" TextAlignment="Right" FontSize="10"
|
||||
FontFamily="{StaticResource FontFamily.Mono}" VerticalAlignment="Center"
|
||||
Foreground="{StaticResource Brush.OnSurfaceVariant}"
|
||||
Text="{Binding MicVolumeDbLabel}"/>
|
||||
<Slider Minimum="-60" Maximum="6" SmallChange="0.5" LargeChange="3"
|
||||
Value="{Binding MicVolumeDb, Mode=TwoWay}" VerticalAlignment="Center"/>
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
<Border Grid.Column="1" Background="{StaticResource Brush.SurfaceContainerLow}" CornerRadius="{StaticResource Radius.Sm}" Margin="4,0" Padding="10,6">
|
||||
<StackPanel>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>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.
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>"-6.0 dB" beside the fader; "−∞ dB" at the floor.</summary>
|
||||
public string MicVolumeDbLabel => Helpers.GainDb.Label(MicVolumeDb) + " dB";
|
||||
|
||||
/// <summary>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.</summary>
|
||||
public double MicMeterFraction => MeterLevel;
|
||||
|
||||
/// <summary>Read-only: true whenever the volume is 0. Only MicVolume may
|
||||
/// change it, so the slider and the speaker can never disagree.</summary>
|
||||
public bool MicMuted => _micMuted;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
using ytLive.ViewModels;
|
||||
|
||||
namespace ytLive.Tests;
|
||||
|
||||
/// <summary>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.
|
||||
///
|
||||
/// <para>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.</para></summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user