e8ff4df05f
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.
78 lines
2.2 KiB
C#
78 lines
2.2 KiB
C#
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;
|
|
}
|
|
}
|