Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion ControlPad/Arduino/ArduinoController.cs
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion ControlPad/Arduino/ArduinoController.cs
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion ControlPad/Arduino/ArduinoController.cs
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion ControlPad/Arduino/ArduinoController.cs
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion ControlPad/Arduino/ArduinoController.cs
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion ControlPad/Arduino/ArduinoController.cs
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion ControlPad/Arduino/ArduinoController.cs
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion ControlPad/Arduino/ArduinoController.cs
Original file line numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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