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:
@@ -495,3 +495,25 @@ stamp BEFORE theorizing. (Also this session: a sentinel-byte test where the sour
|
||||
GENERATE the sentinel, and a fixed-point bilinear that shifted BOTH stages and silently drew nothing —
|
||||
see the rawvideo recipe.)
|
||||
|
||||
## Signed A/V sync: negative offset = eat the buffer head (OBS semantics), armed at go-live
|
||||
|
||||
(2026-09-14) The ring-backlog fix shrank A/V lag from ~2.2s to ~0.54s, and ~300ms of that was
|
||||
baked into the DB (`Audio.SyncOffsetMs=300`) via a POSITIVE-only control. For audio running BEHIND
|
||||
video you cannot push audio later — positive delay makes it worse. OBS's answer is a NEGATIVE offset
|
||||
that eats the buffer head: drop the first |N| ms of the written stream so every audio event lands
|
||||
|N| ms EARLIER relative to video. It only makes sense at the head of the stream, so it is armed once
|
||||
at `StartLive` (positive stays live-reactive via the delay line). Rule: record the signed semantics
|
||||
together — positive = delay (ahead), negative = eat-head advance (behind) — and lock the control
|
||||
while live (`IsEditMode`) since a mid-stream advance flip is meaningless. Test recipe:
|
||||
`StartLive_NegativeOffset_AdvancesAudio_ByDroppingTheStreamHead` (emit 6×0.9 into an 8-tick budget,
|
||||
long 0.2 bed, wire must show only 0.2).
|
||||
|
||||
## A pipe-read test that samples one tick is a timing flake by construction
|
||||
|
||||
(2026-09-14, re-discovered) `Mix_HonorsProviderGains_AndGameMute_KillsTheLoopback` fails
|
||||
sporadically in ISOLATION (3/3 on the clean tree) but passes in the full suite: it reads exactly one
|
||||
tick's worth after muting, and if the pipe still carries pre-mute buffered bytes the read spikes at
|
||||
0.4 instead of silence. Any caller of `ReadFullyAsync` on a live pipe must either close/drain the
|
||||
pipe first or assert on a multi-tick window. Do not blame a sync change for this — verify the grain
|
||||
against the clean tree in the same mode before touching the mixer.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user