Peer rename + details box on Connectivity tab; Add band dialog fixes

Add band dialog (from testing feedback):
 * Tab order is now Start, End, Gain, OK, Cancel (OK was after Cancel).
 * Gain box takes decimals like 1.5 (half-dB steps); parametric list shows one decimal.
 * The two frequency boxes start empty — nothing prepopulated to mislead; both required on OK.

New Connectivity-tab feature (held for next release):
 * Rename peer (Alt+M) opens a dialog to give a peer a friendly name, with a Clear custom name
   button. Names are keyed by the peer's MACHINE NAME (stable across restarts, IP changes and
   networks), stored machine-wide in AppConfig.PeerFriendlyNames, and resolved everywhere a peer
   shows: both peer lists (via PeerListItem.DisplayNameProvider), the volume/pan/EQ list, the
   status line and split-recording filenames. Manual-by-IP peers with no announced name fall back
   to keying by address.
 * Peer details (Alt+E): a read-only box for the highlighted connected peer showing name, machine
   name, IP, connected-for, link health + ping, what they're sending, and whether they're
   receiving our audio. "Sending: 2 devices on ASIO at 48 kHz, Opus" is derived from the live
   receive streams — each stream is one device and its lane gives WASAPI vs ASIO — so NO protocol
   change and no new privacy exposure. AudioReceiver.ActiveFormatsFromAddress added for this.

