Compare commits
22 Commits
bf258ab84e
...
v1.1.1
Author | SHA1 | Date | |
---|---|---|---|
ceb3494e43 | |||
f9b23a62a3 | |||
06266daa92 | |||
1fea2a2e11 | |||
2e44a27b2b | |||
84a9a89074 | |||
b57ea24b11 | |||
0d056215bc | |||
f0a5a48c73 | |||
cdf05d2913 | |||
8b87ea39cc | |||
2f410f431e | |||
709ed8a89a | |||
29da6a8d17 | |||
07ddd3bf89 | |||
65ad532adb | |||
b4f4bea0fc | |||
90c014e932 | |||
1dc8ab8a2d | |||
a429a435bc | |||
ab769bf7d2 | |||
65f0c9faf6 |
263
.gitignore
vendored
Normal file
@ -0,0 +1,263 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
###################
|
||||
# compiled source #
|
||||
###################
|
||||
*.com
|
||||
*.class
|
||||
*.dll
|
||||
*.exe
|
||||
*.pdb
|
||||
*.dll.config
|
||||
*.cache
|
||||
*.suo
|
||||
# Include dlls if they<65>re in the NuGet packages directory
|
||||
!/packages/*/lib/*.dll
|
||||
# Include dlls if they're in the CommonReferences directory
|
||||
!*CommonReferences/*.dll
|
||||
####################
|
||||
# VS Upgrade stuff #
|
||||
####################
|
||||
_UpgradeReport_Files/
|
||||
###############
|
||||
# Directories #
|
||||
###############
|
||||
bin/
|
||||
obj/
|
||||
TestResults/
|
||||
###################
|
||||
# Web publish log #
|
||||
###################
|
||||
*.Publish.xml
|
||||
#############
|
||||
# Resharper #
|
||||
#############
|
||||
/_ReSharper.*
|
||||
*.ReSharper.*
|
||||
############
|
||||
# Packages #
|
||||
############
|
||||
# it<69>s better to unpack these files and commit the raw source
|
||||
# git has its own built in compression methods
|
||||
*.7z
|
||||
*.dmg
|
||||
*.gz
|
||||
*.iso
|
||||
*.jar
|
||||
*.rar
|
||||
*.tar
|
||||
*.zip
|
||||
######################
|
||||
# Logs and databases #
|
||||
######################
|
||||
*.log
|
||||
*.sqlite
|
||||
# OS generated files #
|
||||
######################
|
||||
.DS_Store?
|
||||
ehthumbs.db
|
||||
Icon?
|
||||
Thumbs.db
|
||||
|
||||
|
||||
# User-specific files
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
build/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
|
||||
# Visual Studo 2015 cache/options directory
|
||||
.vs/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUNIT
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# DNX
|
||||
project.lock.json
|
||||
artifacts/
|
||||
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_i.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.pch
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding add-in
|
||||
.JustCode
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/packages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/packages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/packages/repositories.config
|
||||
|
||||
# Windows Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Windows Store app package directory
|
||||
AppPackages/
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
[Ss]tyle[Cc]op.*
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
node_modules/
|
||||
bower_components/
|
||||
orleans.codegen.cs
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
18
LICENSE
@ -2,8 +2,20 @@ MIT License
|
||||
|
||||
Copyright (c) 2023 dlprows
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
38
README.md
@ -1,2 +1,40 @@
|
||||
# Focus Volume Control Plugin
|
||||
|
||||
A plugin for the Stream Deck+ to control the volume of the focused application.
|
||||
|
||||
## Description
|
||||
|
||||
This Stream Deck plugin utilizes the Stream Deck+ encoders and screen to allow you to control the volume of the focused application.
|
||||
Application volume is changed with the windows volume mixer.
|
||||
|
||||
Unlike faders or potentiometers, the encoders of the Stream Deck+ spin infinitely in either direction. Which means when you change your focused application, you don't have to worry about desynchronization with the current app.
|
||||
The screen updates to show the name/icon of the app so that you can always know what you're about to change.
|
||||
|
||||

|
||||
|
||||
## Developing
|
||||
|
||||
build the solution with visual studio
|
||||
download the [stream deck distribution tool](https://docs.elgato.com/sdk/plugins/packaging) to `src/FocusVolumeControl/`
|
||||
run `install.bat <debug | release>`
|
||||
|
||||
to debug, attach to the FocusVolumeControl running process
|
||||
|
||||
|
||||
There is also a secondary sound browser project which can be used for viewing information about processes and how the algorithm matches them to volume mixers
|
||||
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for detiails
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
Inspiration, code snippets, etc.
|
||||
* [BinRaider's streamdeck-tools](https://github.com/BarRaider/streamdeck-tools)
|
||||
* [Deej](https://github.com/omriharel/deej)
|
||||
* [Stream Deck Developer Guide](https://docs.elgato.com/sdk/plugins/getting-started)
|
||||
* [CoreAudio](https://github.com/morphx666/CoreAudio)
|
||||
|
||||
Inspiration
|
||||
* [PCPanel](https://www.getpcpanel.com/)
|
||||
|
BIN
previews/1-preview.png
Normal file
After Width: | Height: | Size: 285 KiB |
BIN
previews/1-preview.xcf
Normal file
91
src/.editorconfig
Normal file
@ -0,0 +1,91 @@
|
||||
[*.{cs,vb}]
|
||||
dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
||||
tab_width = 4
|
||||
indent_size = 4
|
||||
end_of_line = crlf
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
|
||||
dotnet_style_prefer_auto_properties = true:silent
|
||||
dotnet_style_object_initializer = true:suggestion
|
||||
dotnet_style_collection_initializer = true:suggestion
|
||||
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
|
||||
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
|
||||
dotnet_style_prefer_conditional_expression_over_return = true:silent
|
||||
dotnet_style_explicit_tuple_names = true:suggestion
|
||||
dotnet_style_prefer_inferred_tuple_names = true:suggestion
|
||||
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
|
||||
dotnet_style_prefer_compound_assignment = true:suggestion
|
||||
dotnet_style_prefer_simplified_interpolation = true:suggestion
|
||||
dotnet_style_namespace_match_folder = true:suggestion
|
||||
[*.cs]
|
||||
csharp_indent_labels = one_less_than_current
|
||||
csharp_using_directive_placement = outside_namespace:silent
|
||||
csharp_prefer_simple_using_statement = true:suggestion
|
||||
csharp_prefer_braces = true:silent
|
||||
csharp_style_namespace_declarations = block_scoped:silent
|
||||
csharp_style_prefer_method_group_conversion = true:silent
|
||||
csharp_style_prefer_top_level_statements = true:silent
|
||||
csharp_style_expression_bodied_methods = false:silent
|
||||
csharp_style_expression_bodied_constructors = false:silent
|
||||
csharp_style_expression_bodied_operators = false:silent
|
||||
csharp_style_expression_bodied_properties = true:silent
|
||||
csharp_style_expression_bodied_indexers = true:silent
|
||||
csharp_style_expression_bodied_accessors = true:silent
|
||||
csharp_style_expression_bodied_lambdas = true:silent
|
||||
csharp_style_expression_bodied_local_functions = false:silent
|
||||
csharp_style_throw_expression = true:suggestion
|
||||
csharp_style_prefer_null_check_over_type_check = true:suggestion
|
||||
csharp_prefer_simple_default_expression = true:suggestion
|
||||
csharp_style_prefer_local_over_anonymous_function = true:suggestion
|
||||
csharp_style_prefer_index_operator = true:suggestion
|
||||
csharp_style_prefer_range_operator = true:suggestion
|
||||
csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
|
||||
csharp_style_prefer_tuple_swap = true:suggestion
|
||||
[*.{cs,vb}]
|
||||
#### Naming styles ####
|
||||
|
||||
# Naming rules
|
||||
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
|
||||
|
||||
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
|
||||
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
|
||||
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
|
||||
|
||||
# Symbol specifications
|
||||
|
||||
dotnet_naming_symbols.interface.applicable_kinds = interface
|
||||
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.interface.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
|
||||
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.types.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
|
||||
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.non_field_members.required_modifiers =
|
||||
|
||||
# Naming styles
|
||||
|
||||
dotnet_naming_style.begins_with_i.required_prefix = I
|
||||
dotnet_naming_style.begins_with_i.required_suffix =
|
||||
dotnet_naming_style.begins_with_i.word_separator =
|
||||
dotnet_naming_style.begins_with_i.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.pascal_case.required_prefix =
|
||||
dotnet_naming_style.pascal_case.required_suffix =
|
||||
dotnet_naming_style.pascal_case.word_separator =
|
||||
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||
|
||||
dotnet_naming_style.pascal_case.required_prefix =
|
||||
dotnet_naming_style.pascal_case.required_suffix =
|
||||
dotnet_naming_style.pascal_case.word_separator =
|
||||
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
31
src/FocusVolumeControl.sln
Normal file
@ -0,0 +1,31 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.6.33829.357
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FocusVolumeControl", "FocusVolumeControl\FocusVolumeControl.csproj", "{4635D874-69C0-4010-BE46-77EF92EB1553}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SoundBrowser", "SoundBrowser\SoundBrowser.csproj", "{0E8AB334-82F1-4DBC-9BDA-B6F9714A1847}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{4635D874-69C0-4010-BE46-77EF92EB1553}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4635D874-69C0-4010-BE46-77EF92EB1553}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4635D874-69C0-4010-BE46-77EF92EB1553}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4635D874-69C0-4010-BE46-77EF92EB1553}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0E8AB334-82F1-4DBC-9BDA-B6F9714A1847}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{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
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {22FD348F-67D1-423B-B3E3-C8C0022DCD96}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
26
src/FocusVolumeControl/App.config
Normal file
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8"/>
|
||||
</startup>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="CommandLine" publicKeyToken="5a870481e358d379" culture="neutral"/>
|
||||
<bindingRedirect oldVersion="0.0.0.0-2.9.1.0" newVersion="2.9.1.0"/>
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral"/>
|
||||
<bindingRedirect oldVersion="0.0.0.0-13.0.0.0" newVersion="13.0.0.0"/>
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Drawing.Common" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
||||
<bindingRedirect oldVersion="0.0.0.0-7.0.0.0" newVersion="7.0.0.0"/>
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="NLog" publicKeyToken="5120e14c03d0593c" culture="neutral"/>
|
||||
<bindingRedirect oldVersion="0.0.0.0-5.0.0.0" newVersion="5.0.0.0"/>
|
||||
</dependentAssembly>
|
||||
</assemblyBinding>
|
||||
</runtime>
|
||||
</configuration>
|
217
src/FocusVolumeControl/AudioHelper.cs
Normal file
@ -0,0 +1,217 @@
|
||||
using FocusVolumeControl.AudioSessions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace FocusVolumeControl;
|
||||
|
||||
public class AudioHelper
|
||||
{
|
||||
static object _lock = new object();
|
||||
List<Process> _currentProcesses;
|
||||
|
||||
public IAudioSession Current { get; private set; }
|
||||
|
||||
public void ResetCache()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
Current = null;
|
||||
}
|
||||
}
|
||||
|
||||
public IAudioSession FindSession(List<Process> processes)
|
||||
{
|
||||
var deviceEnumerator = (CoreAudio)new MMDeviceEnumerator();
|
||||
|
||||
deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, out var device);
|
||||
|
||||
Guid iid = typeof(IAudioSessionManager2).GUID;
|
||||
device.Activate(ref iid, 0, IntPtr.Zero, out var m);
|
||||
var manager = (IAudioSessionManager2)m;
|
||||
|
||||
|
||||
manager.GetSessionEnumerator(out var sessionEnumerator);
|
||||
|
||||
var results = new ActiveAudioSessionWrapper();
|
||||
|
||||
sessionEnumerator.GetCount(out var count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
sessionEnumerator.GetSession(i, out var session);
|
||||
|
||||
session.GetProcessId(out var sessionProcessId);
|
||||
var audioProcess = Process.GetProcessById(sessionProcessId);
|
||||
|
||||
if (processes.Any(x => x.Id == sessionProcessId || x.ProcessName == audioProcess?.ProcessName))
|
||||
{
|
||||
try
|
||||
{
|
||||
var displayName = audioProcess.MainModule.FileVersionInfo.FileDescription;
|
||||
if (string.IsNullOrEmpty(displayName))
|
||||
{
|
||||
displayName = audioProcess.ProcessName;
|
||||
}
|
||||
results.DisplayName = displayName;
|
||||
}
|
||||
catch
|
||||
{
|
||||
results.DisplayName ??= audioProcess.ProcessName;
|
||||
}
|
||||
|
||||
results.ExecutablePath ??= audioProcess.MainModule.FileName;
|
||||
|
||||
//some apps like discord have multiple volume processes.
|
||||
results.AddSession(session);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return results.Any() ? results : null;
|
||||
}
|
||||
|
||||
|
||||
public IAudioSession GetActiveSession(FallbackBehavior fallbackBehavior)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var processes = GetPossibleProcesses();
|
||||
|
||||
if (_currentProcesses == null || !_currentProcesses.SequenceEqual(processes))
|
||||
{
|
||||
Current = FindSession(processes);
|
||||
}
|
||||
|
||||
if (Current == null)
|
||||
{
|
||||
if (fallbackBehavior == FallbackBehavior.SystemSounds && Current is not SystemSoundsAudioSession)
|
||||
{
|
||||
Current = GetSystemSounds();
|
||||
}
|
||||
else if (fallbackBehavior == FallbackBehavior.SystemVolume && Current is not SystemVolumeAudioSession)
|
||||
{
|
||||
Current = GetSystemVolume();
|
||||
}
|
||||
}
|
||||
|
||||
_currentProcesses = processes;
|
||||
return Current;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the list of processes that might be currently selected
|
||||
/// This includes getting the child window's processes
|
||||
///
|
||||
/// This helps to find the audo process for windows store apps whose process is "ApplicationFrameHost.exe"
|
||||
///
|
||||
/// The list may optionally include a parent process, because that helps thing steam to be more reliable because the steamwebhelper (ui) is a child of steam.exe
|
||||
///
|
||||
/// According to deej, getting the ForegroundWindow and enumerating steam windows should work, but it doesn't seem to work for me without including the parent process
|
||||
/// https://github.com/omriharel/deej/blob/master/pkg/deej/util/util_windows.go#L22
|
||||
///
|
||||
/// but the parent process is sometimes useless (explorer, svchost, etc) so i filter some of them out because i felt like it when i wrote the code
|
||||
///
|
||||
/// I also experimented with grabbing the parent process and enumerating through the windows to see if that would help, but any time the parent process was an unexpected process (explorer) it could blow up. so i decided not to bother for now
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public List<Process> GetPossibleProcesses()
|
||||
{
|
||||
var handle = Native.GetForegroundWindow();
|
||||
|
||||
if (handle == IntPtr.Zero)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var ids = Native.GetProcessesOfChildWindows(handle);
|
||||
|
||||
Native.GetWindowThreadProcessId(handle, out var pid);
|
||||
ids.Insert(0, pid);
|
||||
|
||||
var processes = ids.Distinct()
|
||||
.Select(x => Process.GetProcessById(x))
|
||||
.ToList();
|
||||
|
||||
try
|
||||
{
|
||||
var blah = ParentProcessUtilities.GetParentProcess(pid);
|
||||
if (blah != null && blah.ProcessName != "explorer" && blah.ProcessName != "svchost")
|
||||
{
|
||||
processes.Add(blah);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return processes;
|
||||
|
||||
}
|
||||
|
||||
public void ResetAll()
|
||||
{
|
||||
var deviceEnumerator = (CoreAudio)new MMDeviceEnumerator();
|
||||
|
||||
deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, out var device);
|
||||
|
||||
Guid iid = typeof(IAudioSessionManager2).GUID;
|
||||
device.Activate(ref iid, 0, IntPtr.Zero, out var m);
|
||||
var manager = (IAudioSessionManager2)m;
|
||||
|
||||
|
||||
manager.GetSessionEnumerator(out var sessionEnumerator);
|
||||
|
||||
sessionEnumerator.GetCount(out var count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
sessionEnumerator.GetSession(i, out var session);
|
||||
|
||||
var volume = (ISimpleAudioVolume)session;
|
||||
var guid = Guid.Empty;
|
||||
volume.SetMasterVolume(1, ref guid);
|
||||
volume.SetMute(false, ref guid);
|
||||
}
|
||||
}
|
||||
|
||||
public IAudioSession GetSystemSounds()
|
||||
{
|
||||
var deviceEnumerator = (CoreAudio)new MMDeviceEnumerator();
|
||||
|
||||
deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, out var device);
|
||||
|
||||
Guid iid = typeof(IAudioSessionManager2).GUID;
|
||||
device.Activate(ref iid, 0, IntPtr.Zero, out var m);
|
||||
var manager = (IAudioSessionManager2)m;
|
||||
|
||||
|
||||
manager.GetSessionEnumerator(out var sessionEnumerator);
|
||||
|
||||
sessionEnumerator.GetCount(out var count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
sessionEnumerator.GetSession(i, out var session);
|
||||
|
||||
if (session.IsSystemSoundsSession() == 0)
|
||||
{
|
||||
return new SystemSoundsAudioSession(session);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public IAudioSession GetSystemVolume()
|
||||
{
|
||||
var deviceEnumerator = (CoreAudio)new MMDeviceEnumerator();
|
||||
|
||||
deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia, out var device);
|
||||
|
||||
Guid iid = typeof(IAudioEndpointVolume).GUID;
|
||||
device.Activate(ref iid, 0, IntPtr.Zero, out var o);
|
||||
var endpointVolume = (IAudioEndpointVolume)o;
|
||||
|
||||
return new SystemVolumeAudioSession(endpointVolume);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using BarRaider.SdTools;
|
||||
using System.Drawing;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace FocusVolumeControl.AudioSessions;
|
||||
|
||||
public sealed class ActiveAudioSessionWrapper : IAudioSession
|
||||
{
|
||||
public string DisplayName { get; set; }
|
||||
public string ExecutablePath { get; set; }
|
||||
private List<IAudioSessionControl2> Sessions { get; } = new List<IAudioSessionControl2>();
|
||||
private IEnumerable<ISimpleAudioVolume> Volume => Sessions.Cast<ISimpleAudioVolume>();
|
||||
|
||||
string _icon;
|
||||
|
||||
public string GetIcon()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_icon))
|
||||
{
|
||||
try
|
||||
{
|
||||
var tmp = Icon.ExtractAssociatedIcon(ExecutablePath);
|
||||
_icon = Tools.ImageToBase64(tmp.ToBitmap(), true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_icon = "Image/encoderIcon";
|
||||
}
|
||||
}
|
||||
return _icon;
|
||||
}
|
||||
|
||||
public bool Any()
|
||||
{
|
||||
return Volume.Any();
|
||||
}
|
||||
public int Count => Sessions.Count;
|
||||
|
||||
public void AddSession(IAudioSessionControl2 session)
|
||||
{
|
||||
Sessions.Add(session);
|
||||
}
|
||||
|
||||
public void ToggleMute()
|
||||
{
|
||||
//when all volumes are muted, Volume.All will return true
|
||||
//so we swap from muted to false (opposite of Volume.All)
|
||||
|
||||
//when any volumes are unmuted, Volume.All will return false
|
||||
//so we set muted to true (opposite of Volume.All)
|
||||
|
||||
var muted = IsMuted();
|
||||
|
||||
foreach(var v in Volume)
|
||||
{
|
||||
var guid = Guid.Empty;
|
||||
v.SetMute(!muted, ref guid);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsMuted()
|
||||
{
|
||||
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 volume = Volume.FirstOrDefault();
|
||||
var level = 0f;
|
||||
if (volume != null)
|
||||
{
|
||||
volume.GetMasterVolume(out level);
|
||||
}
|
||||
|
||||
level = VolumeHelpers.GetAdjustedVolume(level, step, ticks);
|
||||
|
||||
foreach(var v in Volume)
|
||||
{
|
||||
var guid = Guid.Empty;
|
||||
v.SetMasterVolume(level, ref guid);
|
||||
}
|
||||
}
|
||||
|
||||
public int GetVolumeLevel()
|
||||
{
|
||||
var volume = Volume.FirstOrDefault();
|
||||
var level = 0f;
|
||||
if(volume != null)
|
||||
{
|
||||
volume.GetMasterVolume(out level);
|
||||
}
|
||||
|
||||
return VolumeHelpers.GetVolumePercentage(level);
|
||||
}
|
||||
}
|
306
src/FocusVolumeControl/AudioSessions/CoreAudio.cs
Normal file
@ -0,0 +1,306 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace FocusVolumeControl.AudioSessions;
|
||||
|
||||
|
||||
[ComImport]
|
||||
[Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")]
|
||||
internal class MMDeviceEnumerator
|
||||
{
|
||||
}
|
||||
|
||||
internal enum EDataFlow
|
||||
{
|
||||
eRender,
|
||||
eCapture,
|
||||
eAll,
|
||||
EDataFlow_enum_count
|
||||
}
|
||||
|
||||
internal enum ERole
|
||||
{
|
||||
eConsole,
|
||||
eMultimedia,
|
||||
eCommunications,
|
||||
ERole_enum_count
|
||||
}
|
||||
|
||||
[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
internal interface CoreAudio
|
||||
{
|
||||
int NotImpl1();
|
||||
|
||||
[PreserveSig]
|
||||
int GetDefaultAudioEndpoint(EDataFlow dataFlow, ERole role, out IMMDevice ppDevice);
|
||||
}
|
||||
|
||||
[Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
internal interface IMMDevice
|
||||
{
|
||||
[PreserveSig]
|
||||
int Activate(ref Guid iid, int dwClsCtx, IntPtr pActivationParams, [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface);
|
||||
}
|
||||
|
||||
[Guid("77AA99A0-1BD6-484F-8BC7-2C654C9A9B6F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
internal interface IAudioSessionManager2
|
||||
{
|
||||
int NotImpl1();
|
||||
int NotImpl2();
|
||||
|
||||
[PreserveSig]
|
||||
int GetSessionEnumerator(out IAudioSessionEnumerator SessionEnum);
|
||||
}
|
||||
|
||||
[Guid("E2F5BB11-0570-40CA-ACDD-3AA01277DEE8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
internal 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)]
|
||||
internal interface ISimpleAudioVolume
|
||||
{
|
||||
[PreserveSig]
|
||||
int SetMasterVolume(float fLevel, ref Guid EventContext);
|
||||
|
||||
[PreserveSig]
|
||||
int GetMasterVolume(out float pfLevel);
|
||||
|
||||
[PreserveSig]
|
||||
int SetMute(bool bMute, ref Guid EventContext);
|
||||
|
||||
[PreserveSig]
|
||||
int GetMute(out bool pbMute);
|
||||
}
|
||||
|
||||
[Guid("bfb7ff88-7239-4fc9-8fa2-07c950be9c6d"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IAudioSessionControl2
|
||||
{
|
||||
// IAudioSessionControl
|
||||
[PreserveSig]
|
||||
int NotImpl0();
|
||||
|
||||
[PreserveSig]
|
||||
int GetDisplayName([MarshalAs(UnmanagedType.LPWStr)] out string pRetVal);
|
||||
|
||||
[PreserveSig]
|
||||
int SetDisplayName([MarshalAs(UnmanagedType.LPWStr)] string Value, [MarshalAs(UnmanagedType.LPStruct)] Guid EventContext);
|
||||
|
||||
[PreserveSig]
|
||||
int GetIconPath([MarshalAs(UnmanagedType.LPWStr)] out string pRetVal);
|
||||
|
||||
[PreserveSig]
|
||||
int SetIconPath([MarshalAs(UnmanagedType.LPWStr)] string Value, [MarshalAs(UnmanagedType.LPStruct)] Guid EventContext);
|
||||
|
||||
[PreserveSig]
|
||||
int GetGroupingParam(out Guid pRetVal);
|
||||
|
||||
[PreserveSig]
|
||||
int SetGroupingParam([MarshalAs(UnmanagedType.LPStruct)] Guid Override, [MarshalAs(UnmanagedType.LPStruct)] Guid EventContext);
|
||||
|
||||
[PreserveSig]
|
||||
int NotImpl1();
|
||||
|
||||
[PreserveSig]
|
||||
int NotImpl2();
|
||||
|
||||
// IAudioSessionControl2
|
||||
[PreserveSig]
|
||||
int GetSessionIdentifier([MarshalAs(UnmanagedType.LPWStr)] out string pRetVal);
|
||||
|
||||
[PreserveSig]
|
||||
int GetSessionInstanceIdentifier([MarshalAs(UnmanagedType.LPWStr)] out string pRetVal);
|
||||
|
||||
[PreserveSig]
|
||||
int GetProcessId(out int pRetVal);
|
||||
|
||||
[PreserveSig]
|
||||
int IsSystemSoundsSession();
|
||||
|
||||
[PreserveSig]
|
||||
int SetDuckingPreference(bool optOut);
|
||||
}
|
||||
|
||||
// http://netcoreaudio.codeplex.com/SourceControl/latest#trunk/Code/CoreAudio/Interfaces/IAudioEndpointVolume.cs
|
||||
[Guid("5CDF2C82-841E-4546-9722-0CF74078229A"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IAudioEndpointVolume
|
||||
{
|
||||
[PreserveSig]
|
||||
int NotImpl1();
|
||||
|
||||
[PreserveSig]
|
||||
int NotImpl2();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a count of the channels in the audio stream.
|
||||
/// </summary>
|
||||
/// <param name="channelCount">The number of channels.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int GetChannelCount(
|
||||
[Out][MarshalAs(UnmanagedType.U4)] out UInt32 channelCount);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the master volume level of the audio stream, in decibels.
|
||||
/// </summary>
|
||||
/// <param name="level">The new master volume level in decibels.</param>
|
||||
/// <param name="eventContext">A user context value that is passed to the notification callback.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int SetMasterVolumeLevel(
|
||||
[In][MarshalAs(UnmanagedType.R4)] float level,
|
||||
[In][MarshalAs(UnmanagedType.LPStruct)] Guid eventContext);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the master volume level, expressed as a normalized, audio-tapered value.
|
||||
/// </summary>
|
||||
/// <param name="level">The new master volume level expressed as a normalized value between 0.0 and 1.0.</param>
|
||||
/// <param name="eventContext">A user context value that is passed to the notification callback.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int SetMasterVolumeLevelScalar(
|
||||
[In][MarshalAs(UnmanagedType.R4)] float level,
|
||||
[In][MarshalAs(UnmanagedType.LPStruct)] Guid eventContext);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the master volume level of the audio stream, in decibels.
|
||||
/// </summary>
|
||||
/// <param name="level">The volume level in decibels.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int GetMasterVolumeLevel(
|
||||
[Out][MarshalAs(UnmanagedType.R4)] out float level);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the master volume level, expressed as a normalized, audio-tapered value.
|
||||
/// </summary>
|
||||
/// <param name="level">The volume level expressed as a normalized value between 0.0 and 1.0.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int GetMasterVolumeLevelScalar(
|
||||
[Out][MarshalAs(UnmanagedType.R4)] out float level);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the volume level, in decibels, of the specified channel of the audio stream.
|
||||
/// </summary>
|
||||
/// <param name="channelNumber">The channel number.</param>
|
||||
/// <param name="level">The new volume level in decibels.</param>
|
||||
/// <param name="eventContext">A user context value that is passed to the notification callback.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int SetChannelVolumeLevel(
|
||||
[In][MarshalAs(UnmanagedType.U4)] UInt32 channelNumber,
|
||||
[In][MarshalAs(UnmanagedType.R4)] float level,
|
||||
[In][MarshalAs(UnmanagedType.LPStruct)] Guid eventContext);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the normalized, audio-tapered volume level of the specified channel in the audio stream.
|
||||
/// </summary>
|
||||
/// <param name="channelNumber">The channel number.</param>
|
||||
/// <param name="level">The new master volume level expressed as a normalized value between 0.0 and 1.0.</param>
|
||||
/// <param name="eventContext">A user context value that is passed to the notification callback.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int SetChannelVolumeLevelScalar(
|
||||
[In][MarshalAs(UnmanagedType.U4)] UInt32 channelNumber,
|
||||
[In][MarshalAs(UnmanagedType.R4)] float level,
|
||||
[In][MarshalAs(UnmanagedType.LPStruct)] Guid eventContext);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the volume level, in decibels, of the specified channel in the audio stream.
|
||||
/// </summary>
|
||||
/// <param name="channelNumber">The zero-based channel number.</param>
|
||||
/// <param name="level">The volume level in decibels.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int GetChannelVolumeLevel(
|
||||
[In][MarshalAs(UnmanagedType.U4)] UInt32 channelNumber,
|
||||
[Out][MarshalAs(UnmanagedType.R4)] out float level);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the normalized, audio-tapered volume level of the specified channel of the audio stream.
|
||||
/// </summary>
|
||||
/// <param name="channelNumber">The zero-based channel number.</param>
|
||||
/// <param name="level">The volume level expressed as a normalized value between 0.0 and 1.0.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int GetChannelVolumeLevelScalar(
|
||||
[In][MarshalAs(UnmanagedType.U4)] UInt32 channelNumber,
|
||||
[Out][MarshalAs(UnmanagedType.R4)] out float level);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the muting state of the audio stream.
|
||||
/// </summary>
|
||||
/// <param name="isMuted">True to mute the stream, or false to unmute the stream.</param>
|
||||
/// <param name="eventContext">A user context value that is passed to the notification callback.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int SetMute(
|
||||
[In][MarshalAs(UnmanagedType.Bool)] Boolean isMuted,
|
||||
[In][MarshalAs(UnmanagedType.LPStruct)] Guid eventContext);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the muting state of the audio stream.
|
||||
/// </summary>
|
||||
/// <param name="isMuted">The muting state. True if the stream is muted, false otherwise.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int GetMute(
|
||||
[Out][MarshalAs(UnmanagedType.Bool)] out Boolean isMuted);
|
||||
|
||||
/// <summary>
|
||||
/// Gets information about the current step in the volume range.
|
||||
/// </summary>
|
||||
/// <param name="step">The current zero-based step index.</param>
|
||||
/// <param name="stepCount">The total number of steps in the volume range.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int GetVolumeStepInfo(
|
||||
[Out][MarshalAs(UnmanagedType.U4)] out UInt32 step,
|
||||
[Out][MarshalAs(UnmanagedType.U4)] out UInt32 stepCount);
|
||||
|
||||
/// <summary>
|
||||
/// Increases the volume level by one step.
|
||||
/// </summary>
|
||||
/// <param name="eventContext">A user context value that is passed to the notification callback.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int VolumeStepUp(
|
||||
[In][MarshalAs(UnmanagedType.LPStruct)] Guid eventContext);
|
||||
|
||||
/// <summary>
|
||||
/// Decreases the volume level by one step.
|
||||
/// </summary>
|
||||
/// <param name="eventContext">A user context value that is passed to the notification callback.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int VolumeStepDown(
|
||||
[In][MarshalAs(UnmanagedType.LPStruct)] Guid eventContext);
|
||||
|
||||
/// <summary>
|
||||
/// Queries the audio endpoint device for its hardware-supported functions.
|
||||
/// </summary>
|
||||
/// <param name="hardwareSupportMask">A hardware support mask that indicates the capabilities of the endpoint.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int QueryHardwareSupport(
|
||||
[Out][MarshalAs(UnmanagedType.U4)] out UInt32 hardwareSupportMask);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the volume range of the audio stream, in decibels.
|
||||
/// </summary>
|
||||
/// <param name="volumeMin">The minimum volume level in decibels.</param>
|
||||
/// <param name="volumeMax">The maximum volume level in decibels.</param>
|
||||
/// <param name="volumeStep">The volume increment level in decibels.</param>
|
||||
/// <returns>An HRESULT code indicating whether the operation passed of failed.</returns>
|
||||
[PreserveSig]
|
||||
int GetVolumeRange(
|
||||
[Out][MarshalAs(UnmanagedType.R4)] out float volumeMin,
|
||||
[Out][MarshalAs(UnmanagedType.R4)] out float volumeMax,
|
||||
[Out][MarshalAs(UnmanagedType.R4)] out float volumeStep);
|
||||
}
|
18
src/FocusVolumeControl/AudioSessions/IAudioSession.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System;
|
||||
|
||||
namespace FocusVolumeControl.AudioSessions;
|
||||
|
||||
public interface IAudioSession
|
||||
{
|
||||
public string DisplayName { get; }
|
||||
|
||||
public string GetIcon();
|
||||
|
||||
public void ToggleMute();
|
||||
|
||||
public bool IsMuted();
|
||||
|
||||
public void IncrementVolumeLevel(int step, int ticks);
|
||||
|
||||
public int GetVolumeLevel();
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace FocusVolumeControl.AudioSessions;
|
||||
|
||||
internal sealed class SystemSoundsAudioSession : IAudioSession
|
||||
{
|
||||
public SystemSoundsAudioSession(IAudioSessionControl2 sessionControl)
|
||||
{
|
||||
_sessionControl = sessionControl;
|
||||
_volumeControl = (ISimpleAudioVolume)sessionControl;
|
||||
}
|
||||
|
||||
IAudioSessionControl2 _sessionControl;
|
||||
ISimpleAudioVolume _volumeControl;
|
||||
|
||||
public string DisplayName => "System sounds";
|
||||
public string GetIcon() => "Images/systemSounds";
|
||||
|
||||
public void ToggleMute()
|
||||
{
|
||||
var guid = Guid.Empty;
|
||||
_volumeControl.SetMute(!IsMuted(), ref guid);
|
||||
}
|
||||
|
||||
public bool IsMuted()
|
||||
{
|
||||
_volumeControl.GetMute(out var mute);
|
||||
return mute;
|
||||
}
|
||||
|
||||
public void IncrementVolumeLevel(int step, int ticks)
|
||||
{
|
||||
_volumeControl.GetMasterVolume(out var level);
|
||||
level = VolumeHelpers.GetAdjustedVolume(level, step, ticks);
|
||||
|
||||
var guid = Guid.Empty;
|
||||
_volumeControl.SetMasterVolume(level, ref guid);
|
||||
}
|
||||
|
||||
public int GetVolumeLevel()
|
||||
{
|
||||
_volumeControl.GetMasterVolume(out var level);
|
||||
return VolumeHelpers.GetVolumePercentage(level);
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace FocusVolumeControl.AudioSessions;
|
||||
|
||||
internal sealed class SystemVolumeAudioSession : IAudioSession
|
||||
{
|
||||
public SystemVolumeAudioSession(IAudioEndpointVolume volumeControl)
|
||||
{
|
||||
_volumeControl = volumeControl;
|
||||
}
|
||||
|
||||
IAudioEndpointVolume _volumeControl;
|
||||
|
||||
public string DisplayName => "System Volume";
|
||||
public string GetIcon() => "Images/encoderIcon";
|
||||
|
||||
public void ToggleMute()
|
||||
{
|
||||
_volumeControl.SetMute(!IsMuted(), Guid.Empty);
|
||||
}
|
||||
|
||||
public bool IsMuted()
|
||||
{
|
||||
_volumeControl.GetMute(out var mute);
|
||||
return mute;
|
||||
}
|
||||
|
||||
public void IncrementVolumeLevel(int step, int ticks)
|
||||
{
|
||||
_volumeControl.GetMasterVolumeLevelScalar(out var level);
|
||||
level = VolumeHelpers.GetAdjustedVolume(level, step, ticks);
|
||||
_volumeControl.SetMasterVolumeLevelScalar(level, Guid.Empty);
|
||||
}
|
||||
|
||||
public int GetVolumeLevel()
|
||||
{
|
||||
_volumeControl.GetMasterVolumeLevelScalar(out var level);
|
||||
return VolumeHelpers.GetVolumePercentage(level);
|
||||
}
|
||||
}
|
25
src/FocusVolumeControl/AudioSessions/VolumeHelpers.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FocusVolumeControl.AudioSessions
|
||||
{
|
||||
internal class VolumeHelpers
|
||||
{
|
||||
public static float GetAdjustedVolume(float startingVolume, int step, int ticks)
|
||||
{
|
||||
var level = startingVolume;
|
||||
|
||||
level += 0.01f * step * ticks;
|
||||
level = Math.Max(level, 0);
|
||||
level = Math.Min(level, 1);
|
||||
|
||||
return level;
|
||||
}
|
||||
|
||||
public static int GetVolumePercentage(float volume) => (int)Math.Round(volume * 100);
|
||||
|
||||
}
|
||||
}
|
272
src/FocusVolumeControl/DialAction.cs
Normal file
@ -0,0 +1,272 @@
|
||||
using BarRaider.SdTools;
|
||||
using BarRaider.SdTools.Payloads;
|
||||
using FocusVolumeControl.AudioSessions;
|
||||
using FocusVolumeControl.UI;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace FocusVolumeControl;
|
||||
|
||||
[PluginActionId("com.dlprows.focusvolumecontrol.dialaction")]
|
||||
public class DialAction : EncoderBase
|
||||
{
|
||||
private class PluginSettings
|
||||
{
|
||||
[JsonProperty("fallbackBehavior")]
|
||||
public FallbackBehavior FallbackBehavior { get; set; }
|
||||
|
||||
[JsonProperty("stepSize")]
|
||||
public int StepSize { get; set; }
|
||||
|
||||
public static PluginSettings CreateDefaultSettings()
|
||||
{
|
||||
PluginSettings instance = new PluginSettings();
|
||||
instance.FallbackBehavior = FallbackBehavior.SystemSounds;
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
private PluginSettings settings;
|
||||
|
||||
IntPtr _foregroundWindowChangedEvent;
|
||||
Native.WinEventDelegate _delegate;
|
||||
|
||||
AudioHelper _audioHelper = new AudioHelper();
|
||||
|
||||
Thread _thread;
|
||||
Dispatcher _dispatcher;
|
||||
|
||||
UIState _previousState;
|
||||
|
||||
public DialAction(ISDConnection connection, InitialPayload payload) : base(connection, payload)
|
||||
{
|
||||
if (payload.Settings == null || payload.Settings.Count == 0)
|
||||
{
|
||||
settings = PluginSettings.CreateDefaultSettings();
|
||||
_ = SaveSettings();
|
||||
}
|
||||
else
|
||||
{
|
||||
settings = payload.Settings.ToObject<PluginSettings>();
|
||||
}
|
||||
|
||||
_thread = new Thread(() =>
|
||||
{
|
||||
Logger.Instance.LogMessage(TracingLevel.DEBUG, "Registering for events");
|
||||
_delegate = new Native.WinEventDelegate(WinEventProc);
|
||||
_foregroundWindowChangedEvent = Native.RegisterForForegroundWindowChangedEvent(_delegate);
|
||||
|
||||
Logger.Instance.LogMessage(TracingLevel.DEBUG, "Starting Dispatcher");
|
||||
_dispatcher = Dispatcher.CurrentDispatcher;
|
||||
Dispatcher.Run();
|
||||
Logger.Instance.LogMessage(TracingLevel.DEBUG, "Dispatcher Stopped");
|
||||
});
|
||||
_thread.SetApartmentState(ApartmentState.STA);
|
||||
_thread.Start();
|
||||
|
||||
var session = _audioHelper.GetActiveSession(settings.FallbackBehavior);
|
||||
_ = UpdateStateIfNeeded(session);
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
Logger.Instance.LogMessage(TracingLevel.DEBUG, "Disposing");
|
||||
if (_foregroundWindowChangedEvent != IntPtr.Zero)
|
||||
{
|
||||
Native.UnhookWinEvent(_foregroundWindowChangedEvent);
|
||||
}
|
||||
_dispatcher.InvokeShutdown();
|
||||
}
|
||||
|
||||
public override async void DialDown(DialPayload payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Down");
|
||||
await ToggleMuteAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unexpected Error in DialDown:\n {ex}");
|
||||
}
|
||||
}
|
||||
public override void DialUp(DialPayload payload) { }
|
||||
|
||||
public override async void TouchPress(TouchpadPressPayload payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.Instance.LogMessage(TracingLevel.INFO, "Touch Press");
|
||||
if (payload.IsLongPress)
|
||||
{
|
||||
await ResetAllAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
await ToggleMuteAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unexpected Error in TouchPress:\n {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
public override async void DialRotate(DialRotatePayload payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Rotate");
|
||||
//dial rotated. ticks positive for right, negative for left
|
||||
var activeSession = _audioHelper.Current;
|
||||
if (activeSession != null)
|
||||
{
|
||||
activeSession.IncrementVolumeLevel(settings.StepSize, payload.Ticks);
|
||||
await UpdateStateIfNeeded(activeSession);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Connection.ShowAlert();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_audioHelper.ResetCache();
|
||||
await Connection.ShowAlert();
|
||||
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unable to increment volume:\n {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
async Task ResetAllAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_audioHelper.ResetAll();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_audioHelper.ResetCache();
|
||||
await Connection.ShowAlert();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
async Task ToggleMuteAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var activeSession = _audioHelper.Current;
|
||||
if (activeSession != null)
|
||||
{
|
||||
activeSession.ToggleMute();
|
||||
await UpdateStateIfNeeded(activeSession);
|
||||
}
|
||||
else
|
||||
{
|
||||
await Connection.ShowAlert();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
_audioHelper.ResetCache();
|
||||
await Connection.ShowAlert();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public override async void OnTick()
|
||||
{
|
||||
try
|
||||
{
|
||||
//called once every 1000ms and can be used for updating the title/image of the key
|
||||
var activeSession = _audioHelper.GetActiveSession(settings.FallbackBehavior);
|
||||
|
||||
await UpdateStateIfNeeded(activeSession);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_audioHelper.ResetCache();
|
||||
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Exception on Tick:\n {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateStateIfNeeded(IAudioSession audioSession)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (audioSession != null)
|
||||
{
|
||||
|
||||
var uiState = new UIState(audioSession);
|
||||
|
||||
if (_previousState != null && uiState != null &&
|
||||
uiState.Title == _previousState.Title &&
|
||||
uiState.Value.Value == _previousState.Value.Value &&
|
||||
uiState.Value.Opacity == _previousState.Value.Opacity &&
|
||||
uiState.Indicator.Value == _previousState.Indicator.Value &&
|
||||
uiState.Indicator.Opacity == _previousState.Indicator.Opacity &&
|
||||
uiState.icon.Value == _previousState.icon.Value &&
|
||||
uiState.icon.Opacity == _previousState.icon.Opacity
|
||||
)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await Connection.SetFeedbackAsync(uiState);
|
||||
_previousState = uiState;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Failed to update screen\n {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public override void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public override void ReceivedSettings(ReceivedSettingsPayload payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
Tools.AutoPopulateSettings(settings, payload.Settings);
|
||||
_ = SaveSettings();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unexpected Error in SaveSettings:\n {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
await Connection.SetSettingsAsync(JObject.FromObject(settings));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unexpected Error in SaveSettings:\n {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
|
||||
{
|
||||
try
|
||||
{
|
||||
OnTick();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unexpected Error in DialDown:\n {ex}");
|
||||
}
|
||||
}
|
||||
}
|
BIN
src/FocusVolumeControl/DistributionTool.exe
Normal file
8
src/FocusVolumeControl/FallbackBehavior.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace FocusVolumeControl;
|
||||
|
||||
public enum FallbackBehavior
|
||||
{
|
||||
SystemSounds,
|
||||
PreviousApp,
|
||||
SystemVolume
|
||||
}
|
120
src/FocusVolumeControl/FocusVolumeControl.csproj
Normal file
@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{4635D874-69C0-4010-BE46-77EF92EB1553}</ProjectGuid>
|
||||
<OutputType>Exe</OutputType>
|
||||
<RootNamespace>FocusVolumeControl</RootNamespace>
|
||||
<AssemblyName>FocusVolumeControl</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<Deterministic>true</Deterministic>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<TargetFrameworkProfile />
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\com.dlprows.focusvolumecontrol.sdPlugin\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\com.dlprows.focusvolumecontrol.sdPlugin\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Configuration" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.IO.Compression" />
|
||||
<Reference Include="System.Runtime.Serialization" />
|
||||
<Reference Include="System.ServiceModel" />
|
||||
<Reference Include="System.Transactions" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="WindowsBase" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="AudioSessions\ActiveAudioSessionWrapper.cs" />
|
||||
<Compile Include="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="UI\ISDConnectionExtensions.cs" />
|
||||
<Compile Include="Native.cs" />
|
||||
<Compile Include="ParentProcessUtilities.cs" />
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="UI\UIState.cs" />
|
||||
<Compile Include="UI\ValueWithOpacity.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="App.config" />
|
||||
<None Include="install.bat" />
|
||||
<None Include="manifest.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="$(SolutionDir)..\previews\**\*.png" Link="previews\%(Filename)%(Extension)">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="Images\**\*.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="PropertyInspector\**\*.js;PropertyInspector\**\*.css">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="PropertyInspector\PluginActionPI.html">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="IsExternalInit">
|
||||
<Version>1.0.3</Version>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Newtonsoft.Json">
|
||||
<Version>13.0.3</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NLog">
|
||||
<Version>5.2.3</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="RequiredMemberAttribute">
|
||||
<Version>1.0.0</Version>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="streamdeck-client-csharp">
|
||||
<Version>4.3.0</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="StreamDeck-Tools">
|
||||
<Version>6.1.1</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
BIN
src/FocusVolumeControl/Images/actionIcon.png
Normal file
After Width: | Height: | Size: 897 B |
BIN
src/FocusVolumeControl/Images/actionIcon@2x.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
src/FocusVolumeControl/Images/categoryIcon.png
Normal file
After Width: | Height: | Size: 7.8 KiB |
BIN
src/FocusVolumeControl/Images/categoryIcon@2x.png
Normal file
After Width: | Height: | Size: 9.1 KiB |
BIN
src/FocusVolumeControl/Images/encoderIcon.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
src/FocusVolumeControl/Images/encoderIcon@2x.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
src/FocusVolumeControl/Images/pluginIcon.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/FocusVolumeControl/Images/pluginIcon@2x.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/FocusVolumeControl/Images/stateIcon.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
src/FocusVolumeControl/Images/stateIcon@2x.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
src/FocusVolumeControl/Images/systemSounds.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
src/FocusVolumeControl/Images/systemSounds@2x.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
62
src/FocusVolumeControl/Native.cs
Normal file
@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace FocusVolumeControl;
|
||||
|
||||
public class Native
|
||||
{
|
||||
public delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern bool UnhookWinEvent(IntPtr hWinEventHook);
|
||||
|
||||
|
||||
private const uint WINEVENT_OUTOFCONTEXT = 0;
|
||||
private const uint EVENT_SYSTEM_FOREGROUND = 3;
|
||||
|
||||
public static IntPtr RegisterForForegroundWindowChangedEvent(WinEventDelegate dele) => SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, IntPtr.Zero, dele, 0, 0, WINEVENT_OUTOFCONTEXT);
|
||||
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
public static extern IntPtr GetForegroundWindow();
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out int processId);
|
||||
|
||||
|
||||
private delegate bool EnumWindowProc(IntPtr hwnd, IntPtr lParam);
|
||||
|
||||
[DllImport("user32")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool EnumChildWindows(IntPtr window, EnumWindowProc callback, IntPtr lParam);
|
||||
|
||||
|
||||
public static List<int> GetProcessesOfChildWindows(IntPtr windowHandle)
|
||||
{
|
||||
var ids = new List<int>();
|
||||
|
||||
if(windowHandle != IntPtr.Zero)
|
||||
{
|
||||
|
||||
EnumChildWindows(windowHandle,
|
||||
(hWnd, lParam) =>
|
||||
{
|
||||
Native.GetWindowThreadProcessId(hWnd, out var pid);
|
||||
ids.Add(pid);
|
||||
|
||||
return true;
|
||||
|
||||
}, IntPtr.Zero);
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
[DllImport("ntdll.dll")]
|
||||
public static extern int NtQueryInformationProcess(IntPtr processHandle, int processInformationClass, ref ParentProcessUtilities processInformation, int processInformationLength, out int returnLength);
|
||||
|
||||
}
|
57
src/FocusVolumeControl/ParentProcessUtilities.cs
Normal file
@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace FocusVolumeControl;
|
||||
|
||||
/// <summary>
|
||||
/// A utility class to determine a process parent.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct ParentProcessUtilities
|
||||
{
|
||||
// These members must match PROCESS_BASIC_INFORMATION
|
||||
internal IntPtr Reserved1;
|
||||
internal IntPtr PebBaseAddress;
|
||||
internal IntPtr Reserved2_0;
|
||||
internal IntPtr Reserved2_1;
|
||||
internal IntPtr UniqueProcessId;
|
||||
internal IntPtr InheritedFromUniqueProcessId;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parent process of specified process.
|
||||
/// </summary>
|
||||
/// <param name="id">The process id.</param>
|
||||
/// <returns>An instance of the Process class.</returns>
|
||||
public static Process GetParentProcess(int id)
|
||||
{
|
||||
var process = Process.GetProcessById(id);
|
||||
return GetParentProcess(process);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parent process of a specified process.
|
||||
/// </summary>
|
||||
/// <param name="handle">The process handle.</param>
|
||||
/// <returns>An instance of the Process class.</returns>
|
||||
public static Process GetParentProcess(Process process)
|
||||
{
|
||||
var data = new ParentProcessUtilities();
|
||||
int status = Native.NtQueryInformationProcess(process.Handle, 0, ref data, Marshal.SizeOf(data), out var returnLength);
|
||||
if (status != 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Process.GetProcessById(data.InheritedFromUniqueProcessId.ToInt32());
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
14
src/FocusVolumeControl/Program.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using BarRaider.SdTools;
|
||||
|
||||
namespace FocusVolumeControl;
|
||||
|
||||
internal class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
// Uncomment this line of code to allow for debugging
|
||||
//while (!System.Diagnostics.Debugger.IsAttached) { System.Threading.Thread.Sleep(100); }
|
||||
|
||||
SDWrapper.Run(args);
|
||||
}
|
||||
}
|
36
src/FocusVolumeControl/Properties/AssemblyInfo.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("FocusVolumeControl")]
|
||||
[assembly: AssemblyDescription("")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("")]
|
||||
[assembly: AssemblyProduct("FocusVolumeControl")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2020")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("1478b282-5356-4535-b478-b68cfa9cca59")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
46
src/FocusVolumeControl/PropertyInspector/PluginActionPI.html
Normal file
@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no,minimal-ui,viewport-fit=cover">
|
||||
<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">
|
||||
<script src="lib/sdtools.common.js"></script>
|
||||
<script src="lib/rangeTooltip.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="sdpi-wrapper">
|
||||
|
||||
<div class="sdpi-item">
|
||||
<div class="sdpi-item-label">Fallback</div>
|
||||
<select class="sdpi-item-value sdProperty" id="fallbackBehavior" oninput="setSettings()">
|
||||
<option value="0">System Sounds</option>
|
||||
<option value="1">Previous App</option>
|
||||
<option value="2">Main System Volume</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
<div type="range" class="sdpi-item sdShowTooltip">
|
||||
<div class="sdpi-item-label">Step Size</div>
|
||||
<div class="sdpi-item-value">
|
||||
<span class="clickable" value="1">1</span>
|
||||
<input type="range" min="1" max="10" value="1" class="sdProperty" data-suffix=" %" id="stepSize" oninput="setSettings()" />
|
||||
<span class="clickable" value="1">10</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sdpi-info-label hidden" style="top: -1000;" value="">Tooltip</div>
|
||||
|
||||
<details>
|
||||
<p>If you look at windows volume mixer, you will see that not all applications can have their volume controlled. The fallback behavior controls what happens when you are in an application that doesn't show up in the volume mixer</p>
|
||||
<p>* System Sounds - Switch to system sounds. This will control windows sound effects such as when an error sound plays. If you're in an application that is making beeping sounds, this will often allow you to control those sounds while leaving things like your music/videos alone</p>
|
||||
<p>* Previous App - Use the last app that had a volume control. This can result in the stream deck not changing after you have quit an application.</p>
|
||||
<p>* Main System Volume - Switch to the main volume control for the system. This will change the volume of all applications</p>
|
||||
</details>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,41 @@
|
||||
.sdpi-info-label {
|
||||
display: inline-block;
|
||||
user-select: none;
|
||||
position: absolute;
|
||||
height: 15px;
|
||||
width: auto;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
min-width: 44px;
|
||||
max-width: 80px;
|
||||
background: white;
|
||||
font-size: 11px;
|
||||
color: black;
|
||||
z-index: 1000;
|
||||
box-shadow: 0px 0px 12px rgba(0,0,0,.8);
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.sdpi-info-label.hidden {
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s linear;
|
||||
}
|
||||
|
||||
.sdpi-info-label.shown {
|
||||
position: absolute;
|
||||
opacity: 1;
|
||||
transition: opacity 0.25s ease-out;
|
||||
}
|
||||
|
||||
.rangeLabel {
|
||||
position: relative;
|
||||
font-weight: normal;
|
||||
margin-top: 22px;
|
||||
left: -200px;
|
||||
min-width: 200px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.percent::after {
|
||||
content: "%";
|
||||
}
|
122
src/FocusVolumeControl/PropertyInspector/lib/rangeTooltip.js
Normal file
@ -0,0 +1,122 @@
|
||||
// ****************************************************************
|
||||
// * EasyPI v1.3
|
||||
// * Author: BarRaider
|
||||
// *
|
||||
// * rangeTooltip.js adds a tooltip showing the value of a range slider.
|
||||
// * Requires rangeTooltip.css to be referenced in the HTML file.
|
||||
// *
|
||||
// * Project page: https://github.com/BarRaider/streamdeck-easypi
|
||||
// * Support: http://discord.barraider.com
|
||||
// ****************************************************************
|
||||
|
||||
var tooltip = document.querySelector('.sdpi-info-label');
|
||||
var tw;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
// Handler when the DOM is fully loaded
|
||||
setRangeTooltips();
|
||||
});
|
||||
|
||||
function calcRangeLabel(elem) {
|
||||
const value = elem.value;
|
||||
const percent = (elem.value - elem.min) / (elem.max - elem.min);
|
||||
let tooltipValue = value;
|
||||
let outputType = elem.dataset.suffix;
|
||||
if (outputType && outputType == '%') {
|
||||
tooltipValue = Math.round(100 * percent);
|
||||
}
|
||||
|
||||
return tooltipValue + outputType;
|
||||
}
|
||||
|
||||
function setElementLabel(elem, str) {
|
||||
// Try to set this for the rangeLabel class, if it exists
|
||||
let label = elem.querySelector('.rangeLabel');
|
||||
if (label) {
|
||||
label.innerHTML = str;
|
||||
}
|
||||
else {
|
||||
console.log('setElementLabel ERROR! No .rangeLabel found', elem);
|
||||
}
|
||||
}
|
||||
|
||||
function setRangeTooltips() {
|
||||
console.log("Loading setRangeTooltips");
|
||||
|
||||
if (!tooltip) {
|
||||
tooltip = document.querySelector('.sdpi-info-label');
|
||||
}
|
||||
|
||||
if (!tw) {
|
||||
tw = tooltip.getBoundingClientRect().width;
|
||||
}
|
||||
|
||||
const rangeToolTips = document.querySelectorAll('div[type=range].sdShowTooltip');
|
||||
rangeToolTips.forEach(elem => {
|
||||
let rangeSelector = elem.querySelector('input[type=range]');
|
||||
let fn = () => {
|
||||
const rangeRect = rangeSelector.getBoundingClientRect();
|
||||
const w = rangeRect.width - tw / 2;
|
||||
const labelStr = calcRangeLabel(rangeSelector);
|
||||
// Set the tooltip
|
||||
if (tooltip.classList.contains('hidden')) {
|
||||
tooltip.style.top = '-1000px';
|
||||
} else {
|
||||
const percent = (rangeSelector.value - rangeSelector.min) / (rangeSelector.max - rangeSelector.min);
|
||||
tooltip.style.left = (rangeRect.left + Math.round(w * percent) - tw / 4) + 'px';
|
||||
tooltip.textContent = labelStr;
|
||||
tooltip.style.top = (rangeRect.top - 32) + 'px';
|
||||
}
|
||||
|
||||
setElementLabel(elem, labelStr)
|
||||
};
|
||||
|
||||
rangeSelector.addEventListener(
|
||||
'mouseenter',
|
||||
function () {
|
||||
tooltip.classList.remove('hidden');
|
||||
tooltip.classList.add('shown');
|
||||
fn();
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
rangeSelector.addEventListener(
|
||||
'mouseout',
|
||||
function () {
|
||||
tooltip.classList.remove('shown');
|
||||
tooltip.classList.add('hidden');
|
||||
fn();
|
||||
},
|
||||
false
|
||||
);
|
||||
rangeSelector.addEventListener('input', fn, false);
|
||||
|
||||
rangeSelector.addEventListener("change", fn, false);
|
||||
|
||||
document.addEventListener(
|
||||
'settingsUpdated',
|
||||
function () {
|
||||
console.log('rangeTooltip settingsUpdated called');
|
||||
window.setTimeout(function () {
|
||||
let str = calcRangeLabel(rangeSelector);
|
||||
setElementLabel(elem, str);
|
||||
}, 500);
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
document.addEventListener(
|
||||
'websocketCreate',
|
||||
function () {
|
||||
console.log('rangeTooltip websocketCreate called');
|
||||
window.setTimeout(function () {
|
||||
let str = calcRangeLabel(rangeSelector);
|
||||
setElementLabel(elem, str);
|
||||
}, 500);
|
||||
},
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
}
|
1650
src/FocusVolumeControl/PropertyInspector/lib/sdpi.css
Normal file
321
src/FocusVolumeControl/PropertyInspector/lib/sdtools.common.js
Normal file
@ -0,0 +1,321 @@
|
||||
// ****************************************************************
|
||||
// * EasyPI v1.4
|
||||
// * Author: BarRaider
|
||||
// *
|
||||
// * JS library to simplify the communication between the
|
||||
// * Stream Deck's Property Inspector and the plugin.
|
||||
// *
|
||||
// * Project page: https://github.com/BarRaider/streamdeck-easypi
|
||||
// * Support: http://discord.barraider.com
|
||||
// *
|
||||
// * Initially forked from Elgato's common.js file
|
||||
// ****************************************************************
|
||||
|
||||
var websocket = null,
|
||||
uuid = null,
|
||||
registerEventName = null,
|
||||
actionInfo = {},
|
||||
inInfo = {},
|
||||
runningApps = [],
|
||||
isQT = navigator.appVersion.includes('QtWebEngine');
|
||||
|
||||
function connectElgatoStreamDeckSocket(inPort, inUUID, inRegisterEvent, inInfo, inActionInfo) {
|
||||
uuid = inUUID;
|
||||
registerEventName = inRegisterEvent;
|
||||
console.log(inUUID, inActionInfo);
|
||||
actionInfo = JSON.parse(inActionInfo); // cache the info
|
||||
inInfo = JSON.parse(inInfo);
|
||||
websocket = new WebSocket('ws://127.0.0.1:' + inPort);
|
||||
|
||||
addDynamicStyles(inInfo.colors);
|
||||
|
||||
websocket.onopen = websocketOnOpen;
|
||||
websocket.onmessage = websocketOnMessage;
|
||||
|
||||
// Allow others to get notified that the websocket is created
|
||||
var event = new Event('websocketCreate');
|
||||
document.dispatchEvent(event);
|
||||
|
||||
loadConfiguration(actionInfo.payload.settings);
|
||||
initPropertyInspector();
|
||||
}
|
||||
|
||||
function websocketOnOpen() {
|
||||
var json = {
|
||||
event: registerEventName,
|
||||
uuid: uuid
|
||||
};
|
||||
websocket.send(JSON.stringify(json));
|
||||
|
||||
// Notify the plugin that we are connected
|
||||
sendValueToPlugin('propertyInspectorConnected', 'property_inspector');
|
||||
}
|
||||
|
||||
function websocketOnMessage(evt) {
|
||||
// Received message from Stream Deck
|
||||
var jsonObj = JSON.parse(evt.data);
|
||||
|
||||
if (jsonObj.event === 'didReceiveSettings') {
|
||||
var payload = jsonObj.payload;
|
||||
loadConfiguration(payload.settings);
|
||||
}
|
||||
else {
|
||||
console.log("Ignored websocketOnMessage: " + jsonObj.event);
|
||||
}
|
||||
}
|
||||
|
||||
function loadConfiguration(payload) {
|
||||
console.log('loadConfiguration');
|
||||
console.log(payload);
|
||||
for (var key in payload) {
|
||||
try {
|
||||
var elem = document.getElementById(key);
|
||||
if (elem.classList.contains("sdCheckbox")) { // Checkbox
|
||||
elem.checked = payload[key];
|
||||
}
|
||||
else if (elem.classList.contains("sdFile")) { // File
|
||||
var elemFile = document.getElementById(elem.id + "Filename");
|
||||
elemFile.innerText = payload[key];
|
||||
if (!elemFile.innerText) {
|
||||
elemFile.innerText = "No file...";
|
||||
}
|
||||
}
|
||||
else if (elem.classList.contains("sdList")) { // Dynamic dropdown
|
||||
var textProperty = elem.getAttribute("sdListTextProperty");
|
||||
var valueProperty = elem.getAttribute("sdListValueProperty");
|
||||
var valueField = elem.getAttribute("sdValueField");
|
||||
|
||||
var items = payload[key];
|
||||
elem.options.length = 0;
|
||||
|
||||
for (var idx = 0; idx < items.length; idx++) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = items[idx][valueProperty];
|
||||
opt.text = items[idx][textProperty];
|
||||
elem.appendChild(opt);
|
||||
}
|
||||
elem.value = payload[valueField];
|
||||
}
|
||||
else if (elem.classList.contains("sdHTML")) { // HTML element
|
||||
elem.innerHTML = payload[key];
|
||||
}
|
||||
else { // Normal value
|
||||
elem.value = payload[key];
|
||||
}
|
||||
console.log("Load: " + key + "=" + payload[key]);
|
||||
}
|
||||
catch (err) {
|
||||
console.log("loadConfiguration failed for key: " + key + " - " + err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setSettings() {
|
||||
var payload = {};
|
||||
var elements = document.getElementsByClassName("sdProperty");
|
||||
|
||||
Array.prototype.forEach.call(elements, function (elem) {
|
||||
var key = elem.id;
|
||||
if (elem.classList.contains("sdCheckbox")) { // Checkbox
|
||||
payload[key] = elem.checked;
|
||||
}
|
||||
else if (elem.classList.contains("sdFile")) { // File
|
||||
var elemFile = document.getElementById(elem.id + "Filename");
|
||||
payload[key] = elem.value;
|
||||
if (!elem.value) {
|
||||
// Fetch innerText if file is empty (happens when we lose and regain focus to this key)
|
||||
payload[key] = elemFile.innerText;
|
||||
}
|
||||
else {
|
||||
// Set value on initial file selection
|
||||
elemFile.innerText = elem.value;
|
||||
}
|
||||
}
|
||||
else if (elem.classList.contains("sdList")) { // Dynamic dropdown
|
||||
var valueField = elem.getAttribute("sdValueField");
|
||||
payload[valueField] = elem.value;
|
||||
}
|
||||
else if (elem.classList.contains("sdHTML")) { // HTML element
|
||||
var valueField = elem.getAttribute("sdValueField");
|
||||
payload[valueField] = elem.innerHTML;
|
||||
}
|
||||
else { // Normal value
|
||||
payload[key] = elem.value;
|
||||
}
|
||||
console.log("Save: " + key + "<=" + payload[key]);
|
||||
});
|
||||
setSettingsToPlugin(payload);
|
||||
}
|
||||
|
||||
function setSettingsToPlugin(payload) {
|
||||
if (websocket && (websocket.readyState === 1)) {
|
||||
const json = {
|
||||
'event': 'setSettings',
|
||||
'context': uuid,
|
||||
'payload': payload
|
||||
};
|
||||
websocket.send(JSON.stringify(json));
|
||||
var event = new Event('settingsUpdated');
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
// Sends an entire payload to the sendToPlugin method
|
||||
function sendPayloadToPlugin(payload) {
|
||||
if (websocket && (websocket.readyState === 1)) {
|
||||
const json = {
|
||||
'action': actionInfo['action'],
|
||||
'event': 'sendToPlugin',
|
||||
'context': uuid,
|
||||
'payload': payload
|
||||
};
|
||||
websocket.send(JSON.stringify(json));
|
||||
}
|
||||
}
|
||||
|
||||
// Sends one value to the sendToPlugin method
|
||||
function sendValueToPlugin(value, param) {
|
||||
if (websocket && (websocket.readyState === 1)) {
|
||||
const json = {
|
||||
'action': actionInfo['action'],
|
||||
'event': 'sendToPlugin',
|
||||
'context': uuid,
|
||||
'payload': {
|
||||
[param]: value
|
||||
}
|
||||
};
|
||||
websocket.send(JSON.stringify(json));
|
||||
}
|
||||
}
|
||||
|
||||
function openWebsite() {
|
||||
if (websocket && (websocket.readyState === 1)) {
|
||||
const json = {
|
||||
'event': 'openUrl',
|
||||
'payload': {
|
||||
'url': 'https://BarRaider.com'
|
||||
}
|
||||
};
|
||||
websocket.send(JSON.stringify(json));
|
||||
}
|
||||
}
|
||||
|
||||
if (!isQT) {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
initPropertyInspector();
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('beforeunload', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Notify the plugin we are about to leave
|
||||
sendValueToPlugin('propertyInspectorWillDisappear', 'property_inspector');
|
||||
|
||||
// Don't set a returnValue to the event, otherwise Chromium with throw an error.
|
||||
});
|
||||
|
||||
function prepareDOMElements(baseElement) {
|
||||
baseElement = baseElement || document;
|
||||
|
||||
/**
|
||||
* You could add a 'label' to a textares, e.g. to show the number of charactes already typed
|
||||
* or contained in the textarea. This helper updates this label for you.
|
||||
*/
|
||||
baseElement.querySelectorAll('textarea').forEach((e) => {
|
||||
const maxl = e.getAttribute('maxlength');
|
||||
e.targets = baseElement.querySelectorAll(`[for='${e.id}']`);
|
||||
if (e.targets.length) {
|
||||
let fn = () => {
|
||||
for (let x of e.targets) {
|
||||
x.textContent = maxl ? `${e.value.length}/${maxl}` : `${e.value.length}`;
|
||||
}
|
||||
};
|
||||
fn();
|
||||
e.onkeyup = fn;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initPropertyInspector() {
|
||||
// Place to add functions
|
||||
prepareDOMElements(document);
|
||||
}
|
||||
|
||||
|
||||
function addDynamicStyles(clrs) {
|
||||
const node = document.getElementById('#sdpi-dynamic-styles') || document.createElement('style');
|
||||
if (!clrs.mouseDownColor) clrs.mouseDownColor = fadeColor(clrs.highlightColor, -100);
|
||||
const clr = clrs.highlightColor.slice(0, 7);
|
||||
const clr1 = fadeColor(clr, 100);
|
||||
const clr2 = fadeColor(clr, 60);
|
||||
const metersActiveColor = fadeColor(clr, -60);
|
||||
|
||||
node.setAttribute('id', 'sdpi-dynamic-styles');
|
||||
node.innerHTML = `
|
||||
|
||||
input[type="radio"]:checked + label span,
|
||||
input[type="checkbox"]:checked + label span {
|
||||
background-color: ${clrs.highlightColor};
|
||||
}
|
||||
|
||||
input[type="radio"]:active:checked + label span,
|
||||
input[type="radio"]:active + label span,
|
||||
input[type="checkbox"]:active:checked + label span,
|
||||
input[type="checkbox"]:active + label span {
|
||||
background-color: ${clrs.mouseDownColor};
|
||||
}
|
||||
|
||||
input[type="radio"]:active + label span,
|
||||
input[type="checkbox"]:active + label span {
|
||||
background-color: ${clrs.buttonPressedBorderColor};
|
||||
}
|
||||
|
||||
td.selected,
|
||||
td.selected:hover,
|
||||
li.selected:hover,
|
||||
li.selected {
|
||||
color: white;
|
||||
background-color: ${clrs.highlightColor};
|
||||
}
|
||||
|
||||
.sdpi-file-label > label:active,
|
||||
.sdpi-file-label.file:active,
|
||||
label.sdpi-file-label:active,
|
||||
label.sdpi-file-info:active,
|
||||
input[type="file"]::-webkit-file-upload-button:active,
|
||||
button:active {
|
||||
background-color: ${clrs.buttonPressedBackgroundColor};
|
||||
color: ${clrs.buttonPressedTextColor};
|
||||
border-color: ${clrs.buttonPressedBorderColor};
|
||||
}
|
||||
|
||||
::-webkit-progress-value,
|
||||
meter::-webkit-meter-optimum-value {
|
||||
background: linear-gradient(${clr2}, ${clr1} 20%, ${clr} 45%, ${clr} 55%, ${clr2})
|
||||
}
|
||||
|
||||
::-webkit-progress-value:active,
|
||||
meter::-webkit-meter-optimum-value:active {
|
||||
background: linear-gradient(${clr}, ${clr2} 20%, ${metersActiveColor} 45%, ${metersActiveColor} 55%, ${clr})
|
||||
}
|
||||
`;
|
||||
document.body.appendChild(node);
|
||||
};
|
||||
|
||||
/** UTILITIES */
|
||||
|
||||
/*
|
||||
Quick utility to lighten or darken a color (doesn't take color-drifting, etc. into account)
|
||||
Usage:
|
||||
fadeColor('#061261', 100); // will lighten the color
|
||||
fadeColor('#200867'), -100); // will darken the color
|
||||
*/
|
||||
function fadeColor(col, amt) {
|
||||
const min = Math.min, max = Math.max;
|
||||
const num = parseInt(col.replace(/#/g, ''), 16);
|
||||
const r = min(255, max((num >> 16) + amt, 0));
|
||||
const g = min(255, max((num & 0x0000FF) + amt, 0));
|
||||
const b = min(255, max(((num >> 8) & 0x00FF) + amt, 0));
|
||||
return '#' + (g | (b << 8) | (r << 16)).toString(16).padStart(6, 0);
|
||||
}
|
13
src/FocusVolumeControl/UI/ISDConnectionExtensions.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using BarRaider.SdTools;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FocusVolumeControl.UI;
|
||||
|
||||
internal static class ISDConnectionExtensions
|
||||
{
|
||||
public static async Task SetFeedbackAsync(this ISDConnection _this, object feedbackPayload)
|
||||
{
|
||||
await _this.SetFeedbackAsync(JObject.FromObject(feedbackPayload));
|
||||
}
|
||||
}
|
41
src/FocusVolumeControl/UI/UIState.cs
Normal file
@ -0,0 +1,41 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using BarRaider.SdTools;
|
||||
using Newtonsoft.Json;
|
||||
using FocusVolumeControl.AudioSessions;
|
||||
|
||||
namespace FocusVolumeControl.UI;
|
||||
|
||||
internal class UIState
|
||||
{
|
||||
[JsonProperty("title")]
|
||||
public string Title { get; private init; }
|
||||
|
||||
[JsonProperty("value")]
|
||||
public ValueWithOpacity<string> Value { get; private init; }
|
||||
|
||||
[JsonProperty("indicator")]
|
||||
public ValueWithOpacity<float>Indicator { get; private init; }
|
||||
|
||||
[JsonProperty("icon")]
|
||||
public ValueWithOpacity<string> icon { get; private init; }
|
||||
|
||||
public UIState(IAudioSession session)
|
||||
{
|
||||
var volume = session.GetVolumeLevel();
|
||||
var opacity = session.IsMuted() ? 0.5f : 1;
|
||||
var iconData = session.GetIcon();
|
||||
|
||||
Title = session.DisplayName;
|
||||
Value = new() { Value = $"{volume}%", Opacity = opacity };
|
||||
Indicator = new() { Value = volume, Opacity = opacity };
|
||||
icon = new() { Value = iconData, Opacity = opacity };
|
||||
}
|
||||
|
||||
}
|
13
src/FocusVolumeControl/UI/ValueWithOpacity.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace FocusVolumeControl.UI;
|
||||
|
||||
internal class ValueWithOpacity<T>
|
||||
{
|
||||
[JsonProperty("value")]
|
||||
public required T Value { get; init; }
|
||||
|
||||
[JsonProperty("opacity")]
|
||||
public required float Opacity { get; init; }
|
||||
|
||||
}
|
27
src/FocusVolumeControl/install.bat
Normal file
@ -0,0 +1,27 @@
|
||||
@echo off
|
||||
REM USAGE: Install.bat <DEBUG/RELEASE>
|
||||
setlocal
|
||||
|
||||
REM cd to directory of install.bat
|
||||
cd /d %~dp0
|
||||
|
||||
REM cd to bin/<Debug|Release>
|
||||
cd bin/%1
|
||||
|
||||
|
||||
SET DISTRIBUTION_TOOL="%~dp0%DistributionTool.exe"
|
||||
SET STREAM_DECK_FILE="c:\Program Files\Elgato\StreamDeck\StreamDeck.exe"
|
||||
SET STREAM_DECK_LOAD_TIMEOUT=7
|
||||
|
||||
REM close processes
|
||||
taskkill /f /im streamdeck.exe
|
||||
taskkill /f /im FocusVolumeControl.exe
|
||||
timeout /t 2
|
||||
|
||||
del com.dlprows.focusvolumecontrol.streamDeckPlugin
|
||||
%DISTRIBUTION_TOOL% -b -i com.dlprows.focusvolumecontrol.sdPlugin -o ./
|
||||
rmdir %APPDATA%\Elgato\StreamDeck\Plugins\com.dlprows.focusvolumecontrol.sdPlugin /s /q
|
||||
START "" %STREAM_DECK_FILE%
|
||||
|
||||
timeout /t %STREAM_DECK_LOAD_TIMEOUT%
|
||||
com.dlprows.focusvolumecontrol.streamDeckPlugin
|
51
src/FocusVolumeControl/manifest.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"Actions": [
|
||||
{
|
||||
"Name": "Focused App Volume",
|
||||
"Icon": "Images/actionIcon",
|
||||
"States": [
|
||||
{
|
||||
"Image": "Images/stateIcon",
|
||||
"TitleAlignment": "middle",
|
||||
"FontSize": "12"
|
||||
}
|
||||
],
|
||||
"Controllers": [ "Encoder" ],
|
||||
"Encoder": {
|
||||
"background": "backgroundImage",
|
||||
"Icon": "Images/actionIcon",
|
||||
"layout": "$B1",
|
||||
"StackColor": "#AABBCC",
|
||||
"TriggerDescription": {
|
||||
"Rotate": "Change the volume",
|
||||
"Push": "Mute",
|
||||
"Touch": "Mute",
|
||||
"LongTouch": "Reset"
|
||||
}
|
||||
},
|
||||
"SupportedInMultiActions": false,
|
||||
"Tooltip": "Control the volume of the focused application",
|
||||
"UUID": "com.dlprows.focusvolumecontrol.dialaction",
|
||||
"PropertyInspectorPath": "PropertyInspector/PluginActionPI.html"
|
||||
}
|
||||
],
|
||||
"Author": "dlprows",
|
||||
"Name": "Focused Application Volume",
|
||||
"Description": "Control the volume of the focused application",
|
||||
"URL": "https://github.com/dlprows/FocusVolumeControl",
|
||||
"Version": "1.1.1",
|
||||
"CodePath": "FocusVolumeControl",
|
||||
"Category": "Volume Control [dlprows]",
|
||||
"Icon": "Images/pluginIcon",
|
||||
"CategoryIcon": "Images/categoryIcon",
|
||||
"OS": [
|
||||
{
|
||||
"Platform": "windows",
|
||||
"MinimumVersion": "10"
|
||||
}
|
||||
],
|
||||
"SDKVersion": 2,
|
||||
"Software": {
|
||||
"MinimumVersion": "6.0"
|
||||
}
|
||||
}
|
9
src/SoundBrowser/App.xaml
Normal file
@ -0,0 +1,9 @@
|
||||
<Application x:Class="SoundBrowser.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:SoundBrowser"
|
||||
StartupUri="MainWindow.xaml">
|
||||
<Application.Resources>
|
||||
|
||||
</Application.Resources>
|
||||
</Application>
|
17
src/SoundBrowser/App.xaml.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
namespace SoundBrowser
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
}
|
||||
}
|
10
src/SoundBrowser/AssemblyInfo.cs
Normal file
@ -0,0 +1,10 @@
|
||||
using System.Windows;
|
||||
|
||||
[assembly: ThemeInfo(
|
||||
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
||||
//(used if a resource is not found in the page,
|
||||
// or application resource dictionaries)
|
||||
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||
//(used if a resource is not found in the page,
|
||||
// app, or any theme specific resource dictionaries)
|
||||
)]
|
19
src/SoundBrowser/MainWindow.xaml
Normal file
@ -0,0 +1,19 @@
|
||||
<Window x:Class="SoundBrowser.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:SoundBrowser"
|
||||
mc:Ignorable="d"
|
||||
Title="MainWindow" Height="800" Width="800">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock x:Name="_tf" Grid.Row="0">current</TextBlock>
|
||||
<TextBlock x:Name="_tf2" Grid.Row="1">list</TextBlock>
|
||||
|
||||
</Grid>
|
||||
</Window>
|
118
src/SoundBrowser/MainWindow.xaml.cs
Normal file
@ -0,0 +1,118 @@
|
||||
using CoreAudio;
|
||||
using FocusVolumeControl;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
|
||||
namespace SoundBrowser;
|
||||
|
||||
/// <summary>
|
||||
/// Interaction logic for MainWindow.xaml
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
|
||||
AudioHelper _audioHelper;
|
||||
Native.WinEventDelegate _delegate;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
_audioHelper = new AudioHelper();
|
||||
|
||||
//normally you can just pass a lambda, but for some reason, that seems to get garbage collected
|
||||
_delegate = new Native.WinEventDelegate(WinEventProc);
|
||||
Native.RegisterForForegroundWindowChangedEvent(_delegate);
|
||||
}
|
||||
|
||||
public void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
|
||||
{
|
||||
SetupCurrentAppFields();
|
||||
SetupAllSessionFields();
|
||||
}
|
||||
|
||||
private void SetupCurrentAppFields()
|
||||
{
|
||||
var handle = Native.GetForegroundWindow();
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (handle != IntPtr.Zero)
|
||||
{
|
||||
//use this in debug to help there be less events
|
||||
|
||||
/*
|
||||
Native.GetWindowThreadProcessId(handle, out var fpid);
|
||||
var fp = Process.GetProcessById(fpid);
|
||||
|
||||
if(!fp.ProcessName.Contains("FSD"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
*/
|
||||
|
||||
var processes = _audioHelper.GetPossibleProcesses();
|
||||
var session = _audioHelper.FindSession(processes);
|
||||
|
||||
foreach (var p in processes)
|
||||
{
|
||||
|
||||
sb.AppendLine($"pid: {p.Id}");
|
||||
sb.AppendLine($"\tprocessName: {p.ProcessName}");
|
||||
try
|
||||
{
|
||||
sb.AppendLine($"\tFileDescription: {p!.MainModule!.FileVersionInfo.FileDescription}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
sb.AppendLine("\tFileDescription: ##ERROR##");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
if (session != null)
|
||||
{
|
||||
sb.AppendLine("picked the following best match");
|
||||
sb.AppendLine($"\tsession: {session.DisplayName}");
|
||||
sb.AppendLine($"\tvolume: {session.GetVolumeLevel()}");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine("No Match");
|
||||
}
|
||||
}
|
||||
|
||||
_tf.Text = sb.ToString();
|
||||
}
|
||||
|
||||
private void SetupAllSessionFields()
|
||||
{
|
||||
_tf2.Text = "";
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("-------------------------------------------------------------------------------");
|
||||
|
||||
var deviceEnumerator = new MMDeviceEnumerator(Guid.NewGuid());
|
||||
|
||||
using var device = deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
|
||||
using var manager = device.AudioSessionManager2;
|
||||
|
||||
var sessions = manager!.Sessions;
|
||||
|
||||
foreach (var session in sessions!)
|
||||
{
|
||||
var audioProcess = Process.GetProcessById((int)session.ProcessID);
|
||||
|
||||
var displayName = audioProcess!.MainModule!.FileVersionInfo.FileDescription;
|
||||
|
||||
sb.AppendLine($"pid: {audioProcess.Id}");
|
||||
sb.AppendLine($"\tprocessName: {audioProcess.ProcessName}");
|
||||
sb.AppendLine($"\tsession: {displayName}");
|
||||
}
|
||||
|
||||
_tf2.Text = sb.ToString();
|
||||
}
|
||||
|
||||
|
||||
}
|
18
src/SoundBrowser/SoundBrowser.csproj
Normal file
@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net7.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseWPF>true</UseWPF>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CoreAudio" Version="1.27.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FocusVolumeControl\FocusVolumeControl.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|