Bump to v1.2.0: recording, sound cues, drift compensation, diagnostics
New user-facing features:
* Recording. Dedicated Record menu (Alt+O — moved from Alt+R to
avoid clashing with the Receive audio checkbox), Start/Stop on
Ctrl+R, settings dialog, per-profile source / format / bit-depth /
channel-mode / folder. Three source modes (received only, sent
only, both). Files are crash-resilient — a process crash
mid-recording leaves a playable file containing everything up to
the last header refresh (~5 seconds).
* Four output formats, all functional:
- WAV: 16/24-bit PCM or 32-bit float, custom writer with
periodic RIFF re-patching.
- MP3: LAME 128–320 kbps CBR (via NAudio.Lame).
- OGG-Opus: 96–256 kbps VBR (via Concentus.Oggfile, reusing the
Concentus encoder from the wire path).
- FLAC: 16/24-bit lossless (via CUETools.Codecs.FLAKE — pure
managed, no native DLL).
* Recording start/stop sound cues. record start.wav and
record stop.wav play around the recording transition. Played via
System.Media.SoundPlayer to the default Windows output, separate
from the recording pipeline so a normal recording does not contain
the cue.
* Per-cue Preferences. The old single "Mute connect/disconnect
sounds" checkbox is replaced by a CheckedListBox: Connect /
Disconnect / Recording start / Recording stop. Old profiles with
the legacy MuteConnectionCues=true are honoured on first load via
a migration path in the new Load* helpers.
* Receiver-side drift compensation switched from discrete
single-frame splices to a continuous WdlResampler at a smoothed
rate ratio. SessionPlayout.cs rewrite.
Diagnostics (only active with Enable logs ticked):
* Per-stage discontinuity probes — sender raw capture (per backend,
PushModeWasapi + Asio both wired), sender pre-encode (now per
lane in BothIndependent, fixing a cross-stream artefact), receiver
post-decode, post-ring, post-resampler.
* Wire-level packet sequence tracking on each PCM stream — in-order
/ missed / reordered / duplicated counts in the diag log.
* Clipped-sample delta in the diag log.
* New AudioStepProbe in RemSound.Core with per-channel scan helper.
UI changes:
* Record menu uses Alt+O (Rec&ord). Inside the menu, item mnemonics
unchanged (S / T / O / C).
* Auto-tune interval combo label is mode-aware: "Auto-tune latency
interval" in classic modes, "Auto-tune interval — WASAPI and ASIO"
in BothIndependent. The combo's Enabled state now follows EITHER
lane's auto-tune checkbox (was only the WASAPI one — bug).
Wire format and audio pipeline unchanged from v1.1 — v1.1 and v1.2
peers interoperate.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
a0fe8070ed
commit
c59e413c1f
@@ -133,6 +133,19 @@ public sealed class AudioReceiver : IDisposable
|
||||
public void SetConcealmentArtifact(ConcealmentArtifact artifact) =>
|
||||
playoutEngine.SetConcealmentArtifact(artifact);
|
||||
|
||||
/// <summary>
|
||||
/// Optional callback invoked when the engine produces fully-processed mixed received
|
||||
/// audio (volume / mute / limiter all applied). Span is 48 kHz interleaved stereo
|
||||
/// float, lives on the render thread — copy or consume quickly. Used by the recorder
|
||||
/// to capture "what we heard". Setter mirrors directly onto <see cref="PlayoutEngine"/>;
|
||||
/// null clears the tap.
|
||||
/// </summary>
|
||||
public Action<ReadOnlyMemory<float>>? OnReceivedSamples
|
||||
{
|
||||
get => playoutEngine.OnReceivedSamples;
|
||||
set => playoutEngine.OnReceivedSamples = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the allow-list of sender endpoints whose audio will be rendered. Pass an empty set
|
||||
/// to block all (the user has selected no peers); pass null to disable filtering and accept
|
||||
@@ -268,6 +281,24 @@ public sealed class AudioReceiver : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Take the worst post-decode single-sample step magnitude across all active
|
||||
/// stream sessions since the last call, resetting each session's probe. Used by the
|
||||
/// diag log to pinpoint where in the pipeline audio discontinuities are being
|
||||
/// introduced.</summary>
|
||||
public float TakeMaxPostDecodeStep()
|
||||
{
|
||||
lock (sessionsLock)
|
||||
{
|
||||
var max = 0f;
|
||||
foreach (var s in sessions.Values)
|
||||
{
|
||||
var v = s.TakeMaxPostDecodeStep();
|
||||
if (v > max) max = v;
|
||||
}
|
||||
return max;
|
||||
}
|
||||
}
|
||||
|
||||
public long PcmFrameDiscardedPartials
|
||||
{
|
||||
get
|
||||
@@ -331,6 +362,26 @@ public sealed class AudioReceiver : IDisposable
|
||||
/// buffer aligned with target. Each event = 21 µs of audio at 48 kHz, sub-audible.</summary>
|
||||
public long DriftDropFrames => playoutEngine.AggregateDriftDropFrames;
|
||||
public long DriftRepeatFrames => playoutEngine.AggregateDriftRepeatFrames;
|
||||
/// <summary>Cumulative count of FULL-empty playout reads (framesRead == 0) — the audible
|
||||
/// underrun events that trigger noise-burst concealment + fade-in. Separated from
|
||||
/// <see cref="Underruns"/> (which conflates full and partial short reads) so the diag
|
||||
/// log can show "real underruns this second" distinct from "partial near-misses".</summary>
|
||||
public long ConcealmentFires => playoutEngine.AggregateConcealmentFires;
|
||||
/// <summary>Cumulative count of sub-frame partial reads (0 < framesRead < requested).
|
||||
/// Inaudible since the 2026-05-14 concealment fix but tracked so we can see clock
|
||||
/// in-phase patterns.</summary>
|
||||
public long ShortReadFires => playoutEngine.AggregateShortReadFires;
|
||||
/// <summary>Live LP-filtered drift error of the primary active session (stereo frames,
|
||||
/// signed). Negative = buffer running below target on average; positive = above.</summary>
|
||||
public double FilteredDriftErrorFrames => playoutEngine.PrimaryFilteredDriftErrorFrames;
|
||||
/// <summary>Live drift integrator accumulator of the primary session. Crosses ±1 to fire
|
||||
/// a drop / repeat correction.</summary>
|
||||
public double DriftAccumulator => playoutEngine.PrimaryDriftAccumulator;
|
||||
/// <summary>Take the worst single-sample step out of the ring buffer (after decode +
|
||||
/// SessionPlayout.Write, before resampler) since the last call.</summary>
|
||||
public float TakeMaxPostRingReadStep() => playoutEngine.TakeMaxPostRingReadStep();
|
||||
/// <summary>Take the worst single-sample step out of the resampler since the last call.</summary>
|
||||
public float TakeMaxPostResamplerStep() => playoutEngine.TakeMaxPostResamplerStep();
|
||||
/// <summary>RingbufferOverflowDropBytes = AggregateDrops minus the deliberate trim+drain
|
||||
/// causes. Whatever's left was the producer-side overflow (Write into a full buffer) or
|
||||
/// the catastrophic-cap trim from NoteFramesQueued. Both indicate "we genuinely couldn't
|
||||
@@ -371,6 +422,71 @@ public sealed class AudioReceiver : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
// === Wire-level packet sequence diagnostics ===
|
||||
// Each audio packet carries a per-session sequence number from the sender. Tracking it
|
||||
// at receipt tells us whether the network or NIC stack between sender and receiver is
|
||||
// reordering, dropping, or duplicating packets — any of which would manifest as audible
|
||||
// pops on the PCM path. On a healthy LAN all four counters should grow as
|
||||
// WireInOrder == packets, all others == 0. A non-zero Missed / Reordered / Duplicated
|
||||
// points straight at transport pathology and rules out codec / playout / hardware as
|
||||
// pop sources.
|
||||
|
||||
/// <summary>Cumulative count of audio packets that arrived with the expected wire sequence.</summary>
|
||||
public long WireInOrderCount
|
||||
{
|
||||
get
|
||||
{
|
||||
long total = 0;
|
||||
lock (sessionsLock)
|
||||
{
|
||||
foreach (var s in sessions.Values) total += s.WireInOrderCount;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Cumulative count of packets that the wire claims went missing (forward gaps).</summary>
|
||||
public long WireMissedCount
|
||||
{
|
||||
get
|
||||
{
|
||||
long total = 0;
|
||||
lock (sessionsLock)
|
||||
{
|
||||
foreach (var s in sessions.Values) total += s.WireMissedCount;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Cumulative count of packets that arrived out-of-order (later sequence first, then earlier).</summary>
|
||||
public long WireReorderedCount
|
||||
{
|
||||
get
|
||||
{
|
||||
long total = 0;
|
||||
lock (sessionsLock)
|
||||
{
|
||||
foreach (var s in sessions.Values) total += s.WireReorderedCount;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Cumulative count of duplicate-sequence packets (the same wire seq delivered twice).</summary>
|
||||
public long WireDuplicatedCount
|
||||
{
|
||||
get
|
||||
{
|
||||
long total = 0;
|
||||
lock (sessionsLock)
|
||||
{
|
||||
foreach (var s in sessions.Values) total += s.WireDuplicatedCount;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
}
|
||||
|
||||
public float Volume { get => playoutEngine.Volume; set => playoutEngine.Volume = value; }
|
||||
public bool IsMuted { get => playoutEngine.IsMuted; set => playoutEngine.IsMuted = value; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user