Rewrite how picking a matching audio session works

rewrite the UI layer to make it only send updates to the stream deck if needed
This commit is contained in:
dlprows 2023-08-06 21:51:04 -06:00
parent ab769bf7d2
commit a429a435bc
18 changed files with 888 additions and 448 deletions

91
src/.editorconfig Normal file
View File

@ -0,0 +1,91 @@
[*.{cs,vb}]
dotnet_style_operator_placement_when_wrapping = beginning_of_line
tab_width = 4
indent_size = 4
end_of_line = crlf
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
dotnet_style_prefer_auto_properties = true:silent
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
dotnet_style_prefer_conditional_expression_over_return = true:silent
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_compound_assignment = true:suggestion
dotnet_style_prefer_simplified_interpolation = true:suggestion
dotnet_style_namespace_match_folder = true:suggestion
[*.cs]
csharp_indent_labels = one_less_than_current
csharp_using_directive_placement = outside_namespace:silent
csharp_prefer_simple_using_statement = true:suggestion
csharp_prefer_braces = true:silent
csharp_style_namespace_declarations = block_scoped:silent
csharp_style_prefer_method_group_conversion = true:silent
csharp_style_prefer_top_level_statements = true:silent
csharp_style_expression_bodied_methods = false:silent
csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:silent
csharp_style_expression_bodied_indexers = true:silent
csharp_style_expression_bodied_accessors = true:silent
csharp_style_expression_bodied_lambdas = true:silent
csharp_style_expression_bodied_local_functions = false:silent
csharp_style_throw_expression = true:suggestion
csharp_style_prefer_null_check_over_type_check = true:suggestion
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_prefer_local_over_anonymous_function = true:suggestion
csharp_style_prefer_index_operator = true:suggestion
csharp_style_prefer_range_operator = true:suggestion
csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
csharp_style_prefer_tuple_swap = true:suggestion
[*.{cs,vb}]
#### Naming styles ####
# Naming rules
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
# Symbol specifications
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
# Naming styles
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case

View File

@ -7,11 +7,67 @@ using System.Threading.Tasks;
namespace FocusVolumeControl namespace FocusVolumeControl
{ {
internal class ActiveAudioSessionWrapper public class ActiveAudioSessionWrapper
{ {
public string DisplayName { get; set; } public string DisplayName { get; set; }
public string ExecutablePath { get; set; } public string ExecutablePath { get; set; }
public SimpleAudioVolume Volume { get; set; } private List<SimpleAudioVolume> Volume { get; } = new List<SimpleAudioVolume>();
public string Icon { get; set; }
public bool Any()
{
return Volume.Any();
}
public int Count => Volume.Count;
public void AddVolume(SimpleAudioVolume volume)
{
Volume.Add(volume);
}
public void ToggleMute()
{
//when all volumes are muted, Volume.All will return true
//so we swap from muted to false (opposite of Volume.All)
//when any volumes are unmuted, Volume.All will return false
//so we set muted to true (opposite of Volume.All)
var muted = Volume.All(x => x.Mute);
Volume.ForEach(x => x.Mute = !muted);
}
public bool? GetMuted()
{
var muted = Volume.All(x => x.Mute);
var unmuted = Volume.All(x => !x.Mute);
if(muted == !unmuted)
{
return muted;
}
return null;
}
public void IncrementVolumeLevel(int step, int ticks)
{
//if you have more than one volume. they will all get set based on the first volume control
var level = Volume.FirstOrDefault()?.MasterVolume ?? 0;
level += (0.01f * step) * ticks;
level = Math.Max(level, 0);
Volume.ForEach(x => x.MasterVolume = level);
}
public int GetVolumeLevel()
{
var level = Volume.FirstOrDefault()?.MasterVolume ?? 0;
return (int)(level * 100);
}
} }
} }

View File

