v3.9.1: plain/WASAPI streams now play on an ASIO-mode receiver (silent-mic fix)

Receiver: a Mixed (plain) session is rendered on an active lane in BothIndependent mode instead of being skipped, so a WASAPI-only sender is no longer silent to a receiver that has an ASIO driver selected. Also port the per-session buffer depth-drain (stops the receive jitter buffer bloating).

Sender: add sndAudFr meter (audio frames actually sent) to localise capture vs send.

App: startup sound cue (machine-wide, Preferences); stop sending audio when no peer is reachable (issue #8). Version 3.9.1.

Server (relay): fix updater version-compare for multi-dot tags, guard the main loop against crashes, reject spoofed BYE from a mismatched endpoint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-06-12 19:00:33 +01:00
co-authored by Claude Opus 4.8
parent 04b17ff1ab
commit 84c4a47411
15 changed files with 360 additions and 148 deletions
+9
View File
@@ -188,6 +188,15 @@ public sealed class AudioSender : IDisposable
return a > b ? a : b;
}
/// <summary>Total audio frames both lanes actually handed to the wire since the last call
/// (resets on read). Pairs with <see cref="TakeMaxSenderPreEncodePeak"/> on the diag line:
/// capPeak proves real signal reached the encoder; this proves frames left the socket. A
/// high capPeak with zero frames sent localises a silence to the encode/encrypt stage — the
/// missing measurement behind the "mic only works in ASIO" report. In WasapiOnly mode only
/// defaultLane fires, so this number IS the WASAPI mic lane's output.</summary>
public long TakeSenderAudioFramesSent() =>
defaultLane.TakeAudioFramesSent() + asioLane.TakeAudioFramesSent();
// Cross-buffer (boundary) and within-buffer (content) split — see AudioStepProbe for the
// diagnostic distinction. Used by the per-second diag logger to emit two extra columns so
// an offline log inspection can tell a real audio transient apart from a buffer-boundary
+13
View File
@@ -83,6 +83,17 @@ internal sealed class SenderLane
private float preEncodePeak;
public float TakeMaxPreEncodePeak() { var p = preEncodePeak; preEncodePeak = 0f; return p; }
// Count of audio frames this lane actually handed to the wire (encode AND encrypt both
// succeeded → SendAudio / SendPcmPart called) since the last drain. Pairs with preEncodePeak:
// capPeak proves real signal reached the encoder INPUT, but every post-encode early-return —
// the Opus encoder returning len<=0, no password so cryptoGcm is null, or the accumulator
// never completing a frame — is INVISIBLE to it. This counts what actually left the machine,
// so a log can finally tell "mic captured but nothing sent" (a drop at encode/encrypt) from
// "mic captured and sent" (the silence is downstream). Added 2026-06-12 for Andre's
// WASAPI-mic-only-works-in-ASIO investigation. Reset on read, like the peak.
private long audioFramesSent;
public long TakeAudioFramesSent() => Interlocked.Exchange(ref audioFramesSent, 0);
// Which render route this lane announces in its format packets. The receiver reads the
// Lane byte on the wire and tags the matching SessionPlayout, which makes PlayoutEngine
// route the lane's audio to the corresponding per-route IWaveProvider surface (lane
@@ -297,6 +308,7 @@ internal sealed class SenderLane
var maxPart = RemPacket.MaxAudioPayloadBytes;
var totalParts = (byte)((ctLen + maxPart - 1) / maxPart);
pcmFrameId++;
Interlocked.Increment(ref audioFramesSent);
for (byte part = 0; part < totalParts; part++)
{
var offset = part * maxPart;
@@ -324,6 +336,7 @@ internal sealed class SenderLane
EnsureCrypto();
if (cryptoGcm is null) return; // no password yet → never send audio in the clear
var ctLen = RemSoundCrypto.EncryptInto(cryptoGcm, opusBytes, cipherScratch);
Interlocked.Increment(ref audioFramesSent);
SendAudio(cipherScratch.AsSpan(0, ctLen));
}