diff --git a/ControlPad/Arduino/ArduinoController.cs b/ControlPad/Arduino/ArduinoController.cs index 853eeee..6c7f582 100644 --- a/ControlPad/Arduino/ArduinoController.cs +++ b/ControlPad/Arduino/ArduinoController.cs @@ -24,6 +24,8 @@ public static class ArduinoController private static readonly StringBuilder _lineBuf = new(); private static BoardType _lastBoardType = BoardType.None; private static BadgeType _lastBadgeType = BadgeType.None; + private static long _lastEventDispatchTick; + private const int EventDispatchIntervalMs = 16; public static void Initialize(MainWindow mainWindow, EventHandler eventHandler) { @@ -202,7 +204,16 @@ private static void ProcessLine(string line) UpdateValues(inputs[2..]); - // UI-Updates ggf. drosseln + long nowTick = Environment.TickCount64; + long lastTick = Interlocked.Read(ref _lastEventDispatchTick); + bool shouldDispatch = (nowTick - lastTick) >= EventDispatchIntervalMs && + Interlocked.CompareExchange(ref _lastEventDispatchTick, nowTick, lastTick) == lastTick; + + if (!shouldDispatch) + { + return; + } + _mainWindow._homeUserControl.Dispatcher.BeginInvoke(() => _eventHandler.Update(DataHandler.SliderValues, DataHandler.ButtonValues) ); diff --git a/ControlPad/EventHandler.cs b/ControlPad/EventHandler.cs index bea3292..9a2752a 100644 --- a/ControlPad/EventHandler.cs +++ b/ControlPad/EventHandler.cs @@ -67,7 +67,7 @@ private void SliderEvent(CustomSlider slider, int value) else if (stream.MicName != null) Task.Run(() => AudioController.MuteMic(stream.MicName, false)); else if (stream.Process == null && stream.MicName == null) - Task.Run(() => AudioController.MuteSystem(false)); + Task.Run(() => AudioController.MuteSystem(false, stream.DeviceName)); } if (stream.Process != null) diff --git a/ControlPad/System/AudioController.cs b/ControlPad/System/AudioController.cs index 41be290..4d7447a 100644 --- a/ControlPad/System/AudioController.cs +++ b/ControlPad/System/AudioController.cs @@ -3,15 +3,24 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; +using System.Collections.Concurrent; using System.Diagnostics; using System.Threading; using System.Windows; +using System.Linq; namespace ControlPad { public class AudioController { private MMDeviceEnumerator _enum; + private readonly ConcurrentDictionary _processIdsCache = new(); + private readonly ConcurrentDictionary _outputDeviceIdCache = new(); + private const int ProcessIdsCacheLifetimeMs = 500; + private const int ProcessIdsCacheTrimIntervalMs = 5000; + private const int ProcessIdsCacheMaxEntries = 128; + private const int OutputDeviceCacheLifetimeMs = 3000; + private long _lastProcessCacheTrimTick = Environment.TickCount64; public AudioController() { @@ -24,14 +33,14 @@ public void SetProcessVolume(string processName, float volume) volume = Math.Clamp(volume, 0f, 1f); - List processIds = Process.GetProcessesByName(processName).Select(c => c.Id).ToList(); // this might be slow + int[] processIds = GetProcessIds(processName); for (int i = 0; i < sessions?.Count; i++) { var session = sessions[i]; - if (processIds.Contains((int)session.GetProcessID)) + if (Array.IndexOf(processIds, (int)session.GetProcessID) >= 0) { session.SimpleAudioVolume.Volume = volume; } @@ -69,12 +78,12 @@ public void MuteProcess(string processName, bool mute) using var device = _enum.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia); var sessions = device.AudioSessionManager.Sessions; - List processIds = Process.GetProcessesByName(processName).Select(c => c.Id).ToList(); // this might be slow + int[] processIds = GetProcessIds(processName); for (int i = 0; i < sessions?.Count; i++) { var session = sessions[i]; - if (processIds.Contains((int)session.GetProcessID)) + if (Array.IndexOf(processIds, (int)session.GetProcessID) >= 0) { session.SimpleAudioVolume.Mute = mute; } @@ -83,7 +92,13 @@ public void MuteProcess(string processName, bool mute) public void MuteSystem(bool mute) { - using var device = _enum.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia); + using var device = GetOutputDevice(null); + if (device != null) device.AudioEndpointVolume.Mute = mute; + } + + public void MuteSystem(bool mute, string? outputDeviceName) + { + using var device = GetOutputDevice(outputDeviceName); if (device != null) device.AudioEndpointVolume.Mute = mute; } @@ -102,12 +117,12 @@ public bool IsProcessMute(string processName) using var device = _enum.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia); var sessions = device.AudioSessionManager.Sessions; - List processIds = Process.GetProcessesByName(processName).Select(c => c.Id).ToList(); // this might be slow + int[] processIds = GetProcessIds(processName); for (int i = 0; i < sessions?.Count; i++) { var session = sessions[i]; - if (processIds.Contains((int)session.GetProcessID)) + if (Array.IndexOf(processIds, (int)session.GetProcessID) >= 0) { return session.SimpleAudioVolume.Mute; } @@ -117,7 +132,14 @@ public bool IsProcessMute(string processName) public bool IsSystemMute() { - using var device = _enum.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia); + using var device = GetOutputDevice(null); + if (device != null) return device.AudioEndpointVolume.Mute; + return false; + } + + public bool IsSystemMute(string? outputDeviceName) + { + using var device = GetOutputDevice(outputDeviceName); if (device != null) return device.AudioEndpointVolume.Mute; return false; } @@ -149,6 +171,18 @@ public List GetMics() return mics; } + public List GetMicNames() + { + var names = new List(); + using var enumerator = new MMDeviceEnumerator(); + foreach (var device in enumerator.EnumerateAudioEndPoints(DataFlow.Capture, DeviceState.Active)) + { + names.Add(device.DeviceFriendlyName); + device.Dispose(); + } + return names; + } + public SessionCollection GetAudioSessions() { using var device = _enum.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia); @@ -156,18 +190,122 @@ public SessionCollection GetAudioSessions() return sessions; } + private int[] GetProcessIds(string processName) + { + long nowTick = Environment.TickCount64; + if (_processIdsCache.TryGetValue(processName, out var cacheEntry)) + { + if ((nowTick - cacheEntry.Tick) <= ProcessIdsCacheLifetimeMs) + { + return cacheEntry.ProcessIds; + } + } + + var processes = Process.GetProcessesByName(processName); + var processIds = new int[processes.Length]; + for (int i = 0; i < processes.Length; i++) + { + processIds[i] = processes[i].Id; + processes[i].Dispose(); + } + _processIdsCache[processName] = (nowTick, processIds); + long lastTrimTick = Interlocked.Read(ref _lastProcessCacheTrimTick); + if (_processIdsCache.Count >= ProcessIdsCacheMaxEntries || + (nowTick - lastTrimTick) > ProcessIdsCacheTrimIntervalMs) + { + if (Interlocked.CompareExchange(ref _lastProcessCacheTrimTick, nowTick, lastTrimTick) == lastTrimTick) + { + TrimProcessCache(nowTick); + } + } + return processIds; + } + + private void TrimProcessCache(long nowTick) + { + var staleEntries = new List<(string Key, long Tick)>(); + foreach (var entry in _processIdsCache) + { + if ((nowTick - entry.Value.Tick) > ProcessIdsCacheLifetimeMs) + staleEntries.Add((entry.Key, entry.Value.Tick)); + } + + foreach (var entry in staleEntries) + { + _processIdsCache.TryRemove(entry.Key, out _); + } + + int overflow = _processIdsCache.Count - ProcessIdsCacheMaxEntries; + if (overflow <= 0) return; + + var staleKeySet = staleEntries.Count > 0 + ? new HashSet(staleEntries.Select(e => e.Key)) + : null; + var oldestKeys = _processIdsCache + .Where(entry => staleKeySet == null || !staleKeySet.Contains(entry.Key)) + .OrderBy(entry => entry.Value.Tick) + .Take(overflow) + .Select(entry => entry.Key) + .ToList(); + foreach (var key in oldestKeys) + _processIdsCache.TryRemove(key, out _); + } + public List GetOutputDevices() { return _enum.EnumerateAudioEndPoints(DataFlow.Render, DeviceState.Active).ToList(); } + public List GetOutputDeviceNames() + { + var names = new List(); + foreach (var device in _enum.EnumerateAudioEndPoints(DataFlow.Render, DeviceState.Active)) + { + names.Add(device.DeviceFriendlyName); + device.Dispose(); + } + return names; + } + private MMDevice? GetOutputDevice(string? outputDeviceName) { if (string.IsNullOrWhiteSpace(outputDeviceName)) return _enum.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia); - return GetOutputDevices().FirstOrDefault(d => d.DeviceFriendlyName == outputDeviceName) - ?? _enum.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia); + long nowTick = Environment.TickCount64; + if (_outputDeviceIdCache.TryGetValue(outputDeviceName, out var cacheEntry) && + (nowTick - cacheEntry.Tick) <= OutputDeviceCacheLifetimeMs) + { + try + { + return _enum.GetDevice(cacheEntry.DeviceId); + } + catch + { + _outputDeviceIdCache.TryRemove(outputDeviceName, out _); + } + } + + MMDevice? matchedDevice = null; + foreach (var device in _enum.EnumerateAudioEndPoints(DataFlow.Render, DeviceState.Active)) + { + if (matchedDevice == null && device.DeviceFriendlyName == outputDeviceName) + { + matchedDevice = device; + } + else + { + device.Dispose(); + } + } + + if (matchedDevice != null) + { + _outputDeviceIdCache[outputDeviceName] = (nowTick, matchedDevice.ID); + return matchedDevice; + } + + return _enum.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia); } } } diff --git a/ControlPad/Windows/EditButtonCategoryWindow.xaml.cs b/ControlPad/Windows/EditButtonCategoryWindow.xaml.cs index 0f8c57d..f10e385 100644 --- a/ControlPad/Windows/EditButtonCategoryWindow.xaml.cs +++ b/ControlPad/Windows/EditButtonCategoryWindow.xaml.cs @@ -125,9 +125,9 @@ private void btn_settings_Click(object sender, EventArgs e) if (micDialog.ShowDialog() == true) { - control.ButtonAction.ActionProperty = micDialog.SelectedMic?.DeviceFriendlyName; - control.ButtonAction.ActionPropertyDisplay = micDialog.SelectedMic?.DeviceFriendlyName; - control.TextBlock.Text = $"{control.ButtonAction.ActionType.Description}: {micDialog.SelectedMic?.DeviceFriendlyName}"; + control.ButtonAction.ActionProperty = micDialog.SelectedMicName; + control.ButtonAction.ActionPropertyDisplay = micDialog.SelectedMicName; + control.TextBlock.Text = $"{control.ButtonAction.ActionType.Description}: {micDialog.SelectedMicName}"; } break; } diff --git a/ControlPad/Windows/EditSliderCategoryWindow.xaml.cs b/ControlPad/Windows/EditSliderCategoryWindow.xaml.cs index c012a58..9cf8cd4 100644 --- a/ControlPad/Windows/EditSliderCategoryWindow.xaml.cs +++ b/ControlPad/Windows/EditSliderCategoryWindow.xaml.cs @@ -49,7 +49,7 @@ private void btn_AddMic_Click(object sender, RoutedEventArgs e) if (dialog.ShowDialog() == true) { - DataHandler.SliderCategories[indexOfCategory].AudioStreams.Add(new AudioStream(null, dialog.SelectedMic?.DeviceFriendlyName)); + DataHandler.SliderCategories[indexOfCategory].AudioStreams.Add(new AudioStream(null, dialog.SelectedMicName)); } } @@ -74,7 +74,7 @@ private void btn_AddMainOutput_Click(object sender, RoutedEventArgs e) if (dialog.ShowDialog() == true) { DataHandler.SliderCategories[indexOfCategory].AudioStreams.Add( - new AudioStream(null, null, dialog.SelectedOutputDevice?.DeviceFriendlyName) + new AudioStream(null, null, dialog.SelectedOutputDeviceName) ); } } diff --git a/ControlPad/Windows/Popups/SelectMicPopup.xaml.cs b/ControlPad/Windows/Popups/SelectMicPopup.xaml.cs index bad3cd3..2bc78bb 100644 --- a/ControlPad/Windows/Popups/SelectMicPopup.xaml.cs +++ b/ControlPad/Windows/Popups/SelectMicPopup.xaml.cs @@ -1,18 +1,4 @@ -using NAudio.CoreAudioApi; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Shapes; +using System.Windows; using Wpf.Ui.Controls; namespace ControlPad @@ -20,19 +6,18 @@ namespace ControlPad public partial class SelectMicPopup : FluentWindow { AudioController audioController = new AudioController(); - public MMDevice? SelectedMic { get; set; } + public string? SelectedMicName { get; set; } public SelectMicPopup() { InitializeComponent(); - cb_Mics.DisplayMemberPath = "DeviceFriendlyName"; - cb_Mics.ItemsSource = audioController.GetMics(); + cb_Mics.ItemsSource = audioController.GetMicNames(); } private void btn_Ok_Click(object sender, RoutedEventArgs e) { - if (cb_Mics.SelectedItem is not MMDevice device) return; + if (cb_Mics.SelectedItem is not string micName) return; - SelectedMic = device; + SelectedMicName = micName; DialogResult = true; } diff --git a/ControlPad/Windows/Popups/SelectOutputDevicePopup.xaml.cs b/ControlPad/Windows/Popups/SelectOutputDevicePopup.xaml.cs index 47063d3..564b1cb 100644 --- a/ControlPad/Windows/Popups/SelectOutputDevicePopup.xaml.cs +++ b/ControlPad/Windows/Popups/SelectOutputDevicePopup.xaml.cs @@ -1,25 +1,23 @@ -using NAudio.CoreAudioApi; -using System.Windows; +using System.Windows; using Wpf.Ui.Controls; namespace ControlPad { public partial class SelectOutputDevicePopup : FluentWindow { - public MMDevice? SelectedOutputDevice { get; set; } + public string? SelectedOutputDeviceName { get; set; } public SelectOutputDevicePopup() { InitializeComponent(); - cb_OutputDevices.DisplayMemberPath = "DeviceFriendlyName"; - cb_OutputDevices.ItemsSource = new AudioController().GetOutputDevices(); + cb_OutputDevices.ItemsSource = new AudioController().GetOutputDeviceNames(); } private void btn_Ok_Click(object sender, RoutedEventArgs e) { - if (cb_OutputDevices.SelectedItem is not MMDevice device) return; + if (cb_OutputDevices.SelectedItem is not string outputName) return; - SelectedOutputDevice = device; + SelectedOutputDeviceName = outputName; DialogResult = true; }