* 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>
56 lines
2.7 KiB
C#
56 lines
2.7 KiB
C#
namespace RemSound.Core;
|
|
|
|
/// <summary>
|
|
/// Audio format announcement carried in every Format packet. <see cref="Lane"/> was added
|
|
/// 2026-05-11 alongside the BothIndependent audio mode — see <see cref="RenderRoute"/> for
|
|
/// the semantics. The field is wire-backward-compatible: old receivers parse the first 32
|
|
/// bytes of the format payload and ignore the extra; new receivers reading a 32-byte
|
|
/// payload from an old sender default Lane to <see cref="RenderRoute.Mixed"/>.
|
|
///
|
|
/// 2026-05-23 — <c>FrameDurationMilliseconds</c> renamed to <c>FrameSamplesPerChannel</c>
|
|
/// (v3.0 wire-format change). Previously held an int millisecond value; now holds the exact
|
|
/// sample-count per channel at the announced <see cref="SampleRate"/>. Conversion is
|
|
/// <c>ms = FrameSamplesPerChannel * 1000 / SampleRate</c> for display. The change exists
|
|
/// because Opus's 2.5 ms RESTRICTED_LOWDELAY frame (= 120 samples at 48 kHz) can't be
|
|
/// expressed cleanly in integer milliseconds; using sample-count also removes a lossy
|
|
/// conversion that the encoder/decoder pipeline previously did at every announcement.
|
|
/// v2.x receivers reading this field will misinterpret the value as milliseconds and
|
|
/// over-size their internal buffers; the actual decode still works because the Opus
|
|
/// decoder is self-describing from the packet TOC byte. Within-major-version (v3.x ↔ v3.x)
|
|
/// the field is unambiguous.
|
|
/// </summary>
|
|
public sealed record AudioFormatInfo(
|
|
int SampleRate,
|
|
int Channels,
|
|
int BitsPerSample,
|
|
int Encoding,
|
|
int BlockAlign,
|
|
int AverageBytesPerSecond,
|
|
int Codec = (int)AudioTransportCodec.Pcm,
|
|
int FrameSamplesPerChannel = 480,
|
|
RenderRoute Lane = RenderRoute.Mixed)
|
|
{
|
|
/// <summary>Human-friendly frame duration in milliseconds, derived from
|
|
/// <see cref="FrameSamplesPerChannel"/> and <see cref="SampleRate"/>. May be a fraction
|
|
/// (2.5 ms at 48 kHz / 120 samples). For string formatting only — the encoder/decoder
|
|
/// hot path uses <see cref="FrameSamplesPerChannel"/> directly.</summary>
|
|
public double FrameDurationMs => SampleRate > 0 ? FrameSamplesPerChannel * 1000.0 / SampleRate : 0;
|
|
|
|
public override string ToString()
|
|
{
|
|
var encodingName = Encoding switch
|
|
{
|
|
1 => "PCM",
|
|
3 => "IEEE float",
|
|
_ => $"encoding {Encoding}"
|
|
};
|
|
var codecName = (AudioTransportCodec)Codec switch
|
|
{
|
|
AudioTransportCodec.Opus => $" over Opus ({FrameDurationMs:0.##} ms)",
|
|
_ => ""
|
|
};
|
|
var laneName = Lane == RenderRoute.Mixed ? "" : $" [{Lane}]";
|
|
return $"{SampleRate} Hz, {Channels} channel(s), {BitsPerSample}-bit {encodingName}{codecName}{laneName}";
|
|
}
|
|
}
|