@ -9,9 +9,12 @@ using System.Threading.Tasks;
namespace FocusVolumeControl namespace FocusVolumeControl
{ {
internal class AudioHelper public class AudioHelper
{ {
ActiveAudioSessionWrapper GetSessionForProcess(Process process) ActiveAudioSessionWrapper _current;
List<Process> _currentProcesses;
public ActiveAudioSessionWrapper FindSession(List<Process> processes)
{ {
var deviceEnumerator = new MMDeviceEnumerator(Guid.NewGuid()); var deviceEnumerator = new MMDeviceEnumerator(Guid.NewGuid());
@ -20,44 +23,99 @@ namespace FocusVolumeControl
var sessions = manager.Sessions; var sessions = manager.Sessions;
var matchingSession = new ActiveAudioSessionWrapper();
foreach (var session in sessions) foreach (var session in sessions)
{ {
var audioProcess = Process.GetProcessById((int)session.ProcessID); var audioProcess = Process.GetProcessById((int)session.ProcessID);
if (session.ProcessID == process.Id || audioProcess?.ProcessName == process.ProcessName) if (processes.Any(x => x.Id == session.ProcessID || x.ProcessName == audioProcess?.ProcessName))
{
try
{ {
var displayName = audioProcess.MainModule.FileVersionInfo.FileDescription; var displayName = audioProcess.MainModule.FileVersionInfo.FileDescription;
var path = audioProcess.MainModule.FileName; if(string.IsNullOrEmpty(displayName))
return new ActiveAudioSessionWrapper()
{ {
DisplayName = displayName, displayName = audioProcess.ProcessName;
ExecutablePath = path,
Volume = session.SimpleAudioVolume
};
} }
matchingSession.DisplayName = displayName;
} }
return null; catch
{
matchingSession.DisplayName ??= audioProcess.ProcessName;
} }
internal ActiveAudioSessionWrapper GetActiveSession() matchingSession.ExecutablePath ??= audioProcess.MainModule.FileName;
//some apps like discord have multiple volume processes.
matchingSession.AddVolume(session.SimpleAudioVolume);
}
}
return matchingSession.Any() ? matchingSession : null;
}
public ActiveAudioSessionWrapper GetActiveSession()
{ {
const int nChars = 256; var processes = GetPossibleProcesses();
IntPtr handle = IntPtr.Zero;
StringBuilder Buff = new StringBuilder(nChars); if (_currentProcesses == null || !_currentProcesses.SequenceEqual(processes))
handle = Native.GetForegroundWindow(); {
_current = FindSession(processes);
}
_currentProcesses = processes;
return _current;
}
/// <summary>
/// Get the list of processes that might be currently selected
/// This includes getting the child window's processes
///
/// This helps to find the audo process for windows store apps whose process is "ApplicationFrameHost.exe"
///
/// The list may optionally include a parent process, because that helps thing steam to be more reliable because the steamwebhelper (ui) is a child of steam.exe
///
/// According to deej, getting the ForegroundWindow and enumerating steam windows should work, but it doesn't seem to work for me without including the parent process
/// https://github.com/omriharel/deej/blob/master/pkg/deej/util/util_windows.go#L22
///
/// but the parent process is sometimes useless (explorer, svchost, etc) so i filter some of them out because i felt like it when i wrote the code
///
/// I also experimented with grabbing the parent process and enumerating through the windows to see if that would help, but any time the parent process was an unexpected process (explorer) it could blow up. so i decided not to bother for now
/// </summary>
/// <returns></returns>
public List<Process> GetPossibleProcesses()
{
var handle = Native.GetForegroundWindow();
if (handle == IntPtr.Zero) if (handle == IntPtr.Zero)
{ {
//todo: return system or something like that?
return null; return null;
} }
var tid = Native.GetWindowThreadProcessId(handle, out var pid); var ids = Native.GetProcessesOfChildWindows(handle);
var process = Process.GetProcessById(pid);
return GetSessionForProcess(process); Native.GetWindowThreadProcessId(handle, out var pid);
ids.Insert(0, pid);
var processes = ids.Distinct()
.Select(x => Process.GetProcessById(x))
.ToList();
try
{
var blah = ParentProcessUtilities.GetParentProcess(pid);
if (blah != null && blah.ProcessName != "explorer" && blah.ProcessName != "svchost")
{
processes.Add(blah);
}
}
catch
{
} }
return processes;
}
} }
} }

