Files
voice-cat/clients/windows/VoiceCat.App/Forms/PerUserTuningDialog.cs
Talon 2e0e0caccb feat(clients): wire RNNoise mic noise reduction into Windows, macOS, and iOS
Expose the existing send-side vc_set_input_noise_reduction C ABI (MIC-only,
mono, LOCAL — denoises captured mic PCM before input gain and VAD/PTT gate)
as a persisted global toggle in each client's audio settings, applied live
and re-applied on Join Voice. Mirrors the existing mic-gain wiring pattern.

- Shared Swift (VoiceCatCore): add setInputNoiseReduction(_:) wrapper
- Windows: P/Invoke + SetInputNoiseReduction wrapper, MicNoiseReduction in
  VoiceSettings, new checkbox in AudioSettingsForm (layout shifted +28px),
  apply on Join Voice; also fix stale 'planned - currently passthrough'
  label on the receive-side per-user NR checkbox (RNNoise now backs it)
- macOS: inputNoiseReduction state + UserDefaults in MainWindowController,
  NR checkbox + nrChanged action in SettingsWindowController
- iOS: inputNoiseReduction in VoiceState + setter + restore in SessionState,
  NR Toggle in SettingsView Voice section

Aux/screen are out of scope by design (core's NR guards kind == MIC). Apple
builds require a rebuilt VoiceCatCore.xcframework with VOICECAT_HAS_NS.
2026-06-23 14:11:18 +02:00

216 lines
6.7 KiB
C#
Raw 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",
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);
}
}
}