Files
voice-cat/clients/apple/dotnet/VoiceCat.Mac/SettingsWindowController.cs
T
Talon dd811a0bb8
.NET port / test (macos-latest) (push) Canceled after 0s
.NET port / test (ubuntu-24.04) (push) Canceled after 0s
.NET port / test (windows-latest) (push) Canceled after 0s
.NET port / apple-client (push) Canceled after 0s
Fix audio clock drift and adaptive jitter buffering
2026-09-20 16:26:03 +02:00

127 lines
9.1 KiB
C#

using AppKit;
using CoreGraphics;
using VoiceCat.Audio;
namespace VoiceCat.Mac;
internal sealed class SettingsWindowController : NSWindowController
{
private readonly MainWindowController owner;
private readonly MacSettings settings;
private readonly MacAudioBackend audio;
private readonly NSPopUpButton mode = new(new CGRect(160, 455, 260, 28), false);
private readonly NSSlider vad = new(new CGRect(160, 415, 260, 24)) { MinValue = 0, MaxValue = 100 };
private readonly NSSlider inputGain = new(new CGRect(160, 375, 260, 24)) { MinValue = 0, MaxValue = 400 };
private readonly NSButton noiseReduction = NSButton.CreateCheckbox("Noise reduction (RNNoise)", () => { });
private readonly NSButton stereo = NSButton.CreateCheckbox("Stereo microphone", () => { });
private readonly NSPopUpButton input = new(new CGRect(160, 285, 260, 28), false);
private readonly NSPopUpButton output = new(new CGRect(160, 245, 260, 28), false);
private readonly NSSlider outputGain = new(new CGRect(160, 205, 260, 24)) { MinValue = 0, MaxValue = 100 };
private readonly NSButton auxiliary = NSButton.CreateCheckbox("Aux input stream", () => { });
private readonly NSPopUpButton auxiliaryDevice = new(new CGRect(160, 125, 260, 28), false);
private readonly NSSlider auxiliaryGain = new(new CGRect(160, 85, 260, 24)) { MinValue = 0, MaxValue = 400 };
private readonly NSButton sounds = NSButton.CreateCheckbox("Event sounds", () => { });
private readonly NSButton speech = NSButton.CreateCheckbox("Speak events", () => { });
private readonly NSButton selfTalk = NSButton.CreateCheckbox("Own voice-activity sounds", () => { });
private readonly NSButton pttSound = NSButton.CreateCheckbox("Push-to-talk cue", () => { });
private readonly NSButton pttKey = new(new CGRect(220, 12, 200, 32)) { Title = "Change PTT key…" };
private readonly NSSlider eventVolume = new(new CGRect(160, 505, 260, 24)) { MinValue = 0, MaxValue = 100 };
private readonly NSPopUpButton audioBuffer = new(new CGRect(160, 545, 260, 28), false);
internal SettingsWindowController(MainWindowController owner, MacSettings settings, MacAudioBackend audio) : base(
new NSWindow(new CGRect(0, 0, 460, 660), NSWindowStyle.Titled | NSWindowStyle.Closable,
NSBackingStore.Buffered, false))
{
this.owner = owner; this.settings = settings; this.audio = audio;
Window!.Title = "VoiceCat Settings"; Window.Center();
NSView view = Window.ContentView!;
mode.AddItems(["Voice activation", "Push to talk", "Always on"]);
audioBuffer.AddItems(["Low latency (20 ms)", "Balanced (40 ms)", "Stable (60 ms)"]);
Add(view, "Audio buffering", audioBuffer, 555);
Add(view, "Sound volume", eventVolume, 515);
Add(view, "Input mode", mode, 465); Add(view, "VAD sensitivity", vad, 425); Add(view, "Microphone volume", inputGain, 385);
noiseReduction.Frame = new CGRect(160, 340, 260, 24); stereo.Frame = new CGRect(160, 315, 260, 24);
view.AddSubview(noiseReduction); view.AddSubview(stereo);
Add(view, "Input device", input, 295); Add(view, "Output device", output, 255); Add(view, "Output volume", outputGain, 215);
auxiliary.Frame = new CGRect(160, 165, 260, 24); view.AddSubview(auxiliary);
Add(view, "Aux device", auxiliaryDevice, 135); Add(view, "Aux volume", auxiliaryGain, 95);
sounds.Frame = new CGRect(25, 45, 110, 24); speech.Frame = new CGRect(145, 45, 110, 24);
selfTalk.Frame = new CGRect(265, 45, 180, 24); pttSound.Frame = new CGRect(25, 18, 180, 24);
view.AddSubview(sounds); view.AddSubview(speech); view.AddSubview(selfTalk); view.AddSubview(pttSound); view.AddSubview(pttKey);
foreach (NSControl control in new NSControl[] { mode, vad, inputGain, noiseReduction, stereo, input, output,
outputGain, auxiliary, auxiliaryDevice, auxiliaryGain, sounds, speech, selfTalk, pttSound, eventVolume, audioBuffer }) control.Activated += Changed;
pttKey.Activated += CapturePushToTalkKey;
Populate(input, true, settings.InputDeviceId); Populate(output, false, settings.OutputDeviceId); Populate(auxiliaryDevice, true, settings.AuxiliaryDeviceId);
LoadValues(); SetAccessibility();
}
private static void Add(NSView view, string label, NSView control, double y)
{
NSTextField text = NSTextField.CreateLabel(label); text.Frame = new CGRect(25, y, 125, 22); view.AddSubview(text); view.AddSubview(control);
}
private void Populate(NSPopUpButton picker, bool capture, string? selected)
{
picker.RemoveAllItems(); IReadOnlyList<AudioDeviceInfo> devices = audio.Enumerate(capture);
foreach (AudioDeviceInfo device in devices) { picker.AddItem(device.Name + (device.IsDefault ? " (default)" : "")); picker.LastItem!.RepresentedObject = new Foundation.NSString(device.Id); }
int index = devices.ToList().FindIndex(device => device.Id == selected);
if (index < 0) index = devices.ToList().FindIndex(device => device.IsDefault);
if (index >= 0) picker.SelectItem(index); picker.Enabled = devices.Count > 0;
}
private void LoadValues()
{
mode.SelectItem((int)settings.InputMode); vad.DoubleValue = settings.VadThreshold * 100;
inputGain.DoubleValue = settings.InputGain * 100; outputGain.DoubleValue = settings.OutputGain * 100;
noiseReduction.State = State(settings.InputNoiseReduction); stereo.State = State(settings.StereoMicrophone);
auxiliary.State = State(settings.AuxiliaryEnabled); auxiliaryGain.DoubleValue = settings.AuxiliaryGain * 100;
sounds.State = State(settings.EventSounds); speech.State = State(settings.SpokenEvents);
eventVolume.DoubleValue = settings.EventVolume * 100;
selfTalk.State = State(settings.SelfTalkSounds); pttSound.State = State(settings.PushToTalkSound);
pttKey.Title = $"PTT key code: {settings.PushToTalkKeyCode}";
audioBuffer.SelectItem(settings.AudioBufferMilliseconds switch { 20 => 0, 60 => 2, _ => 1 });
UpdateEnabled();
}
private void CapturePushToTalkKey(object? sender, EventArgs args)
{
var alert = new NSAlert { MessageText = "Set push-to-talk key", InformativeText = "Press the key to use while VoiceCat is focused." };
alert.AddButton("Cancel"); NSObject? monitor = null;
monitor = NSEvent.AddLocalMonitorForEventsMatchingMask(NSEventMask.KeyDown, value =>
{
settings.PushToTalkKeyCode = value.KeyCode; settings.Save(); pttKey.Title = $"PTT key code: {value.KeyCode}";
NSApplication.SharedApplication.AbortModal(); return null!;
});
try { alert.RunModal(); }
finally { if (monitor is not null) NSEvent.RemoveMonitor(monitor); }
}
private async void Changed(object? sender, EventArgs args)
{
settings.InputMode = (AudioInputMode)mode.IndexOfSelectedItem; settings.VadThreshold = (float)vad.DoubleValue / 100;
settings.InputGain = (float)inputGain.DoubleValue / 100; settings.OutputGain = (float)outputGain.DoubleValue / 100;
settings.InputNoiseReduction = On(noiseReduction); settings.StereoMicrophone = On(stereo);
settings.InputDeviceId = Selected(input); settings.OutputDeviceId = Selected(output);
settings.AuxiliaryEnabled = On(auxiliary); settings.AuxiliaryDeviceId = Selected(auxiliaryDevice);
settings.AuxiliaryGain = (float)auxiliaryGain.DoubleValue / 100; settings.EventSounds = On(sounds);
settings.EventVolume = (float)eventVolume.DoubleValue / 100; settings.SpokenEvents = On(speech);
settings.SelfTalkSounds = On(selfTalk); settings.PushToTalkSound = On(pttSound);
settings.AudioBufferMilliseconds = audioBuffer.IndexOfSelectedItem switch { 0 => 20, 2 => 60, _ => 40 };
settings.Save(); UpdateEnabled(); await owner.ApplySettingsAsync();
}
private void UpdateEnabled() { vad.Enabled = settings.InputMode == AudioInputMode.VoiceActivation; auxiliaryDevice.Enabled = auxiliaryGain.Enabled = settings.AuxiliaryEnabled; }
private void SetAccessibility()
{
((INSAccessibility)mode).AccessibilityLabel = "Microphone input mode"; ((INSAccessibility)vad).AccessibilityLabel = "Voice activation sensitivity";
((INSAccessibility)inputGain).AccessibilityLabel = "Microphone volume"; ((INSAccessibility)input).AccessibilityLabel = "Microphone device";
((INSAccessibility)output).AccessibilityLabel = "Output device"; ((INSAccessibility)outputGain).AccessibilityLabel = "Output volume";
((INSAccessibility)auxiliaryDevice).AccessibilityLabel = "Auxiliary input device"; ((INSAccessibility)auxiliaryGain).AccessibilityLabel = "Auxiliary input volume";
((INSAccessibility)eventVolume).AccessibilityLabel = "Event sound volume";
((INSAccessibility)audioBuffer).AccessibilityLabel = "Audio buffering";
}
private static bool On(NSButton button) => button.State == NSCellStateValue.On;
private static NSCellStateValue State(bool value) => value ? NSCellStateValue.On : NSCellStateValue.Off;
private static string? Selected(NSPopUpButton picker) => picker.SelectedItem?.RepresentedObject?.ToString();
}