* New "Opus, live latency" codec mode: 2.5 ms frames (120 samples/ch at 48 kHz)
via the float-input encode path. End-to-end codec delay drops to ~5 ms (vs
~12.5 ms at standard 10 ms Opus). Test on LAN: 400 pps/lane, zero missed /
reordered / duplicate packets, ~15 ms one-way saved end-to-end.
* Wire-format change: AudioFormatInfo.FrameDurationMilliseconds renamed to
FrameSamplesPerChannel (int sample-count at announced sample rate). Removes
the lossy 48000*ms/1000 conversion that couldn't represent 2.5 ms. v3 <-> v3
exact; v3 <-> v2 still passes audio (Opus decoder is self-describing from
packet TOC) but v2 side over-sizes its buffer wildly. v2.x profiles auto-
migrate via <120 sentinel rule in RemSoundSettingsStore (anything below 120
is treated as legacy ms and multiplied by 48). Profile JSON key kept as
OpusFrameMilliseconds via [JsonPropertyName] so old profile files still load.
* Codec dropdown rebuilt with use-case names: "PCM 48K 24 bit - uncompressed",
"Opus, broadcast quality - loss tolerant", "Opus, live latency - for jamming
and monitoring". Middle 10 ms option retired; saved 480-sample profiles
collapse to broadcast quality (safer-side default).
* Profile auto-resume after self-update: RemSoundUpdater writes a one-shot
_resume-after-update.txt sentinel containing the active profile title before
exit; Program.Main reads + deletes it on next start and silently loads that
profile, skipping the picker. Helper batch's robocopy /XF excludes the
sentinel and the failure-branch cleans it up if the install aborts. Falls
through to normal startup behaviour (StartWithProfileTitle or picker) if the
sentinel is missing, empty, or names a profile that no longer exists.
* Read-only profile saves now go through on explicit Ctrl+S / File -> Save
with a one-time TaskDialog warning ("Save anyway" / "Cancel" + Do-not-show-
again). Lock continues to suppress the automatic unsaved-changes prompt on
close / profile switch (its main job). AppConfig.SaveOnReadOnlyMessageSuppressed
renamed to SaveOnReadOnlyWarningSuppressed; v2.x suppression flag is silently
discarded since the behaviour changed and the user needs to see the warning
once on each machine.
* Manual (readme.html) updated: codec table rewritten with the three new
choices and corrected bandwidth figures, send-rate description updated, new
sections "The same profile picks up automatically after an update" and
"Saving on purpose while a profile is locked".
* Subsumes the never-separately-released v2.2 work: native Opus encoder
(~97% less per-second memory churn on Opus send path via Concentus.Native),
efficiency tidy-ups (item 4 ASIO probe rate, item 6 WaitHandle, item 7
snapshot cache, items 14/16 heartbeat + discovery), legacy cleanup
(items 30/34/35/36: KeepAlive infrastructure, drift drop/repeat/accumulator
fields, fan-out cache stat). New diagnostic columns cpu/memMB/wsMB/
allocKBps/captureMs/sendMs/recvMs/renderMs gated on Enable-logs.
About dialog updated with v3.0 block at top; v2.2 block retained for the
subsumed work.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
101 lines
3.9 KiB
C#
101 lines
3.9 KiB
C#
using System.Windows.Forms;
|
|
using RemSound.Core;
|
|
|
|
namespace RemSound.App;
|
|
|
|
/// <summary>
|
|
/// CheckedListBox subclass that exposes the protected <c>RefreshItem</c> method publicly. Used
|
|
/// for the connectivity dialog's "Connected peers" list, where each row's text is updated in
|
|
/// place (live RTT, codec, direction) without re-adding items — that would destroy NVDA's row
|
|
/// focus on every tick.
|
|
/// </summary>
|
|
internal sealed class LiveCheckedListBox : CheckedListBox
|
|
{
|
|
public void RefreshItemPublic(int index) => RefreshItem(index);
|
|
}
|
|
|
|
/// <summary>
|
|
/// User-facing choice that maps a friendly label to a codec + Opus frame size pair. The frame
|
|
/// size is in samples-per-channel at 48 kHz (v3.0 wire-format unit — see
|
|
/// <see cref="AudioFormatInfo"/>). 120 = 2.5 ms, 240 = 5 ms, 480 = 10 ms, 960 = 20 ms.
|
|
/// Only meaningful when Codec == Opus; for PCM it's ignored.
|
|
/// </summary>
|
|
internal sealed record CodecChoice(string Label, AudioTransportCodec Codec, int OpusFrameSamples)
|
|
{
|
|
public override string ToString() => Label;
|
|
}
|
|
|
|
internal sealed record AudioDeviceChoice(string Name, string? DeviceId, CaptureKind Kind = CaptureKind.Loopback)
|
|
{
|
|
public override string ToString() => Name;
|
|
}
|
|
|
|
internal sealed record RememberedPeerItem(string Entry)
|
|
{
|
|
public override string ToString() => Entry;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Live per-peer status surfaced in the connectivity dialog's listbox text. Mutated in place
|
|
/// each tick by MainForm.SyncAllDialogPeerLists, then ListBox.RefreshItem(i) is called on the
|
|
/// containing item so the visible label updates without rebuilding the listbox (which would
|
|
/// destroy NVDA focus on the row).
|
|
/// </summary>
|
|
internal sealed class PeerLineStatus
|
|
{
|
|
public bool Connected;
|
|
/// <summary>True when our sender is actively pushing audio at this peer.</summary>
|
|
public bool Sending;
|
|
/// <summary>True when audio is arriving from this peer's IP (a fresh receiver session exists).</summary>
|
|
public bool Receiving;
|
|
/// <summary>Codec label like "Opus 10ms", "Opus 20ms", "PCM". Null when not connected.</summary>
|
|
public string? CodecLabel;
|
|
/// <summary>Round-trip ping ms from heartbeat. Null when not connected or pending.</summary>
|
|
public int? RttMs;
|
|
}
|
|
|
|
internal sealed class PeerListItem
|
|
{
|
|
public PeerAnnouncement Peer { get; }
|
|
public PeerLineStatus Status { get; } = new();
|
|
|
|
public PeerListItem(PeerAnnouncement peer) { Peer = peer; }
|
|
|
|
/// <summary>
|
|
/// Stable identity for signature-based listbox-rebuild detection. Does NOT include live
|
|
/// status — that gets updated in-place via RefreshItem so NVDA focus survives tick updates.
|
|
/// </summary>
|
|
public string StableKey() => $"{Peer.InstanceId}:{Peer.Name}:{Peer.Address}:{Peer.AudioPort}";
|
|
|
|
public override string ToString()
|
|
{
|
|
// Base label: "hostname (ip)" for discovered peers, just "ip" for manual-by-IP entries
|
|
// (where hostname equals the IP address). Avoids "192.168.1.95 (192.168.1.95)" duplication.
|
|
var addr = Peer.Address.ToString();
|
|
var basePart = Peer.Name == addr ? addr : $"{Peer.Name} ({addr})";
|
|
|
|
if (!Status.Connected)
|
|
{
|
|
return basePart;
|
|
}
|
|
|
|
// Connected line — extra metadata after a dash. Comma-separated so NVDA reads naturally:
|
|
// "Andre's PC (1.2.3.4) — connected, Opus 10ms, send and receive, 32ms"
|
|
var parts = new List<string> { "connected" };
|
|
if (Status.CodecLabel is { Length: > 0 } codec) parts.Add(codec);
|
|
|
|
var direction = (Status.Sending, Status.Receiving) switch
|
|
{
|
|
(true, true) => "send and receive",
|
|
(true, false) => "send only",
|
|
(false, true) => "receive only",
|
|
_ => null,
|
|
};
|
|
if (direction is not null) parts.Add(direction);
|
|
|
|
if (Status.RttMs is { } rtt) parts.Add($"{rtt}ms");
|
|
|
|
return $"{basePart} — {string.Join(", ", parts)}";
|
|
}
|
|
}
|