Skip to content
13 changes: 12 additions & 1 deletion ControlPad/Arduino/ArduinoController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@
private static SerialPort? _serialPort;
private static ManagementEventWatcher? _insertWatcher;
private static ManagementEventWatcher? _removeWatcher;
private static MainWindow _mainWindow;

Check warning on line 20 in ControlPad/Arduino/ArduinoController.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable field '_mainWindow' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.
private static EventHandler _eventHandler;

Check warning on line 21 in ControlPad/Arduino/ArduinoController.cs

View workflow job for this annotation

GitHub Actions / build

Non-nullable field '_eventHandler' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.
public static bool IsConnected = false;
private static CancellationTokenSource? _readCts;
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)
{
Expand Down Expand Up @@ -202,7 +204,16 @@

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)
);
Expand Down
2 changes: 1 addition & 1 deletion ControlPad/EventHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
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)
Expand Down Expand Up @@ -101,7 +101,7 @@
{
if (IsPressed && !IsPressedOld)
{
AudioController.MuteProcess(buttonAction.ActionProperty, !AudioController.IsProcessMute(buttonAction.ActionProperty));

Check warning on line 104 in ControlPad/EventHandler.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'processName' in 'void AudioController.MuteProcess(string processName, bool mute)'.

Check warning on line 104 in ControlPad/EventHandler.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'processName' in 'bool AudioController.IsProcessMute(string processName)'.
}
break;
}
Expand All @@ -117,7 +117,7 @@
{
if (IsPressed && !IsPressedOld)
{
AudioController.MuteMic(buttonAction.ActionProperty, !AudioController.IsMicMute(buttonAction.ActionProperty));

Check warning on line 120 in ControlPad/EventHandler.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'micName' in 'void AudioController.MuteMic(string micName, bool mute)'.

Check warning on line 120 in ControlPad/EventHandler.cs

View workflow job for this annotation

GitHub Actions / build

Possible null reference argument for parameter 'micName' in 'bool AudioController.IsMicMute(string micName)'.
}
break;
}
Expand Down
158 changes: 148 additions & 10 deletions ControlPad/System/AudioController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, (long Tick, int[] ProcessIds)> _processIdsCache = new();
private readonly ConcurrentDictionary<string, (long Tick, string DeviceId)> _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()
{
Expand All @@ -24,14 +33,14 @@ public void SetProcessVolume(string processName, float volume)

volume = Math.Clamp(volume, 0f, 1f);

List<int> 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;
}
Expand Down Expand Up @@ -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<int> 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;
}
Expand All @@ -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;
}

Expand All @@ -102,12 +117,12 @@ public bool IsProcessMute(string processName)
using var device = _enum.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
var sessions = device.AudioSessionManager.Sessions;

List<int> 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;
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -149,25 +171,141 @@ public List<MMDevice> GetMics()
return mics;
}

public List<string> GetMicNames()
{
var names = new List<string>();
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);
var sessions = device.AudioSessionManager.Sessions;
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<string>(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<MMDevice> GetOutputDevices()
{
return _enum.EnumerateAudioEndPoints(DataFlow.Render, DeviceState.Active).ToList();
}

public List<string> GetOutputDeviceNames()
{
var names = new List<string>();
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);
}
}
}
6 changes: 3 additions & 3 deletions ControlPad/Windows/EditButtonCategoryWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
4 changes: 2 additions & 2 deletions ControlPad/Windows/EditSliderCategoryWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}

Expand All @@ -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)
);
}
}
Expand Down
25 changes: 5 additions & 20 deletions ControlPad/Windows/Popups/SelectMicPopup.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,38 +1,23 @@
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
{
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;
}

Expand Down
12 changes: 5 additions & 7 deletions ControlPad/Windows/Popups/SelectOutputDevicePopup.xaml.cs
Original file line number Diff line number Diff line change
@@ -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;
}

Expand Down
Loading