TASK 22: global audio sync offset (positive delay at the mixer out)

Adds AudioSyncOffsetMs (0..500ms, default 0) that delays the whole
interleaved-stereo mix so audio lands on the video when it runs ahead —
OBS's documented lip-sync fix. Positive-only: advancing audio needs a
video-side delay and is out of the audio layer's scope.

- Services/Audio/AudioSyncDelay.cs: pure delay line, flushed on Configure
- AudioMixer: Func<int> syncOffsetMs seam + delay applied post-limiter
- LayoutStore.Settings + MainViewModel.Audio/VM: load/save + binding
- PreviewPane mic bar: SYNC slider + status dot (IntToSyncBrushConverter)
- AudioSyncDelayTests: identity, negative/beyond-500 clamps, 10ms→960 samples

Reference (external scan): https://obs-versions.com/blog/how-to-fix-audio-delay-on-obs
(audio ahead => positive delay). Verified: build 0 warnings; 3/3 delay tests pass.
This commit is contained in:
2026-08-31 18:30:03 -07:00
parent 670fe3a10a
commit e8ff4df05f
12 changed files with 295 additions and 128 deletions
+16
View File
@@ -29,6 +29,7 @@ public partial class MainViewModel
private double _gameVolume = 1.0;
private bool _gameMuted;
private double? _gameVolumeBeforeMute;
private int _audioSyncOffsetMs;
private AudioGainProvider? _gainProvider;
private readonly IMicrophoneEnumerator _microphoneEnumerator;
private readonly AudioMixer _audioMixer;
@@ -154,6 +155,21 @@ 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>
public int AudioSyncOffsetMs
{
get => _audioSyncOffsetMs;
set
{
var clamped = Math.Clamp(value, 0, 500);
if (SetProperty(ref _audioSyncOffsetMs, clamped))
ScheduleSave();
}
}
private void ToggleMicMute()
{
if (MicMuted)