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:
2026-09-12 13:56:55 -07:00
parent a62a283fd4
commit 724af1499b
9 changed files with 310 additions and 134 deletions
+3 -53
View File
@@ -267,9 +267,6 @@ public sealed class MediaCaptureFrameSource : ICameraFrameSource
private int _ringNext;
private long _epoch;
// Alpha probe diagnostics (one-shot, throttled)
private DateTime _lastAlphaProbe = DateTime.MinValue;
private byte[] RentRingBuffer(int size)
{
for (var tries = 0; tries < _frameRing.Length; tries++)
@@ -308,25 +305,11 @@ public sealed class MediaCaptureFrameSource : ICameraFrameSource
var pixels = RentRingBuffer((int)capacity);
Marshal.Copy(pixelsPtr, pixels, 0, (int)capacity);
// Alpha probe diagnostics (throttled to once per 5s)
var now = DateTime.UtcNow;
if ((now - _lastAlphaProbe).TotalSeconds >= 5.0)
{
_lastAlphaProbe = now;
ProbeAlphaStats(pixels, bitmap.PixelWidth, bitmap.PixelHeight);
}
// MediaCapture video carries no alpha plane — the OS fills 255.
var videoFrame = new VideoFrame(bitmap.PixelWidth, bitmap.PixelHeight, pixels)
var outFrame = new VideoFrame(bitmap.PixelWidth, bitmap.PixelHeight, pixels)
{ IsOpaque = true, Epoch = ++_epoch };
// Diagnostic: Log frame properties for compositor debugging
if ((now - _lastAlphaProbe).TotalSeconds >= 5.0)
{
AppLog.Write($"Webcam frame emit: {videoFrame.Width}x{videoFrame.Height}, IsOpaque={videoFrame.IsOpaque}, Epoch={videoFrame.Epoch}");
}
FrameAvailable?.Invoke(videoFrame);
FrameAvailable?.Invoke(outFrame);
}
}
catch (Exception ex)
@@ -339,37 +322,4 @@ public sealed class MediaCaptureFrameSource : ICameraFrameSource
bitmap.Dispose();
}
}
private void ProbeAlphaStats(byte[] pixels, int width, int height)
{
try
{
// Sample 1000 alpha bytes (every 4th byte in BGRA format)
var sampleCount = Math.Min(1000, pixels.Length / 4);
var alphaMin = 255;
var alphaMax = 0;
var alphaSum = 0;
for (var i = 0; i < sampleCount; i++)
{
var alphaIndex = (i * 4) + 3; // Alpha is 4th byte in BGRA
if (alphaIndex < pixels.Length)
{
var alpha = pixels[alphaIndex];
alphaMin = Math.Min(alphaMin, alpha);
alphaMax = Math.Max(alphaMax, alpha);
alphaSum += alpha;
}
}
var alphaMean = sampleCount > 0 ? alphaSum / (double)sampleCount : 0;
AppLog.Write($"Webcam alpha probe: {width}x{height}, epoch {_epoch}, " +
$"alpha[min={alphaMin}, max={alphaMax}, mean={alphaMean:F1}], " +
$"sampled {sampleCount} pixels");
}
catch (Exception ex)
{
AppLog.Write($"Webcam alpha probe failed: {ex.Message}");
}
}
}