Files
voice-cat/clients/windows/VoiceCat.App/Forms/PerUserTuningDialog.cs
Talon d397731db9 feat(audio): per-stream mix controls in Windows client + vc_get_remote_stream getter
PerUserTuningDialog previously broadcast one gain/mute/NR set to *all* of a
user's streams, even though the core mixer (AudioEngine::RemoteStream) and
the C ABI (vc_set_remote_stream) were already per-stream. The UI had no
per-mix controls anywhere.

Reworks the dialog to enumerate ListUserStreams on open and render one row
per stream (kind + label + Gain + Mute + NR), each wiring only to its own
stream_id. Adds a read-back ABI counterpart, vc_get_remote_stream, so the
dialog opens at the listener's actual current per-stream settings (defaults
1.0/unmuted/NR-off) rather than always 100%. Additive ABI change only; no
existing symbols touched.

Tests: test_m3_multistream extended with getter round-trip assertions; new
C# smoke test exercises the full P/Invoke marshaling path with two clients.
Docs: voice.md §10 notes the getter. NR checkbox keeps its honest
'passthrough' label (NS DSP still unbuilt per §8).
2026-06-18 02:06:44 +02:00

216 lines
6.7 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using VoiceCat.Interop;
namespace VoiceCat.App.Forms;
/// <summary>
/// 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.
/// </summary>
public sealed class PerUserTuningDialog : Form
{
private readonly VoiceCatClient _client;
private readonly uint _userId;
private readonly List<StreamRow> _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 (planned — currently passthrough)",
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);
}
}
}