Compare commits
12 Commits
ab769bf7d2
...
v1.0.1
Author | SHA1 | Date | |
---|---|---|---|
f0a5a48c73 | |||
cdf05d2913 | |||
8b87ea39cc | |||
2f410f431e | |||
709ed8a89a | |||
29da6a8d17 | |||
07ddd3bf89 | |||
65ad532adb | |||
b4f4bea0fc | |||
90c014e932 | |||
1dc8ab8a2d | |||
a429a435bc |
18
LICENSE
@ -2,8 +2,20 @@ MIT License
|
|||||||
|
|
||||||
Copyright (c) 2023 dlprows
|
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.
|
||||||
|
40
README.md
@ -1,2 +1,40 @@
|
|||||||
# FocusVolumeControlPlugin
|
# 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
|
@ -1,17 +0,0 @@
|
|||||||
using CoreAudio;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace FocusVolumeControl
|
|
||||||
{
|
|
||||||
internal class ActiveAudioSessionWrapper
|
|
||||||
{
|
|
||||||
public string DisplayName { get; set; }
|
|
||||||
public string ExecutablePath { get; set; }
|
|
||||||
public SimpleAudioVolume Volume { get; set; }
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,63 +1,180 @@
|
|||||||
using CoreAudio;
|
using CoreAudio;
|
||||||
|
using FocusVolumeControl.AudioSessions;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace FocusVolumeControl
|
namespace FocusVolumeControl;
|
||||||
|
|
||||||
|
public class AudioHelper
|
||||||
{
|
{
|
||||||
internal class AudioHelper
|
IAudioSession _current;
|
||||||
{
|
List<Process> _currentProcesses;
|
||||||
ActiveAudioSessionWrapper GetSessionForProcess(Process process)
|
|
||||||
{
|
|
||||||
var deviceEnumerator = new MMDeviceEnumerator(Guid.NewGuid());
|
|
||||||
|
|
||||||
using var device = deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
|
public IAudioSession FindSession(List<Process> processes)
|
||||||
using var manager = device.AudioSessionManager2;
|
{
|
||||||
|
var deviceEnumerator = new MMDeviceEnumerator(Guid.NewGuid());
|
||||||
|
|
||||||
var sessions = manager.Sessions;
|
using var device = deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
|
||||||
|
using var manager = device.AudioSessionManager2;
|
||||||
|
|
||||||
foreach (var session in sessions)
|
var sessions = manager.Sessions;
|
||||||
{
|
|
||||||
var audioProcess = Process.GetProcessById((int)session.ProcessID);
|
|
||||||
|
|
||||||
if (session.ProcessID == process.Id || audioProcess?.ProcessName == process.ProcessName)
|
var matchingSession = new ActiveAudioSessionWrapper();
|
||||||
{
|
|
||||||
var displayName = audioProcess.MainModule.FileVersionInfo.FileDescription;
|
|
||||||
var path = audioProcess.MainModule.FileName;
|
|
||||||
return new ActiveAudioSessionWrapper()
|
|
||||||
{
|
|
||||||
DisplayName = displayName,
|
|
||||||
ExecutablePath = path,
|
|
||||||
Volume = session.SimpleAudioVolume
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal ActiveAudioSessionWrapper GetActiveSession()
|
foreach (var session in sessions)
|
||||||
{
|
{
|
||||||
const int nChars = 256;
|
var audioProcess = Process.GetProcessById((int)session.ProcessID);
|
||||||
IntPtr handle = IntPtr.Zero;
|
|
||||||
StringBuilder Buff = new StringBuilder(nChars);
|
|
||||||
handle = Native.GetForegroundWindow();
|
|
||||||
|
|
||||||
if (handle == IntPtr.Zero)
|
if (processes.Any(x => x.Id == session.ProcessID || x.ProcessName == audioProcess?.ProcessName))
|
||||||
{
|
{
|
||||||
//todo: return system or something like that?
|
try
|
||||||
return null;
|
{
|
||||||
}
|
var displayName = audioProcess.MainModule.FileVersionInfo.FileDescription;
|
||||||
|
if (string.IsNullOrEmpty(displayName))
|
||||||
|
{
|
||||||
|
displayName = audioProcess.ProcessName;
|
||||||
|
}
|
||||||
|
matchingSession.DisplayName = displayName;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
matchingSession.DisplayName ??= audioProcess.ProcessName;
|
||||||
|
}
|
||||||
|
|
||||||
var tid = Native.GetWindowThreadProcessId(handle, out var pid);
|
matchingSession.ExecutablePath ??= audioProcess.MainModule.FileName;
|
||||||
var process = Process.GetProcessById(pid);
|
|
||||||
|
|
||||||
return GetSessionForProcess(process);
|
//some apps like discord have multiple volume processes.
|
||||||
}
|
matchingSession.AddVolume(session.SimpleAudioVolume);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matchingSession.Any() ? matchingSession : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static object _lock = new object();
|
||||||
|
|
||||||
|
public IAudioSession GetActiveSession(FallbackBehavior fallbackBehavior)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
var processes = GetPossibleProcesses();
|
||||||
|
|
||||||
|
if (_currentProcesses == null || !_currentProcesses.SequenceEqual(processes))
|
||||||
|
{
|
||||||
|
_current = FindSession(processes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(_current == null)
|
||||||
|
{
|
||||||
|
if(fallbackBehavior == FallbackBehavior.SystemSounds)
|
||||||
|
{
|
||||||
|
_current = GetSystemSounds();
|
||||||
|
}
|
||||||
|
else if(fallbackBehavior == FallbackBehavior.SystemVolume)
|
||||||
|
{
|
||||||
|
_current = GetSystemVolume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentProcesses = processes;
|
||||||
|
return _current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the list of processes that might be currently selected
|
||||||
|
/// This includes getting the child window's processes
|
||||||
|
///
|
||||||
|
/// This helps to find the audo process for windows store apps whose process is "ApplicationFrameHost.exe"
|
||||||
|
///
|
||||||
|
/// The list may optionally include a parent process, because that helps thing steam to be more reliable because the steamwebhelper (ui) is a child of steam.exe
|
||||||
|
///
|
||||||
|
/// According to deej, getting the ForegroundWindow and enumerating steam windows should work, but it doesn't seem to work for me without including the parent process
|
||||||
|
/// https://github.com/omriharel/deej/blob/master/pkg/deej/util/util_windows.go#L22
|
||||||
|
///
|
||||||
|
/// but the parent process is sometimes useless (explorer, svchost, etc) so i filter some of them out because i felt like it when i wrote the code
|
||||||
|
///
|
||||||
|
/// I also experimented with grabbing the parent process and enumerating through the windows to see if that would help, but any time the parent process was an unexpected process (explorer) it could blow up. so i decided not to bother for now
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public List<Process> GetPossibleProcesses()
|
||||||
|
{
|
||||||
|
var handle = Native.GetForegroundWindow();
|
||||||
|
|
||||||
|
if (handle == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ids = Native.GetProcessesOfChildWindows(handle);
|
||||||
|
|
||||||
|
Native.GetWindowThreadProcessId(handle, out var pid);
|
||||||
|
ids.Insert(0, pid);
|
||||||
|
|
||||||
|
var processes = ids.Distinct()
|
||||||
|
.Select(x => Process.GetProcessById(x))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var blah = ParentProcessUtilities.GetParentProcess(pid);
|
||||||
|
if (blah != null && blah.ProcessName != "explorer" && blah.ProcessName != "svchost")
|
||||||
|
{
|
||||||
|
processes.Add(blah);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
return processes;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetAll()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var deviceEnumerator = new MMDeviceEnumerator(Guid.NewGuid());
|
||||||
|
|
||||||
|
using var device = deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
|
||||||
|
using var manager = device.AudioSessionManager2;
|
||||||
|
|
||||||
|
foreach (var session in manager.Sessions)
|
||||||
|
{
|
||||||
|
session.SimpleAudioVolume.MasterVolume = 1;
|
||||||
|
session.SimpleAudioVolume.Mute = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAudioSession GetSystemSounds()
|
||||||
|
{
|
||||||
|
var deviceEnumerator = new MMDeviceEnumerator(Guid.NewGuid());
|
||||||
|
|
||||||
|
using var device = deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
|
||||||
|
using var manager = device.AudioSessionManager2;
|
||||||
|
|
||||||
|
var sessions = manager.Sessions;
|
||||||
|
|
||||||
|
foreach (var session in sessions)
|
||||||
|
{
|
||||||
|
if (session.IsSystemSoundsSession)
|
||||||
|
{
|
||||||
|
return new SystemSoundsAudioSession(session.SimpleAudioVolume);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
public IAudioSession GetSystemVolume()
|
||||||
|
{
|
||||||
|
var deviceEnumerator = new MMDeviceEnumerator(Guid.NewGuid());
|
||||||
|
|
||||||
|
using var device = deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
|
||||||
|
return new SystemVolumeAudioSession(device.AudioEndpointVolume);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,82 @@
|
|||||||
|
using CoreAudio;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using BarRaider.SdTools;
|
||||||
|
using System.Drawing;
|
||||||
|
|
||||||
|
namespace FocusVolumeControl.AudioSessions;
|
||||||
|
|
||||||
|
public class ActiveAudioSessionWrapper : IAudioSession
|
||||||
|
{
|
||||||
|
public string DisplayName { get; set; }
|
||||||
|
public string ExecutablePath { get; set; }
|
||||||
|
private List<SimpleAudioVolume> Volume { get; } = new List<SimpleAudioVolume>();
|
||||||
|
|
||||||
|
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 => Volume.Count;
|
||||||
|
|
||||||
|
public void AddVolume(SimpleAudioVolume volume)
|
||||||
|
{
|
||||||
|
Volume.Add(volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = Volume.All(x => x.Mute);
|
||||||
|
|
||||||
|
Volume.ForEach(x => x.Mute = !muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsMuted()
|
||||||
|
{
|
||||||
|
return Volume.All(x => x.Mute);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void IncrementVolumeLevel(int step, int ticks)
|
||||||
|
{
|
||||||
|
//if you have more than one volume. they will all get set based on the first volume control
|
||||||
|
var level = Volume.FirstOrDefault()?.MasterVolume ?? 0;
|
||||||
|
|
||||||
|
level += (0.01f * step) * ticks;
|
||||||
|
level = Math.Max(level, 0);
|
||||||
|
level = Math.Min(level, 1);
|
||||||
|
|
||||||
|
Volume.ForEach(x => x.MasterVolume = level);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetVolumeLevel()
|
||||||
|
{
|
||||||
|
var level = Volume.FirstOrDefault()?.MasterVolume ?? 0;
|
||||||
|
return (int)(level * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
16
src/FocusVolumeControl/AudioSessions/IAudioSession.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
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,38 @@
|
|||||||
|
using CoreAudio;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace FocusVolumeControl.AudioSessions;
|
||||||
|
|
||||||
|
internal class SystemSoundsAudioSession : IAudioSession
|
||||||
|
{
|
||||||
|
public SystemSoundsAudioSession(SimpleAudioVolume volumeControl)
|
||||||
|
{
|
||||||
|
_volumeControl = volumeControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
SimpleAudioVolume _volumeControl;
|
||||||
|
|
||||||
|
public string DisplayName => "System sounds";
|
||||||
|
public string GetIcon() => "Images/systemSounds";
|
||||||
|
|
||||||
|
public void ToggleMute()
|
||||||
|
{
|
||||||
|
_volumeControl.Mute = !_volumeControl.Mute;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsMuted() => _volumeControl.Mute;
|
||||||
|
|
||||||
|
public void IncrementVolumeLevel(int step, int ticks)
|
||||||
|
{
|
||||||
|
var level = _volumeControl.MasterVolume;
|
||||||
|
|
||||||
|
level += (0.01f * step) * ticks;
|
||||||
|
level = Math.Max(level, 0);
|
||||||
|
level = Math.Min(level, 1);
|
||||||
|
|
||||||
|
_volumeControl.MasterVolume = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetVolumeLevel() => (int)(_volumeControl.MasterVolume * 100);
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
using CoreAudio;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace FocusVolumeControl.AudioSessions;
|
||||||
|
|
||||||
|
internal class SystemVolumeAudioSession : IAudioSession
|
||||||
|
{
|
||||||
|
public SystemVolumeAudioSession(AudioEndpointVolume volumeControl)
|
||||||
|
{
|
||||||
|
_volumeControl = volumeControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioEndpointVolume _volumeControl;
|
||||||
|
|
||||||
|
public string DisplayName => "System Volume";
|
||||||
|
public string GetIcon() => "Images/encoderIcon";
|
||||||
|
|
||||||
|
public void ToggleMute()
|
||||||
|
{
|
||||||
|
_volumeControl.Mute = !_volumeControl.Mute;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsMuted() => _volumeControl.Mute;
|
||||||
|
|
||||||
|
public void IncrementVolumeLevel(int step, int ticks)
|
||||||
|
{
|
||||||
|
var level = _volumeControl.MasterVolumeLevelScalar;
|
||||||
|
|
||||||
|
level += (0.01f * step) * ticks;
|
||||||
|
level = Math.Max(level, 0);
|
||||||
|
level = Math.Min(level, 1);
|
||||||
|
|
||||||
|
_volumeControl.MasterVolumeLevelScalar = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetVolumeLevel() => (int)(_volumeControl.MasterVolumeLevelScalar * 100);
|
||||||
|
|
||||||
|
}
|
@ -1,203 +1,222 @@
|
|||||||
using BarRaider.SdTools;
|
using BarRaider.SdTools;
|
||||||
using BarRaider.SdTools.Payloads;
|
using BarRaider.SdTools.Payloads;
|
||||||
using CoreAudio;
|
using FocusVolumeControl.AudioSessions;
|
||||||
|
using FocusVolumeControl.UI;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Data;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Drawing;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
|
||||||
namespace FocusVolumeControl
|
namespace FocusVolumeControl;
|
||||||
|
|
||||||
|
[PluginActionId("com.dlprows.focusvolumecontrol.dialaction")]
|
||||||
|
public class DialAction : EncoderBase
|
||||||
{
|
{
|
||||||
|
private class PluginSettings
|
||||||
|
{
|
||||||
|
[JsonProperty("fallbackBehavior")]
|
||||||
|
public FallbackBehavior FallbackBehavior { get; set; }
|
||||||
|
|
||||||
/*
|
public static PluginSettings CreateDefaultSettings()
|
||||||
todo:
|
{
|
||||||
link both discord processes
|
PluginSettings instance = new PluginSettings();
|
||||||
steam not detecting
|
instance.FallbackBehavior = FallbackBehavior.SystemSounds;
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
long press reset
|
private PluginSettings settings;
|
||||||
|
|
||||||
option for what to do when on app without sound
|
IntPtr _foregroundWindowChangedEvent;
|
||||||
|
Native.WinEventDelegate _delegate;
|
||||||
|
|
||||||
gitea
|
IAudioSession _currentAudioSession;
|
||||||
*/
|
AudioHelper _audioHelper = new AudioHelper();
|
||||||
|
|
||||||
|
Thread _thread;
|
||||||
|
Dispatcher _dispatcher;
|
||||||
|
|
||||||
[PluginActionId("com.dlprows.focusvolumecontrol.dialaction")]
|
UIState _previousState;
|
||||||
public class DialAction : EncoderBase
|
|
||||||
{
|
|
||||||
private class PluginSettings
|
|
||||||
{
|
|
||||||
public static PluginSettings CreateDefaultSettings()
|
|
||||||
{
|
|
||||||
PluginSettings instance = new PluginSettings();
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private PluginSettings settings;
|
public DialAction(ISDConnection connection, InitialPayload payload) : base(connection, payload)
|
||||||
//IntPtr _foregroundWindowChangedEvent;
|
{
|
||||||
//WinEventDelegate _delegate;
|
if (payload.Settings == null || payload.Settings.Count == 0)
|
||||||
ActiveAudioSessionWrapper _currentAudioSession;
|
{
|
||||||
AudioHelper _audioHelper = new AudioHelper();
|
settings = PluginSettings.CreateDefaultSettings();
|
||||||
|
SaveSettings();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
settings = payload.Settings.ToObject<PluginSettings>();
|
||||||
|
}
|
||||||
|
|
||||||
public DialAction(ISDConnection connection, InitialPayload payload) : base(connection, payload)
|
_thread = new Thread(() =>
|
||||||
{
|
{
|
||||||
if (payload.Settings == null || payload.Settings.Count == 0)
|
Logger.Instance.LogMessage(TracingLevel.DEBUG, "Registering for events");
|
||||||
{
|
_delegate = new Native.WinEventDelegate(WinEventProc);
|
||||||
settings = PluginSettings.CreateDefaultSettings();
|
_foregroundWindowChangedEvent = Native.RegisterForForegroundWindowChangedEvent(_delegate);
|
||||||
SaveSettings();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
settings = payload.Settings.ToObject<PluginSettings>();
|
|
||||||
}
|
|
||||||
|
|
||||||
//_delegate = new WinEventDelegate(WinEventProc);
|
Logger.Instance.LogMessage(TracingLevel.DEBUG, "Starting Dispatcher");
|
||||||
//_foregroundWindowChangedEvent = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, IntPtr.Zero, _delegate, 0, 0, WINEVENT_OUTOFCONTEXT);
|
_dispatcher = Dispatcher.CurrentDispatcher;
|
||||||
}
|
Dispatcher.Run();
|
||||||
|
Logger.Instance.LogMessage(TracingLevel.DEBUG, "Dispatcher Stopped");
|
||||||
|
});
|
||||||
|
_thread.SetApartmentState(ApartmentState.STA);
|
||||||
|
_thread.Start();
|
||||||
|
|
||||||
public override async void DialDown(DialPayload payload)
|
_currentAudioSession = settings.FallbackBehavior == FallbackBehavior.SystemSounds ? _audioHelper.GetSystemSounds() : _audioHelper.GetSystemVolume();
|
||||||
{
|
}
|
||||||
//dial pressed down
|
|
||||||
Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Down");
|
|
||||||
if(_currentAudioSession != null)
|
|
||||||
{
|
|
||||||
_currentAudioSession.Volume.Mute = !_currentAudioSession.Volume.Mute;
|
|
||||||
var uiState = UIState.Build(_currentAudioSession);
|
|
||||||
await Connection.SetFeedbackAsync(uiState);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await Connection.ShowAlert();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async void TouchPress(TouchpadPressPayload payload)
|
public override async void DialDown(DialPayload payload)
|
||||||
{
|
{
|
||||||
Logger.Instance.LogMessage(TracingLevel.INFO, "Touch Press");
|
//dial pressed down
|
||||||
if (payload.IsLongPress)
|
Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Down");
|
||||||
{
|
await ToggleMuteAsync();
|
||||||
//todo: iterate through all sessions setting them back to 100 except the master volume
|
}
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
|
|
||||||
|
public override async void TouchPress(TouchpadPressPayload payload)
|
||||||
|
{
|
||||||
|
Logger.Instance.LogMessage(TracingLevel.INFO, "Touch Press");
|
||||||
|
if (payload.IsLongPress)
|
||||||
|
{
|
||||||
|
_audioHelper.ResetAll();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await ToggleMuteAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (_currentAudioSession != null)
|
async Task ToggleMuteAsync()
|
||||||
{
|
{
|
||||||
_currentAudioSession.Volume.Mute = !_currentAudioSession.Volume.Mute;
|
try
|
||||||
var uiState = UIState.Build(_currentAudioSession);
|
{
|
||||||
await Connection.SetFeedbackAsync(uiState);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await Connection.ShowAlert();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async void DialRotate(DialRotatePayload payload)
|
if (_currentAudioSession != null)
|
||||||
{
|
{
|
||||||
Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Rotate");
|
_currentAudioSession.ToggleMute();
|
||||||
//dial rotated. ticks positive for right, negative for left
|
await UpdateStateIfNeeded();
|
||||||
if(_currentAudioSession != null)
|
}
|
||||||
{
|
else
|
||||||
_currentAudioSession.Volume.MasterVolume += (0.01f) * payload.Ticks;
|
{
|
||||||
|
await Connection.ShowAlert();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await Connection.ShowAlert();
|
||||||
|
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unable to toggle mute: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var uiState = UIState.Build(_currentAudioSession);
|
public override async void DialRotate(DialRotatePayload payload)
|
||||||
await Connection.SetFeedbackAsync(uiState);
|
{
|
||||||
}
|
Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Rotate");
|
||||||
else
|
//dial rotated. ticks positive for right, negative for left
|
||||||
{
|
try
|
||||||
await Connection.ShowAlert();
|
{
|
||||||
}
|
if (_currentAudioSession != null)
|
||||||
}
|
{
|
||||||
|
_currentAudioSession.IncrementVolumeLevel(1, payload.Ticks);
|
||||||
|
await UpdateStateIfNeeded();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await Connection.ShowAlert();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await Connection.ShowAlert();
|
||||||
|
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unable to toggle mute: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public override void DialUp(DialPayload payload)
|
public override void DialUp(DialPayload payload)
|
||||||
{
|
{
|
||||||
//dial unpressed
|
//dial unpressed
|
||||||
Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Up");
|
Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Up");
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Dispose()
|
public override void Dispose()
|
||||||
{
|
{
|
||||||
/*
|
Logger.Instance.LogMessage(TracingLevel.DEBUG, "Disposing");
|
||||||
if(_foregroundWindowChangedEvent != IntPtr.Zero)
|
if(_foregroundWindowChangedEvent != IntPtr.Zero)
|
||||||
{
|
{
|
||||||
Native.UnhookWinEvent(_foregroundWindowChangedEvent);
|
Native.UnhookWinEvent(_foregroundWindowChangedEvent);
|
||||||
}
|
}
|
||||||
*/
|
_dispatcher.InvokeShutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async void OnTick()
|
public override async void OnTick()
|
||||||
{
|
{
|
||||||
//called once every 1000ms and can be used for updating the title/image fo the key
|
//called once every 1000ms and can be used for updating the title/image of the key
|
||||||
var activeSession = _audioHelper.GetActiveSession();
|
var activeSession = _audioHelper.GetActiveSession(settings.FallbackBehavior);
|
||||||
|
|
||||||
if (activeSession == null)
|
if(activeSession != null)
|
||||||
{
|
{
|
||||||
//todo: something?
|
_currentAudioSession = activeSession;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
_currentAudioSession = activeSession;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(_currentAudioSession != null)
|
await UpdateStateIfNeeded();
|
||||||
{
|
}
|
||||||
var uiState = UIState.BuildWithImage(_currentAudioSession);
|
|
||||||
await Connection.SetFeedbackAsync(uiState);
|
private async Task UpdateStateIfNeeded()
|
||||||
}
|
{
|
||||||
}
|
if (_currentAudioSession != null)
|
||||||
|
{
|
||||||
|
|
||||||
|
var uiState = new UIState(_currentAudioSession);
|
||||||
|
|
||||||
|
if ( _previousState != null && uiState != null &&
|
||||||
|
uiState.Title == _previousState.Title &&
|
||||||
|
uiState.Value.Value == _previousState.Value.Value &&
|
||||||
|
uiState.Value.Opacity == _previousState.Value.Opacity &&
|
||||||
|
uiState.Indicator.Value == _previousState.Indicator.Value &&
|
||||||
|
uiState.Indicator.Opacity == _previousState.Indicator.Opacity &&
|
||||||
|
uiState.icon.Value == _previousState.icon.Value &&
|
||||||
|
uiState.icon.Opacity == _previousState.icon.Opacity
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Connection.SetFeedbackAsync(uiState);
|
||||||
|
_previousState = uiState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public override void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload)
|
public override void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public override void ReceivedSettings(ReceivedSettingsPayload payload)
|
public override void ReceivedSettings(ReceivedSettingsPayload payload)
|
||||||
{
|
{
|
||||||
Tools.AutoPopulateSettings(settings, payload.Settings);
|
Tools.AutoPopulateSettings(settings, payload.Settings);
|
||||||
SaveSettings();
|
SaveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task SaveSettings()
|
private Task SaveSettings()
|
||||||
{
|
{
|
||||||
return Connection.SetSettingsAsync(JObject.FromObject(settings));
|
return Connection.SetSettingsAsync(JObject.FromObject(settings));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
public void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
|
||||||
public async void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
|
{
|
||||||
{
|
try
|
||||||
var activeSession = _audioHelper.GetActiveSession();
|
{
|
||||||
|
OnTick();
|
||||||
|
Thread.Sleep(TimeSpan.FromSeconds(1));
|
||||||
if(activeSession == null)
|
}
|
||||||
{
|
catch (Exception ex)
|
||||||
//todo: something?
|
{
|
||||||
|
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Exception on WinEventProc\n {ex}");
|
||||||
}
|
}
|
||||||
else
|
}
|
||||||
{
|
|
||||||
_currentAudioSession = activeSession;
|
|
||||||
|
|
||||||
//populate the UI
|
|
||||||
await Connection.SetTitleAsync(_currentAudioSession.DisplayName);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
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
|
||||||
|
}
|
@ -51,17 +51,23 @@
|
|||||||
<Reference Include="System.Data" />
|
<Reference Include="System.Data" />
|
||||||
<Reference Include="System.Net.Http" />
|
<Reference Include="System.Net.Http" />
|
||||||
<Reference Include="System.Xml" />
|
<Reference Include="System.Xml" />
|
||||||
|
<Reference Include="WindowsBase" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="ActiveAudioSessionWrapper.cs" />
|
<Compile Include="AudioSessions\ActiveAudioSessionWrapper.cs" />
|
||||||
<Compile Include="AudioHelper.cs" />
|
<Compile Include="AudioHelper.cs" />
|
||||||
|
<Compile Include="AudioSessions\SystemSoundsAudioSession.cs" />
|
||||||
|
<Compile Include="AudioSessions\SystemVolumeAudioSession.cs" />
|
||||||
<Compile Include="DialAction.cs" />
|
<Compile Include="DialAction.cs" />
|
||||||
<Compile Include="ISDConnectionExtensions.cs" />
|
<Compile Include="AudioSessions\IAudioSession.cs" />
|
||||||
|
<Compile Include="FallbackBehavior.cs" />
|
||||||
|
<Compile Include="UI\ISDConnectionExtensions.cs" />
|
||||||
<Compile Include="Native.cs" />
|
<Compile Include="Native.cs" />
|
||||||
<Compile Include="PluginAction.cs" />
|
<Compile Include="ParentProcessUtilities.cs" />
|
||||||
<Compile Include="Program.cs" />
|
<Compile Include="Program.cs" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
<Compile Include="UIState.cs" />
|
<Compile Include="UI\UIState.cs" />
|
||||||
|
<Compile Include="UI\ValueWithOpacity.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="App.config" />
|
<None Include="App.config" />
|
||||||
@ -71,53 +77,36 @@
|
|||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Include="Images\categoryIcon%402x.png">
|
<Content Include="$(SolutionDir)..\previews\**\*.png" Link="previews\%(Filename)%(Extension)">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
<Content Include="Images\categoryIcon.png">
|
<Content Include="Images\**\*.png">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
<Content Include="Images\actionIcon%402x.png">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
<Content Include="Images\actionIcon.png">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
<Content Include="Images\icon%402x.png">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
<Content Include="Images\icon.png">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
<Content Include="Images\pluginAction%402x.png">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
<Content Include="Images\pluginAction.png">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
<Content Include="Images\pluginIcon%402x.png">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
<Content Include="Images\pluginIcon.png">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
<Content Include="PropertyInspector\PluginActionPI.html">
|
<Content Include="PropertyInspector\PluginActionPI.html">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
</Content>
|
</Content>
|
||||||
<Content Include="PropertyInspector\PluginActionPI.js">
|
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
|
||||||
</Content>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CoreAudio">
|
<PackageReference Include="CoreAudio">
|
||||||
<Version>1.27.0</Version>
|
<Version>1.27.0</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
<PackageReference Include="IsExternalInit">
|
||||||
|
<Version>1.0.3</Version>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
<PackageReference Include="Newtonsoft.Json">
|
<PackageReference Include="Newtonsoft.Json">
|
||||||
<Version>13.0.3</Version>
|
<Version>13.0.3</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="NLog">
|
<PackageReference Include="NLog">
|
||||||
<Version>5.2.3</Version>
|
<Version>5.2.3</Version>
|
||||||
</PackageReference>
|
</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">
|
<PackageReference Include="streamdeck-client-csharp">
|
||||||
<Version>4.3.0</Version>
|
<Version>4.3.0</Version>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
@ -126,8 +115,4 @@
|
|||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
<PropertyGroup>
|
|
||||||
<PostBuildEvent>
|
|
||||||
</PostBuildEvent>
|
|
||||||
</PropertyGroup>
|
|
||||||
</Project>
|
</Project>
|
@ -1,18 +0,0 @@
|
|||||||
using BarRaider.SdTools;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace FocusVolumeControl
|
|
||||||
{
|
|
||||||
internal static class ISDConnectionExtensions
|
|
||||||
{
|
|
||||||
public static async Task SetFeedbackAsync(this ISDConnection _this, object feedbackPayload)
|
|
||||||
{
|
|
||||||
await _this.SetFeedbackAsync(JObject.FromObject(feedbackPayload));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 897 B |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 7.8 KiB |
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 9.1 KiB |
Before Width: | Height: | Size: 1.6 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 |
Before Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 8.3 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 36 KiB 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 |
@ -1,34 +1,62 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace FocusVolumeControl
|
namespace FocusVolumeControl;
|
||||||
|
|
||||||
|
public class Native
|
||||||
{
|
{
|
||||||
internal class Native
|
public delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);
|
||||||
{
|
|
||||||
internal delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);
|
|
||||||
|
|
||||||
[DllImport("user32.dll")]
|
[DllImport("user32.dll")]
|
||||||
static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags);
|
static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags);
|
||||||
|
|
||||||
[DllImport("user32.dll")]
|
[DllImport("user32.dll")]
|
||||||
internal static extern bool UnhookWinEvent(IntPtr hWinEventHook);
|
public static extern bool UnhookWinEvent(IntPtr hWinEventHook);
|
||||||
|
|
||||||
|
|
||||||
private const uint WINEVENT_OUTOFCONTEXT = 0;
|
private const uint WINEVENT_OUTOFCONTEXT = 0;
|
||||||
private const uint EVENT_SYSTEM_FOREGROUND = 3;
|
private const uint EVENT_SYSTEM_FOREGROUND = 3;
|
||||||
|
|
||||||
internal static IntPtr RegisterForForegroundWindowChangedEvent(WinEventDelegate dele) => SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, IntPtr.Zero, dele, 0, 0, WINEVENT_OUTOFCONTEXT);
|
public static IntPtr RegisterForForegroundWindowChangedEvent(WinEventDelegate dele) => SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, IntPtr.Zero, dele, 0, 0, WINEVENT_OUTOFCONTEXT);
|
||||||
|
|
||||||
|
|
||||||
[DllImport("user32.dll")]
|
[DllImport("user32.dll")]
|
||||||
internal static extern IntPtr GetForegroundWindow();
|
public static extern IntPtr GetForegroundWindow();
|
||||||
|
|
||||||
[DllImport("user32.dll", SetLastError = true)]
|
[DllImport("user32.dll", SetLastError = true)]
|
||||||
internal static extern uint GetWindowThreadProcessId(IntPtr hWnd, out int processId);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,83 +0,0 @@
|
|||||||
using BarRaider.SdTools;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace FocusVolumeControl
|
|
||||||
{
|
|
||||||
[PluginActionId("FocusVolumeControl.pluginaction")]
|
|
||||||
public class PluginAction : KeypadBase
|
|
||||||
{
|
|
||||||
private class PluginSettings
|
|
||||||
{
|
|
||||||
public static PluginSettings CreateDefaultSettings()
|
|
||||||
{
|
|
||||||
PluginSettings instance = new PluginSettings();
|
|
||||||
instance.OutputFileName = String.Empty;
|
|
||||||
instance.InputString = String.Empty;
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
[FilenameProperty]
|
|
||||||
[JsonProperty(PropertyName = "outputFileName")]
|
|
||||||
public string OutputFileName { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty(PropertyName = "inputString")]
|
|
||||||
public string InputString { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Private Members
|
|
||||||
|
|
||||||
private PluginSettings settings;
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
public PluginAction(SDConnection connection, InitialPayload payload) : base(connection, payload)
|
|
||||||
{
|
|
||||||
if (payload.Settings == null || payload.Settings.Count == 0)
|
|
||||||
{
|
|
||||||
this.settings = PluginSettings.CreateDefaultSettings();
|
|
||||||
SaveSettings();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
this.settings = payload.Settings.ToObject<PluginSettings>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Dispose()
|
|
||||||
{
|
|
||||||
Logger.Instance.LogMessage(TracingLevel.INFO, $"Destructor called");
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void KeyPressed(KeyPayload payload)
|
|
||||||
{
|
|
||||||
Logger.Instance.LogMessage(TracingLevel.INFO, "Key Pressed");
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void KeyReleased(KeyPayload payload) { }
|
|
||||||
|
|
||||||
public override void OnTick() { }
|
|
||||||
|
|
||||||
public override void ReceivedSettings(ReceivedSettingsPayload payload)
|
|
||||||
{
|
|
||||||
Tools.AutoPopulateSettings(settings, payload.Settings);
|
|
||||||
SaveSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload) { }
|
|
||||||
|
|
||||||
#region Private Methods
|
|
||||||
|
|
||||||
private Task SaveSettings()
|
|
||||||
{
|
|
||||||
return Connection.SetSettingsAsync(JObject.FromObject(settings));
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +1,14 @@
|
|||||||
using BarRaider.SdTools;
|
using BarRaider.SdTools;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace FocusVolumeControl
|
namespace FocusVolumeControl;
|
||||||
|
|
||||||
|
internal class Program
|
||||||
{
|
{
|
||||||
internal class Program
|
static void Main(string[] args)
|
||||||
{
|
{
|
||||||
static void Main(string[] args)
|
// Uncomment this line of code to allow for debugging
|
||||||
{
|
//while (!System.Diagnostics.Debugger.IsAttached) { System.Threading.Thread.Sleep(100); }
|
||||||
// Uncomment this line of code to allow for debugging
|
|
||||||
//while (!System.Diagnostics.Debugger.IsAttached) { System.Threading.Thread.Sleep(100); }
|
|
||||||
|
|
||||||
SDWrapper.Run(args);
|
SDWrapper.Run(args);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -8,10 +8,23 @@
|
|||||||
<title>FocusVolumeControl Settings</title>
|
<title>FocusVolumeControl Settings</title>
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/barraider/streamdeck-easypi@latest/src/sdpi.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/barraider/streamdeck-easypi@latest/src/sdpi.css">
|
||||||
<script src="https://cdn.jsdelivr.net/gh/barraider/streamdeck-easypi@latest/src/sdtools.common.js"></script>
|
<script src="https://cdn.jsdelivr.net/gh/barraider/streamdeck-easypi@latest/src/sdtools.common.js"></script>
|
||||||
<script src="PluginActionPI.js"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="sdpi-wrapper">
|
<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>
|
||||||
|
<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>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1 +0,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; }
|
||||||
|
|
||||||
|
}
|
@ -1,68 +0,0 @@
|
|||||||
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;
|
|
||||||
|
|
||||||
namespace FocusVolumeControl
|
|
||||||
{
|
|
||||||
internal class UIState
|
|
||||||
{
|
|
||||||
public static Dictionary<string, object> Build(ActiveAudioSessionWrapper session)
|
|
||||||
{
|
|
||||||
var volume = (int)(session.Volume.MasterVolume * 100);
|
|
||||||
|
|
||||||
var valueThing = new Dictionary<string, string>()
|
|
||||||
{
|
|
||||||
{ "value", $"{volume}%" },
|
|
||||||
{ "opacity", "0.5" }
|
|
||||||
};
|
|
||||||
var opacity = session.Volume.Mute ? 0.5f : 1;
|
|
||||||
|
|
||||||
var payload = new Dictionary<string, object>()
|
|
||||||
{
|
|
||||||
{ "indicator", ValueWithOpacity(volume, opacity) },
|
|
||||||
{ "value", ValueWithOpacity($"{volume}%", opacity ) },
|
|
||||||
{ "title", session.DisplayName },
|
|
||||||
};
|
|
||||||
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Dictionary<string, object> ValueWithOpacity(object value, float opacity)
|
|
||||||
{
|
|
||||||
return new Dictionary<string, object>()
|
|
||||||
{
|
|
||||||
{ "value", value },
|
|
||||||
{ "opacity", opacity }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Dictionary<string, object> BuildWithImage(ActiveAudioSessionWrapper session)
|
|
||||||
{
|
|
||||||
var payload = Build(session);
|
|
||||||
var opacity = session.Volume.Mute ? 0.5f : 1;
|
|
||||||
|
|
||||||
var iconData = "";
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var icon = Icon.ExtractAssociatedIcon(session.ExecutablePath);
|
|
||||||
iconData = Tools.ImageToBase64(icon.ToBitmap(), true);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
iconData = "Image/pluginIcon.png";
|
|
||||||
}
|
|
||||||
|
|
||||||
payload["icon"] = ValueWithOpacity(iconData, opacity);
|
|
||||||
|
|
||||||
return payload;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,10 +2,10 @@
|
|||||||
"Actions": [
|
"Actions": [
|
||||||
{
|
{
|
||||||
"Name": "Focused App Volume",
|
"Name": "Focused App Volume",
|
||||||
"Icon": "Images/icon",
|
"Icon": "Images/actionIcon",
|
||||||
"States": [
|
"States": [
|
||||||
{
|
{
|
||||||
"Image": "Images/pluginAction",
|
"Image": "Images/stateIcon",
|
||||||
"TitleAlignment": "middle",
|
"TitleAlignment": "middle",
|
||||||
"FontSize": "12"
|
"FontSize": "12"
|
||||||
}
|
}
|
||||||
@ -18,9 +18,9 @@
|
|||||||
"StackColor": "#AABBCC",
|
"StackColor": "#AABBCC",
|
||||||
"TriggerDescription": {
|
"TriggerDescription": {
|
||||||
"Rotate": "Change the volume",
|
"Rotate": "Change the volume",
|
||||||
"Push": "Mute/UnMute",
|
"Push": "Mute",
|
||||||
"Touch": "Mute/UnMute",
|
"Touch": "Mute",
|
||||||
"LongTouch": "Reset all apps"
|
"LongTouch": "Reset"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"SupportedInMultiActions": false,
|
"SupportedInMultiActions": false,
|
||||||
@ -29,11 +29,11 @@
|
|||||||
"PropertyInspectorPath": "PropertyInspector/PluginActionPI.html"
|
"PropertyInspectorPath": "PropertyInspector/PluginActionPI.html"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"Author": "Daniel Prows",
|
"Author": "dlprows",
|
||||||
"Name": "FocusVolumeControl",
|
"Name": "Focused Application Volume",
|
||||||
"Description": "Control the volume of the focused application",
|
"Description": "Control the volume of the focused application",
|
||||||
"URL": "https://encyclopediaofdaniel.com",
|
"URL": "https://github.com/dlprows/FocusVolumeControl",
|
||||||
"Version": "1.0",
|
"Version": "1.0.1",
|
||||||
"CodePath": "FocusVolumeControl",
|
"CodePath": "FocusVolumeControl",
|
||||||
"Category": "Volume Control [dlprows]",
|
"Category": "Volume Control [dlprows]",
|
||||||
"Icon": "Images/pluginIcon",
|
"Icon": "Images/pluginIcon",
|
||||||
|
@ -5,11 +5,15 @@
|
|||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:local="clr-namespace:SoundBrowser"
|
xmlns:local="clr-namespace:SoundBrowser"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="MainWindow" Height="150" Width="800">
|
Title="MainWindow" Height="800" Width="800">
|
||||||
<Grid>
|
<Grid>
|
||||||
<StackPanel>
|
<Grid.RowDefinitions>
|
||||||
<TextBlock x:Name="_tf">blah</TextBlock>
|
<RowDefinition Height="auto"/>
|
||||||
</StackPanel>
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock x:Name="_tf" Grid.Row="0">current</TextBlock>
|
||||||
|
<TextBlock x:Name="_tf2" Grid.Row="1">list</TextBlock>
|
||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
@ -1,116 +1,118 @@
|
|||||||
using CoreAudio;
|
using CoreAudio;
|
||||||
|
using FocusVolumeControl;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
|
||||||
using System.Windows.Data;
|
|
||||||
using System.Windows.Documents;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using System.Windows.Media;
|
|
||||||
using System.Windows.Media.Imaging;
|
|
||||||
using System.Windows.Navigation;
|
|
||||||
using System.Windows.Shapes;
|
|
||||||
|
|
||||||
namespace SoundBrowser
|
namespace SoundBrowser;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interaction logic for MainWindow.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class MainWindow : Window
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Interaction logic for MainWindow.xaml
|
AudioHelper _audioHelper;
|
||||||
/// </summary>
|
Native.WinEventDelegate _delegate;
|
||||||
public partial class MainWindow : Window
|
|
||||||
{
|
public MainWindow()
|
||||||
public MainWindow()
|
{
|
||||||
{
|
InitializeComponent();
|
||||||
InitializeComponent();
|
_audioHelper = new AudioHelper();
|
||||||
dele = new WinEventDelegate(WinEventProc);
|
|
||||||
IntPtr m_hhook = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, IntPtr.Zero, dele, 0, 0, WINEVENT_OUTOFCONTEXT);
|
//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##");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
WinEventDelegate dele = null;
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);
|
_tf.Text = sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
[DllImport("user32.dll")]
|
private void SetupAllSessionFields()
|
||||||
static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags);
|
{
|
||||||
|
_tf2.Text = "";
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("-------------------------------------------------------------------------------");
|
||||||
|
|
||||||
private const uint WINEVENT_OUTOFCONTEXT = 0;
|
var deviceEnumerator = new MMDeviceEnumerator(Guid.NewGuid());
|
||||||
private const uint EVENT_SYSTEM_FOREGROUND = 3;
|
|
||||||
|
|
||||||
[DllImport("user32.dll")]
|
using var device = deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
|
||||||
static extern IntPtr GetForegroundWindow();
|
using var manager = device.AudioSessionManager2;
|
||||||
|
|
||||||
[DllImport("user32.dll", SetLastError = true)]
|
var sessions = manager!.Sessions;
|
||||||
static extern uint GetWindowThreadProcessId(IntPtr hWnd, out int processId);
|
|
||||||
|
|
||||||
[DllImport("user32.dll")]
|
foreach (var session in sessions!)
|
||||||
static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
|
{
|
||||||
|
var audioProcess = Process.GetProcessById((int)session.ProcessID);
|
||||||
|
|
||||||
private static SimpleAudioVolume GetVolumeObject(Process process)
|
var displayName = audioProcess!.MainModule!.FileVersionInfo.FileDescription;
|
||||||
{
|
|
||||||
var deviceEnumerator = new MMDeviceEnumerator(Guid.NewGuid());
|
|
||||||
|
|
||||||
using var device = deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
|
sb.AppendLine($"pid: {audioProcess.Id}");
|
||||||
using var manager = device.AudioSessionManager2;
|
sb.AppendLine($"\tprocessName: {audioProcess.ProcessName}");
|
||||||
|
sb.AppendLine($"\tsession: {displayName}");
|
||||||
|
}
|
||||||
|
|
||||||
var sessions = manager.Sessions;
|
_tf2.Text = sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var session in sessions)
|
|
||||||
{
|
|
||||||
if (session.ProcessID == process.Id)
|
|
||||||
{
|
|
||||||
return session.SimpleAudioVolume;
|
|
||||||
}
|
|
||||||
|
|
||||||
var audioProcess = Process.GetProcessById((int)session.ProcessID);
|
|
||||||
if(audioProcess?.ProcessName == process.ProcessName)
|
|
||||||
{
|
|
||||||
Console.WriteLine(process.MainModule.FileVersionInfo.FileDescription);
|
|
||||||
return session.SimpleAudioVolume;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
SimpleAudioVolume _current;
|
|
||||||
|
|
||||||
private string GetActiveWindowTitle()
|
|
||||||
{
|
|
||||||
const int nChars = 256;
|
|
||||||
IntPtr handle = IntPtr.Zero;
|
|
||||||
StringBuilder Buff = new StringBuilder(nChars);
|
|
||||||
handle = GetForegroundWindow();
|
|
||||||
|
|
||||||
if (handle <= 0)
|
|
||||||
{
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
var tid = GetWindowThreadProcessId(handle, out var pid);
|
|
||||||
var process = Process.GetProcessById(pid);
|
|
||||||
|
|
||||||
var vol = GetVolumeObject(process);
|
|
||||||
_current = vol;
|
|
||||||
if(vol != null)
|
|
||||||
{
|
|
||||||
vol.Mute = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $"{pid} vol:{vol?.MasterVolume ?? -1} - {process.ProcessName}";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
|
|
||||||
{
|
|
||||||
_tf.Text = GetActiveWindowTitle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -11,4 +11,8 @@
|
|||||||
<PackageReference Include="CoreAudio" Version="1.27.0" />
|
<PackageReference Include="CoreAudio" Version="1.27.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\FocusVolumeControl\FocusVolumeControl.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|