The webcam's frames reach the compositor later than the mic's samples reach the audio pipe, so the voice runs ahead of the lips. The existing lip-sync offset could not fix that: it delays the WHOLE mix, which drags desktop and alert audio — neither of which has a camera to follow — out of sync in the process. So the delay line learned channels (mono for a source, stereo for the mix, offset always in milliseconds so the frame count is what stays constant), and FillAndMix runs a second line over micChunk before the mix. Slider-side: presets off / 150 / 200 in the LIP-SYNC bay under the whole-mix control, persisted as Audio.MicSyncOffsetMs. Default 175 ms is the creator's own number for a C920 pipeline (they asked for +150..+200). It is a setting, not a constant, because the right figure belongs to the camera and the machine — a faster camera wants less, no camera wants none. Both delay lines are reconfigured every tick, so the 2026-09-12 trap still applies and is documented at both sites: a Configure that reallocates discards the buffered voice every 10 ms. 673 pass, 0 warnings. Ear check pending: this is an audio change.
101 lines
3.6 KiB
C#
101 lines
3.6 KiB
C#
using System.Windows;
|
||
using System.Windows.Controls;
|
||
using ytLive.Models;
|
||
|
||
namespace ytLive.Controls;
|
||
|
||
/// <summary>The sound bars (creator ruling 2026-10-30). This unit owns the SHELL: the piece of the
|
||
/// window it occupies and the collapse behaviour. The bays and the master readouts fill in over
|
||
/// the following units, so nothing here pretends to be a working meter.
|
||
///
|
||
/// <para>Collapse is driven by the window height, not a user setting: at 1366×768 a full dock
|
||
/// squeezes the preview, and the creator should not have to manage that.</para></summary>
|
||
public partial class AudioDock : UserControl
|
||
{
|
||
private Window? _window;
|
||
|
||
public AudioDock()
|
||
{
|
||
InitializeComponent();
|
||
Loaded += OnLoaded;
|
||
Unloaded += OnUnloaded;
|
||
}
|
||
|
||
/// <summary>Is the dock showing only the telemetry strip?</summary>
|
||
internal bool IsCollapsed { get; private set; }
|
||
|
||
/// <summary>Test seam: the same code the window's SizeChanged runs, without a window.</summary>
|
||
internal void ApplyWindowHeight(double windowHeight)
|
||
{
|
||
var collapsed = AudioDockLayout.IsCollapsed(windowHeight);
|
||
IsCollapsed = collapsed;
|
||
Height = AudioDockLayout.HeightFor(windowHeight);
|
||
Bays.Visibility = collapsed ? Visibility.Collapsed : Visibility.Visible;
|
||
}
|
||
|
||
private void MicLabel_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||
{
|
||
if (DataContext is ViewModels.MainViewModel vm && vm.OpenMicPickerCommand.CanExecute(null))
|
||
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;
|
||
if (sender is Button { Tag: string tag } && int.TryParse(tag, out var ms))
|
||
vm.SetSyncOffset(ms);
|
||
}
|
||
|
||
private void MicSyncPreset_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
if (DataContext is not ViewModels.MainViewModel vm) return;
|
||
if (sender is Button { Tag: string tag } && int.TryParse(tag, out var ms))
|
||
vm.MicSyncOffsetMs = ms;
|
||
}
|
||
|
||
private void TraxPlayPause_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
if (DataContext is ViewModels.MainViewModel vm) vm.TraxLeftClick();
|
||
}
|
||
|
||
private void TraxLabel_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||
{
|
||
if (DataContext is ViewModels.MainViewModel vm) vm.TraxLeftClick();
|
||
}
|
||
|
||
private void TraxLabel_MouseRightButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||
{
|
||
if (DataContext is ViewModels.MainViewModel vm) vm.ChooseTrack();
|
||
}
|
||
|
||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||
{
|
||
_window = Window.GetWindow(this);
|
||
if (_window == null)
|
||
{
|
||
ApplyWindowHeight(AudioDockLayout.CollapseBelowHeight); // no host: show the dock
|
||
return;
|
||
}
|
||
_window.SizeChanged += OnWindowSizeChanged;
|
||
ApplyWindowHeight(_window.ActualHeight > 0 ? _window.ActualHeight : _window.Height);
|
||
}
|
||
|
||
private void OnUnloaded(object sender, RoutedEventArgs e)
|
||
{
|
||
if (_window != null) _window.SizeChanged -= OnWindowSizeChanged;
|
||
_window = null;
|
||
}
|
||
|
||
private void OnWindowSizeChanged(object sender, SizeChangedEventArgs e) => ApplyWindowHeight(e.NewSize.Height);
|
||
}
|