diff --git a/lab-asio-err.txt b/lab-asio-err.txt new file mode 100644 index 0000000..e69de29 diff --git a/lab-asio.txt b/lab-asio.txt new file mode 100644 index 0000000..10d1a95 --- /dev/null +++ b/lab-asio.txt @@ -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 diff --git a/src/RemSound.App/LatencyLab.cs b/src/RemSound.App/LatencyLab.cs index c2644c6..1ffaac3 100644 --- a/src/RemSound.App/LatencyLab.cs +++ b/src/RemSound.App/LatencyLab.cs @@ -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 /// 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. - private static void RunClassicAppPath(List results, string name) + private static void RunClassicAppPath(List 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();