Add adaptive packet loss handling
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

This commit is contained in:
2026-09-22 16:54:49 +02:00
parent 7c4708c3de
commit 7b0c003ad4
35 changed files with 518 additions and 60 deletions
@@ -71,8 +71,8 @@ internal sealed class AdministrationWindowController : NSWindowController
private async void ServerMute(object? sender, EventArgs args) { User? user = User(); if (user is not null) Show(await client.SetServerMuteAsync(user.Id, !user.ServerMuted, user.ServerDeafened)); }
private async void ServerDeafen(object? sender, EventArgs args) { User? user = User(); if (user is not null) Show(await client.SetServerMuteAsync(user.Id, user.ServerMuted, !user.ServerDeafened)); }
private void Tune(object? sender, EventArgs args) { User? user = User(); if (user is not null) PerUserTuning.Run(client, user); }
private async void CreateChannel(object? sender, EventArgs args) { ChannelEdit? edit = ChannelEditor.Run(null, client.Channels); if (edit is not null) { Show(await client.CreateChannelAsync(edit.Channel, edit.Password)); Refresh(); } }
private async void EditChannel(object? sender, EventArgs args) { Channel? channel = Channel(); if (channel is null) return; ChannelEdit? edit = ChannelEditor.Run(channel, client.Channels); if (edit is not null) { Show(await client.EditChannelAsync(edit.Channel, edit.Password)); Refresh(); } }
private async void CreateChannel(object? sender, EventArgs args) { ChannelEdit? edit = ChannelEditor.Run(null, client.Channels, client.SupportsAdaptivePacketLoss); if (edit is not null) { Show(await client.CreateChannelAsync(edit.Channel, edit.Password)); Refresh(); } }
private async void EditChannel(object? sender, EventArgs args) { Channel? channel = Channel(); if (channel is null) return; ChannelEdit? edit = ChannelEditor.Run(channel, client.Channels, client.SupportsAdaptivePacketLoss); if (edit is not null) { Show(await client.EditChannelAsync(edit.Channel, edit.Password)); Refresh(); } }
private async void DeleteChannel(object? sender, EventArgs args)
{
Channel? channel = Channel(); if (channel is null || channel.Id == 1) return;
+19 -9
View File
@@ -7,7 +7,7 @@ namespace VoiceCat.Mac;
internal static class ChannelEditor
{
internal static ChannelEdit? Run(Channel? existing, IReadOnlyList<Channel> channels)
internal static ChannelEdit? Run(Channel? existing, IReadOnlyList<Channel> channels, bool supportsAdaptivePacketLoss)
{
var view = new NSView(new CGRect(0, 0, 520, 500));
var name = Field(existing?.Name ?? "", 360); var topic = Field(existing?.Topic ?? "", 330);
@@ -25,13 +25,21 @@ internal static class ChannelEditor
Add(view, "Maximum users (0=none)", maximum, 240); Add(view, "Sort order", order, 210); Add(view, "Password", password, 180);
Add(view, "Mode", mode, 150); Add(view, "Sample rate", rate, 120); Add(view, "Frame ms", frame, 90); Add(view, "Application", application, 60); Add(view, "Bitrate", bitrate, 30);
var advanced = new NSView(new CGRect(0, 0, 520, 105));
var fec = Check("In-band FEC", existing?.Audio?.Fec ?? true, 0, 75); var dtx = Check("DTX", existing?.Audio?.Dtx ?? true, 130, 75);
var dred = Check("Deep redundancy", existing?.Audio?.Dred ?? false, 230, 75);
var loss = Field((existing?.Audio?.ExpectedPacketLoss ?? 5).ToString(), 40); loss.Frame = new CGRect(155, 40, 100, 24);
var complexity = Field((existing?.Audio?.Complexity ?? 10).ToString(), 10); complexity.Frame = new CGRect(155, 10, 100, 24);
advanced.AddSubview(fec); advanced.AddSubview(dtx); advanced.AddSubview(dred); Add(advanced, "Packet loss %", loss, 40); Add(advanced, "Complexity 010", complexity, 10);
var container = new NSView(new CGRect(0, 0, 520, 620)); view.Frame = new CGRect(0, 110, 520, 500); container.AddSubview(view); container.AddSubview(advanced);
var advanced = new NSView(new CGRect(0, 0, 520, 175));
var automaticLoss = Check("Automatic packet loss", existing?.Audio?.PacketLossMode != PacketLossMode.PacketLossManual, 155, 140);
automaticLoss.Frame = new CGRect(155, 140, 200, 24);
automaticLoss.Enabled = supportsAdaptivePacketLoss;
var lossSpeed = Picker(["Fast", "Balanced", "Stable"], 105);
lossSpeed.SelectItem(existing?.Audio?.PacketLossMode switch { PacketLossMode.PacketLossAutoFast => 0, PacketLossMode.PacketLossAutoStable => 2, _ => 1 });
var loss = Field((existing?.Audio?.ExpectedPacketLoss ?? 5).ToString(), 70); loss.Frame = new CGRect(155, 70, 100, 24);
var complexity = Field((existing?.Audio?.Complexity ?? 10).ToString(), 40); complexity.Frame = new CGRect(155, 40, 100, 24);
var fec = Check("In-band FEC", existing?.Audio?.Fec ?? true, 0, 10); var dtx = Check("DTX", existing?.Audio?.Dtx ?? true, 130, 10);
var dred = Check("Deep redundancy", existing?.Audio?.Dred ?? false, 230, 10);
void UpdateLossControls() { loss.Enabled = !On(automaticLoss); lossSpeed.Enabled = supportsAdaptivePacketLoss && On(automaticLoss); }
automaticLoss.Activated += (_, _) => UpdateLossControls(); UpdateLossControls();
advanced.AddSubview(automaticLoss); Add(advanced, "Adaptation speed", lossSpeed, 105); Add(advanced, "Packet loss %", loss, 70); Add(advanced, "Complexity 010", complexity, 40);
advanced.AddSubview(fec); advanced.AddSubview(dtx); advanced.AddSubview(dred);
var container = new NSView(new CGRect(0, 0, 520, 690)); view.Frame = new CGRect(0, 180, 520, 500); container.AddSubview(view); container.AddSubview(advanced);
var alert = new NSAlert { MessageText = existing is null ? "Create channel" : "Edit channel", AccessoryView = container };
alert.AddButton(existing is null ? "Create" : "Save"); alert.AddButton("Cancel"); if (alert.RunModal() != 1000) return null;
if (string.IsNullOrWhiteSpace(name.StringValue) || !uint.TryParse(maximum.StringValue, out uint max) || !int.TryParse(order.StringValue, out int sort) ||
@@ -45,7 +53,9 @@ internal static class ChannelEditor
Type = type.IndexOfSelectedItem == 1 ? ChannelType.ChannelTemporary : ChannelType.ChannelPermanent,
Audio = new AudioConfig { Codec = 0, Mode = mode.IndexOfSelectedItem == 1 ? ChannelMode.ModeStereo : ChannelMode.ModeMono,
SampleRate = sampleRate, FrameMs = frameMs, Application = (OpusApplication)(int)application.IndexOfSelectedItem, BitrateBps = bitrateValue,
Fec = On(fec), Dtx = On(dtx), Dred = On(dred), ExpectedPacketLoss = packetLoss, Complexity = complexityValue }
Fec = On(fec), Dtx = On(dtx), Dred = On(dred), ExpectedPacketLoss = packetLoss, Complexity = complexityValue,
PacketLossMode = On(automaticLoss) ? lossSpeed.IndexOfSelectedItem switch
{ 0 => PacketLossMode.PacketLossAutoFast, 2 => PacketLossMode.PacketLossAutoStable, _ => PacketLossMode.PacketLossAutoBalanced } : PacketLossMode.PacketLossManual }
}, password.StringValue);
}
+1
View File
@@ -43,6 +43,7 @@ internal sealed class AppModel
internal IosSettings Settings => settings;
internal VoiceCatClient? Client => client;
internal bool IsConnected => client?.State == ClientConnectionState.Connected;
internal bool SupportsAdaptivePacketLoss => client?.SupportsAdaptivePacketLoss == true;
internal bool IsConnecting { get; private set; }
internal bool VoiceJoined => microphoneStream != 0;
internal bool ScreenSharing => broadcast?.IsActive == true;
@@ -10,17 +10,21 @@ internal sealed class ChannelEditorController : UIViewController
private readonly UITextField name = UiHelpers.Field("Channel name"), topic = UiHelpers.Field("Topic (optional)"), maximum = UiHelpers.Field("Maximum users, 0 is unlimited"), order = UiHelpers.Field("Sort order"), password = UiHelpers.Field("Password; blank preserves existing", true), bitrate = UiHelpers.Field("Bitrate in bits per second"), loss = UiHelpers.Field("Expected packet loss percent"), complexity = UiHelpers.Field("Complexity 0 through 10");
private readonly UISegmentedControl type = new(["Permanent", "Temporary"]), mode = new(["Mono", "Stereo"]), application = new(["VoIP", "Audio", "Low delay"]);
private readonly UIButton parent = UIButton.FromType(UIButtonType.System), sampleRate = UIButton.FromType(UIButtonType.System), frame = UIButton.FromType(UIButtonType.System);
private readonly UISwitch fec = new(), dtx = new(), dred = new(); private uint parentId;
private readonly UIButton lossSpeed = UIButton.FromType(UIButtonType.System);
private readonly UISwitch automaticLoss = new(), fec = new(), dtx = new(), dred = new(); private uint parentId;
internal ChannelEditorController(AppModel model, Channel? existing) { this.model = model; this.existing = existing; Title = existing is null ? "New Channel" : "Edit Channel"; }
public override void ViewDidLoad()
{
base.ViewDidLoad(); View!.BackgroundColor = UIColor.SystemGroupedBackground; UIScrollView scroll = new() { TranslatesAutoresizingMaskIntoConstraints = false }; UIStackView stack = new() { Axis = UILayoutConstraintAxis.Vertical, Spacing = 12, TranslatesAutoresizingMaskIntoConstraints = false };
View.AddSubview(scroll); scroll.AddSubview(stack); NSLayoutConstraint.ActivateConstraints([scroll.TopAnchor.ConstraintEqualTo(View.SafeAreaLayoutGuide.TopAnchor), scroll.BottomAnchor.ConstraintEqualTo(View.BottomAnchor), scroll.LeadingAnchor.ConstraintEqualTo(View.LeadingAnchor), scroll.TrailingAnchor.ConstraintEqualTo(View.TrailingAnchor), stack.TopAnchor.ConstraintEqualTo(scroll.ContentLayoutGuide.TopAnchor, 16), stack.BottomAnchor.ConstraintEqualTo(scroll.ContentLayoutGuide.BottomAnchor, -24), stack.LeadingAnchor.ConstraintEqualTo(scroll.FrameLayoutGuide.LeadingAnchor, 20), stack.TrailingAnchor.ConstraintEqualTo(scroll.FrameLayoutGuide.TrailingAnchor, -20)]);
foreach (UIView row in new UIView[] { name, topic, PickerRow("Parent channel", parent), PickerRow("Channel type", type), maximum, order, password, PickerRow("Channel mode", mode), PickerRow("Sample rate", sampleRate), PickerRow("Frame duration", frame), PickerRow("Opus application", application), bitrate, loss, complexity, SwitchRow("Forward error correction", fec), SwitchRow("Discontinuous transmission", dtx), SwitchRow("Deep redundancy", dred) }) stack.AddArrangedSubview(row);
foreach (UIView row in new UIView[] { name, topic, PickerRow("Parent channel", parent), PickerRow("Channel type", type), maximum, order, password, PickerRow("Channel mode", mode), PickerRow("Sample rate", sampleRate), PickerRow("Frame duration", frame), PickerRow("Opus application", application), bitrate, SwitchRow("Automatic packet loss", automaticLoss), PickerRow("Adaptation speed", lossSpeed), loss, complexity, SwitchRow("Forward error correction", fec), SwitchRow("Discontinuous transmission", dtx), SwitchRow("Deep redundancy", dred) }) stack.AddArrangedSubview(row);
maximum.KeyboardType = order.KeyboardType = bitrate.KeyboardType = loss.KeyboardType = complexity.KeyboardType = UIKeyboardType.NumberPad;
parent.Menu = UIMenu.Create(model.Channels.Where(channel => channel.Id != existing?.Id).OrderBy(channel => channel.Name).Select(channel => UIAction.Create(channel.Name, null, null, _ => { parentId = channel.Id; parent.SetTitle(channel.Name, UIControlState.Normal); })).Prepend(UIAction.Create("Root", null, null, _ => { parentId = 0; parent.SetTitle("Root", UIControlState.Normal); })).ToArray()); parent.ShowsMenuAsPrimaryAction = true;
sampleRate.Menu = Choice(sampleRate, ["48000"]); sampleRate.ShowsMenuAsPrimaryAction = true; frame.Menu = Choice(frame, ["5", "10", "20", "40", "60"]); frame.ShowsMenuAsPrimaryAction = true;
lossSpeed.Menu = Choice(lossSpeed, ["Fast", "Balanced", "Stable"]); lossSpeed.ShowsMenuAsPrimaryAction = true;
automaticLoss.Enabled = model.SupportsAdaptivePacketLoss;
automaticLoss.ValueChanged += (_, _) => UpdateLossControls();
Load(); NavigationItem.RightBarButtonItem = new("Save", UIBarButtonItemStyle.Done, async (_, _) => await Save());
}
private void Load()
@@ -30,6 +34,9 @@ internal sealed class ChannelEditorController : UIViewController
parent.SetTitle(model.Channels.FirstOrDefault(value => value.Id == parentId)?.Name ?? "Root", UIControlState.Normal); type.SelectedSegment = channel.Type == ChannelType.ChannelTemporary ? 1 : 0; mode.SelectedSegment = channel.Audio?.Mode == ChannelMode.ModeStereo ? 1 : 0;
sampleRate.SetTitle((channel.Audio?.SampleRate ?? 48000).ToString(), UIControlState.Normal); frame.SetTitle((channel.Audio?.FrameMs ?? 20).ToString(), UIControlState.Normal); application.SelectedSegment = (nint)(channel.Audio?.Application ?? OpusApplication.OpusVoip);
bitrate.Text = (channel.Audio?.BitrateBps ?? 64000).ToString(); loss.Text = (channel.Audio?.ExpectedPacketLoss ?? 5).ToString(); complexity.Text = (channel.Audio?.Complexity ?? 10).ToString(); fec.On = channel.Audio?.Fec ?? true; dtx.On = channel.Audio?.Dtx ?? false; dred.On = channel.Audio?.Dred ?? false;
automaticLoss.On = channel.Audio?.PacketLossMode != PacketLossMode.PacketLossManual;
lossSpeed.SetTitle(channel.Audio?.PacketLossMode switch { PacketLossMode.PacketLossAutoFast => "Fast", PacketLossMode.PacketLossAutoStable => "Stable", _ => "Balanced" }, UIControlState.Normal);
UpdateLossControls();
}
private async Task Save()
{
@@ -37,11 +44,13 @@ internal sealed class ChannelEditorController : UIViewController
{
if (string.IsNullOrWhiteSpace(name.Text) || !uint.TryParse(maximum.Text, out uint max) || !int.TryParse(order.Text, out int sort) || !uint.TryParse(sampleRate.Title(UIControlState.Normal), out uint rate) || !uint.TryParse(frame.Title(UIControlState.Normal), out uint frameMs) || !uint.TryParse(bitrate.Text, out uint bits) || !uint.TryParse(loss.Text, out uint packetLoss) || !uint.TryParse(complexity.Text, out uint cpu) || packetLoss > 100 || cpu > 10) throw new ArgumentException("Enter valid channel and Opus settings.");
var channel = new Channel { Id = existing?.Id ?? 0, ParentId = parentId, Name = name.Text.Trim(), Topic = topic.Text?.Trim() ?? "", MaxUsers = max, Order = sort, Type = type.SelectedSegment == 1 ? ChannelType.ChannelTemporary : ChannelType.ChannelPermanent,
Audio = new AudioConfig { Codec = 0, Mode = mode.SelectedSegment == 1 ? ChannelMode.ModeStereo : ChannelMode.ModeMono, SampleRate = rate, FrameMs = frameMs, Application = (OpusApplication)(int)application.SelectedSegment, BitrateBps = bits, ExpectedPacketLoss = packetLoss, Complexity = cpu, Fec = fec.On, Dtx = dtx.On, Dred = dred.On } };
Audio = new AudioConfig { Codec = 0, Mode = mode.SelectedSegment == 1 ? ChannelMode.ModeStereo : ChannelMode.ModeMono, SampleRate = rate, FrameMs = frameMs, Application = (OpusApplication)(int)application.SelectedSegment, BitrateBps = bits, ExpectedPacketLoss = packetLoss, Complexity = cpu, Fec = fec.On, Dtx = dtx.On, Dred = dred.On,
PacketLossMode = automaticLoss.On ? lossSpeed.Title(UIControlState.Normal) switch { "Fast" => PacketLossMode.PacketLossAutoFast, "Stable" => PacketLossMode.PacketLossAutoStable, _ => PacketLossMode.PacketLossAutoBalanced } : PacketLossMode.PacketLossManual } };
GenericResult result = await model.RunAdminAsync(client => existing is null ? client.CreateChannelAsync(channel, password.Text ?? "") : client.EditChannelAsync(channel, password.Text ?? "")); if (!result.Ok) throw new InvalidOperationException(result.Message); NavigationController?.PopViewController(true);
}
catch (Exception exception) { UiHelpers.ShowError(this, exception); }
}
private void UpdateLossControls() { loss.Enabled = !automaticLoss.On; lossSpeed.Enabled = model.SupportsAdaptivePacketLoss && automaticLoss.On; }
private static UIView PickerRow(string label, UIView control) { var row = new UIStackView { Axis = UILayoutConstraintAxis.Horizontal, Distribution = UIStackViewDistribution.FillEqually, Spacing = 8 }; var text = new UILabel { Text = label }; control.AccessibilityLabel = label; row.AddArrangedSubview(text); row.AddArrangedSubview(control); return row; }
private static UIView SwitchRow(string label, UISwitch toggle) { var row = new UIStackView { Axis = UILayoutConstraintAxis.Horizontal, Distribution = UIStackViewDistribution.EqualSpacing }; row.AddArrangedSubview(new UILabel { Text = label }); toggle.AccessibilityLabel = label; row.AddArrangedSubview(toggle); return row; }
private static UIMenu Choice(UIButton button, string[] values) => UIMenu.Create(values.Select(value => UIAction.Create(value, null, null, _ => button.SetTitle(value, UIControlState.Normal))).ToArray());
@@ -26,6 +26,8 @@ public sealed class ChannelEditDialog : Form
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!;
@@ -33,11 +35,14 @@ public sealed class ChannelEditDialog : Form
public ChannelEditInfo? Result { get; private set; }
public ChannelEditDialog(IEnumerable<ChannelInfo> channels, ChannelEditInfo? existing = null)
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;
@@ -269,6 +274,31 @@ public sealed class ChannelEditDialog : Form
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
{
@@ -277,9 +307,16 @@ public sealed class ChannelEditDialog : Form
Minimum = 0,
Maximum = 100,
Value = audio.ExpectedPacketLoss,
TabIndex = 15,
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 (010):", 12, y, labelWidth);
@@ -290,7 +327,7 @@ public sealed class ChannelEditDialog : Form
Minimum = 0,
Maximum = 10,
Value = audio.Complexity,
TabIndex = 16,
TabIndex = 18,
};
page.Controls.Add(_numComplexity);
y += 34;
@@ -301,7 +338,7 @@ public sealed class ChannelEditDialog : Form
Location = new Point(inputX, y),
AutoSize = true,
Checked = audio.Fec,
TabIndex = 17,
TabIndex = 19,
};
page.Controls.Add(_chkFec);
y += 28;
@@ -312,7 +349,7 @@ public sealed class ChannelEditDialog : Form
Location = new Point(inputX, y),
AutoSize = true,
Checked = audio.Dtx,
TabIndex = 18,
TabIndex = 20,
};
page.Controls.Add(_chkDtx);
y += 28;
@@ -323,7 +360,7 @@ public sealed class ChannelEditDialog : Form
Location = new Point(inputX, y),
AutoSize = true,
Checked = audio.Dred,
TabIndex = 19,
TabIndex = 21,
};
page.Controls.Add(_chkDred);
}
@@ -375,7 +412,8 @@ public sealed class ChannelEditDialog : Form
ExpectedPacketLoss: (uint)_numExpectedLoss.Value,
Dtx: _chkDtx.Checked,
Complexity: (uint)_numComplexity.Value,
Dred: _chkDred.Checked);
Dred: _chkDred.Checked,
PacketLossMode: _chkAutomaticLoss.Checked ? (VcPacketLossMode)(_cboLossSpeed.SelectedIndex + 1) : VcPacketLossMode.Manual);
Result = new ChannelEditInfo(
Id: _editingId,
@@ -1132,7 +1132,7 @@ public partial class MainForm : Form
private void CreateChannel()
{
using var dlg = new ChannelEditDialog(_channels);
using var dlg = new ChannelEditDialog(_channels, supportsAdaptivePacketLoss: _client.SupportsAdaptivePacketLoss);
if (dlg.ShowDialog(this) != DialogResult.OK || dlg.Result is null) return;
_client.CreateChannel(dlg.Result);
}
@@ -1148,7 +1148,7 @@ public partial class MainForm : Form
channel.PasswordProtected, null, channel.MaxUsers, channel.SortOrder,
channel.Audio);
using var dlg = new ChannelEditDialog(_channels, editInfo);
using var dlg = new ChannelEditDialog(_channels, editInfo, _client.SupportsAdaptivePacketLoss);
if (dlg.ShowDialog(this) != DialogResult.OK || dlg.Result is null) return;
_client.EditChannel(dlg.Result);
}
@@ -27,6 +27,7 @@ public sealed partial class VoiceCatClient
Id = info.Id, ParentId = info.ParentId, Name = info.Name, Topic = info.Topic, MaxUsers = info.MaxUsers, Order = info.SortOrder,
Audio = new() { Codec = info.Audio.Codec, Mode = info.Audio.Stereo ? ChannelMode.ModeStereo : ChannelMode.ModeMono, SampleRate = info.Audio.SampleRate,
BitrateBps = info.Audio.BitrateBps, FrameMs = info.Audio.FrameMs, Application = (OpusApplication)info.Audio.Application, Complexity = info.Audio.Complexity,
Fec = info.Audio.Fec, ExpectedPacketLoss = info.Audio.ExpectedPacketLoss, Dtx = info.Audio.Dtx, Dred = info.Audio.Dred }
Fec = info.Audio.Fec, ExpectedPacketLoss = info.Audio.ExpectedPacketLoss, Dtx = info.Audio.Dtx, Dred = info.Audio.Dred,
PacketLossMode = (PacketLossMode)info.Audio.PacketLossMode }
};
}
@@ -1,6 +1,8 @@
// Windows-facing state and command values used by the WinForms application.
namespace VoiceCat.Windows;
public enum VcPacketLossMode { Manual, AutoFast, AutoBalanced, AutoStable }
public enum VcResult
{
Ok = 0,
+2 -1
View File
@@ -73,4 +73,5 @@ public sealed record AudioConfigInfo(
uint ExpectedPacketLoss,
bool Dtx,
uint Complexity,
bool Dred);
bool Dred,
VcPacketLossMode PacketLossMode = VcPacketLossMode.Manual);
@@ -87,6 +87,7 @@ public sealed partial class VoiceCatClient : IDisposable
StopDevices(); core.DisconnectAsync().GetAwaiter().GetResult(); return VcResult.Ok;
}
public string GetServerIdentityDisplay() => core.ServerHello is { } hello ? Convert.ToHexString(hello.ServerIdentityFingerprint.Span) : "";
public bool SupportsAdaptivePacketLoss => core.SupportsAdaptivePacketLoss;
public void PumpEvents()
{
@@ -182,7 +183,7 @@ public sealed partial class VoiceCatClient : IDisposable
public List<DeviceInfo> ListDevices(VcDeviceKind kind) => backend?.Enumerate(kind == VcDeviceKind.Input).Select(d => new DeviceInfo(d.Id, d.Name, d.IsDefault)).ToList() ?? [];
public static string VersionString => "VoiceCat managed core 0.1.0 (protocol v2)";
public static string ResultString(VcResult result) => result.ToString();
private static AudioConfigInfo Audio(AudioConfig a) => new(a.Codec, a.Mode == ChannelMode.ModeStereo, a.SampleRate, a.BitrateBps, a.FrameMs, (uint)a.Application, a.Fec, a.ExpectedPacketLoss, a.Dtx, a.Complexity, a.Dred);
private static AudioConfigInfo Audio(AudioConfig a) => new(a.Codec, a.Mode == ChannelMode.ModeStereo, a.SampleRate, a.BitrateBps, a.FrameMs, (uint)a.Application, a.Fec, a.ExpectedPacketLoss, a.Dtx, a.Complexity, a.Dred, (VcPacketLossMode)a.PacketLossMode);
private void StopDevices()
{