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:
@@ -0,0 +1,77 @@
|
||||
using Xunit;
|
||||
using ytLive.Services.Audio;
|
||||
|
||||
namespace ytLive.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// TASK 22: the global audio-sync delay line. It is a pure fixed positive
|
||||
/// delay on the interleaved stereo mix — the OBS fix for audio running ahead
|
||||
/// of video. Positive-only: advancing audio needs a video-side delay and is
|
||||
/// out of the audio layer's scope.
|
||||
/// </summary>
|
||||
public class AudioSyncDelayTests
|
||||
{
|
||||
[Fact]
|
||||
public void ZeroDelay_PassesThroughUnchanged()
|
||||
{
|
||||
var delay = new AudioSyncDelay(48000);
|
||||
delay.Configure(0);
|
||||
|
||||
var input = new float[16];
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
input[i] = i - 8;
|
||||
var output = new float[16];
|
||||
|
||||
delay.Process(input, output);
|
||||
|
||||
Assert.Equal(input, output);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConfigureClampsNegativeToZero_AndBeyond500msTo500ms()
|
||||
{
|
||||
var delay = new AudioSyncDelay(48000);
|
||||
delay.Configure(-50);
|
||||
Assert.Equal(0, MarkerShift(delay, 1000));
|
||||
|
||||
// 500 ms at 48 kHz = 24000 frames = 48000 interleaved samples.
|
||||
delay.Configure(6000);
|
||||
Assert.Equal(48000, MarkerShift(delay, 48000));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TenMsDelay_EmitsTheSignal960SamplesLater()
|
||||
{
|
||||
// 10 ms at 48 kHz = 480 frames; interleaved stereo = 960 samples.
|
||||
var delay = new AudioSyncDelay(48000);
|
||||
Assert.Equal(960, MarkerShift(delay, 480 * 2, configureMs: 10));
|
||||
}
|
||||
|
||||
private static int MarkerShift(AudioSyncDelay delay, int chunkLen, int configureMs = -1)
|
||||
{
|
||||
if (configureMs >= 0)
|
||||
delay.Configure(configureMs);
|
||||
|
||||
var chunk = new float[chunkLen];
|
||||
var output = new float[chunkLen];
|
||||
chunk[0] = 1f; // sole non-zero: left of frame 0
|
||||
|
||||
var foundAt = -1;
|
||||
var total = 0;
|
||||
for (var c = 0; c < 4 && foundAt < 0; c++)
|
||||
{
|
||||
delay.Process(chunk, output);
|
||||
for (var i = 0; i < chunkLen; i++)
|
||||
{
|
||||
if (output[i] != 0f)
|
||||
{
|
||||
foundAt = total + i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
total += chunkLen;
|
||||
}
|
||||
Assert.True(foundAt >= 0, "marker never appeared in the delayed output");
|
||||
return foundAt;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user