View File

@ -1,6 +1,7 @@
using BarRaider.SdTools; using BarRaider.SdTools;
using BarRaider.SdTools.Payloads; using BarRaider.SdTools.Payloads;
using CoreAudio; using CoreAudio;
using FocusVolumeControl.UI;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using System; using System;
@ -11,9 +12,11 @@ using System.Drawing;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.ServiceModel.Description;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Threading;
namespace FocusVolumeControl namespace FocusVolumeControl
{ {
@ -27,7 +30,6 @@ namespace FocusVolumeControl
option for what to do when on app without sound option for what to do when on app without sound
gitea
*/ */
@ -44,11 +46,18 @@ namespace FocusVolumeControl
} }
private PluginSettings settings; private PluginSettings settings;
//IntPtr _foregroundWindowChangedEvent;
//WinEventDelegate _delegate; IntPtr _foregroundWindowChangedEvent;
Native.WinEventDelegate _delegate;
ActiveAudioSessionWrapper _currentAudioSession; ActiveAudioSessionWrapper _currentAudioSession;
AudioHelper _audioHelper = new AudioHelper(); AudioHelper _audioHelper = new AudioHelper();
Thread _thread;
Dispatcher _dispatcher;
UIState _previousState;
public DialAction(ISDConnection connection, InitialPayload payload) : base(connection, payload) public DialAction(ISDConnection connection, InitialPayload payload) : base(connection, payload)
{ {
if (payload.Settings == null || payload.Settings.Count == 0) if (payload.Settings == null || payload.Settings.Count == 0)
@ -61,24 +70,27 @@ namespace FocusVolumeControl
settings = payload.Settings.ToObject<PluginSettings>(); settings = payload.Settings.ToObject<PluginSettings>();
} }
//_delegate = new WinEventDelegate(WinEventProc); _thread = new Thread(() =>
//_foregroundWindowChangedEvent = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, IntPtr.Zero, _delegate, 0, 0, WINEVENT_OUTOFCONTEXT); {
Logger.Instance.LogMessage(TracingLevel.DEBUG, "Registering for events");
_delegate = new Native.WinEventDelegate(WinEventProc);
_foregroundWindowChangedEvent = Native.RegisterForForegroundWindowChangedEvent(_delegate);
Logger.Instance.LogMessage(TracingLevel.DEBUG, "Starting Dispatcher");
_dispatcher = Dispatcher.CurrentDispatcher;
Dispatcher.Run();
Logger.Instance.LogMessage(TracingLevel.DEBUG, "Dispatcher Stopped");
});
_thread.SetApartmentState(ApartmentState.STA);
_thread.Start();
} }
public override async void DialDown(DialPayload payload) public override async void DialDown(DialPayload payload)
{ {
//dial pressed down //dial pressed down
Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Down"); Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Down");
if(_currentAudioSession != null) await ToggleMuteAsync();
{
_currentAudioSession.Volume.Mute = !_currentAudioSession.Volume.Mute;
var uiState = UIState.Build(_currentAudioSession);
await Connection.SetFeedbackAsync(uiState);
}
else
{
await Connection.ShowAlert();
}
} }
public override async void TouchPress(TouchpadPressPayload payload) public override async void TouchPress(TouchpadPressPayload payload)
@ -90,31 +102,31 @@ namespace FocusVolumeControl
} }
else else
{ {
await ToggleMuteAsync();
}
}
async Task ToggleMuteAsync()
{
if (_currentAudioSession != null) if (_currentAudioSession != null)
{ {
_currentAudioSession.Volume.Mute = !_currentAudioSession.Volume.Mute; _currentAudioSession.ToggleMute();
var uiState = UIState.Build(_currentAudioSession); await UpdateStateIfNeeded();
await Connection.SetFeedbackAsync(uiState);
} }
else else
{ {
await Connection.ShowAlert(); await Connection.ShowAlert();
} }
} }
}
public override async void DialRotate(DialRotatePayload payload) public override async void DialRotate(DialRotatePayload payload)
{ {
Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Rotate"); Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Rotate");
//dial rotated. ticks positive for right, negative for left //dial rotated. ticks positive for right, negative for left
if(_currentAudioSession != null) if (_currentAudioSession != null)
{ {
_currentAudioSession.Volume.MasterVolume += (0.01f) * payload.Ticks; _currentAudioSession.IncrementVolumeLevel(1, payload.Ticks);
await UpdateStateIfNeeded();
var uiState = UIState.Build(_currentAudioSession);
await Connection.SetFeedbackAsync(uiState);
} }
else else
{ {
@ -130,12 +142,12 @@ namespace FocusVolumeControl
public override void Dispose() public override void Dispose()
{ {
/* Logger.Instance.LogMessage(TracingLevel.DEBUG, "Disposing");
if(_foregroundWindowChangedEvent != IntPtr.Zero) if(_foregroundWindowChangedEvent != IntPtr.Zero)
{ {
Native.UnhookWinEvent(_foregroundWindowChangedEvent); Native.UnhookWinEvent(_foregroundWindowChangedEvent);
} }
*/ _dispatcher.InvokeShutdown();
} }
public override async void OnTick() public override async void OnTick()
@ -152,10 +164,31 @@ namespace FocusVolumeControl
_currentAudioSession = activeSession; _currentAudioSession = activeSession;
} }
if(_currentAudioSession != null) await UpdateStateIfNeeded();
}
private async Task UpdateStateIfNeeded()
{ {
var uiState = UIState.BuildWithImage(_currentAudioSession); if (_currentAudioSession != null)
{
var uiState = UIState.Build(_currentAudioSession);
if ( _previousState != null && uiState != null &&
uiState.Title == _previousState.Title &&
uiState.Value.Value == _previousState.Value.Value &&
uiState.Value.Opacity == _previousState.Value.Opacity &&
uiState.Indicator.Value == _previousState.Indicator.Value &&
uiState.Indicator.Opacity == _previousState.Indicator.Opacity &&
uiState.icon.Value == _previousState.icon.Value &&
uiState.icon.Opacity == _previousState.icon.Opacity
)
{
return;
}
await Connection.SetFeedbackAsync(uiState); await Connection.SetFeedbackAsync(uiState);
_previousState = uiState;
} }
} }
@ -177,27 +210,12 @@ namespace FocusVolumeControl
} }
/* public void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
public async void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
{ {
var activeSession = _audioHelper.GetActiveSession(); OnTick();
if(activeSession == null)
{
//todo: something?
} }
else
{
_currentAudioSession = activeSession;
//populate the UI
await Connection.SetTitleAsync(_currentAudioSession.DisplayName);
} }
}
*/
}
} }

