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
+4 -1
View File
@@ -233,6 +233,7 @@ public partial class MainViewModel : ViewModelBase
// the same, already-vetted device (green dot) or reports it missing
// (yellow) instead of silently falling back to the default.
MicSourceName = _layoutStore.LoadMicSourceName();
AudioSyncOffsetMs = _layoutStore.LoadAudioSyncOffsetMs();
_cameraEnumerator = new MediaCaptureCameraEnumerator();
_cameraManager = new CameraManager(
@@ -258,7 +259,8 @@ public partial class MainViewModel : ViewModelBase
new WasapiLoopbackAudioSource(),
message => AppLog.Write(message),
micGain: _gainProvider.MicGain,
loopbackGain: _gainProvider.LoopbackGain);
loopbackGain: _gainProvider.LoopbackGain,
syncOffsetMs: () => AudioSyncOffsetMs);
_audioMixer.MicLevelChanged += OnMicLevelChanged;
_audioMixer.LoopbackLevelChanged += OnLoopbackLevelChanged;
_audioMixer.MicConnected += OnMicConnected;
@@ -409,6 +411,7 @@ public partial class MainViewModel : ViewModelBase
_layoutStore.Save(Scenes, _webcam, _socials, _music);
_layoutStore.SaveTransitionSettings(
SelectedTransitionType.ToString(), TransitionDurationMs);
_layoutStore.SaveAudioSyncOffsetMs(AudioSyncOffsetMs);
}
catch (Exception ex)
{