b00a4cbd5e
- 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
87 lines
2.4 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|