View File

@ -51,6 +51,7 @@
<Reference Include="System.Data" /> <Reference Include="System.Data" />
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
<Reference Include="System.Xml" /> <Reference Include="System.Xml" />
<Reference Include="WindowsBase" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="ActiveAudioSessionWrapper.cs" /> <Compile Include="ActiveAudioSessionWrapper.cs" />
@ -58,10 +59,12 @@
<Compile Include="DialAction.cs" /> <Compile Include="DialAction.cs" />
<Compile Include="ISDConnectionExtensions.cs" /> <Compile Include="ISDConnectionExtensions.cs" />
<Compile Include="Native.cs" /> <Compile Include="Native.cs" />
<Compile Include="ParentProcessUtilities.cs" />
<Compile Include="PluginAction.cs" /> <Compile Include="PluginAction.cs" />
<Compile Include="Program.cs" /> <Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="UIState.cs" /> <Compile Include="UI\UIState.cs" />
<Compile Include="UI\ValueWithOpacity.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="App.config" /> <None Include="App.config" />
@ -112,12 +115,22 @@
<PackageReference Include="CoreAudio"> <PackageReference Include="CoreAudio">
<Version>1.27.0</Version> <Version>1.27.0</Version>
</PackageReference> </PackageReference>
<PackageReference Include="IsExternalInit">
<Version>1.0.3</Version>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json"> <PackageReference Include="Newtonsoft.Json">
<Version>13.0.3</Version> <Version>13.0.3</Version>
</PackageReference> </PackageReference>
<PackageReference Include="NLog"> <PackageReference Include="NLog">
<Version>5.2.3</Version> <Version>5.2.3</Version>
</PackageReference> </PackageReference>
<PackageReference Include="RequiredMemberAttribute">
<Version>1.0.0</Version>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="streamdeck-client-csharp"> <PackageReference Include="streamdeck-client-csharp">
<Version>4.3.0</Version> <Version>4.3.0</Version>
</PackageReference> </PackageReference>

