Files
ytLlive/ViewModels/MicPickerViewModel.cs
gramps b00a4cbd5e 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
2026-08-07 17:44:16 -07:00

87 lines
2.4 KiB
C#

using System.Collections.ObjectModel;
using System.Windows.Input;
using ytLive.Helpers;
using ytLive.Services;
namespace ytLive.ViewModels;
public class MicPickerCandidate
{
public MicrophoneDeviceInfo Device { get; }
public string Name => Device.DisplayName;
public MicPickerCandidate(MicrophoneDeviceInfo device)
{
Device = device;
}
}
public class MicPickerViewModel : ViewModelBase
{
private readonly IMicrophoneEnumerator _enumerator;
private MicPickerCandidate? _selectedMic;
private bool _isLoading = true;
public ObservableCollection<MicPickerCandidate> Microphones { get; } = new();
public bool IsLoading
{
get => _isLoading;
private set => SetProperty(ref _isLoading, value);
}
public bool HasMicrophones => !IsLoading && Microphones.Count > 0;
public bool NoMicrophones => !IsLoading && Microphones.Count == 0;
public MicPickerCandidate? SelectedMic
{
get => _selectedMic;
set
{
if (SetProperty(ref _selectedMic, value))
CommandManager.InvalidateRequerySuggested();
}
}
public ICommand UseSelectedCommand { get; }
public ICommand CancelCommand { get; }
public event Action<MicrophoneDeviceInfo>? UseRequested;
public event Action? CancelRequested;
public MicPickerViewModel(IMicrophoneEnumerator enumerator)
{
_enumerator = enumerator;
UseSelectedCommand = new RelayCommand(
_ => { if (SelectedMic != null) UseRequested?.Invoke(SelectedMic.Device); },
_ => SelectedMic != null);
CancelCommand = new RelayCommand(_ => CancelRequested?.Invoke());
_ = LoadAsync();
}
private async Task LoadAsync()
{
IsLoading = true;
try
{
var devices = await _enumerator.GetMicrophonesAsync();
Microphones.Clear();
foreach (var device in devices)
Microphones.Add(new MicPickerCandidate(device));
if (Microphones.Count > 0)
SelectedMic = Microphones[0];
}
catch (Exception ex)
{
AppLog.Write($"MicPickerViewModel: enumerating microphones failed: {ex.Message}");
}
finally
{
IsLoading = false;
OnPropertyChanged(nameof(HasMicrophones));
OnPropertyChanged(nameof(NoMicrophones));
CommandManager.InvalidateRequerySuggested();
}
}
}