Multi-output fan-out, offline-marker fix, #19, and per-app send engine core
Local checkpoint - NOT for public release. Fan-out (every received stream to every selected output, no added latency): - SessionPlayout mirror replicas fed the same decoded bytes; delicate ReadFloats untouched. - PlayoutEngine reconciles replicas per active output lane; per-route tuner aggregation keeps lanes from disturbing each other. Recording dispatch gated to the recording route only. - PeerDspChain.Clone() gives each output lane independent biquad state. - ReceiverSelfChecks.FanOutToBothOutputs proves both lanes get audio (self-test). Offline-marker pile-up fix: - ResolvePeerDisplayName strips the " (offline)" marker before reuse, so a ghost peer no longer compounds the suffix hundreds of times in the status line. Issue #19 (Use-Windows-default follower in the loopback send list): - DefaultLoopbackSendFollower resolves to the current default render device's loopback spec, re-applied when the default changes. Per-application send engine core (issue #20) - WASAPI-only, Win10 19041+ gated: - CaptureKind.ProcessLoopback + ProcessLoopbackId ("proc:<pid>"). - AudioAppEnumerator: snapshots apps with audio sessions, tracked by process name, releasing every session object each pass so nothing piles up. - ProcessLoopbackCapture: IWaveIn over the process-loopback activation API (hand-rolled COM interop; NAudio has no binding). Fixed 48k/float/stereo. - CaptureSource IWaveIn overload; MixingEngine opens "proc:<pid>" sources with no MMDevice and no render keepalive. ASIO path untouched. - Self-test enumerated real apps on hardware; support gate verified. UI (Preferences device/app mode + app checklist), ApplySendSources app specs, and the reconcile timer are still to come. Gate: 15/15. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
2fb9274a95
commit
8a7c4ddf2b
@@ -58,6 +58,8 @@ internal static class SelfTest
|
||||
RunStep(results, "Server wire-format compatibility", ServerWireCompat);
|
||||
RunStep(results, "App settings save and reload", SettingsRoundTrip);
|
||||
RunStep(results, "Per-peer shaping DSP", PeerShapingDsp);
|
||||
RunStep(results, "Multi-output fan-out (both lanes)", FanOutToBothOutputs);
|
||||
RunStep(results, "Per-application send enumeration", AppSendEnumeration);
|
||||
RunStep(results, "v5 settings and shaping round-trip", V5ConfigRoundTrip);
|
||||
RunStep(results, "Profile save and reload", ProfileRoundTrip);
|
||||
RunStep(results, "What's-new update marker", WhatsNewMarkerRoundTrip);
|
||||
@@ -248,6 +250,40 @@ internal static class SelfTest
|
||||
return "unity→null, master-off→null, volume, parametric";
|
||||
}
|
||||
|
||||
/// <summary>Proves the "every received stream plays to EVERY active output" fan-out: with both output
|
||||
/// lanes active (BothIndependent), one incoming stream must produce audio on BOTH the WASAPI and the
|
||||
/// ASIO lane surface — the WASAPI lane from the primary session, the ASIO lane from its mirror
|
||||
/// replica. Before the fan-out, only the lane matching the sender's capture tag played and the other
|
||||
/// output was silent (the bug Ed hit: ASIO-sent audio never reached the WASAPI output).</summary>
|
||||
private static string? FanOutToBothOutputs()
|
||||
{
|
||||
// Driven inside RemSound.Receiver (PlayoutEngine/SessionPlayout are internal there).
|
||||
var err = ReceiverSelfChecks.FanOutToBothOutputs();
|
||||
Check(err is null, err ?? "");
|
||||
return "one stream played to both output lanes (WASAPI + ASIO fan-out)";
|
||||
}
|
||||
|
||||
/// <summary>Per-application send plumbing: the enumerator returns a well-formed snapshot without
|
||||
/// throwing (it may be empty on a silent/headless box — that's fine), the "proc:PID" id round-trips,
|
||||
/// and the Windows-version support gate answers consistently. Does NOT open a real process-loopback
|
||||
/// capture — that needs a live playing app + hardware, validated separately.</summary>
|
||||
private static string? AppSendEnumeration()
|
||||
{
|
||||
var apps = RemSound.Sender.AudioAppEnumerator.Snapshot();
|
||||
Check(apps is not null, "enumerator returned null");
|
||||
foreach (var a in apps!)
|
||||
Check(!string.IsNullOrWhiteSpace(a.ProcessName), "an app had an empty process name");
|
||||
|
||||
Check(ProcessLoopbackId.TryParse(ProcessLoopbackId.Format(1234), out var pid) && pid == 1234,
|
||||
"proc:PID id did not round-trip");
|
||||
Check(!ProcessLoopbackId.TryParse("asio:0", out _), "ASIO id wrongly parsed as a process id");
|
||||
|
||||
var supported = RemSound.Sender.ProcessLoopbackCapture.IsSupported;
|
||||
Check(supported == OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041),
|
||||
"support gate disagrees with the OS build check");
|
||||
return $"enumerated {apps.Count} app(s); process-loopback supported={supported}";
|
||||
}
|
||||
|
||||
/// <summary>The v5 machine-wide settings and per-peer shaping survive a JSON save/reload: new
|
||||
/// AppConfig defaults, the named-peers book, the main tab order, per-peer shaping with parametric
|
||||
/// bands, and the new recording default. All in-memory — the real config/profiles aren't touched.</summary>
|
||||
|
||||
Reference in New Issue
Block a user