View File

@ -7,28 +7,58 @@ using System.Threading.Tasks;
namespace FocusVolumeControl namespace FocusVolumeControl
{ {
internal class Native public class Native
{ {
internal delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime); public delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);
[DllImport("user32.dll")] [DllImport("user32.dll")]
static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags); static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags);
[DllImport("user32.dll")] [DllImport("user32.dll")]
internal static extern bool UnhookWinEvent(IntPtr hWinEventHook); public static extern bool UnhookWinEvent(IntPtr hWinEventHook);
private const uint WINEVENT_OUTOFCONTEXT = 0; private const uint WINEVENT_OUTOFCONTEXT = 0;
private const uint EVENT_SYSTEM_FOREGROUND = 3; private const uint EVENT_SYSTEM_FOREGROUND = 3;
internal static IntPtr RegisterForForegroundWindowChangedEvent(WinEventDelegate dele) => SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, IntPtr.Zero, dele, 0, 0, WINEVENT_OUTOFCONTEXT); public static IntPtr RegisterForForegroundWindowChangedEvent(WinEventDelegate dele) => SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, IntPtr.Zero, dele, 0, 0, WINEVENT_OUTOFCONTEXT);
[DllImport("user32.dll")] [DllImport("user32.dll")]
internal static extern IntPtr GetForegroundWindow(); public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll", SetLastError = true)] [DllImport("user32.dll", SetLastError = true)]
internal static extern uint GetWindowThreadProcessId(IntPtr hWnd, out int processId); public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out int processId);
private delegate bool EnumWindowProc(IntPtr hwnd, IntPtr lParam);
[DllImport("user32")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool EnumChildWindows(IntPtr window, EnumWindowProc callback, IntPtr lParam);
public static List<int> GetProcessesOfChildWindows(IntPtr windowHandle)
{
var ids = new List<int>();
if(windowHandle != IntPtr.Zero)
{
EnumChildWindows(windowHandle,
(hWnd, lParam) =>
{
Native.GetWindowThreadProcessId(hWnd, out var pid);
ids.Add(pid);
return true;
}, IntPtr.Zero);
}
return ids;
}
} }
} }

View File

