feat(dock): TRAX becomes a background level, not a channel strip

Creator ruling (2026-10-30): TRAX is background music and is controlled separately from desktop and
mic — so KISS it:

- TRAX loses its own ladder and slider. It is one number plus ±5 dB buttons, disabled at the ends
  (−60 dB floor, +6 dB ceiling).
- It STARTS at −50 dB, taken from the creator's own meter readings, and moves in 5 dB clicks. If that
  proves inaudible on their setup, six clicks lands at −20 — the buttons exist for exactly that, and
  `TraxDefaultDb` is one constant.
- Layout follows: Desktop's volume now sits directly beneath its ladder, and the TRAX row (name,
  play/pause, level) sits beneath that, inside the GAME bay. The bays are MIC | GAME+TRAX | LIP-SYNC.
- The metering tap on the player stays (it is how "is the music producing sound" is answered) but has
  no ladder of its own; the desktop bed already shows the music.

Guards updated: TraxBayTests now asserts the stepper wiring and that the row has NO SegmentMeter;
GameBayTests asserts the desktop fader sits below the desktop ladder.
This commit is contained in:
2026-10-30 13:46:05 -07:00
parent 93dc6592ac
commit ec293e54b0
6 changed files with 112 additions and 97 deletions
+66 -89
View File
@@ -18,11 +18,11 @@
<Grid x:Name="Bays" Grid.Row="1" Margin="8,4,8,6">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="1.4*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- MIC -->
<Border Grid.Column="0" Background="{StaticResource Brush.SurfaceContainerLow}" CornerRadius="{StaticResource Radius.Sm}" Margin="0,0,4,0" Padding="10,6">
<Grid>
<Grid.RowDefinitions>
@@ -30,10 +30,7 @@
<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 Grid.Row="0" Cursor="Hand" ToolTip="Change microphone" MouseLeftButtonUp="MicLabel_MouseLeftButtonUp">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
@@ -42,14 +39,11 @@
<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'}"
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"
@@ -58,21 +52,22 @@
</Button>
<TextBlock DockPanel.Dock="Right" Width="56" TextAlignment="Right" FontSize="10"
FontFamily="{StaticResource FontFamily.Mono}" VerticalAlignment="Center"
Foreground="{StaticResource Brush.OnSurfaceVariant}"
Text="{Binding MicVolumeDbLabel}"/>
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>
<!-- GAME / PC, then TRAX beneath it: meter -> desktop volume -> TRAX line -->
<Border Grid.Column="1" 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="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
@@ -82,15 +77,10 @@
<Border Background="{StaticResource Brush.SurfaceContainerHighest}" CornerRadius="{StaticResource Radius.Sm}" Padding="5,1">
<TextBlock Text="GAME / PC" FontSize="9" FontWeight="Bold" Foreground="{StaticResource Brush.OnSurfaceVariant}"/>
</Border>
<!-- TRAX plays through the default device, so the loopback bed carries it:
this ladder shows everything the viewer hears from the desktop,
music included. The TRAX bay's ladder is a tap on the player. -->
<TextBlock Grid.Column="1" Margin="6,0,0,0" FontSize="11" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" Foreground="{StaticResource Brush.OnSurfaceVariant}"
Text="Desktop loopback · incl. TRAX"
ToolTip="Everything playing on the desktop — including TRAX, which rides the same device."/>
<!-- The duck badge only exists while the duck is happening, and it carries
the gain the mixer actually applied. -->
<Border Grid.Column="2" Background="{StaticResource Brush.SurfaceContainerHighest}"
CornerRadius="{StaticResource Radius.Sm}" Padding="5,1"
Visibility="{Binding IsGameDucking, Converter={StaticResource BoolToVis}}">
@@ -99,10 +89,11 @@
</Border>
</Grid>
<controls:SegmentMeter Grid.Row="1" Height="10" Margin="0,6,0,0" Orientation="Horizontal"
<controls:SegmentMeter Grid.Row="1" Height="10" Margin="0,6,0,4" Orientation="Horizontal"
Level="{Binding GameMeterFraction}" Segments="24"/>
<DockPanel Grid.Row="2" Margin="0,6,0,0" VerticalAlignment="Bottom">
<!-- Desktop volume, directly beneath its meter. -->
<DockPanel Grid.Row="2" VerticalAlignment="Center">
<Button DockPanel.Dock="Left" Width="24" Height="20" Margin="0,0,6,0"
Style="{StaticResource YtButtonSecondary}" Padding="0"
Command="{Binding ToggleGameMuteCommand}" ToolTip="Mute desktop audio">
@@ -110,79 +101,69 @@
</Button>
<TextBlock DockPanel.Dock="Right" Width="56" TextAlignment="Right" FontSize="10"
FontFamily="{StaticResource FontFamily.Mono}" VerticalAlignment="Center"
Foreground="{StaticResource Brush.OnSurfaceVariant}"
Text="{Binding GameVolumeDbLabel}"/>
Foreground="{StaticResource Brush.OnSurfaceVariant}" Text="{Binding GameVolumeDbLabel}"/>
<Slider Minimum="-60" Maximum="6" SmallChange="0.5" LargeChange="3"
Value="{Binding GameVolumeDb, Mode=TwoWay}" VerticalAlignment="Center"/>
</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>
</Border>
<Border Grid.Column="2" Background="{StaticResource Brush.SurfaceContainerLow}" CornerRadius="{StaticResource Radius.Sm}" Margin="4,0" Padding="10,6">
<!-- LIP-SYNC -->
<Border Grid.Column="2" Background="{StaticResource Brush.SurfaceContainerLow}" CornerRadius="{StaticResource Radius.Sm}" Margin="4,0,0,0" Padding="10,6">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</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>
<!-- The name is the control: left-click toggles play, right-click picks (§TRAX). -->
<TextBlock Grid.Column="1" Margin="6,0,0,0" FontSize="11" VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" Foreground="{StaticResource Brush.OnSurface}"
Text="{Binding TraxTrackLabel}"
MouseLeftButtonUp="TraxLabel_MouseLeftButtonUp"
MouseRightButtonUp="TraxLabel_MouseRightButtonUp" Cursor="Hand"/>
<Button Grid.Column="2" Width="22" Height="18" Style="{StaticResource YtButtonSecondary}" Padding="0"
Margin="4,0,0,0" Click="TraxPlayPause_Click" ToolTip="{Binding TraxToolTip}">
<TextBlock FontSize="10" 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>
<Border Grid.Column="3" Margin="4,0,0,0" Background="{StaticResource Brush.SurfaceContainerHighest}"
CornerRadius="{StaticResource Radius.Sm}" Padding="5,1"
Visibility="{Binding IsTraxDucking, Converter={StaticResource BoolToVis}}">
<TextBlock FontSize="9" FontWeight="Bold" FontFamily="{StaticResource FontFamily.Mono}"
Foreground="{StaticResource Brush.Secondary}" Text="DUCK"/>
</Border>
</Grid>
<controls:SegmentMeter Grid.Row="1" Height="10" Margin="0,6,0,6" Orientation="Horizontal"
Level="{Binding TraxMeterFraction}" Segments="24"/>
<DockPanel Grid.Row="1" Margin="0,22,0,0" VerticalAlignment="Bottom">
<TextBlock DockPanel.Dock="Right" Width="56" TextAlignment="Right" FontSize="10"
FontFamily="{StaticResource FontFamily.Mono}" VerticalAlignment="Center"
Foreground="{StaticResource Brush.OnSurfaceVariant}"
Text="{Binding TraxVolumeDbLabel}"/>
<Slider Minimum="-60" Maximum="6" SmallChange="0.5" LargeChange="3"
Value="{Binding TraxVolumeDb, Mode=TwoWay}" VerticalAlignment="Center"/>
</DockPanel>
</Grid>
</Border>
<Border Grid.Column="3" Background="{StaticResource Brush.SurfaceContainerLow}" CornerRadius="{StaticResource Radius.Sm}" Margin="4,0,0,0" Padding="10,6">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
@@ -193,13 +174,9 @@
</Border>
<TextBlock Grid.Column="1" Margin="6,0,0,0" FontSize="10" VerticalAlignment="Center"
FontFamily="{StaticResource FontFamily.Mono}" TextAlignment="Right"
Foreground="{StaticResource Brush.Tertiary}"
Text="{Binding SyncOffsetLabel}"/>
Foreground="{StaticResource Brush.Tertiary}" Text="{Binding SyncOffsetLabel}"/>
</Grid>
<StackPanel Grid.Row="1" VerticalAlignment="Bottom">
<!-- A detent marker would need a custom track; the 0 ms preset is the
detent as a button, which is also easier to hit than a snap. -->
<Slider Minimum="-200" Maximum="200" SmallChange="5" LargeChange="25"
TickFrequency="5" IsSnapToTickEnabled="True"
Value="{Binding AudioSyncOffsetMs, Mode=TwoWay}" VerticalAlignment="Center"/>
+10
View File
@@ -39,6 +39,16 @@ public partial class AudioDock : UserControl
vm.OpenMicPickerCommand.Execute(null);
}
private void TraxStepDown_Click(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.MainViewModel vm) vm.StepTrax(-1);
}
private void TraxStepUp_Click(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.MainViewModel vm) vm.StepTrax(+1);
}
private void SyncPreset_Click(object sender, RoutedEventArgs e)
{
if (DataContext is not ViewModels.MainViewModel vm) return;
+17 -1
View File
@@ -63,7 +63,21 @@ public partial class MainViewModel
? BarOnBrush
: TraxError != null ? BarOffBrush : TraxIdleBrush;
/// <summary>The dock's TRAX fader, in dB. Unity means the bed's design level, not the raw file.</summary>
/// <summary>Where TRAX starts: quiet background under the desktop bed. The ±5 dB buttons are the
/// control surface — a creator who wants it louder clicks up three times rather than hunting a
/// 6px thumb on a 148px dock. (Creator ruling 2026-10-30, from their own meter readings.)</summary>
public const double TraxDefaultDb = -50;
public const double TraxStepDb = 5;
/// <summary>+6 dB is the ceiling; −60 dB is silence. The buttons disable at both ends.</summary>
public bool CanRaiseTrax => TraxVolumeDb < Helpers.GainDb.MaxDb;
public bool CanLowerTrax => TraxVolumeDb > Helpers.GainDb.FloorDb;
public void StepTrax(int steps) => TraxVolumeDb += steps * TraxStepDb;
/// <summary>The dock's TRAX level, in dB. Unity means the bed's design level, not the raw file.</summary>
public double TraxVolumeDb
{
get => Helpers.GainDb.ToDb(_musicPlayer.UserGain);
@@ -74,6 +88,8 @@ public partial class MainViewModel
_musicPlayer.UserGain = (float)gain;
OnPropertyChanged(nameof(TraxVolumeDb));
OnPropertyChanged(nameof(TraxVolumeDbLabel));
OnPropertyChanged(nameof(CanRaiseTrax));
OnPropertyChanged(nameof(CanLowerTrax));
}
}
+1
View File
@@ -277,6 +277,7 @@ public partial class MainViewModel : ViewModelBase
MicSourceName = _layoutStore.LoadMicSourceName();
AudioSyncOffsetMs = _layoutStore.LoadAudioSyncOffsetMs();
MonoOutput = _layoutStore.LoadMonoOutput(); // applied to the mixer by the setter
_musicPlayer.UserGain = (float)Helpers.GainDb.FromDb(TraxDefaultDb); // background by default
try
{
var s = _layoutStore.GetSetting("ZoomShape");
+4
View File
@@ -15,6 +15,10 @@ public class GameBayTests
{
var dock = SourceAssert.Text("Controls", "AudioDock.xaml");
Assert.Contains("Value=\"{Binding GameVolumeDb, Mode=TwoWay}\"", dock);
// Desktop volume sits directly beneath its meter (creator's layout ruling).
var desktopMeter = dock.IndexOf("Level=\"{Binding GameMeterFraction}\"", System.StringComparison.Ordinal);
var desktopFader = dock.IndexOf("Value=\"{Binding GameVolumeDb, Mode=TwoWay}\"", System.StringComparison.Ordinal);
Assert.True(desktopFader > desktopMeter, "the desktop fader belongs under the desktop ladder");
Assert.Contains("Level=\"{Binding GameMeterFraction}\"", dock);
Assert.Contains("Text=\"{Binding GameDuckLabel}\"", dock);
Assert.Contains("Visibility=\"{Binding IsGameDucking, Converter={StaticResource BoolToVis}}\"", dock);
+14 -7
View File
@@ -16,20 +16,25 @@ public class TraxBayTests
{
var dock = SourceAssert.Text("Controls", "AudioDock.xaml");
Assert.Contains("Text=\"{Binding TraxTrackLabel}\"", dock);
Assert.Contains("Value=\"{Binding TraxVolumeDb, Mode=TwoWay}\"", dock);
// KISS (creator 2026-10-30): background music is one number and two ±5 dB buttons, not a
// channel strip — and it has no ladder of its own, because the desktop bed already carries it.
Assert.Contains("Click=\"TraxStepDown_Click\"", dock);
Assert.Contains("Click=\"TraxStepUp_Click\"", dock);
Assert.Contains("IsEnabled=\"{Binding CanLowerTrax}\"", dock);
Assert.Contains("IsEnabled=\"{Binding CanRaiseTrax}\"", dock);
Assert.Contains("Text=\"{Binding TraxVolumeDbLabel}\"", dock);
Assert.Contains("Binding=\"{Binding IsMusicPlaying}\" Value=\"True\"", dock);
Assert.Contains("Click=\"TraxPlayPause_Click\"", dock);
// The bay DOES carry a ladder now — the player measures its own output, so "is music making
// sound" is answerable without hearing it (a play glyph cannot answer that).
var traxBay = dock[dock.IndexOf("Text=\"TRAX\"", System.StringComparison.Ordinal)
// The TRAX row lives under the desktop volume, inside the GAME bay.
var traxRow = dock[dock.IndexOf("Text=\"TRAX\"", System.StringComparison.Ordinal)
..dock.IndexOf("Text=\"LIP-SYNC\"", System.StringComparison.Ordinal)];
Assert.Contains("TraxTrackLabel", traxBay);
Assert.Contains("SegmentMeter", traxBay);
Assert.Contains("TraxMeterFraction", traxBay);
Assert.Contains("TraxTrackLabel", traxRow);
Assert.DoesNotContain("SegmentMeter", traxRow);
foreach (var name in new[] { "TraxVolumeDb", "TraxVolumeDbLabel", "TraxTrackLabel", "IsTraxDucking", "TraxMeterFraction" })
foreach (var name in new[] { "TraxVolumeDb", "TraxVolumeDbLabel", "TraxTrackLabel", "CanRaiseTrax", "CanLowerTrax" })
Assert.NotNull(typeof(MainViewModel).GetProperty(name));
// The player gained a real user level, separate from the mute-follow LocalGain.
@@ -37,7 +42,9 @@ public class TraxBayTests
Assert.NotNull(typeof(ITraxPlayer).GetProperty("Level"));
Assert.NotNull(typeof(MusicPlayer).GetProperty("UserGain"));
// Unity means the bed's design level, and the fader's scale is the same one as the others.
// It starts as background: −50 dB, and the ±5 dB steps are how it moves.
Assert.Equal(-50, MainViewModel.TraxDefaultDb);
Assert.Equal(5, MainViewModel.TraxStepDb);
Assert.Equal(0, GainDb.ToDb(1f), 6);
Assert.Contains("∞", GainDb.Label(GainDb.FloorDb)); // true silence, not "-60.0"
}