Input mode (VAD/PTT/Always-On), VAD threshold, and the new mic gain were
applied to the core + UI but never saved, so every relaunch reset to VAD
defaults. Each client now persists them and re-applies on connect:
- iOS: UserDefaults (SessionState.loadAndApplyVoiceSettings + setter writes)
- macOS: UserDefaults via MainWindowController didSet + loadPersistedAudioSettings
(settings window also restores the VAD slider from the stored threshold)
- Windows: new Models/VoiceSettings.cs (JSON at %AppData%\VoiceCat\voice.json,
mirrors FeedbackSettings) loaded/applied in MainForm
Add global send-side mic gain API vc_set_input_gain (applied to MIC PCM in
on_capture_frame before the VAD gate, clamped to int16) + Swift/C# bindings,
and a 0-300% (default 100%) mic-volume slider on all three clients.
Fix iOS chat: ChatView called sendText(scope:.channel) with no targetId (0),
so channel messages went nowhere; now passes session.currentChannelId.
Fix iOS per-user tuning for VoiceOver: the tuning sheet was long-press
.contextMenu only (invisible to VoiceOver); UserRow now also exposes the same
buttons via .accessibilityActions (no visual change).
Verified: core builds clean; ctest 24/27 (3 pre-existing teardown crashes,
reproduced with changes stashed); VoiceCatMac + VoiceCatiOS (arm64 sim) build
SUCCEEDED; VoiceCat.Interop dotnet build succeeded. Windows App not built
(WinForms can't build on macOS) — follows existing patterns.
415 lines
18 KiB
C#
415 lines
18 KiB
C#
namespace VoiceCat.App.Forms;
|
||
|
||
partial class MainForm
|
||
{
|
||
private System.ComponentModel.IContainer components = null!;
|
||
|
||
// Menu bar + toolbar
|
||
private MenuStrip menuStrip = null!;
|
||
private ToolStrip toolStrip = null!;
|
||
private ToolStripButton tsbJoinVoice = null!;
|
||
private ToolStripButton tsbScreenShare = null!;
|
||
|
||
// Status bar
|
||
private Label lblStatus = null!;
|
||
|
||
// Main left/right split
|
||
private SplitContainer splitMain = null!;
|
||
|
||
// Left panel: channel tree on top, user list on bottom
|
||
private SplitContainer splitLeft = null!;
|
||
private Label lblChannels = null!;
|
||
private TreeView tvChannels = null!;
|
||
private Label lblUsers = null!;
|
||
private ListBox lstUsers = null!;
|
||
|
||
// Right panel: unified log, compose row, output volume
|
||
private TableLayoutPanel tblRight = null!;
|
||
private Label lblLog = null!;
|
||
private RichTextBox rtbLog = null!;
|
||
private TableLayoutPanel tblCompose = null!;
|
||
private TextBox txtCompose = null!;
|
||
private Button btnSend = null!;
|
||
private Label lblOutputVolume = null!;
|
||
private TrackBar trkOutputVolume = null!;
|
||
|
||
// Voice control panel (docked Bottom)
|
||
private Panel pnlVoice = null!;
|
||
private FlowLayoutPanel flpVoiceTop = null!;
|
||
private FlowLayoutPanel flpVoiceBottom = null!;
|
||
private CheckBox chkMute = null!;
|
||
private CheckBox chkDeafen = null!;
|
||
private RadioButton radioVad = null!;
|
||
private RadioButton radioPtt = null!;
|
||
private RadioButton radioAlwaysOn = null!;
|
||
private Label lblPttKey = null!;
|
||
private Button btnChangePtt = null!;
|
||
private Label lblInputDevice = null!;
|
||
private ComboBox cboInputDevice = null!;
|
||
private Button btnRefreshDevices = null!;
|
||
private Label lblLevel = null!;
|
||
private ProgressBar pbLevel = null!;
|
||
private Label lblVadThreshold = null!;
|
||
private TrackBar trkVadThreshold = null!;
|
||
private Label lblMicGain = null!;
|
||
private TrackBar trkMicGain = null!;
|
||
|
||
protected override void Dispose(bool disposing)
|
||
{
|
||
if (disposing) components?.Dispose();
|
||
base.Dispose(disposing);
|
||
}
|
||
|
||
private void InitializeComponent()
|
||
{
|
||
components = new System.ComponentModel.Container();
|
||
|
||
lblStatus = new Label();
|
||
splitMain = new SplitContainer();
|
||
splitLeft = new SplitContainer();
|
||
lblChannels = new Label();
|
||
tvChannels = new TreeView();
|
||
lblUsers = new Label();
|
||
lstUsers = new ListBox();
|
||
tblRight = new TableLayoutPanel();
|
||
lblLog = new Label();
|
||
rtbLog = new RichTextBox();
|
||
tblCompose = new TableLayoutPanel();
|
||
txtCompose = new TextBox();
|
||
btnSend = new Button();
|
||
lblOutputVolume = new Label();
|
||
trkOutputVolume = new TrackBar();
|
||
pnlVoice = new Panel();
|
||
flpVoiceTop = new FlowLayoutPanel();
|
||
flpVoiceBottom = new FlowLayoutPanel();
|
||
chkMute = new CheckBox();
|
||
chkDeafen = new CheckBox();
|
||
radioVad = new RadioButton();
|
||
radioPtt = new RadioButton();
|
||
radioAlwaysOn = new RadioButton();
|
||
lblPttKey = new Label();
|
||
btnChangePtt = new Button();
|
||
lblInputDevice = new Label();
|
||
cboInputDevice = new ComboBox();
|
||
btnRefreshDevices = new Button();
|
||
lblLevel = new Label();
|
||
pbLevel = new ProgressBar();
|
||
lblVadThreshold = new Label();
|
||
trkVadThreshold = new TrackBar();
|
||
lblMicGain = new Label();
|
||
trkMicGain = new TrackBar();
|
||
menuStrip = new MenuStrip();
|
||
toolStrip = new ToolStrip();
|
||
tsbJoinVoice = new ToolStripButton();
|
||
tsbScreenShare = new ToolStripButton();
|
||
|
||
// ── Status label ──────────────────────────────────────────────────────
|
||
lblStatus.AccessibleName = "Connection status";
|
||
lblStatus.Dock = DockStyle.Top;
|
||
lblStatus.AutoSize = true;
|
||
lblStatus.Padding = new Padding(6, 4, 6, 4);
|
||
lblStatus.TabIndex = 0;
|
||
lblStatus.Text = "Connecting...";
|
||
|
||
// ── Channel tree ──────────────────────────────────────────────────────
|
||
lblChannels.Text = "Channels:";
|
||
lblChannels.Dock = DockStyle.Top;
|
||
lblChannels.AutoSize = true;
|
||
lblChannels.Padding = new Padding(4, 4, 4, 2);
|
||
|
||
tvChannels.AccessibleName = "Channel list";
|
||
tvChannels.AccessibleDescription =
|
||
"Double-click or press Enter to join a channel. " +
|
||
"Channels marked [password] require a password.";
|
||
tvChannels.Dock = DockStyle.Fill;
|
||
tvChannels.HideSelection = false;
|
||
tvChannels.TabIndex = 0;
|
||
|
||
// ── User list ─────────────────────────────────────────────────────────
|
||
lblUsers.Text = "Users in channel:";
|
||
lblUsers.Dock = DockStyle.Top;
|
||
lblUsers.AutoSize = true;
|
||
lblUsers.Padding = new Padding(4, 4, 4, 2);
|
||
|
||
lstUsers.AccessibleName = "Users in current channel";
|
||
lstUsers.AccessibleDescription =
|
||
"People in the same channel. Double-click or press Enter for per-user volume settings. " +
|
||
"Talking users are marked (talking).";
|
||
lstUsers.Dock = DockStyle.Fill;
|
||
lstUsers.TabIndex = 1;
|
||
|
||
// ── Left split (channels top, users bottom) ───────────────────────────
|
||
splitLeft.Orientation = Orientation.Horizontal;
|
||
splitLeft.Dock = DockStyle.Fill;
|
||
splitLeft.Panel1MinSize = 100;
|
||
splitLeft.Panel2MinSize = 80;
|
||
splitLeft.TabIndex = 0;
|
||
// Keep the resize splitter out of the Tab cycle so focus moves control-to-control.
|
||
splitLeft.TabStop = false;
|
||
// Name the otherwise-anonymous "pane" containers so screen readers announce
|
||
// orientation instead of a stack of unnamed panes.
|
||
splitLeft.AccessibleName = "Channels and users";
|
||
splitLeft.Panel1.AccessibleName = "Channels";
|
||
splitLeft.Panel2.AccessibleName = "Users";
|
||
splitLeft.Panel1.Controls.Add(tvChannels);
|
||
splitLeft.Panel1.Controls.Add(lblChannels);
|
||
splitLeft.Panel2.Controls.Add(lstUsers);
|
||
splitLeft.Panel2.Controls.Add(lblUsers);
|
||
|
||
// ── Unified log ───────────────────────────────────────────────────────
|
||
lblLog.Text = "Chat & Activity:";
|
||
lblLog.AutoSize = true;
|
||
lblLog.Padding = new Padding(2, 2, 2, 1);
|
||
|
||
rtbLog.AccessibleName = "Chat and activity log";
|
||
rtbLog.AccessibleDescription = "Combined history of chat messages (normal) and activity events (gray).";
|
||
rtbLog.Dock = DockStyle.Fill;
|
||
rtbLog.ReadOnly = true;
|
||
rtbLog.ScrollBars = RichTextBoxScrollBars.Vertical;
|
||
rtbLog.BackColor = SystemColors.Window;
|
||
rtbLog.TabIndex = 0;
|
||
|
||
// ── Compose row ───────────────────────────────────────────────────────
|
||
txtCompose.AccessibleName = "Message text";
|
||
txtCompose.AccessibleDescription = "Type your channel message. Press Enter or click Send to send.";
|
||
txtCompose.Dock = DockStyle.Fill;
|
||
txtCompose.TabIndex = 1;
|
||
|
||
btnSend.Text = "&Send";
|
||
btnSend.Dock = DockStyle.Fill;
|
||
btnSend.TabIndex = 2;
|
||
|
||
tblCompose.ColumnCount = 2;
|
||
tblCompose.RowCount = 1;
|
||
tblCompose.Dock = DockStyle.Fill;
|
||
tblCompose.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||
tblCompose.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 68F));
|
||
tblCompose.RowStyles.Add(new RowStyle(SizeType.Percent, 100F));
|
||
tblCompose.Padding = new Padding(0, 3, 0, 0);
|
||
tblCompose.Controls.Add(txtCompose, 0, 0);
|
||
tblCompose.Controls.Add(btnSend, 1, 0);
|
||
|
||
// ── Output volume row ─────────────────────────────────────────────────
|
||
lblOutputVolume.Text = "Output volume:";
|
||
lblOutputVolume.AutoSize = true;
|
||
lblOutputVolume.Padding = new Padding(2, 6, 4, 0);
|
||
|
||
trkOutputVolume.AccessibleName = "Output volume";
|
||
trkOutputVolume.AccessibleDescription = "Global playback volume for all incoming audio.";
|
||
trkOutputVolume.Minimum = 0;
|
||
trkOutputVolume.Maximum = 100;
|
||
trkOutputVolume.Value = 80;
|
||
trkOutputVolume.TickFrequency = 10;
|
||
trkOutputVolume.SmallChange = 1;
|
||
trkOutputVolume.LargeChange = 10;
|
||
trkOutputVolume.Dock = DockStyle.Fill;
|
||
trkOutputVolume.TabIndex = 3;
|
||
|
||
// ── Right table layout ────────────────────────────────────────────────
|
||
tblRight.ColumnCount = 1;
|
||
tblRight.RowCount = 5;
|
||
tblRight.Dock = DockStyle.Fill;
|
||
tblRight.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F));
|
||
tblRight.RowStyles.Add(new RowStyle(SizeType.AutoSize)); // row 0: lblLog
|
||
tblRight.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); // row 1: rtbLog
|
||
tblRight.RowStyles.Add(new RowStyle(SizeType.Absolute, 34F)); // row 2: compose
|
||
tblRight.RowStyles.Add(new RowStyle(SizeType.AutoSize)); // row 3: vol label
|
||
tblRight.RowStyles.Add(new RowStyle(SizeType.Absolute, 45F)); // row 4: vol slider
|
||
tblRight.Controls.Add(lblLog, 0, 0);
|
||
tblRight.Controls.Add(rtbLog, 0, 1);
|
||
tblRight.Controls.Add(tblCompose, 0, 2);
|
||
tblRight.Controls.Add(lblOutputVolume, 0, 3);
|
||
tblRight.Controls.Add(trkOutputVolume, 0, 4);
|
||
|
||
// ── Main split ────────────────────────────────────────────────────────
|
||
splitMain.Dock = DockStyle.Fill;
|
||
splitMain.Panel1MinSize = 150;
|
||
splitMain.TabIndex = 1;
|
||
splitMain.TabStop = false;
|
||
splitMain.AccessibleName = "Main";
|
||
splitMain.Panel1.AccessibleName = "Channels and users";
|
||
splitMain.Panel2.AccessibleName = "Chat and activity";
|
||
splitMain.Panel1.Controls.Add(splitLeft);
|
||
splitMain.Panel2.Controls.Add(tblRight);
|
||
|
||
// ── Voice control panel ───────────────────────────────────────────────
|
||
chkMute.Text = "&Mute mic";
|
||
chkMute.AutoSize = true;
|
||
chkMute.Enabled = false;
|
||
chkMute.Margin = new Padding(0, 4, 6, 0);
|
||
chkMute.TabIndex = 1;
|
||
|
||
chkDeafen.Text = "&Deafen";
|
||
chkDeafen.AutoSize = true;
|
||
chkDeafen.Enabled = false;
|
||
chkDeafen.Margin = new Padding(0, 4, 12, 0);
|
||
chkDeafen.TabIndex = 2;
|
||
|
||
var lblMode = new Label { Text = "Mode:", AutoSize = true, Margin = new Padding(0, 5, 4, 0) };
|
||
|
||
radioVad.Text = "&Voice activation";
|
||
radioVad.AutoSize = true;
|
||
radioVad.Checked = true;
|
||
radioVad.Enabled = false;
|
||
radioVad.Margin = new Padding(0, 4, 6, 0);
|
||
radioVad.TabIndex = 3;
|
||
|
||
radioPtt.Text = "&Push to talk";
|
||
radioPtt.AutoSize = true;
|
||
radioPtt.Enabled = false;
|
||
radioPtt.Margin = new Padding(0, 4, 4, 0);
|
||
radioPtt.TabIndex = 4;
|
||
|
||
radioAlwaysOn.Text = "A&lways on";
|
||
radioAlwaysOn.AutoSize = true;
|
||
radioAlwaysOn.Enabled = false;
|
||
radioAlwaysOn.Margin = new Padding(0, 4, 12, 0);
|
||
radioAlwaysOn.TabIndex = 5;
|
||
|
||
lblPttKey.Text = "(F8)";
|
||
lblPttKey.AutoSize = true;
|
||
lblPttKey.Margin = new Padding(2, 5, 4, 0);
|
||
lblPttKey.Visible = false;
|
||
|
||
btnChangePtt.Text = "Change key...";
|
||
btnChangePtt.AutoSize = true;
|
||
btnChangePtt.Margin = new Padding(0, 2, 0, 0);
|
||
btnChangePtt.Visible = false;
|
||
btnChangePtt.TabIndex = 6;
|
||
|
||
flpVoiceTop.Dock = DockStyle.Top;
|
||
flpVoiceTop.Height = 34;
|
||
flpVoiceTop.AutoSize = false;
|
||
flpVoiceTop.Padding = new Padding(4, 2, 4, 0);
|
||
flpVoiceTop.Controls.Add(chkMute);
|
||
flpVoiceTop.Controls.Add(chkDeafen);
|
||
flpVoiceTop.Controls.Add(lblMode);
|
||
flpVoiceTop.Controls.Add(radioVad);
|
||
flpVoiceTop.Controls.Add(radioPtt);
|
||
flpVoiceTop.Controls.Add(radioAlwaysOn);
|
||
flpVoiceTop.Controls.Add(lblPttKey);
|
||
flpVoiceTop.Controls.Add(btnChangePtt);
|
||
|
||
// Bottom row: device picker + level meter
|
||
lblInputDevice.Text = "Input:";
|
||
lblInputDevice.AutoSize = true;
|
||
lblInputDevice.Margin = new Padding(0, 5, 4, 0);
|
||
|
||
cboInputDevice.AccessibleName = "Input device";
|
||
cboInputDevice.AccessibleDescription = "Select which microphone or audio device to use.";
|
||
cboInputDevice.DropDownStyle = ComboBoxStyle.DropDownList;
|
||
cboInputDevice.Width = 200;
|
||
cboInputDevice.Margin = new Padding(0, 2, 4, 0);
|
||
cboInputDevice.TabIndex = 7;
|
||
|
||
btnRefreshDevices.Text = "Re&fresh";
|
||
btnRefreshDevices.AutoSize = true;
|
||
btnRefreshDevices.Margin = new Padding(0, 2, 12, 0);
|
||
btnRefreshDevices.TabIndex = 8;
|
||
|
||
lblLevel.Text = "Level:";
|
||
lblLevel.AutoSize = true;
|
||
lblLevel.Margin = new Padding(0, 5, 4, 0);
|
||
|
||
pbLevel.AccessibleName = "Microphone level";
|
||
pbLevel.AccessibleDescription = "Current input level from the microphone.";
|
||
pbLevel.Width = 120;
|
||
pbLevel.Height = 16;
|
||
pbLevel.Maximum = 100;
|
||
pbLevel.Margin = new Padding(0, 6, 0, 0);
|
||
pbLevel.Style = ProgressBarStyle.Continuous;
|
||
pbLevel.TabStop = false;
|
||
|
||
lblVadThreshold.Text = "Sensitivity:";
|
||
lblVadThreshold.AutoSize = true;
|
||
lblVadThreshold.Margin = new Padding(12, 5, 4, 0);
|
||
lblVadThreshold.Visible = true;
|
||
|
||
trkVadThreshold.AccessibleName = "VAD sensitivity";
|
||
trkVadThreshold.AccessibleDescription =
|
||
"Voice detection sensitivity. Higher = more sensitive (triggers on quieter sounds). " +
|
||
"Range 1–100; default 25.";
|
||
trkVadThreshold.Minimum = 1;
|
||
trkVadThreshold.Maximum = 100;
|
||
trkVadThreshold.Value = 76;
|
||
trkVadThreshold.TickFrequency = 10;
|
||
trkVadThreshold.SmallChange = 1;
|
||
trkVadThreshold.LargeChange = 10;
|
||
trkVadThreshold.Width = 120;
|
||
trkVadThreshold.Margin = new Padding(0, 2, 0, 0);
|
||
trkVadThreshold.TabIndex = 9;
|
||
trkVadThreshold.Visible = true;
|
||
|
||
lblMicGain.Text = "Mic volume:";
|
||
lblMicGain.AutoSize = true;
|
||
lblMicGain.Margin = new Padding(12, 5, 4, 0);
|
||
|
||
trkMicGain.AccessibleName = "Microphone volume";
|
||
trkMicGain.AccessibleDescription =
|
||
"Boost a quiet microphone. 100 is unity; range 0–300 percent.";
|
||
trkMicGain.Minimum = 0;
|
||
trkMicGain.Maximum = 300;
|
||
trkMicGain.Value = 100;
|
||
trkMicGain.TickFrequency = 25;
|
||
trkMicGain.SmallChange = 5;
|
||
trkMicGain.LargeChange = 25;
|
||
trkMicGain.Width = 120;
|
||
trkMicGain.Margin = new Padding(0, 2, 0, 0);
|
||
trkMicGain.TabIndex = 10;
|
||
|
||
flpVoiceBottom.Dock = DockStyle.Fill;
|
||
flpVoiceBottom.Padding = new Padding(4, 0, 4, 2);
|
||
flpVoiceBottom.Controls.Add(lblInputDevice);
|
||
flpVoiceBottom.Controls.Add(cboInputDevice);
|
||
flpVoiceBottom.Controls.Add(btnRefreshDevices);
|
||
flpVoiceBottom.Controls.Add(lblLevel);
|
||
flpVoiceBottom.Controls.Add(pbLevel);
|
||
flpVoiceBottom.Controls.Add(lblVadThreshold);
|
||
flpVoiceBottom.Controls.Add(trkVadThreshold);
|
||
flpVoiceBottom.Controls.Add(lblMicGain);
|
||
flpVoiceBottom.Controls.Add(trkMicGain);
|
||
|
||
pnlVoice.Dock = DockStyle.Bottom;
|
||
pnlVoice.Height = 68;
|
||
pnlVoice.BorderStyle = BorderStyle.FixedSingle;
|
||
pnlVoice.Padding = new Padding(0);
|
||
pnlVoice.Controls.Add(flpVoiceBottom); // Fill — added first
|
||
pnlVoice.Controls.Add(flpVoiceTop); // Top — added last
|
||
|
||
// ── Toolbar ───────────────────────────────────────────────────────────
|
||
tsbJoinVoice.Text = "Join Voice";
|
||
tsbJoinVoice.DisplayStyle = ToolStripItemDisplayStyle.Text;
|
||
tsbJoinVoice.CheckOnClick = false;
|
||
|
||
tsbScreenShare.Text = "Share Screen Audio";
|
||
tsbScreenShare.DisplayStyle = ToolStripItemDisplayStyle.Text;
|
||
tsbScreenShare.CheckOnClick = false;
|
||
|
||
toolStrip.Dock = DockStyle.Top;
|
||
toolStrip.Items.Add(tsbJoinVoice);
|
||
toolStrip.Items.Add(new ToolStripSeparator());
|
||
toolStrip.Items.Add(tsbScreenShare);
|
||
toolStrip.TabIndex = 1;
|
||
toolStrip.AccessibleName = "Toolbar";
|
||
|
||
// ── Menu strip ────────────────────────────────────────────────────────
|
||
menuStrip.AccessibleName = "Main menu";
|
||
menuStrip.Dock = DockStyle.Top;
|
||
menuStrip.TabIndex = 0;
|
||
MainMenuStrip = menuStrip;
|
||
|
||
// ── Form ──────────────────────────────────────────────────────────────
|
||
AutoScaleMode = AutoScaleMode.Font;
|
||
ClientSize = new Size(960, 680);
|
||
MinimumSize = new Size(700, 540);
|
||
KeyPreview = true;
|
||
Controls.Add(splitMain); // DockStyle.Fill
|
||
Controls.Add(pnlVoice); // DockStyle.Bottom
|
||
Controls.Add(lblStatus); // DockStyle.Top
|
||
Controls.Add(toolStrip); // DockStyle.Top (above status)
|
||
Controls.Add(menuStrip); // DockStyle.Top (topmost)
|
||
StartPosition = FormStartPosition.CenterScreen;
|
||
Text = "VoiceCat";
|
||
}
|
||
}
|