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×), 200 is double.",
Location = new Point(12, y + 20),
Size = new Size(260, 45),
Minimum = 0,
Maximum = 200,
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(200, 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);
}
}
}