feat: signed A/V sync offset (−500..+500), negative advances by eating live stream head (OBS eat-head semantics)

Positive offsets still delay the whole mix via the delay line (lip-sync fix);
negative offsets now ARM once at StartLive and drop |N| ms off the pipe's write
head so audio events land earlier when audio runs BEHIND video. Slider relabeled
AUDIO SYNC, Min −500, locked while live/recording (IsEditMode). LayoutStore and
VM clamp to −500..500.

OBS reference for eat-the-head negative sync: https://obsproject.com/kb/obs-studio/buffering-time (negative sync values pull audio earlier by discarding buffered player audio).

Test: StartLive_NegativeOffset_AdvancesAudio_ByDroppingTheStreamHead (6x0.9 head
must be eaten before 0.2 bed reaches the wire).
This commit is contained in:
2026-09-14 12:30:44 -07:00
parent 11a7af2dc0
commit b22d08eca6
12 changed files with 219 additions and 144 deletions
+6 -5
View File
@@ -155,16 +155,17 @@ public partial class MainViewModel
private set => SetProperty(ref _micSourceName, value);
}
/// <summary>Global audio-sync delay in milliseconds (0..500, default 0),
/// applied to the whole mix so audio lands on the video when it runs ahead
/// — the OBS fix for lip-sync. Consumed live by the mixer's delay line via
/// a Func seam; persisted with the layout.</summary>
/// <summary>Global audio-sync offset in milliseconds (−500..500, default 0):
/// POSITIVE delays the whole mix so audio lands on the video when it runs
/// AHEAD (OBS's lip-sync fix); NEGATIVE advances it by dropping the first
/// |N| ms of the live stream head when audio runs BEHIND the video.
/// Consumed live by the mixer via a Func seam; persisted with the layout.</summary>
public int AudioSyncOffsetMs
{
get => _audioSyncOffsetMs;
set
{
var clamped = Math.Clamp(value, 0, 500);
var clamped = Math.Clamp(value, -500, 500);
if (SetProperty(ref _audioSyncOffsetMs, clamped))
ScheduleSave();
}