@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace FocusVolumeControl
{
/// <summary>
/// A utility class to determine a process parent.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct ParentProcessUtilities
{
// These members must match PROCESS_BASIC_INFORMATION
internal IntPtr Reserved1;
internal IntPtr PebBaseAddress;
internal IntPtr Reserved2_0;
internal IntPtr Reserved2_1;
internal IntPtr UniqueProcessId;
internal IntPtr InheritedFromUniqueProcessId;
[DllImport("ntdll.dll")]
private static extern int NtQueryInformationProcess(IntPtr processHandle, int processInformationClass, ref ParentProcessUtilities processInformation, int processInformationLength, out int returnLength);
/// <summary>
/// Gets the parent process of specified process.
/// </summary>
/// <param name="id">The process id.</param>
/// <returns>An instance of the Process class.</returns>
public static Process GetParentProcess(int id)
{
var process = Process.GetProcessById(id);
return GetParentProcess(process);
}
/// <summary>
/// Gets the parent process of a specified process.
/// </summary>
/// <param name="handle">The process handle.</param>
/// <returns>An instance of the Process class.</returns>
public static Process GetParentProcess(Process process)
{
var data = new ParentProcessUtilities();
int status = NtQueryInformationProcess(process.Handle, 0, ref data, Marshal.SizeOf(data), out var returnLength);
if (status != 0)
{
return null;
}
try
{
return Process.GetProcessById(data.InheritedFromUniqueProcessId.ToInt32());
}
catch
{
return null;
}
}
}
}

View File

@ -0,0 +1,65 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BarRaider.SdTools;
using Newtonsoft.Json;
namespace FocusVolumeControl.UI
{
internal class UIState
{
[JsonProperty("title")]
public string Title { get; private init; }
[JsonProperty("value")]
public ValueWithOpacity<string> Value { get; private init; }
[JsonProperty("indicator")]
public ValueWithOpacity<float>Indicator { get; private init; }
[JsonProperty("icon")]
public ValueWithOpacity<string> icon { get; private init; }
public static UIState Build(ActiveAudioSessionWrapper session)
{
var volume = session.GetVolumeLevel();
var opacity = session.GetMuted() != true ? 1 : 0.5f;
var iconData = "";
if (session.Icon != null)
{
iconData = session.Icon;
}
else
{
try
{
var icon = Icon.ExtractAssociatedIcon(session.ExecutablePath);
iconData = Tools.ImageToBase64(icon.ToBitmap(), true);
}
catch
{
iconData = "Image/pluginIcon.png";
}
session.Icon = iconData;
}
return new UIState()
{
Title = session.DisplayName,
Value = new() { Value = $"{volume}%", Opacity = opacity },
Indicator = new() { Value = volume, Opacity = opacity },
icon = new() { Value = iconData, Opacity = opacity },
};
}
}
}

View File

@ -0,0 +1,20 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Data.SqlTypes;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FocusVolumeControl.UI
{
internal class ValueWithOpacity<T>
{
[JsonProperty("value")]
public required T Value { get; init; }
[JsonProperty("opacity")]
public required float Opacity { get; init; }
}
}

View File

@ -1,68 +0,0 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BarRaider.SdTools;
namespace FocusVolumeControl
{
internal class UIState
{
public static Dictionary<string, object> Build(ActiveAudioSessionWrapper session)
{
var volume = (int)(session.Volume.MasterVolume * 100);
var valueThing = new Dictionary<string, string>()
{
{ "value", $"{volume}%" },
{ "opacity", "0.5" }
};
var opacity = session.Volume.Mute ? 0.5f : 1;
var payload = new Dictionary<string, object>()
{
{ "indicator", ValueWithOpacity(volume, opacity) },
{ "value", ValueWithOpacity($"{volume}%", opacity ) },
{ "title", session.DisplayName },
};
return payload;
}
public static Dictionary<string, object> ValueWithOpacity(object value, float opacity)
{
return new Dictionary<string, object>()
{
{ "value", value },
{ "opacity", opacity }
};
}
public static Dictionary<string, object> BuildWithImage(ActiveAudioSessionWrapper session)
{
var payload = Build(session);
var opacity = session.Volume.Mute ? 0.5f : 1;
var iconData = "";
try
{
var icon = Icon.ExtractAssociatedIcon(session.ExecutablePath);
iconData = Tools.ImageToBase64(icon.ToBitmap(), true);
}
catch
{
iconData = "Image/pluginIcon.png";
}
payload["icon"] = ValueWithOpacity(iconData, opacity);
return payload;
}
}
}

View File

