Compare commits
1 Commits
main
...
playWithAp
Author | SHA1 | Date | |
---|---|---|---|
f1d7aeed5d |
67
Overrides.md
@ -1,67 +0,0 @@
|
||||
## 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,11 +12,6 @@ The screen updates to show the name/icon of the app so that you can always know
|
||||
|
||||
![Focus volume control plugin preview](previews/1-preview.png?raw=true)
|
||||
|
||||
## 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
|
||||
|
@ -1,29 +0,0 @@
|
||||
<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>
|
@ -1,168 +0,0 @@
|
||||
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 +0,0 @@
|
||||
global using Xunit;
|
@ -7,8 +7,6 @@ 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
|
||||
@ -23,10 +21,6 @@ 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,10 +1,9 @@
|
||||
using FocusVolumeControl.AudioSessions;
|
||||
using FocusVolumeControl.Overrides;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace FocusVolumeControl.AudioHelpers;
|
||||
|
||||
@ -16,8 +15,6 @@ public class AudioHelper
|
||||
int[] _currentProcesses;
|
||||
int _retryFallbackCount = 0;
|
||||
|
||||
public List<Override> Overrides { get; set; }
|
||||
|
||||
public IAudioSession Current { get; private set; }
|
||||
|
||||
public void ResetCache()
|
||||
@ -42,6 +39,7 @@ public class AudioHelper
|
||||
|
||||
public IAudioSession FindSession(List<Process> processes)
|
||||
{
|
||||
//var blah = new AudioPolicyConfigFactoryImplFor21H2();
|
||||
var results = new ActiveAudioSessionWrapper();
|
||||
Process bestProcessMatch = null;
|
||||
|
||||
@ -53,11 +51,6 @@ public class AudioHelper
|
||||
{
|
||||
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;
|
||||
@ -66,11 +59,6 @@ public class AudioHelper
|
||||
|
||||
manager.GetSessionEnumerator(out var sessionEnumerator);
|
||||
|
||||
if(sessionEnumerator == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var currentIndex = int.MaxValue;
|
||||
|
||||
sessionEnumerator.GetCount(out var count);
|
||||
@ -78,11 +66,6 @@ public class AudioHelper
|
||||
{
|
||||
sessionEnumerator.GetSession(i, out var session);
|
||||
|
||||
if(session == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
session.GetProcessId(out var sessionProcessId);
|
||||
var audioProcess = GetProcessById(sessionProcessId);
|
||||
|
||||
@ -91,6 +74,16 @@ public class AudioHelper
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
blah.GetPersistedDefaultAudioEndpoint(sessionProcessId, DataFlow.Render, Role.Multimedia, out var persistedDeviceId);
|
||||
persistedDeviceId = UnpackDeviceId(persistedDeviceId);
|
||||
if(!string.IsNullOrEmpty(persistedDeviceId) && persistedDeviceId != currentDeviceId)
|
||||
{
|
||||
continue;
|
||||
|
||||
}
|
||||
*/
|
||||
|
||||
var index = processes.FindIndex(x => x.Id == sessionProcessId || x.ProcessName == audioProcess?.ProcessName);
|
||||
|
||||
if (index > -1)
|
||||
@ -132,12 +125,7 @@ public class AudioHelper
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var processes = TryGetProcessFromOverrides();
|
||||
|
||||
if(processes == null)
|
||||
{
|
||||
processes = GetPossibleProcesses();
|
||||
}
|
||||
var processes = GetPossibleProcesses();
|
||||
var processIds = processes?.Select(x => x.Id).ToArray();
|
||||
|
||||
//_currentProcesses null - first time getting sessions
|
||||
@ -186,9 +174,9 @@ public class AudioHelper
|
||||
/// 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)
|
||||
public List<Process> GetPossibleProcesses()
|
||||
{
|
||||
var handle = handleOverride ?? Native.GetForegroundWindow();
|
||||
var handle = Native.GetForegroundWindow();
|
||||
|
||||
if (handle == IntPtr.Zero)
|
||||
{
|
||||
@ -198,19 +186,10 @@ public class AudioHelper
|
||||
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)
|
||||
.Select(x => Process.GetProcessById(x))
|
||||
.ToList();
|
||||
|
||||
if(processes.FirstOrDefault()?.ProcessName == "explorer")
|
||||
@ -256,11 +235,6 @@ public class AudioHelper
|
||||
{
|
||||
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;
|
||||
@ -268,21 +242,11 @@ public class AudioHelper
|
||||
|
||||
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);
|
||||
@ -301,10 +265,6 @@ public class AudioHelper
|
||||
{
|
||||
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);
|
||||
@ -313,21 +273,11 @@ public class AudioHelper
|
||||
|
||||
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);
|
||||
@ -350,138 +300,123 @@ public class AudioHelper
|
||||
}
|
||||
|
||||
|
||||
static Dictionary<string, List<int>> _audioSessionNameCache = new Dictionary<string, List<int>>();
|
||||
private List<int> GetFromNameCacheIfPossible(string processName)
|
||||
|
||||
|
||||
class AudioPolicyConfigFactoryImplFor21H2
|
||||
{
|
||||
if(_audioSessionNameCache.TryGetValue(processName, out var result))
|
||||
private readonly IAudioPolicyConfigFactoryVariantFor21H2 _factory;
|
||||
|
||||
internal AudioPolicyConfigFactoryImplFor21H2()
|
||||
{
|
||||
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;
|
||||
var iid = typeof(IAudioPolicyConfigFactoryVariantFor21H2).GUID;
|
||||
Combase.RoGetActivationFactory("Windows.Media.Internal.AudioPolicyConfig", ref iid, out object factory);
|
||||
_factory = (IAudioPolicyConfigFactoryVariantFor21H2)factory;
|
||||
}
|
||||
|
||||
private List<Process> TryGetProcessFromOverrides(IntPtr? handleOverride = null)
|
||||
public uint ClearAllPersistedApplicationDefaultEndpoints()
|
||||
{
|
||||
var handle = handleOverride ?? Native.GetForegroundWindow();
|
||||
return _factory.ClearAllPersistedApplicationDefaultEndpoints();
|
||||
}
|
||||
|
||||
if (Overrides?.Any() == true)
|
||||
public uint GetPersistedDefaultAudioEndpoint(int processId, DataFlow flow, Role role, out string deviceId)
|
||||
{
|
||||
Process tmp = null;
|
||||
foreach (var p in Process.GetProcesses())
|
||||
return _factory.GetPersistedDefaultAudioEndpoint(processId, flow, role, out deviceId);
|
||||
}
|
||||
|
||||
public uint SetPersistedDefaultAudioEndpoint(int processId, DataFlow flow, Role role, IntPtr deviceId)
|
||||
{
|
||||
if (p.MainWindowHandle == handle)
|
||||
{
|
||||
tmp = p;
|
||||
break;
|
||||
return _factory.SetPersistedDefaultAudioEndpoint(processId, flow, role, deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
if (tmp != null)
|
||||
|
||||
[Guid("ab3d4648-e242-459f-b02f-541c70306324")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIInspectable)]
|
||||
public interface IAudioPolicyConfigFactoryVariantFor21H2
|
||||
{
|
||||
foreach (var o in Overrides)
|
||||
int __incomplete__add_CtxVolumeChange();
|
||||
int __incomplete__remove_CtxVolumeChanged();
|
||||
int __incomplete__add_RingerVibrateStateChanged();
|
||||
int __incomplete__remove_RingerVibrateStateChange();
|
||||
int __incomplete__SetVolumeGroupGainForId();
|
||||
int __incomplete__GetVolumeGroupGainForId();
|
||||
int __incomplete__GetActiveVolumeGroupForEndpointId();
|
||||
int __incomplete__GetVolumeGroupsForEndpoint();
|
||||
int __incomplete__GetCurrentVolumeContext();
|
||||
int __incomplete__SetVolumeGroupMuteForId();
|
||||
int __incomplete__GetVolumeGroupMuteForId();
|
||||
int __incomplete__SetRingerVibrateState();
|
||||
int __incomplete__GetRingerVibrateState();
|
||||
int __incomplete__SetPreferredChatApplication();
|
||||
int __incomplete__ResetPreferredChatApplication();
|
||||
int __incomplete__GetPreferredChatApplication();
|
||||
int __incomplete__GetCurrentChatApplications();
|
||||
int __incomplete__add_ChatContextChanged();
|
||||
int __incomplete__remove_ChatContextChanged();
|
||||
[PreserveSig]
|
||||
uint SetPersistedDefaultAudioEndpoint(int processId, DataFlow flow, Role role, IntPtr deviceId);
|
||||
[PreserveSig]
|
||||
uint GetPersistedDefaultAudioEndpoint(int processId, DataFlow flow, Role role, [Out, MarshalAs(UnmanagedType.HString)] out string deviceId);
|
||||
[PreserveSig]
|
||||
uint ClearAllPersistedApplicationDefaultEndpoints();
|
||||
}
|
||||
|
||||
[Guid("2a59116d-6c4f-45e0-a74f-707e3fef9258")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIInspectable)]
|
||||
public interface IAudioPolicyConfigFactoryVariantForDownlevel
|
||||
{
|
||||
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))
|
||||
)
|
||||
int __incomplete__add_CtxVolumeChange();
|
||||
int __incomplete__remove_CtxVolumeChanged();
|
||||
int __incomplete__add_RingerVibrateStateChanged();
|
||||
int __incomplete__remove_RingerVibrateStateChange();
|
||||
int __incomplete__SetVolumeGroupGainForId();
|
||||
int __incomplete__GetVolumeGroupGainForId();
|
||||
int __incomplete__GetActiveVolumeGroupForEndpointId();
|
||||
int __incomplete__GetVolumeGroupsForEndpoint();
|
||||
int __incomplete__GetCurrentVolumeContext();
|
||||
int __incomplete__SetVolumeGroupMuteForId();
|
||||
int __incomplete__GetVolumeGroupMuteForId();
|
||||
int __incomplete__SetRingerVibrateState();
|
||||
int __incomplete__GetRingerVibrateState();
|
||||
int __incomplete__SetPreferredChatApplication();
|
||||
int __incomplete__ResetPreferredChatApplication();
|
||||
int __incomplete__GetPreferredChatApplication();
|
||||
int __incomplete__GetCurrentChatApplications();
|
||||
int __incomplete__add_ChatContextChanged();
|
||||
int __incomplete__remove_ChatContextChanged();
|
||||
[PreserveSig]
|
||||
uint SetPersistedDefaultAudioEndpoint(int processId, DataFlow flow, Role role, IntPtr deviceId);
|
||||
[PreserveSig]
|
||||
uint GetPersistedDefaultAudioEndpoint(int processId, DataFlow flow, Role role, [Out, MarshalAs(UnmanagedType.HString)] out string deviceId);
|
||||
[PreserveSig]
|
||||
uint ClearAllPersistedApplicationDefaultEndpoints();
|
||||
}
|
||||
|
||||
static class Combase
|
||||
{
|
||||
var ids = FindAudioSessionByProcessName(o.AudioProcessName);
|
||||
if (ids?.Count > 0)
|
||||
[DllImport("combase.dll", PreserveSig = false)]
|
||||
public static extern void RoGetActivationFactory(
|
||||
[MarshalAs(UnmanagedType.HString)] string activatableClassId,
|
||||
[In] ref Guid iid,
|
||||
[Out, MarshalAs(UnmanagedType.IInspectable)] out Object factory);
|
||||
|
||||
[DllImport("combase.dll", PreserveSig = false)]
|
||||
public static extern void WindowsCreateString(
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string src,
|
||||
[In] uint length,
|
||||
[Out] out IntPtr hstring);
|
||||
}
|
||||
|
||||
private const string DEVINTERFACE_AUDIO_RENDER = "#{e6327cad-dcec-4949-ae8a-991e976a79d2}";
|
||||
private const string DEVINTERFACE_AUDIO_CAPTURE = "#{2eef81be-33fa-4800-9670-1cd474972c3f}";
|
||||
private const string MMDEVAPI_TOKEN = @"\\?\SWD#MMDEVAPI#";
|
||||
private string UnpackDeviceId(string deviceId)
|
||||
{
|
||||
return ids.Distinct()
|
||||
.Select(Process.GetProcessById)
|
||||
.ToList();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
if (deviceId.StartsWith(MMDEVAPI_TOKEN)) deviceId = deviceId.Remove(0, MMDEVAPI_TOKEN.Length);
|
||||
if (deviceId.EndsWith(DEVINTERFACE_AUDIO_RENDER)) deviceId = deviceId.Remove(deviceId.Length - DEVINTERFACE_AUDIO_RENDER.Length);
|
||||
if (deviceId.EndsWith(DEVINTERFACE_AUDIO_CAPTURE)) deviceId = deviceId.Remove(deviceId.Length - DEVINTERFACE_AUDIO_CAPTURE.Length);
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -83,41 +83,6 @@ public class NameAndIconHelper
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
@ -10,19 +10,13 @@ using FocusVolumeControl.AudioSession;
|
||||
|
||||
namespace FocusVolumeControl.AudioSessions;
|
||||
|
||||
#nullable enable
|
||||
|
||||
public sealed class ActiveAudioSessionWrapper : IAudioSession
|
||||
{
|
||||
public string DisplayName { get; set; } = null!;
|
||||
public string DisplayName { get; set; }
|
||||
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()
|
||||
{
|
||||
@ -104,4 +98,3 @@ public sealed class ActiveAudioSessionWrapper : IAudioSession
|
||||
return VolumeHelpers.GetVolumePercentage(level);
|
||||
}
|
||||
}
|
||||
#nullable restore
|
||||
|
@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Runtime.InteropServices;
|
||||
using FILETIME = System.Runtime.InteropServices.ComTypes.FILETIME;
|
||||
|
||||
namespace FocusVolumeControl.AudioSessions;
|
||||
|
||||
@ -92,91 +91,6 @@ public interface IMMDeviceEnumerator
|
||||
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
|
||||
{
|
||||
@ -184,7 +98,7 @@ public interface IMMDevice
|
||||
int Activate(ref Guid iid, CLSCTX dwClsCtx, IntPtr pActivationParams, [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface);
|
||||
|
||||
[PreserveSig]
|
||||
int OpenPropertyStore(EStgmAccess stgmAccess, out IPropertyStore propertyStore);
|
||||
int NotImpl1();
|
||||
[PreserveSig]
|
||||
int GetId([Out, MarshalAs(UnmanagedType.LPWStr)] out string ppstrId);
|
||||
|
||||
|
@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace FocusVolumeControl.AudioSessions;
|
||||
|
||||
@ -17,6 +15,4 @@ public interface IAudioSession
|
||||
public void IncrementVolumeLevel(int step, int ticks);
|
||||
|
||||
public int GetVolumeLevel();
|
||||
|
||||
public IEnumerable<int> Pids { get; }
|
||||
}
|
||||
|
@ -4,8 +4,6 @@ using FocusVolumeControl.UI;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace FocusVolumeControl.AudioSession
|
||||
{
|
||||
public abstract class IconWrapper
|
||||
@ -103,4 +101,3 @@ namespace FocusVolumeControl.AudioSession
|
||||
}
|
||||
|
||||
}
|
||||
#nullable restore
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace FocusVolumeControl.AudioSessions;
|
||||
@ -16,9 +15,6 @@ internal sealed class SystemSoundsAudioSession : IAudioSession
|
||||
ISimpleAudioVolume _volumeControl;
|
||||
|
||||
public string DisplayName => "System sounds";
|
||||
|
||||
public IEnumerable<int> Pids => new int[0];
|
||||
|
||||
public string GetIcon() => "Images/systemSounds";
|
||||
|
||||
public void ToggleMute()
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace FocusVolumeControl.AudioSessions;
|
||||
@ -16,8 +15,6 @@ internal sealed class SystemVolumeAudioSession : IAudioSession
|
||||
public string DisplayName => "System Volume";
|
||||
public string GetIcon() => "Images/encoderIcon";
|
||||
|
||||
public IEnumerable<int> Pids => new int[0];
|
||||
|
||||
public void ToggleMute()
|
||||
{
|
||||
_volumeControl.SetMute(!IsMuted(), Guid.Empty);
|
||||
|
@ -2,7 +2,6 @@
|
||||
using BarRaider.SdTools.Payloads;
|
||||
using FocusVolumeControl.AudioHelpers;
|
||||
using FocusVolumeControl.AudioSessions;
|
||||
using FocusVolumeControl.Overrides;
|
||||
using FocusVolumeControl.UI;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@ -16,12 +15,6 @@ namespace FocusVolumeControl;
|
||||
[PluginActionId("com.dlprows.focusvolumecontrol.dialaction")]
|
||||
public class DialAction : EncoderBase
|
||||
{
|
||||
private const string DefaultOverrides =
|
||||
"""
|
||||
//eq: HELLDIVERS™ 2
|
||||
//helldivers2
|
||||
""";
|
||||
|
||||
private class PluginSettings
|
||||
{
|
||||
[JsonProperty("fallbackBehavior")]
|
||||
@ -30,15 +23,11 @@ public class DialAction : EncoderBase
|
||||
[JsonProperty("stepSize")]
|
||||
public int StepSize { get; set; }
|
||||
|
||||
[JsonProperty("overrides")]
|
||||
public string Overrides { get; set; }
|
||||
|
||||
public static PluginSettings CreateDefaultSettings()
|
||||
{
|
||||
PluginSettings instance = new PluginSettings();
|
||||
instance.FallbackBehavior = FallbackBehavior.SystemSounds;
|
||||
instance.StepSize = 1;
|
||||
instance.Overrides = DefaultOverrides;
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
@ -57,18 +46,12 @@ public class DialAction : EncoderBase
|
||||
else
|
||||
{
|
||||
settings = payload.Settings.ToObject<PluginSettings>();
|
||||
if(string.IsNullOrEmpty(settings.Overrides))
|
||||
{
|
||||
settings.Overrides = DefaultOverrides;
|
||||
_ = SaveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
WindowChangedEventLoop.Instance.WindowChanged += WindowChanged;
|
||||
|
||||
try
|
||||
{
|
||||
_audioHelper.Overrides = OverrideParser.Parse(settings.Overrides);
|
||||
//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);
|
||||
@ -237,8 +220,7 @@ public class DialAction : EncoderBase
|
||||
try
|
||||
{
|
||||
Tools.AutoPopulateSettings(settings, payload.Settings);
|
||||
_audioHelper.Overrides = OverrideParser.Parse(settings.Overrides);
|
||||
//_ = SaveSettings();
|
||||
_ = SaveSettings();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -66,10 +66,6 @@
|
||||
<Compile Include="AudioSessions\IAudioSession.cs" />
|
||||
<Compile Include="FallbackBehavior.cs" />
|
||||
<Compile Include="AudioHelpers\NameAndIconHelper.cs" />
|
||||
<Compile Include="InternalsVisibleTo.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" />
|
||||
@ -95,7 +91,7 @@
|
||||
<Content Include="Images\**\*.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="PropertyInspector\**\*.js;PropertyInspector\**\*.css;PropertyInspector\assets\*.*">
|
||||
<Content Include="PropertyInspector\**\*.js;PropertyInspector\**\*.css">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="PropertyInspector\PluginActionPI.html">
|
||||
|
@ -1,4 +0,0 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("FocusVolumeControl.UnitTests")]
|
||||
[assembly: InternalsVisibleTo("SoundBrowser")]
|
@ -1,10 +0,0 @@
|
||||
namespace FocusVolumeControl.Overrides
|
||||
{
|
||||
public enum MatchType
|
||||
{
|
||||
Equal,
|
||||
StartsWith,
|
||||
EndsWith,
|
||||
Regex,
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
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; }
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
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,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="./css/sdpi.css">
|
||||
<link rel="sytlesheet" href="./css/rangeTooltip.css">
|
||||
<link rel="stylesheet" href="./lib/sdpi.css">
|
||||
<link rel="sytlesheet" href="./lib/rangeTooltip.css">
|
||||
<script src="lib/sdtools.common.js"></script>
|
||||
<script src="lib/rangeTooltip.js"></script>
|
||||
</head>
|
||||
@ -16,20 +16,13 @@
|
||||
|
||||
<div class="sdpi-item">
|
||||
<div class="sdpi-item-label">Fallback</div>
|
||||
<select class="sdpi-item-value select sdProperty" id="fallbackBehavior" oninput="setSettings()">
|
||||
<select class="sdpi-item-value sdProperty" id="fallbackBehavior" oninput="setSettings()">
|
||||
<option value="0">System Sounds</option>
|
||||
<option value="1">Previous App</option>
|
||||
<option value="2">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>
|
||||
@ -39,29 +32,15 @@
|
||||
<span class="clickable" value="1">10</span>
|
||||
</div>
|
||||
</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>
|
||||
<div class="sdpi-info-label hidden" style="top: -1000;" value="">Tooltip</div>
|
||||
|
||||
<details>
|
||||
<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>
|
||||
<p>If you look at windows volume mixer, you will see that not all applications can have their volume controlled. The fallback behavior controls what happens when you are in an application that doesn't show up in the volume mixer</p>
|
||||
<p>* System Sounds - Switch to system sounds. This will control windows sound effects such as when an error sound plays. If you're in an application that is making beeping sounds, this will often allow you to control those sounds while leaving things like your music/videos alone</p>
|
||||
<p>* Previous App - Use the last app that had a volume control. This can result in the stream deck not changing after you have quit an application.</p>
|
||||
<p>* Default Output Device Volume - Switch to the main volume control for the default output device. This will change the volume of the default output device. This is usually volume for all applications, unless you override the output device for specific applications.</p>
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,3 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 173 B |
@ -1,5 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 268 B |
Before Width: | Height: | Size: 234 B |
@ -1,3 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 212 B |
@ -1,25 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 1.4 KiB |
@ -1,7 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 780 B |
@ -1,3 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 133 B |
@ -1,3 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 159 B |
@ -29,7 +29,6 @@ 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');
|
||||
@ -40,7 +39,6 @@ function setElementLabel(elem, str) {
|
||||
console.log('setElementLabel ERROR! No .rangeLabel found', elem);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
function setRangeTooltips() {
|
||||
console.log("Loading setRangeTooltips");
|
||||
@ -70,7 +68,7 @@ function setRangeTooltips() {
|
||||
tooltip.style.top = (rangeRect.top - 32) + 'px';
|
||||
}
|
||||
|
||||
//setElementLabel(elem, labelStr)
|
||||
setElementLabel(elem, labelStr)
|
||||
};
|
||||
|
||||
rangeSelector.addEventListener(
|
||||
@ -102,7 +100,7 @@ function setRangeTooltips() {
|
||||
console.log('rangeTooltip settingsUpdated called');
|
||||
window.setTimeout(function () {
|
||||
let str = calcRangeLabel(rangeSelector);
|
||||
//setElementLabel(elem, str);
|
||||
setElementLabel(elem, str);
|
||||
}, 500);
|
||||
},
|
||||
false
|
||||
@ -114,7 +112,7 @@ function setRangeTooltips() {
|
||||
console.log('rangeTooltip websocketCreate called');
|
||||
window.setTimeout(function () {
|
||||
let str = calcRangeLabel(rangeSelector);
|
||||
//setElementLabel(elem, str);
|
||||
setElementLabel(elem, str);
|
||||
}, 500);
|
||||
},
|
||||
false
|
||||
|
@ -1,3 +1,65 @@
|
||||
.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;
|
||||
@ -8,16 +70,6 @@ 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;
|
||||
@ -67,14 +119,6 @@ hr2,
|
||||
margin: 8px 0px;
|
||||
}
|
||||
|
||||
|
||||
h1 {
|
||||
font-size: 1.3em;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.sdpi-heading::before,
|
||||
.sdpi-heading::after {
|
||||
content: "";
|
||||
@ -181,9 +225,6 @@ progress {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
|
||||
/* TABS */
|
||||
|
||||
.tabs {
|
||||
/**
|
||||
* Setting display to flex makes this container lay
|
||||
@ -191,91 +232,19 @@ progress {
|
||||
* as in the above "Stepper input" example.
|
||||
*/
|
||||
display: flex;
|
||||
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;
|
||||
border-bottom: 1px solid #D7DBDD;
|
||||
}
|
||||
|
||||
.tab {
|
||||
cursor: pointer;
|
||||
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;
|
||||
padding: 5px 30px;
|
||||
color: #16a2d7;
|
||||
font-size: 9pt;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
.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;
|
||||
.tab.is-tab-selected {
|
||||
border-bottom-color: #4ebbe4;
|
||||
}
|
||||
|
||||
select {
|
||||
@ -337,30 +306,18 @@ 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: 30px;
|
||||
align-items: first baseline;
|
||||
min-height: 32px;
|
||||
align-items: center;
|
||||
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;
|
||||
}
|
||||
@ -455,7 +412,7 @@ table > caption {
|
||||
padding-right: 5px;
|
||||
font-weight: 600;
|
||||
-webkit-user-select: none;
|
||||
line-height: normal;
|
||||
line-height: 24px;
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
@ -529,8 +486,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 {
|
||||
@ -675,16 +632,6 @@ 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;
|
||||
@ -707,21 +654,21 @@ details.message {
|
||||
}
|
||||
|
||||
details.message > summary:first-of-type {
|
||||
line-height: 20px;
|
||||
/*line-height: 48px;*/
|
||||
}
|
||||
|
||||
details.message h1 {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* details:not(.pointer)>summary {
|
||||
details:not(.pointer) > summary {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
details > summary::-webkit-details-marker,
|
||||
details > summary::-webkit-details-marker
|
||||
.message > summary::-webkit-details-marker {
|
||||
display: none;
|
||||
} */
|
||||
}
|
||||
|
||||
.info20,
|
||||
.question,
|
||||
@ -755,6 +702,7 @@ 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;
|
||||
@ -783,6 +731,7 @@ details > summary::-webkit-details-marker,
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
|
||||
.sdpi-bottom-bar {
|
||||
display: flex;
|
||||
align-self: right;
|
||||
@ -810,6 +759,7 @@ details a {
|
||||
padding-right: 28px;
|
||||
}
|
||||
|
||||
|
||||
input:not([type="range"]),
|
||||
textarea {
|
||||
-webkit-appearance: none;
|
||||
@ -877,6 +827,7 @@ input[type="checkbox"] {
|
||||
margin-top: -2px;
|
||||
min-width: 8px;
|
||||
text-align: right;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
@ -889,10 +840,6 @@ input[type="checkbox"] {
|
||||
|
||||
span + input[type="range"] {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
span + .range-container > input[type="range"],
|
||||
span + input[type="range"] {
|
||||
max-width: 168px;
|
||||
}
|
||||
|
||||
@ -1081,6 +1028,7 @@ textarea {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
.card-carousel-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -1143,8 +1091,7 @@ 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; */
|
||||
text-align: center;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
z-index: 3;
|
||||
}
|
||||
@ -1167,7 +1114,6 @@ 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 {
|
||||
@ -1195,8 +1141,44 @@ 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: url(../assets/elg_calendar_inv.svg) no-repeat center;
|
||||
background: transparent;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
@ -1204,30 +1186,53 @@ textarea {
|
||||
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 {
|
||||
flex: 1;
|
||||
-webkit-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;
|
||||
@ -1303,6 +1308,7 @@ input:required:valid {
|
||||
background-color: var(--sdpi-background);
|
||||
}
|
||||
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
@ -1324,72 +1330,23 @@ a {
|
||||
color: #7397d2;
|
||||
}
|
||||
|
||||
input[type="week"] {
|
||||
-webkit-appearance: auto !important;
|
||||
appearance: auto !important;
|
||||
.testcontainer {
|
||||
display: flex;
|
||||
background-color: #0000ff20;
|
||||
max-width: 400px;
|
||||
height: 200px;
|
||||
align-content: space-evenly;
|
||||
}
|
||||
|
||||
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;
|
||||
input[type=range] {
|
||||
-webkit-appearance: none;
|
||||
/* background-color: green; */
|
||||
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;
|
||||
@ -1638,8 +1595,7 @@ input[type="range"].colortemperature::-webkit-slider-runnable-track {
|
||||
background-image: linear-gradient(to right, #94d0ec, #ffb165);
|
||||
}
|
||||
|
||||
input[type="range"].colorbrightness.greyscale::-webkit-slider-runnable-track,
|
||||
input[type="range"].colorbrightness.grayscale::-webkit-slider-runnable-track {
|
||||
input[type="range"].colorbrightness::-webkit-slider-runnable-track {
|
||||
background-color: #efefef;
|
||||
background-image: linear-gradient(to right, black, rgba(0, 0, 0, 0));
|
||||
}
|
||||
@ -1692,54 +1648,3 @@ 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;
|
||||
}
|
@ -7,8 +7,6 @@ using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace FocusVolumeControl.UI
|
||||
{
|
||||
internal class JavaIconExtractor
|
||||
@ -96,5 +94,3 @@ namespace FocusVolumeControl.UI
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#nullable restore
|
||||
|
@ -8,8 +8,6 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Threading;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace FocusVolumeControl
|
||||
{
|
||||
internal class WindowChangedEventLoop
|
||||
@ -18,10 +16,10 @@ namespace FocusVolumeControl
|
||||
public static WindowChangedEventLoop Instance => _lazy.Value;
|
||||
|
||||
readonly Thread _thread;
|
||||
Dispatcher? _dispatcher;
|
||||
Dispatcher _dispatcher;
|
||||
|
||||
IntPtr _foregroundWindowChangedEvent;
|
||||
Native.WinEventDelegate? _delegate;
|
||||
Native.WinEventDelegate _delegate;
|
||||
|
||||
private WindowChangedEventLoop()
|
||||
{
|
||||
@ -39,7 +37,7 @@ namespace FocusVolumeControl
|
||||
_thread.Start();
|
||||
}
|
||||
|
||||
public event Action? WindowChanged;
|
||||
public event Action WindowChanged;
|
||||
|
||||
CancellationTokenSource? _cancellationTokenSource = null;
|
||||
|
||||
@ -66,5 +64,3 @@ namespace FocusVolumeControl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#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.3.1",
|
||||
"Version": "1.2.0",
|
||||
"CodePath": "FocusVolumeControl",
|
||||
"Category": "Volume Control [dlprows]",
|
||||
"Icon": "Images/pluginIcon",
|
||||
|
@ -12,9 +12,9 @@
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Button x:Name="_pauseButton" Grid.Row="0" Click="PauseClicked"></Button>
|
||||
|
||||
<TextBox x:Name="_tf" Grid.Row="1" IsReadOnly="True">current</TextBox>
|
||||
<TextBlock x:Name="_tf" Grid.Row="0">current</TextBlock>
|
||||
<TextBlock x:Name="_tf2" Grid.Row="1">list</TextBlock>
|
||||
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
|
@ -1,13 +1,10 @@
|
||||
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;
|
||||
|
||||
@ -19,8 +16,6 @@ public partial class MainWindow : Window
|
||||
|
||||
AudioHelper _audioHelper;
|
||||
Native.WinEventDelegate _delegate;
|
||||
bool _paused = false;
|
||||
bool _doOnce = false;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
@ -30,93 +25,20 @@ 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)
|
||||
{
|
||||
_handle = Native.GetForegroundWindow();
|
||||
DoThing();
|
||||
SetupCurrentAppFields();
|
||||
SetupAllSessionFields();
|
||||
}
|
||||
|
||||
private void DoThing()
|
||||
private void SetupCurrentAppFields()
|
||||
{
|
||||
if(_paused)
|
||||
{
|
||||
if(!_doOnce)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_doOnce = false;
|
||||
_pauseButton.Content = "Paused - click to resume";
|
||||
}
|
||||
else
|
||||
{
|
||||
_pauseButton.Content = "Running - click to pause on next app";
|
||||
}
|
||||
|
||||
var handle = Native.GetForegroundWindow();
|
||||
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)
|
||||
if (handle != IntPtr.Zero)
|
||||
{
|
||||
//use this in debug to help there be less events
|
||||
|
||||
@ -130,15 +52,14 @@ public partial class MainWindow : Window
|
||||
}
|
||||
*/
|
||||
|
||||
var processes = _audioHelper.GetPossibleProcesses(_handle);
|
||||
var processes = _audioHelper.GetPossibleProcesses();
|
||||
var session = _audioHelper.FindSession(processes);
|
||||
|
||||
sb.AppendLine("Possible Current Processes");
|
||||
foreach (var p in processes)
|
||||
{
|
||||
var displayName = (new NameAndIconHelper()).GetProcessInfo(p);
|
||||
|
||||
sb.AppendLine($"\tpid: {p.Id}");
|
||||
sb.AppendLine($"pid: {p.Id}");
|
||||
sb.AppendLine($"\tprocessName: {p.ProcessName}");
|
||||
sb.AppendLine($"\tDisplayName: {displayName}");
|
||||
|
||||
@ -148,7 +69,6 @@ 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()}");
|
||||
}
|
||||
@ -158,10 +78,15 @@ public partial class MainWindow : Window
|
||||
}
|
||||
}
|
||||
|
||||
_tf.Text = sb.ToString();
|
||||
}
|
||||
|
||||
private void SetupAllSessionFields(StringBuilder sb)
|
||||
private void SetupAllSessionFields()
|
||||
{
|
||||
_tf2.Text = "";
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("-------------------------------------------------------------------------------");
|
||||
|
||||
var deviceEnumerator = (IMMDeviceEnumerator)new MMDeviceEnumerator();
|
||||
|
||||
deviceEnumerator.EnumAudioEndpoints(DataFlow.Render, DeviceState.Active, out var deviceCollection);
|
||||
@ -172,8 +97,7 @@ public partial class MainWindow : Window
|
||||
{
|
||||
deviceCollection.Item(i, out var device);
|
||||
//todo: put the device name in the output
|
||||
var name = GetDeviceName(device);
|
||||
sb.AppendLine($"----{name}----");
|
||||
sb.AppendLine($"----");
|
||||
|
||||
|
||||
Guid iid = typeof(IAudioSessionManager2).GUID;
|
||||
@ -192,36 +116,13 @@ public partial class MainWindow : Window
|
||||
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 "";
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -7,10 +7,6 @@
|
||||
<UseWPF>true</UseWPF>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NHotkey.Wpf" Version="3.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FocusVolumeControl\FocusVolumeControl.csproj" />
|
||||
</ItemGroup>
|
||||
|