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
@@ -9,6 +9,11 @@ public enum CaptureKind
|
||||
{
|
||||
Loopback,
|
||||
Input,
|
||||
/// <summary>Per-application capture: loopback of ONE process (and its child process tree) via the
|
||||
/// Windows process-loopback API (Windows 10 build 19041+). The <see cref="CaptureSourceSpec.DeviceId"/>
|
||||
/// carries the target PID as <c>"proc:<pid>"</c> (see <see cref="ProcessLoopbackId"/>). WASAPI-only
|
||||
/// — ASIO has no per-app concept.</summary>
|
||||
ProcessLoopback,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -36,3 +41,19 @@ public static class AsioDeviceId
|
||||
return int.TryParse(deviceId.AsSpan("asio:".Length), out channelPair) && channelPair >= 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Synthetic device-id for a per-application (process-loopback) capture: <c>"proc:<pid>"</c>.
|
||||
/// The PID is resolved fresh each time the app list reconciles, so the id is transient (an app that
|
||||
/// restarts gets a new PID and a new spec) — selection is tracked by process NAME elsewhere.</summary>
|
||||
public static class ProcessLoopbackId
|
||||
{
|
||||
public static string Format(int processId) => $"proc:{processId}";
|
||||
|
||||
public static bool TryParse(string deviceId, out int processId)
|
||||
{
|
||||
processId = -1;
|
||||
if (string.IsNullOrEmpty(deviceId)) return false;
|
||||
if (!deviceId.StartsWith("proc:", StringComparison.OrdinalIgnoreCase)) return false;
|
||||
return int.TryParse(deviceId.AsSpan("proc:".Length), out processId) && processId > 0;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user