@ -29,7 +29,7 @@
"PropertyInspectorPath": "PropertyInspector/PluginActionPI.html" "PropertyInspectorPath": "PropertyInspector/PluginActionPI.html"
} }
], ],
"Author": "Daniel Prows", "Author": "dlprows",
"Name": "FocusVolumeControl", "Name": "FocusVolumeControl",
"Description": "Control the volume of the focused application", "Description": "Control the volume of the focused application",
"URL": "https://encyclopediaofdaniel.com", "URL": "https://encyclopediaofdaniel.com",

View File

@ -5,11 +5,15 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SoundBrowser" xmlns:local="clr-namespace:SoundBrowser"
mc:Ignorable="d" mc:Ignorable="d"
Title="MainWindow" Height="150" Width="800"> Title="MainWindow" Height="800" Width="800">
<Grid> <Grid>
<StackPanel> <Grid.RowDefinitions>
<TextBlock x:Name="_tf">blah</TextBlock> <RowDefinition Height="auto"/>
</StackPanel> <RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock x:Name="_tf" Grid.Row="0">current</TextBlock>
<TextBlock x:Name="_tf2" Grid.Row="1">list</TextBlock>
</Grid> </Grid>
</Window> </Window>

View File

@ -1,9 +1,11 @@
using CoreAudio; using CoreAudio;
using FocusVolumeControl;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Management;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -24,93 +26,110 @@ namespace SoundBrowser
/// </summary> /// </summary>
public partial class MainWindow : Window public partial class MainWindow : Window
{ {
AudioHelper _audioHelper;
Native.WinEventDelegate _delegate;
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
dele = new WinEventDelegate(WinEventProc); _audioHelper = new AudioHelper();
IntPtr m_hhook = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, IntPtr.Zero, dele, 0, 0, WINEVENT_OUTOFCONTEXT);
//normally you can just pass a lambda, but for some reason, that seems to get garbage collected
_delegate = new Native.WinEventDelegate(WinEventProc);
Native.RegisterForForegroundWindowChangedEvent(_delegate);
}
public void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
{
SetupCurrentAppFields();
SetupAllSessionFields();
}
private void SetupCurrentAppFields()
{
var handle = Native.GetForegroundWindow();
var sb = new StringBuilder();
if (handle != IntPtr.Zero)
{
//use this in debug to help there be less events
/*
Native.GetWindowThreadProcessId(handle, out var fpid);
var fp = Process.GetProcessById(fpid);
if(!fp.ProcessName.Contains("FSD"))
{
return;
}
*/
var processes = _audioHelper.GetPossibleProcesses();
var session = _audioHelper.FindSession(processes);
foreach (var p in processes)
{
sb.AppendLine($"pid: {p.Id}");
sb.AppendLine($"\tprocessName: {p.ProcessName}");
try
{
sb.AppendLine($"\tFileDescription: {p!.MainModule!.FileVersionInfo.FileDescription}");
}
catch
{
sb.AppendLine("\tFileDescription: ##ERROR##");
} }
WinEventDelegate dele = null; }
sb.AppendLine();
delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime); if (session != null)
[DllImport("user32.dll")]
static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags);
private const uint WINEVENT_OUTOFCONTEXT = 0;
private const uint EVENT_SYSTEM_FOREGROUND = 3;
[DllImport("user32.dll")]
static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll", SetLastError = true)]
static extern uint GetWindowThreadProcessId(IntPtr hWnd, out int processId);
[DllImport("user32.dll")]
static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
private static SimpleAudioVolume GetVolumeObject(Process process)
{ {
sb.AppendLine("picked the following best match");
sb.AppendLine($"\tsession: {session.DisplayName}");
sb.AppendLine($"\tvolume: {session.GetVolumeLevel()}");
sb.AppendLine($"\tcount: {session.Count}");
}
else
{
sb.AppendLine("No Match");
}
}
_tf.Text = sb.ToString();
}
private void SetupAllSessionFields()
{
_tf2.Text = "";
var sb = new StringBuilder();
sb.AppendLine("-------------------------------------------------------------------------------");
var deviceEnumerator = new MMDeviceEnumerator(Guid.NewGuid()); var deviceEnumerator = new MMDeviceEnumerator(Guid.NewGuid());
using var device = deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia); using var device = deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
using var manager = device.AudioSessionManager2; using var manager = device.AudioSessionManager2;
var sessions = manager.Sessions; var sessions = manager!.Sessions;
foreach (var session in sessions) foreach (var session in sessions!)
{ {
if (session.ProcessID == process.Id)
{
return session.SimpleAudioVolume;
}
var audioProcess = Process.GetProcessById((int)session.ProcessID); var audioProcess = Process.GetProcessById((int)session.ProcessID);
if(audioProcess?.ProcessName == process.ProcessName)
{ var displayName = audioProcess!.MainModule!.FileVersionInfo.FileDescription;
Console.WriteLine(process.MainModule.FileVersionInfo.FileDescription);
return session.SimpleAudioVolume; sb.AppendLine($"pid: {audioProcess.Id}");
sb.AppendLine($"\tprocessName: {audioProcess.ProcessName}");
sb.AppendLine($"\tsession: {displayName}");
} }
}
return null; _tf2.Text = sb.ToString();
} }
SimpleAudioVolume _current;
private string GetActiveWindowTitle()
{
const int nChars = 256;
IntPtr handle = IntPtr.Zero;
StringBuilder Buff = new StringBuilder(nChars);
handle = GetForegroundWindow();
if (handle <= 0)
{
return "";
}
var tid = GetWindowThreadProcessId(handle, out var pid);
var process = Process.GetProcessById(pid);
var vol = GetVolumeObject(process);
_current = vol;
if(vol != null)
{
vol.Mute = true;
}
return $"{pid} vol:{vol?.MasterVolume ?? -1} - {process.ProcessName}";
}
public void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
{
_tf.Text = GetActiveWindowTitle();
}
} }
} }

