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
+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));
}