fix: audio silence (idempotent sync-delay configure), webcam gray block, truncated videos
- 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.
This commit is contained in:
@@ -47,6 +47,44 @@ public class AudioSyncDelayTests
|
||||
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)
|
||||
|
||||
@@ -303,6 +303,46 @@ public class SceneCompositorTests
|
||||
Assert.Equal(build.BgraPixels, hit.BgraPixels);
|
||||
}
|
||||
|
||||
/// <summary>2026-09-12 regression — the webcam gray-block bug: the CropBounds
|
||||
/// Fill-scaling work (ed9d7c1, same day as the transparency fix) added
|
||||
/// cbX/cbY/cbW/cbH but only assigned them inside the CropBounds-present branch;
|
||||
/// every CropBounds-less source (webcam, images — anything but the web widget)
|
||||
/// fell through with cbW=cbH=0, so sxCrop/syCrop (= norm * cbW/cbH) were always
|
||||
/// 0 and EVERY destination pixel sampled source (0,0) — a whole scaled webcam
|
||||
/// collapsed to one corner pixel's color. All rect-placed elements route through
|
||||
/// this exact sampler via the paste cache (take-9), so this is not webcam-specific;
|
||||
/// a two-color source proves distinct destination regions read distinct source
|
||||
/// regions instead of a single flat pixel.</summary>
|
||||
[Fact]
|
||||
public void BlitContentRaw_NoCropBounds_SamplesAcrossFullSource_NotJustOrigin()
|
||||
{
|
||||
var source = Split(64, 32, 255, 0, 0, 0, 0, 255); // left=red, right=blue, no CropBounds
|
||||
var background = new Source { Type = SourceType.DisplayCapture, IsBackground = true, CaptureKey = "monitor:0" };
|
||||
var webcam = new WebcamSceneConfig { X = 0, Y = 0, Width = 40, Height = 20 }; // scaled (not 1:1)
|
||||
var scene = new Scene { Name = "Live" };
|
||||
scene.Elements.Add(background);
|
||||
scene.Elements.Add(webcam);
|
||||
|
||||
VideoFrame? FrameFor(SceneElement e) => e switch
|
||||
{
|
||||
Source { IsBackground: true } => Solid(128, 64, 0, 0, 0),
|
||||
WebcamSceneConfig => source,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
var options = new CompositorOptions
|
||||
{
|
||||
SourceRectX = 0, SourceRectY = 0, SourceRectWidth = 128, SourceRectHeight = 64,
|
||||
OutputWidth = 128, OutputHeight = 64,
|
||||
};
|
||||
|
||||
var compositor = new SceneCompositor();
|
||||
var output = compositor.Render(scene, FrameFor, null, options);
|
||||
|
||||
AssertColor(output, 5, 10, 255, 0, 0); // near the left edge of the webcam rect: source's red half
|
||||
AssertColor(output, 35, 10, 0, 0, 255); // near the right edge: source's blue half — fails (reads red) on the bug
|
||||
}
|
||||
|
||||
/// <summary>take-16 regression gate — GC churn convicted mechanically: a ring-style
|
||||
/// producer hands out a small set of rotating arrays with a NEW EPOCH every frame
|
||||
/// (exactly what the capture/camera rings do). The compositor must re-rasterize INTO
|
||||
|
||||
Reference in New Issue
Block a user