View File

@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace SoundBrowser
{
/// <summary>
/// A utility class to determine a process parent.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct ParentProcessUtilities
{
// These members must match PROCESS_BASIC_INFORMATION
internal IntPtr Reserved1;
internal IntPtr PebBaseAddress;
internal IntPtr Reserved2_0;
internal IntPtr Reserved2_1;
internal IntPtr UniqueProcessId;
internal IntPtr InheritedFromUniqueProcessId;
[DllImport("ntdll.dll")]
private static extern int NtQueryInformationProcess(IntPtr processHandle, int processInformationClass, ref ParentProcessUtilities processInformation, int processInformationLength, out int returnLength);
/// <summary>
/// Gets the parent process of specified process.
/// </summary>
/// <param name="id">The process id.</param>
/// <returns>An instance of the Process class.</returns>
public static Process? GetParentProcess(int id)
{
Process process = Process.GetProcessById(id);
return GetParentProcess(process.Handle);
}
/// <summary>
/// Gets the parent process of a specified process.
/// </summary>
/// <param name="handle">The process handle.</param>
/// <returns>An instance of the Process class.</returns>
public static Process? GetParentProcess(IntPtr handle)
{
ParentProcessUtilities pbi = new ParentProcessUtilities();
int returnLength;
int status = NtQueryInformationProcess(handle, 0, ref pbi, Marshal.SizeOf(pbi), out returnLength);
if (status != 0)
throw new Win32Exception(status);
try
{
return Process.GetProcessById(pbi.InheritedFromUniqueProcessId.ToInt32());
}
catch (ArgumentException)
{
// not found
return null;
}
}
}
}

View File

@ -11,4 +11,8 @@
<PackageReference Include="CoreAudio" Version="1.27.0" /> <PackageReference Include="CoreAudio" Version="1.27.0" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FocusVolumeControl\FocusVolumeControl.csproj" />
</ItemGroup>
</Project> </Project>