724af1499b
- AudioSyncDelay.Configure reallocated/zeroed its buffer every ~10ms tick
(AudioMixer re-reads the UI setting each mix), so any non-zero sync offset
erased the just-written audio -> total silence. Now early-returns when the
delay samples are unchanged. Regression test proven both ways.
- SceneCompositor.BlitContentRaw defaulted cbW/cbH=0 when CropBounds is null
(regression from ed9d7c1) -> webcam blit to an empty rect = gray block.
Default to src.Width/Height. Regression test proven both ways.
- Truncated recordings: rawvideo mux stamps frames at declared 60fps by
arrival; a scene whose first layer is dynamic (hidden elements still count)
kills the bake cache -> full render ~35ms -> ~27fps submitted -> halved
file length. Static scenes bake once (246ms cold, then <1ms) -> 60fps,
full-length (probe + 13:53 take, 301/300 per 5s, 12.46s file from 12.3s
wall). FramePump.ProbeRender names the hot render path on slow frames.
116 lines
4.1 KiB
C#
116 lines
4.1 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));
|
|
}
|
|
|
|
/// <summary>2026-09-12 total-silence regression: <see cref="AudioMixer.FillAndMix"/>
|
|
/// calls <c>Configure(_syncOffsetMs?.Invoke() ?? 0)</c> on EVERY ~10ms tick (it
|
|
/// re-reads the live UI setting each mix, not just on change) — the old
|
|
/// `Configure` unconditionally reallocated and zeroed the delay line and reset
|
|
/// `_writePos` every single call, discarding the buffered marker before the
|
|
/// delay offset could ever read it back. Any persisted non-zero
|
|
/// `Audio.SyncOffsetMs` (this repo's default is 0, but TASK 22 testing/a user
|
|
/// slider left 500 saved) turned the WHOLE recording into digital silence:
|
|
/// mic/loop level meters (sampled upstream of this stage) stayed honest while
|
|
/// the delay line fed the encoder pipe pure zeros forever. This test
|
|
/// reconfigures with the SAME delay before every Process call — exactly the
|
|
/// mixer's calling pattern — and the marker must still survive.</summary>
|
|
[Fact]
|
|
public void RepeatedConfigureWithSameDelay_EveryTick_StillDelivers_TheMarker()
|
|
{
|
|
const int chunkLen = 960; // 10ms of 48kHz interleaved stereo
|
|
var delay = new AudioSyncDelay(48000);
|
|
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 < 6 && foundAt < 0; c++)
|
|
{
|
|
delay.Configure(10); // the mixer calls this every tick, not just on change
|
|
delay.Process(chunk, output);
|
|
for (var i = 0; i < chunkLen; i++)
|
|
{
|
|
if (output[i] != 0f) { foundAt = total + i; break; }
|
|
}
|
|
total += chunkLen;
|
|
chunk[0] = 0f; // only the first tick carries the marker
|
|
}
|
|
|
|
Assert.Equal(960, foundAt); // same 10ms-delay shift as the single-Configure test
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|