Compare commits
18 Commits
v1.1.1
...
v1.2.0-pre
Author | SHA1 | Date | |
---|---|---|---|
8eebf1af47 | |||
6de76da8ad | |||
48161b5c2e | |||
d1df235af0 | |||
6aaa32cf92 | |||
4ca0ad021f | |||
4c1ccd9025 | |||
2b10b6d7a6 | |||
ca634f8d3c | |||
bbb0e55ed6 | |||
520659ac52 | |||
609a7bdb65 | |||
13fdfde3e5 | |||
bbad79b4f3 | |||
708180dc8e | |||
5711ace990 | |||
d89c8b1ffa | |||
f94052e54b |
@ -1,217 +0,0 @@
|
|||||||
using FocusVolumeControl.AudioSessions;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace FocusVolumeControl;
|
|
||||||
|
|
||||||
public class AudioHelper
|
|
||||||
{
|
|
||||||
static object _lock = new object();
|
|
||||||
List<Process> _currentProcesses;
|
|
||||||
|
|
||||||
public IAudioSession Current { get; private set; }
|
|
||||||
|
|
||||||
public void ResetCache()
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
Current = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IAudioSession FindSession(List<Process> processes)
|
|
||||||
{
|
|
||||||
var deviceEnumerator = (CoreAudio)new MMDeviceEnumerator();
|
|
||||||
|
|
||||||
deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, out var device);
|
|
||||||
|
|
||||||
Guid iid = typeof(IAudioSessionManager2).GUID;
|
|
||||||
device.Activate(ref iid, 0, IntPtr.Zero, out var m);
|
|
||||||
var manager = (IAudioSessionManager2)m;
|
|
||||||
|
|
||||||
|
|
||||||
manager.GetSessionEnumerator(out var sessionEnumerator);
|
|
||||||
|
|
||||||
var results = new ActiveAudioSessionWrapper();
|
|
||||||
|
|
||||||
sessionEnumerator.GetCount(out var count);
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
sessionEnumerator.GetSession(i, out var session);
|
|
||||||
|
|
||||||
session.GetProcessId(out var sessionProcessId);
|
|
||||||
var audioProcess = Process.GetProcessById(sessionProcessId);
|
|
||||||
|
|
||||||
if (processes.Any(x => x.Id == sessionProcessId || x.ProcessName == audioProcess?.ProcessName))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var displayName = audioProcess.MainModule.FileVersionInfo.FileDescription;
|
|
||||||
if (string.IsNullOrEmpty(displayName))
|
|
||||||
{
|
|
||||||
displayName = audioProcess.ProcessName;
|
|
||||||
}
|
|
||||||
results.DisplayName = displayName;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
results.DisplayName ??= audioProcess.ProcessName;
|
|
||||||
}
|
|
||||||
|
|
||||||
results.ExecutablePath ??= audioProcess.MainModule.FileName;
|
|
||||||
|
|
||||||
//some apps like discord have multiple volume processes.
|
|
||||||
results.AddSession(session);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results.Any() ? results : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public IAudioSession GetActiveSession(FallbackBehavior fallbackBehavior)
|
|
||||||
{
|
|
||||||
lock (_lock)
|
|
||||||
{
|
|
||||||
var processes = GetPossibleProcesses();
|
|
||||||
|
|
||||||
if (_currentProcesses == null || !_currentProcesses.SequenceEqual(processes))
|
|
||||||
{
|
|
||||||
Current = FindSession(processes);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Current == null)
|
|
||||||
{
|
|
||||||
if (fallbackBehavior == FallbackBehavior.SystemSounds && Current is not SystemSoundsAudioSession)
|
|
||||||
{
|
|
||||||
Current = GetSystemSounds();
|
|
||||||
}
|
|
||||||
else if (fallbackBehavior == FallbackBehavior.SystemVolume && Current is not SystemVolumeAudioSession)
|
|
||||||
{
|
|
||||||
Current = GetSystemVolume();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_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)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ids = Native.GetProcessesOfChildWindows(handle);
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ResetAll()
|
|
||||||
{
|
|
||||||
var deviceEnumerator = (CoreAudio)new MMDeviceEnumerator();
|
|
||||||
|
|
||||||
deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, out var device);
|
|
||||||
|
|
||||||
Guid iid = typeof(IAudioSessionManager2).GUID;
|
|
||||||
device.Activate(ref iid, 0, IntPtr.Zero, out var m);
|
|
||||||
var manager = (IAudioSessionManager2)m;
|
|
||||||
|
|
||||||
|
|
||||||
manager.GetSessionEnumerator(out var sessionEnumerator);
|
|
||||||
|
|
||||||
sessionEnumerator.GetCount(out var count);
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
sessionEnumerator.GetSession(i, out var session);
|
|
||||||
|
|
||||||
var volume = (ISimpleAudioVolume)session;
|
|
||||||
var guid = Guid.Empty;
|
|
||||||
volume.SetMasterVolume(1, ref guid);
|
|
||||||
volume.SetMute(false, ref guid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IAudioSession GetSystemSounds()
|
|
||||||
{
|
|
||||||
var deviceEnumerator = (CoreAudio)new MMDeviceEnumerator();
|
|
||||||
|
|
||||||
deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, out var device);
|
|
||||||
|
|
||||||
Guid iid = typeof(IAudioSessionManager2).GUID;
|
|
||||||
device.Activate(ref iid, 0, IntPtr.Zero, out var m);
|
|
||||||
var manager = (IAudioSessionManager2)m;
|
|
||||||
|
|
||||||
|
|
||||||
manager.GetSessionEnumerator(out var sessionEnumerator);
|
|
||||||
|
|
||||||
sessionEnumerator.GetCount(out var count);
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
sessionEnumerator.GetSession(i, out var session);
|
|
||||||
|
|
||||||
if (session.IsSystemSoundsSession() == 0)
|
|
||||||
{
|
|
||||||
return new SystemSoundsAudioSession(session);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
public IAudioSession GetSystemVolume()
|
|
||||||
{
|
|
||||||
var deviceEnumerator = (CoreAudio)new MMDeviceEnumerator();
|
|
||||||
|
|
||||||
deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, out var device);
|
|
||||||
|
|
||||||
Guid iid = typeof(IAudioEndpointVolume).GUID;
|
|
||||||
device.Activate(ref iid, 0, IntPtr.Zero, out var o);
|
|
||||||
var endpointVolume = (IAudioEndpointVolume)o;
|
|
||||||
|
|
||||||
return new SystemVolumeAudioSession(endpointVolume);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
286
src/FocusVolumeControl/AudioHelpers/AppxPackage.cs
Normal file
286
src/FocusVolumeControl/AudioHelpers/AppxPackage.cs
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices.ComTypes;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace FocusVolumeControl.AudioHelpers;
|
||||||
|
|
||||||
|
public sealed class AppxPackage
|
||||||
|
{
|
||||||
|
private AppxPackage()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Path { get; private set; }
|
||||||
|
public string Logo { get; private set; }
|
||||||
|
public string DisplayName { get; private set; }
|
||||||
|
|
||||||
|
public static AppxPackage FromProcess(Process process)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return FromProcess(process.Handle);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AppxPackage FromProcess(int processId)
|
||||||
|
{
|
||||||
|
const int QueryLimitedInformation = 0x1000;
|
||||||
|
IntPtr hProcess = OpenProcess(QueryLimitedInformation, false, processId);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return FromProcess(hProcess);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (hProcess != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
CloseHandle(hProcess);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static AppxPackage FromProcess(IntPtr hProcess)
|
||||||
|
{
|
||||||
|
if (hProcess == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int len = 0;
|
||||||
|
GetPackageFullName(hProcess, ref len, null);
|
||||||
|
if (len == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb = new StringBuilder(len);
|
||||||
|
string fullName = GetPackageFullName(hProcess, ref len, sb) == 0 ? sb.ToString() : null;
|
||||||
|
if (string.IsNullOrEmpty(fullName)) // not an AppX
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var package = QueryPackageInfo(fullName, PackageConstants.PACKAGE_FILTER_HEAD).First();
|
||||||
|
|
||||||
|
return package;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<AppxPackage> QueryPackageInfo(string fullName, PackageConstants flags)
|
||||||
|
{
|
||||||
|
IntPtr infoRef;
|
||||||
|
OpenPackageInfoByFullName(fullName, 0, out infoRef);
|
||||||
|
if (infoRef != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
IntPtr infoBuffer = IntPtr.Zero;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int len = 0;
|
||||||
|
int count;
|
||||||
|
GetPackageInfo(infoRef, flags, ref len, IntPtr.Zero, out count);
|
||||||
|
if (len > 0)
|
||||||
|
{
|
||||||
|
var factory = (IAppxFactory)new AppxFactory();
|
||||||
|
infoBuffer = Marshal.AllocHGlobal(len);
|
||||||
|
int res = GetPackageInfo(infoRef, flags, ref len, infoBuffer, out count);
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var info = (PACKAGE_INFO)Marshal.PtrToStructure(infoBuffer + i * Marshal.SizeOf(typeof(PACKAGE_INFO)), typeof(PACKAGE_INFO));
|
||||||
|
var package = new AppxPackage();
|
||||||
|
package.Path = Marshal.PtrToStringUni(info.path);
|
||||||
|
|
||||||
|
// read manifest
|
||||||
|
string manifestPath = System.IO.Path.Combine(package.Path, "AppXManifest.xml");
|
||||||
|
const int STGM_SHARE_DENY_NONE = 0x40;
|
||||||
|
|
||||||
|
SHCreateStreamOnFileEx(manifestPath, STGM_SHARE_DENY_NONE, 0, false, IntPtr.Zero, out var strm);
|
||||||
|
if (strm != null)
|
||||||
|
{
|
||||||
|
var reader = factory.CreateManifestReader(strm);
|
||||||
|
var properties = reader.GetProperties();
|
||||||
|
|
||||||
|
properties.GetStringValue("DisplayName", out var displayName);
|
||||||
|
package.DisplayName = displayName;
|
||||||
|
|
||||||
|
properties.GetStringValue("Logo", out var logo);
|
||||||
|
package.Logo = logo;
|
||||||
|
|
||||||
|
/*
|
||||||
|
var apps = reader.GetApplications();
|
||||||
|
while (apps.GetHasCurrent())
|
||||||
|
{
|
||||||
|
var app = apps.GetCurrent();
|
||||||
|
var appx = new AppxApp(app);
|
||||||
|
appx.Description = GetStringValue(app, "Description");
|
||||||
|
appx.DisplayName = GetStringValue(app, "DisplayName");
|
||||||
|
appx.EntryPoint = GetStringValue(app, "EntryPoint");
|
||||||
|
appx.Executable = GetStringValue(app, "Executable");
|
||||||
|
appx.Id = GetStringValue(app, "Id");
|
||||||
|
appx.Logo = GetStringValue(app, "Logo");
|
||||||
|
appx.SmallLogo = GetStringValue(app, "SmallLogo");
|
||||||
|
appx.StartPage = GetStringValue(app, "StartPage");
|
||||||
|
appx.Square150x150Logo = GetStringValue(app, "Square150x150Logo");
|
||||||
|
appx.Square30x30Logo = GetStringValue(app, "Square30x30Logo");
|
||||||
|
appx.BackgroundColor = GetStringValue(app, "BackgroundColor");
|
||||||
|
appx.ForegroundText = GetStringValue(app, "ForegroundText");
|
||||||
|
appx.WideLogo = GetStringValue(app, "WideLogo");
|
||||||
|
appx.Wide310x310Logo = GetStringValue(app, "Wide310x310Logo");
|
||||||
|
appx.ShortName = GetStringValue(app, "ShortName");
|
||||||
|
appx.Square310x310Logo = GetStringValue(app, "Square310x310Logo");
|
||||||
|
appx.Square70x70Logo = GetStringValue(app, "Square70x70Logo");
|
||||||
|
appx.MinWidth = GetStringValue(app, "MinWidth");
|
||||||
|
package._apps.Add(appx);
|
||||||
|
apps.MoveNext();
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
Marshal.ReleaseComObject(strm);
|
||||||
|
}
|
||||||
|
yield return package;
|
||||||
|
}
|
||||||
|
Marshal.ReleaseComObject(factory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (infoBuffer != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(infoBuffer);
|
||||||
|
}
|
||||||
|
ClosePackageInfo(infoRef);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Guid("5842a140-ff9f-4166-8f5c-62f5b7b0c781"), ComImport]
|
||||||
|
private class AppxFactory
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[Guid("BEB94909-E451-438B-B5A7-D79E767B75D8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||||
|
private interface IAppxFactory
|
||||||
|
{
|
||||||
|
void _VtblGap0_2(); // skip 2 methods
|
||||||
|
IAppxManifestReader CreateManifestReader(IStream inputStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Guid("4E1BD148-55A0-4480-A3D1-15544710637C"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||||
|
private interface IAppxManifestReader
|
||||||
|
{
|
||||||
|
void _VtblGap0_1(); // skip 1 method
|
||||||
|
IAppxManifestProperties GetProperties();
|
||||||
|
void _VtblGap1_5(); // skip 5 methods
|
||||||
|
IAppxManifestApplicationsEnumerator GetApplications();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Guid("9EB8A55A-F04B-4D0D-808D-686185D4847A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||||
|
private interface IAppxManifestApplicationsEnumerator
|
||||||
|
{
|
||||||
|
IAppxManifestApplication GetCurrent();
|
||||||
|
bool GetHasCurrent();
|
||||||
|
bool MoveNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Guid("5DA89BF4-3773-46BE-B650-7E744863B7E8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||||
|
internal interface IAppxManifestApplication
|
||||||
|
{
|
||||||
|
[PreserveSig]
|
||||||
|
int GetStringValue([MarshalAs(UnmanagedType.LPWStr)] string name, [MarshalAs(UnmanagedType.LPWStr)] out string vaue);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Guid("03FAF64D-F26F-4B2C-AAF7-8FE7789B8BCA"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||||
|
private interface IAppxManifestProperties
|
||||||
|
{
|
||||||
|
[PreserveSig]
|
||||||
|
int GetBoolValue([MarshalAs(UnmanagedType.LPWStr)] string name, out bool value);
|
||||||
|
[PreserveSig]
|
||||||
|
int GetStringValue([MarshalAs(UnmanagedType.LPWStr)] string name, [MarshalAs(UnmanagedType.LPWStr)] out string vaue);
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
|
||||||
|
private static extern int SHLoadIndirectString(string pszSource, StringBuilder pszOutBuf, int cchOutBuf, IntPtr ppvReserved);
|
||||||
|
|
||||||
|
[DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
|
||||||
|
private static extern int SHCreateStreamOnFileEx(string fileName, int grfMode, int attributes, bool create, IntPtr reserved, out IStream stream);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
private static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
private static extern bool CloseHandle(IntPtr hObject);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
||||||
|
private static extern int OpenPackageInfoByFullName(string packageFullName, int reserved, out IntPtr packageInfoReference);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
||||||
|
private static extern int GetPackageInfo(IntPtr packageInfoReference, PackageConstants flags, ref int bufferLength, IntPtr buffer, out int count);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
||||||
|
private static extern int ClosePackageInfo(IntPtr packageInfoReference);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
||||||
|
private static extern int GetPackageFullName(IntPtr hProcess, ref int packageFullNameLength, StringBuilder packageFullName);
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
private enum PackageConstants
|
||||||
|
{
|
||||||
|
PACKAGE_FILTER_ALL_LOADED = 0x00000000,
|
||||||
|
PACKAGE_PROPERTY_FRAMEWORK = 0x00000001,
|
||||||
|
PACKAGE_PROPERTY_RESOURCE = 0x00000002,
|
||||||
|
PACKAGE_PROPERTY_BUNDLE = 0x00000004,
|
||||||
|
PACKAGE_FILTER_HEAD = 0x00000010,
|
||||||
|
PACKAGE_FILTER_DIRECT = 0x00000020,
|
||||||
|
PACKAGE_FILTER_RESOURCE = 0x00000040,
|
||||||
|
PACKAGE_FILTER_BUNDLE = 0x00000080,
|
||||||
|
PACKAGE_INFORMATION_BASIC = 0x00000000,
|
||||||
|
PACKAGE_INFORMATION_FULL = 0x00000100,
|
||||||
|
PACKAGE_PROPERTY_DEVELOPMENT_MODE = 0x00010000,
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||||
|
private struct PACKAGE_INFO
|
||||||
|
{
|
||||||
|
public int reserved;
|
||||||
|
public int flags;
|
||||||
|
public IntPtr path;
|
||||||
|
public IntPtr packageFullName;
|
||||||
|
public IntPtr packageFamilyName;
|
||||||
|
public PACKAGE_ID packageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||||
|
private struct PACKAGE_ID
|
||||||
|
{
|
||||||
|
public int reserved;
|
||||||
|
public AppxPackageArchitecture processorArchitecture;
|
||||||
|
public ushort VersionRevision;
|
||||||
|
public ushort VersionBuild;
|
||||||
|
public ushort VersionMinor;
|
||||||
|
public ushort VersionMajor;
|
||||||
|
public IntPtr name;
|
||||||
|
public IntPtr publisher;
|
||||||
|
public IntPtr resourceId;
|
||||||
|
public IntPtr publisherId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum AppxPackageArchitecture
|
||||||
|
{
|
||||||
|
x86 = 0,
|
||||||
|
Arm = 5,
|
||||||
|
x64 = 9,
|
||||||
|
Neutral = 11,
|
||||||
|
Arm64 = 12
|
||||||
|
}
|
280
src/FocusVolumeControl/AudioHelpers/AudioHelper.cs
Normal file
280
src/FocusVolumeControl/AudioHelpers/AudioHelper.cs
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
using FocusVolumeControl.AudioSessions;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace FocusVolumeControl.AudioHelpers;
|
||||||
|
|
||||||
|
public class AudioHelper
|
||||||
|
{
|
||||||
|
NameAndIconHelper _nameAndIconHelper = new NameAndIconHelper();
|
||||||
|
|
||||||
|
static object _lock = new object();
|
||||||
|
int[] _currentProcesses;
|
||||||
|
|
||||||
|
public IAudioSession Current { get; private set; }
|
||||||
|
|
||||||
|
public void ResetCache()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
Current = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Process GetProcessById(int id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Process.GetProcessById(id);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAudioSession FindSession(List<Process> processes)
|
||||||
|
{
|
||||||
|
var results = new ActiveAudioSessionWrapper();
|
||||||
|
Process bestProcessMatch = null;
|
||||||
|
|
||||||
|
var deviceEnumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator();
|
||||||
|
|
||||||
|
deviceEnumerator.EnumAudioEndpoints(DataFlow.Render, DeviceState.Active, out var deviceCollection);
|
||||||
|
deviceCollection.GetCount(out var numDevices);
|
||||||
|
for (int d = 0; d < numDevices; d++)
|
||||||
|
{
|
||||||
|
deviceCollection.Item(d, out var device);
|
||||||
|
|
||||||
|
Guid iid = typeof(IAudioSessionManager2).GUID;
|
||||||
|
device.Activate(ref iid, 0, IntPtr.Zero, out var m);
|
||||||
|
var manager = (IAudioSessionManager2)m;
|
||||||
|
|
||||||
|
|
||||||
|
manager.GetSessionEnumerator(out var sessionEnumerator);
|
||||||
|
|
||||||
|
var currentIndex = int.MaxValue;
|
||||||
|
|
||||||
|
sessionEnumerator.GetCount(out var count);
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
sessionEnumerator.GetSession(i, out var session);
|
||||||
|
|
||||||
|
session.GetProcessId(out var sessionProcessId);
|
||||||
|
var audioProcess = GetProcessById(sessionProcessId);
|
||||||
|
|
||||||
|
if(audioProcess == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = processes.FindIndex(x => x.Id == sessionProcessId || x.ProcessName == audioProcess?.ProcessName);
|
||||||
|
|
||||||
|
if (index > -1)
|
||||||
|
{
|
||||||
|
//processes will be ordered from best to worst (starts with the app, goes to parent)
|
||||||
|
//so we want the display name and executable path to come from the process that is closest to the front of the list
|
||||||
|
//but we want all matching sessions so things like discord work right
|
||||||
|
if (index < currentIndex)
|
||||||
|
{
|
||||||
|
bestProcessMatch = audioProcess;
|
||||||
|
currentIndex = index;
|
||||||
|
|
||||||
|
if(string.IsNullOrEmpty(results.DisplayName))
|
||||||
|
{
|
||||||
|
session.GetDisplayName(out var displayName);
|
||||||
|
results.DisplayName = displayName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//some apps like discord have multiple volume processes.
|
||||||
|
//and some apps will be on multiple devices
|
||||||
|
//so we add all sessions so we can keep them in sync
|
||||||
|
results.AddSession(session);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(bestProcessMatch != null)
|
||||||
|
{
|
||||||
|
_nameAndIconHelper.SetProcessInfo(bestProcessMatch, results);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.Any() ? results : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAudioSession GetActiveSession(FallbackBehavior fallbackBehavior)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
var processes = GetPossibleProcesses();
|
||||||
|
var processIds = processes?.Select(x => x.Id).ToArray();
|
||||||
|
|
||||||
|
if (_currentProcesses == null || !_currentProcesses.SequenceEqual(processIds))
|
||||||
|
{
|
||||||
|
Current = FindSession(processes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Current == null)
|
||||||
|
{
|
||||||
|
if (fallbackBehavior == FallbackBehavior.SystemSounds && Current is not SystemSoundsAudioSession)
|
||||||
|
{
|
||||||
|
Current = GetSystemSounds();
|
||||||
|
}
|
||||||
|
else if (fallbackBehavior == FallbackBehavior.SystemVolume && Current is not SystemVolumeAudioSession)
|
||||||
|
{
|
||||||
|
Current = GetSystemVolume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentProcesses = processIds;
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
return new List<Process>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var ids = Native.GetProcessesOfChildWindows(handle);
|
||||||
|
|
||||||
|
Native.GetWindowThreadProcessId(handle, out var pid);
|
||||||
|
ids.Insert(0, pid);
|
||||||
|
|
||||||
|
var processes = ids.Distinct()
|
||||||
|
.Select(x => Process.GetProcessById(x))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if(processes.FirstOrDefault()?.ProcessName == "explorer")
|
||||||
|
{
|
||||||
|
return new List<Process>();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//note. in instances where you launch a game from steam. this ends up mapping the process to both steam and to the game. which is unfortunate
|
||||||
|
//The problem is that if you don't use the parent processes, then the actual steam window won't get recognized. But if you do, then games will map to steam.
|
||||||
|
//
|
||||||
|
//Additionally, I group all audio processes that match instead of just the most specific, or the first, etc. Because Discord uses two processes, one for voice chat, and one for discord sounds.
|
||||||
|
//
|
||||||
|
//Steam and Discord are both very common, and end up butting heads in the algorithm.
|
||||||
|
//I want to avoid special cases, but since steam and discord are both so common, i'm making an exception.
|
||||||
|
var parentProcess = ParentProcessUtilities.GetParentProcess(pid);
|
||||||
|
if (parentProcess != null
|
||||||
|
&& parentProcess.ProcessName != "explorer"
|
||||||
|
&& parentProcess.ProcessName != "svchost"
|
||||||
|
&& (parentProcess.ProcessName == "steam" && processes.Any(x => x.ProcessName == "steamwebhelper")) //only include steam if the parent process is the steamwebhelper
|
||||||
|
)
|
||||||
|
{
|
||||||
|
processes.Add(parentProcess);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
return processes;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetAll()
|
||||||
|
{
|
||||||
|
var deviceEnumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator();
|
||||||
|
|
||||||
|
deviceEnumerator.EnumAudioEndpoints(DataFlow.Render, DeviceState.Active, out var deviceCollection);
|
||||||
|
|
||||||
|
deviceCollection.GetCount(out var numDevices);
|
||||||
|
for (int d = 0; d < numDevices; d++)
|
||||||
|
{
|
||||||
|
deviceCollection.Item(d, out var device);
|
||||||
|
|
||||||
|
Guid iid = typeof(IAudioSessionManager2).GUID;
|
||||||
|
device.Activate(ref iid, 0, IntPtr.Zero, out var m);
|
||||||
|
var manager = (IAudioSessionManager2)m;
|
||||||
|
|
||||||
|
|
||||||
|
manager.GetSessionEnumerator(out var sessionEnumerator);
|
||||||
|
|
||||||
|
sessionEnumerator.GetCount(out var count);
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
sessionEnumerator.GetSession(i, out var session);
|
||||||
|
|
||||||
|
var volume = (ISimpleAudioVolume)session;
|
||||||
|
var guid = Guid.Empty;
|
||||||
|
volume.SetMasterVolume(1, ref guid);
|
||||||
|
volume.SetMute(false, ref guid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAudioSession GetSystemSounds()
|
||||||
|
{
|
||||||
|
var deviceEnumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator();
|
||||||
|
deviceEnumerator.EnumAudioEndpoints(DataFlow.Render, DeviceState.Active, out var deviceCollection);
|
||||||
|
|
||||||
|
deviceCollection.GetCount(out var numDevices);
|
||||||
|
for (int d = 0; d < numDevices; d++)
|
||||||
|
{
|
||||||
|
deviceCollection.Item(d, out var device);
|
||||||
|
|
||||||
|
|
||||||
|
Guid iid = typeof(IAudioSessionManager2).GUID;
|
||||||
|
device.Activate(ref iid, 0, IntPtr.Zero, out var m);
|
||||||
|
var manager = (IAudioSessionManager2)m;
|
||||||
|
|
||||||
|
|
||||||
|
manager.GetSessionEnumerator(out var sessionEnumerator);
|
||||||
|
|
||||||
|
sessionEnumerator.GetCount(out var count);
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
sessionEnumerator.GetSession(i, out var session);
|
||||||
|
|
||||||
|
if (session.IsSystemSoundsSession() == 0)
|
||||||
|
{
|
||||||
|
return new SystemSoundsAudioSession(session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
public IAudioSession GetSystemVolume()
|
||||||
|
{
|
||||||
|
var deviceEnumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator();
|
||||||
|
|
||||||
|
deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia, out var device);
|
||||||
|
|
||||||
|
Guid iid = typeof(IAudioEndpointVolume).GUID;
|
||||||
|
device.Activate(ref iid, 0, IntPtr.Zero, out var o);
|
||||||
|
var endpointVolume = (IAudioEndpointVolume)o;
|
||||||
|
|
||||||
|
return new SystemVolumeAudioSession(endpointVolume);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
120
src/FocusVolumeControl/AudioHelpers/NameAndIconHelper.cs
Normal file
120
src/FocusVolumeControl/AudioHelpers/NameAndIconHelper.cs
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
using BarRaider.SdTools;
|
||||||
|
using FocusVolumeControl.AudioSessions;
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace FocusVolumeControl.AudioHelpers;
|
||||||
|
|
||||||
|
public class NameAndIconHelper
|
||||||
|
{
|
||||||
|
public (string name, string icon) GetProcessInfo(Process process)
|
||||||
|
{
|
||||||
|
//i know this is dumb, but its only used by the sound browser, not real prod code
|
||||||
|
var blah = new ActiveAudioSessionWrapper();
|
||||||
|
SetProcessInfo(process, blah);
|
||||||
|
return (blah.DisplayName, blah.IconPath ?? blah.ExecutablePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetProcessInfo(Process process, ActiveAudioSessionWrapper results)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//appx packages are installed from the windows store. eg, itunes
|
||||||
|
var appx = AppxPackage.FromProcess(process);
|
||||||
|
if (appx == null)
|
||||||
|
{
|
||||||
|
//using process.MainModule.FileVersionInfo sometimes throws permission exceptions
|
||||||
|
//we get the file version info with a limited query flag to avoid that
|
||||||
|
var fileVersionInfo = GetFileVersionInfo(process);
|
||||||
|
|
||||||
|
//if the display name is already set, then it came from the display name of the audio session
|
||||||
|
if (string.IsNullOrEmpty(results.DisplayName))
|
||||||
|
{
|
||||||
|
results.DisplayName = process.MainWindowTitle;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(results.DisplayName))
|
||||||
|
{
|
||||||
|
results.DisplayName = fileVersionInfo?.FileDescription;
|
||||||
|
if (string.IsNullOrEmpty(results.DisplayName))
|
||||||
|
{
|
||||||
|
results.DisplayName = process.ProcessName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results.ExecutablePath = fileVersionInfo?.FileName;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
results.DisplayName = appx.DisplayName;
|
||||||
|
results.IconPath = Path.Combine(appx.Path, appx.Logo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
//if anything threw an exception, set the display name to the process name, and just let the
|
||||||
|
// icon/executable path be blank and the stream deck will just show the default icon
|
||||||
|
if (string.IsNullOrEmpty(results.DisplayName))
|
||||||
|
{
|
||||||
|
results.DisplayName = process.ProcessName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileVersionInfo GetFileVersionInfo(Process process)
|
||||||
|
{
|
||||||
|
var path = GetExecutablePathWithPInvoke(process);
|
||||||
|
if (!string.IsNullOrEmpty(path))
|
||||||
|
{
|
||||||
|
return FileVersionInfo.GetVersionInfo(path);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
string GetExecutablePathWithPInvoke(Process process)
|
||||||
|
{
|
||||||
|
IntPtr processHandle = IntPtr.Zero;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string pathToExe = string.Empty;
|
||||||
|
|
||||||
|
if (process != null)
|
||||||
|
{
|
||||||
|
//use query limited information handle instead of process.handle to prevent permission errors
|
||||||
|
processHandle = Native.OpenProcess(0x00001000, false, process.Id);
|
||||||
|
|
||||||
|
var buffer = new StringBuilder(1024);
|
||||||
|
var bufferSize = (uint)buffer.Capacity + 1;
|
||||||
|
var success = Native.QueryFullProcessImageName(processHandle, 0, buffer, ref bufferSize);
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
return buffer.ToString();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var error = Marshal.GetLastWin32Error();
|
||||||
|
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Error = {error} getting process name");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if(processHandle != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
Native.CloseHandle(processHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -2,7 +2,7 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace FocusVolumeControl;
|
namespace FocusVolumeControl.AudioHelpers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A utility class to determine a process parent.
|
/// A utility class to determine a process parent.
|
@ -4,33 +4,58 @@ using System.Linq;
|
|||||||
using BarRaider.SdTools;
|
using BarRaider.SdTools;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using FocusVolumeControl.UI;
|
||||||
|
using BitFaster.Caching.Lru;
|
||||||
|
|
||||||
namespace FocusVolumeControl.AudioSessions;
|
namespace FocusVolumeControl.AudioSessions;
|
||||||
|
|
||||||
public sealed class ActiveAudioSessionWrapper : IAudioSession
|
public sealed class ActiveAudioSessionWrapper : IAudioSession
|
||||||
{
|
{
|
||||||
|
static ConcurrentLru<string, string> _iconCache = new ConcurrentLru<string, string>(10);
|
||||||
|
|
||||||
public string DisplayName { get; set; }
|
public string DisplayName { get; set; }
|
||||||
public string ExecutablePath { get; set; }
|
public string ExecutablePath { get; set; }
|
||||||
|
public string IconPath { get; set; }
|
||||||
private List<IAudioSessionControl2> Sessions { get; } = new List<IAudioSessionControl2>();
|
private List<IAudioSessionControl2> Sessions { get; } = new List<IAudioSessionControl2>();
|
||||||
private IEnumerable<ISimpleAudioVolume> Volume => Sessions.Cast<ISimpleAudioVolume>();
|
private IEnumerable<ISimpleAudioVolume> Volume => Sessions.Cast<ISimpleAudioVolume>();
|
||||||
|
|
||||||
string _icon;
|
string GetIconFromIconPath()
|
||||||
|
{
|
||||||
|
return _iconCache.GetOrAdd(IconPath, (key) =>
|
||||||
|
{
|
||||||
|
var tmp = (Bitmap)Bitmap.FromFile(IconPath);
|
||||||
|
tmp.MakeTransparent();
|
||||||
|
return Tools.ImageToBase64(tmp, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
string GetIconFromExecutablePath()
|
||||||
|
{
|
||||||
|
return _iconCache.GetOrAdd(ExecutablePath, (key) =>
|
||||||
|
{
|
||||||
|
var tmp = IconExtraction.GetIcon(ExecutablePath);
|
||||||
|
//var tmp = Icon.ExtractAssociatedIcon(ExecutablePath);
|
||||||
|
return Tools.ImageToBase64(tmp, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public string GetIcon()
|
public string GetIcon()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(_icon))
|
try
|
||||||
{
|
{
|
||||||
try
|
if (!string.IsNullOrEmpty(IconPath))
|
||||||
{
|
{
|
||||||
var tmp = Icon.ExtractAssociatedIcon(ExecutablePath);
|
return GetIconFromIconPath();
|
||||||
_icon = Tools.ImageToBase64(tmp.ToBitmap(), true);
|
|
||||||
}
|
}
|
||||||
catch
|
else
|
||||||
{
|
{
|
||||||
_icon = "Image/encoderIcon";
|
return GetIconFromExecutablePath();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return _icon;
|
catch
|
||||||
|
{
|
||||||
|
return "Images/encoderIcon";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Any()
|
public bool Any()
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Data;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace FocusVolumeControl.AudioSessions;
|
namespace FocusVolumeControl.AudioSessions;
|
||||||
@ -6,44 +7,66 @@ namespace FocusVolumeControl.AudioSessions;
|
|||||||
|
|
||||||
[ComImport]
|
[ComImport]
|
||||||
[Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")]
|
[Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")]
|
||||||
internal class MMDeviceEnumerator
|
public class MMDeviceEnumerator
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
internal enum EDataFlow
|
public enum DataFlow
|
||||||
{
|
{
|
||||||
eRender,
|
Render,
|
||||||
eCapture,
|
Capture,
|
||||||
eAll,
|
All,
|
||||||
EDataFlow_enum_count
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal enum ERole
|
public enum Role
|
||||||
{
|
{
|
||||||
eConsole,
|
Console,
|
||||||
eMultimedia,
|
Multimedia,
|
||||||
eCommunications,
|
Communications,
|
||||||
ERole_enum_count
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum DeviceState : uint
|
||||||
|
{
|
||||||
|
Active = 1 << 0,
|
||||||
|
Disabled = 1 << 1,
|
||||||
|
NotPresent = 1 << 2,
|
||||||
|
Unplugged = 1 << 3,
|
||||||
|
MaskAll = 0xFu
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Guid("0BD7A1BE-7A1A-44DB-8397-CC5392387B5E")]
|
||||||
|
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||||
|
public interface IMMDeviceCollection
|
||||||
|
{
|
||||||
|
[PreserveSig]
|
||||||
|
int GetCount(out int nDevices);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int Item(int nDevice, out IMMDevice Device);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||||
internal interface CoreAudio
|
public interface IMMDeviceEnumerator
|
||||||
{
|
{
|
||||||
int NotImpl1();
|
|
||||||
|
|
||||||
[PreserveSig]
|
[PreserveSig]
|
||||||
int GetDefaultAudioEndpoint(EDataFlow dataFlow, ERole role, out IMMDevice ppDevice);
|
int EnumAudioEndpoints(DataFlow dataFlow, DeviceState StateMask, out IMMDeviceCollection deviceCollection);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int GetDefaultAudioEndpoint(DataFlow dataFlow, Role role, out IMMDevice device);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
[Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||||
internal interface IMMDevice
|
public interface IMMDevice
|
||||||
{
|
{
|
||||||
[PreserveSig]
|
[PreserveSig]
|
||||||
int Activate(ref Guid iid, int dwClsCtx, IntPtr pActivationParams, [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface);
|
int Activate(ref Guid iid, int dwClsCtx, IntPtr pActivationParams, [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Guid("77AA99A0-1BD6-484F-8BC7-2C654C9A9B6F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
[Guid("77AA99A0-1BD6-484F-8BC7-2C654C9A9B6F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||||
internal interface IAudioSessionManager2
|
public interface IAudioSessionManager2
|
||||||
{
|
{
|
||||||
int NotImpl1();
|
int NotImpl1();
|
||||||
int NotImpl2();
|
int NotImpl2();
|
||||||
@ -53,7 +76,7 @@ internal interface IAudioSessionManager2
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Guid("E2F5BB11-0570-40CA-ACDD-3AA01277DEE8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
[Guid("E2F5BB11-0570-40CA-ACDD-3AA01277DEE8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||||
internal interface IAudioSessionEnumerator
|
public interface IAudioSessionEnumerator
|
||||||
{
|
{
|
||||||
[PreserveSig]
|
[PreserveSig]
|
||||||
int GetCount(out int SessionCount);
|
int GetCount(out int SessionCount);
|
||||||
@ -63,7 +86,7 @@ internal interface IAudioSessionEnumerator
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Guid("87CE5498-68D6-44E5-9215-6DA47EF883D8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
[Guid("87CE5498-68D6-44E5-9215-6DA47EF883D8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||||
internal interface ISimpleAudioVolume
|
public interface ISimpleAudioVolume
|
||||||
{
|
{
|
||||||
[PreserveSig]
|
[PreserveSig]
|
||||||
int SetMasterVolume(float fLevel, ref Guid EventContext);
|
int SetMasterVolume(float fLevel, ref Guid EventContext);
|
||||||
|
@ -10,6 +10,11 @@ namespace FocusVolumeControl.AudioSessions
|
|||||||
{
|
{
|
||||||
public static float GetAdjustedVolume(float startingVolume, int step, int ticks)
|
public static float GetAdjustedVolume(float startingVolume, int step, int ticks)
|
||||||
{
|
{
|
||||||
|
if(step <= 0)
|
||||||
|
{
|
||||||
|
step = 1;
|
||||||
|
}
|
||||||
|
|
||||||
var level = startingVolume;
|
var level = startingVolume;
|
||||||
|
|
||||||
level += 0.01f * step * ticks;
|
level += 0.01f * step * ticks;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using BarRaider.SdTools;
|
using BarRaider.SdTools;
|
||||||
using BarRaider.SdTools.Payloads;
|
using BarRaider.SdTools.Payloads;
|
||||||
|
using FocusVolumeControl.AudioHelpers;
|
||||||
using FocusVolumeControl.AudioSessions;
|
using FocusVolumeControl.AudioSessions;
|
||||||
using FocusVolumeControl.UI;
|
using FocusVolumeControl.UI;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
@ -26,20 +27,13 @@ public class DialAction : EncoderBase
|
|||||||
{
|
{
|
||||||
PluginSettings instance = new PluginSettings();
|
PluginSettings instance = new PluginSettings();
|
||||||
instance.FallbackBehavior = FallbackBehavior.SystemSounds;
|
instance.FallbackBehavior = FallbackBehavior.SystemSounds;
|
||||||
|
instance.StepSize = 1;
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private PluginSettings settings;
|
PluginSettings settings;
|
||||||
|
static AudioHelper _audioHelper = new AudioHelper();
|
||||||
IntPtr _foregroundWindowChangedEvent;
|
|
||||||
Native.WinEventDelegate _delegate;
|
|
||||||
|
|
||||||
AudioHelper _audioHelper = new AudioHelper();
|
|
||||||
|
|
||||||
Thread _thread;
|
|
||||||
Dispatcher _dispatcher;
|
|
||||||
|
|
||||||
UIState _previousState;
|
UIState _previousState;
|
||||||
|
|
||||||
public DialAction(ISDConnection connection, InitialPayload payload) : base(connection, payload)
|
public DialAction(ISDConnection connection, InitialPayload payload) : base(connection, payload)
|
||||||
@ -54,39 +48,28 @@ public class DialAction : EncoderBase
|
|||||||
settings = payload.Settings.ToObject<PluginSettings>();
|
settings = payload.Settings.ToObject<PluginSettings>();
|
||||||
}
|
}
|
||||||
|
|
||||||
_thread = new Thread(() =>
|
WindowChangedEventLoop.Instance.WindowChanged += WindowChanged;
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
Logger.Instance.LogMessage(TracingLevel.DEBUG, "Registering for events");
|
//just in case we fail to get the active session, don't prevent the plugin from launching
|
||||||
_delegate = new Native.WinEventDelegate(WinEventProc);
|
var session = _audioHelper.GetActiveSession(settings.FallbackBehavior);
|
||||||
_foregroundWindowChangedEvent = Native.RegisterForForegroundWindowChangedEvent(_delegate);
|
_ = UpdateStateIfNeeded(session);
|
||||||
|
}
|
||||||
Logger.Instance.LogMessage(TracingLevel.DEBUG, "Starting Dispatcher");
|
catch { }
|
||||||
_dispatcher = Dispatcher.CurrentDispatcher;
|
|
||||||
Dispatcher.Run();
|
|
||||||
Logger.Instance.LogMessage(TracingLevel.DEBUG, "Dispatcher Stopped");
|
|
||||||
});
|
|
||||||
_thread.SetApartmentState(ApartmentState.STA);
|
|
||||||
_thread.Start();
|
|
||||||
|
|
||||||
var session = _audioHelper.GetActiveSession(settings.FallbackBehavior);
|
|
||||||
_ = UpdateStateIfNeeded(session);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Dispose()
|
public override void Dispose()
|
||||||
{
|
{
|
||||||
Logger.Instance.LogMessage(TracingLevel.DEBUG, "Disposing");
|
//Logger.Instance.LogMessage(TracingLevel.DEBUG, "Disposing");
|
||||||
if (_foregroundWindowChangedEvent != IntPtr.Zero)
|
WindowChangedEventLoop.Instance.WindowChanged -= WindowChanged;
|
||||||
{
|
|
||||||
Native.UnhookWinEvent(_foregroundWindowChangedEvent);
|
|
||||||
}
|
|
||||||
_dispatcher.InvokeShutdown();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async void DialDown(DialPayload payload)
|
public override async void DialDown(DialPayload payload)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Down");
|
//Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Down");
|
||||||
await ToggleMuteAsync();
|
await ToggleMuteAsync();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -100,7 +83,7 @@ public class DialAction : EncoderBase
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Logger.Instance.LogMessage(TracingLevel.INFO, "Touch Press");
|
//Logger.Instance.LogMessage(TracingLevel.INFO, "Touch Press");
|
||||||
if (payload.IsLongPress)
|
if (payload.IsLongPress)
|
||||||
{
|
{
|
||||||
await ResetAllAsync();
|
await ResetAllAsync();
|
||||||
@ -120,7 +103,7 @@ public class DialAction : EncoderBase
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
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
|
||||||
var activeSession = _audioHelper.Current;
|
var activeSession = _audioHelper.Current;
|
||||||
if (activeSession != null)
|
if (activeSession != null)
|
||||||
@ -258,7 +241,7 @@ public class DialAction : EncoderBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
|
public void WindowChanged()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -266,7 +249,7 @@ public class DialAction : EncoderBase
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unexpected Error in DialDown:\n {ex}");
|
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unexpected Error in Window Down:\n {ex}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,8 +54,9 @@
|
|||||||
<Reference Include="WindowsBase" />
|
<Reference Include="WindowsBase" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Compile Include="AudioHelpers\AppxPackage.cs" />
|
||||||
<Compile Include="AudioSessions\ActiveAudioSessionWrapper.cs" />
|
<Compile Include="AudioSessions\ActiveAudioSessionWrapper.cs" />
|
||||||
<Compile Include="AudioHelper.cs" />
|
<Compile Include="AudioHelpers\AudioHelper.cs" />
|
||||||
<Compile Include="AudioSessions\CoreAudio.cs" />
|
<Compile Include="AudioSessions\CoreAudio.cs" />
|
||||||
<Compile Include="AudioSessions\VolumeHelpers.cs" />
|
<Compile Include="AudioSessions\VolumeHelpers.cs" />
|
||||||
<Compile Include="AudioSessions\SystemSoundsAudioSession.cs" />
|
<Compile Include="AudioSessions\SystemSoundsAudioSession.cs" />
|
||||||
@ -63,13 +64,16 @@
|
|||||||
<Compile Include="DialAction.cs" />
|
<Compile Include="DialAction.cs" />
|
||||||
<Compile Include="AudioSessions\IAudioSession.cs" />
|
<Compile Include="AudioSessions\IAudioSession.cs" />
|
||||||
<Compile Include="FallbackBehavior.cs" />
|
<Compile Include="FallbackBehavior.cs" />
|
||||||
|
<Compile Include="AudioHelpers\NameAndIconHelper.cs" />
|
||||||
|
<Compile Include="UI\IconExtraction.cs" />
|
||||||
<Compile Include="UI\ISDConnectionExtensions.cs" />
|
<Compile Include="UI\ISDConnectionExtensions.cs" />
|
||||||
<Compile Include="Native.cs" />
|
<Compile Include="Native.cs" />
|
||||||
<Compile Include="ParentProcessUtilities.cs" />
|
<Compile Include="AudioHelpers\ParentProcessUtilities.cs" />
|
||||||
<Compile Include="Program.cs" />
|
<Compile Include="Program.cs" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
<Compile Include="UI\UIState.cs" />
|
<Compile Include="UI\UIState.cs" />
|
||||||
<Compile Include="UI\ValueWithOpacity.cs" />
|
<Compile Include="UI\ValueWithOpacity.cs" />
|
||||||
|
<Compile Include="WindowChangedEventLoop.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="App.config" />
|
<None Include="App.config" />
|
||||||
@ -93,6 +97,9 @@
|
|||||||
</Content>
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="BitFaster.Caching">
|
||||||
|
<Version>2.2.1</Version>
|
||||||
|
</PackageReference>
|
||||||
<PackageReference Include="IsExternalInit">
|
<PackageReference Include="IsExternalInit">
|
||||||
<Version>1.0.3</Version>
|
<Version>1.0.3</Version>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
using System;
|
using FocusVolumeControl.AudioHelpers;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace FocusVolumeControl;
|
namespace FocusVolumeControl;
|
||||||
|
|
||||||
@ -59,4 +61,15 @@ public class Native
|
|||||||
[DllImport("ntdll.dll")]
|
[DllImport("ntdll.dll")]
|
||||||
public static extern int NtQueryInformationProcess(IntPtr processHandle, int processInformationClass, ref ParentProcessUtilities processInformation, int processInformationLength, out int returnLength);
|
public static extern int NtQueryInformationProcess(IntPtr processHandle, int processInformationClass, ref ParentProcessUtilities processInformation, int processInformationLength, out int returnLength);
|
||||||
|
|
||||||
|
|
||||||
|
[DllImport("Kernel32.dll")]
|
||||||
|
public static extern bool QueryFullProcessImageName(IntPtr hProcess, uint flags, StringBuilder buffer, ref uint bufferSize);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
public static extern IntPtr OpenProcess(uint processAccess, bool inheritHandle, int processId);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
public static extern bool CloseHandle(IntPtr hObject);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
<select class="sdpi-item-value sdProperty" id="fallbackBehavior" oninput="setSettings()">
|
<select class="sdpi-item-value sdProperty" id="fallbackBehavior" oninput="setSettings()">
|
||||||
<option value="0">System Sounds</option>
|
<option value="0">System Sounds</option>
|
||||||
<option value="1">Previous App</option>
|
<option value="1">Previous App</option>
|
||||||
<option value="2">Main System Volume</option>
|
<option value="2">Default Output Device Volume</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -38,7 +38,7 @@
|
|||||||
<p>If you look at windows volume mixer, you will see that not all applications can have their volume controlled. The fallback behavior controls what happens when you are in an application that doesn't show up in the volume mixer</p>
|
<p>If you look at windows volume mixer, you will see that not all applications can have their volume controlled. The fallback behavior controls what happens when you are in an application that doesn't show up in the volume mixer</p>
|
||||||
<p>* System Sounds - Switch to system sounds. This will control windows sound effects such as when an error sound plays. If you're in an application that is making beeping sounds, this will often allow you to control those sounds while leaving things like your music/videos alone</p>
|
<p>* System Sounds - Switch to system sounds. This will control windows sound effects such as when an error sound plays. If you're in an application that is making beeping sounds, this will often allow you to control those sounds while leaving things like your music/videos alone</p>
|
||||||
<p>* Previous App - Use the last app that had a volume control. This can result in the stream deck not changing after you have quit an application.</p>
|
<p>* Previous App - Use the last app that had a volume control. This can result in the stream deck not changing after you have quit an application.</p>
|
||||||
<p>* Main System Volume - Switch to the main volume control for the system. This will change the volume of all applications</p>
|
<p>* Default Output Device Volume - Switch to the main volume control for the default output device. This will change the volume of the default output device. This is usually volume for all applications, unless you override the output device for specific applications.</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
267
src/FocusVolumeControl/UI/IconExtraction.cs
Normal file
267
src/FocusVolumeControl/UI/IconExtraction.cs
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace FocusVolumeControl.UI
|
||||||
|
{
|
||||||
|
internal class IconExtraction
|
||||||
|
{
|
||||||
|
public static Bitmap GetIcon(string path)
|
||||||
|
{
|
||||||
|
var index = GetIconIndex(path);
|
||||||
|
var handle = GetIconHandle(index);
|
||||||
|
|
||||||
|
using var icon = (Icon)Icon.FromHandle(handle).Clone();
|
||||||
|
|
||||||
|
Shell32.DestroyIcon(handle);
|
||||||
|
return icon.ToBitmap();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int GetIconIndex(string pszFile)
|
||||||
|
{
|
||||||
|
SHFILEINFO sfi = new SHFILEINFO();
|
||||||
|
Shell32.SHGetFileInfo(pszFile, 0, ref sfi, (uint)Marshal.SizeOf(sfi), (uint)(SHGFI.SysIconIndex | SHGFI.LargeIcon | SHGFI.UseFileAttributes));
|
||||||
|
return sfi.iIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 256*256
|
||||||
|
static IntPtr GetIconHandle(int iImage)
|
||||||
|
{
|
||||||
|
IImageList spiml = null;
|
||||||
|
Guid guil = new Guid(IID_IImageList2);//or IID_IImageList
|
||||||
|
|
||||||
|
Shell32.SHGetImageList(Shell32.SHIL_EXTRALARGE, ref guil, ref spiml);
|
||||||
|
IntPtr hIcon = IntPtr.Zero;
|
||||||
|
spiml.GetIcon(iImage, Shell32.ILD_TRANSPARENT | Shell32.ILD_IMAGE, ref hIcon);
|
||||||
|
|
||||||
|
return hIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
const string IID_IImageList = "46EB5926-582E-4017-9FDF-E8998DAA0950";
|
||||||
|
const string IID_IImageList2 = "192B9D83-50FC-457B-90A0-2B82A8B5DAE1";
|
||||||
|
|
||||||
|
public static class Shell32
|
||||||
|
{
|
||||||
|
|
||||||
|
public const int SHIL_LARGE = 0x0;
|
||||||
|
public const int SHIL_SMALL = 0x1;
|
||||||
|
public const int SHIL_EXTRALARGE = 0x2;
|
||||||
|
public const int SHIL_SYSSMALL = 0x3;
|
||||||
|
public const int SHIL_JUMBO = 0x4;
|
||||||
|
public const int SHIL_LAST = 0x4;
|
||||||
|
|
||||||
|
public const int ILD_TRANSPARENT = 0x00000001;
|
||||||
|
public const int ILD_IMAGE = 0x00000020;
|
||||||
|
|
||||||
|
[DllImport("shell32.dll", EntryPoint = "#727")]
|
||||||
|
public extern static int SHGetImageList(int iImageList, ref Guid riid, ref IImageList ppv);
|
||||||
|
|
||||||
|
[DllImport("user32.dll", EntryPoint = "DestroyIcon", SetLastError = true)]
|
||||||
|
public static extern int DestroyIcon(IntPtr hIcon);
|
||||||
|
|
||||||
|
//[DllImport("shell32.dll")]
|
||||||
|
//public static extern uint SHGetIDListFromObject([MarshalAs(UnmanagedType.IUnknown)] object iUnknown, out IntPtr ppidl);
|
||||||
|
|
||||||
|
[DllImport("Shell32.dll")]
|
||||||
|
public static extern IntPtr SHGetFileInfo(string pszPath, uint dwFileAttributes, ref SHFILEINFO psfi, uint cbFileInfo, uint uFlags);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
enum SHGFI : uint
|
||||||
|
{
|
||||||
|
/// <summary>get icon</summary>
|
||||||
|
Icon = 0x000000100,
|
||||||
|
/// <summary>get display name</summary>
|
||||||
|
DisplayName = 0x000000200,
|
||||||
|
/// <summary>get type name</summary>
|
||||||
|
TypeName = 0x000000400,
|
||||||
|
/// <summary>get attributes</summary>
|
||||||
|
Attributes = 0x000000800,
|
||||||
|
/// <summary>get icon location</summary>
|
||||||
|
IconLocation = 0x000001000,
|
||||||
|
/// <summary>return exe type</summary>
|
||||||
|
ExeType = 0x000002000,
|
||||||
|
/// <summary>get system icon index</summary>
|
||||||
|
SysIconIndex = 0x000004000,
|
||||||
|
/// <summary>put a link overlay on icon</summary>
|
||||||
|
LinkOverlay = 0x000008000,
|
||||||
|
/// <summary>show icon in selected state</summary>
|
||||||
|
Selected = 0x000010000,
|
||||||
|
/// <summary>get only specified attributes</summary>
|
||||||
|
Attr_Specified = 0x000020000,
|
||||||
|
/// <summary>get large icon</summary>
|
||||||
|
LargeIcon = 0x000000000,
|
||||||
|
/// <summary>get small icon</summary>
|
||||||
|
SmallIcon = 0x000000001,
|
||||||
|
/// <summary>get open icon</summary>
|
||||||
|
OpenIcon = 0x000000002,
|
||||||
|
/// <summary>get shell size icon</summary>
|
||||||
|
ShellIconSize = 0x000000004,
|
||||||
|
/// <summary>pszPath is a pidl</summary>
|
||||||
|
PIDL = 0x000000008,
|
||||||
|
/// <summary>use passed dwFileAttribute</summary>
|
||||||
|
UseFileAttributes = 0x000000010,
|
||||||
|
/// <summary>apply the appropriate overlays</summary>
|
||||||
|
AddOverlays = 0x000000020,
|
||||||
|
/// <summary>Get the index of the overlay in the upper 8 bits of the iIcon</summary>
|
||||||
|
OverlayIndex = 0x000000040,
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct SHFILEINFO
|
||||||
|
{
|
||||||
|
public const int NAMESIZE = 80;
|
||||||
|
public IntPtr hIcon;
|
||||||
|
public int iIcon;
|
||||||
|
public uint dwAttributes;
|
||||||
|
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
|
||||||
|
public string szDisplayName;
|
||||||
|
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]
|
||||||
|
public string szTypeName;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct RECT
|
||||||
|
{
|
||||||
|
public int left, top, right, bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct POINT
|
||||||
|
{
|
||||||
|
int x;
|
||||||
|
int y;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct IMAGELISTDRAWPARAMS
|
||||||
|
{
|
||||||
|
public int cbSize;
|
||||||
|
public IntPtr himl;
|
||||||
|
public int i;
|
||||||
|
public IntPtr hdcDst;
|
||||||
|
public int x;
|
||||||
|
public int y;
|
||||||
|
public int cx;
|
||||||
|
public int cy;
|
||||||
|
public int xBitmap; // x offest from the upperleft of bitmap
|
||||||
|
public int yBitmap; // y offset from the upperleft of bitmap
|
||||||
|
public int rgbBk;
|
||||||
|
public int rgbFg;
|
||||||
|
public int fStyle;
|
||||||
|
public int dwRop;
|
||||||
|
public int fState;
|
||||||
|
public int Frame;
|
||||||
|
public int crEffect;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
public struct IMAGEINFO
|
||||||
|
{
|
||||||
|
public IntPtr hbmImage;
|
||||||
|
public IntPtr hbmMask;
|
||||||
|
public int Unused1;
|
||||||
|
public int Unused2;
|
||||||
|
public RECT rcImage;
|
||||||
|
}
|
||||||
|
[ComImportAttribute()]
|
||||||
|
[GuidAttribute("46EB5926-582E-4017-9FDF-E8998DAA0950")]
|
||||||
|
[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
|
||||||
|
public interface IImageList
|
||||||
|
{
|
||||||
|
[PreserveSig]
|
||||||
|
int Add(IntPtr hbmImage, IntPtr hbmMask, ref int pi);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int ReplaceIcon(int i, IntPtr hicon, ref int pi);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int SetOverlayImage(int iImage, int iOverlay);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int Replace(int i, IntPtr hbmImage, IntPtr hbmMask);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int AddMasked(IntPtr hbmImage, int crMask, ref int pi);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int Draw(ref IMAGELISTDRAWPARAMS pimldp);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int Remove(int i);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int GetIcon(int i, int flags, ref IntPtr picon);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int GetImageInfo(int i, ref IMAGEINFO pImageInfo);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int Copy(int iDst, IImageList punkSrc, int iSrc, int uFlags);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int Merge(int i1, IImageList punk2, int i2, int dx, int dy, ref Guid riid, ref IntPtr ppv);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int Clone(ref Guid riid, ref IntPtr ppv);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int GetImageRect(int i, ref RECT prc);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int GetIconSize(ref int cx, ref int cy);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int SetIconSize(int cx, int cy);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int GetImageCount(ref int pi);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int SetImageCount(int uNewCount);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int SetBkColor(int clrBk, ref int pclr);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int GetBkColor(ref int pclr);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int BeginDrag(int iTrack, int dxHotspot, int dyHotspot);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int EndDrag();
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int DragEnter(IntPtr hwndLock, int x, int y);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int DragLeave(IntPtr hwndLock);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int DragMove(int x, int y);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int SetDragCursorImage(ref IImageList punk, int iDrag, int dxHotspot, int dyHotspot);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int DragShowNolock(int fShow);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int GetDragImage(ref POINT ppt, ref POINT pptHotspot, ref Guid riid, ref IntPtr ppv);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int GetItemFlags(int i, ref int dwFlags);
|
||||||
|
|
||||||
|
[PreserveSig]
|
||||||
|
int GetOverlayImage(int iOverlay, ref int piIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
66
src/FocusVolumeControl/WindowChangedEventLoop.cs
Normal file
66
src/FocusVolumeControl/WindowChangedEventLoop.cs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
using BarRaider.SdTools;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
|
||||||
|
namespace FocusVolumeControl
|
||||||
|
{
|
||||||
|
internal class WindowChangedEventLoop
|
||||||
|
{
|
||||||
|
private static readonly Lazy<WindowChangedEventLoop> _lazy = new Lazy<WindowChangedEventLoop>(() => new WindowChangedEventLoop());
|
||||||
|
public static WindowChangedEventLoop Instance => _lazy.Value;
|
||||||
|
|
||||||
|
readonly Thread _thread;
|
||||||
|
Dispatcher _dispatcher;
|
||||||
|
|
||||||
|
IntPtr _foregroundWindowChangedEvent;
|
||||||
|
Native.WinEventDelegate _delegate;
|
||||||
|
|
||||||
|
private WindowChangedEventLoop()
|
||||||
|
{
|
||||||
|
_thread = new Thread(() =>
|
||||||
|
{
|
||||||
|
Logger.Instance.LogMessage(TracingLevel.DEBUG, "Starting Window Changed Event Loop");
|
||||||
|
_delegate = new Native.WinEventDelegate(WinEventProc);
|
||||||
|
_foregroundWindowChangedEvent = Native.RegisterForForegroundWindowChangedEvent(_delegate);
|
||||||
|
|
||||||
|
_dispatcher = Dispatcher.CurrentDispatcher;
|
||||||
|
Dispatcher.Run();
|
||||||
|
Logger.Instance.LogMessage(TracingLevel.DEBUG, "Window Changed Event Loop Stopped");
|
||||||
|
});
|
||||||
|
_thread.SetApartmentState(ApartmentState.STA);
|
||||||
|
_thread.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public event Action WindowChanged;
|
||||||
|
|
||||||
|
CancellationTokenSource? _cancellationTokenSource = null;
|
||||||
|
|
||||||
|
private async void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//debounce the window changed events by 100 ms because if you click mouse over an application on the start bar
|
||||||
|
//and then click on the preview window, it will quickly go from current -> fallback -> new app
|
||||||
|
//which can often result in it getting stuck on the fallback app
|
||||||
|
_cancellationTokenSource?.Cancel();
|
||||||
|
_cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
await Task.Delay(100, _cancellationTokenSource.Token);
|
||||||
|
WindowChanged?.Invoke();
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
//ignored
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unexpected Error in EventHandler:\n {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -33,7 +33,7 @@
|
|||||||
"Name": "Focused Application Volume",
|
"Name": "Focused Application Volume",
|
||||||
"Description": "Control the volume of the focused application",
|
"Description": "Control the volume of the focused application",
|
||||||
"URL": "https://github.com/dlprows/FocusVolumeControl",
|
"URL": "https://github.com/dlprows/FocusVolumeControl",
|
||||||
"Version": "1.1.1",
|
"Version": "1.2.0",
|
||||||
"CodePath": "FocusVolumeControl",
|
"CodePath": "FocusVolumeControl",
|
||||||
"Category": "Volume Control [dlprows]",
|
"Category": "Volume Control [dlprows]",
|
||||||
"Icon": "Images/pluginIcon",
|
"Icon": "Images/pluginIcon",
|
||||||
|
@ -6,14 +6,16 @@
|
|||||||
xmlns:local="clr-namespace:SoundBrowser"
|
xmlns:local="clr-namespace:SoundBrowser"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="MainWindow" Height="800" Width="800">
|
Title="MainWindow" Height="800" Width="800">
|
||||||
<Grid>
|
<ScrollViewer>
|
||||||
<Grid.RowDefinitions>
|
<Grid>
|
||||||
<RowDefinition Height="auto"/>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="*"/>
|
<RowDefinition Height="auto"/>
|
||||||
</Grid.RowDefinitions>
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<TextBlock x:Name="_tf" Grid.Row="0">current</TextBlock>
|
<TextBlock x:Name="_tf" Grid.Row="0">current</TextBlock>
|
||||||
<TextBlock x:Name="_tf2" Grid.Row="1">list</TextBlock>
|
<TextBlock x:Name="_tf2" Grid.Row="1">list</TextBlock>
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
</ScrollViewer>
|
||||||
</Window>
|
</Window>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using CoreAudio;
|
using FocusVolumeControl;
|
||||||
using FocusVolumeControl;
|
using FocusVolumeControl.AudioHelpers;
|
||||||
|
using FocusVolumeControl.AudioSessions;
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@ -56,18 +57,11 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
foreach (var p in processes)
|
foreach (var p in processes)
|
||||||
{
|
{
|
||||||
|
var (displayName, _) = (new NameAndIconHelper()).GetProcessInfo(p);
|
||||||
|
|
||||||
sb.AppendLine($"pid: {p.Id}");
|
sb.AppendLine($"pid: {p.Id}");
|
||||||
sb.AppendLine($"\tprocessName: {p.ProcessName}");
|
sb.AppendLine($"\tprocessName: {p.ProcessName}");
|
||||||
try
|
sb.AppendLine($"\tDisplayName: {displayName}");
|
||||||
{
|
|
||||||
sb.AppendLine($"\tFileDescription: {p!.MainModule!.FileVersionInfo.FileDescription}");
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
sb.AppendLine("\tFileDescription: ##ERROR##");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,26 +87,39 @@ public partial class MainWindow : Window
|
|||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.AppendLine("-------------------------------------------------------------------------------");
|
sb.AppendLine("-------------------------------------------------------------------------------");
|
||||||
|
|
||||||
var deviceEnumerator = new MMDeviceEnumerator(Guid.NewGuid());
|
var deviceEnumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator();
|
||||||
|
|
||||||
using var device = deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
|
deviceEnumerator.EnumAudioEndpoints(DataFlow.Render, DeviceState.Active, out var deviceCollection);
|
||||||
using var manager = device.AudioSessionManager2;
|
deviceCollection.GetCount(out var num);
|
||||||
|
|
||||||
var sessions = manager!.Sessions;
|
for(int i = 0; i < num; i++)
|
||||||
|
|
||||||
foreach (var session in sessions!)
|
|
||||||
{
|
{
|
||||||
var audioProcess = Process.GetProcessById((int)session.ProcessID);
|
deviceCollection.Item(i, out var device);
|
||||||
|
//todo: put the device name in the output
|
||||||
|
sb.AppendLine("----");
|
||||||
|
|
||||||
var displayName = audioProcess!.MainModule!.FileVersionInfo.FileDescription;
|
Guid iid = typeof(IAudioSessionManager2).GUID;
|
||||||
|
device.Activate(ref iid, 0, IntPtr.Zero, out var m);
|
||||||
|
var manager = (IAudioSessionManager2)m;
|
||||||
|
|
||||||
sb.AppendLine($"pid: {audioProcess.Id}");
|
|
||||||
sb.AppendLine($"\tprocessName: {audioProcess.ProcessName}");
|
manager.GetSessionEnumerator(out var sessionEnumerator);
|
||||||
sb.AppendLine($"\tsession: {displayName}");
|
|
||||||
|
|
||||||
|
sessionEnumerator.GetCount(out var count);
|
||||||
|
for (int s = 0; s < count; s++)
|
||||||
|
{
|
||||||
|
sessionEnumerator.GetSession(s, out var session);
|
||||||
|
|
||||||
|
session.GetProcessId(out var processId);
|
||||||
|
var audioProcess = Process.GetProcessById(processId);
|
||||||
|
|
||||||
|
var (displayName, _) = (new NameAndIconHelper()).GetProcessInfo(audioProcess);
|
||||||
|
sb.AppendLine($"pid: {audioProcess.Id}\t\t processName: {displayName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
_tf2.Text = sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
_tf2.Text = sb.ToString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -7,10 +7,6 @@
|
|||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="CoreAudio" Version="1.27.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\FocusVolumeControl\FocusVolumeControl.csproj" />
|
<ProjectReference Include="..\FocusVolumeControl\FocusVolumeControl.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
Reference in New Issue
Block a user