Compare commits
20 Commits
v1.0.1
...
v1.2.0-pre
Author | SHA1 | Date | |
---|---|---|---|
4c1ccd9025 | |||
2b10b6d7a6 | |||
ca634f8d3c | |||
bbb0e55ed6 | |||
520659ac52 | |||
609a7bdb65 | |||
13fdfde3e5 | |||
bbad79b4f3 | |||
708180dc8e | |||
5711ace990 | |||
d89c8b1ffa | |||
f94052e54b | |||
ceb3494e43 | |||
f9b23a62a3 | |||
06266daa92 | |||
1fea2a2e11 | |||
2e44a27b2b | |||
84a9a89074 | |||
b57ea24b11 | |||
0d056215bc |
@ -1,180 +0,0 @@
|
||||
using CoreAudio;
|
||||
using FocusVolumeControl.AudioSessions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace FocusVolumeControl;
|
||||
|
||||
public class AudioHelper
|
||||
{
|
||||
IAudioSession _current;
|
||||
List<Process> _currentProcesses;
|
||||
|
||||
public IAudioSession FindSession(List<Process> processes)
|
||||
{
|
||||
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 matchingSession = new ActiveAudioSessionWrapper();
|
||||
|
||||
foreach (var session in sessions)
|
||||
{
|
||||
var audioProcess = Process.GetProcessById((int)session.ProcessID);
|
||||
|
||||
if (processes.Any(x => x.Id == session.ProcessID || x.ProcessName == audioProcess?.ProcessName))
|
||||
{
|
||||
try
|
||||
{
|
||||
var displayName = audioProcess.MainModule.FileVersionInfo.FileDescription;
|
||||
if (string.IsNullOrEmpty(displayName))
|
||||
{
|
||||
displayName = audioProcess.ProcessName;
|
||||
}
|
||||
matchingSession.DisplayName = displayName;
|
||||
}
|
||||
catch
|
||||
{
|
||||
matchingSession.DisplayName ??= audioProcess.ProcessName;
|
||||
}
|
||||
|
||||
matchingSession.ExecutablePath ??= audioProcess.MainModule.FileName;
|
||||
|
||||
//some apps like discord have multiple volume processes.
|
||||
matchingSession.AddVolume(session.SimpleAudioVolume);
|
||||
}
|
||||
}
|
||||
return matchingSession.Any() ? matchingSession : null;
|
||||
}
|
||||
|
||||
static object _lock = new object();
|
||||
|
||||
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 = GetSystemSounds();
|
||||
}
|
||||
else if(fallbackBehavior == FallbackBehavior.SystemVolume)
|
||||
{
|
||||
_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()
|
||||
{
|
||||
try
|
||||
{
|
||||
var deviceEnumerator = new MMDeviceEnumerator(Guid.NewGuid());
|
||||
|
||||
using var device = deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
|
||||
using var manager = device.AudioSessionManager2;
|
||||
|
||||
foreach (var session in manager.Sessions)
|
||||
{
|
||||
session.SimpleAudioVolume.MasterVolume = 1;
|
||||
session.SimpleAudioVolume.Mute = false;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public IAudioSession GetSystemSounds()
|
||||
{
|
||||
var deviceEnumerator = new MMDeviceEnumerator(Guid.NewGuid());
|
||||
|
||||
using var device = deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
|
||||
using var manager = device.AudioSessionManager2;
|
||||
|
||||
var sessions = manager.Sessions;
|
||||
|
||||
foreach (var session in sessions)
|
||||
{
|
||||
if (session.IsSystemSoundsSession)
|
||||
{
|
||||
return new SystemSoundsAudioSession(session.SimpleAudioVolume);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
public IAudioSession GetSystemVolume()
|
||||
{
|
||||
var deviceEnumerator = new MMDeviceEnumerator(Guid.NewGuid());
|
||||
|
||||
using var device = deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
|
||||
return new SystemVolumeAudioSession(device.AudioEndpointVolume);
|
||||
}
|
||||
|
||||
}
|
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
|
||||
}
|
255
src/FocusVolumeControl/AudioHelpers/AudioHelper.cs
Normal file
255
src/FocusVolumeControl/AudioHelpers/AudioHelper.cs
Normal file
@ -0,0 +1,255 @@
|
||||
using BarRaider.SdTools;
|
||||
using FocusVolumeControl.AudioSessions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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 = Process.GetProcessById(sessionProcessId);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
//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 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
|
||||
{
|
||||
//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);
|
||||
}
|
||||
|
||||
}
|
116
src/FocusVolumeControl/AudioHelpers/NameAndIconHelper.cs
Normal file
116
src/FocusVolumeControl/AudioHelpers/NameAndIconHelper.cs
Normal file
@ -0,0 +1,116 @@
|
||||
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)
|
||||
{
|
||||
//usingg 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);
|
||||
|
||||
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.Runtime.InteropServices;
|
||||
|
||||
namespace FocusVolumeControl;
|
||||
namespace FocusVolumeControl.AudioHelpers;
|
||||
|
||||
/// <summary>
|
||||
/// A utility class to determine a process parent.
|
@ -1,17 +1,19 @@
|
||||
using CoreAudio;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BarRaider.SdTools;
|
||||
using System.Drawing;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace FocusVolumeControl.AudioSessions;
|
||||
|
||||
public class ActiveAudioSessionWrapper : IAudioSession
|
||||
public sealed class ActiveAudioSessionWrapper : IAudioSession
|
||||
{
|
||||
public string DisplayName { get; set; }
|
||||
public string ExecutablePath { get; set; }
|
||||
private List<SimpleAudioVolume> Volume { get; } = new List<SimpleAudioVolume>();
|
||||
public string IconPath { get; set; }
|
||||
private List<IAudioSessionControl2> Sessions { get; } = new List<IAudioSessionControl2>();
|
||||
private IEnumerable<ISimpleAudioVolume> Volume => Sessions.Cast<ISimpleAudioVolume>();
|
||||
|
||||
string _icon;
|
||||
|
||||
@ -21,12 +23,21 @@ public class ActiveAudioSessionWrapper : IAudioSession
|
||||
{
|
||||
try
|
||||
{
|
||||
var tmp = Icon.ExtractAssociatedIcon(ExecutablePath);
|
||||
_icon = Tools.ImageToBase64(tmp.ToBitmap(), true);
|
||||
if(!string.IsNullOrEmpty(IconPath))
|
||||
{
|
||||
var tmp = (Bitmap)Bitmap.FromFile(IconPath);
|
||||
tmp.MakeTransparent();
|
||||
_icon = Tools.ImageToBase64(tmp, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
var tmp = Icon.ExtractAssociatedIcon(ExecutablePath);
|
||||
_icon = Tools.ImageToBase64(tmp.ToBitmap(), true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
_icon = "Image/encoderIcon";
|
||||
_icon = "Images/encoderIcon";
|
||||
}
|
||||
}
|
||||
return _icon;
|
||||
@ -36,11 +47,11 @@ public class ActiveAudioSessionWrapper : IAudioSession
|
||||
{
|
||||
return Volume.Any();
|
||||
}
|
||||
public int Count => Volume.Count;
|
||||
public int Count => Sessions.Count;
|
||||
|
||||
public void AddVolume(SimpleAudioVolume volume)
|
||||
public void AddSession(IAudioSessionControl2 session)
|
||||
{
|
||||
Volume.Add(volume);
|
||||
Sessions.Add(session);
|
||||
}
|
||||
|
||||
public void ToggleMute()
|
||||
@ -51,32 +62,52 @@ public class ActiveAudioSessionWrapper : IAudioSession
|
||||
//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);
|
||||
var muted = IsMuted();
|
||||
|
||||
Volume.ForEach(x => x.Mute = !muted);
|
||||
foreach(var v in Volume)
|
||||
{
|
||||
var guid = Guid.Empty;
|
||||
v.SetMute(!muted, ref guid);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsMuted()
|
||||
{
|
||||
return Volume.All(x => x.Mute);
|
||||
return Volume.All(x =>
|
||||
{
|
||||
x.GetMute(out var mute);
|
||||
return mute;
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
var volume = Volume.FirstOrDefault();
|
||||
var level = 0f;
|
||||
if (volume != null)
|
||||
{
|
||||
volume.GetMasterVolume(out level);
|
||||
}
|
||||
|
||||
level = VolumeHelpers.GetAdjustedVolume(level, step, ticks);
|
||||
|
||||
level += (0.01f * step) * ticks;
|
||||
level = Math.Max(level, 0);
|
||||
level = Math.Min(level, 1);
|
||||
|
||||
Volume.ForEach(x => x.MasterVolume = level);
|
||||
foreach(var v in Volume)
|
||||
{
|
||||
var guid = Guid.Empty;
|
||||
v.SetMasterVolume(level, ref guid);
|
||||
}
|
||||
}
|
||||
|
||||
public int GetVolumeLevel()
|
||||
{
|
||||
var level = Volume.FirstOrDefault()?.MasterVolume ?? 0;
|
||||
return (int)(level * 100);
|
||||
}
|
||||
var volume = Volume.FirstOrDefault();
|
||||
var level = 0f;
|
||||
if(volume != null)
|
||||
{
|
||||
volume.GetMasterVolume(out level);
|
||||
}
|
||||
|
||||
return VolumeHelpers.GetVolumePercentage(level);
|
||||
}
|
||||
}
|
||||
|
329
src/FocusVolumeControl/AudioSessions/CoreAudio.cs
Normal file
329
src/FocusVolumeControl/AudioSessions/CoreAudio.cs
Normal file
@ -0,0 +1,329 @@
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace FocusVolumeControl.AudioSessions;
|
||||
|
||||
|
||||
[ComImport]
|
||||
[Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")]
|
||||
public class MMDeviceEnumerator
|
||||
{
|
||||
}
|
||||
|
||||
public enum DataFlow
|
||||
{
|
||||
Render,
|
||||
Capture,
|
||||
All,
|
||||
}
|
||||
|
||||
public enum Role
|
||||
{
|
||||
Console,
|
||||
Multimedia,
|
||||
Communications,
|
||||
}
|
||||
|
||||
[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)]
|
||||
public interface IMMDeviceEnumerator
|
||||
{
|
||||
|
||||
[PreserveSig]
|
||||
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)]
|
||||
public interface IMMDevice
|
||||
{
|
||||
[PreserveSig]
|
||||
int Activate(ref Guid iid, int dwClsCtx, IntPtr pActivationParams, [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface);
|
||||
}
|
||||
|
||||
[Guid("77AA99A0-1BD6-484F-8BC7-2C654C9A9B6F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IAudioSessionManager2
|
||||
{
|
||||
int NotImpl1();
|
||||
int NotImpl2();
|
||||
|
||||
[PreserveSig]
|
||||
int GetSessionEnumerator(out IAudioSessionEnumerator SessionEnum);
|
||||
}
|
||||
|
||||
[Guid("E2F5BB11-0570-40CA-ACDD-3AA01277DEE8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IAudioSessionEnumerator
|
||||
{
|
||||
[PreserveSig]
|
||||
int GetCount(out int SessionCount);
|
||||
|
||||
[PreserveSig]
|
||||
int GetSession(int SessionCount, out IAudioSessionControl2 Session);
|
||||
}
|
||||
|
||||
[Guid("87CE5498-68D6-44E5-9215-6DA47EF883D8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface ISimpleAudioVolume
|
||||
{
|
||||
[PreserveSig]
|
||||
int SetMasterVolume(float fLevel, ref Guid EventContext);
|
||||
|
||||
[PreserveSig]
|
||||
int GetMasterVolume(out float pfLevel);
|
||||
|
||||
[PreserveSig]
|
||||
int SetMute(bool bMute, ref Guid EventContext);
|
||||
|
||||
[PreserveSig]
|
||||
int GetMute(out bool pbMute);
|
||||
}
|
||||
|
||||
[Guid("bfb7ff88-7239-4fc9-8fa2-07c950be9c6d"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IAudioSessionControl2
|
||||
{
|
||||
// IAudioSessionControl
|
||||
[PreserveSig]
|
||||
int NotImpl0();
|
||||
|
||||
[PreserveSig]
|
||||
int GetDisplayName([MarshalAs(UnmanagedType.LPWStr)] out string pRetVal);
|
||||
|
||||
[PreserveSig]
|
||||
int SetDisplayName([MarshalAs(UnmanagedType.LPWStr)] string Value, [MarshalAs(UnmanagedType.LPStruct)] Guid EventContext);
|
||||
|
||||
[PreserveSig]
|
||||
int GetIconPath([MarshalAs(UnmanagedType.LPWStr)] out string pRetVal);
|
||||
|
||||
[PreserveSig]
|
||||
int SetIconPath([MarshalAs(UnmanagedType.LPWStr)] string Value, [MarshalAs(UnmanagedType.LPStruct)] Guid EventContext);
|
||||
|
||||
[PreserveSig]
|
||||
int GetGroupingParam(out Guid pRetVal);
|
||||
|
||||
[PreserveSig]
|
||||
int SetGroupingParam([MarshalAs(UnmanagedType.LPStruct)] Guid Override, [MarshalAs(UnmanagedType.LPStruct)] Guid EventContext);
|
||||
|
||||
[PreserveSig]
|
||||
int NotImpl1();
|
||||
|
||||
[PreserveSig]
|
||||
int NotImpl2();
|
||||
|
||||
// IAudioSessionControl2
|
||||
[PreserveSig]
|
||||
int GetSessionIdentifier([MarshalAs(UnmanagedType.LPWStr)] out string pRetVal);
|
||||
|
||||
[PreserveSig]
|
||||
int GetSessionInstanceIdentifier([MarshalAs(UnmanagedType.LPWStr)] out string pRetVal);
|
||||
|
||||
[PreserveSig]
|
||||
int GetProcessId(out int pRetVal);
|
||||
|
||||
[PreserveSig]
|
||||
int IsSystemSoundsSession();
|
||||
|
||||
[PreserveSig]
|
||||
int SetDuckingPreference(bool optOut);
|
||||
}
|
||||
|
||||
// http://netcoreaudio.codeplex.com/SourceControl/latest#trunk/Code/CoreAudio/Interfaces/IAudioEndpointVolume.cs
|
||||
[Guid("5CDF2C82-841E-4546-9722-0CF74078229A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IAudioEndpointVolume
|
||||
{
|
||||
[PreserveSig]
|
||||
int NotImpl1();
|
||||
|
||||
[PreserveSig]
|
||||
int NotImpl2();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a count of the channels in the audio stream.
|
||||
/// </summary>
|
||||
/// <param name="channelCount">The number of channels.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int GetChannelCount(
|
||||
[Out][MarshalAs(UnmanagedType.U4)] out UInt32 channelCount);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the master volume level of the audio stream, in decibels.
|
||||
/// </summary>
|
||||
/// <param name="level">The new master volume level in decibels.</param>
|
||||
/// <param name="eventContext">A user context value that is passed to the notification callback.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int SetMasterVolumeLevel(
|
||||
[In][MarshalAs(UnmanagedType.R4)] float level,
|
||||
[In][MarshalAs(UnmanagedType.LPStruct)] Guid eventContext);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the master volume level, expressed as a normalized, audio-tapered value.
|
||||
/// </summary>
|
||||
/// <param name="level">The new master volume level expressed as a normalized value between 0.0 and 1.0.</param>
|
||||
/// <param name="eventContext">A user context value that is passed to the notification callback.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int SetMasterVolumeLevelScalar(
|
||||
[In][MarshalAs(UnmanagedType.R4)] float level,
|
||||
[In][MarshalAs(UnmanagedType.LPStruct)] Guid eventContext);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the master volume level of the audio stream, in decibels.
|
||||
/// </summary>
|
||||
/// <param name="level">The volume level in decibels.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int GetMasterVolumeLevel(
|
||||
[Out][MarshalAs(UnmanagedType.R4)] out float level);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the master volume level, expressed as a normalized, audio-tapered value.
|
||||
/// </summary>
|
||||
/// <param name="level">The volume level expressed as a normalized value between 0.0 and 1.0.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int GetMasterVolumeLevelScalar(
|
||||
[Out][MarshalAs(UnmanagedType.R4)] out float level);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the volume level, in decibels, of the specified channel of the audio stream.
|
||||
/// </summary>
|
||||
/// <param name="channelNumber">The channel number.</param>
|
||||
/// <param name="level">The new volume level in decibels.</param>
|
||||
/// <param name="eventContext">A user context value that is passed to the notification callback.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int SetChannelVolumeLevel(
|
||||
[In][MarshalAs(UnmanagedType.U4)] UInt32 channelNumber,
|
||||
[In][MarshalAs(UnmanagedType.R4)] float level,
|
||||
[In][MarshalAs(UnmanagedType.LPStruct)] Guid eventContext);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the normalized, audio-tapered volume level of the specified channel in the audio stream.
|
||||
/// </summary>
|
||||
/// <param name="channelNumber">The channel number.</param>
|
||||
/// <param name="level">The new master volume level expressed as a normalized value between 0.0 and 1.0.</param>
|
||||
/// <param name="eventContext">A user context value that is passed to the notification callback.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int SetChannelVolumeLevelScalar(
|
||||
[In][MarshalAs(UnmanagedType.U4)] UInt32 channelNumber,
|
||||
[In][MarshalAs(UnmanagedType.R4)] float level,
|
||||
[In][MarshalAs(UnmanagedType.LPStruct)] Guid eventContext);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the volume level, in decibels, of the specified channel in the audio stream.
|
||||
/// </summary>
|
||||
/// <param name="channelNumber">The zero-based channel number.</param>
|
||||
/// <param name="level">The volume level in decibels.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int GetChannelVolumeLevel(
|
||||
[In][MarshalAs(UnmanagedType.U4)] UInt32 channelNumber,
|
||||
[Out][MarshalAs(UnmanagedType.R4)] out float level);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the normalized, audio-tapered volume level of the specified channel of the audio stream.
|
||||
/// </summary>
|
||||
/// <param name="channelNumber">The zero-based channel number.</param>
|
||||
/// <param name="level">The volume level expressed as a normalized value between 0.0 and 1.0.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int GetChannelVolumeLevelScalar(
|
||||
[In][MarshalAs(UnmanagedType.U4)] UInt32 channelNumber,
|
||||
[Out][MarshalAs(UnmanagedType.R4)] out float level);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the muting state of the audio stream.
|
||||
/// </summary>
|
||||
/// <param name="isMuted">True to mute the stream, or false to unmute the stream.</param>
|
||||
/// <param name="eventContext">A user context value that is passed to the notification callback.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int SetMute(
|
||||
[In][MarshalAs(UnmanagedType.Bool)] Boolean isMuted,
|
||||
[In][MarshalAs(UnmanagedType.LPStruct)] Guid eventContext);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the muting state of the audio stream.
|
||||
/// </summary>
|
||||
/// <param name="isMuted">The muting state. True if the stream is muted, false otherwise.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int GetMute(
|
||||
[Out][MarshalAs(UnmanagedType.Bool)] out Boolean isMuted);
|
||||
|
||||
/// <summary>
|
||||
/// Gets information about the current step in the volume range.
|
||||
/// </summary>
|
||||
/// <param name="step">The current zero-based step index.</param>
|
||||
/// <param name="stepCount">The total number of steps in the volume range.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int GetVolumeStepInfo(
|
||||
[Out][MarshalAs(UnmanagedType.U4)] out UInt32 step,
|
||||
[Out][MarshalAs(UnmanagedType.U4)] out UInt32 stepCount);
|
||||
|
||||
/// <summary>
|
||||
/// Increases the volume level by one step.
|
||||
/// </summary>
|
||||
/// <param name="eventContext">A user context value that is passed to the notification callback.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int VolumeStepUp(
|
||||
[In][MarshalAs(UnmanagedType.LPStruct)] Guid eventContext);
|
||||
|
||||
/// <summary>
|
||||
/// Decreases the volume level by one step.
|
||||
/// </summary>
|
||||
/// <param name="eventContext">A user context value that is passed to the notification callback.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int VolumeStepDown(
|
||||
[In][MarshalAs(UnmanagedType.LPStruct)] Guid eventContext);
|
||||
|
||||
/// <summary>
|
||||
/// Queries the audio endpoint device for its hardware-supported functions.
|
||||
/// </summary>
|
||||
/// <param name="hardwareSupportMask">A hardware support mask that indicates the capabilities of the endpoint.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int QueryHardwareSupport(
|
||||
[Out][MarshalAs(UnmanagedType.U4)] out UInt32 hardwareSupportMask);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the volume range of the audio stream, in decibels.
|
||||
/// </summary>
|
||||
/// <param name="volumeMin">The minimum volume level in decibels.</param>
|
||||
/// <param name="volumeMax">The maximum volume level in decibels.</param>
|
||||
/// <param name="volumeStep">The volume increment level in decibels.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int GetVolumeRange(
|
||||
[Out][MarshalAs(UnmanagedType.R4)] out float volumeMin,
|
||||
[Out][MarshalAs(UnmanagedType.R4)] out float volumeMax,
|
||||
[Out][MarshalAs(UnmanagedType.R4)] out float volumeStep);
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
namespace FocusVolumeControl.AudioSessions;
|
||||
using System;
|
||||
|
||||
namespace FocusVolumeControl.AudioSessions;
|
||||
|
||||
public interface IAudioSession
|
||||
{
|
||||
|
@ -1,38 +1,46 @@
|
||||
using CoreAudio;
|
||||
using System;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace FocusVolumeControl.AudioSessions;
|
||||
|
||||
internal class SystemSoundsAudioSession : IAudioSession
|
||||
internal sealed class SystemSoundsAudioSession : IAudioSession
|
||||
{
|
||||
public SystemSoundsAudioSession(SimpleAudioVolume volumeControl)
|
||||
public SystemSoundsAudioSession(IAudioSessionControl2 sessionControl)
|
||||
{
|
||||
_volumeControl = volumeControl;
|
||||
_sessionControl = sessionControl;
|
||||
_volumeControl = (ISimpleAudioVolume)sessionControl;
|
||||
}
|
||||
|
||||
SimpleAudioVolume _volumeControl;
|
||||
IAudioSessionControl2 _sessionControl;
|
||||
ISimpleAudioVolume _volumeControl;
|
||||
|
||||
public string DisplayName => "System sounds";
|
||||
public string GetIcon() => "Images/systemSounds";
|
||||
|
||||
public void ToggleMute()
|
||||
{
|
||||
_volumeControl.Mute = !_volumeControl.Mute;
|
||||
var guid = Guid.Empty;
|
||||
_volumeControl.SetMute(!IsMuted(), ref guid);
|
||||
}
|
||||
|
||||
public bool IsMuted() => _volumeControl.Mute;
|
||||
public bool IsMuted()
|
||||
{
|
||||
_volumeControl.GetMute(out var mute);
|
||||
return mute;
|
||||
}
|
||||
|
||||
public void IncrementVolumeLevel(int step, int ticks)
|
||||
{
|
||||
var level = _volumeControl.MasterVolume;
|
||||
_volumeControl.GetMasterVolume(out var level);
|
||||
level = VolumeHelpers.GetAdjustedVolume(level, step, ticks);
|
||||
|
||||
level += (0.01f * step) * ticks;
|
||||
level = Math.Max(level, 0);
|
||||
level = Math.Min(level, 1);
|
||||
|
||||
_volumeControl.MasterVolume = level;
|
||||
var guid = Guid.Empty;
|
||||
_volumeControl.SetMasterVolume(level, ref guid);
|
||||
}
|
||||
|
||||
public int GetVolumeLevel() => (int)(_volumeControl.MasterVolume * 100);
|
||||
|
||||
public int GetVolumeLevel()
|
||||
{
|
||||
_volumeControl.GetMasterVolume(out var level);
|
||||
return VolumeHelpers.GetVolumePercentage(level);
|
||||
}
|
||||
}
|
||||
|
@ -1,38 +1,41 @@
|
||||
using CoreAudio;
|
||||
using System;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace FocusVolumeControl.AudioSessions;
|
||||
|
||||
internal class SystemVolumeAudioSession : IAudioSession
|
||||
internal sealed class SystemVolumeAudioSession : IAudioSession
|
||||
{
|
||||
public SystemVolumeAudioSession(AudioEndpointVolume volumeControl)
|
||||
public SystemVolumeAudioSession(IAudioEndpointVolume volumeControl)
|
||||
{
|
||||
_volumeControl = volumeControl;
|
||||
}
|
||||
|
||||
AudioEndpointVolume _volumeControl;
|
||||
IAudioEndpointVolume _volumeControl;
|
||||
|
||||
public string DisplayName => "System Volume";
|
||||
public string GetIcon() => "Images/encoderIcon";
|
||||
|
||||
public void ToggleMute()
|
||||
{
|
||||
_volumeControl.Mute = !_volumeControl.Mute;
|
||||
_volumeControl.SetMute(!IsMuted(), Guid.Empty);
|
||||
}
|
||||
|
||||
public bool IsMuted() => _volumeControl.Mute;
|
||||
public bool IsMuted()
|
||||
{
|
||||
_volumeControl.GetMute(out var mute);
|
||||
return mute;
|
||||
}
|
||||
|
||||
public void IncrementVolumeLevel(int step, int ticks)
|
||||
{
|
||||
var level = _volumeControl.MasterVolumeLevelScalar;
|
||||
|
||||
level += (0.01f * step) * ticks;
|
||||
level = Math.Max(level, 0);
|
||||
level = Math.Min(level, 1);
|
||||
|
||||
_volumeControl.MasterVolumeLevelScalar = level;
|
||||
_volumeControl.GetMasterVolumeLevelScalar(out var level);
|
||||
level = VolumeHelpers.GetAdjustedVolume(level, step, ticks);
|
||||
_volumeControl.SetMasterVolumeLevelScalar(level, Guid.Empty);
|
||||
}
|
||||
|
||||
public int GetVolumeLevel() => (int)(_volumeControl.MasterVolumeLevelScalar * 100);
|
||||
|
||||
public int GetVolumeLevel()
|
||||
{
|
||||
_volumeControl.GetMasterVolumeLevelScalar(out var level);
|
||||
return VolumeHelpers.GetVolumePercentage(level);
|
||||
}
|
||||
}
|
||||
|
30
src/FocusVolumeControl/AudioSessions/VolumeHelpers.cs
Normal file
30
src/FocusVolumeControl/AudioSessions/VolumeHelpers.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FocusVolumeControl.AudioSessions
|
||||
{
|
||||
internal class VolumeHelpers
|
||||
{
|
||||
public static float GetAdjustedVolume(float startingVolume, int step, int ticks)
|
||||
{
|
||||
if(step <= 0)
|
||||
{
|
||||
step = 1;
|
||||
}
|
||||
|
||||
var level = startingVolume;
|
||||
|
||||
level += 0.01f * step * ticks;
|
||||
level = Math.Max(level, 0);
|
||||
level = Math.Min(level, 1);
|
||||
|
||||
return level;
|
||||
}
|
||||
|
||||
public static int GetVolumePercentage(float volume) => (int)Math.Round(volume * 100);
|
||||
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
using BarRaider.SdTools;
|
||||
using BarRaider.SdTools.Payloads;
|
||||
using FocusVolumeControl.AudioHelpers;
|
||||
using FocusVolumeControl.AudioSessions;
|
||||
using FocusVolumeControl.UI;
|
||||
using Newtonsoft.Json;
|
||||
@ -19,25 +20,20 @@ public class DialAction : EncoderBase
|
||||
[JsonProperty("fallbackBehavior")]
|
||||
public FallbackBehavior FallbackBehavior { get; set; }
|
||||
|
||||
[JsonProperty("stepSize")]
|
||||
public int StepSize { get; set; }
|
||||
|
||||
public static PluginSettings CreateDefaultSettings()
|
||||
{
|
||||
PluginSettings instance = new PluginSettings();
|
||||
instance.FallbackBehavior = FallbackBehavior.SystemSounds;
|
||||
instance.StepSize = 1;
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
private PluginSettings settings;
|
||||
|
||||
IntPtr _foregroundWindowChangedEvent;
|
||||
Native.WinEventDelegate _delegate;
|
||||
|
||||
IAudioSession _currentAudioSession;
|
||||
PluginSettings settings;
|
||||
AudioHelper _audioHelper = new AudioHelper();
|
||||
|
||||
Thread _thread;
|
||||
Dispatcher _dispatcher;
|
||||
|
||||
UIState _previousState;
|
||||
|
||||
public DialAction(ISDConnection connection, InitialPayload payload) : base(connection, payload)
|
||||
@ -45,47 +41,100 @@ public class DialAction : EncoderBase
|
||||
if (payload.Settings == null || payload.Settings.Count == 0)
|
||||
{
|
||||
settings = PluginSettings.CreateDefaultSettings();
|
||||
SaveSettings();
|
||||
_ = SaveSettings();
|
||||
}
|
||||
else
|
||||
{
|
||||
settings = payload.Settings.ToObject<PluginSettings>();
|
||||
}
|
||||
|
||||
_thread = new Thread(() =>
|
||||
WindowChangedEventLoop.Instance.WindowChanged += WindowChanged;
|
||||
|
||||
try
|
||||
{
|
||||
Logger.Instance.LogMessage(TracingLevel.DEBUG, "Registering for events");
|
||||
_delegate = new Native.WinEventDelegate(WinEventProc);
|
||||
_foregroundWindowChangedEvent = Native.RegisterForForegroundWindowChangedEvent(_delegate);
|
||||
//just in case we fail to get the active session, don't prevent the plugin from launching
|
||||
var session = _audioHelper.GetActiveSession(settings.FallbackBehavior);
|
||||
_ = UpdateStateIfNeeded(session);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
_currentAudioSession = settings.FallbackBehavior == FallbackBehavior.SystemSounds ? _audioHelper.GetSystemSounds() : _audioHelper.GetSystemVolume();
|
||||
public override void Dispose()
|
||||
{
|
||||
//Logger.Instance.LogMessage(TracingLevel.DEBUG, "Disposing");
|
||||
WindowChangedEventLoop.Instance.WindowChanged -= WindowChanged;
|
||||
}
|
||||
|
||||
public override async void DialDown(DialPayload payload)
|
||||
{
|
||||
//dial pressed down
|
||||
Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Down");
|
||||
await ToggleMuteAsync();
|
||||
try
|
||||
{
|
||||
//Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Down");
|
||||
await ToggleMuteAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unexpected Error in DialDown:\n {ex}");
|
||||
}
|
||||
}
|
||||
public override void DialUp(DialPayload payload) { }
|
||||
|
||||
public override async void TouchPress(TouchpadPressPayload payload)
|
||||
{
|
||||
Logger.Instance.LogMessage(TracingLevel.INFO, "Touch Press");
|
||||
if (payload.IsLongPress)
|
||||
try
|
||||
{
|
||||
//Logger.Instance.LogMessage(TracingLevel.INFO, "Touch Press");
|
||||
if (payload.IsLongPress)
|
||||
{
|
||||
await ResetAllAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
await ToggleMuteAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unexpected Error in TouchPress:\n {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
public override async void DialRotate(DialRotatePayload payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
//Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Rotate");
|
||||
//dial rotated. ticks positive for right, negative for left
|
||||
var activeSession = _audioHelper.Current;
|
||||
if (activeSession != null)
|
||||
{
|
||||
activeSession.IncrementVolumeLevel(settings.StepSize, payload.Ticks);
|
||||
await UpdateStateIfNeeded(activeSession);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Connection.ShowAlert();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_audioHelper.ResetCache();
|
||||
await Connection.ShowAlert();
|
||||
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unable to increment volume:\n {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
async Task ResetAllAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_audioHelper.ResetAll();
|
||||
}
|
||||
else
|
||||
catch
|
||||
{
|
||||
await ToggleMuteAsync();
|
||||
_audioHelper.ResetCache();
|
||||
await Connection.ShowAlert();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,98 +142,70 @@ public class DialAction : EncoderBase
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
if (_currentAudioSession != null)
|
||||
var activeSession = _audioHelper.Current;
|
||||
if (activeSession != null)
|
||||
{
|
||||
_currentAudioSession.ToggleMute();
|
||||
await UpdateStateIfNeeded();
|
||||
activeSession.ToggleMute();
|
||||
await UpdateStateIfNeeded(activeSession);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Connection.ShowAlert();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch
|
||||
{
|
||||
_audioHelper.ResetCache();
|
||||
await Connection.ShowAlert();
|
||||
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unable to toggle mute: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public override async void DialRotate(DialRotatePayload payload)
|
||||
{
|
||||
Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Rotate");
|
||||
//dial rotated. ticks positive for right, negative for left
|
||||
try
|
||||
{
|
||||
if (_currentAudioSession != null)
|
||||
{
|
||||
_currentAudioSession.IncrementVolumeLevel(1, payload.Ticks);
|
||||
await UpdateStateIfNeeded();
|
||||
}
|
||||
else
|
||||
{
|
||||
await Connection.ShowAlert();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await Connection.ShowAlert();
|
||||
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unable to toggle mute: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public override void DialUp(DialPayload payload)
|
||||
{
|
||||
//dial unpressed
|
||||
Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Up");
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
Logger.Instance.LogMessage(TracingLevel.DEBUG, "Disposing");
|
||||
if(_foregroundWindowChangedEvent != IntPtr.Zero)
|
||||
{
|
||||
Native.UnhookWinEvent(_foregroundWindowChangedEvent);
|
||||
}
|
||||
_dispatcher.InvokeShutdown();
|
||||
}
|
||||
|
||||
public override async void OnTick()
|
||||
{
|
||||
//called once every 1000ms and can be used for updating the title/image of the key
|
||||
var activeSession = _audioHelper.GetActiveSession(settings.FallbackBehavior);
|
||||
|
||||
if(activeSession != null)
|
||||
try
|
||||
{
|
||||
_currentAudioSession = activeSession;
|
||||
}
|
||||
//called once every 1000ms and can be used for updating the title/image of the key
|
||||
var activeSession = _audioHelper.GetActiveSession(settings.FallbackBehavior);
|
||||
|
||||
await UpdateStateIfNeeded();
|
||||
await UpdateStateIfNeeded(activeSession);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_audioHelper.ResetCache();
|
||||
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Exception on Tick:\n {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateStateIfNeeded()
|
||||
private async Task UpdateStateIfNeeded(IAudioSession audioSession)
|
||||
{
|
||||
if (_currentAudioSession != null)
|
||||
try
|
||||
{
|
||||
|
||||
var uiState = new UIState(_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
|
||||
)
|
||||
if (audioSession != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await Connection.SetFeedbackAsync(uiState);
|
||||
_previousState = uiState;
|
||||
var uiState = new UIState(audioSession);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Failed to update screen\n {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
@ -196,27 +217,39 @@ public class DialAction : EncoderBase
|
||||
|
||||
public override void ReceivedSettings(ReceivedSettingsPayload payload)
|
||||
{
|
||||
Tools.AutoPopulateSettings(settings, payload.Settings);
|
||||
SaveSettings();
|
||||
try
|
||||
{
|
||||
Tools.AutoPopulateSettings(settings, payload.Settings);
|
||||
_ = SaveSettings();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unexpected Error in SaveSettings:\n {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private Task SaveSettings()
|
||||
private async Task SaveSettings()
|
||||
{
|
||||
return Connection.SetSettingsAsync(JObject.FromObject(settings));
|
||||
try
|
||||
{
|
||||
await Connection.SetSettingsAsync(JObject.FromObject(settings));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unexpected Error in SaveSettings:\n {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
|
||||
public void WindowChanged()
|
||||
{
|
||||
try
|
||||
{
|
||||
OnTick();
|
||||
Thread.Sleep(TimeSpan.FromSeconds(1));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Exception on WinEventProc\n {ex}");
|
||||
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unexpected Error in Window Down:\n {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -54,20 +54,25 @@
|
||||
<Reference Include="WindowsBase" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="AudioHelpers\AppxPackage.cs" />
|
||||
<Compile Include="AudioSessions\ActiveAudioSessionWrapper.cs" />
|
||||
<Compile Include="AudioHelper.cs" />
|
||||
<Compile Include="AudioHelpers\AudioHelper.cs" />
|
||||
<Compile Include="AudioSessions\CoreAudio.cs" />
|
||||
<Compile Include="AudioSessions\VolumeHelpers.cs" />
|
||||
<Compile Include="AudioSessions\SystemSoundsAudioSession.cs" />
|
||||
<Compile Include="AudioSessions\SystemVolumeAudioSession.cs" />
|
||||
<Compile Include="DialAction.cs" />
|
||||
<Compile Include="AudioSessions\IAudioSession.cs" />
|
||||
<Compile Include="FallbackBehavior.cs" />
|
||||
<Compile Include="AudioHelpers\NameAndIconHelper.cs" />
|
||||
<Compile Include="UI\ISDConnectionExtensions.cs" />
|
||||
<Compile Include="Native.cs" />
|
||||
<Compile Include="ParentProcessUtilities.cs" />
|
||||
<Compile Include="AudioHelpers\ParentProcessUtilities.cs" />
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="UI\UIState.cs" />
|
||||
<Compile Include="UI\ValueWithOpacity.cs" />
|
||||
<Compile Include="WindowChangedEventLoop.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="App.config" />
|
||||
@ -83,14 +88,14 @@
|
||||
<Content Include="Images\**\*.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="PropertyInspector\**\*.js;PropertyInspector\**\*.css">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="PropertyInspector\PluginActionPI.html">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<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>
|
||||
@ -115,4 +120,4 @@
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
</Project>
|
@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using FocusVolumeControl.AudioHelpers;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace FocusVolumeControl;
|
||||
|
||||
@ -59,4 +61,15 @@ public class Native
|
||||
[DllImport("ntdll.dll")]
|
||||
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);
|
||||
|
||||
|
||||
}
|
||||
|
@ -6,25 +6,41 @@
|
||||
<meta name=apple-mobile-web-app-capable content=yes>
|
||||
<meta name=apple-mobile-web-app-status-bar-style content=black>
|
||||
<title>FocusVolumeControl Settings</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/barraider/streamdeck-easypi@latest/src/sdpi.css">
|
||||
<script src="https://cdn.jsdelivr.net/gh/barraider/streamdeck-easypi@latest/src/sdtools.common.js"></script>
|
||||
<link rel="stylesheet" href="./lib/sdpi.css">
|
||||
<link rel="sytlesheet" href="./lib/rangeTooltip.css">
|
||||
<script src="lib/sdtools.common.js"></script>
|
||||
<script src="lib/rangeTooltip.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="sdpi-wrapper">
|
||||
|
||||
<div class="sdpi-item">
|
||||
<div class="sdpi-item-label">Fallback</div>
|
||||
<select class="sdpi-item-value sdProperty" id="fallbackBehavior" oninput="setSettings()">
|
||||
<option value="0">System Sounds</option>
|
||||
<option value="1">Previous App</option>
|
||||
<option value="2">Main System Volume</option>
|
||||
<option value="2">Default Output Device Volume</option>
|
||||
</select>
|
||||
</div>
|
||||
<details>
|
||||
<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>* 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>
|
||||
</details>
|
||||
|
||||
|
||||
<div type="range" class="sdpi-item sdShowTooltip">
|
||||
<div class="sdpi-item-label">Step Size</div>
|
||||
<div class="sdpi-item-value">
|
||||
<span class="clickable" value="1">1</span>
|
||||
<input type="range" min="1" max="10" value="1" class="sdProperty" data-suffix=" %" id="stepSize" oninput="setSettings()" />
|
||||
<span class="clickable" value="1">10</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sdpi-info-label hidden" style="top: -1000;" value="">Tooltip</div>
|
||||
|
||||
<details>
|
||||
<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>* 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>* 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>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -0,0 +1,41 @@
|
||||
.sdpi-info-label {
|
||||
display: inline-block;
|
||||
user-select: none;
|
||||
position: absolute;
|
||||
height: 15px;
|
||||
width: auto;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
min-width: 44px;
|
||||
max-width: 80px;
|
||||
background: white;
|
||||
font-size: 11px;
|
||||
color: black;
|
||||
z-index: 1000;
|
||||
box-shadow: 0px 0px 12px rgba(0,0,0,.8);
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.sdpi-info-label.hidden {
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s linear;
|
||||
}
|
||||
|
||||
.sdpi-info-label.shown {
|
||||
position: absolute;
|
||||
opacity: 1;
|
||||
transition: opacity 0.25s ease-out;
|
||||
}
|
||||
|
||||
.rangeLabel {
|
||||
position: relative;
|
||||
font-weight: normal;
|
||||
margin-top: 22px;
|
||||
left: -200px;
|
||||
min-width: 200px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.percent::after {
|
||||
content: "%";
|
||||
}
|
122
src/FocusVolumeControl/PropertyInspector/lib/rangeTooltip.js
Normal file
122
src/FocusVolumeControl/PropertyInspector/lib/rangeTooltip.js
Normal file
@ -0,0 +1,122 @@
|
||||
// ****************************************************************
|
||||
// * EasyPI v1.3
|
||||
// * Author: BarRaider
|
||||
// *
|
||||
// * rangeTooltip.js adds a tooltip showing the value of a range slider.
|
||||
// * Requires rangeTooltip.css to be referenced in the HTML file.
|
||||
// *
|
||||
// * Project page: https://github.com/BarRaider/streamdeck-easypi
|
||||
// * Support: http://discord.barraider.com
|
||||
// ****************************************************************
|
||||
|
||||
var tooltip = document.querySelector('.sdpi-info-label');
|
||||
var tw;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Handler when the DOM is fully loaded
|
||||
setRangeTooltips();
|
||||
});
|
||||
|
||||
function calcRangeLabel(elem) {
|
||||
const value = elem.value;
|
||||
const percent = (elem.value - elem.min) / (elem.max - elem.min);
|
||||
let tooltipValue = value;
|
||||
let outputType = elem.dataset.suffix;
|
||||
if (outputType && outputType == '%') {
|
||||
tooltipValue = Math.round(100 * percent);
|
||||
}
|
||||
|
||||
return tooltipValue + outputType;
|
||||
}
|
||||
|
||||
function setElementLabel(elem, str) {
|
||||
// Try to set this for the rangeLabel class, if it exists
|
||||
let label = elem.querySelector('.rangeLabel');
|
||||
if (label) {
|
||||
label.innerHTML = str;
|
||||
}
|
||||
else {
|
||||
console.log('setElementLabel ERROR! No .rangeLabel found', elem);
|
||||
}
|
||||
}
|
||||
|
||||
function setRangeTooltips() {
|
||||
console.log("Loading setRangeTooltips");
|
||||
|
||||
if (!tooltip) {
|
||||
tooltip = document.querySelector('.sdpi-info-label');
|
||||
}
|
||||
|
||||
if (!tw) {
|
||||
tw = tooltip.getBoundingClientRect().width;
|
||||
}
|
||||
|
||||
const rangeToolTips = document.querySelectorAll('div[type=range].sdShowTooltip');
|
||||
rangeToolTips.forEach(elem => {
|
||||
let rangeSelector = elem.querySelector('input[type=range]');
|
||||
let fn = () => {
|
||||
const rangeRect = rangeSelector.getBoundingClientRect();
|
||||
const w = rangeRect.width - tw / 2;
|
||||
const labelStr = calcRangeLabel(rangeSelector);
|
||||
// Set the tooltip
|
||||
if (tooltip.classList.contains('hidden')) {
|
||||
tooltip.style.top = '-1000px';
|
||||
} else {
|
||||
const percent = (rangeSelector.value - rangeSelector.min) / (rangeSelector.max - rangeSelector.min);
|
||||
tooltip.style.left = (rangeRect.left + Math.round(w * percent) - tw / 4) + 'px';
|
||||
tooltip.textContent = labelStr;
|
||||
tooltip.style.top = (rangeRect.top - 32) + 'px';
|
||||
}
|
||||
|
||||
setElementLabel(elem, labelStr)
|
||||
};
|
||||
|
||||
rangeSelector.addEventListener(
|
||||
'mouseenter',
|
||||
function () {
|
||||
tooltip.classList.remove('hidden');
|
||||
tooltip.classList.add('shown');
|
||||
fn();
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
rangeSelector.addEventListener(
|
||||
'mouseout',
|
||||
function () {
|
||||
tooltip.classList.remove('shown');
|
||||
tooltip.classList.add('hidden');
|
||||
fn();
|
||||
},
|
||||
false
|
||||
);
|
||||
rangeSelector.addEventListener('input', fn, false);
|
||||
|
||||
rangeSelector.addEventListener("change", fn, false);
|
||||
|
||||
document.addEventListener(
|
||||
'settingsUpdated',
|
||||
function () {
|
||||
console.log('rangeTooltip settingsUpdated called');
|
||||
window.setTimeout(function () {
|
||||
let str = calcRangeLabel(rangeSelector);
|
||||
setElementLabel(elem, str);
|
||||
}, 500);
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
document.addEventListener(
|
||||
'websocketCreate',
|
||||
function () {
|
||||
console.log('rangeTooltip websocketCreate called');
|
||||
window.setTimeout(function () {
|
||||
let str = calcRangeLabel(rangeSelector);
|
||||
setElementLabel(elem, str);
|
||||
}, 500);
|
||||
},
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
}
|
1650
src/FocusVolumeControl/PropertyInspector/lib/sdpi.css
Normal file
1650
src/FocusVolumeControl/PropertyInspector/lib/sdpi.css
Normal file
File diff suppressed because it is too large
Load Diff
321
src/FocusVolumeControl/PropertyInspector/lib/sdtools.common.js
Normal file
321
src/FocusVolumeControl/PropertyInspector/lib/sdtools.common.js
Normal file
@ -0,0 +1,321 @@
|
||||
// ****************************************************************
|
||||
// * EasyPI v1.4
|
||||
// * Author: BarRaider
|
||||
// *
|
||||
// * JS library to simplify the communication between the
|
||||
// * Stream Deck's Property Inspector and the plugin.
|
||||
// *
|
||||
// * Project page: https://github.com/BarRaider/streamdeck-easypi
|
||||
// * Support: http://discord.barraider.com
|
||||
// *
|
||||
// * Initially forked from Elgato's common.js file
|
||||
// ****************************************************************
|
||||
|
||||
var websocket = null,
|
||||
uuid = null,
|
||||
registerEventName = null,
|
||||
actionInfo = {},
|
||||
inInfo = {},
|
||||
runningApps = [],
|
||||
isQT = navigator.appVersion.includes('QtWebEngine');
|
||||
|
||||
function connectElgatoStreamDeckSocket(inPort, inUUID, inRegisterEvent, inInfo, inActionInfo) {
|
||||
uuid = inUUID;
|
||||
registerEventName = inRegisterEvent;
|
||||
console.log(inUUID, inActionInfo);
|
||||
actionInfo = JSON.parse(inActionInfo); // cache the info
|
||||
inInfo = JSON.parse(inInfo);
|
||||
websocket = new WebSocket('ws://127.0.0.1:' + inPort);
|
||||
|
||||
addDynamicStyles(inInfo.colors);
|
||||
|
||||
websocket.onopen = websocketOnOpen;
|
||||
websocket.onmessage = websocketOnMessage;
|
||||
|
||||
// Allow others to get notified that the websocket is created
|
||||
var event = new Event('websocketCreate');
|
||||
document.dispatchEvent(event);
|
||||
|
||||
loadConfiguration(actionInfo.payload.settings);
|
||||
initPropertyInspector();
|
||||
}
|
||||
|
||||
function websocketOnOpen() {
|
||||
var json = {
|
||||
event: registerEventName,
|
||||
uuid: uuid
|
||||
};
|
||||
websocket.send(JSON.stringify(json));
|
||||
|
||||
// Notify the plugin that we are connected
|
||||
sendValueToPlugin('propertyInspectorConnected', 'property_inspector');
|
||||
}
|
||||
|
||||
function websocketOnMessage(evt) {
|
||||
// Received message from Stream Deck
|
||||
var jsonObj = JSON.parse(evt.data);
|
||||
|
||||
if (jsonObj.event === 'didReceiveSettings') {
|
||||
var payload = jsonObj.payload;
|
||||
loadConfiguration(payload.settings);
|
||||
}
|
||||
else {
|
||||
console.log("Ignored websocketOnMessage: " + jsonObj.event);
|
||||
}
|
||||
}
|
||||
|
||||
function loadConfiguration(payload) {
|
||||
console.log('loadConfiguration');
|
||||
console.log(payload);
|
||||
for (var key in payload) {
|
||||
try {
|
||||
var elem = document.getElementById(key);
|
||||
if (elem.classList.contains("sdCheckbox")) { // Checkbox
|
||||
elem.checked = payload[key];
|
||||
}
|
||||
else if (elem.classList.contains("sdFile")) { // File
|
||||
var elemFile = document.getElementById(elem.id + "Filename");
|
||||
elemFile.innerText = payload[key];
|
||||
if (!elemFile.innerText) {
|
||||
elemFile.innerText = "No file...";
|
||||
}
|
||||
}
|
||||
else if (elem.classList.contains("sdList")) { // Dynamic dropdown
|
||||
var textProperty = elem.getAttribute("sdListTextProperty");
|
||||
var valueProperty = elem.getAttribute("sdListValueProperty");
|
||||
var valueField = elem.getAttribute("sdValueField");
|
||||
|
||||
var items = payload[key];
|
||||
elem.options.length = 0;
|
||||
|
||||
for (var idx = 0; idx < items.length; idx++) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = items[idx][valueProperty];
|
||||
opt.text = items[idx][textProperty];
|
||||
elem.appendChild(opt);
|
||||
}
|
||||
elem.value = payload[valueField];
|
||||
}
|
||||
else if (elem.classList.contains("sdHTML")) { // HTML element
|
||||
elem.innerHTML = payload[key];
|
||||
}
|
||||
else { // Normal value
|
||||
elem.value = payload[key];
|
||||
}
|
||||
console.log("Load: " + key + "=" + payload[key]);
|
||||
}
|
||||
catch (err) {
|
||||
console.log("loadConfiguration failed for key: " + key + " - " + err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setSettings() {
|
||||
var payload = {};
|
||||
var elements = document.getElementsByClassName("sdProperty");
|
||||
|
||||
Array.prototype.forEach.call(elements, function (elem) {
|
||||
var key = elem.id;
|
||||
if (elem.classList.contains("sdCheckbox")) { // Checkbox
|
||||
payload[key] = elem.checked;
|
||||
}
|
||||
else if (elem.classList.contains("sdFile")) { // File
|
||||
var elemFile = document.getElementById(elem.id + "Filename");
|
||||
payload[key] = elem.value;
|
||||
if (!elem.value) {
|
||||
// Fetch innerText if file is empty (happens when we lose and regain focus to this key)
|
||||
payload[key] = elemFile.innerText;
|
||||
}
|
||||
else {
|
||||
// Set value on initial file selection
|
||||
elemFile.innerText = elem.value;
|
||||
}
|
||||
}
|
||||
else if (elem.classList.contains("sdList")) { // Dynamic dropdown
|
||||
var valueField = elem.getAttribute("sdValueField");
|
||||
payload[valueField] = elem.value;
|
||||
}
|
||||
else if (elem.classList.contains("sdHTML")) { // HTML element
|
||||
var valueField = elem.getAttribute("sdValueField");
|
||||
payload[valueField] = elem.innerHTML;
|
||||
}
|
||||
else { // Normal value
|
||||
payload[key] = elem.value;
|
||||
}
|
||||
console.log("Save: " + key + "<=" + payload[key]);
|
||||
});
|
||||
setSettingsToPlugin(payload);
|
||||
}
|
||||
|
||||
function setSettingsToPlugin(payload) {
|
||||
if (websocket && (websocket.readyState === 1)) {
|
||||
const json = {
|
||||
'event': 'setSettings',
|
||||
'context': uuid,
|
||||
'payload': payload
|
||||
};
|
||||
websocket.send(JSON.stringify(json));
|
||||
var event = new Event('settingsUpdated');
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
// Sends an entire payload to the sendToPlugin method
|
||||
function sendPayloadToPlugin(payload) {
|
||||
if (websocket && (websocket.readyState === 1)) {
|
||||
const json = {
|
||||
'action': actionInfo['action'],
|
||||
'event': 'sendToPlugin',
|
||||
'context': uuid,
|
||||
'payload': payload
|
||||
};
|
||||
websocket.send(JSON.stringify(json));
|
||||
}
|
||||
}
|
||||
|
||||
// Sends one value to the sendToPlugin method
|
||||
function sendValueToPlugin(value, param) {
|
||||
if (websocket && (websocket.readyState === 1)) {
|
||||
const json = {
|
||||
'action': actionInfo['action'],
|
||||
'event': 'sendToPlugin',
|
||||
'context': uuid,
|
||||
'payload': {
|
||||
[param]: value
|
||||
}
|
||||
};
|
||||
websocket.send(JSON.stringify(json));
|
||||
}
|
||||
}
|
||||
|
||||
function openWebsite() {
|
||||
if (websocket && (websocket.readyState === 1)) {
|
||||
const json = {
|
||||
'event': 'openUrl',
|
||||
'payload': {
|
||||
'url': 'https://BarRaider.com'
|
||||
}
|
||||
};
|
||||
websocket.send(JSON.stringify(json));
|
||||
}
|
||||
}
|
||||
|
||||
if (!isQT) {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
initPropertyInspector();
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Notify the plugin we are about to leave
|
||||
sendValueToPlugin('propertyInspectorWillDisappear', 'property_inspector');
|
||||
|
||||
// Don't set a returnValue to the event, otherwise Chromium with throw an error.
|
||||
});
|
||||
|
||||
function prepareDOMElements(baseElement) {
|
||||
baseElement = baseElement || document;
|
||||
|
||||
/**
|
||||
* You could add a 'label' to a textares, e.g. to show the number of charactes already typed
|
||||
* or contained in the textarea. This helper updates this label for you.
|
||||
*/
|
||||
baseElement.querySelectorAll('textarea').forEach((e) => {
|
||||
const maxl = e.getAttribute('maxlength');
|
||||
e.targets = baseElement.querySelectorAll(`[for='${e.id}']`);
|
||||
if (e.targets.length) {
|
||||
let fn = () => {
|
||||
for (let x of e.targets) {
|
||||
x.textContent = maxl ? `${e.value.length}/${maxl}` : `${e.value.length}`;
|
||||
}
|
||||
};
|
||||
fn();
|
||||
e.onkeyup = fn;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initPropertyInspector() {
|
||||
// Place to add functions
|
||||
prepareDOMElements(document);
|
||||
}
|
||||
|
||||
|
||||
function addDynamicStyles(clrs) {
|
||||
const node = document.getElementById('#sdpi-dynamic-styles') || document.createElement('style');
|
||||
if (!clrs.mouseDownColor) clrs.mouseDownColor = fadeColor(clrs.highlightColor, -100);
|
||||
const clr = clrs.highlightColor.slice(0, 7);
|
||||
const clr1 = fadeColor(clr, 100);
|
||||
const clr2 = fadeColor(clr, 60);
|
||||
const metersActiveColor = fadeColor(clr, -60);
|
||||
|
||||
node.setAttribute('id', 'sdpi-dynamic-styles');
|
||||
node.innerHTML = `
|
||||
|
||||
input[type="radio"]:checked + label span,
|
||||
input[type="checkbox"]:checked + label span {
|
||||
background-color: ${clrs.highlightColor};
|
||||
}
|
||||
|
||||
input[type="radio"]:active:checked + label span,
|
||||
input[type="radio"]:active + label span,
|
||||
input[type="checkbox"]:active:checked + label span,
|
||||
input[type="checkbox"]:active + label span {
|
||||
background-color: ${clrs.mouseDownColor};
|
||||
}
|
||||
|
||||
input[type="radio"]:active + label span,
|
||||
input[type="checkbox"]:active + label span {
|
||||
background-color: ${clrs.buttonPressedBorderColor};
|
||||
}
|
||||
|
||||
td.selected,
|
||||
td.selected:hover,
|
||||
li.selected:hover,
|
||||
li.selected {
|
||||
color: white;
|
||||
background-color: ${clrs.highlightColor};
|
||||
}
|
||||
|
||||
.sdpi-file-label > label:active,
|
||||
.sdpi-file-label.file:active,
|
||||
label.sdpi-file-label:active,
|
||||
label.sdpi-file-info:active,
|
||||
input[type="file"]::-webkit-file-upload-button:active,
|
||||
button:active {
|
||||
background-color: ${clrs.buttonPressedBackgroundColor};
|
||||
color: ${clrs.buttonPressedTextColor};
|
||||
border-color: ${clrs.buttonPressedBorderColor};
|
||||
}
|
||||
|
||||
::-webkit-progress-value,
|
||||
meter::-webkit-meter-optimum-value {
|
||||
background: linear-gradient(${clr2}, ${clr1} 20%, ${clr} 45%, ${clr} 55%, ${clr2})
|
||||
}
|
||||
|
||||
::-webkit-progress-value:active,
|
||||
meter::-webkit-meter-optimum-value:active {
|
||||
background: linear-gradient(${clr}, ${clr2} 20%, ${metersActiveColor} 45%, ${metersActiveColor} 55%, ${clr})
|
||||
}
|
||||
`;
|
||||
document.body.appendChild(node);
|
||||
};
|
||||
|
||||
/** UTILITIES */
|
||||
|
||||
/*
|
||||
Quick utility to lighten or darken a color (doesn't take color-drifting, etc. into account)
|
||||
Usage:
|
||||
fadeColor('#061261', 100); // will lighten the color
|
||||
fadeColor('#200867'), -100); // will darken the color
|
||||
*/
|
||||
function fadeColor(col, amt) {
|
||||
const min = Math.min, max = Math.max;
|
||||
const num = parseInt(col.replace(/#/g, ''), 16);
|
||||
const r = min(255, max((num >> 16) + amt, 0));
|
||||
const g = min(255, max((num & 0x0000FF) + amt, 0));
|
||||
const b = min(255, max(((num >> 8) & 0x00FF) + amt, 0));
|
||||
return '#' + (g | (b << 8) | (r << 16)).toString(16).padStart(6, 0);
|
||||
}
|
54
src/FocusVolumeControl/WindowChangedEventLoop.cs
Normal file
54
src/FocusVolumeControl/WindowChangedEventLoop.cs
Normal file
@ -0,0 +1,54 @@
|
||||
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;
|
||||
|
||||
private void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
|
||||
{
|
||||
try
|
||||
{
|
||||
WindowChanged?.Invoke();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unexpected Error in EventHandler:\n {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -33,7 +33,7 @@
|
||||
"Name": "Focused Application Volume",
|
||||
"Description": "Control the volume of the focused application",
|
||||
"URL": "https://github.com/dlprows/FocusVolumeControl",
|
||||
"Version": "1.0.1",
|
||||
"Version": "1.2.0",
|
||||
"CodePath": "FocusVolumeControl",
|
||||
"Category": "Volume Control [dlprows]",
|
||||
"Icon": "Images/pluginIcon",
|
||||
|
@ -6,14 +6,16 @@
|
||||
xmlns:local="clr-namespace:SoundBrowser"
|
||||
mc:Ignorable="d"
|
||||
Title="MainWindow" Height="800" Width="800">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<ScrollViewer>
|
||||
<Grid>
|
||||
<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>
|
||||
<TextBlock x:Name="_tf" Grid.Row="0">current</TextBlock>
|
||||
<TextBlock x:Name="_tf2" Grid.Row="1">list</TextBlock>
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Window>
|
||||
|
@ -1,5 +1,6 @@
|
||||
using CoreAudio;
|
||||
using FocusVolumeControl;
|
||||
using FocusVolumeControl;
|
||||
using FocusVolumeControl.AudioHelpers;
|
||||
using FocusVolumeControl.AudioSessions;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
@ -56,18 +57,11 @@ public partial class MainWindow : Window
|
||||
|
||||
foreach (var p in processes)
|
||||
{
|
||||
var (displayName, _) = (new NameAndIconHelper()).GetProcessInfo(p);
|
||||
|
||||
sb.AppendLine($"pid: {p.Id}");
|
||||
sb.AppendLine($"\tprocessName: {p.ProcessName}");
|
||||
try
|
||||
{
|
||||
sb.AppendLine($"\tFileDescription: {p!.MainModule!.FileVersionInfo.FileDescription}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
sb.AppendLine("\tFileDescription: ##ERROR##");
|
||||
}
|
||||
|
||||
sb.AppendLine($"\tDisplayName: {displayName}");
|
||||
|
||||
}
|
||||
|
||||
@ -93,26 +87,39 @@ public partial class MainWindow : Window
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("-------------------------------------------------------------------------------");
|
||||
|
||||
var deviceEnumerator = new MMDeviceEnumerator(Guid.NewGuid());
|
||||
var deviceEnumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator();
|
||||
|
||||
using var device = deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
|
||||
using var manager = device.AudioSessionManager2;
|
||||
deviceEnumerator.EnumAudioEndpoints(DataFlow.Render, DeviceState.Active, out var deviceCollection);
|
||||
deviceCollection.GetCount(out var num);
|
||||
|
||||
var sessions = manager!.Sessions;
|
||||
|
||||
foreach (var session in sessions!)
|
||||
for(int i = 0; i < num; i++)
|
||||
{
|
||||
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}");
|
||||
sb.AppendLine($"\tsession: {displayName}");
|
||||
|
||||
manager.GetSessionEnumerator(out var sessionEnumerator);
|
||||
|
||||
|
||||
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>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CoreAudio" Version="1.27.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FocusVolumeControl\FocusVolumeControl.csproj" />
|
||||
</ItemGroup>
|
||||
|
Reference in New Issue
Block a user