ui(audio): master meter centred and biggest; TRAX gets its own bay and meter

Creator rulings, 2026-11-01.

- Master strip is 44px (from 36) and its meter is now the largest on screen: 420x14 with 32
  segments, centred under the preview, with a 12px MASTER label — bigger than the 9px bay
  labels. L/R imbalance is a flag on the left, MONO and the session clock on the right.
- The kbps / fps / dropped counters are gone from that line: the live readouts already sit
  above the preview, and the duplicate was noise rather than telemetry.
- TRAX is its own bay (MIC | GAME | TRAX | LIP-SYNC) with its own meter bound to
  TraxMeterFraction. It lived inside the game bay with no ladder, which meant whether the bed
  was actually playing could not be answered from the dock — and the player already measures
  its own output.
- Lip-sync is narrow (0.45 star) since it is one slider and three presets; the three sound bays
  are equal width, so their meters keep the same relative size.

AudioDockLayout moves with it (156 = 112 + 44; collapsed 44) and the layout test is updated.
TraxBayTests now asserts the opposite of the old ruling: the bay HAS a meter, and the game bay
no longer carries it.
This commit is contained in:
2026-11-01 13:48:28 -08:00
parent 9b337ad91b
commit f8e4f2cf29
5 changed files with 128 additions and 106 deletions
+104 -91
View File
@@ -10,16 +10,19 @@
<Grid.RowDefinitions> <Grid.RowDefinitions>
<!-- Master FIRST: during a live session attention is on the preview, so the <!-- Master FIRST: during a live session attention is on the preview, so the
master strip sits against it and the bays hang below (creator, 2026-10-30). --> master strip sits against it and the bays hang below (creator, 2026-10-30). -->
<RowDefinition Height="36"/> <RowDefinition Height="44"/>
<RowDefinition Height="*"/> <RowDefinition Height="*"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- Four bays: MIC | GAME | TRAX | SYNC. Filled in by the bay units. --> <!-- Four bays (creator ruling 2026-11-01): MIC | GAME | TRAX | LIP-SYNC. The three
sound bays share equal width so their meters stay the same relative size; lip-sync
is narrow because its function is one slider and three presets. -->
<Grid x:Name="Bays" Grid.Row="1" Margin="8,4,8,6"> <Grid x:Name="Bays" Grid.Row="1" Margin="8,4,8,6">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
<ColumnDefinition Width="1.4*"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="0.45*"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<!-- MIC --> <!-- MIC -->
@@ -66,7 +69,6 @@
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid Grid.Row="0"> <Grid Grid.Row="0">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@@ -106,59 +108,72 @@
Value="{Binding GameVolumeDb, Mode=TwoWay}" VerticalAlignment="Center"/> Value="{Binding GameVolumeDb, Mode=TwoWay}" VerticalAlignment="Center"/>
</DockPanel> </DockPanel>
<!-- TRAX: background music — one number, two buttons. No meter: it is
background, and the desktop ladder already carries it. -->
<Border Grid.Row="3" Margin="0,4,0,0" Padding="6,3" CornerRadius="{StaticResource Radius.Sm}"
Background="{StaticResource Brush.SurfaceContainerLowest}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="TRAX" FontSize="9" FontWeight="Bold" VerticalAlignment="Center"
Foreground="{StaticResource Brush.OnSurfaceVariant}"/>
<TextBlock Grid.Column="1" Margin="6,0,0,0" FontSize="10" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" Foreground="{StaticResource Brush.OnSurface}"
Text="{Binding TraxTrackLabel}" Cursor="Hand"
MouseLeftButtonUp="TraxLabel_MouseLeftButtonUp"
MouseRightButtonUp="TraxLabel_MouseRightButtonUp"/>
<Button Grid.Column="2" Width="20" Height="16" Margin="4,0,0,0" Padding="0"
Style="{StaticResource YtButtonSecondary}" Click="TraxPlayPause_Click"
ToolTip="{Binding TraxToolTip}">
<TextBlock FontSize="9" Foreground="{StaticResource Brush.OnSurface}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="▶"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsMusicPlaying}" Value="True">
<Setter Property="Text" Value="❚❚"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Button>
<StackPanel Grid.Column="3" Orientation="Horizontal" Margin="6,0,0,0" VerticalAlignment="Center">
<Button Content="−" Width="18" Height="16" Padding="0" FontSize="11"
Style="{StaticResource YtButtonSecondary}" Click="TraxStepDown_Click"
IsEnabled="{Binding CanLowerTrax}" ToolTip="5 dB quieter"/>
<TextBlock Width="52" TextAlignment="Center" FontSize="10" VerticalAlignment="Center"
FontFamily="{StaticResource FontFamily.Mono}"
Foreground="{StaticResource Brush.Tertiary}"
Text="{Binding TraxVolumeDbLabel}"/>
<Button Content="+" Width="18" Height="16" Padding="0" FontSize="11"
Style="{StaticResource YtButtonSecondary}" Click="TraxStepUp_Click"
IsEnabled="{Binding CanRaiseTrax}" ToolTip="5 dB louder"/>
</StackPanel>
</Grid>
</Border>
</Grid> </Grid>
</Border> </Border>
<!-- LIP-SYNC --> <!-- TRAX: its own bay and its own meter now (creator ruling 2026-11-01). It used to
<Border Grid.Column="2" Background="{StaticResource Brush.SurfaceContainerLow}" CornerRadius="{StaticResource Radius.Sm}" Margin="4,0,0,0" Padding="10,6"> be a strip inside the game bay because the desktop ladder "already carried it";
that hid whether the music was actually playing. -->
<Border Grid.Column="2" Background="{StaticResource Brush.SurfaceContainerLow}" CornerRadius="{StaticResource Radius.Sm}" Margin="4,0" Padding="10,6">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Background="{StaticResource Brush.SurfaceContainerHighest}" CornerRadius="{StaticResource Radius.Sm}" Padding="5,1">
<TextBlock Text="TRAX" FontSize="9" FontWeight="Bold" Foreground="{StaticResource Brush.OnSurfaceVariant}"/>
</Border>
<TextBlock Grid.Column="1" Margin="6,0,0,0" FontSize="11" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" Foreground="{StaticResource Brush.OnSurfaceVariant}"
Text="{Binding TraxTrackLabel}" Cursor="Hand"
MouseLeftButtonUp="TraxLabel_MouseLeftButtonUp"
MouseRightButtonUp="TraxLabel_MouseRightButtonUp"/>
</Grid>
<controls:SegmentMeter Grid.Row="1" Height="10" Margin="0,6,0,4" Orientation="Horizontal"
Level="{Binding TraxMeterFraction}" Segments="24"/>
<DockPanel Grid.Row="2" VerticalAlignment="Center">
<Button DockPanel.Dock="Left" Width="24" Height="20" Margin="0,0,6,0" Padding="0"
Style="{StaticResource YtButtonSecondary}" Click="TraxPlayPause_Click"
ToolTip="{Binding TraxToolTip}">
<TextBlock FontSize="9" Foreground="{StaticResource Brush.OnSurface}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="▶"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsMusicPlaying}" Value="True">
<Setter Property="Text" Value="❚❚"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Button>
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" VerticalAlignment="Center">
<Button Content="−" Width="18" Height="16" Padding="0" FontSize="11"
Style="{StaticResource YtButtonSecondary}" Click="TraxStepDown_Click"
IsEnabled="{Binding CanLowerTrax}" ToolTip="5 dB quieter"/>
<TextBlock Width="52" TextAlignment="Center" FontSize="10" VerticalAlignment="Center"
FontFamily="{StaticResource FontFamily.Mono}"
Foreground="{StaticResource Brush.OnSurfaceVariant}"
Text="{Binding TraxVolumeDbLabel}"/>
<Button Content="+" Width="18" Height="16" Padding="0" FontSize="11"
Style="{StaticResource YtButtonSecondary}" Click="TraxStepUp_Click"
IsEnabled="{Binding CanRaiseTrax}" ToolTip="5 dB louder"/>
</StackPanel>
</DockPanel>
</Grid>
</Border>
<!-- LIP-SYNC (narrow: one slider, three presets) -->
<Border Grid.Column="3" Background="{StaticResource Brush.SurfaceContainerLow}" CornerRadius="{StaticResource Radius.Sm}" Margin="4,0,0,0" Padding="10,6">
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
@@ -193,23 +208,20 @@
</Border> </Border>
</Grid> </Grid>
<!-- Master strip: always visible, even collapsed — it is the telemetry line. --> <!-- Master strip: always visible, even collapsed. Rebuilt 2026-11-01 — the meter is
CENTRED under the preview and is the biggest one on screen; MASTER's own label is
larger than the bay labels. The kbps / fps / dropped counters that used to end this
line are gone: the live readouts already sit above the preview. -->
<Border x:Name="MasterStrip" Grid.Row="0" Background="{StaticResource Brush.SurfaceContainerLowest}" Padding="12,0"> <Border x:Name="MasterStrip" Grid.Row="0" Background="{StaticResource Brush.SurfaceContainerLowest}" Padding="12,0">
<DockPanel VerticalAlignment="Center"> <Grid>
<TextBlock DockPanel.Dock="Left" Text="MASTER" FontSize="10" FontWeight="SemiBold" <Grid.ColumnDefinitions>
Foreground="{StaticResource Brush.OnSurfaceVariant}" VerticalAlignment="Center"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- One ladder for the mix, plus the imbalance flag: two ladders would cost the <!-- Left: the imbalance flag, the one case that silently ruins a mix. -->
width that the telemetry needs, and the flag is the case that matters. --> <Border Grid.Column="0" Margin="0,0,8,0" Padding="5,1" VerticalAlignment="Center"
<controls:SegmentMeter DockPanel.Dock="Left" Width="120" Height="8" Margin="10,0,0,0"
Orientation="Horizontal" Segments="24"
Level="{Binding MasterMeterFraction}" VerticalAlignment="Center"/>
<TextBlock DockPanel.Dock="Left" Width="62" Margin="6,0,0,0" FontSize="10"
FontFamily="{StaticResource FontFamily.Mono}" VerticalAlignment="Center"
Foreground="{StaticResource Brush.OnSurfaceVariant}"
Text="{Binding MasterLevelLabel}"/>
<Border DockPanel.Dock="Left" Margin="6,0,0,0" Padding="5,1" VerticalAlignment="Center"
CornerRadius="{StaticResource Radius.Sm}" Background="{StaticResource Brush.SurfaceContainerHigh}" CornerRadius="{StaticResource Radius.Sm}" Background="{StaticResource Brush.SurfaceContainerHigh}"
Visibility="{Binding MasterImbalance, Converter={StaticResource BoolToVis}}" Visibility="{Binding MasterImbalance, Converter={StaticResource BoolToVis}}"
ToolTip="Left and right differ by more than 6 dB — one side may be inaudible."> ToolTip="Left and right differ by more than 6 dB — one side may be inaudible.">
@@ -217,32 +229,33 @@
Foreground="{StaticResource Brush.Attention}"/> Foreground="{StaticResource Brush.Attention}"/>
</Border> </Border>
<!-- MONO: the accessibility switch, one click, remembered. --> <!-- Centre: MASTER + the long meter + its reading. -->
<ToggleButton DockPanel.Dock="Left" Margin="10,0,0,0" Height="20" Padding="8,0" <StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
Style="{StaticResource SegmentToggle}" IsChecked="{Binding MonoOutput, Mode=TwoWay}" <TextBlock Text="MASTER" FontSize="12" FontWeight="Bold" VerticalAlignment="Center"
ToolTip="Fold the output to mono — both ears hear everything"> Foreground="{StaticResource Brush.OnSurface}"/>
<TextBlock Text="MONO" FontSize="10" FontWeight="Bold" <controls:SegmentMeter Width="420" Height="14" Margin="12,0,0,0"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=ToggleButton}}"/> Orientation="Horizontal" Segments="32"
</ToggleButton> Level="{Binding MasterMeterFraction}" VerticalAlignment="Center"/>
<TextBlock Width="72" Margin="10,0,0,0" FontSize="12"
FontFamily="{StaticResource FontFamily.Mono}" VerticalAlignment="Center"
Foreground="{StaticResource Brush.OnSurface}"
Text="{Binding MasterLevelLabel}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center"> <!-- Right: MONO (accessibility, remembered) + the session clock. -->
<TextBlock FontSize="10" FontFamily="{StaticResource FontFamily.Mono}" Margin="0,0,12,0" <StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
Foreground="{StaticResource Brush.OnSurfaceVariant}"> <ToggleButton Height="20" Padding="8,0"
<Run Text="{Binding CurrentHealth.CurrentBitrate, StringFormat={}{0:0} kbps}"/> Style="{StaticResource SegmentToggle}" IsChecked="{Binding MonoOutput, Mode=TwoWay}"
</TextBlock> ToolTip="Fold the output to mono — both ears hear everything">
<TextBlock FontSize="10" FontFamily="{StaticResource FontFamily.Mono}" Margin="0,0,12,0" <TextBlock Text="MONO" FontSize="10" FontWeight="Bold"
Foreground="{StaticResource Brush.OnSurfaceVariant}"> Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=ToggleButton}}"/>
<Run Text="{Binding CurrentHealth.FPS, StringFormat={}{0:0} fps}"/> </ToggleButton>
</TextBlock> <TextBlock FontSize="11" FontFamily="{StaticResource FontFamily.Mono}" Margin="12,0,0,0"
<TextBlock FontSize="10" FontFamily="{StaticResource FontFamily.Mono}" Margin="0,0,12,0" VerticalAlignment="Center"
Foreground="{StaticResource Brush.OnSurfaceVariant}">
<Run Text="drop "/><Run Text="{Binding CurrentHealth.DroppedFrames}"/>
</TextBlock>
<TextBlock FontSize="10" FontFamily="{StaticResource FontFamily.Mono}"
Foreground="{StaticResource Brush.OnSurfaceVariant}" Foreground="{StaticResource Brush.OnSurfaceVariant}"
Text="{Binding CurrentHealth.StreamDuration, StringFormat={}{0:hh\:mm\:ss}}"/> Text="{Binding CurrentHealth.StreamDuration, StringFormat={}{0:hh\:mm\:ss}}"/>
</StackPanel> </StackPanel>
</DockPanel> </Grid>
</Border> </Border>
</Grid> </Grid>
</Border> </Border>
+5 -2
View File
@@ -6,6 +6,9 @@ namespace ytLive.Models;
/// telemetry strip when the window is short, because at 1366×768 a full dock leaves ~398px for the /// telemetry strip when the window is short, because at 1366×768 a full dock leaves ~398px for the
/// preview and the composition starts letterboxing into a postage stamp.</para> /// preview and the composition starts letterboxing into a postage stamp.</para>
/// ///
/// <para>2026-11-01: the master strip grew from 36 to 44 so its meter could be the biggest one on
/// screen (centred under the preview), so both constants moved with it — 156 = 112 bays + 44.</para>
///
/// <para>Pure, so the switch is testable without a window.</para></summary> /// <para>Pure, so the switch is testable without a window.</para></summary>
public static class AudioDockLayout public static class AudioDockLayout
{ {
@@ -15,8 +18,8 @@ public static class AudioDockLayout
/// <summary>148 = 112 of bays + the 36px master strip. The mockup wanted 168; the creator /// <summary>148 = 112 of bays + the 36px master strip. The mockup wanted 168; the creator
/// read that as wasted space under a 154px scene strip, so the bays lost 20px of air.</summary> /// read that as wasted space under a 154px scene strip, so the bays lost 20px of air.</summary>
public const double ExpandedHeight = 148; public const double ExpandedHeight = 156;
public const double CollapsedHeight = 36; public const double CollapsedHeight = 44;
public static bool IsCollapsed(double windowHeight) => windowHeight < CollapseBelowHeight; public static bool IsCollapsed(double windowHeight) => windowHeight < CollapseBelowHeight;
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -26,7 +26,7 @@ public class AudioDockLayoutTests
Assert.True(AudioDockLayout.IsCollapsed(AudioDockLayout.CollapseBelowHeight - 1)); Assert.True(AudioDockLayout.IsCollapsed(AudioDockLayout.CollapseBelowHeight - 1));
// The two heights are the design's numbers, not whatever looked fine. // The two heights are the design's numbers, not whatever looked fine.
Assert.Equal(148, AudioDockLayout.ExpandedHeight); // 112 bays + 36 strip Assert.Equal(156, AudioDockLayout.ExpandedHeight); // 112 bays + 44 master strip
Assert.Equal(36, AudioDockLayout.CollapsedHeight); Assert.Equal(44, AudioDockLayout.CollapsedHeight);
} }
} }
+16 -10
View File
@@ -5,14 +5,15 @@ using ytLive.ViewModels;
namespace ytLive.Tests; namespace ytLive.Tests;
/// <summary>The TRAX bay shows the track, plays/pauses it, and carries a level — but no meter: /// <summary>The TRAX bay shows the track, plays/pauses it, and carries a level. It got its OWN bay
/// nothing measures the bed, and a decorative ladder would be a lie. Its fader moves a gain the /// and its own meter on 2026-11-01 (creator ruling): the player measures its real output, so the
/// player actually applies, separate from the desktop-mute follow. /// ladder is not decorative, and living inside the game bay hid whether the bed was playing.
/// Its fader moves a gain the player actually applies, separate from the desktop-mute follow.
/// </summary> /// </summary>
public class TraxBayTests public class TraxBayTests
{ {
[Fact] [Fact]
public void The_trax_bay_binds_the_track_level_and_play_state_without_inventing_a_meter() public void The_trax_bay_is_its_own_bay_with_a_real_meter()
{ {
var dock = SourceAssert.Text("Controls", "AudioDock.xaml"); var dock = SourceAssert.Text("Controls", "AudioDock.xaml");
Assert.Contains("Text=\"{Binding TraxTrackLabel}\"", dock); Assert.Contains("Text=\"{Binding TraxTrackLabel}\"", dock);
@@ -26,13 +27,18 @@ public class TraxBayTests
Assert.Contains("Binding=\"{Binding IsMusicPlaying}\" Value=\"True\"", dock); Assert.Contains("Binding=\"{Binding IsMusicPlaying}\" Value=\"True\"", dock);
Assert.Contains("Click=\"TraxPlayPause_Click\"", dock); Assert.Contains("Click=\"TraxPlayPause_Click\"", dock);
// The bay DOES carry a ladder now — the player measures its own output, so "is music making // TRAX is its own bay, between GAME and LIP-SYNC, and it carries a real ladder bound to the
// sound" is answerable without hearing it (a play glyph cannot answer that). // player's own level — "is the bed making sound" is not answerable from a play glyph.
// The TRAX row lives under the desktop volume, inside the GAME bay. var traxRow = dock[dock.IndexOf("<!-- TRAX: its own bay", System.StringComparison.Ordinal)
var traxRow = dock[dock.IndexOf("Text=\"TRAX\"", System.StringComparison.Ordinal) ..dock.IndexOf("<!-- LIP-SYNC", System.StringComparison.Ordinal)];
..dock.IndexOf("Text=\"LIP-SYNC\"", System.StringComparison.Ordinal)];
Assert.Contains("TraxTrackLabel", traxRow); Assert.Contains("TraxTrackLabel", traxRow);
Assert.DoesNotContain("SegmentMeter", traxRow); Assert.Contains("SegmentMeter", traxRow);
Assert.Contains("Level=\"{Binding TraxMeterFraction}\"", traxRow);
// …and the GAME bay no longer embeds it.
var gameBay = dock[dock.IndexOf("<!-- GAME / PC", System.StringComparison.Ordinal)
..dock.IndexOf("<!-- TRAX: its own bay", System.StringComparison.Ordinal)];
Assert.DoesNotContain("TraxTrackLabel", gameBay);
foreach (var name in new[] { "TraxVolumeDb", "TraxVolumeDbLabel", "TraxTrackLabel", "CanRaiseTrax", "CanLowerTrax" }) foreach (var name in new[] { "TraxVolumeDb", "TraxVolumeDbLabel", "TraxTrackLabel", "CanRaiseTrax", "CanLowerTrax" })
Assert.NotNull(typeof(MainViewModel).GetProperty(name)); Assert.NotNull(typeof(MainViewModel).GetProperty(name));