Latency lab: add the ASIO-lane scenario (--latency-lab asio)
Answers Ed's "and adjusting ASIO latency also happens quickly now?" with a measurement rather than an assertion, and without needing an ASIO device: wires the engine as BothIndependent with only the ASIO lane active, confirms the stream lands on AsioLane, and drives the raise/lower through the ASIO slider's own route. Measured: 54ms -> 289ms within 10s, holds at the 330ms target, lower back to 30ms within 3s — the same fast approach as the single slider, as expected (one hard-setter path feeding whichever lane the moved slider owns). No product change; harness only. The shipped v5.9 build is untouched. Also made the harness faithful to the 5.9 arming change (NoteFramesQueued against the session's own route target, not the engine-wide one) so the ASIO case arms the way the app does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
RemSound latency lab - measuring depth-target convergence with the shipped playout code.
|
||||
Raise under test: 30ms -> 330ms. Expected growth at full depth-bias: ~3ms/s.
|
||||
|
||||
--- T4 ASIO lane (two-slider mode): ASIO slider drives an ASIO-lane stream ---
|
||||
session route after arrival: AsioLane (slider writes to: AsioLane)
|
||||
settled at slider 30ms: buffered=54ms
|
||||
t+10s buffered= 289ms
|
||||
t+20s buffered= 308ms
|
||||
t+30s buffered= 311ms
|
||||
t+40s buffered= 303ms
|
||||
t+50s buffered= 322ms
|
||||
t+60s buffered= 299ms
|
||||
RAISE via slider: 54ms -> 299ms in 60s = 4.08ms/s GROWS (slider works)
|
||||
LOWER via slider: back to 30ms -> buffered=30ms after 3s
|
||||
|
||||
|
||||
=== SUMMARY ===
|
||||
T4 ASIO lane (two-slider mode): ASIO slider drives an ASIO-lane stream: raise 4.08ms/s [GROWS (slider works)], lower -> 20ms
|
||||
@@ -45,6 +45,17 @@ internal static class LatencyLab
|
||||
// output, no ASIO), which tags every session RenderRoute.WasapiLane; the single latency
|
||||
// slider in every non-BothIndependent mode drives RenderRoute.Mixed (MainForm.MaxLatencyBox-
|
||||
// Route). Run this alone with: --latency-lab classic
|
||||
// T4 — the BothIndependent ASIO lane: does a move of the ASIO slider reach an ASIO-lane
|
||||
// stream, and arrive in seconds like the single slider does? Run with: --latency-lab asio
|
||||
if (args.Any(a => string.Equals(a, "asio", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
RunClassicAppPath(results, "T4 ASIO lane (two-slider mode): ASIO slider drives an ASIO-lane stream", asioLane: true);
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("=== SUMMARY ===");
|
||||
foreach (var line in results) Console.WriteLine(" " + line);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (args.Any(a => string.Equals(a, "classic", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
RunClassicAppPath(results, "T3 classic app path: WASAPI lane active, slider drives Mixed");
|
||||
@@ -69,20 +80,25 @@ internal static class LatencyLab
|
||||
/// <summary>The classic-mode reproduction: engine wired exactly as the shipped app wires it for a
|
||||
/// plain WASAPI setup, then the slider raised through the very call MainForm makes. Measures the
|
||||
/// buffered depth the same way as the other scenarios.</summary>
|
||||
private static void RunClassicAppPath(List<string> results, string name)
|
||||
private static void RunClassicAppPath(List<string> results, string name, bool asioLane = false)
|
||||
{
|
||||
Console.WriteLine($"--- {name} ---");
|
||||
var endpoint = new IPEndPoint(IPAddress.Loopback, 47831);
|
||||
var engine = new PlayoutEngine(new ReceiverDiagnostics());
|
||||
|
||||
// 1. CompositeRenderBackend.SetOutputDevices: one WASAPI output ticked, no ASIO.
|
||||
engine.SetLaneActive(RenderRoute.WasapiLane, true);
|
||||
engine.SetLaneActive(RenderRoute.AsioLane, false);
|
||||
// 2. The slider's startup value, applied the way MainForm applies it in classic mode.
|
||||
engine.SetMaxLatencyMs(RenderRoute.Mixed, StartTargetMs);
|
||||
// Which control the user is actually moving: the single slider (classic modes → Mixed), or
|
||||
// the ASIO slider in BothIndependent (→ AsioLane, its own value, its own lane's streams).
|
||||
var sliderRoute = asioLane ? RenderRoute.AsioLane : RenderRoute.Mixed;
|
||||
if (asioLane) engine.SetIndependentLaneLatency(true); // AudioReceiver.SetAudioMode(BothIndependent)
|
||||
|
||||
// 1. CompositeRenderBackend.SetOutputDevices: which lanes have a ticked output device.
|
||||
engine.SetLaneActive(RenderRoute.WasapiLane, !asioLane);
|
||||
engine.SetLaneActive(RenderRoute.AsioLane, asioLane);
|
||||
// 2. The slider's startup value, applied the way MainForm applies it.
|
||||
engine.SetMaxLatencyMs(sliderRoute, StartTargetMs);
|
||||
// 3. A peer's stream arrives — ReconcileReplicasLocked tags it with the active lane.
|
||||
var session = engine.GetOrCreateSession(endpoint, 1, capacityBytes: 4 * 1024 * 1024);
|
||||
Console.WriteLine($" session route after arrival: {session.Route} (slider writes to: {RenderRoute.Mixed})");
|
||||
Console.WriteLine($" session route after arrival: {session.Route} (slider writes to: {sliderRoute})");
|
||||
|
||||
const int WriteMs = 20, ReadMs = 10;
|
||||
var stop = false;
|
||||
@@ -96,7 +112,7 @@ internal static class LatencyLab
|
||||
while (!Volatile.Read(ref stop))
|
||||
{
|
||||
session.Write(writeBlock);
|
||||
session.NoteFramesQueued(engine.TargetLatencyMs); // AudioReceiver's queued-callback lambda
|
||||
session.NoteFramesQueued(engine.TargetLatencyMsFor(session.Route)); // AudioReceiver's queued-callback lambda
|
||||
nextDueMs += WriteMs;
|
||||
var sleep = nextDueMs - sw.Elapsed.TotalMilliseconds;
|
||||
if (sleep > 0) Thread.Sleep((int)sleep);
|
||||
@@ -126,7 +142,7 @@ internal static class LatencyLab
|
||||
Console.WriteLine($" settled at slider {StartTargetMs}ms: buffered={settled}ms");
|
||||
|
||||
// 4. The user drags the slider up mid-stream — MainForm's exact call.
|
||||
engine.SetMaxLatencyMs(RenderRoute.Mixed, RaisedTargetMs);
|
||||
engine.SetMaxLatencyMs(sliderRoute, RaisedTargetMs);
|
||||
for (var s = 0; s < MeasureSeconds; s++)
|
||||
{
|
||||
Thread.Sleep(1000);
|
||||
@@ -137,7 +153,7 @@ internal static class LatencyLab
|
||||
var verdict = growth >= 2.0 ? "GROWS (slider works)" : growth >= 0.5 ? "SLOW" : "STALLED (the field bug)";
|
||||
Console.WriteLine($" RAISE via slider: {settled}ms -> {after}ms in {MeasureSeconds}s = {growth:F2}ms/s {verdict}");
|
||||
|
||||
engine.SetMaxLatencyMs(RenderRoute.Mixed, StartTargetMs);
|
||||
engine.SetMaxLatencyMs(sliderRoute, StartTargetMs);
|
||||
Thread.Sleep(3000);
|
||||
Console.WriteLine($" LOWER via slider: back to {StartTargetMs}ms -> buffered={session.BufferedMs}ms after 3s");
|
||||
Console.WriteLine();
|
||||
|
||||
Reference in New Issue
Block a user