diff --git a/Controls/AudioDock.xaml b/Controls/AudioDock.xaml
index 049adb2..40ae1af 100644
--- a/Controls/AudioDock.xaml
+++ b/Controls/AudioDock.xaml
@@ -170,10 +170,42 @@
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Controls/AudioDock.xaml.cs b/Controls/AudioDock.xaml.cs
index 1fcd815..2a7bff9 100644
--- a/Controls/AudioDock.xaml.cs
+++ b/Controls/AudioDock.xaml.cs
@@ -39,6 +39,13 @@ public partial class AudioDock : UserControl
vm.OpenMicPickerCommand.Execute(null);
}
+ 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 TraxPlayPause_Click(object sender, RoutedEventArgs e)
{
if (DataContext is ViewModels.MainViewModel vm) vm.TraxLeftClick();
diff --git a/ViewModels/MainViewModel.Audio.cs b/ViewModels/MainViewModel.Audio.cs
index 5203913..a892f7f 100644
--- a/ViewModels/MainViewModel.Audio.cs
+++ b/ViewModels/MainViewModel.Audio.cs
@@ -194,11 +194,21 @@ public partial class MainViewModel
set
{
var clamped = Math.Clamp(value, -500, 500);
- if (SetProperty(ref _audioSyncOffsetMs, clamped))
- ScheduleSave();
+ if (!SetProperty(ref _audioSyncOffsetMs, clamped)) return;
+ OnPropertyChanged(nameof(SyncOffsetLabel));
+ ScheduleSave();
}
}
+ /// "0 ms (aligned)", "+50 ms", "−120 ms" — what the dock shows beside the slider.
+ public string SyncOffsetLabel => AudioSyncOffsetMs == 0
+ ? "0 ms (aligned)"
+ : (AudioSyncOffsetMs > 0 ? "+" : "−") + Math.Abs(AudioSyncOffsetMs) + " ms";
+
+ /// Preset buttons in the LIP-SYNC bay. 0 is the detent: the offset that means "the
+ /// lip-sync is right", so it deserves one click, not a drag.
+ internal void SetSyncOffset(int milliseconds) => AudioSyncOffsetMs = milliseconds;
+
private void ToggleMicMute()
{
if (MicMuted)
diff --git a/ytLive.Tests/SyncBayTests.cs b/ytLive.Tests/SyncBayTests.cs
new file mode 100644
index 0000000..2a01e75
--- /dev/null
+++ b/ytLive.Tests/SyncBayTests.cs
@@ -0,0 +1,30 @@
+using Xunit;
+using ytLive.ViewModels;
+
+namespace ytLive.Tests;
+
+/// The LIP-SYNC bay re-houses the offset that shipped as TASK 22, with the two things the
+/// dock asks of it: a readable readout ("0 ms (aligned)") and 0 as a one-click detent. The offset
+/// itself is clamped and persisted by the existing property — this pins the surface.
+///
+public class SyncBayTests
+{
+ [Fact]
+ public void The_lip_sync_bay_uses_the_persisted_offset_with_a_zero_detent()
+ {
+ var dock = SourceAssert.Text("Controls", "AudioDock.xaml");
+ Assert.Contains("Value=\"{Binding AudioSyncOffsetMs, Mode=TwoWay}\"", dock);
+ Assert.Contains("Text=\"{Binding SyncOffsetLabel}\"", dock);
+ Assert.Contains("Click=\"SyncPreset_Click\"", dock);
+ Assert.Contains("Tag=\"0\"", dock);
+ Assert.Contains("Minimum=\"-200\" Maximum=\"200\"", dock);
+
+ // The readout names the good state, and signs the others the way a person reads them.
+ Assert.Matches(@"IsSnapToTickEnabled=""True""", dock);
+
+ var type = typeof(MainViewModel);
+ Assert.NotNull(type.GetProperty("AudioSyncOffsetMs"));
+ Assert.NotNull(type.GetProperty("SyncOffsetLabel"));
+ Assert.NotNull(type.GetMethod("SetSyncOffset", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance));
+ }
+}