Manual updated (readme.html + MANUAL.md). Build clean; --selftest passes; deployed to both test
folders. Held for next release.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-07-07 23:56:42 +01:00
co-authored by Claude Opus 4.8
parent 033edd776f
commit f35f6c3c28
8 changed files with 402 additions and 52 deletions
+10
View File
@@ -294,12 +294,20 @@ This is where you manage peers and reach the logging options. The controls on th
Control| Shortcut| What it does Control| Shortcut| What it does
---|---|--- ---|---|---
**Connected peers**| Alt+C| The people you currently have sound flowing with. Unticking a row disconnects that peer. **Connected peers**| Alt+C| The people you currently have sound flowing with. Unticking a row disconnects that peer.
**Peer details**| Alt+E| A read-only box describing whichever connected peer you're on in the list above. Arrow through it to read: their name, their machine name, their IP address, how long you've been connected, the link health and ping, what they're sending (how many devices, on WASAPI or ASIO, at what sample rate and codec), and whether they're receiving your audio. The device and WASAPI/ASIO detail only shows while you're actually receiving that peer.
**Rename peer**| Alt+M| Give the highlighted peer a friendly name of your choosing. It opens a box with the name and a **Clear custom name** button. The name sticks to that machine for good — across restarts, IP changes and networks — and shows everywhere that peer appears: the lists here, the volume, pan and EQ for peers tab, the status line and split-recording filenames. See below.
**Discovered peers**| Alt+D| People RemSound has heard from in the last few seconds. Tick someone to connect to them. **Discovered peers**| Alt+D| People RemSound has heard from in the last few seconds. Tick someone to connect to them.
**Remembered peers**| Alt+R| People you've connected to before, or added by address. This list is kept between sessions. Tick someone to reconnect. **Remembered peers**| Alt+R| People you've connected to before, or added by address. This list is kept between sessions. Tick someone to reconnect.
**Add peer by IP**| Alt+A| Opens a small box where you type an address or computer name. It adds that peer to the remembered list and connects. **Add peer by IP**| Alt+A| Opens a small box where you type an address or computer name. It adds that peer to the remembered list and connects.
**Lock to these exact peer addresses**| Alt+L| When ticked, this profile uses only the exact addresses you set and never follows the other computer by name or switches to a different address — even if the address stops working. Off by default, saved with the profile. See Locking a profile to one exact address. **Lock to these exact peer addresses**| Alt+L| When ticked, this profile uses only the exact addresses you set and never follows the other computer by name or switches to a different address — even if the address stops working. Off by default, saved with the profile. See Locking a profile to one exact address.
**Connection status**| Alt+S| A read-only box of text that sums up everything happening right now — how long you've been connected, how many peers you have, how much sound is flowing each way, and the connection health of each peer. Open it to read the current connection status. **Connection status**| Alt+S| A read-only box of text that sums up everything happening right now — how long you've been connected, how many peers you have, how much sound is flowing each way, and the connection health of each peer. Open it to read the current connection status.
### Giving a peer a friendly name
Highlight a peer in the **Connected peers** list and press **Rename peer (Alt+M)** to call them whatever you like — “Andre's desktop” instead of “ANDRE-DESKTOP”. The name is tied to that _machine_ , not to its address, so it survives their restarting RemSound, their address changing, and their reaching you on a different network (local network one day, Tailscale the next). Once set, it replaces the machine name everywhere that peer shows up — both peer lists, the volume, pan and EQ for peers list, the connection status, and the per-peer files a split recording makes.
In the rename box, type the name and press OK, or press **Clear custom name (Alt+C)** to drop back to the machine name. (Leaving the box empty and pressing OK does the same.) The names are kept per machine you're using RemSound on, and apply in every profile. One case to know about: a peer you added purely by address that never announces a name has no machine name to pin to, so its friendly name is tied to the address instead and would need re-setting if that address changes.
## 7. Audio inputs and outputs tab ## 7. Audio inputs and outputs tab
This tab controls everything to do with which sound devices are involved. The ASIO driver picker at the top decides whether ASIO is being used at all. The Receive side and the Send side each have their own master checkbox and their own device lists. This tab controls everything to do with which sound devices are involved. The ASIO driver picker at the top decides whether ASIO is being used at all. The Receive side and the Send side each have their own master checkbox and their own device lists.
@@ -757,6 +765,8 @@ Each tab has its own Alt+letter shortcuts. The same letter can do different thin
Key| Action Key| Action
---|--- ---|---
Alt+C| Focus the Connected peers list Alt+C| Focus the Connected peers list
Alt+E| Focus the Peer details box (for the highlighted connected peer)
Alt+M| Rename the highlighted connected peer
Alt+D| Focus the Discovered peers list Alt+D| Focus the Discovered peers list
Alt+R| Focus the Remembered peers list Alt+R| Focus the Remembered peers list
Alt+A| Add peer by IP Alt+A| Add peer by IP
+10
View File
@@ -332,6 +332,8 @@ ul, ol { padding-left: 1.4em; }
<table> <table>
<tr><th>Control</th><th>Shortcut</th><th>What it does</th></tr> <tr><th>Control</th><th>Shortcut</th><th>What it does</th></tr>
<tr><td><strong>Connected peers</strong></td><td>Alt+C</td><td>The people you currently have sound flowing with. Unticking a row disconnects that peer.</td></tr> <tr><td><strong>Connected peers</strong></td><td>Alt+C</td><td>The people you currently have sound flowing with. Unticking a row disconnects that peer.</td></tr>
<tr><td><strong>Peer details</strong></td><td>Alt+E</td><td>A read-only box describing whichever connected peer you're on in the list above. Arrow through it to read: their name, their machine name, their IP address, how long you've been connected, the link health and ping, what they're sending (how many devices, on WASAPI or ASIO, at what sample rate and codec), and whether they're receiving your audio. The device and WASAPI/ASIO detail only shows while you're actually receiving that peer.</td></tr>
<tr><td><strong>Rename peer</strong></td><td>Alt+M</td><td>Give the highlighted peer a friendly name of your choosing. It opens a box with the name and a <strong>Clear custom name</strong> button. The name sticks to that machine for good &mdash; across restarts, IP changes and networks &mdash; and shows everywhere that peer appears: the lists here, the volume, pan and EQ for peers tab, the status line and split-recording filenames. See below.</td></tr>
<tr><td><strong>Discovered peers</strong></td><td>Alt+D</td><td>People RemSound has heard from in the last few seconds. Tick someone to connect to them.</td></tr> <tr><td><strong>Discovered peers</strong></td><td>Alt+D</td><td>People RemSound has heard from in the last few seconds. Tick someone to connect to them.</td></tr>
<tr><td><strong>Remembered peers</strong></td><td>Alt+R</td><td>People you've connected to before, or added by address. This list is kept between sessions. Tick someone to reconnect.</td></tr> <tr><td><strong>Remembered peers</strong></td><td>Alt+R</td><td>People you've connected to before, or added by address. This list is kept between sessions. Tick someone to reconnect.</td></tr>
<tr><td><strong>Add peer by IP</strong></td><td>Alt+A</td><td>Opens a small box where you type an address or computer name. It adds that peer to the remembered list and connects.</td></tr> <tr><td><strong>Add peer by IP</strong></td><td>Alt+A</td><td>Opens a small box where you type an address or computer name. It adds that peer to the remembered list and connects.</td></tr>
@@ -339,6 +341,12 @@ ul, ol { padding-left: 1.4em; }
<tr><td><strong>Connection status</strong></td><td>Alt+S</td><td>A read-only box of text that sums up everything happening right now &mdash; how long you've been connected, how many peers you have, how much sound is flowing each way, and the connection health of each peer. Open it to read the current connection status.</td></tr> <tr><td><strong>Connection status</strong></td><td>Alt+S</td><td>A read-only box of text that sums up everything happening right now &mdash; how long you've been connected, how many peers you have, how much sound is flowing each way, and the connection health of each peer. Open it to read the current connection status.</td></tr>
</table> </table>
<h3>Giving a peer a friendly name</h3>
<p>Highlight a peer in the <strong>Connected peers</strong> list and press <strong>Rename peer (Alt+M)</strong> to call them whatever you like &mdash; &ldquo;Andre's desktop&rdquo; instead of &ldquo;ANDRE-DESKTOP&rdquo;. The name is tied to that <em>machine</em>, not to its address, so it survives their restarting RemSound, their address changing, and their reaching you on a different network (local network one day, Tailscale the next). Once set, it replaces the machine name everywhere that peer shows up &mdash; both peer lists, the <a href="#pan-eq">volume, pan and EQ for peers</a> list, the connection status, and the per-peer files a split recording makes.</p>
<p>In the rename box, type the name and press OK, or press <strong>Clear custom name (Alt+C)</strong> to drop back to the machine name. (Leaving the box empty and pressing OK does the same.) The names are kept per machine you're using RemSound on, and apply in every profile. One case to know about: a peer you added purely by address that never announces a name has no machine name to pin to, so its friendly name is tied to the address instead and would need re-setting if that address changes.</p>
<h2 id="tab-io">7. Audio inputs and outputs tab</h2> <h2 id="tab-io">7. Audio inputs and outputs tab</h2>
<p>This tab controls everything to do with which sound devices are involved. The ASIO driver picker at the top decides whether ASIO is being used at all. The Receive side and the Send side each have their own master checkbox and their own device lists.</p> <p>This tab controls everything to do with which sound devices are involved. The ASIO driver picker at the top decides whether ASIO is being used at all. The Receive side and the Send side each have their own master checkbox and their own device lists.</p>
@@ -797,6 +805,8 @@ Audient USB Audio ASIO Driver &mdash; Pair 3 (channels 5/6): Loop-back 1 (L) / L
<table> <table>
<tr><th>Key</th><th>Action</th></tr> <tr><th>Key</th><th>Action</th></tr>
<tr><td>Alt+C</td><td>Focus the Connected peers list</td></tr> <tr><td>Alt+C</td><td>Focus the Connected peers list</td></tr>
<tr><td>Alt+E</td><td>Focus the Peer details box (for the highlighted connected peer)</td></tr>
<tr><td>Alt+M</td><td>Rename the highlighted connected peer</td></tr>
<tr><td>Alt+D</td><td>Focus the Discovered peers list</td></tr> <tr><td>Alt+D</td><td>Focus the Discovered peers list</td></tr>
<tr><td>Alt+R</td><td>Focus the Remembered peers list</td></tr> <tr><td>Alt+R</td><td>Focus the Remembered peers list</td></tr>
<tr><td>Alt+A</td><td>Add peer by IP</td></tr> <tr><td>Alt+A</td><td>Add peer by IP</td></tr>
+80 -38
View File
@@ -1,22 +1,25 @@
using System.Globalization;
using RemSound.Core; using RemSound.Core;
namespace RemSound.App; namespace RemSound.App;
/// <summary>Modal dialog for adding one parametric EQ band. The user sets a start frequency, an end /// <summary>Modal dialog for adding one parametric EQ band. The user sets a start frequency, an end
/// frequency and a gain; all three are spin-or-type boxes that refuse non-numbers and clamp to range. /// frequency and a gain; all three are spin-or-type boxes that refuse non-numbers and clamp to range.
/// While the dialog is open it previews the in-progress band live (so the peer's sound changes as the /// The two frequency boxes start EMPTY (no prepopulated value to mislead the user); the gain defaults
/// values move); OK keeps the band, Cancel/Escape drops it and reverts the preview.</summary> /// to a gentle boost. While the dialog is open it previews the in-progress band live (so the peer's
/// sound changes as the values move); OK keeps the band, Cancel/Escape drops it and reverts.</summary>
internal sealed class AddBandDialog : Form internal sealed class AddBandDialog : Form
{ {
private readonly NumericUpDown startFreq = new() private readonly NumericUpDown startFreq = new()
{ {
Minimum = (decimal)PeerEqBands.ParametricMinHz, Minimum = (decimal)PeerEqBands.ParametricMinHz,
Maximum = (decimal)PeerEqBands.ParametricMaxHz, Maximum = (decimal)PeerEqBands.ParametricMaxHz,
Value = 200, Value = (decimal)PeerEqBands.ParametricMinHz,
Increment = 10, Increment = 10,
DecimalPlaces = 0, DecimalPlaces = 0,
Width = 120, Width = 120,
TextAlign = HorizontalAlignment.Right, TextAlign = HorizontalAlignment.Right,
TabIndex = 0,
AccessibleName = "Start frequency in Hertz (Alt+S)", AccessibleName = "Start frequency in Hertz (Alt+S)",
}; };
@@ -24,11 +27,12 @@ internal sealed class AddBandDialog : Form
{ {
Minimum = (decimal)PeerEqBands.ParametricMinHz, Minimum = (decimal)PeerEqBands.ParametricMinHz,
Maximum = (decimal)PeerEqBands.ParametricMaxHz, Maximum = (decimal)PeerEqBands.ParametricMaxHz,
Value = 2000, Value = (decimal)PeerEqBands.ParametricMinHz,
Increment = 10, Increment = 10,
DecimalPlaces = 0, DecimalPlaces = 0,
Width = 120, Width = 120,
TextAlign = HorizontalAlignment.Right, TextAlign = HorizontalAlignment.Right,
TabIndex = 1,
AccessibleName = "End frequency in Hertz (Alt+E)", AccessibleName = "End frequency in Hertz (Alt+E)",
}; };
@@ -37,27 +41,24 @@ internal sealed class AddBandDialog : Form
Minimum = -(decimal)PeerEqBands.MaxGainDb, Minimum = -(decimal)PeerEqBands.MaxGainDb,
Maximum = (decimal)PeerEqBands.MaxGainDb, Maximum = (decimal)PeerEqBands.MaxGainDb,
Value = 3, Value = 3,
Increment = 1, Increment = 0.5m, // allow half-dB steps; typing any value like 1.5 works too
DecimalPlaces = 0, DecimalPlaces = 1,
Width = 120, Width = 120,
TextAlign = HorizontalAlignment.Right, TextAlign = HorizontalAlignment.Right,
TabIndex = 2,
AccessibleName = "Gain in dB (Alt+G)", AccessibleName = "Gain in dB (Alt+G)",
}; };
private readonly Action<ParametricBand?>? livePreview; private readonly Action<ParametricBand?>? livePreview;
private ParametricBand? resultBand;
private bool accepted; private bool accepted;
/// <summary>The band the user built, valid only when <see cref="Form.ShowDialog()"/> returned OK.</summary> /// <summary>The band the user built, valid only when <see cref="Form.ShowDialog()"/> returned OK.</summary>
public ParametricBand Result => new() public ParametricBand Result => resultBand ?? new ParametricBand();
{
StartHz = (float)startFreq.Value,
EndHz = (float)endFreq.Value,
GainDb = (float)gainDb.Value,
};
/// <param name="livePreview">Called with the in-progress band on every value change so the caller /// <param name="livePreview">Called with the in-progress band on every value change so the caller
/// can apply it to the peer in real time, and with null when the dialog is cancelled/closed so the /// can apply it to the peer in real time, and with null when the band is incomplete or the dialog
/// caller reverts to the saved shaping.</param> /// is cancelled/closed so the caller reverts to the saved shaping.</param>
public AddBandDialog(Action<ParametricBand?>? livePreview = null) public AddBandDialog(Action<ParametricBand?>? livePreview = null)
{ {
this.livePreview = livePreview; this.livePreview = livePreview;
@@ -84,29 +85,39 @@ internal sealed class AddBandDialog : Form
AddRow(grid, 1, "&End frequency in Hz (Alt+E)", endFreq); AddRow(grid, 1, "&End frequency in Hz (Alt+E)", endFreq);
AddRow(grid, 2, "&Gain in dB (Alt+G)", gainDb); AddRow(grid, 2, "&Gain in dB (Alt+G)", gainDb);
var okButton = new Button { Text = "OK", AutoSize = true, DialogResult = DialogResult.None }; var okButton = new Button { Text = "OK", AutoSize = true, DialogResult = DialogResult.None, TabIndex = 0 };
var cancelButton = new Button { Text = "Cancel", AutoSize = true, DialogResult = DialogResult.Cancel }; var cancelButton = new Button { Text = "Cancel", AutoSize = true, DialogResult = DialogResult.Cancel, TabIndex = 1 };
okButton.Click += (_, _) => TryAccept(); okButton.Click += (_, _) => TryAccept();
// OK before Cancel, in both tab order and left-to-right layout.
var buttonRow = new FlowLayoutPanel var buttonRow = new FlowLayoutPanel
{ {
Dock = DockStyle.Fill, Dock = DockStyle.Fill,
FlowDirection = FlowDirection.RightToLeft, FlowDirection = FlowDirection.LeftToRight,
AutoSize = true, AutoSize = true,
TabIndex = 3,
}; };
buttonRow.Controls.Add(cancelButton);
buttonRow.Controls.Add(okButton); buttonRow.Controls.Add(okButton);
buttonRow.Controls.Add(cancelButton);
grid.Controls.Add(buttonRow, 1, 3); grid.Controls.Add(buttonRow, 1, 3);
Controls.Add(grid); Controls.Add(grid);
AcceptButton = okButton; AcceptButton = okButton;
CancelButton = cancelButton; CancelButton = cancelButton;
startFreq.ValueChanged += (_, _) => Preview(); foreach (var box in new[] { startFreq, endFreq, gainDb })
endFreq.ValueChanged += (_, _) => Preview(); {
gainDb.ValueChanged += (_, _) => Preview(); box.ValueChanged += (_, _) => Preview();
box.TextChanged += (_, _) => Preview();
}
Shown += (_, _) => { startFreq.Focus(); Preview(); }; Shown += (_, _) =>
{
// Blank the two frequency boxes so nothing is prepopulated; the user must enter both.
startFreq.Text = "";
endFreq.Text = "";
startFreq.Focus();
};
} }
// The mnemonic hint is embedded in each NumericUpDown's AccessibleName; the label carries the // The mnemonic hint is embedded in each NumericUpDown's AccessibleName; the label carries the
@@ -114,39 +125,70 @@ internal sealed class AddBandDialog : Form
private static void AddRow(TableLayoutPanel grid, int row, string labelText, NumericUpDown box) private static void AddRow(TableLayoutPanel grid, int row, string labelText, NumericUpDown box)
{ {
var label = new Label { Text = labelText, AutoSize = true, Anchor = AnchorStyles.Left, Margin = new Padding(0, 6, 8, 0) }; var label = new Label { Text = labelText, AutoSize = true, Anchor = AnchorStyles.Left, Margin = new Padding(0, 6, 8, 0) };
// A plain Label with '&' wires the mnemonic to the next control in tab order (the box).
grid.Controls.Add(label, 0, row); grid.Controls.Add(label, 0, row);
grid.Controls.Add(box, 1, row); grid.Controls.Add(box, 1, row);
} }
private ParametricBand Current() => new() private static bool TryReadHz(NumericUpDown box, out float hz)
{ {
StartHz = (float)startFreq.Value, hz = 0f;
EndHz = (float)endFreq.Value, var t = box.Text.Trim();
GainDb = (float)gainDb.Value, if (t.Length == 0) return false;
}; if (!float.TryParse(t, NumberStyles.Float, CultureInfo.CurrentCulture, out hz)) return false;
return hz >= PeerEqBands.ParametricMinHz && hz <= PeerEqBands.ParametricMaxHz;
}
private void Preview() => livePreview?.Invoke(Current()); private bool TryReadDb(out float db)
{
db = 0f;
var t = gainDb.Text.Trim();
if (t.Length == 0) return false;
if (!float.TryParse(t, NumberStyles.Float, CultureInfo.CurrentCulture, out db)) return false;
db = Math.Clamp(db, -PeerEqBands.MaxGainDb, PeerEqBands.MaxGainDb);
return true;
}
private void Preview()
{
if (TryReadHz(startFreq, out float s) && TryReadHz(endFreq, out float e) && e > s)
livePreview?.Invoke(new ParametricBand { StartHz = s, EndHz = e, GainDb = TryReadDb(out float d) ? d : 0f });
else
livePreview?.Invoke(null); // incomplete / invalid → nothing to preview
}
private void TryAccept() private void TryAccept()
{ {
if (endFreq.Value <= startFreq.Value) if (!TryReadHz(startFreq, out float s) || !TryReadHz(endFreq, out float e))
{
Warn("Enter both frequencies",
$"Enter a start and an end frequency, each between {PeerEqBands.ParametricMinHz:0} and {PeerEqBands.ParametricMaxHz:0} Hz.");
startFreq.Focus();
return;
}
if (e <= s)
{
Warn("End frequency must be higher than start",
$"The end frequency ({e:0} Hz) must be higher than the start frequency ({s:0} Hz). Adjust one of them and try again.");
endFreq.Focus();
return;
}
resultBand = new ParametricBand { StartHz = s, EndHz = e, GainDb = TryReadDb(out float d) ? d : 0f };
accepted = true;
DialogResult = DialogResult.OK;
Close();
}
private void Warn(string heading, string text)
{ {
var page = new TaskDialogPage var page = new TaskDialogPage
{ {
Caption = "Add EQ band", Caption = "Add EQ band",
Heading = "End frequency must be higher than start", Heading = heading,
Text = $"The end frequency ({endFreq.Value:0} Hz) must be higher than the start frequency ({startFreq.Value:0} Hz). Adjust one of them and try again.", Text = text,
Icon = TaskDialogIcon.Warning, Icon = TaskDialogIcon.Warning,
Buttons = { TaskDialogButton.OK }, Buttons = { TaskDialogButton.OK },
}; };
TaskDialog.ShowDialog(this, page); TaskDialog.ShowDialog(this, page);
endFreq.Focus();
return;
}
accepted = true;
DialogResult = DialogResult.OK;
Close();
} }
protected override void OnFormClosing(FormClosingEventArgs e) protected override void OnFormClosing(FormClosingEventArgs e)
+179 -12
View File
@@ -268,6 +268,9 @@ public sealed class MainForm : Form
private readonly CheckedListBox rememberedPeersList = new() { CheckOnClick = true, Width = 430, Height = 90, AccessibleName = "Remembered peers (Alt+R)" }; private readonly CheckedListBox rememberedPeersList = new() { CheckOnClick = true, Width = 430, Height = 90, AccessibleName = "Remembered peers (Alt+R)" };
private readonly Label rememberedPeersStatus = new() { AutoSize = true, Text = "No remembered peer selected." }; private readonly Label rememberedPeersStatus = new() { AutoSize = true, Text = "No remembered peer selected." };
private readonly Button manualAddButton = new() { Text = "Add peer by IP (Alt+&A)", AutoSize = true, AccessibleName = "Add peer by IP" }; private readonly Button manualAddButton = new() { Text = "Add peer by IP (Alt+&A)", AutoSize = true, AccessibleName = "Add peer by IP" };
// Read-only details for the peer highlighted in the connected list, and a button to give it a name.
private readonly TextBox peerDetailsBox = new() { Multiline = true, ReadOnly = true, Width = 430, Height = 132, ScrollBars = ScrollBars.Vertical, AccessibleName = "Peer details (Alt+E)" };
private readonly Button renamePeerButton = new() { Text = "Rena&me peer (Alt+M)", AutoSize = true, AccessibleName = "Rename peer" };
// Per-profile "pin to exact addresses" toggle (#17). When ticked, RefreshKnownPeers skips the // Per-profile "pin to exact addresses" toggle (#17). When ticked, RefreshKnownPeers skips the
// discovered-peer merge and the address-follow, so the profile's peers stay exactly as the user set. // discovered-peer merge and the address-follow, so the profile's peers stay exactly as the user set.
private readonly AccessibleCheckBox lockPeerAddressesBox = new() private readonly AccessibleCheckBox lockPeerAddressesBox = new()
@@ -419,6 +422,12 @@ public sealed class MainForm : Form
// Display labels for selected peers so we can render them in the dialog list even when // Display labels for selected peers so we can render them in the dialog list even when
// discovery has temporarily lost sight of them ("Foo (192.168.1.5) — offline"). // discovery has temporarily lost sight of them ("Foo (192.168.1.5) — offline").
private readonly Dictionary<Guid, string> selectedPeerLabels = []; private readonly Dictionary<Guid, string> selectedPeerLabels = [];
// Custom friendly names, keyed by peer identity (machine name, else address). Loaded from AppConfig
// (machine-wide) at startup and mirrored back on rename. Resolved to display names everywhere a peer
// shows — connected/discovered lists, the volume/pan/EQ list, the status line, split recordings.
private Dictionary<string, string> peerFriendlyNames = new(StringComparer.OrdinalIgnoreCase);
// When each connected peer (by per-run InstanceId) first went healthy — for the "connected for" line.
private readonly Dictionary<Guid, DateTime> peerConnectedSinceUtc = [];
// Anti-thrash state for the discovery-driven endpoint follow (see the peer-rebuild loop). A peer // Anti-thrash state for the discovery-driven endpoint follow (see the peer-rebuild loop). A peer
// reachable at two addresses at once (a VPN address AND a LAN address, say) announces from both, // reachable at two addresses at once (a VPN address AND a LAN address, say) announces from both,
@@ -842,6 +851,11 @@ public sealed class MainForm : Form
.Select(kv => (kv.Value.Address, selectedPeerLabels.GetValueOrDefault(kv.Key, kv.Value.Address.ToString()))) .Select(kv => (kv.Value.Address, selectedPeerLabels.GetValueOrDefault(kv.Key, kv.Value.Address.ToString())))
.ToList(); .ToList();
// Load the machine-wide friendly-name book and make every peer list resolve display names
// through it. Set before any list is built so names show from the first render.
peerFriendlyNames = new Dictionary<string, string>(AppConfig.Load().PeerFriendlyNames ?? new(), StringComparer.OrdinalIgnoreCase);
PeerListItem.DisplayNameProvider = ResolvePeerDisplayName;
// --- Set accessibility names --- // --- Set accessibility names ---
// For these four controls the keyboard shortcut is included explicitly in both the // For these four controls the keyboard shortcut is included explicitly in both the
// visible label (set in BuildLayout) and the AccessibleName, instead of relying on the // visible label (set in BuildLayout) and the AccessibleName, instead of relying on the
@@ -2974,13 +2988,24 @@ public sealed class MainForm : Form
// Preferences dialog (File → Preferences, Ctrl+P) as the last two items. // Preferences dialog (File → Preferences, Ctrl+P) as the last two items.
// === Layout === // === Layout ===
// 6 rows: 02 the three peer lists, 3 manual-add, 4 the lock-to-fixed-addresses toggle, 5 status. // 8 rows: 0 connected peers, 1 the details box + 2 rename button (both about the highlighted
panel.RowCount = 6; // connected peer), 34 discovered & remembered lists, 5 manual-add, 6 the lock toggle, 7 status.
panel.RowCount = 8;
FormLayoutRows.AddCheckedListRow(panel, 0, "Connected peers (Alt+&C)", connectedPeersList, connectedPeersStatus, FocusListControl); FormLayoutRows.AddCheckedListRow(panel, 0, "Connected peers (Alt+&C)", connectedPeersList, connectedPeersStatus, FocusListControl);
FormLayoutRows.AddCheckedListRow(panel, 1, "Discovered peers (Alt+&D)", discoveredPeersList, discoveredPeersStatus, FocusListControl);
FormLayoutRows.AddCheckedListRow(panel, 2, "Remembered peers (Alt+&R)", rememberedPeersList, rememberedPeersStatus, FocusListControl); // Details of, and a rename for, the peer highlighted in the connected list above.
panel.Controls.Add(new Label { Text = "Manual peer", AutoSize = true, Anchor = AnchorStyles.Left }, 0, 3); var detailsLabel = new MnemonicLabel { Text = "Peer d&etails (Alt+E)", AutoSize = true, Anchor = AnchorStyles.Left, MnemonicTarget = peerDetailsBox };
panel.Controls.Add(manualAddButton, 1, 3); detailsLabel.Click += (_, _) => peerDetailsBox.Focus();
panel.Controls.Add(detailsLabel, 0, 1);
panel.Controls.Add(peerDetailsBox, 1, 1);
renamePeerButton.Click += (_, _) => OnRenamePeer();
panel.Controls.Add(renamePeerButton, 1, 2);
connectedPeersList.SelectedIndexChanged += (_, _) => UpdatePeerDetails();
FormLayoutRows.AddCheckedListRow(panel, 3, "Discovered peers (Alt+&D)", discoveredPeersList, discoveredPeersStatus, FocusListControl);
FormLayoutRows.AddCheckedListRow(panel, 4, "Remembered peers (Alt+&R)", rememberedPeersList, rememberedPeersStatus, FocusListControl);
panel.Controls.Add(new Label { Text = "Manual peer", AutoSize = true, Anchor = AnchorStyles.Left }, 0, 5);
panel.Controls.Add(manualAddButton, 1, 5);
lockPeerAddressesBox.Checked = settings.LoadLockPeerAddresses(); lockPeerAddressesBox.Checked = settings.LoadLockPeerAddresses();
lockPeerAddressesBox.CheckedChanged += (_, _) => lockPeerAddressesBox.CheckedChanged += (_, _) =>
@@ -2989,14 +3014,15 @@ public sealed class MainForm : Form
MarkProfileDirty(); MarkProfileDirty();
logFile.Event($"lock peer addresses: {(lockPeerAddressesBox.Checked ? "on" : "off")}"); logFile.Event($"lock peer addresses: {(lockPeerAddressesBox.Checked ? "on" : "off")}");
}; };
panel.Controls.Add(lockPeerAddressesBox, 0, 4); panel.Controls.Add(lockPeerAddressesBox, 0, 6);
panel.SetColumnSpan(lockPeerAddressesBox, 2); panel.SetColumnSpan(lockPeerAddressesBox, 2);
// Connection status readout — last row, tab-into-able. // Connection status readout — last row, tab-into-able.
var statusLabel = new MnemonicLabel { Text = "Connection status (Alt+&S)", AutoSize = true, Anchor = AnchorStyles.Left, MnemonicTarget = statusReadout }; var statusLabel = new MnemonicLabel { Text = "Connection status (Alt+&S)", AutoSize = true, Anchor = AnchorStyles.Left, MnemonicTarget = statusReadout };
statusLabel.Click += (_, _) => statusReadout.Focus(); statusLabel.Click += (_, _) => statusReadout.Focus();
panel.Controls.Add(statusLabel, 0, 5); panel.Controls.Add(statusLabel, 0, 7);
panel.Controls.Add(statusReadout, 1, 5); panel.Controls.Add(statusReadout, 1, 7);
UpdatePeerDetails();
// Initial render so the box has content the moment the user tabs into it. // Initial render so the box has content the moment the user tabs into it.
RefreshStatusReadout(); RefreshStatusReadout();
@@ -3495,12 +3521,19 @@ public sealed class MainForm : Form
// dB spoken as words — most NVDA users run with punctuation off and would never hear a "+" sign, // dB spoken as words — most NVDA users run with punctuation off and would never hear a "+" sign,
// so a boost must say "plus". ("minus" comes through on its own, but we spell both for symmetry.) // so a boost must say "plus". ("minus" comes through on its own, but we spell both for symmetry.)
// The graphic sliders read whole dB; the parametric list keeps up to one decimal (e.g. 1.5 dB).
private static string FormatGainDb(float db) private static string FormatGainDb(float db)
{ {
if (MathF.Abs(db) < 0.5f) return "flat"; if (MathF.Abs(db) < 0.5f) return "flat";
return db > 0 ? $"plus {db:0} dB" : $"minus {MathF.Abs(db):0} dB"; return db > 0 ? $"plus {db:0} dB" : $"minus {MathF.Abs(db):0} dB";
} }
private static string FormatGainDbPrecise(float db)
{
if (MathF.Abs(db) < 0.05f) return "flat";
return db > 0 ? $"plus {db:0.#} dB" : $"minus {MathF.Abs(db):0.#} dB";
}
/// <summary>Whether a given peer is shaped right now: the profile-wide master switch AND that peer's /// <summary>Whether a given peer is shaped right now: the profile-wide master switch AND that peer's
/// own tick (per-peer bypass) both have to be on.</summary> /// own tick (per-peer bypass) both have to be on.</summary>
private bool ShapingActiveFor(string? key) private bool ShapingActiveFor(string? key)
@@ -3649,7 +3682,7 @@ public sealed class MainForm : Form
private sealed class ParametricBandItem(ParametricBand band) private sealed class ParametricBandItem(ParametricBand band)
{ {
public ParametricBand Band { get; } = band; public ParametricBand Band { get; } = band;
public override string ToString() => $"{Band.StartHz:0} Hz to {Band.EndHz:0} Hz, {FormatGainDb(Band.GainDb)}"; public override string ToString() => $"{Band.StartHz:0} Hz to {Band.EndHz:0} Hz, {FormatGainDbPrecise(Band.GainDb)}";
} }
/// <summary>Send-side controls: codec + packet size on row 0, lock-to-audio-clock on /// <summary>Send-side controls: codec + packet size on row 0, lock-to-audio-clock on
@@ -4189,11 +4222,145 @@ public sealed class MainForm : Form
? RoundToFive(rtt) ? RoundToFive(rtt)
: null; : null;
// Keep the friendly-name label fresh for the pan/EQ list, status line and recordings, and
// track when this peer first went healthy for the "connected for" line.
selectedPeerLabels[item.Peer.InstanceId] = ResolvePeerDisplayName(item.Peer);
if (isHealthy) { if (!peerConnectedSinceUtc.ContainsKey(item.Peer.InstanceId)) peerConnectedSinceUtc[item.Peer.InstanceId] = DateTime.UtcNow; }
else peerConnectedSinceUtc.Remove(item.Peer.InstanceId);
if (item.ToString() != prevText) if (item.ToString() != prevText)
{ {
connectedPeersList.RefreshItemPublic(i); connectedPeersList.RefreshItemPublic(i);
} }
} }
UpdatePeerDetails();
}
// === Peer identity, friendly names, and the details box ===
/// <summary>The stable key a friendly name is stored under: the peer's machine name when it's a real
/// name, otherwise its address (the manual-by-IP case, where Name equals the address string).</summary>
private static string PeerIdentityKey(PeerAnnouncement peer)
{
var addr = peer.Address.ToString();
return string.IsNullOrWhiteSpace(peer.Name) || peer.Name == addr ? addr : peer.Name;
}
/// <summary>The name to show for a peer: its custom friendly name if set, else its machine name
/// (which for a manual-by-IP peer is the address). Used by every peer list via
/// <see cref="PeerListItem.DisplayNameProvider"/>.</summary>
private string ResolvePeerDisplayName(PeerAnnouncement peer)
=> peerFriendlyNames.TryGetValue(PeerIdentityKey(peer), out var custom) && !string.IsNullOrWhiteSpace(custom)
? custom
: peer.Name;
private PeerListItem? SelectedConnectedPeer() => SafeSelectedItem(connectedPeersList) as PeerListItem;
/// <summary>Refreshes the read-only details box for whichever peer is highlighted in the connected
/// list. Runs on selection change and each status tick (so "connected for" and ping stay live).</summary>
private void UpdatePeerDetails()
{
var item = SelectedConnectedPeer();
renamePeerButton.Enabled = item is not null;
var text = item is null ? "Select a connected peer to see its details." : BuildPeerDetailsText(item);
if (peerDetailsBox.Text != text) peerDetailsBox.Text = text;
}
private string BuildPeerDetailsText(PeerListItem item)
{
var peer = item.Peer;
var lines = new List<string>();
var display = ResolvePeerDisplayName(peer);
var machine = string.IsNullOrWhiteSpace(peer.Name) || peer.Name == peer.Address.ToString() ? null : peer.Name;
if (!string.Equals(display, machine, StringComparison.Ordinal) && !string.Equals(display, peer.Address.ToString(), StringComparison.Ordinal))
lines.Add($"Name: {display}");
lines.Add($"Machine name: {machine ?? "(unknown added by address)"}");
lines.Add($"Address: {peer.Address}");
if (peerConnectedSinceUtc.TryGetValue(peer.InstanceId, out var since))
lines.Add($"Connected for: {DescribeDuration(DateTime.UtcNow - since)}");
if (item.Status.Connected)
lines.Add(item.Status.RttMs is { } rtt ? $"Link: healthy, ping {rtt} ms" : "Link: healthy");
else
lines.Add("Link: not connected");
lines.Add("Sending: " + DescribeSending(peer));
lines.Add("Receiving your audio: " + (item.Status.Sending ? "yes" : "no"));
return string.Join(Environment.NewLine, lines);
}
/// <summary>"2 devices on ASIO at 48 kHz, Opus" — built from the live receive streams (each stream is
/// one capture device; its lane tells us WASAPI vs ASIO). Falls back gracefully when we're not
/// receiving that peer.</summary>
private string DescribeSending(PeerAnnouncement peer)
{
var formats = receiver.ActiveFormatsFromAddress(peer.Address);
if (formats.Count == 0)
return peer.CanSend ? "yes — turn on Receive audio to see the details" : "no";
string deviceWord = formats.Count == 1 ? "1 device" : $"{formats.Count} devices";
var apis = formats.Select(f => f.Lane == RenderRoute.AsioLane ? "ASIO" : "WASAPI").Distinct().OrderBy(a => a).ToList();
string apiPart = apis.Count == 1 ? $" on {apis[0]}" : $" on {string.Join(" and ", apis)}";
var rates = formats.Select(f => f.SampleRate).Distinct().ToList();
string ratePart = rates.Count == 1 ? $" at {rates[0]} Hz" : "";
var codecs = formats.Select(f => ((AudioTransportCodec)f.Codec) == AudioTransportCodec.Opus ? "Opus" : "PCM").Distinct().ToList();
string codecPart = codecs.Count == 1 ? $", {codecs[0]}" : "";
return $"{deviceWord}{apiPart}{ratePart}{codecPart}";
}
private static string DescribeDuration(TimeSpan d)
{
if (d.TotalSeconds < 60) return $"{(int)d.TotalSeconds} seconds";
if (d.TotalMinutes < 60) return $"{(int)d.TotalMinutes} minute{((int)d.TotalMinutes == 1 ? "" : "s")}";
int hours = (int)d.TotalHours;
int mins = d.Minutes;
return mins == 0 ? $"{hours} hour{(hours == 1 ? "" : "s")}" : $"{hours} hour{(hours == 1 ? "" : "s")} {mins} minute{(mins == 1 ? "" : "s")}";
}
private void OnRenamePeer()
{
var item = SelectedConnectedPeer();
if (item is null) return;
var peer = item.Peer;
var key = PeerIdentityKey(peer);
var machineForDisplay = string.IsNullOrWhiteSpace(peer.Name) || peer.Name == peer.Address.ToString()
? peer.Address.ToString()
: peer.Name;
peerFriendlyNames.TryGetValue(key, out var current);
using var dlg = new RenamePeerDialog(machineForDisplay, current);
if (dlg.ShowDialog(this) != DialogResult.OK) return;
ApplyFriendlyName(key, dlg.FriendlyName);
}
/// <summary>Store (or clear) a peer's friendly name in the machine-wide book, persist it, and
/// refresh every place a peer name shows.</summary>
private void ApplyFriendlyName(string identityKey, string? name)
{
if (string.IsNullOrWhiteSpace(name)) peerFriendlyNames.Remove(identityKey);
else peerFriendlyNames[identityKey] = name.Trim();
try
{
var cfg = AppConfig.Load();
cfg.PeerFriendlyNames = new Dictionary<string, string>(peerFriendlyNames, StringComparer.OrdinalIgnoreCase);
cfg.Save();
}
catch (Exception ex) { logFile.Event($"peer rename: failed to save friendly name: {ex.Message}"); }
// Refresh everywhere: connected/discovered lists rebuild their labels, the pan/EQ list re-reads
// its peer names, and the details box updates.
lastPanEqPeerSignature = "";
SyncAllPeerLists();
RefreshPanEqPeerList();
UpdatePeerDetails();
} }
private void SyncDiscoveredList() private void SyncDiscoveredList()
@@ -5872,7 +6039,7 @@ public sealed class MainForm : Form
foreach (var (id, oldEndpoint) in selectedPeerEndpoints.ToList()) foreach (var (id, oldEndpoint) in selectedPeerEndpoints.ToList())
{ {
if (!knownPeers.TryGetValue(id, out var peer)) continue; if (!knownPeers.TryGetValue(id, out var peer)) continue;
selectedPeerLabels[id] = peer.Name; selectedPeerLabels[id] = ResolvePeerDisplayName(peer);
var newEndpoint = new IPEndPoint(peer.Address, peer.AudioPort); var newEndpoint = new IPEndPoint(peer.Address, peer.AudioPort);
// Same address, or the one we're on is still healthy: nothing to do — and reset the // Same address, or the one we're on is still healthy: nothing to do — and reset the
@@ -5912,7 +6079,7 @@ public sealed class MainForm : Form
private void SelectPeer(PeerAnnouncement peer, bool fromProfileRestore) private void SelectPeer(PeerAnnouncement peer, bool fromProfileRestore)
{ {
selectedPeerEndpoints[peer.InstanceId] = new IPEndPoint(peer.Address, peer.AudioPort); selectedPeerEndpoints[peer.InstanceId] = new IPEndPoint(peer.Address, peer.AudioPort);
selectedPeerLabels[peer.InstanceId] = peer.Name; selectedPeerLabels[peer.InstanceId] = ResolvePeerDisplayName(peer);
logFile.Event($"peer selected: {peer.Name} {peer.Address}:{peer.AudioPort}"); logFile.Event($"peer selected: {peer.Name} {peer.Address}:{peer.AudioPort}");
InvalidateAutoTuneHistory(); InvalidateAutoTuneHistory();
PushAllowedReceiveSenders(); PushAllowedReceiveSenders();
+7 -1
View File
@@ -63,6 +63,11 @@ internal sealed class PeerLineStatus
internal sealed class PeerListItem internal sealed class PeerListItem
{ {
/// <summary>Optional resolver mapping a peer to the display name to show — e.g. a user-set friendly
/// name. Set once by MainForm. When null (or it returns the machine name) behaviour is unchanged,
/// so this covers the connected AND discovered lists in one place.</summary>
public static Func<PeerAnnouncement, string>? DisplayNameProvider;
public PeerAnnouncement Peer { get; } public PeerAnnouncement Peer { get; }
public PeerLineStatus Status { get; } = new(); public PeerLineStatus Status { get; } = new();
@@ -79,7 +84,8 @@ internal sealed class PeerListItem
// Base label: "hostname (ip)" for discovered peers, just "ip" for manual-by-IP entries // 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. // (where hostname equals the IP address). Avoids "192.168.1.95 (192.168.1.95)" duplication.
var addr = Peer.Address.ToString(); var addr = Peer.Address.ToString();
var basePart = Peer.Name == addr ? addr : $"{Peer.Name} ({addr})"; var name = DisplayNameProvider?.Invoke(Peer) ?? Peer.Name;
var basePart = name == addr ? addr : $"{name} ({addr})";
if (!Status.Connected) if (!Status.Connected)
{ {
+84
View File
@@ -0,0 +1,84 @@
namespace RemSound.App;
/// <summary>Modal dialog for giving a peer a friendly name. Shows the machine name for reference, a
/// text box for the custom name, and a "Clear custom name" button that wipes it back to the machine
/// name. OK (or Clear) returns DialogResult.OK; the caller reads <see cref="FriendlyName"/> — null
/// meaning "no custom name" (cleared or left blank). Cancel/Escape makes no change.</summary>
internal sealed class RenamePeerDialog : Form
{
private readonly TextBox nameBox = new()
{
Width = 260,
TabIndex = 0,
AccessibleName = "Friendly name (Alt+N)",
};
/// <summary>The chosen name, valid only when ShowDialog returned OK. Null = no custom name.</summary>
public string? FriendlyName { get; private set; }
public RenamePeerDialog(string machineName, string? currentCustomName)
{
Text = "Rename peer";
StartPosition = FormStartPosition.CenterParent;
FormBorderStyle = FormBorderStyle.FixedDialog;
MinimizeBox = false;
MaximizeBox = false;
ShowInTaskbar = false;
ClientSize = new Size(380, 170);
nameBox.Text = currentCustomName ?? "";
var grid = new TableLayoutPanel
{
Dock = DockStyle.Fill,
Padding = new Padding(12),
ColumnCount = 2,
RowCount = 3,
};
grid.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
grid.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
var machineLabel = new Label
{
Text = $"Machine name: {machineName}",
AutoSize = true,
Anchor = AnchorStyles.Left,
Margin = new Padding(0, 0, 0, 8),
};
grid.Controls.Add(machineLabel, 0, 0);
grid.SetColumnSpan(machineLabel, 2);
// Plain label with '&' wires the mnemonic to the next control in tab order (the text box).
var nameLabel = new Label { Text = "Friendly &name (Alt+N)", AutoSize = true, Anchor = AnchorStyles.Left, Margin = new Padding(0, 4, 8, 0) };
grid.Controls.Add(nameLabel, 0, 1);
grid.Controls.Add(nameBox, 1, 1);
var clearButton = new Button { Text = "&Clear custom name (Alt+C)", AutoSize = true, AccessibleName = "Clear custom name", TabIndex = 1 };
var okButton = new Button { Text = "OK", AutoSize = true, DialogResult = DialogResult.OK, TabIndex = 2 };
var cancelButton = new Button { Text = "Cancel", AutoSize = true, DialogResult = DialogResult.Cancel, TabIndex = 3 };
clearButton.Click += (_, _) =>
{
FriendlyName = null;
DialogResult = DialogResult.OK;
Close();
};
okButton.Click += (_, _) =>
{
var t = nameBox.Text.Trim();
FriendlyName = t.Length == 0 ? null : t; // blank box = clear, same as the Clear button
};
var buttonRow = new FlowLayoutPanel { Dock = DockStyle.Fill, FlowDirection = FlowDirection.LeftToRight, AutoSize = true, TabIndex = 2 };
buttonRow.Controls.Add(clearButton);
buttonRow.Controls.Add(okButton);
buttonRow.Controls.Add(cancelButton);
grid.Controls.Add(buttonRow, 1, 2);
Controls.Add(grid);
AcceptButton = okButton;
CancelButton = cancelButton;
Shown += (_, _) => { nameBox.Focus(); nameBox.SelectAll(); };
}
}
+5
View File
@@ -75,6 +75,11 @@ public sealed class AppConfig
/// <see cref="Profile.PeerShaping"/>.</summary> /// <see cref="Profile.PeerShaping"/>.</summary>
public bool ShowPanEqTab { get; set; } = true; public bool ShowPanEqTab { get; set; } = true;
/// <summary>Custom friendly names the user has given to peers, keyed by the peer's stable identity —
/// its machine name when known, otherwise its address. Machine-wide (a name applies in every profile)
/// and shown wherever that peer appears. Empty by default. See the Connectivity tab's Rename peer.</summary>
public Dictionary<string, string> PeerFriendlyNames { get; set; } = new();
/// <summary>If true (the default), RemSound plays the startup cue once, right after this /// <summary>If true (the default), RemSound plays the startup cue once, right after this
/// copy wins the single-instance takeover and before the profile loads. Machine-wide (not /// copy wins the single-instance takeover and before the profile loads. Machine-wide (not
/// per-<see cref="Profile"/>) because it fires before any profile — and its per-profile /// per-<see cref="Profile"/>) because it fires before any profile — and its per-profile
+26
View File
@@ -864,6 +864,32 @@ public sealed class AudioReceiver : IDisposable
return null; return null;
} }
/// <summary>
/// The format of every currently-active (non-idle) receive session from the given peer IP — one
/// entry per stream, i.e. per capture device the peer is sending. Each carries its sample rate,
/// codec and lane (WASAPI vs ASIO), so the UI can say "sending N devices on ASIO at 48 kHz, Opus"
/// with no protocol change. Empty when nothing is arriving from that peer.
/// </summary>
public IReadOnlyList<AudioFormatInfo> ActiveFormatsFromAddress(IPAddress address)
{
var now = DateTime.UtcNow;
var fresh = new List<SessionPlayout>();
foreach (var sp in playoutEngine.ActiveSessions)
{
if (!sp.Endpoint.Address.Equals(address)) continue;
if (now - sp.LastWriteUtc > SessionIdleTimeout) continue;
fresh.Add(sp);
}
var formats = new List<AudioFormatInfo>();
lock (sessionsLock)
{
foreach (var sp in fresh)
if (sessions.TryGetValue((sp.Endpoint, sp.StreamId), out var session) && session.Format is not null)
formats.Add(session.Format);
}
return formats;
}
/// <summary> /// <summary>
/// The wire codec of the freshest currently-active receive session across all peers, or /// The wire codec of the freshest currently-active receive session across all peers, or
/// null when nothing is being received. Surfaced in the SNAP log's Codec column so a /// null when nothing is being received. Surfaced in the SNAP log's Codec column so a