Files
voice-cat/clients/windows/VoiceCat.App/Forms/ChannelEditDialog.cs
T
Talon 7b0c003ad4
Build and test / test (macos-latest) (push) Canceled after 0s
Build and test / test (ubuntu-24.04) (push) Canceled after 0s
Build and test / test (windows-latest) (push) Canceled after 0s
Build and test / apple-client (push) Canceled after 0s
Add adaptive packet loss handling
2026-09-22 16:54:49 +02:00

436 lines
14 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.Windows;
namespace VoiceCat.App.Forms;
/// <summary>
/// Create or edit a channel, including its full per-channel Opus audio config.
/// Returns DialogResult.OK with Result set.
/// </summary>
public sealed class ChannelEditDialog : Form
{
private readonly bool _isCreate;
private readonly uint _editingId;
private readonly List<ChannelInfo> _channels;
private TextBox _txtName = null!;
private TextBox _txtTopic = null!;
private ComboBox _cboParent = null!;
private NumericUpDown _numMaxUsers = null!;
private NumericUpDown _numSortOrder = null!;
private CheckBox _chkPassword = null!;
private TextBox _txtPassword = null!;
private ComboBox _cboMode = null!;
private NumericUpDown _numSampleRate = null!;
private NumericUpDown _numBitrate = null!;
private NumericUpDown _numFrameMs = null!;
private ComboBox _cboApplication = null!;
private CheckBox _chkFec = null!;
private CheckBox _chkAutomaticLoss = null!;
private ComboBox _cboLossSpeed = null!;
private NumericUpDown _numExpectedLoss = null!;
private CheckBox _chkDtx = null!;
private CheckBox _chkDred = null!;
private NumericUpDown _numComplexity = null!;
public ChannelEditInfo? Result { get; private set; }
private readonly bool _supportsAdaptivePacketLoss;
public ChannelEditDialog(IEnumerable<ChannelInfo> channels, ChannelEditInfo? existing = null, bool supportsAdaptivePacketLoss = true)
{
_isCreate = existing is null;
_editingId = existing?.Id ?? 0;
_channels = channels.Where(c => c.Id != _editingId).ToList();
_supportsAdaptivePacketLoss = supportsAdaptivePacketLoss;
Text = _isCreate ? "Create channel" : "Edit channel";
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
StartPosition = FormStartPosition.CenterParent;
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(520, 520);
var tabs = new TabControl
{
Dock = DockStyle.Fill,
TabIndex = 0,
};
var pageGeneral = new TabPage("General");
BuildGeneralPage(pageGeneral, existing);
var pageAudio = new TabPage("Audio config");
BuildAudioPage(pageAudio, existing?.Audio);
tabs.TabPages.Add(pageGeneral);
tabs.TabPages.Add(pageAudio);
var btnOk = new Button
{
Text = "&OK",
DialogResult = DialogResult.OK,
Location = new Point(350, 486),
Size = new Size(75, 27),
TabIndex = 100,
};
var btnCancel = new Button
{
Text = "&Cancel",
DialogResult = DialogResult.Cancel,
Location = new Point(431, 486),
Size = new Size(75, 27),
TabIndex = 101,
};
AcceptButton = btnOk;
CancelButton = btnCancel;
Controls.Add(tabs);
Controls.Add(btnOk);
Controls.Add(btnCancel);
btnOk.Click += BtnOk_Click;
}
private void BuildGeneralPage(TabPage page, ChannelEditInfo? existing)
{
int y = 16;
int labelWidth = 110;
int inputX = 128;
AddLabel(page, "&Name:", 12, y, labelWidth);
_txtName = new TextBox
{
Location = new Point(inputX, y - 2),
Size = new Size(360, 23),
Text = existing?.Name ?? "",
TabIndex = 1,
};
page.Controls.Add(_txtName);
y += 36;
AddLabel(page, "&Topic:", 12, y, labelWidth);
_txtTopic = new TextBox
{
Location = new Point(inputX, y - 2),
Size = new Size(360, 23),
Text = existing?.Topic ?? "",
TabIndex = 2,
};
page.Controls.Add(_txtTopic);
y += 36;
AddLabel(page, "&Parent channel:", 12, y, labelWidth);
_cboParent = new ComboBox
{
Location = new Point(inputX, y - 2),
Size = new Size(360, 23),
DropDownStyle = ComboBoxStyle.DropDownList,
TabIndex = 3,
};
_cboParent.Items.Add(new ChannelItem("(root)", 0));
foreach (var ch in _channels.OrderBy(c => c.Name))
_cboParent.Items.Add(new ChannelItem(ch.Name, ch.Id));
SelectParent(existing?.ParentId ?? 0);
page.Controls.Add(_cboParent);
y += 36;
AddLabel(page, "&Max users:", 12, y, labelWidth);
_numMaxUsers = new NumericUpDown
{
Location = new Point(inputX, y - 2),
Size = new Size(120, 23),
Minimum = 0,
Maximum = 10000,
Value = existing?.MaxUsers ?? 0,
TabIndex = 4,
};
page.Controls.Add(_numMaxUsers);
AddLabel(page, "(0 = unlimited)", inputX + 128, y, 100);
y += 36;
AddLabel(page, "&Sort order:", 12, y, labelWidth);
_numSortOrder = new NumericUpDown
{
Location = new Point(inputX, y - 2),
Size = new Size(120, 23),
Minimum = int.MinValue,
Maximum = int.MaxValue,
Value = existing?.SortOrder ?? 0,
TabIndex = 5,
};
page.Controls.Add(_numSortOrder);
y += 36;
_chkPassword = new CheckBox
{
Text = "&Password protected",
Location = new Point(inputX, y),
AutoSize = true,
Checked = existing?.PasswordProtected ?? false,
TabIndex = 6,
};
page.Controls.Add(_chkPassword);
y += 28;
_txtPassword = new TextBox
{
Location = new Point(inputX, y),
Size = new Size(360, 23),
UseSystemPasswordChar = true,
Enabled = _chkPassword.Checked,
Text = existing?.Password ?? "",
TabIndex = 7,
};
page.Controls.Add(_txtPassword);
_chkPassword.CheckedChanged += (_, _) => _txtPassword.Enabled = _chkPassword.Checked;
}
private void BuildAudioPage(TabPage page, AudioConfigInfo? audio)
{
audio ??= new AudioConfigInfo(0, false, 48000, 0, 20, 0, true, 0, false, 10, false);
int y = 16;
int labelWidth = 150;
int inputX = 168;
AddLabel(page, "Codec (0 = Opus):", 12, y, labelWidth);
var numCodec = new NumericUpDown
{
Location = new Point(inputX, y - 2),
Size = new Size(120, 23),
Minimum = 0,
Maximum = 0,
Value = audio.Codec,
Enabled = false,
};
page.Controls.Add(numCodec);
y += 34;
AddLabel(page, "&Mode:", 12, y, labelWidth);
_cboMode = new ComboBox
{
Location = new Point(inputX, y - 2),
Size = new Size(160, 23),
DropDownStyle = ComboBoxStyle.DropDownList,
TabIndex = 10,
};
_cboMode.Items.AddRange(["Mono", "Stereo"]);
_cboMode.SelectedIndex = audio.Stereo ? 1 : 0;
page.Controls.Add(_cboMode);
y += 34;
AddLabel(page, "Sample &rate (Hz):", 12, y, labelWidth);
_numSampleRate = new NumericUpDown
{
Location = new Point(inputX, y - 2),
Size = new Size(120, 23),
Minimum = 8000,
Maximum = 96000,
Value = audio.SampleRate,
TabIndex = 11,
};
page.Controls.Add(_numSampleRate);
y += 34;
AddLabel(page, "&Bitrate (bps, 0 = default):", 12, y, labelWidth);
_numBitrate = new NumericUpDown
{
Location = new Point(inputX, y - 2),
Size = new Size(120, 23),
Minimum = 0,
Maximum = 512000,
Increment = 1000,
Value = audio.BitrateBps,
TabIndex = 12,
};
page.Controls.Add(_numBitrate);
y += 34;
AddLabel(page, "Frame &ms:", 12, y, labelWidth);
_numFrameMs = new NumericUpDown
{
Location = new Point(inputX, y - 2),
Size = new Size(120, 23),
Minimum = 5,
Maximum = 120,
Value = audio.FrameMs,
TabIndex = 13,
};
page.Controls.Add(_numFrameMs);
y += 34;
AddLabel(page, "&Application:", 12, y, labelWidth);
_cboApplication = new ComboBox
{
Location = new Point(inputX, y - 2),
Size = new Size(160, 23),
DropDownStyle = ComboBoxStyle.DropDownList,
TabIndex = 14,
};
_cboApplication.Items.AddRange(["VoIP", "Audio", "Low delay"]);
_cboApplication.SelectedIndex = (int)Math.Min(2, audio.Application);
page.Controls.Add(_cboApplication);
y += 34;
_chkAutomaticLoss = new CheckBox
{
Text = "&Automatic packet loss",
Location = new Point(inputX, y),
AutoSize = true,
Checked = audio.PacketLossMode != VcPacketLossMode.Manual,
Enabled = _supportsAdaptivePacketLoss,
TabIndex = 15,
};
page.Controls.Add(_chkAutomaticLoss);
y += 30;
AddLabel(page, "Adaptation &speed:", 12, y, labelWidth);
_cboLossSpeed = new ComboBox
{
Location = new Point(inputX, y - 2),
Size = new Size(160, 23),
DropDownStyle = ComboBoxStyle.DropDownList,
TabIndex = 16,
};
_cboLossSpeed.Items.AddRange(["Fast", "Balanced", "Stable"]);
_cboLossSpeed.SelectedIndex = audio.PacketLossMode == VcPacketLossMode.Manual ? 1 : (int)audio.PacketLossMode - 1;
page.Controls.Add(_cboLossSpeed);
y += 34;
AddLabel(page, "Expected packet loss (%):", 12, y, labelWidth);
_numExpectedLoss = new NumericUpDown
{
Location = new Point(inputX, y - 2),
Size = new Size(120, 23),
Minimum = 0,
Maximum = 100,
Value = audio.ExpectedPacketLoss,
TabIndex = 17,
};
page.Controls.Add(_numExpectedLoss);
void UpdateLossControls()
{
_numExpectedLoss.Enabled = !_chkAutomaticLoss.Checked;
_cboLossSpeed.Enabled = _supportsAdaptivePacketLoss && _chkAutomaticLoss.Checked;
}
_chkAutomaticLoss.CheckedChanged += (_, _) => UpdateLossControls();
UpdateLossControls();
y += 34;
AddLabel(page, "Com&plexity (0–10):", 12, y, labelWidth);
_numComplexity = new NumericUpDown
{
Location = new Point(inputX, y - 2),
Size = new Size(120, 23),
Minimum = 0,
Maximum = 10,
Value = audio.Complexity,
TabIndex = 18,
};
page.Controls.Add(_numComplexity);
y += 34;
_chkFec = new CheckBox
{
Text = "&FEC",
Location = new Point(inputX, y),
AutoSize = true,
Checked = audio.Fec,
TabIndex = 19,
};
page.Controls.Add(_chkFec);
y += 28;
_chkDtx = new CheckBox
{
Text = "&DTX",
Location = new Point(inputX, y),
AutoSize = true,
Checked = audio.Dtx,
TabIndex = 20,
};
page.Controls.Add(_chkDtx);
y += 28;
_chkDred = new CheckBox
{
Text = "D&RED (deep redundancy)",
Location = new Point(inputX, y),
AutoSize = true,
Checked = audio.Dred,
TabIndex = 21,
};
page.Controls.Add(_chkDred);
}
private static void AddLabel(Control parent, string text, int x, int y, int width)
{
parent.Controls.Add(new Label
{
Text = text,
Location = new Point(x, y),
Size = new Size(width, 17),
TextAlign = System.Drawing.ContentAlignment.MiddleLeft,
});
}
private void SelectParent(uint parentId)
{
for (int i = 0; i < _cboParent.Items.Count; i++)
{
if (_cboParent.Items[i] is ChannelItem item && item.Id == parentId)
{
_cboParent.SelectedIndex = i;
return;
}
}
_cboParent.SelectedIndex = 0;
}
private void BtnOk_Click(object? sender, EventArgs e)
{
string name = _txtName.Text.Trim();
if (string.IsNullOrEmpty(name))
{
MessageBox.Show(this, "Channel name is required.", "VoiceCat",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
DialogResult = DialogResult.None;
return;
}
uint parentId = (_cboParent.SelectedItem as ChannelItem)?.Id ?? 0;
var audio = new AudioConfigInfo(
Codec: 0,
Stereo: _cboMode.SelectedIndex == 1,
SampleRate: (uint)_numSampleRate.Value,
BitrateBps: (uint)_numBitrate.Value,
FrameMs: (uint)_numFrameMs.Value,
Application: (uint)_cboApplication.SelectedIndex,
Fec: _chkFec.Checked,
ExpectedPacketLoss: (uint)_numExpectedLoss.Value,
Dtx: _chkDtx.Checked,
Complexity: (uint)_numComplexity.Value,
Dred: _chkDred.Checked,
PacketLossMode: _chkAutomaticLoss.Checked ? (VcPacketLossMode)(_cboLossSpeed.SelectedIndex + 1) : VcPacketLossMode.Manual);
Result = new ChannelEditInfo(
Id: _editingId,
ParentId: parentId,
Name: name,
Topic: _txtTopic.Text.Trim(),
PasswordProtected: _chkPassword.Checked,
Password: _chkPassword.Checked ? _txtPassword.Text : null,
MaxUsers: (uint)_numMaxUsers.Value,
SortOrder: (int)_numSortOrder.Value,
Audio: audio);
}
private sealed class ChannelItem(string name, uint id)
{
public uint Id { get; } = id;
public override string ToString() => name;
}
}