Bump to v2.1.0: UPnP, read-only profile lock, sleep/hibernate audio fix
Headline features: * Automatic router port opening (UPnP / NAT-PMP / PCP). Opt-in via Preferences; surfaces external address + carrier-grade NAT detection. * Lock profile (read-only). New File-menu tick that makes a profile load-only — session changes don't persist, no save prompt on close. Unblocks unattended shutdowns (NVDA gone, remote dropped, hibernate) where the existing save prompt could deadlock. * Check for updates on startup (default on) + brief countdown notice before silent updates install, so a launch-time update doesn't make the app silently vanish. * "Cue sounds" -> "Audio cue sounds" label clarification. Bug fixes: * No sound after the computer wakes from sleep. PowerResumeHandler rebuilds the audio backend automatically on resume; brief "Reconnecting to audio driver" splash during the rebuild. * Receiver audio silent after waking from hibernate. RefreshAudioDeviceLists now treats a transient ASIO probe failure (returns -1/-1 because the driver is mid-teardown / mid-reinit) as "retry next tick" instead of clearing the user's tick selection. Diagnostic-only changes (gated on the existing Enable-logs checkbox, zero cost when off): * AudioStepProbe split into cross-buffer vs within-buffer maxes so log inspection can tell a real-content sharp transient apart from a pipeline-boundary glitch. Plumbed through every probe owner. * New rxNetGapMs + gc0/gc1/gc2 delta columns in the diag log to split receive-side jitter into network-layer vs managed-runtime causes. Files touched: RELEASE_NOTES.md + readme.html + 24 source files across RemSound.Core / RemSound.Sender / RemSound.Receiver / RemSound.App. Three new app files: PowerResumeHandler, RouterPortMapper, UpdateInstallNoticeDialog. Wire format and audio pipeline unchanged from v1.5 onward — v1.5 through v2.1 peers interoperate.
This commit is contained in:
@@ -88,6 +88,8 @@ internal sealed class AsioCaptureBackend : ICaptureBackend
|
||||
onMixedSamples = callback;
|
||||
|
||||
public float TakeMaxRawCaptureStep() => rawCaptureStepProbe.TakeMax();
|
||||
public float TakeMaxRawCaptureStepCrossBuffer() => rawCaptureStepProbe.TakeMaxCrossBuffer();
|
||||
public float TakeMaxRawCaptureStepWithinBuffer() => rawCaptureStepProbe.TakeMaxWithinBuffer();
|
||||
|
||||
public bool IsRunning => asio is not null;
|
||||
public long TotalCaptureCallbacks => Interlocked.Read(ref callbackCount);
|
||||
|
||||
@@ -141,12 +141,23 @@ public sealed class AudioSender : IDisposable
|
||||
public float TakeMaxPreEncodeStepWasapiLane() => defaultLane.TakeMaxPreEncodeStep();
|
||||
public float TakeMaxPreEncodeStepAsioLane() => asioLane.TakeMaxPreEncodeStep();
|
||||
|
||||
// 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
|
||||
// glitch. 2026-05-21 addition.
|
||||
public float TakeMaxPreEncodeStepWasapiLaneCrossBuffer() => defaultLane.TakeMaxPreEncodeStepCrossBuffer();
|
||||
public float TakeMaxPreEncodeStepWasapiLaneWithinBuffer() => defaultLane.TakeMaxPreEncodeStepWithinBuffer();
|
||||
public float TakeMaxPreEncodeStepAsioLaneCrossBuffer() => asioLane.TakeMaxPreEncodeStepCrossBuffer();
|
||||
public float TakeMaxPreEncodeStepAsioLaneWithinBuffer() => asioLane.TakeMaxPreEncodeStepWithinBuffer();
|
||||
|
||||
// Raw capture-side step probe — now lives inside each <see cref="ICaptureBackend"/>
|
||||
// implementation so the ASIO path and the WASAPI path each measure their own buffers
|
||||
// independently. The aggregate just asks the backend for the max since last read; in
|
||||
// BothIndependent mode the composite backend forwards to both inners and returns the
|
||||
// larger value.
|
||||
public float TakeMaxSenderRawCaptureStep() => engine.TakeMaxRawCaptureStep();
|
||||
public float TakeMaxSenderRawCaptureStepCrossBuffer() => engine.TakeMaxRawCaptureStepCrossBuffer();
|
||||
public float TakeMaxSenderRawCaptureStepWithinBuffer() => engine.TakeMaxRawCaptureStepWithinBuffer();
|
||||
|
||||
// Snapshot the cumulative "hit the hard clamp" sample counter. The sender's mix path
|
||||
// clamps any sample whose magnitude exceeds 1.0 (avoids producing samples the int24 path
|
||||
|
||||
@@ -129,6 +129,22 @@ internal sealed class CompositeCaptureBackend : ICaptureBackend
|
||||
return w > a ? w : a;
|
||||
}
|
||||
|
||||
/// <summary>Cross-buffer (boundary) max across both inner backends. Drains BOTH.</summary>
|
||||
public float TakeMaxRawCaptureStepCrossBuffer()
|
||||
{
|
||||
var w = wasapi?.TakeMaxRawCaptureStepCrossBuffer() ?? 0f;
|
||||
var a = asio?.TakeMaxRawCaptureStepCrossBuffer() ?? 0f;
|
||||
return w > a ? w : a;
|
||||
}
|
||||
|
||||
/// <summary>Within-buffer max across both inner backends. Drains BOTH.</summary>
|
||||
public float TakeMaxRawCaptureStepWithinBuffer()
|
||||
{
|
||||
var w = wasapi?.TakeMaxRawCaptureStepWithinBuffer() ?? 0f;
|
||||
var a = asio?.TakeMaxRawCaptureStepWithinBuffer() ?? 0f;
|
||||
return w > a ? w : a;
|
||||
}
|
||||
|
||||
public void Start(IReadOnlyList<CaptureSourceSpec> specs)
|
||||
{
|
||||
lock (gate)
|
||||
|
||||
@@ -49,9 +49,25 @@ internal interface ICaptureBackend : IDisposable
|
||||
/// the last call; resets on read. Each backend owns its own probe instance so the
|
||||
/// cross-buffer step measurement doesn't get fooled by another backend's interleaved
|
||||
/// callbacks (which is what produced spurious 0.4-0.5 readings in BothIndependent mode
|
||||
/// before 2026-05-15). Backends that can't sensibly expose raw samples return 0.</summary>
|
||||
/// before 2026-05-15). Backends that can't sensibly expose raw samples return 0.
|
||||
/// Returns the max-of-(cross-buffer, within-buffer); for the split values use
|
||||
/// <see cref="TakeMaxRawCaptureStepCrossBuffer"/> + <see cref="TakeMaxRawCaptureStepWithinBuffer"/>
|
||||
/// and do NOT also call this in the same drain window.</summary>
|
||||
float TakeMaxRawCaptureStep();
|
||||
|
||||
/// <summary>Worst CROSS-BUFFER (boundary) raw-capture step since the last call. Non-zero =
|
||||
/// the first sample of some delivered capture buffer didn't continue smoothly from the
|
||||
/// last sample of the previous one. The clicks-at-buffer-boundary signal we're hunting.
|
||||
/// Resets on read. Backends that can't sensibly expose raw samples return 0.</summary>
|
||||
float TakeMaxRawCaptureStepCrossBuffer();
|
||||
|
||||
/// <summary>Worst WITHIN-BUFFER raw-capture step since the last call. Non-zero = a sharp
|
||||
/// edge between two consecutive samples inside the same delivered buffer — typically
|
||||
/// real audio content (percussion, syllable onset) rather than a pipeline glitch. The
|
||||
/// "this is just music" baseline against which the cross-buffer reading is interpreted.
|
||||
/// Resets on read. Backends that can't sensibly expose raw samples return 0.</summary>
|
||||
float TakeMaxRawCaptureStepWithinBuffer();
|
||||
|
||||
void Start(IReadOnlyList<CaptureSourceSpec> specs);
|
||||
|
||||
/// <summary>Live-update of the active source set without stopping the mix loop. Adds/removes
|
||||
|
||||
@@ -118,6 +118,8 @@ internal sealed class MixingEngine : ICaptureBackend
|
||||
/// <see cref="PushModeWasapiBackend"/> instead. Stays at zero here; if a future
|
||||
/// multi-source WASAPI test needs the probe, add it per-source in CaptureSource.</summary>
|
||||
public float TakeMaxRawCaptureStep() => 0f;
|
||||
public float TakeMaxRawCaptureStepCrossBuffer() => 0f;
|
||||
public float TakeMaxRawCaptureStepWithinBuffer() => 0f;
|
||||
|
||||
/// <summary>
|
||||
/// Starts the mix loop with the given initial source set. If already running, the existing
|
||||
|
||||
@@ -109,6 +109,8 @@ internal sealed class PushModeWasapiBackend : ICaptureBackend
|
||||
public int TakeMaxCallbackGapMs() => 0;
|
||||
|
||||
public float TakeMaxRawCaptureStep() => rawCaptureStepProbe.TakeMax();
|
||||
public float TakeMaxRawCaptureStepCrossBuffer() => rawCaptureStepProbe.TakeMaxCrossBuffer();
|
||||
public float TakeMaxRawCaptureStepWithinBuffer() => rawCaptureStepProbe.TakeMaxWithinBuffer();
|
||||
|
||||
public void Start(IReadOnlyList<CaptureSourceSpec> specs)
|
||||
{
|
||||
|
||||
@@ -62,6 +62,8 @@ internal sealed class SenderLane
|
||||
// that without changing what the probe measures.
|
||||
private readonly AudioStepProbe preEncodeStepProbe = new();
|
||||
public float TakeMaxPreEncodeStep() => preEncodeStepProbe.TakeMax();
|
||||
public float TakeMaxPreEncodeStepCrossBuffer() => preEncodeStepProbe.TakeMaxCrossBuffer();
|
||||
public float TakeMaxPreEncodeStepWithinBuffer() => preEncodeStepProbe.TakeMaxWithinBuffer();
|
||||
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user