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