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 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(); }