Compare commits
30 Commits
Author | SHA1 | Date | |
---|---|---|---|
23ae43c155 | |||
d8b4bf340b | |||
cd306b0aab | |||
f5ab700817 | |||
d1a5e37067 | |||
fdfa32909f | |||
7abbc92080 | |||
0f7f1fffcd | |||
aa905fe443 | |||
160bedd461 | |||
68d5154756 | |||
8eebf1af47 | |||
6de76da8ad | |||
48161b5c2e | |||
d1df235af0 | |||
6aaa32cf92 | |||
4ca0ad021f | |||
4c1ccd9025 | |||
2b10b6d7a6 | |||
ca634f8d3c | |||
bbb0e55ed6 | |||
520659ac52 | |||
609a7bdb65 | |||
13fdfde3e5 | |||
bbad79b4f3 | |||
708180dc8e | |||
5711ace990 | |||
d89c8b1ffa | |||
f94052e54b | |||
ceb3494e43 |
67
Overrides.md
Normal file
@ -0,0 +1,67 @@
|
||||
## Overrides
|
||||
|
||||
Some games use particularly agressive forms of anti-cheat that interfere with the ability to determine information about the focused application, and then pair it to the appropriate audio process.
|
||||
|
||||
Unfortunately, there is nothing I can do about that.
|
||||
|
||||
In order to work around this, the overrides mechanism is there for you to set up manual mappings.
|
||||
|
||||
I chose to base it off of a process's Main Window Title.
|
||||
|
||||
These can change throughout the usage of an application. Once again, there's nothing I can do about that.
|
||||
|
||||
The reason I chose to use the Main Window Title despite this problem, is because in the case of some games, it was one of the only pieces of information that I could get about the running process due to its anti-cheat.
|
||||
|
||||
There is no way of knowing if this will work in all cases, but it seems to be reliable for the time being. And if I'm ever unable to get the Main Window's Title, I don't know if there will be a different data point to use for matching. Its kind of just the only thing available.
|
||||
|
||||
In order to make it so that I don't have to update the plugin for each game using agressive anti-cheat, and to make it so that you don't have to wait for me to fix something, I have created a way to put overrides into the plugin directly.
|
||||
|
||||
|
||||
## Syntax
|
||||
|
||||
```
|
||||
<Match type>: Window title string
|
||||
audio process string
|
||||
|
||||
//lines starting in // are comments, and are ignored
|
||||
```
|
||||
|
||||
|
||||
```
|
||||
Match Type
|
||||
eq: equals - case insensitive
|
||||
start: starts with - case insensitive
|
||||
end: ends with - case insensitive
|
||||
regex: regular expression - case sensitive
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
```
|
||||
//helldivers 2 has a trademark symbol in it, and those are hard to type.
|
||||
//so we just find a window that starts with helldivers
|
||||
start: Helldivers
|
||||
helldivers
|
||||
```
|
||||
|
||||
|
||||
```
|
||||
//you can actually map anything you want. it doesn't have to be only things with anti-cheat problems
|
||||
eq: task manager
|
||||
Google Chrome
|
||||
```
|
||||
|
||||
## Help
|
||||
|
||||
Getting window titles can be a little hard sometimes. You can mouse over the icon on the start bar, and get it from there.
|
||||
|
||||
Another great way to get it is to run this powershell
|
||||
|
||||
```
|
||||
Get-Process | Where-Object ($_.mainWindowTitle} | Format-Table mainWindowTitle
|
||||
```
|
||||
|
||||
For audio processes right click on the volume in the tray of the start bar, and open the Volume Mixer. When you look through the list of apps, you can just type the name from that.
|
||||
|
||||
Alternatively you can put in the name of the actual executable. The easiest way to get to those is to run the SoundBrowser published in the releases in github.
|
||||
|
@ -12,6 +12,11 @@ The screen updates to show the name/icon of the app so that you can always know
|
||||
|
||||

|
||||
|
||||
## Help
|
||||
|
||||
If you're having trouble with a specific applicaiton, there is an override that you can put in.
|
||||
[More Details Here](Overrides.md)
|
||||
|
||||
## Developing
|
||||
|
||||
build the solution with visual studio
|
||||
|
@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="3.2.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FocusVolumeControl\FocusVolumeControl.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
168
src/FocusVolumeControl.UnitTests/OverrideParserTests.cs
Normal file
@ -0,0 +1,168 @@
|
||||
using FocusVolumeControl.Overrides;
|
||||
|
||||
namespace FocusVolumeControl.UnitTests
|
||||
{
|
||||
public class OverrideParserTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
[InlineData("\n")]
|
||||
public void BlankReturnsEmpty(string str)
|
||||
{
|
||||
//arrange
|
||||
|
||||
//act
|
||||
var overrides = OverrideParser.Parse(str);
|
||||
|
||||
//assert
|
||||
Assert.Empty(overrides);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HelldiversParses()
|
||||
{
|
||||
//arrange
|
||||
var str =
|
||||
"""
|
||||
eq: HELLDIVERS™ 2
|
||||
helldivers2
|
||||
""";
|
||||
|
||||
//act
|
||||
var overrides = OverrideParser.Parse(str);
|
||||
|
||||
//assert
|
||||
Assert.Single(overrides);
|
||||
Assert.Equal(MatchType.Equal, overrides[0].MatchType);
|
||||
Assert.Equal("HELLDIVERS™ 2", overrides[0].WindowQuery);
|
||||
Assert.Equal("helldivers2", overrides[0].AudioProcessName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultipleOverridesParse()
|
||||
{
|
||||
//arrange
|
||||
var str =
|
||||
"""
|
||||
eq: HELLDIVERS™ 2
|
||||
helldivers2
|
||||
|
||||
start: Task
|
||||
Steam
|
||||
""";
|
||||
|
||||
//act
|
||||
var overrides = OverrideParser.Parse(str);
|
||||
|
||||
//assert
|
||||
Assert.Equal(MatchType.Equal, overrides[0].MatchType);
|
||||
Assert.Equal("HELLDIVERS™ 2", overrides[0].WindowQuery);
|
||||
Assert.Equal("helldivers2", overrides[0].AudioProcessName);
|
||||
Assert.Equal(MatchType.StartsWith, overrides[1].MatchType);
|
||||
Assert.Equal("Task", overrides[1].WindowQuery);
|
||||
Assert.Equal("Steam", overrides[1].AudioProcessName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IncompleteMatchesAreSkipped()
|
||||
{
|
||||
//arrange
|
||||
var str =
|
||||
"""
|
||||
eq: HELLDIVERS™ 2
|
||||
|
||||
start: Task
|
||||
Steam
|
||||
""";
|
||||
|
||||
//act
|
||||
var overrides = OverrideParser.Parse(str);
|
||||
|
||||
//assert
|
||||
Assert.Single(overrides);
|
||||
Assert.Equal(MatchType.StartsWith, overrides[0].MatchType);
|
||||
Assert.Equal("Task", overrides[0].WindowQuery);
|
||||
Assert.Equal("Steam", overrides[0].AudioProcessName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidMatchesAreSkipped()
|
||||
{
|
||||
//arrange
|
||||
var str =
|
||||
"""
|
||||
equal: Chrome
|
||||
chrome
|
||||
|
||||
end: Task
|
||||
Steam
|
||||
""";
|
||||
|
||||
//act
|
||||
var overrides = OverrideParser.Parse(str);
|
||||
|
||||
//assert
|
||||
Assert.Single(overrides);
|
||||
Assert.Equal(MatchType.EndsWith, overrides[0].MatchType);
|
||||
Assert.Equal("Task", overrides[0].WindowQuery);
|
||||
Assert.Equal("Steam", overrides[0].AudioProcessName);
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MatchesAreCaseInsensitive()
|
||||
{
|
||||
//arrange
|
||||
var str =
|
||||
"""
|
||||
Eq: 0
|
||||
0
|
||||
|
||||
eNd: 1
|
||||
1
|
||||
|
||||
StArT: 2
|
||||
2
|
||||
|
||||
Regex: 3
|
||||
3
|
||||
""";
|
||||
|
||||
//act
|
||||
var overrides = OverrideParser.Parse(str);
|
||||
|
||||
//assert
|
||||
Assert.Equal(MatchType.Equal, overrides[0].MatchType);
|
||||
Assert.Equal(MatchType.EndsWith, overrides[1].MatchType);
|
||||
Assert.Equal(MatchType.StartsWith, overrides[2].MatchType);
|
||||
Assert.Equal(MatchType.Regex, overrides[3].MatchType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CommentsAreSkipped()
|
||||
{
|
||||
//arrange
|
||||
var str =
|
||||
"""
|
||||
//Eq: 0
|
||||
//0
|
||||
|
||||
end: 1
|
||||
1
|
||||
""";
|
||||
|
||||
//act
|
||||
var overrides = OverrideParser.Parse(str);
|
||||
|
||||
//assert
|
||||
Assert.Single(overrides);
|
||||
Assert.Equal(MatchType.EndsWith, overrides[0].MatchType);
|
||||
Assert.Equal("1", overrides[0].WindowQuery);
|
||||
Assert.Equal("1", overrides[0].AudioProcessName);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
1
src/FocusVolumeControl.UnitTests/Usings.cs
Normal file
@ -0,0 +1 @@
|
||||
global using Xunit;
|
@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FocusVolumeControl", "Focus
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SoundBrowser", "SoundBrowser\SoundBrowser.csproj", "{0E8AB334-82F1-4DBC-9BDA-B6F9714A1847}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FocusVolumeControl.UnitTests", "FocusVolumeControl.UnitTests\FocusVolumeControl.UnitTests.csproj", "{322E16C9-C96E-45DF-912F-DB6366170645}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -21,6 +23,10 @@ Global
|
||||
{0E8AB334-82F1-4DBC-9BDA-B6F9714A1847}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0E8AB334-82F1-4DBC-9BDA-B6F9714A1847}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0E8AB334-82F1-4DBC-9BDA-B6F9714A1847}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{322E16C9-C96E-45DF-912F-DB6366170645}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{322E16C9-C96E-45DF-912F-DB6366170645}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{322E16C9-C96E-45DF-912F-DB6366170645}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{322E16C9-C96E-45DF-912F-DB6366170645}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
@ -1,188 +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 void ResetCache()
|
||||
{
|
||||
lock(_lock)
|
||||
{
|
||||
_current = null;
|
||||
}
|
||||
}
|
||||
|
||||
public IAudioSession GetActiveSession(FallbackBehavior fallbackBehavior)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var processes = GetPossibleProcesses();
|
||||
|
||||
if (_currentProcesses == null || !_currentProcesses.SequenceEqual(processes))
|
||||
{
|
||||
_current = FindSession(processes);
|
||||
}
|
||||
|
||||
if(_current == null)
|
||||
{
|
||||
if(fallbackBehavior == FallbackBehavior.SystemSounds && _current is not SystemSoundsAudioSession)
|
||||
{
|
||||
_current = GetSystemSounds();
|
||||
}
|
||||
else if(fallbackBehavior == FallbackBehavior.SystemVolume && _current is not SystemVolumeAudioSession)
|
||||
{
|
||||
_current = GetSystemVolume();
|
||||
}
|
||||
}
|
||||
|
||||
_currentProcesses = processes;
|
||||
return _current;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the list of processes that might be currently selected
|
||||
/// This includes getting the child window's processes
|
||||
///
|
||||
/// This helps to find the audo process for windows store apps whose process is "ApplicationFrameHost.exe"
|
||||
///
|
||||
/// The list may optionally include a parent process, because that helps thing steam to be more reliable because the steamwebhelper (ui) is a child of steam.exe
|
||||
///
|
||||
/// According to deej, getting the ForegroundWindow and enumerating steam windows should work, but it doesn't seem to work for me without including the parent process
|
||||
/// https://github.com/omriharel/deej/blob/master/pkg/deej/util/util_windows.go#L22
|
||||
///
|
||||
/// but the parent process is sometimes useless (explorer, svchost, etc) so i filter some of them out because i felt like it when i wrote the code
|
||||
///
|
||||
/// I also experimented with grabbing the parent process and enumerating through the windows to see if that would help, but any time the parent process was an unexpected process (explorer) it could blow up. so i decided not to bother for now
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public List<Process> GetPossibleProcesses()
|
||||
{
|
||||
var handle = Native.GetForegroundWindow();
|
||||
|
||||
if (handle == IntPtr.Zero)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var ids = Native.GetProcessesOfChildWindows(handle);
|
||||
|
||||
Native.GetWindowThreadProcessId(handle, out var pid);
|
||||
ids.Insert(0, pid);
|
||||
|
||||
var processes = ids.Distinct()
|
||||
.Select(x => Process.GetProcessById(x))
|
||||
.ToList();
|
||||
|
||||
try
|
||||
{
|
||||
var blah = ParentProcessUtilities.GetParentProcess(pid);
|
||||
if (blah != null && blah.ProcessName != "explorer" && blah.ProcessName != "svchost")
|
||||
{
|
||||
processes.Add(blah);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return processes;
|
||||
|
||||
}
|
||||
|
||||
public void ResetAll()
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
327
src/FocusVolumeControl/AudioHelpers/AppxPackage.cs
Normal file
@ -0,0 +1,327 @@
|
||||
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);
|
||||
|
||||
if(displayName.StartsWith("ms-resource:"))
|
||||
{
|
||||
var packageFullName = Marshal.PtrToStringUni(info.packageFullName);
|
||||
displayName = LoadResourceString(fullName, 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static string LoadResourceString(string packageFullName, string resource)
|
||||
{
|
||||
if (packageFullName == null)
|
||||
throw new ArgumentNullException("packageFullName");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(resource))
|
||||
return null;
|
||||
|
||||
const string resourceScheme = "ms-resource:";
|
||||
if (!resource.StartsWith(resourceScheme))
|
||||
return null;
|
||||
|
||||
string part = resource.Substring(resourceScheme.Length);
|
||||
string url;
|
||||
|
||||
if (part.StartsWith("/"))
|
||||
{
|
||||
url = resourceScheme + "//" + part;
|
||||
}
|
||||
else
|
||||
{
|
||||
url = resourceScheme + "///resources/" + part;
|
||||
}
|
||||
|
||||
string source = string.Format("@{{{0}? {1}}}", packageFullName, url);
|
||||
var sb = new StringBuilder(1024);
|
||||
int i = SHLoadIndirectString(source, sb, sb.Capacity, IntPtr.Zero);
|
||||
if (i != 0)
|
||||
return null;
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
|
||||
[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
|
||||
}
|
499
src/FocusVolumeControl/AudioHelpers/AudioHelper.cs
Normal file
@ -0,0 +1,499 @@
|
||||
using FocusVolumeControl.AudioSessions;
|
||||
using FocusVolumeControl.Overrides;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace FocusVolumeControl.AudioHelpers;
|
||||
|
||||
public class AudioHelper
|
||||
{
|
||||
NameAndIconHelper _nameAndIconHelper = new NameAndIconHelper();
|
||||
|
||||
static object _lock = new object();
|
||||
int[] _currentProcesses;
|
||||
int _retryFallbackCount = 0;
|
||||
|
||||
public List<Override> Overrides { get; set; }
|
||||
public List<string> Ignored { get; set; }
|
||||
|
||||
public IAudioSession Current { get; private set; }
|
||||
|
||||
public void ResetCache()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
Current = null;
|
||||
}
|
||||
}
|
||||
|
||||
private Process GetProcessById(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Process.GetProcessById(id);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public IAudioSession FindSession(List<Process> processes)
|
||||
{
|
||||
var results = new ActiveAudioSessionWrapper();
|
||||
Process bestProcessMatch = null;
|
||||
|
||||
var deviceEnumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator();
|
||||
|
||||
deviceEnumerator.EnumAudioEndpoints(DataFlow.Render, DeviceState.Active, out var deviceCollection);
|
||||
deviceCollection.GetCount(out var numDevices);
|
||||
for (int d = 0; d < numDevices; d++)
|
||||
{
|
||||
deviceCollection.Item(d, out var device);
|
||||
|
||||
if(device == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Guid iid = typeof(IAudioSessionManager2).GUID;
|
||||
device.Activate(ref iid, CLSCTX.ALL, IntPtr.Zero, out var m);
|
||||
var manager = (IAudioSessionManager2)m;
|
||||
|
||||
device.GetId(out var currentDeviceId);
|
||||
|
||||
manager.GetSessionEnumerator(out var sessionEnumerator);
|
||||
|
||||
if(sessionEnumerator == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var currentIndex = int.MaxValue;
|
||||
|
||||
sessionEnumerator.GetCount(out var count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
sessionEnumerator.GetSession(i, out var session);
|
||||
|
||||
if(session == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
session.GetProcessId(out var sessionProcessId);
|
||||
var audioProcess = GetProcessById(sessionProcessId);
|
||||
|
||||
if(audioProcess == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var index = processes.FindIndex(x => x.Id == sessionProcessId || x.ProcessName == audioProcess?.ProcessName);
|
||||
|
||||
if (index > -1)
|
||||
{
|
||||
//processes will be ordered from best to worst (starts with the app, goes to parent)
|
||||
//so we want the display name and executable path to come from the process that is closest to the front of the list
|
||||
//but we want all matching sessions so things like discord work right
|
||||
if (index < currentIndex)
|
||||
{
|
||||
bestProcessMatch = audioProcess;
|
||||
currentIndex = index;
|
||||
|
||||
if(string.IsNullOrEmpty(results.DisplayName))
|
||||
{
|
||||
session.GetDisplayName(out var displayName);
|
||||
results.DisplayName = displayName;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//some apps like discord have multiple volume processes.
|
||||
//and some apps will be on multiple devices
|
||||
//so we add all sessions so we can keep them in sync
|
||||
results.AddSession(session);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(bestProcessMatch != null)
|
||||
{
|
||||
_nameAndIconHelper.SetProcessInfo(bestProcessMatch, results);
|
||||
}
|
||||
|
||||
return results.Any() ? results : null;
|
||||
}
|
||||
|
||||
public IAudioSession GetActiveSession(FallbackBehavior fallbackBehavior)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var processes = TryGetProcessFromOverrides();
|
||||
|
||||
if(processes == null)
|
||||
{
|
||||
processes = GetPossibleProcesses();
|
||||
}
|
||||
var processIds = processes?.Select(x => x.Id).ToArray();
|
||||
|
||||
//_currentProcesses null - first time getting sessions
|
||||
//_currentProcesses not equal to processIds - changed the active process
|
||||
//_retryFallbackCount - some processes like chrome or minecraft will start their audio process when they first try to do some sound stuff
|
||||
if (_currentProcesses == null || !_currentProcesses.SequenceEqual(processIds) || _retryFallbackCount == 5)
|
||||
{
|
||||
_retryFallbackCount = 0;
|
||||
|
||||
var newSession = FindSession(processes);
|
||||
|
||||
if(newSession != null || fallbackBehavior != FallbackBehavior.PreviousApp)
|
||||
{
|
||||
Current = newSession;
|
||||
}
|
||||
}
|
||||
else if(Current is SystemSoundsAudioSession || Current is SystemVolumeAudioSession)
|
||||
{
|
||||
_retryFallbackCount++;
|
||||
}
|
||||
|
||||
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(IntPtr? handleOverride = null)
|
||||
{
|
||||
var handle = handleOverride ?? Native.GetForegroundWindow();
|
||||
|
||||
if (handle == IntPtr.Zero)
|
||||
{
|
||||
return new List<Process>();
|
||||
}
|
||||
|
||||
var ids = Native.GetProcessesOfChildWindows(handle);
|
||||
|
||||
Native.GetWindowThreadProcessId(handle, out var pid);
|
||||
|
||||
if(pid != 0)
|
||||
{
|
||||
ids.Insert(0, pid);
|
||||
}
|
||||
|
||||
if(ids.Count == 0)
|
||||
{
|
||||
return new List<Process>();
|
||||
}
|
||||
|
||||
var processes = ids.Distinct()
|
||||
.Select(Process.GetProcessById)
|
||||
.ToList();
|
||||
|
||||
if(processes.FirstOrDefault()?.ProcessName == "explorer")
|
||||
{
|
||||
return new List<Process>();
|
||||
}
|
||||
|
||||
if(processes.Any(x => Ignored.Contains(x.ProcessName)))
|
||||
{
|
||||
return new List<Process>();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
//note. in instances where you launch a game from steam. this ends up mapping the process to both steam and to the game. which is unfortunate
|
||||
//The problem is that if you don't use the parent processes, then the actual steam window won't get recognized. But if you do, then games will map to steam.
|
||||
//
|
||||
//Additionally, I group all audio processes that match instead of just the most specific, or the first, etc. Because Discord uses two processes, one for voice chat, and one for discord sounds.
|
||||
//
|
||||
//Steam and Discord are both very common, and end up butting heads in the algorithm.
|
||||
//I want to avoid special cases, but since steam and discord are both so common, i'm making an exception.
|
||||
var parentProcess = ParentProcessUtilities.GetParentProcess(pid);
|
||||
if (parentProcess != null
|
||||
&& parentProcess.ProcessName != "explorer"
|
||||
&& parentProcess.ProcessName != "svchost"
|
||||
&& (parentProcess.ProcessName == "steam" && processes.Any(x => x.ProcessName == "steamwebhelper")) //only include steam if the parent process is the steamwebhelper
|
||||
)
|
||||
{
|
||||
processes.Add(parentProcess);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return processes;
|
||||
|
||||
}
|
||||
|
||||
public void ResetAll()
|
||||
{
|
||||
var deviceEnumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator();
|
||||
|
||||
deviceEnumerator.EnumAudioEndpoints(DataFlow.Render, DeviceState.Active, out var deviceCollection);
|
||||
|
||||
deviceCollection.GetCount(out var numDevices);
|
||||
for (int d = 0; d < numDevices; d++)
|
||||
{
|
||||
deviceCollection.Item(d, out var device);
|
||||
|
||||
if(device == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Guid iid = typeof(IAudioSessionManager2).GUID;
|
||||
device.Activate(ref iid, CLSCTX.ALL, IntPtr.Zero, out var m);
|
||||
var manager = (IAudioSessionManager2)m;
|
||||
|
||||
|
||||
manager.GetSessionEnumerator(out var sessionEnumerator);
|
||||
|
||||
if(sessionEnumerator == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
sessionEnumerator.GetCount(out var count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
sessionEnumerator.GetSession(i, out var session);
|
||||
|
||||
if(session == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if(device == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Guid iid = typeof(IAudioSessionManager2).GUID;
|
||||
device.Activate(ref iid, CLSCTX.ALL, IntPtr.Zero, out var m);
|
||||
var manager = (IAudioSessionManager2)m;
|
||||
|
||||
|
||||
manager.GetSessionEnumerator(out var sessionEnumerator);
|
||||
|
||||
if(sessionEnumerator == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
sessionEnumerator.GetCount(out var count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
sessionEnumerator.GetSession(i, out var session);
|
||||
|
||||
if(session == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
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, CLSCTX.ALL, IntPtr.Zero, out var o);
|
||||
var endpointVolume = (IAudioEndpointVolume)o;
|
||||
|
||||
return new SystemVolumeAudioSession(endpointVolume);
|
||||
}
|
||||
|
||||
|
||||
static Dictionary<string, List<int>> _audioSessionNameCache = new Dictionary<string, List<int>>();
|
||||
private List<int> GetFromNameCacheIfPossible(string processName)
|
||||
{
|
||||
if(_audioSessionNameCache.TryGetValue(processName, out var result))
|
||||
{
|
||||
if(result.Count!= 0)
|
||||
{
|
||||
foreach(var pid in result)
|
||||
{
|
||||
var p = GetProcessById(pid);
|
||||
if(p == null || p.ProcessName != processName)
|
||||
{
|
||||
_audioSessionNameCache.Remove(processName);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<Process> TryGetProcessFromOverrides(IntPtr? handleOverride = null)
|
||||
{
|
||||
var handle = handleOverride ?? Native.GetForegroundWindow();
|
||||
|
||||
if (Overrides?.Any() == true)
|
||||
{
|
||||
Process tmp = null;
|
||||
foreach (var p in Process.GetProcesses())
|
||||
{
|
||||
if (p.MainWindowHandle == handle)
|
||||
{
|
||||
tmp = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (tmp != null)
|
||||
{
|
||||
foreach (var o in Overrides)
|
||||
{
|
||||
if (
|
||||
(o.MatchType == MatchType.Equal && tmp.MainWindowTitle.Equals(o.WindowQuery, StringComparison.InvariantCultureIgnoreCase))
|
||||
|| (o.MatchType == MatchType.StartsWith && tmp.MainWindowTitle.StartsWith(o.WindowQuery, StringComparison.OrdinalIgnoreCase))
|
||||
|| (o.MatchType == MatchType.EndsWith && tmp.MainWindowTitle.EndsWith(o.WindowQuery, StringComparison.OrdinalIgnoreCase))
|
||||
|| (o.MatchType == MatchType.Regex && Regex.IsMatch(tmp.MainWindowTitle, o.WindowQuery))
|
||||
)
|
||||
{
|
||||
var ids = FindAudioSessionByProcessName(o.AudioProcessName);
|
||||
if (ids?.Count > 0)
|
||||
{
|
||||
return ids.Distinct()
|
||||
.Select(Process.GetProcessById)
|
||||
.ToList();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<int> FindAudioSessionByProcessName(string processName)
|
||||
{
|
||||
var cached = GetFromNameCacheIfPossible(processName);
|
||||
if(cached != null)
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
var results = new List<int>();
|
||||
|
||||
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);
|
||||
|
||||
if(device == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Guid iid = typeof(IAudioSessionManager2).GUID;
|
||||
device.Activate(ref iid, CLSCTX.ALL, IntPtr.Zero, out var m);
|
||||
var manager = (IAudioSessionManager2)m;
|
||||
|
||||
device.GetId(out var currentDeviceId);
|
||||
|
||||
manager.GetSessionEnumerator(out var sessionEnumerator);
|
||||
|
||||
if(sessionEnumerator == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
sessionEnumerator.GetCount(out var count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
sessionEnumerator.GetSession(i, out var session);
|
||||
|
||||
if(session == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
session.GetDisplayName(out var displayName);
|
||||
session.GetProcessId(out var sessionProcessId);
|
||||
|
||||
var audioProcess = GetProcessById(sessionProcessId);
|
||||
|
||||
if(audioProcess == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var audioProcessName = _nameAndIconHelper.TryGetProcessNameWithoutIcon(audioProcess);
|
||||
if(audioProcess.ProcessName == processName || displayName == processName || processName == audioProcessName)
|
||||
{
|
||||
results.Add(sessionProcessId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results = results.Distinct().ToList();
|
||||
|
||||
_audioSessionNameCache[processName] = results;
|
||||
return results;
|
||||
}
|
||||
|
||||
}
|
173
src/FocusVolumeControl/AudioHelpers/NameAndIconHelper.cs
Normal file
@ -0,0 +1,173 @@
|
||||
using BarRaider.SdTools;
|
||||
using FocusVolumeControl.AudioSession;
|
||||
using FocusVolumeControl.AudioSessions;
|
||||
using FocusVolumeControl.UI;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace FocusVolumeControl.AudioHelpers;
|
||||
|
||||
public class NameAndIconHelper
|
||||
{
|
||||
public string 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;
|
||||
}
|
||||
|
||||
public void SetProcessInfo(Process process, ActiveAudioSessionWrapper results)
|
||||
{
|
||||
try
|
||||
{
|
||||
//appx packages are installed from the windows store. eg, itunes
|
||||
var appx = AppxPackage.FromProcess(process);
|
||||
if (appx == null)
|
||||
{
|
||||
//using process.MainModule.FileVersionInfo sometimes throws permission exceptions
|
||||
//we get the file version info with a limited query flag to avoid that
|
||||
var fileVersionInfo = GetFileVersionInfo(process);
|
||||
|
||||
//if the display name is already set, then it came from the display name of the audio session
|
||||
if (string.IsNullOrEmpty(results.DisplayName))
|
||||
{
|
||||
results.DisplayName = process.MainWindowTitle;
|
||||
|
||||
if (string.IsNullOrEmpty(results.DisplayName))
|
||||
{
|
||||
results.DisplayName = fileVersionInfo?.FileDescription;
|
||||
if (string.IsNullOrEmpty(results.DisplayName))
|
||||
{
|
||||
results.DisplayName = process.ProcessName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//for java apps (minecraft), the process will just have a java icon
|
||||
//and there's not just a file that you can get the real icon from
|
||||
//so you have to send some messages to the apps to get the icons.
|
||||
//but they will only be 32x32 (or smaller) so we only want to use this logic for java
|
||||
//because these will be lower resolution than the normal way of getting icons
|
||||
if (process.ProcessName == "javaw" || process.ProcessName == "java" || process.ProcessName == "dotnet")
|
||||
{
|
||||
var windowHandle = process.MainWindowHandle;
|
||||
var lazyIcon = () => JavaIconExtractor.GetWindowBigIconWithRetry(windowHandle);
|
||||
results.IconWrapper = new RawIcon(windowHandle.ToString(), lazyIcon);
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
results.IconWrapper = new NormalIcon(fileVersionInfo?.FileName);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
results.DisplayName = appx.DisplayName;
|
||||
results.IconWrapper = new AppxIcon(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string TryGetProcessNameWithoutIcon(Process process)
|
||||
{
|
||||
try
|
||||
{
|
||||
//appx packages are installed from the windows store. eg, itunes
|
||||
var appx = AppxPackage.FromProcess(process);
|
||||
if (appx == null)
|
||||
{
|
||||
|
||||
//if the display name is already set, then it came from the display name of the audio session
|
||||
if(!string.IsNullOrEmpty(process.MainWindowTitle))
|
||||
{
|
||||
return process.MainWindowTitle;
|
||||
}
|
||||
|
||||
//using process.MainModule.FileVersionInfo sometimes throws permission exceptions
|
||||
//we get the file version info with a limited query flag to avoid that
|
||||
var fileVersionInfo = GetFileVersionInfo(process);
|
||||
if(!string.IsNullOrEmpty(fileVersionInfo?.FileDescription))
|
||||
{
|
||||
return fileVersionInfo.FileDescription;
|
||||
}
|
||||
|
||||
return process.ProcessName;
|
||||
}
|
||||
else
|
||||
{
|
||||
return appx.DisplayName ?? process.ProcessName;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
return 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,46 +1,50 @@
|
||||
using CoreAudio;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BarRaider.SdTools;
|
||||
using System.Drawing;
|
||||
using System.Runtime.InteropServices;
|
||||
using FocusVolumeControl.UI;
|
||||
using BitFaster.Caching.Lru;
|
||||
using FocusVolumeControl.AudioSession;
|
||||
|
||||
namespace FocusVolumeControl.AudioSessions;
|
||||
|
||||
public class ActiveAudioSessionWrapper : IAudioSession
|
||||
{
|
||||
public string DisplayName { get; set; }
|
||||
public string ExecutablePath { get; set; }
|
||||
private List<SimpleAudioVolume> Volume { get; } = new List<SimpleAudioVolume>();
|
||||
#nullable enable
|
||||
|
||||
string _icon;
|
||||
public sealed class ActiveAudioSessionWrapper : IAudioSession
|
||||
{
|
||||
public string DisplayName { get; set; } = null!;
|
||||
private List<IAudioSessionControl2> Sessions { get; } = new List<IAudioSessionControl2>();
|
||||
private IEnumerable<ISimpleAudioVolume> Volume => Sessions.Cast<ISimpleAudioVolume>();
|
||||
|
||||
public IconWrapper? IconWrapper { get; set; }
|
||||
public IEnumerable<int> Pids => Sessions.Select(x =>
|
||||
{
|
||||
x.GetProcessId(out var pid); return pid;
|
||||
});
|
||||
|
||||
public string GetIcon()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_icon))
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
var tmp = Icon.ExtractAssociatedIcon(ExecutablePath);
|
||||
_icon = Tools.ImageToBase64(tmp.ToBitmap(), true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_icon = "Image/encoderIcon";
|
||||
}
|
||||
return IconWrapper?.GetIconData() ?? IconWrapper.FallbackIconData;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return IconWrapper.FallbackIconData;
|
||||
}
|
||||
return _icon;
|
||||
}
|
||||
|
||||
public bool Any()
|
||||
{
|
||||
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,28 +55,53 @@ 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);
|
||||
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;
|
||||
var volume = Volume.FirstOrDefault();
|
||||
var level = 0f;
|
||||
if(volume != null)
|
||||
{
|
||||
volume.GetMasterVolume(out level);
|
||||
}
|
||||
|
||||
return VolumeHelpers.GetVolumePercentage(level);
|
||||
}
|
||||
|
||||
}
|
||||
#nullable restore
|
||||
|
454
src/FocusVolumeControl/AudioSessions/CoreAudio.cs
Normal file
@ -0,0 +1,454 @@
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Runtime.InteropServices;
|
||||
using FILETIME = System.Runtime.InteropServices.ComTypes.FILETIME;
|
||||
|
||||
namespace FocusVolumeControl.AudioSessions;
|
||||
|
||||
|
||||
[ComImport]
|
||||
[Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")]
|
||||
public class MMDeviceEnumerator
|
||||
{
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum CLSCTX : uint
|
||||
{
|
||||
INPROC_SERVER = 0x1,
|
||||
INPROC_HANDLER = 0x2,
|
||||
LOCAL_SERVER = 0x4,
|
||||
INPROC_SERVER16 = 0x8,
|
||||
REMOTE_SERVER = 0x10,
|
||||
INPROC_HANDLER16 = 0x20,
|
||||
RESERVED1 = 0x40,
|
||||
RESERVED2 = 0x80,
|
||||
RESERVED3 = 0x100,
|
||||
RESERVED4 = 0x200,
|
||||
NO_CODE_DOWNLOAD = 0x400,
|
||||
RESERVED5 = 0x800,
|
||||
NO_CUSTOM_MARSHAL = 0x1000,
|
||||
ENABLE_CODE_DOWNLOAD = 0x2000,
|
||||
NO_FAILURE_LOG = 0x4000,
|
||||
DISABLE_AAA = 0x8000,
|
||||
ENABLE_AAA = 0x10000,
|
||||
FROM_DEFAULT_CONTEXT = 0x20000,
|
||||
INPROC = INPROC_SERVER | INPROC_HANDLER,
|
||||
SERVER = INPROC_SERVER | LOCAL_SERVER | REMOTE_SERVER,
|
||||
ALL = SERVER | INPROC_HANDLER
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
public enum AudioSessionState
|
||||
{
|
||||
AudioSessionStateInactive = 0,
|
||||
AudioSessionStateActive = 1,
|
||||
AudioSessionStateExpired = 2
|
||||
}
|
||||
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
public enum EStgmAccess
|
||||
{
|
||||
STGM_READ = 0x0
|
||||
}
|
||||
|
||||
[Guid("886d8eeb-8cf2-4446-8d02-cdba1dbdcf99"),
|
||||
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IPropertyStore
|
||||
{
|
||||
[PreserveSig]
|
||||
int GetCount(out int count);
|
||||
[PreserveSig]
|
||||
int GetAt(int iProp, out PropertyKey pkey);
|
||||
[PreserveSig]
|
||||
int GetValue(ref PropertyKey key, out PropVariant pv);
|
||||
[PreserveSig]
|
||||
int SetValue(ref PropertyKey key, ref PropVariant propvar);
|
||||
[PreserveSig]
|
||||
int Commit();
|
||||
}
|
||||
|
||||
public struct PropertyKey
|
||||
{
|
||||
public Guid fmtId;
|
||||
public int PId;
|
||||
|
||||
public PropertyKey(Guid fmtId, int pId)
|
||||
{
|
||||
this.fmtId = fmtId;
|
||||
this.PId = pId;
|
||||
}
|
||||
}
|
||||
public struct Blob
|
||||
{
|
||||
public int Length;
|
||||
public IntPtr Data;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
public struct PropVariant
|
||||
{
|
||||
[FieldOffset(0)] short vt;
|
||||
[FieldOffset(2)] short wReserved1;
|
||||
[FieldOffset(4)] short wReserved2;
|
||||
[FieldOffset(6)] short wReserved3;
|
||||
[FieldOffset(8)] sbyte cVal;
|
||||
[FieldOffset(8)] byte bVal;
|
||||
[FieldOffset(8)] short iVal;
|
||||
[FieldOffset(8)] ushort uiVal;
|
||||
[FieldOffset(8)] int lVal;
|
||||
[FieldOffset(8)] uint ulVal;
|
||||
[FieldOffset(8)] long hVal;
|
||||
[FieldOffset(8)] ulong uhVal;
|
||||
[FieldOffset(8)] float fltVal;
|
||||
[FieldOffset(8)] double dblVal;
|
||||
[FieldOffset(8)] Blob blobVal;
|
||||
[FieldOffset(8)] DateTime date;
|
||||
[FieldOffset(8)] bool boolVal;
|
||||
[FieldOffset(8)] int scode;
|
||||
[FieldOffset(8)] FILETIME filetime;
|
||||
[FieldOffset(8)] IntPtr everything_else;
|
||||
|
||||
public object Value
|
||||
{
|
||||
get
|
||||
{
|
||||
var ve = (VarEnum)vt;
|
||||
|
||||
if((VarEnum)vt == VarEnum.VT_LPWSTR)
|
||||
{
|
||||
return Marshal.PtrToStringUni(everything_else);
|
||||
}
|
||||
throw new Exception();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class PKey
|
||||
{
|
||||
public static readonly PropertyKey DeviceFriendlyName = new(new(0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0), 14);
|
||||
public static readonly PropertyKey DeviceFriendlyNameAttributes = new(new(0x80d81ea6, 0x7473, 0x4b0c, 0x82, 0x16, 0xef, 0xc1, 0x1a, 0x2c, 0x4c, 0x8b), 3);
|
||||
public static readonly PropertyKey DeviceInterfaceFriendlyName = new(new(0x026e516e, 0xb814, 0x414b, 0x83, 0xcd, 0x85, 0x6d, 0x6f, 0xef, 0x48, 0x22), 2);
|
||||
}
|
||||
|
||||
[Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IMMDevice
|
||||
{
|
||||
[PreserveSig]
|
||||
int Activate(ref Guid iid, CLSCTX dwClsCtx, IntPtr pActivationParams, [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface);
|
||||
|
||||
[PreserveSig]
|
||||
int OpenPropertyStore(EStgmAccess stgmAccess, out IPropertyStore propertyStore);
|
||||
[PreserveSig]
|
||||
int GetId([Out, MarshalAs(UnmanagedType.LPWStr)] out string ppstrId);
|
||||
|
||||
}
|
||||
|
||||
[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
|
||||
{
|
||||
//elgato seems to use this to determine whether the icon should be black and white
|
||||
[PreserveSig]
|
||||
int GetState(out AudioSessionState audioSessionState);
|
||||
|
||||
[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]
|
||||
uint 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,8 @@
|
||||
namespace FocusVolumeControl.AudioSessions;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace FocusVolumeControl.AudioSessions;
|
||||
|
||||
public interface IAudioSession
|
||||
{
|
||||
@ -13,4 +17,6 @@ public interface IAudioSession
|
||||
public void IncrementVolumeLevel(int step, int ticks);
|
||||
|
||||
public int GetVolumeLevel();
|
||||
|
||||
public IEnumerable<int> Pids { get; }
|
||||
}
|
||||
|
106
src/FocusVolumeControl/AudioSessions/IconWrapper.cs
Normal file
@ -0,0 +1,106 @@
|
||||
using BarRaider.SdTools;
|
||||
using BitFaster.Caching.Lru;
|
||||
using FocusVolumeControl.UI;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace FocusVolumeControl.AudioSession
|
||||
{
|
||||
public abstract class IconWrapper
|
||||
{
|
||||
protected static ConcurrentLru<string, string> _iconCache = new ConcurrentLru<string, string>(10);
|
||||
|
||||
public abstract string GetIconData();
|
||||
|
||||
internal const string FallbackIconData = "Images/encoderIcon";
|
||||
}
|
||||
|
||||
internal class AppxIcon : IconWrapper
|
||||
{
|
||||
private readonly string _iconPath;
|
||||
|
||||
public AppxIcon(string iconPath)
|
||||
{
|
||||
_iconPath = iconPath;
|
||||
}
|
||||
|
||||
public override string GetIconData()
|
||||
{
|
||||
if(string.IsNullOrEmpty(_iconPath))
|
||||
{
|
||||
return FallbackIconData;
|
||||
}
|
||||
|
||||
return _iconCache.GetOrAdd(_iconPath, (key) =>
|
||||
{
|
||||
var tmp = (Bitmap)Bitmap.FromFile(_iconPath);
|
||||
tmp.MakeTransparent();
|
||||
return Tools.ImageToBase64(tmp, true);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal class NormalIcon : IconWrapper
|
||||
{
|
||||
private readonly string _iconPath;
|
||||
|
||||
public NormalIcon(string iconPath)
|
||||
{
|
||||
_iconPath = iconPath;
|
||||
}
|
||||
|
||||
public override string GetIconData()
|
||||
{
|
||||
if(string.IsNullOrEmpty(_iconPath))
|
||||
{
|
||||
return FallbackIconData;
|
||||
}
|
||||
|
||||
return _iconCache.GetOrAdd(_iconPath, (key) =>
|
||||
{
|
||||
var tmp = IconExtraction.GetIcon(_iconPath);
|
||||
return Tools.ImageToBase64(tmp, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
internal class RawIcon : IconWrapper
|
||||
{
|
||||
private readonly string _data;
|
||||
|
||||
public RawIcon(string name, Func<Bitmap?> getIcon)
|
||||
{
|
||||
_data = _iconCache.GetOrAdd(name, (key) =>
|
||||
{
|
||||
var icon = getIcon();
|
||||
if (icon == null)
|
||||
{
|
||||
return FallbackIconData;
|
||||
}
|
||||
|
||||
if (icon.Height < 48 && icon.Width < 48)
|
||||
{
|
||||
using var newImage = new Bitmap(48, 48);
|
||||
newImage.MakeTransparent();
|
||||
using var graphics = Graphics.FromImage(newImage);
|
||||
|
||||
graphics.DrawImage(icon, 4, 4, 40, 40);
|
||||
|
||||
|
||||
return Tools.ImageToBase64(newImage, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Tools.ImageToBase64(icon, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public override string GetIconData() => _data;
|
||||
}
|
||||
|
||||
}
|
||||
#nullable restore
|
@ -1,33 +1,50 @@
|
||||
using CoreAudio;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
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 IEnumerable<int> Pids => new int[0];
|
||||
|
||||
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 = VolumeHelpers.GetAdjustedVolume(_volumeControl.MasterVolume, step, ticks);
|
||||
_volumeControl.MasterVolume = level;
|
||||
_volumeControl.GetMasterVolume(out var level);
|
||||
level = VolumeHelpers.GetAdjustedVolume(level, step, ticks);
|
||||
|
||||
var guid = Guid.Empty;
|
||||
_volumeControl.SetMasterVolume(level, ref guid);
|
||||
}
|
||||
|
||||
public int GetVolumeLevel() => VolumeHelpers.GetVolumePercentage(_volumeControl.MasterVolume);
|
||||
|
||||
public int GetVolumeLevel()
|
||||
{
|
||||
_volumeControl.GetMasterVolume(out var level);
|
||||
return VolumeHelpers.GetVolumePercentage(level);
|
||||
}
|
||||
}
|
||||
|
@ -1,33 +1,44 @@
|
||||
using CoreAudio;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
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 IEnumerable<int> Pids => new int[0];
|
||||
|
||||
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 = VolumeHelpers.GetAdjustedVolume(_volumeControl.MasterVolumeLevelScalar, step, ticks);
|
||||
_volumeControl.MasterVolumeLevelScalar = level;
|
||||
_volumeControl.GetMasterVolumeLevelScalar(out var level);
|
||||
level = VolumeHelpers.GetAdjustedVolume(level, step, ticks);
|
||||
_volumeControl.SetMasterVolumeLevelScalar(level, Guid.Empty);
|
||||
}
|
||||
|
||||
public int GetVolumeLevel() => VolumeHelpers.GetVolumePercentage(_volumeControl.MasterVolumeLevelScalar);
|
||||
|
||||
public int GetVolumeLevel()
|
||||
{
|
||||
_volumeControl.GetMasterVolumeLevelScalar(out var level);
|
||||
return VolumeHelpers.GetVolumePercentage(level);
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,11 @@ namespace FocusVolumeControl.AudioSessions
|
||||
{
|
||||
public static float GetAdjustedVolume(float startingVolume, int step, int ticks)
|
||||
{
|
||||
if(step <= 0)
|
||||
{
|
||||
step = 1;
|
||||
}
|
||||
|
||||
var level = startingVolume;
|
||||
|
||||
level += 0.01f * step * ticks;
|
||||
|
@ -1,6 +1,8 @@
|
||||
using BarRaider.SdTools;
|
||||
using BarRaider.SdTools.Payloads;
|
||||
using FocusVolumeControl.AudioHelpers;
|
||||
using FocusVolumeControl.AudioSessions;
|
||||
using FocusVolumeControl.Overrides;
|
||||
using FocusVolumeControl.UI;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@ -14,6 +16,12 @@ namespace FocusVolumeControl;
|
||||
[PluginActionId("com.dlprows.focusvolumecontrol.dialaction")]
|
||||
public class DialAction : EncoderBase
|
||||
{
|
||||
private const string DefaultOverrides =
|
||||
"""
|
||||
//eq: HELLDIVERS™ 2
|
||||
//helldivers2
|
||||
""";
|
||||
|
||||
private class PluginSettings
|
||||
{
|
||||
[JsonProperty("fallbackBehavior")]
|
||||
@ -22,25 +30,25 @@ public class DialAction : EncoderBase
|
||||
[JsonProperty("stepSize")]
|
||||
public int StepSize { get; set; }
|
||||
|
||||
[JsonProperty("overrides")]
|
||||
public string Overrides { get; set; }
|
||||
|
||||
[JsonProperty("ignored")]
|
||||
public string Ignored { get; set; }
|
||||
|
||||
public static PluginSettings CreateDefaultSettings()
|
||||
{
|
||||
PluginSettings instance = new PluginSettings();
|
||||
instance.FallbackBehavior = FallbackBehavior.SystemSounds;
|
||||
instance.StepSize = 1;
|
||||
instance.Overrides = DefaultOverrides;
|
||||
instance.Ignored = "";
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
private PluginSettings settings;
|
||||
|
||||
IntPtr _foregroundWindowChangedEvent;
|
||||
Native.WinEventDelegate _delegate;
|
||||
|
||||
IAudioSession _currentAudioSession;
|
||||
AudioHelper _audioHelper = new AudioHelper();
|
||||
|
||||
Thread _thread;
|
||||
Dispatcher _dispatcher;
|
||||
|
||||
PluginSettings settings;
|
||||
static AudioHelper _audioHelper = new AudioHelper();
|
||||
UIState _previousState;
|
||||
|
||||
public DialAction(ISDConnection connection, InitialPayload payload) : base(connection, payload)
|
||||
@ -48,46 +56,53 @@ public class DialAction : EncoderBase
|
||||
if (payload.Settings == null || payload.Settings.Count == 0)
|
||||
{
|
||||
settings = PluginSettings.CreateDefaultSettings();
|
||||
SaveSettings();
|
||||
_ = SaveSettings();
|
||||
}
|
||||
else
|
||||
{
|
||||
settings = payload.Settings.ToObject<PluginSettings>();
|
||||
bool save = false;
|
||||
if(string.IsNullOrEmpty(settings.Overrides))
|
||||
{
|
||||
settings.Overrides = DefaultOverrides;
|
||||
save = true;
|
||||
}
|
||||
if(string.IsNullOrEmpty(settings.Ignored))
|
||||
{
|
||||
settings.Ignored = "";
|
||||
save = true;
|
||||
}
|
||||
|
||||
if(save)
|
||||
{
|
||||
_ = SaveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
_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);
|
||||
|
||||
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();
|
||||
_ = UpdateStateIfNeeded();
|
||||
_audioHelper.Overrides = OverrideParser.Parse(settings.Overrides);
|
||||
_audioHelper.Ignored = IgnoreParser.Parse(settings.Ignored);
|
||||
//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 { }
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
Logger.Instance.LogMessage(TracingLevel.DEBUG, "Disposing");
|
||||
if (_foregroundWindowChangedEvent != IntPtr.Zero)
|
||||
{
|
||||
Native.UnhookWinEvent(_foregroundWindowChangedEvent);
|
||||
}
|
||||
_dispatcher.InvokeShutdown();
|
||||
//Logger.Instance.LogMessage(TracingLevel.DEBUG, "Disposing");
|
||||
WindowChangedEventLoop.Instance.WindowChanged -= WindowChanged;
|
||||
}
|
||||
|
||||
public override async void DialDown(DialPayload payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Down");
|
||||
//Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Down");
|
||||
await ToggleMuteAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
@ -101,7 +116,7 @@ public class DialAction : EncoderBase
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.Instance.LogMessage(TracingLevel.INFO, "Touch Press");
|
||||
//Logger.Instance.LogMessage(TracingLevel.INFO, "Touch Press");
|
||||
if (payload.IsLongPress)
|
||||
{
|
||||
await ResetAllAsync();
|
||||
@ -121,12 +136,13 @@ public class DialAction : EncoderBase
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Rotate");
|
||||
//Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Rotate");
|
||||
//dial rotated. ticks positive for right, negative for left
|
||||
if (_currentAudioSession != null)
|
||||
var activeSession = _audioHelper.Current;
|
||||
if (activeSession != null)
|
||||
{
|
||||
_currentAudioSession.IncrementVolumeLevel(settings.StepSize, payload.Ticks);
|
||||
await UpdateStateIfNeeded();
|
||||
activeSession.IncrementVolumeLevel(settings.StepSize, payload.Ticks);
|
||||
await UpdateStateIfNeeded(activeSession);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -159,10 +175,11 @@ 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
|
||||
{
|
||||
@ -184,12 +201,7 @@ public class DialAction : EncoderBase
|
||||
//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)
|
||||
{
|
||||
_currentAudioSession = activeSession;
|
||||
}
|
||||
|
||||
await UpdateStateIfNeeded();
|
||||
await UpdateStateIfNeeded(activeSession);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -198,14 +210,14 @@ public class DialAction : EncoderBase
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateStateIfNeeded()
|
||||
private async Task UpdateStateIfNeeded(IAudioSession audioSession)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_currentAudioSession != null)
|
||||
if (audioSession != null)
|
||||
{
|
||||
|
||||
var uiState = new UIState(_currentAudioSession);
|
||||
var uiState = new UIState(audioSession);
|
||||
|
||||
if (_previousState != null && uiState != null &&
|
||||
uiState.Title == _previousState.Title &&
|
||||
@ -241,7 +253,9 @@ public class DialAction : EncoderBase
|
||||
try
|
||||
{
|
||||
Tools.AutoPopulateSettings(settings, payload.Settings);
|
||||
SaveSettings();
|
||||
_audioHelper.Overrides = OverrideParser.Parse(settings.Overrides);
|
||||
_audioHelper.Ignored = IgnoreParser.Parse(settings.Ignored);
|
||||
//_ = SaveSettings();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -262,7 +276,7 @@ public class DialAction : EncoderBase
|
||||
}
|
||||
|
||||
|
||||
public void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
|
||||
public void WindowChanged()
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -270,7 +284,7 @@ public class DialAction : EncoderBase
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unexpected Error in DialDown:\n {ex}");
|
||||
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unexpected Error in Window Down:\n {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -54,21 +54,33 @@
|
||||
<Reference Include="WindowsBase" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="AudioHelpers\AppxPackage.cs" />
|
||||
<Compile Include="AudioSessions\IconWrapper.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="InternalsVisibleTo.cs" />
|
||||
<Compile Include="Overrides\IgnoreParser.cs" />
|
||||
<Compile Include="Overrides\Override.cs" />
|
||||
<Compile Include="Overrides\MatchType.cs" />
|
||||
<Compile Include="Overrides\OverrideParser.cs" />
|
||||
<Compile Include="UI\IconExtraction.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\JavaIconExtractor.cs" />
|
||||
<Compile Include="UI\UIState.cs" />
|
||||
<Compile Include="UI\ValueWithOpacity.cs" />
|
||||
<Compile Include="WindowChangedEventLoop.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="App.config" />
|
||||
@ -84,7 +96,7 @@
|
||||
<Content Include="Images\**\*.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="PropertyInspector\**\*.js;PropertyInspector\**\*.css">
|
||||
<Content Include="PropertyInspector\**\*.js;PropertyInspector\**\*.css;PropertyInspector\assets\*.*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="PropertyInspector\PluginActionPI.html">
|
||||
@ -92,8 +104,8 @@
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CoreAudio">
|
||||
<Version>1.27.0</Version>
|
||||
<PackageReference Include="BitFaster.Caching">
|
||||
<Version>2.2.1</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="IsExternalInit">
|
||||
<Version>1.0.3</Version>
|
||||
|
4
src/FocusVolumeControl/InternalsVisibleTo.cs
Normal file
@ -0,0 +1,4 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("FocusVolumeControl.UnitTests")]
|
||||
[assembly: InternalsVisibleTo("SoundBrowser")]
|
@ -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);
|
||||
|
||||
|
||||
}
|
||||
|
36
src/FocusVolumeControl/Overrides/IgnoreParser.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FocusVolumeControl.Overrides
|
||||
{
|
||||
internal class IgnoreParser
|
||||
{
|
||||
public static List<string> Parse(string raw)
|
||||
{
|
||||
var ignores = new List<string>();
|
||||
|
||||
if (raw == null)
|
||||
{
|
||||
return ignores;
|
||||
}
|
||||
|
||||
var lines = raw.Split(new[] { "\r\n", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (string.IsNullOrEmpty(line) || line.StartsWith("//"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var str = line.Trim();
|
||||
ignores.Add(line.Trim());
|
||||
}
|
||||
|
||||
return ignores;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
10
src/FocusVolumeControl/Overrides/MatchType.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace FocusVolumeControl.Overrides
|
||||
{
|
||||
public enum MatchType
|
||||
{
|
||||
Equal,
|
||||
StartsWith,
|
||||
EndsWith,
|
||||
Regex,
|
||||
}
|
||||
}
|
15
src/FocusVolumeControl/Overrides/Override.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FocusVolumeControl.Overrides
|
||||
{
|
||||
public class Override
|
||||
{
|
||||
public MatchType MatchType { get; set; }
|
||||
public string WindowQuery { get; set; }
|
||||
public string AudioProcessName { get; set; }
|
||||
}
|
||||
}
|
78
src/FocusVolumeControl/Overrides/OverrideParser.cs
Normal file
@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FocusVolumeControl.Overrides
|
||||
{
|
||||
internal class OverrideParser
|
||||
{
|
||||
public static List<Override> Parse(string raw)
|
||||
{
|
||||
var overrides = new List<Override>();
|
||||
|
||||
if (raw == null)
|
||||
{
|
||||
return overrides;
|
||||
}
|
||||
|
||||
var lines = raw.Split(new[] { "\r\n", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
|
||||
Override tmp = null;
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
if (string.IsNullOrEmpty(line) || line.StartsWith("//"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var split = line.Split(':');
|
||||
if (split.Length > 1)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(tmp?.WindowQuery) && !string.IsNullOrEmpty(tmp?.AudioProcessName))
|
||||
{
|
||||
overrides.Add(tmp);
|
||||
}
|
||||
tmp = new Override();
|
||||
|
||||
if (string.Equals(split[0], "eq", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
tmp.MatchType = MatchType.Equal;
|
||||
}
|
||||
else if (string.Equals(split[0], "start", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
tmp.MatchType = MatchType.StartsWith;
|
||||
}
|
||||
else if (string.Equals(split[0], "end", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
tmp.MatchType = MatchType.EndsWith;
|
||||
}
|
||||
else if (string.Equals(split[0], "regex", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
tmp.MatchType = MatchType.Regex;
|
||||
}
|
||||
else
|
||||
{
|
||||
continue;
|
||||
}
|
||||
tmp.WindowQuery = split[1].Trim();
|
||||
}
|
||||
else if (tmp != null)
|
||||
{
|
||||
tmp.AudioProcessName = split[0].Trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(tmp?.WindowQuery) && !string.IsNullOrEmpty(tmp?.AudioProcessName))
|
||||
{
|
||||
overrides.Add(tmp);
|
||||
}
|
||||
|
||||
|
||||
return overrides;
|
||||
}
|
||||
}
|
||||
}
|
@ -6,8 +6,10 @@ internal class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
#if DEBUG
|
||||
// Uncomment this line of code to allow for debugging
|
||||
//while (!System.Diagnostics.Debugger.IsAttached) { System.Threading.Thread.Sleep(100); }
|
||||
#endif
|
||||
|
||||
SDWrapper.Run(args);
|
||||
}
|
||||
|
@ -6,8 +6,8 @@
|
||||
<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="./lib/sdpi.css">
|
||||
<link rel="sytlesheet" href="./lib/rangeTooltip.css">
|
||||
<link rel="stylesheet" href="./css/sdpi.css">
|
||||
<link rel="sytlesheet" href="./css/rangeTooltip.css">
|
||||
<script src="lib/sdtools.common.js"></script>
|
||||
<script src="lib/rangeTooltip.js"></script>
|
||||
</head>
|
||||
@ -16,13 +16,20 @@
|
||||
|
||||
<div class="sdpi-item">
|
||||
<div class="sdpi-item-label">Fallback</div>
|
||||
<select class="sdpi-item-value sdProperty" id="fallbackBehavior" oninput="setSettings()">
|
||||
<select class="sdpi-item-value select 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>
|
||||
<summary>Fallback Details</summary>
|
||||
<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 type="range" class="sdpi-item sdShowTooltip">
|
||||
<div class="sdpi-item-label">Step Size</div>
|
||||
@ -32,13 +39,43 @@
|
||||
<span class="clickable" value="1">10</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sdpi-info-label hidden" style="top: -1000;" value="">Tooltip</div>
|
||||
<div class="sdpi-info-label hidden" style="z-index: 999;" value="">Tooltip</div>
|
||||
|
||||
<div type="textarea" class="sdpi-item">
|
||||
<div class="sdpi-item-label">Overrides</div>
|
||||
<span class="sdpi-item-value" textarea>
|
||||
<textarea type="textarea" class="sdProperty" id="overrides" oninput="setSettings()"></textarea>
|
||||
</span>
|
||||
</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>
|
||||
<summary>Override Details</summary>
|
||||
<p>Some games use anti-cheat software that interferes with the ability to know what application is running, and pair it to the appropriate audio process.</p>
|
||||
<p>There is nothing I can do about this.</p>
|
||||
<p>In order to work around this, the overrides mechanism is there for you to manually map an application window to an audio process.</p>
|
||||
<p>They synax for this is</p>
|
||||
<p>[matching]: Window Title <br />Audio Process</p>
|
||||
<p>Blank lines can be used for spacing<br/>Lines starting with // are ignored</p>
|
||||
<p>[matching] can be eq, start, end, or regex. Which will perform an exact match, starts with, ends with, or regular expressions respectively.</p>
|
||||
<p>Example:<br />eq: Task Manager<br />Discord</p>
|
||||
</details>
|
||||
|
||||
|
||||
<div type="textarea" class="sdpi-item">
|
||||
<div class="sdpi-item-label">Ignored</div>
|
||||
<span class="sdpi-item-value" textarea>
|
||||
<textarea type="textarea" class="sdProperty" id="ignored" oninput="setSettings()"></textarea>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<details>
|
||||
<summary>Ignored Details</summary>
|
||||
<p>Sometimes you may not want certain processes to be controlable.</p>
|
||||
<p>In this case, you can add a list of process names which will be ignored.</p>
|
||||
<p>ex:<br /> chrome</p>
|
||||
<p>Blank lines can be used for spacing<br/>Lines starting with // are ignored</p>
|
||||
<p>To get a process name, you can use task manager, right click on a process and select properties</p>
|
||||
<p>Process names are case sensitive and must match exactly</p>
|
||||
</details>
|
||||
|
||||
</div>
|
||||
|
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="6" viewBox="0 0 12 6">
|
||||
<polygon fill="#8E8E92" fill-rule="evenodd" points="5 4 9 0 10 1 5 6 0 1 1 0"/>
|
||||
</svg>
|
After Width: | Height: | Size: 173 B |
@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="7" height="12" viewBox="0 0 7 12">
|
||||
<g fill="#8E8E92" transform="translate(4, 5) rotate(-90) translate(-4, -5) translate(-2, 2)">
|
||||
<polygon id="Path" points="5 4 9 0 10 1 5 6 0 1 1 0"></polygon>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 268 B |
BIN
src/FocusVolumeControl/PropertyInspector/assets/check.png
Normal file
After Width: | Height: | Size: 234 B |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="10" viewBox="0 0 12 10">
|
||||
<polygon fill="#FFF" points="7.2 7.5 7.2 -1.3 8.7 -1.3 8.6 9.1 2.7 8.7 2.7 7.2" transform="rotate(37 5.718 3.896)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 212 B |
@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path fill="#9C9C9C" fill-rule="nonzero"
|
||||
d="M1,5 L1,14 L14,14 L14,5 L1,5 Z M0,1 L15,1 L15,15 L0,15 L0,1 Z M14,4 L14,2 L1,2 L1,4 L14,4 Z"/>
|
||||
<rect width="1" height="1" x="2" fill="#9C9C9C" fill-rule="nonzero"/>
|
||||
<rect width="1" height="1" x="12" fill="#9C9C9C" fill-rule="nonzero"/>
|
||||
<g transform="translate(3 7)">
|
||||
<rect width="1" height="1" x="2" fill="#9C9C9C"/>
|
||||
<rect width="1" height="1" fill="#666"/>
|
||||
<rect width="1" height="1" x="4" fill="#9C9C9C"/>
|
||||
<rect width="1" height="1" x="6" fill="#9C9C9C"/>
|
||||
<rect width="1" height="1" x="8" fill="#9C9C9C"/>
|
||||
<rect width="1" height="1" y="2" fill="#9C9C9C"/>
|
||||
<rect width="1" height="1" x="2" y="2" fill="#9C9C9C"/>
|
||||
<rect width="1" height="1" x="4" y="2" fill="#9C9C9C"/>
|
||||
<rect width="1" height="1" x="6" y="2" fill="#9C9C9C"/>
|
||||
<rect width="1" height="1" x="8" y="2" fill="#9C9C9C"/>
|
||||
<rect width="1" height="1" y="4" fill="#9C9C9C"/>
|
||||
<rect width="1" height="1" x="2" y="4" fill="#9C9C9C"/>
|
||||
<rect width="1" height="1" x="4" y="4" fill="#9C9C9C"/>
|
||||
<rect width="1" height="1" x="6" y="4" fill="#9C9C9C"/>
|
||||
<rect width="1" height="1" x="8" y="4" fill="#666"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<g fill="#9C9C9C">
|
||||
<path d="M15,15 L1.77635684e-15,15 L1.77635684e-15,1 L15,1 L15,15 Z M5,7 L5,8 L6,8 L6,7 L5,7 Z M3,7 L3,8 L4,8 L4,7 L3,7 Z M7,7 L7,8 L8,8 L8,7 L7,7 Z M9,7 L9,8 L10,8 L10,7 L9,7 Z M11,7 L11,8 L12,8 L12,7 L11,7 Z M3,9 L3,10 L4,10 L4,9 L3,9 Z M5,9 L5,10 L6,10 L6,9 L5,9 Z M7,9 L7,10 L8,10 L8,9 L7,9 Z M9,9 L9,10 L10,10 L10,9 L9,9 Z M11,9 L11,10 L12,10 L12,9 L11,9 Z M3,11 L3,12 L4,12 L4,11 L3,11 Z M5,11 L5,12 L6,12 L6,11 L5,11 Z M7,11 L7,12 L8,12 L8,11 L7,11 Z M9,11 L9,12 L10,12 L10,11 L9,11 Z M11,11 L11,12 L12,12 L12,11 L11,11 Z M14,4 L14,2 L1,2 L1,4 L14,4 Z"/>
|
||||
<rect width="1" height="1" x="2"/>
|
||||
<rect width="1" height="1" x="12"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 780 B |
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="6" height="6" viewBox="0 0 6 6">
|
||||
<circle cx="3" cy="3" r="3" fill="#FFF"/>
|
||||
</svg>
|
After Width: | Height: | Size: 133 B |
3
src/FocusVolumeControl/PropertyInspector/assets/tick.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1" height="8" viewBox="0 0 1 8">
|
||||
<rect id="Rectangle" width="1" height="6" x="0" y="1" fill="#555"/>
|
||||
</svg>
|
After Width: | Height: | Size: 159 B |
@ -1,65 +1,3 @@
|
||||
.linkspan {
|
||||
cursor: pointer;
|
||||
color: #7397d2;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.titleAlignedSmall {
|
||||
font-size: 9pt;
|
||||
padding-left: 33px !important;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 9pt !important;
|
||||
}
|
||||
|
||||
.leftMargin10 {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.leftMargin0 {
|
||||
margin-left: 0px !important;
|
||||
}
|
||||
|
||||
.leftPadding3 {
|
||||
padding-left: 3px !important;
|
||||
}
|
||||
|
||||
.leftPadding0 {
|
||||
padding-left: 0px !important;
|
||||
}
|
||||
|
||||
.bright {
|
||||
color: #d8d8d8;
|
||||
}
|
||||
|
||||
.iconLeft {
|
||||
background-position: 0px 4px !important;
|
||||
}
|
||||
|
||||
.summaryIconPadding {
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.subMenu {
|
||||
border-left: 1px dotted gray;
|
||||
padding-left: 15px;
|
||||
max-width: 96%;
|
||||
background-color: #323232;
|
||||
}
|
||||
|
||||
:root {
|
||||
--sdpi-bgcolor: #2D2D2D;
|
||||
--sdpi-background: #3D3D3D;
|
||||
--sdpi-color: #d8d8d8;
|
||||
--sdpi-bordercolor: #3a3a3a;
|
||||
--sdpi-buttonbordercolor: #969696;
|
||||
--sdpi-borderradius: 0px;
|
||||
--sdpi-width: 224px;
|
||||
--sdpi-fontweight: 600;
|
||||
--sdpi-letterspacing: -0.25pt;
|
||||
}
|
||||
|
||||
html {
|
||||
--sdpi-bgcolor: #2D2D2D;
|
||||
--sdpi-background: #3D3D3D;
|
||||
@ -70,6 +8,16 @@ html {
|
||||
--sdpi-width: 224px;
|
||||
--sdpi-fontweight: 600;
|
||||
--sdpi-letterspacing: -0.25pt;
|
||||
--sdpi-tab-color: #969696;
|
||||
--sdpi-tab-left-margin: 1px;
|
||||
--sdpi-tab-top-offset: 1px;
|
||||
--sdpi-tab-selected-color: #333333;
|
||||
--sdpi-tab-selected-top-offset: 0px;
|
||||
--sdpi-tab-font-size: 9pt;
|
||||
--sdpi-tab-container-left-offset: 5px;
|
||||
--sdpi-tab-padding-horizontal: 12px;
|
||||
--sdpi-tab-padding-vertical: 5px;
|
||||
--sdpi-linecolor: #454545;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
@ -119,16 +67,24 @@ hr2,
|
||||
margin: 8px 0px;
|
||||
}
|
||||
|
||||
.sdpi-heading::before,
|
||||
.sdpi-heading::after {
|
||||
content: "";
|
||||
flex-grow: 1;
|
||||
background: var(--sdpi-background);
|
||||
height: 1px;
|
||||
font-size: 0px;
|
||||
line-height: 0px;
|
||||
margin: 0px 16px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.3em;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.sdpi-heading::before,
|
||||
.sdpi-heading::after {
|
||||
content: "";
|
||||
flex-grow: 1;
|
||||
background: var(--sdpi-background);
|
||||
height: 1px;
|
||||
font-size: 0px;
|
||||
line-height: 0px;
|
||||
margin: 0px 16px;
|
||||
}
|
||||
|
||||
hr2 {
|
||||
height: 2px;
|
||||
@ -225,6 +181,9 @@ progress {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
|
||||
/* TABS */
|
||||
|
||||
.tabs {
|
||||
/**
|
||||
* Setting display to flex makes this container lay
|
||||
@ -232,21 +191,93 @@ progress {
|
||||
* as in the above "Stepper input" example.
|
||||
*/
|
||||
display: flex;
|
||||
border-bottom: 1px solid #D7DBDD;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.0);
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
overflow-x: auto;
|
||||
text-transform: capitalize;
|
||||
background-color: transparent;
|
||||
margin-left: var(--sdpi-tab-container-left-offset);
|
||||
}
|
||||
|
||||
.tabs::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tabs::-webkit-scrollbar-track {
|
||||
-webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.tabs::-webkit-scrollbar-thumb {
|
||||
background-color: #444;
|
||||
outline: 1px solid #444;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.tab-separator {
|
||||
margin-left: 100px;
|
||||
max-width: 234px;
|
||||
margin-bottom: 20px;
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
cursor: pointer;
|
||||
padding: 5px 30px;
|
||||
color: #16a2d7;
|
||||
font-size: 9pt;
|
||||
border-bottom: 2px solid transparent;
|
||||
padding: var(--sdpi-tab-padding-vertical) var(--sdpi-tab-padding-horizontal);
|
||||
color: var(--sdpi-tab-color);
|
||||
font-size: var(--sdpi-tab-font-size);
|
||||
font-weight: var(--title-font-weight);
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
margin: 0px;
|
||||
margin-top: var(--sdpi-tab-top-offset);
|
||||
margin-left: var(--sdpi-tab-left-margin);
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-bottom: 1px solid var(--sdpi-linecolor);
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tab.is-tab-selected {
|
||||
border-bottom-color: #4ebbe4;
|
||||
.tab:first-child {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.tab-container {
|
||||
margin-top: -14px;
|
||||
}
|
||||
|
||||
.tab-container > hr {
|
||||
margin-left: 100px;
|
||||
max-width: 234px;
|
||||
}
|
||||
|
||||
.tabs + hr {
|
||||
margin-left: 0px;
|
||||
max-width: 234px;
|
||||
margin-top: -6px;
|
||||
}
|
||||
|
||||
.tab.selected {
|
||||
color: white;
|
||||
background-color: var(--sdpi-tab-selected-color);
|
||||
border-bottom: 2px solid var(--sdpi-tab-selected-color);
|
||||
margin-top: var(--sdpi-tab-selected-top-offset);
|
||||
}
|
||||
|
||||
.sdpi-item.tabgroup {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.istab {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
margin-bottom: 20px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
select {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
@ -306,18 +337,30 @@ option {
|
||||
.sdpi-wrapper {
|
||||
overflow-x: hidden;
|
||||
height: 100%;
|
||||
margin-right: 1px; /* ensure scroller thumb is not clipped */
|
||||
}
|
||||
|
||||
.sdpi-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-height: 32px;
|
||||
align-items: center;
|
||||
min-height: 30px;
|
||||
align-items: first baseline;
|
||||
margin-top: 2px;
|
||||
max-width: 344px;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
|
||||
.sdpi-item[type="textarea"],
|
||||
.sdpi-item[type="color"],
|
||||
.sdpi-item[type="canvas"],
|
||||
.sdpi-item .aligncenter {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sdpi-item[type="color"] > .sdpi-item-label {
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.sdpi-item:first-child {
|
||||
margin-top: -1px;
|
||||
}
|
||||
@ -412,7 +455,7 @@ table > caption {
|
||||
padding-right: 5px;
|
||||
font-weight: 600;
|
||||
-webkit-user-select: none;
|
||||
line-height: 24px;
|
||||
line-height: normal;
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
@ -486,8 +529,8 @@ ol.sdpi-item-value,
|
||||
margin-left: 5px;
|
||||
margin-right: 12px;
|
||||
padding: 4px !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* display: flex;
|
||||
flex-direction: column; */
|
||||
}
|
||||
|
||||
.two-items li {
|
||||
@ -632,6 +675,16 @@ summary {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sdpi-item.details {
|
||||
align-items: first baseline;
|
||||
}
|
||||
|
||||
/* needs Chromium update 2023
|
||||
.sdpi-item:has(>details) {
|
||||
align-items: first baseline;
|
||||
}
|
||||
*/
|
||||
|
||||
details * {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
@ -654,21 +707,21 @@ details.message {
|
||||
}
|
||||
|
||||
details.message > summary:first-of-type {
|
||||
/*line-height: 48px;*/
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
details.message h1 {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
details:not(.pointer) > summary {
|
||||
list-style: none;
|
||||
/* details:not(.pointer)>summary {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
details > summary::-webkit-details-marker
|
||||
details > summary::-webkit-details-marker,
|
||||
.message > summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
display: none;
|
||||
} */
|
||||
|
||||
.info20,
|
||||
.question,
|
||||
@ -702,7 +755,6 @@ details > summary::-webkit-details-marker
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='%23999' d='M10,18 C5.581722,18 2,14.418278 2,10 C2,5.581722 5.581722,2 10,2 C14.418278,2 18,5.581722 18,10 C18,14.418278 14.418278,18 10,18 Z M6.77783203,7.65332031 C6.77783203,7.84798274 6.85929281,8.02888914 7.0222168,8.19604492 C7.18514079,8.36320071 7.38508996,8.44677734 7.62207031,8.44677734 C8.02409055,8.44677734 8.29703704,8.20768468 8.44091797,7.72949219 C8.59326248,7.27245865 8.77945854,6.92651485 8.99951172,6.69165039 C9.2195649,6.45678594 9.56233491,6.33935547 10.027832,6.33935547 C10.4256205,6.33935547 10.7006836,6.37695313 11.0021973,6.68847656 C11.652832,7.53271484 10.942627,8.472229 10.3750916,9.1321106 C9.80755615,9.79199219 8.29492188,11.9897461 10.027832,12.1347656 C10.4498423,12.1700818 10.7027991,11.9147157 10.7832031,11.4746094 C11.0021973,9.59857178 13.1254883,8.82415771 13.1254883,7.53271484 C13.1254883,7.07568131 12.9974785,6.65250846 12.7414551,6.26318359 C12.4854317,5.87385873 12.1225609,5.56600048 11.652832,5.33959961 C11.1831031,5.11319874 10.6414419,5 10.027832,5 C9.36767248,5 8.79004154,5.13541531 8.29492187,5.40625 C7.79980221,5.67708469 7.42317837,6.01879677 7.16503906,6.43139648 C6.90689975,6.8439962 6.77783203,7.25130007 6.77783203,7.65332031 Z M10.0099668,15 C10.2713191,15 10.5016601,14.9108147 10.7009967,14.7324415 C10.9003332,14.5540682 11,14.3088087 11,13.9966555 C11,13.7157177 10.9047629,13.4793767 10.7142857,13.2876254 C10.5238086,13.0958742 10.2890379,13 10.0099668,13 C9.72646591,13 9.48726565,13.0958742 9.2923588,13.2876254 C9.09745196,13.4793767 9,13.7157177 9,13.9966555 C9,14.313268 9.10077419,14.5596424 9.30232558,14.735786 C9.50387698,14.9119295 9.73975502,15 10.0099668,15 Z'/%3E%3C/svg%3E%0A");
|
||||
}
|
||||
|
||||
|
||||
.sdpi-more-info {
|
||||
position: fixed;
|
||||
left: 0px;
|
||||
@ -731,7 +783,6 @@ details > summary::-webkit-details-marker
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
|
||||
.sdpi-bottom-bar {
|
||||
display: flex;
|
||||
align-self: right;
|
||||
@ -759,7 +810,6 @@ details a {
|
||||
padding-right: 28px;
|
||||
}
|
||||
|
||||
|
||||
input:not([type="range"]),
|
||||
textarea {
|
||||
-webkit-appearance: none;
|
||||
@ -827,7 +877,6 @@ input[type="checkbox"] {
|
||||
margin-top: -2px;
|
||||
min-width: 8px;
|
||||
text-align: right;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
@ -840,6 +889,10 @@ input[type="checkbox"] {
|
||||
|
||||
span + input[type="range"] {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
span + .range-container > input[type="range"],
|
||||
span + input[type="range"] {
|
||||
max-width: 168px;
|
||||
}
|
||||
|
||||
@ -1028,7 +1081,6 @@ textarea {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
.card-carousel-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -1091,7 +1143,8 @@ textarea {
|
||||
margin: 0 5px;
|
||||
cursor: pointer;
|
||||
/* box-shadow: 0 4px 15px 0 rgba(40, 44, 53, 0.06), 0 2px 2px 0 rgba(40, 44, 53, 0.08); */
|
||||
background-color: #fff;
|
||||
/* background-color: #fff; */
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
z-index: 3;
|
||||
}
|
||||
@ -1114,6 +1167,7 @@ textarea {
|
||||
|
||||
.card-carousel-cards .card-carousel--card img:hover {
|
||||
opacity: 0.5;
|
||||
background-color: rgba(255, 255, 255, .1);
|
||||
}
|
||||
|
||||
.card-carousel-cards .card-carousel--card--footer {
|
||||
@ -1141,44 +1195,8 @@ textarea {
|
||||
color: #666a73;
|
||||
}
|
||||
|
||||
|
||||
h1 {
|
||||
font-size: 1.3em;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
background: url(../assets/elg_calendar_inv.svg) no-repeat left center;
|
||||
padding-right: 1em;
|
||||
padding-left: 25px;
|
||||
background-position: 4px 0px;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper {
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-text {
|
||||
padding: 0 0.3em;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-month-field {
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-day-field {
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-year-field {
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
/* display: none; */
|
||||
}
|
||||
|
||||
::-webkit-calendar-picker-indicator {
|
||||
background: transparent;
|
||||
background: url(../assets/elg_calendar_inv.svg) no-repeat center;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
@ -1186,53 +1204,30 @@ h1 {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
input[type="text"]::-webkit-calendar-picker-indicator {
|
||||
background: transparent;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
input[type="date"] {
|
||||
-webkit-align-items: center;
|
||||
align-items: center;
|
||||
display: -webkit-inline-flex;
|
||||
font-family: monospace;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
-webkit-padding-start: 1px;
|
||||
}
|
||||
|
||||
input::-webkit-datetime-edit {
|
||||
-webkit-flex: 1;
|
||||
flex: 1;
|
||||
-webkit-user-modify: read-only !important;
|
||||
user-modify: read-only !important;
|
||||
display: inline-block;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
padding: 4px;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
}
|
||||
|
||||
/*
|
||||
input::-webkit-datetime-edit-fields-wrapper {
|
||||
-webkit-user-modify: read-only !important;
|
||||
display: inline-block;
|
||||
padding: 1px 0;
|
||||
white-space: pre;
|
||||
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
input[type="date"] {
|
||||
background-color: red;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type="date"]::-webkit-clear-button {
|
||||
font-size: 18px;
|
||||
height: 30px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
input[type="date"]::-webkit-inner-spin-button {
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
input[type="date"]::-webkit-calendar-picker-indicator {
|
||||
font-size: 15px;
|
||||
} */
|
||||
|
||||
input[type="file"] {
|
||||
opacity: 0;
|
||||
display: none;
|
||||
@ -1308,7 +1303,6 @@ input:required:valid {
|
||||
background-color: var(--sdpi-background);
|
||||
}
|
||||
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
@ -1330,23 +1324,72 @@ a {
|
||||
color: #7397d2;
|
||||
}
|
||||
|
||||
.testcontainer {
|
||||
display: flex;
|
||||
background-color: #0000ff20;
|
||||
max-width: 400px;
|
||||
height: 200px;
|
||||
align-content: space-evenly;
|
||||
input[type="week"] {
|
||||
-webkit-appearance: auto !important;
|
||||
appearance: auto !important;
|
||||
}
|
||||
|
||||
input[type=range] {
|
||||
-webkit-appearance: none;
|
||||
/* background-color: green; */
|
||||
input[type="month"] + datalist,
|
||||
input[type="day"] + datalist,
|
||||
input[type="week"] + datalist,
|
||||
input[type=text] + datalist {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
input[type="range"] {
|
||||
-webkit-appearance: auto;
|
||||
appearance: auto;
|
||||
height: 6px;
|
||||
margin-top: 12px;
|
||||
z-index: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-runnable-track {
|
||||
border: 0px solid transparent;
|
||||
}
|
||||
|
||||
.sdpi-item[type="range"] .sdpi-item-value.datalist {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
datalist {
|
||||
--sdpi-datalist-margin: 7px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 0px;
|
||||
padding-top: 0px;
|
||||
font-size: 12px;
|
||||
margin-left: var(--sdpi-datalist-margin);
|
||||
width: calc(100% - calc(var(--sdpi-datalist-margin) * 2.5));
|
||||
}
|
||||
|
||||
datalist > option {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: end;
|
||||
/* background-image: url(../assets/tick.svg); */
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1' height='8' viewBox='0 0 1 8'%3E%3Crect width='1' height='6' x='0' y='1' fill='%23555'/%3E%3C/svg%3E%0A");
|
||||
padding: 0;
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: #9A9A99;
|
||||
width: 1px;
|
||||
height: 30px;
|
||||
z-index: 1;
|
||||
margin-top: -6px;
|
||||
background-position: top 6px right 5px;
|
||||
background-repeat: repeat no-repeat; /* fallback */
|
||||
background-repeat-y: no-repeat;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
[role="spinbutton"] {
|
||||
-webkit-appearance: auto;
|
||||
appearance: auto;
|
||||
}
|
||||
|
||||
/*
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
@ -1595,7 +1638,8 @@ input[type="range"].colortemperature::-webkit-slider-runnable-track {
|
||||
background-image: linear-gradient(to right, #94d0ec, #ffb165);
|
||||
}
|
||||
|
||||
input[type="range"].colorbrightness::-webkit-slider-runnable-track {
|
||||
input[type="range"].colorbrightness.greyscale::-webkit-slider-runnable-track,
|
||||
input[type="range"].colorbrightness.grayscale::-webkit-slider-runnable-track {
|
||||
background-color: #efefef;
|
||||
background-image: linear-gradient(to right, black, rgba(0, 0, 0, 0));
|
||||
}
|
||||
@ -1648,3 +1692,54 @@ select {
|
||||
-webkit-appearance: media-slider;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/*--------- context menu ----------*/
|
||||
|
||||
.context-menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
padding: 12px 0;
|
||||
width: 120px;
|
||||
background-color: #3D3D3D;
|
||||
border: solid 1px #dfdfdf;
|
||||
box-shadow: 1px 1px 2px #cfcfcf;
|
||||
}
|
||||
|
||||
.context-menu--active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.context-menu__items {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.context-menu__item {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
background-color: #3D3D3D !important;
|
||||
}
|
||||
|
||||
.context-menu__item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.context-menu__link {
|
||||
display: block;
|
||||
padding: 4px 12px;
|
||||
color: #ffff;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.context-menu__link:hover {
|
||||
color: #fff;
|
||||
background-color: #0066aa;
|
||||
}
|
||||
|
||||
.context-menu_message {
|
||||
cursor: default;
|
||||
}
|
@ -29,6 +29,7 @@ function calcRangeLabel(elem) {
|
||||
return tooltipValue + outputType;
|
||||
}
|
||||
|
||||
/*
|
||||
function setElementLabel(elem, str) {
|
||||
// Try to set this for the rangeLabel class, if it exists
|
||||
let label = elem.querySelector('.rangeLabel');
|
||||
@ -39,6 +40,7 @@ function setElementLabel(elem, str) {
|
||||
console.log('setElementLabel ERROR! No .rangeLabel found', elem);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
function setRangeTooltips() {
|
||||
console.log("Loading setRangeTooltips");
|
||||
@ -68,7 +70,7 @@ function setRangeTooltips() {
|
||||
tooltip.style.top = (rangeRect.top - 32) + 'px';
|
||||
}
|
||||
|
||||
setElementLabel(elem, labelStr)
|
||||
//setElementLabel(elem, labelStr)
|
||||
};
|
||||
|
||||
rangeSelector.addEventListener(
|
||||
@ -100,7 +102,7 @@ function setRangeTooltips() {
|
||||
console.log('rangeTooltip settingsUpdated called');
|
||||
window.setTimeout(function () {
|
||||
let str = calcRangeLabel(rangeSelector);
|
||||
setElementLabel(elem, str);
|
||||
//setElementLabel(elem, str);
|
||||
}, 500);
|
||||
},
|
||||
false
|
||||
@ -112,7 +114,7 @@ function setRangeTooltips() {
|
||||
console.log('rangeTooltip websocketCreate called');
|
||||
window.setTimeout(function () {
|
||||
let str = calcRangeLabel(rangeSelector);
|
||||
setElementLabel(elem, str);
|
||||
//setElementLabel(elem, str);
|
||||
}, 500);
|
||||
},
|
||||
false
|
||||
|
267
src/FocusVolumeControl/UI/IconExtraction.cs
Normal file
@ -0,0 +1,267 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FocusVolumeControl.UI
|
||||
{
|
||||
internal class IconExtraction
|
||||
{
|
||||
public static Bitmap GetIcon(string path)
|
||||
{
|
||||
var index = GetIconIndex(path);
|
||||
var handle = GetIconHandle(index);
|
||||
|
||||
using var icon = (Icon)Icon.FromHandle(handle).Clone();
|
||||
|
||||
Shell32.DestroyIcon(handle);
|
||||
return icon.ToBitmap();
|
||||
}
|
||||
|
||||
|
||||
static int GetIconIndex(string pszFile)
|
||||
{
|
||||
SHFILEINFO sfi = new SHFILEINFO();
|
||||
Shell32.SHGetFileInfo(pszFile, 0, ref sfi, (uint)Marshal.SizeOf(sfi), (uint)(SHGFI.SysIconIndex | SHGFI.LargeIcon | SHGFI.UseFileAttributes));
|
||||
return sfi.iIcon;
|
||||
}
|
||||
|
||||
// 256*256
|
||||
static IntPtr GetIconHandle(int iImage)
|
||||
{
|
||||
IImageList spiml = null;
|
||||
Guid guil = new Guid(IID_IImageList2);//or IID_IImageList
|
||||
|
||||
Shell32.SHGetImageList(Shell32.SHIL_EXTRALARGE, ref guil, ref spiml);
|
||||
IntPtr hIcon = IntPtr.Zero;
|
||||
spiml.GetIcon(iImage, Shell32.ILD_TRANSPARENT | Shell32.ILD_IMAGE, ref hIcon);
|
||||
|
||||
return hIcon;
|
||||
}
|
||||
|
||||
const string IID_IImageList = "46EB5926-582E-4017-9FDF-E8998DAA0950";
|
||||
const string IID_IImageList2 = "192B9D83-50FC-457B-90A0-2B82A8B5DAE1";
|
||||
|
||||
public static class Shell32
|
||||
{
|
||||
|
||||
public const int SHIL_LARGE = 0x0;
|
||||
public const int SHIL_SMALL = 0x1;
|
||||
public const int SHIL_EXTRALARGE = 0x2;
|
||||
public const int SHIL_SYSSMALL = 0x3;
|
||||
public const int SHIL_JUMBO = 0x4;
|
||||
public const int SHIL_LAST = 0x4;
|
||||
|
||||
public const int ILD_TRANSPARENT = 0x00000001;
|
||||
public const int ILD_IMAGE = 0x00000020;
|
||||
|
||||
[DllImport("shell32.dll", EntryPoint = "#727")]
|
||||
public extern static int SHGetImageList(int iImageList, ref Guid riid, ref IImageList ppv);
|
||||
|
||||
[DllImport("user32.dll", EntryPoint = "DestroyIcon", SetLastError = true)]
|
||||
public static extern int DestroyIcon(IntPtr hIcon);
|
||||
|
||||
//[DllImport("shell32.dll")]
|
||||
//public static extern uint SHGetIDListFromObject([MarshalAs(UnmanagedType.IUnknown)] object iUnknown, out IntPtr ppidl);
|
||||
|
||||
[DllImport("Shell32.dll")]
|
||||
public static extern IntPtr SHGetFileInfo(string pszPath, uint dwFileAttributes, ref SHFILEINFO psfi, uint cbFileInfo, uint uFlags);
|
||||
}
|
||||
|
||||
[Flags]
|
||||
enum SHGFI : uint
|
||||
{
|
||||
/// <summary>get icon</summary>
|
||||
Icon = 0x000000100,
|
||||
/// <summary>get display name</summary>
|
||||
DisplayName = 0x000000200,
|
||||
/// <summary>get type name</summary>
|
||||
TypeName = 0x000000400,
|
||||
/// <summary>get attributes</summary>
|
||||
Attributes = 0x000000800,
|
||||
/// <summary>get icon location</summary>
|
||||
IconLocation = 0x000001000,
|
||||
/// <summary>return exe type</summary>
|
||||
ExeType = 0x000002000,
|
||||
/// <summary>get system icon index</summary>
|
||||
SysIconIndex = 0x000004000,
|
||||
/// <summary>put a link overlay on icon</summary>
|
||||
LinkOverlay = 0x000008000,
|
||||
/// <summary>show icon in selected state</summary>
|
||||
Selected = 0x000010000,
|
||||
/// <summary>get only specified attributes</summary>
|
||||
Attr_Specified = 0x000020000,
|
||||
/// <summary>get large icon</summary>
|
||||
LargeIcon = 0x000000000,
|
||||
/// <summary>get small icon</summary>
|
||||
SmallIcon = 0x000000001,
|
||||
/// <summary>get open icon</summary>
|
||||
OpenIcon = 0x000000002,
|
||||
/// <summary>get shell size icon</summary>
|
||||
ShellIconSize = 0x000000004,
|
||||
/// <summary>pszPath is a pidl</summary>
|
||||
PIDL = 0x000000008,
|
||||
/// <summary>use passed dwFileAttribute</summary>
|
||||
UseFileAttributes = 0x000000010,
|
||||
/// <summary>apply the appropriate overlays</summary>
|
||||
AddOverlays = 0x000000020,
|
||||
/// <summary>Get the index of the overlay in the upper 8 bits of the iIcon</summary>
|
||||
OverlayIndex = 0x000000040,
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct SHFILEINFO
|
||||
{
|
||||
public const int NAMESIZE = 80;
|
||||
public IntPtr hIcon;
|
||||
public int iIcon;
|
||||
public uint dwAttributes;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
|
||||
public string szDisplayName;
|
||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]
|
||||
public string szTypeName;
|
||||
};
|
||||
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct RECT
|
||||
{
|
||||
public int left, top, right, bottom;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct POINT
|
||||
{
|
||||
int x;
|
||||
int y;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct IMAGELISTDRAWPARAMS
|
||||
{
|
||||
public int cbSize;
|
||||
public IntPtr himl;
|
||||
public int i;
|
||||
public IntPtr hdcDst;
|
||||
public int x;
|
||||
public int y;
|
||||
public int cx;
|
||||
public int cy;
|
||||
public int xBitmap; // x offest from the upperleft of bitmap
|
||||
public int yBitmap; // y offset from the upperleft of bitmap
|
||||
public int rgbBk;
|
||||
public int rgbFg;
|
||||
public int fStyle;
|
||||
public int dwRop;
|
||||
public int fState;
|
||||
public int Frame;
|
||||
public int crEffect;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct IMAGEINFO
|
||||
{
|
||||
public IntPtr hbmImage;
|
||||
public IntPtr hbmMask;
|
||||
public int Unused1;
|
||||
public int Unused2;
|
||||
public RECT rcImage;
|
||||
}
|
||||
[ComImportAttribute()]
|
||||
[GuidAttribute("46EB5926-582E-4017-9FDF-E8998DAA0950")]
|
||||
[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IImageList
|
||||
{
|
||||
[PreserveSig]
|
||||
int Add(IntPtr hbmImage, IntPtr hbmMask, ref int pi);
|
||||
|
||||
[PreserveSig]
|
||||
int ReplaceIcon(int i, IntPtr hicon, ref int pi);
|
||||
|
||||
[PreserveSig]
|
||||
int SetOverlayImage(int iImage, int iOverlay);
|
||||
|
||||
[PreserveSig]
|
||||
int Replace(int i, IntPtr hbmImage, IntPtr hbmMask);
|
||||
|
||||
[PreserveSig]
|
||||
int AddMasked(IntPtr hbmImage, int crMask, ref int pi);
|
||||
|
||||
[PreserveSig]
|
||||
int Draw(ref IMAGELISTDRAWPARAMS pimldp);
|
||||
|
||||
[PreserveSig]
|
||||
int Remove(int i);
|
||||
|
||||
[PreserveSig]
|
||||
int GetIcon(int i, int flags, ref IntPtr picon);
|
||||
|
||||
[PreserveSig]
|
||||
int GetImageInfo(int i, ref IMAGEINFO pImageInfo);
|
||||
|
||||
[PreserveSig]
|
||||
int Copy(int iDst, IImageList punkSrc, int iSrc, int uFlags);
|
||||
|
||||
[PreserveSig]
|
||||
int Merge(int i1, IImageList punk2, int i2, int dx, int dy, ref Guid riid, ref IntPtr ppv);
|
||||
|
||||
[PreserveSig]
|
||||
int Clone(ref Guid riid, ref IntPtr ppv);
|
||||
|
||||
[PreserveSig]
|
||||
int GetImageRect(int i, ref RECT prc);
|
||||
|
||||
[PreserveSig]
|
||||
int GetIconSize(ref int cx, ref int cy);
|
||||
|
||||
[PreserveSig]
|
||||
int SetIconSize(int cx, int cy);
|
||||
|
||||
[PreserveSig]
|
||||
int GetImageCount(ref int pi);
|
||||
|
||||
[PreserveSig]
|
||||
int SetImageCount(int uNewCount);
|
||||
|
||||
[PreserveSig]
|
||||
int SetBkColor(int clrBk, ref int pclr);
|
||||
|
||||
[PreserveSig]
|
||||
int GetBkColor(ref int pclr);
|
||||
|
||||
[PreserveSig]
|
||||
int BeginDrag(int iTrack, int dxHotspot, int dyHotspot);
|
||||
|
||||
[PreserveSig]
|
||||
int EndDrag();
|
||||
|
||||
[PreserveSig]
|
||||
int DragEnter(IntPtr hwndLock, int x, int y);
|
||||
|
||||
[PreserveSig]
|
||||
int DragLeave(IntPtr hwndLock);
|
||||
|
||||
[PreserveSig]
|
||||
int DragMove(int x, int y);
|
||||
|
||||
[PreserveSig]
|
||||
int SetDragCursorImage(ref IImageList punk, int iDrag, int dxHotspot, int dyHotspot);
|
||||
|
||||
[PreserveSig]
|
||||
int DragShowNolock(int fShow);
|
||||
|
||||
[PreserveSig]
|
||||
int GetDragImage(ref POINT ppt, ref POINT pptHotspot, ref Guid riid, ref IntPtr ppv);
|
||||
|
||||
[PreserveSig]
|
||||
int GetItemFlags(int i, ref int dwFlags);
|
||||
|
||||
[PreserveSig]
|
||||
int GetOverlayImage(int iOverlay, ref int piIndex);
|
||||
};
|
||||
|
||||
}
|
||||
}
|
100
src/FocusVolumeControl/UI/JavaIconExtractor.cs
Normal file
@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace FocusVolumeControl.UI
|
||||
{
|
||||
internal class JavaIconExtractor
|
||||
{
|
||||
const int WM_GETICON = 0x7F;
|
||||
const int WM_QUERYDRAGICON = 0x0037;
|
||||
//const int ICON_SMALL = 0; //(16x16)
|
||||
const int ICON_BIG = 1; //(32x32)
|
||||
const int SMTO_ABORTIFHUNG = 0x3;
|
||||
const int GCL_HICON = -14;
|
||||
|
||||
[DllImport("User32.dll")]
|
||||
static extern int SendMessageTimeout(IntPtr hWnd, int uMsg, int wParam, int lParam, int fuFlags, int uTimeout, out int lpdwResult);
|
||||
[DllImport("User32.dll")]
|
||||
static extern int GetClassLong(IntPtr hWnd, int index);
|
||||
|
||||
[DllImport("user32.dll", EntryPoint = "DestroyIcon", SetLastError = true)]
|
||||
public static extern int DestroyIcon(IntPtr hIcon);
|
||||
|
||||
public static Bitmap? GetWindowBigIconWithRetry(IntPtr hWnd)
|
||||
{
|
||||
var retry = 5;
|
||||
var icon = GetWindowBigIcon(hWnd);
|
||||
|
||||
while(icon == null || retry > 0)
|
||||
{
|
||||
Thread.Sleep(100);
|
||||
icon = GetWindowBigIcon(hWnd);
|
||||
retry--;
|
||||
}
|
||||
|
||||
return icon;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a big icon (32*32) of a window
|
||||
/// </summary>
|
||||
/// <param name="hWnd"></param>
|
||||
/// <returns></returns>
|
||||
public static Bitmap? GetWindowBigIcon(IntPtr hWnd)
|
||||
{
|
||||
IntPtr hIcon = IntPtr.Zero;
|
||||
try
|
||||
{
|
||||
int result;
|
||||
SendMessageTimeout(hWnd, WM_GETICON, ICON_BIG, //big icon size
|
||||
0, SMTO_ABORTIFHUNG, 1000, out result);
|
||||
|
||||
hIcon = new IntPtr(result);
|
||||
if (hIcon == IntPtr.Zero) //some applications don't respond to sendmessage, we have to use GetClassLong in that case
|
||||
{
|
||||
result = GetClassLong(hWnd, GCL_HICON); //big icon size
|
||||
hIcon = new IntPtr(result);
|
||||
}
|
||||
|
||||
if (hIcon == IntPtr.Zero)
|
||||
{
|
||||
SendMessageTimeout(hWnd, WM_QUERYDRAGICON, 0, 0, SMTO_ABORTIFHUNG, 1000, out result);
|
||||
hIcon = new IntPtr(result);
|
||||
}
|
||||
|
||||
if (hIcon == IntPtr.Zero)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
using var tmp = (Icon)Icon.FromHandle(hIcon).Clone();
|
||||
return tmp.ToBitmap();
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
if(hIcon != IntPtr.Zero)
|
||||
{
|
||||
DestroyIcon(hIcon);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#nullable restore
|
70
src/FocusVolumeControl/WindowChangedEventLoop.cs
Normal file
@ -0,0 +1,70 @@
|
||||
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;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace FocusVolumeControl
|
||||
{
|
||||
internal class WindowChangedEventLoop
|
||||
{
|
||||
private static readonly Lazy<WindowChangedEventLoop> _lazy = new Lazy<WindowChangedEventLoop>(() => new WindowChangedEventLoop());
|
||||
public static WindowChangedEventLoop Instance => _lazy.Value;
|
||||
|
||||
readonly Thread _thread;
|
||||
Dispatcher? _dispatcher;
|
||||
|
||||
IntPtr _foregroundWindowChangedEvent;
|
||||
Native.WinEventDelegate? _delegate;
|
||||
|
||||
private WindowChangedEventLoop()
|
||||
{
|
||||
_thread = new Thread(() =>
|
||||
{
|
||||
Logger.Instance.LogMessage(TracingLevel.DEBUG, "Starting Window Changed Event Loop");
|
||||
_delegate = new Native.WinEventDelegate(WinEventProc);
|
||||
_foregroundWindowChangedEvent = Native.RegisterForForegroundWindowChangedEvent(_delegate);
|
||||
|
||||
_dispatcher = Dispatcher.CurrentDispatcher;
|
||||
Dispatcher.Run();
|
||||
Logger.Instance.LogMessage(TracingLevel.DEBUG, "Window Changed Event Loop Stopped");
|
||||
});
|
||||
_thread.SetApartmentState(ApartmentState.STA);
|
||||
_thread.Start();
|
||||
}
|
||||
|
||||
public event Action? WindowChanged;
|
||||
|
||||
CancellationTokenSource? _cancellationTokenSource = null;
|
||||
|
||||
private async void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
|
||||
{
|
||||
try
|
||||
{
|
||||
//debounce the window changed events by 100 ms because if you click mouse over an application on the start bar
|
||||
//and then click on the preview window, it will quickly go from current -> fallback -> new app
|
||||
//which can often result in it getting stuck on the fallback app
|
||||
_cancellationTokenSource?.Cancel();
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
await Task.Delay(100, _cancellationTokenSource.Token);
|
||||
WindowChanged?.Invoke();
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
//ignored
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unexpected Error in EventHandler:\n {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#nullable restore
|
@ -33,7 +33,7 @@
|
||||
"Name": "Focused Application Volume",
|
||||
"Description": "Control the volume of the focused application",
|
||||
"URL": "https://github.com/dlprows/FocusVolumeControl",
|
||||
"Version": "1.1.0",
|
||||
"Version": "1.4.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>
|
||||
<Button x:Name="_pauseButton" Grid.Row="0" Click="PauseClicked"></Button>
|
||||
|
||||
<TextBlock x:Name="_tf" Grid.Row="0">current</TextBlock>
|
||||
<TextBlock x:Name="_tf2" Grid.Row="1">list</TextBlock>
|
||||
<TextBox x:Name="_tf" Grid.Row="1" IsReadOnly="True">current</TextBox>
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Window>
|
||||
|
@ -1,9 +1,13 @@
|
||||
using CoreAudio;
|
||||
using FocusVolumeControl;
|
||||
using FocusVolumeControl;
|
||||
using FocusVolumeControl.AudioHelpers;
|
||||
using FocusVolumeControl.AudioSessions;
|
||||
using NHotkey;
|
||||
using NHotkey.Wpf;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace SoundBrowser;
|
||||
|
||||
@ -15,6 +19,8 @@ public partial class MainWindow : Window
|
||||
|
||||
AudioHelper _audioHelper;
|
||||
Native.WinEventDelegate _delegate;
|
||||
bool _paused = false;
|
||||
bool _doOnce = false;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
@ -24,20 +30,93 @@ public partial class MainWindow : Window
|
||||
//normally you can just pass a lambda, but for some reason, that seems to get garbage collected
|
||||
_delegate = new Native.WinEventDelegate(WinEventProc);
|
||||
Native.RegisterForForegroundWindowChangedEvent(_delegate);
|
||||
|
||||
HotkeyManager.Current.AddOrReplace("Pause", Key.P, ModifierKeys.Control | ModifierKeys.Alt | ModifierKeys.Shift, OnPauseShortcut);
|
||||
}
|
||||
|
||||
private void OnPauseShortcut(object? sender, HotkeyEventArgs e)
|
||||
{
|
||||
if( _paused )
|
||||
{
|
||||
_paused = false;
|
||||
_pauseButton.Content = "Running - click to pause on next app";
|
||||
}
|
||||
else
|
||||
{
|
||||
_paused = true;
|
||||
_doOnce = true;
|
||||
_pauseButton.Content = "Paused - click to resume";
|
||||
_handle = Native.GetForegroundWindow();
|
||||
DoThing();
|
||||
}
|
||||
}
|
||||
|
||||
private void PauseClicked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_paused = !_paused;
|
||||
if(_paused)
|
||||
{
|
||||
_pauseButton.Content = "Pausing on next app";
|
||||
_doOnce = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_pauseButton.Content = "Running - click to pause on next app";
|
||||
}
|
||||
}
|
||||
|
||||
public void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
|
||||
{
|
||||
SetupCurrentAppFields();
|
||||
SetupAllSessionFields();
|
||||
_handle = Native.GetForegroundWindow();
|
||||
DoThing();
|
||||
}
|
||||
|
||||
private void SetupCurrentAppFields()
|
||||
private void DoThing()
|
||||
{
|
||||
var handle = Native.GetForegroundWindow();
|
||||
var sb = new StringBuilder();
|
||||
if(_paused)
|
||||
{
|
||||
if(!_doOnce)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_doOnce = false;
|
||||
_pauseButton.Content = "Paused - click to resume";
|
||||
}
|
||||
else
|
||||
{
|
||||
_pauseButton.Content = "Running - click to pause on next app";
|
||||
}
|
||||
|
||||
if (handle != IntPtr.Zero)
|
||||
var sb = new StringBuilder();
|
||||
SetupCurrentAppFields(sb);
|
||||
sb.AppendLine("");
|
||||
sb.AppendLine("-------------------------------------------------------------------------------");
|
||||
sb.AppendLine("");
|
||||
DetermineDefaultDevice(sb);
|
||||
sb.AppendLine("");
|
||||
SetupAllSessionFields(sb);
|
||||
|
||||
_tf.Text = sb.ToString();
|
||||
Trace.WriteLine(sb.ToString());
|
||||
}
|
||||
|
||||
private void DetermineDefaultDevice(StringBuilder sb)
|
||||
{
|
||||
var deviceEnumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator();
|
||||
|
||||
deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia, out var device);
|
||||
|
||||
var name = GetDeviceName(device);
|
||||
|
||||
sb.AppendLine($"Default Audio Device: {name}");
|
||||
}
|
||||
|
||||
IntPtr _handle;
|
||||
|
||||
private void SetupCurrentAppFields(StringBuilder sb)
|
||||
{
|
||||
|
||||
if (_handle != IntPtr.Zero)
|
||||
{
|
||||
//use this in debug to help there be less events
|
||||
|
||||
@ -51,23 +130,17 @@ public partial class MainWindow : Window
|
||||
}
|
||||
*/
|
||||
|
||||
var processes = _audioHelper.GetPossibleProcesses();
|
||||
var processes = _audioHelper.GetPossibleProcesses(_handle);
|
||||
var session = _audioHelper.FindSession(processes);
|
||||
|
||||
sb.AppendLine("Possible Current Processes");
|
||||
foreach (var p in processes)
|
||||
{
|
||||
var displayName = (new NameAndIconHelper()).GetProcessInfo(p);
|
||||
|
||||
sb.AppendLine($"pid: {p.Id}");
|
||||
sb.AppendLine($"\tpid: {p.Id}");
|
||||
sb.AppendLine($"\tprocessName: {p.ProcessName}");
|
||||
try
|
||||
{
|
||||
sb.AppendLine($"\tFileDescription: {p!.MainModule!.FileVersionInfo.FileDescription}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
sb.AppendLine("\tFileDescription: ##ERROR##");
|
||||
}
|
||||
|
||||
sb.AppendLine($"\tDisplayName: {displayName}");
|
||||
|
||||
}
|
||||
|
||||
@ -75,6 +148,7 @@ public partial class MainWindow : Window
|
||||
if (session != null)
|
||||
{
|
||||
sb.AppendLine("picked the following best match");
|
||||
sb.AppendLine($"\tpid: {string.Join(", ", session.Pids)}");
|
||||
sb.AppendLine($"\tsession: {session.DisplayName}");
|
||||
sb.AppendLine($"\tvolume: {session.GetVolumeLevel()}");
|
||||
}
|
||||
@ -84,35 +158,70 @@ public partial class MainWindow : Window
|
||||
}
|
||||
}
|
||||
|
||||
_tf.Text = sb.ToString();
|
||||
}
|
||||
|
||||
private void SetupAllSessionFields()
|
||||
private void SetupAllSessionFields(StringBuilder sb)
|
||||
{
|
||||
_tf2.Text = "";
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("-------------------------------------------------------------------------------");
|
||||
var deviceEnumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator();
|
||||
|
||||
var deviceEnumerator = new MMDeviceEnumerator(Guid.NewGuid());
|
||||
deviceEnumerator.EnumAudioEndpoints(DataFlow.Render, DeviceState.Active, out var deviceCollection);
|
||||
deviceCollection.GetCount(out var num);
|
||||
|
||||
using var device = deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
|
||||
using var manager = device.AudioSessionManager2;
|
||||
|
||||
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
|
||||
var name = GetDeviceName(device);
|
||||
sb.AppendLine($"----{name}----");
|
||||
|
||||
var displayName = audioProcess!.MainModule!.FileVersionInfo.FileDescription;
|
||||
|
||||
sb.AppendLine($"pid: {audioProcess.Id}");
|
||||
sb.AppendLine($"\tprocessName: {audioProcess.ProcessName}");
|
||||
sb.AppendLine($"\tsession: {displayName}");
|
||||
Guid iid = typeof(IAudioSessionManager2).GUID;
|
||||
device.Activate(ref iid, CLSCTX.ALL, IntPtr.Zero, out var m);
|
||||
var manager = (IAudioSessionManager2)m;
|
||||
|
||||
|
||||
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);
|
||||
session.GetIconPath(out var path);
|
||||
var audioProcess = Process.GetProcessById(processId);
|
||||
if(audioProcess.Id == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var displayName = (new NameAndIconHelper()).GetProcessInfo(audioProcess);
|
||||
sb.AppendLine($"pid: {audioProcess.Id}\t\t processName: {displayName}");
|
||||
}
|
||||
|
||||
sb.Append("\n");
|
||||
}
|
||||
|
||||
_tf2.Text = sb.ToString();
|
||||
}
|
||||
|
||||
private string GetDeviceName(IMMDevice device)
|
||||
{
|
||||
var fnkey = PKey.DeviceFriendlyName;
|
||||
device.OpenPropertyStore(EStgmAccess.STGM_READ, out var propertyStore);
|
||||
propertyStore.GetCount(out var count);
|
||||
for(int i = 0; i < count; i++)
|
||||
{
|
||||
propertyStore.GetAt(i, out var pkey);
|
||||
if(pkey.fmtId == fnkey.fmtId && pkey.PId == fnkey.PId)
|
||||
{
|
||||
propertyStore.GetValue(ref pkey, out var pvalue);
|
||||
|
||||
return (string)pvalue.Value;
|
||||
}
|
||||
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -8,7 +8,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CoreAudio" Version="1.27.0" />
|
||||
<PackageReference Include="NHotkey.Wpf" Version="3.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|