using VoiceCat.Interop; namespace VoiceCat.App.Forms; /// /// Per-remote-user, per-stream gain/mute/noise-reduction settings (docs/voice.md §1, §10). /// One row is rendered for each stream the remote user is currently publishing. Each row /// controls only that stream — so a listener can turn down one user's desktop audio while /// keeping their mic, and independently noise-reduce a third user. All of these are /// listener-local: no protocol traffic, no effect on other listeners. /// /// Changes apply in real time as the user adjusts controls (no OK/Cancel round-trip); the /// Close button just dismisses. State lives in the core and is not persisted across sessions. /// The dialog is a modal snapshot of the streams active when it was opened — close and /// reopen to see streams started/stopped in the meantime. /// public sealed class PerUserTuningDialog : Form { private readonly VoiceCatClient _client; private readonly uint _userId; private readonly List _rows = new(); public PerUserTuningDialog(VoiceCatClient client, uint userId, string nickname) { _client = client; _userId = userId; AutoScaleMode = AutoScaleMode.Font; FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; MinimizeBox = false; StartPosition = FormStartPosition.CenterParent; Text = $"User settings — {nickname}"; BuildControls(nickname); } private void BuildControls(string nickname) { var lblTitle = new Label { Text = $"Settings for {nickname}", AutoSize = true, Font = new Font(Font, FontStyle.Bold), Location = new Point(12, 12), TabIndex = 0, }; var streams = _client.ListUserStreams(_userId); const int rowH = 70; // label + trackbar + mute/nr const int rowGap = 8; int y = 44; if (streams.Count == 0) { var lblEmpty = new Label { Text = "No active streams.", AutoSize = true, Location = new Point(12, y), }; Controls.Add(lblTitle); Controls.Add(lblEmpty); ClientSize = new Size(420, 44 + lblEmpty.PreferredHeight + 16); return; } Controls.Add(lblTitle); int tabIndex = 1; foreach (var s in streams) { var row = CreateRow(s, y, ref tabIndex); _rows.Add(row); Controls.AddRange(row.Controls); y += rowH + rowGap; } var btnClose = new Button { Text = "&Close", DialogResult = DialogResult.OK, Location = new Point(336, y + 4), Size = new Size(75, 27), TabIndex = tabIndex, }; AcceptButton = btnClose; Controls.Add(btnClose); ClientSize = new Size(420, y + 40); } private StreamRow CreateRow(StreamSummary s, int y, ref int tabIndex) { // Read back the listener's current state for this stream (defaults if never set). float gain0 = 1.0f; bool mute0 = false; bool nr0 = false; var (r, state) = _client.GetRemoteStream(_userId, s.StreamId); if (r == VcResult.Ok && state is not null) { gain0 = state.Gain; mute0 = state.Muted; nr0 = state.NoiseReduction; } var lblName = new Label { Text = StreamLabel(s), AutoSize = true, Font = new Font(Font, FontStyle.Bold), Location = new Point(12, y), TabIndex = tabIndex++, }; var trkGain = new TrackBar { AccessibleName = $"Gain for {s.Label}", AccessibleDescription = "Volume level for this stream. 100 is normal (1.0×); range 0–400 percent.", Location = new Point(12, y + 20), Size = new Size(260, 45), Minimum = 0, Maximum = 400, Value = ClampToTrack(gain0), TickFrequency = 25, SmallChange = 5, LargeChange = 25, TabIndex = tabIndex++, }; var lblGainValue = new Label { AutoSize = true, Location = new Point(280, y + 28), TabIndex = tabIndex++, }; void UpdateGainLabel() => lblGainValue.Text = $"{trkGain.Value}% ({trkGain.Value / 100f:F1}×)"; UpdateGainLabel(); var chkMute = new CheckBox { Text = "&Mute", AutoSize = true, Location = new Point(12, y + 48), Checked = mute0, TabIndex = tabIndex++, }; var chkNr = new CheckBox { Text = "&Noise reduction", AutoSize = true, Location = new Point(96, y + 48), Checked = nr0, TabIndex = tabIndex++, }; var row = new StreamRow(s.StreamId, trkGain, chkMute, chkNr, new Control[] { lblName, trkGain, lblGainValue, chkMute, chkNr }); trkGain.Scroll += (_, _) => { UpdateGainLabel(); row.Apply(_client, _userId); }; chkMute.CheckedChanged += (_, _) => row.Apply(_client, _userId); chkNr.CheckedChanged += (_, _) => row.Apply(_client, _userId); return row; } private static string StreamLabel(StreamSummary s) { var kind = s.Kind switch { VcStreamKind.Mic => "Mic", VcStreamKind.ScreenAudio => "Screen audio", VcStreamKind.AuxDevice => "Aux device", _ => "Stream", }; return string.IsNullOrWhiteSpace(s.Label) ? kind : $"{kind} — {s.Label}"; } private static int ClampToTrack(float gain) { int v = (int)Math.Round(gain * 100f); return Math.Max(0, Math.Min(400, v)); } private sealed class StreamRow { private readonly uint _streamId; private readonly TrackBar _trkGain; private readonly CheckBox _chkMute; private readonly CheckBox _chkNr; public Control[] Controls { get; } public StreamRow(uint streamId, TrackBar trkGain, CheckBox chkMute, CheckBox chkNr, Control[] controls) { _streamId = streamId; _trkGain = trkGain; _chkMute = chkMute; _chkNr = chkNr; Controls = controls; } public void Apply(VoiceCatClient client, uint userId) { float gain = _trkGain.Value / 100f; client.SetRemoteStream(userId, _streamId, gain, _chkMute.Checked, _chkNr.Checked); } } }