Files
gramps 7f89534309 feat(audio): microphone-only sync delay, on by default at 175 ms
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.
2026-11-12 11:15:50 -08:00

101 lines
3.6 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}