Mic audio UX: realtime read-only meter + mic picker

- Meter is a READ-ONLY realtime level display: fill = Math.Min(1, AudioLevel * MicVolume) (AudioLevel = live input from the future mixer, 0 with no input; MicVolume is gain on ambient noise). While the slider is dragged the bar previews the slider position (PreviewMouseLeftButtonDown/Up + LostMouseCapture -> SetVolumeAdjusting); on release it returns to the live level (bounces to 0 with no input). Clicking the meter does nothing.
- MicVolume drives MicMuted (read-only, muted <=> volume 0): sliding to 0 flips the speaker to the red slashed mute icon, sliding up from 0 clears it; speaker button runs volume to 0 or restores the prior level (_volumeBeforeMute, default 0.8) and flashes the meter to the restored position for ~300ms (BeginVolumeFlash/EndVolumeFlash DispatcherTimer, cancelled if the slider is grabbed).
- MIC label opens MicPickerDialog (WinRT audio-capture device enumeration, mirrors camera picker, no new packages); the chosen voice source name shows left-justified INSIDE the meter bar (fill at 75% opacity so text + ruler markings show through).
- New files: Services/IMicrophoneEnumerator, MicrophoneDeviceInfo, WinRtMicrophoneEnumerator; ViewModels/MicPickerViewModel; MicPickerDialog.
- Docs updated (ai.md, TASKS.md, Services/index.md, ViewModels/index.md); build 0 warnings, 65 tests passing
This commit is contained in:
2026-08-07 17:44:16 -07:00
parent 4b3fb04322
commit b00a4cbd5e
14 changed files with 536 additions and 78 deletions
+76 -50
View File
@@ -753,8 +753,70 @@
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- Line 1: bitrate / fps · quality -->
<Grid Grid.Row="0">
<!-- Line 1: sound meter + volume control, centered beneath the
middle (preview) panel. Audio is KISS: desktop/game audio is
automatic ("it just is" — WASAPI loopback at unity, zero UI);
the creator's only audio control is the mic — meter, volume, mute. -->
<StackPanel Grid.Row="0" Orientation="Horizontal"
HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Text="MIC" Foreground="#a0a0b0" FontSize="11" FontWeight="SemiBold"
VerticalAlignment="Center" Margin="0,0,8,0" Cursor="Hand"
ToolTip="Choose the microphone (voice source)"
MouseLeftButtonUp="MicLabel_MouseLeftButtonUp"/>
<!-- Sound meter: muted track with a ruler scale, zone tints at
the yellow (60%) and red (80%) starts. READ-ONLY realtime
level display (fill = live level scaled by volume). -->
<Border Width="288" Height="14" CornerRadius="7" Background="#3a3b52" Margin="0,0,8,0"
ClipToBounds="True" VerticalAlignment="Center">
<Grid>
<Rectangle Width="57" Fill="#4a4520" HorizontalAlignment="Left" Margin="173,0,0,0"
VerticalAlignment="Stretch"/>
<Rectangle Width="58" Fill="#4a2222" HorizontalAlignment="Left" Margin="230,0,0,0"
VerticalAlignment="Stretch"/>
<!-- Mic source name, left-justified inside the bar; the
semi-transparent fill lets it (and the markings) show through. -->
<TextBlock Text="{Binding MicSourceName}" Foreground="#b8b8c8" FontSize="10"
VerticalAlignment="Center" Margin="6,0,0,0" MaxWidth="270"
TextTrimming="CharacterEllipsis"
Visibility="{Binding MicSourceName, Converter={StaticResource NotNullToVis}}"/>
<Border HorizontalAlignment="Left" Width="{Binding MeterFillWidth}"
Background="{Binding MeterBrush}" Opacity="0.75"/>
<Rectangle Width="1" Height="10" Fill="#26000000" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="36,0,0,0"/>
<Rectangle Width="1" Height="10" Fill="#26000000" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="72,0,0,0"/>
<Rectangle Width="1" Height="10" Fill="#26000000" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="108,0,0,0"/>
<Rectangle Width="1" Height="10" Fill="#26000000" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="144,0,0,0"/>
<Rectangle Width="1" Height="10" Fill="#26000000" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="180,0,0,0"/>
<Rectangle Width="1" Height="10" Fill="#26000000" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="216,0,0,0"/>
<Rectangle Width="1" Height="10" Fill="#26000000" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="252,0,0,0"/>
<Rectangle Width="2" Fill="#00000080" HorizontalAlignment="Left" Margin="173,0,0,0"
VerticalAlignment="Stretch"/>
<Rectangle Width="2" Fill="#00000080" HorizontalAlignment="Left" Margin="230,0,0,0"
VerticalAlignment="Stretch"/>
</Grid>
</Border>
<Grid Width="16" Height="16" Cursor="Hand" Margin="0,0,8,0"
VerticalAlignment="Center" ToolTip="{Binding MicMuteText}"
MouseLeftButtonUp="MicSpeaker_MouseLeftButtonUp">
<Path Fill="#d0d0d0" Stretch="Uniform"
Data="M3,9v6h4l5,5V4L7,9H3zM16.5,12c0,-1.77 -1,-3.29 -2.5,-4.03v8.05c1.5,-0.73 2.5,-2.25 2.5,-4.02zM14,3.23v2.06c2.89,0.86 5,3.54 5,6.71s-2.11,5.85 -5,6.71v2.06c4.01,-0.91 7,-4.49 7,-8.77s-2.99,-7.86 -7,-8.77z"
Visibility="{Binding MicMuted, Converter={StaticResource InverseBoolToVis}}"/>
<Path Fill="#ef4444" Stretch="Uniform"
Data="M3,9v6h4l5,5V4L7,9H3zM16.5,12c0,-1.77 -1,-3.29 -2.5,-4.03v8.05c1.5,-0.73 2.5,-2.25 2.5,-4.02zM14,3.23v2.06c2.89,0.86 5,3.54 5,6.71s-2.11,5.85 -5,6.71v2.06c4.01,-0.91 7,-4.49 7,-8.77s-2.99,-7.86 -7,-8.77z"
Visibility="{Binding MicMuted, Converter={StaticResource BoolToVis}}"/>
<Path Stroke="#ef4444" StrokeThickness="2.5" StrokeEndLineCap="Round" Stretch="Uniform"
Data="M21,3 L3,21"
Visibility="{Binding MicMuted, Converter={StaticResource BoolToVis}}"/>
</Grid>
<Slider Width="130" Minimum="0" Maximum="1"
Value="{Binding MicVolume, Mode=TwoWay}" VerticalAlignment="Center"
PreviewMouseLeftButtonDown="VolumeSlider_PreviewMouseLeftButtonDown"
PreviewMouseLeftButtonUp="VolumeSlider_PreviewMouseLeftButtonUp"
LostMouseCapture="VolumeSlider_LostMouseCapture"/>
</StackPanel>
<!-- Line 2: everything else — stream stats on the left, quality +
gear on the right. -->
<Grid Grid.Row="1" Margin="0,6,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
@@ -771,9 +833,20 @@
<TextBlock Text="FPS:" Style="{StaticResource YtLabel}" Margin="0,0,4,0"/>
<TextBlock Style="{StaticResource YtLabel}"
Foreground="White">
Foreground="White" Margin="0,0,24,0">
<Run Text="{Binding CurrentHealth.FPS, Mode=OneWay, StringFormat={}{0:0}}"/>
</TextBlock>
<TextBlock Text="Dropped:" Style="{StaticResource YtLabel}" Margin="0,0,4,0"/>
<TextBlock Text="{Binding CurrentHealth.DroppedFrames}" Style="{StaticResource YtLabel}"
Foreground="White" Margin="0,0,20,0"/>
<TextBlock Text="Duration:" Style="{StaticResource YtLabel}" Margin="0,0,4,0"/>
<TextBlock Text="{Binding CurrentHealth.StreamDuration}" Style="{StaticResource YtLabel}"
Foreground="White"/>
<TextBlock Text="{Binding CurrentHealth.HealthMessage}" Style="{StaticResource YtLabel}"
Foreground="#e94560" Margin="20,0,0,0"/>
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
@@ -801,53 +874,6 @@
</Button>
</StackPanel>
</Grid>
<!-- Line 2: dropped / duration (under bitrate / fps) + the MIC chip.
Audio is KISS: desktop/game audio is automatic ("it just is" —
WASAPI loopback at unity, zero UI). The creator's only audio
control is the mic — meter, volume, mute. -->
<Grid Grid.Row="1" Margin="0,6,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="Dropped:" Style="{StaticResource YtLabel}" Margin="0,0,4,0"/>
<TextBlock Text="{Binding CurrentHealth.DroppedFrames}" Style="{StaticResource YtLabel}"
Foreground="White" Margin="0,0,20,0"/>
<TextBlock Text="Duration:" Style="{StaticResource YtLabel}" Margin="0,0,4,0"/>
<TextBlock Text="{Binding CurrentHealth.StreamDuration}" Style="{StaticResource YtLabel}"
Foreground="White"/>
<TextBlock Text="{Binding CurrentHealth.HealthMessage}" Style="{StaticResource YtLabel}"
Foreground="#e94560" Margin="20,0,0,0"/>
</StackPanel>
<!-- Sound meter: plain filled bar, blank → green → yellow → red,
with zone markers at the yellow (60%) and red (80%) starts. -->
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Text="MIC" Foreground="#a0a0b0" FontSize="11" FontWeight="SemiBold" VerticalAlignment="Center"/>
<Border Width="360" Height="14" CornerRadius="7" Background="#1a1a2e" Margin="8,0,8,0"
ClipToBounds="True" VerticalAlignment="Center">
<Grid>
<Border HorizontalAlignment="Left" Width="{Binding MeterFillWidth}"
Background="{Binding MeterBrush}"/>
<Rectangle Width="2" Fill="#00000080" HorizontalAlignment="Left" Margin="216,0,0,0"
VerticalAlignment="Stretch"/>
<Rectangle Width="2" Fill="#00000080" HorizontalAlignment="Left" Margin="288,0,0,0"
VerticalAlignment="Stretch"/>
</Grid>
</Border>
<Slider Width="130" Minimum="0" Maximum="1"
Value="{Binding MicVolume, Mode=TwoWay}" VerticalAlignment="Center"/>
<Button Content="{Binding MicMuteText}" Style="{StaticResource YtButtonSecondary}"
Padding="10,3" Margin="8,0,0,0" Command="{Binding ToggleMicMuteCommand}"
VerticalAlignment="Center"/>
</StackPanel>
</Grid>
</Grid>
</Border>