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:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user