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:
parent
ab769bf7d2
commit
a429a435bc
91
src/.editorconfig
Normal file
91
src/.editorconfig
Normal 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
|
@ -7,11 +7,67 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace FocusVolumeControl
|
||||
{
|
||||
internal class ActiveAudioSessionWrapper
|
||||
public class ActiveAudioSessionWrapper
|
||||
{
|
||||
public string DisplayName { 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -9,9 +9,12 @@ using System.Threading.Tasks;
|
||||
|
||||
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());
|
||||
|
||||
@ -20,44 +23,99 @@ namespace FocusVolumeControl
|
||||
|
||||
var sessions = manager.Sessions;
|
||||
|
||||
var matchingSession = new ActiveAudioSessionWrapper();
|
||||
|
||||
foreach (var session in sessions)
|
||||
{
|
||||
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 path = audioProcess.MainModule.FileName;
|
||||
return new ActiveAudioSessionWrapper()
|
||||
if(string.IsNullOrEmpty(displayName))
|
||||
{
|
||||
DisplayName = displayName,
|
||||
ExecutablePath = path,
|
||||
Volume = session.SimpleAudioVolume
|
||||
};
|
||||
displayName = audioProcess.ProcessName;
|
||||
}
|
||||
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;
|
||||
IntPtr handle = IntPtr.Zero;
|
||||
StringBuilder Buff = new StringBuilder(nChars);
|
||||
handle = Native.GetForegroundWindow();
|
||||
var processes = GetPossibleProcesses();
|
||||
|
||||
if (_currentProcesses == null || !_currentProcesses.SequenceEqual(processes))
|
||||
{
|
||||
_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)
|
||||
{
|
||||
//todo: return system or something like that?
|
||||
return null;
|
||||
}
|
||||
|
||||
var tid = Native.GetWindowThreadProcessId(handle, out var pid);
|
||||
var process = Process.GetProcessById(pid);
|
||||
var ids = Native.GetProcessesOfChildWindows(handle);
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using BarRaider.SdTools;
|
||||
using BarRaider.SdTools.Payloads;
|
||||
using CoreAudio;
|
||||
using FocusVolumeControl.UI;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
@ -11,9 +12,11 @@ using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.ServiceModel.Description;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace FocusVolumeControl
|
||||
{
|
||||
@ -27,7 +30,6 @@ namespace FocusVolumeControl
|
||||
|
||||
option for what to do when on app without sound
|
||||
|
||||
gitea
|
||||
*/
|
||||
|
||||
|
||||
@ -44,11 +46,18 @@ namespace FocusVolumeControl
|
||||
}
|
||||
|
||||
private PluginSettings settings;
|
||||
//IntPtr _foregroundWindowChangedEvent;
|
||||
//WinEventDelegate _delegate;
|
||||
|
||||
IntPtr _foregroundWindowChangedEvent;
|
||||
Native.WinEventDelegate _delegate;
|
||||
|
||||
ActiveAudioSessionWrapper _currentAudioSession;
|
||||
AudioHelper _audioHelper = new AudioHelper();
|
||||
|
||||
Thread _thread;
|
||||
Dispatcher _dispatcher;
|
||||
|
||||
UIState _previousState;
|
||||
|
||||
public DialAction(ISDConnection connection, InitialPayload payload) : base(connection, payload)
|
||||
{
|
||||
if (payload.Settings == null || payload.Settings.Count == 0)
|
||||
@ -61,24 +70,27 @@ namespace FocusVolumeControl
|
||||
settings = payload.Settings.ToObject<PluginSettings>();
|
||||
}
|
||||
|
||||
//_delegate = new WinEventDelegate(WinEventProc);
|
||||
//_foregroundWindowChangedEvent = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, IntPtr.Zero, _delegate, 0, 0, WINEVENT_OUTOFCONTEXT);
|
||||
_thread = new Thread(() =>
|
||||
{
|
||||
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)
|
||||
{
|
||||
//dial pressed down
|
||||
Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Down");
|
||||
if(_currentAudioSession != null)
|
||||
{
|
||||
_currentAudioSession.Volume.Mute = !_currentAudioSession.Volume.Mute;
|
||||
var uiState = UIState.Build(_currentAudioSession);
|
||||
await Connection.SetFeedbackAsync(uiState);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Connection.ShowAlert();
|
||||
}
|
||||
await ToggleMuteAsync();
|
||||
}
|
||||
|
||||
public override async void TouchPress(TouchpadPressPayload payload)
|
||||
@ -90,31 +102,31 @@ namespace FocusVolumeControl
|
||||
}
|
||||
else
|
||||
{
|
||||
await ToggleMuteAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async Task ToggleMuteAsync()
|
||||
{
|
||||
if (_currentAudioSession != null)
|
||||
{
|
||||
_currentAudioSession.Volume.Mute = !_currentAudioSession.Volume.Mute;
|
||||
var uiState = UIState.Build(_currentAudioSession);
|
||||
await Connection.SetFeedbackAsync(uiState);
|
||||
_currentAudioSession.ToggleMute();
|
||||
await UpdateStateIfNeeded();
|
||||
}
|
||||
else
|
||||
{
|
||||
await Connection.ShowAlert();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override async void DialRotate(DialRotatePayload payload)
|
||||
{
|
||||
Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Rotate");
|
||||
//dial rotated. ticks positive for right, negative for left
|
||||
if(_currentAudioSession != null)
|
||||
if (_currentAudioSession != null)
|
||||
{
|
||||
_currentAudioSession.Volume.MasterVolume += (0.01f) * payload.Ticks;
|
||||
|
||||
var uiState = UIState.Build(_currentAudioSession);
|
||||
await Connection.SetFeedbackAsync(uiState);
|
||||
_currentAudioSession.IncrementVolumeLevel(1, payload.Ticks);
|
||||
await UpdateStateIfNeeded();
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -130,12 +142,12 @@ namespace FocusVolumeControl
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
/*
|
||||
Logger.Instance.LogMessage(TracingLevel.DEBUG, "Disposing");
|
||||
if(_foregroundWindowChangedEvent != IntPtr.Zero)
|
||||
{
|
||||
Native.UnhookWinEvent(_foregroundWindowChangedEvent);
|
||||
}
|
||||
*/
|
||||
_dispatcher.InvokeShutdown();
|
||||
}
|
||||
|
||||
public override async void OnTick()
|
||||
@ -152,10 +164,31 @@ namespace FocusVolumeControl
|
||||
_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);
|
||||
_previousState = uiState;
|
||||
}
|
||||
}
|
||||
|
||||
@ -177,27 +210,12 @@ namespace FocusVolumeControl
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
public async void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
|
||||
public void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
|
||||
{
|
||||
var activeSession = _audioHelper.GetActiveSession();
|
||||
|
||||
|
||||
if(activeSession == null)
|
||||
{
|
||||
//todo: something?
|
||||
|
||||
OnTick();
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentAudioSession = activeSession;
|
||||
|
||||
//populate the UI
|
||||
await Connection.SetTitleAsync(_currentAudioSession.DisplayName);
|
||||
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -51,6 +51,7 @@
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="WindowsBase" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="ActiveAudioSessionWrapper.cs" />
|
||||
@ -58,10 +59,12 @@
|
||||
<Compile Include="DialAction.cs" />
|
||||
<Compile Include="ISDConnectionExtensions.cs" />
|
||||
<Compile Include="Native.cs" />
|
||||
<Compile Include="ParentProcessUtilities.cs" />
|
||||
<Compile Include="PluginAction.cs" />
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="UIState.cs" />
|
||||
<Compile Include="UI\UIState.cs" />
|
||||
<Compile Include="UI\ValueWithOpacity.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="App.config" />
|
||||
@ -112,12 +115,22 @@
|
||||
<PackageReference Include="CoreAudio">
|
||||
<Version>1.27.0</Version>
|
||||
</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">
|
||||
<Version>13.0.3</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NLog">
|
||||
<Version>5.2.3</Version>
|
||||
</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">
|
||||
<Version>4.3.0</Version>
|
||||
</PackageReference>
|
||||
|
@ -7,28 +7,58 @@ using System.Threading.Tasks;
|
||||
|
||||
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")]
|
||||
static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags);
|
||||
|
||||
[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 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")]
|
||||
internal static extern IntPtr GetForegroundWindow();
|
||||
public static extern IntPtr GetForegroundWindow();
|
||||
|
||||
[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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
65
src/FocusVolumeControl/ParentProcessUtilities.cs
Normal file
65
src/FocusVolumeControl/ParentProcessUtilities.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
65
src/FocusVolumeControl/UI/UIState.cs
Normal file
65
src/FocusVolumeControl/UI/UIState.cs
Normal 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 },
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
20
src/FocusVolumeControl/UI/ValueWithOpacity.cs
Normal file
20
src/FocusVolumeControl/UI/ValueWithOpacity.cs
Normal 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; }
|
||||
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -29,7 +29,7 @@
|
||||
"PropertyInspectorPath": "PropertyInspector/PluginActionPI.html"
|
||||
}
|
||||
],
|
||||
"Author": "Daniel Prows",
|
||||
"Author": "dlprows",
|
||||
"Name": "FocusVolumeControl",
|
||||
"Description": "Control the volume of the focused application",
|
||||
"URL": "https://encyclopediaofdaniel.com",
|
||||
|
@ -5,11 +5,15 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:SoundBrowser"
|
||||
mc:Ignorable="d"
|
||||
Title="MainWindow" Height="150" Width="800">
|
||||
Title="MainWindow" Height="800" Width="800">
|
||||
<Grid>
|
||||
<StackPanel>
|
||||
<TextBlock x:Name="_tf">blah</TextBlock>
|
||||
</StackPanel>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock x:Name="_tf" Grid.Row="0">current</TextBlock>
|
||||
<TextBlock x:Name="_tf2" Grid.Row="1">list</TextBlock>
|
||||
|
||||
</Grid>
|
||||
</Window>
|
||||
|
@ -1,9 +1,11 @@
|
||||
using CoreAudio;
|
||||
using FocusVolumeControl;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Management;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
@ -24,93 +26,110 @@ namespace SoundBrowser
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
|
||||
AudioHelper _audioHelper;
|
||||
Native.WinEventDelegate _delegate;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
dele = new WinEventDelegate(WinEventProc);
|
||||
IntPtr m_hhook = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, IntPtr.Zero, dele, 0, 0, WINEVENT_OUTOFCONTEXT);
|
||||
_audioHelper = new AudioHelper();
|
||||
|
||||
//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;
|
||||
}
|
||||
|
||||
|
||||
delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);
|
||||
|
||||
[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();
|
||||
if (session != null)
|
||||
{
|
||||
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());
|
||||
|
||||
using var device = deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
|
||||
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);
|
||||
if(audioProcess?.ProcessName == process.ProcessName)
|
||||
{
|
||||
Console.WriteLine(process.MainModule.FileVersionInfo.FileDescription);
|
||||
return session.SimpleAudioVolume;
|
||||
|
||||
var displayName = audioProcess!.MainModule!.FileVersionInfo.FileDescription;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
65
src/SoundBrowser/ParentProcessUtilities.cs
Normal file
65
src/SoundBrowser/ParentProcessUtilities.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -11,4 +11,8 @@
|
||||
<PackageReference Include="CoreAudio" Version="1.27.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FocusVolumeControl\FocusVolumeControl.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
Loading…
Reference in New Issue
Block a user