19 Commits

Author SHA1 Message Date
f9b23a62a3 Clean up dial action to make sure all top level code paths have a try catch so the plugin won't crash 2023-09-13 20:46:55 -06:00
06266daa92 Fixed performance issue for system sounds and main volume 2023-09-13 20:46:21 -06:00
1fea2a2e11 Made the display update sooner when launching steam deck software 2023-09-10 22:16:16 -06:00
2e44a27b2b Update manifest 2023-09-10 22:10:00 -06:00
84a9a89074 Adding in a step picker, so that the user can choose how much the volume should change with each tick of the encoder 2023-09-10 22:07:11 -06:00
b57ea24b11 Fixing an issue with rounding causing certain volume levels to not show the same on the steam deck as they did in the volume mixer 2023-09-10 22:06:38 -06:00
0d056215bc Move try catch, because i had issues with it throwing when doing the tick - oops 2023-09-10 22:05:11 -06:00
f0a5a48c73 Fix an issue where sometimes the window proc would throw exceptions and crash the plugin 2023-08-31 22:04:08 -06:00
cdf05d2913 fix application name 2023-08-21 23:31:25 -06:00
8b87ea39cc fix version in manifest 2023-08-21 23:26:24 -06:00
2f410f431e updating icons to match style guide. update preview 2023-08-21 23:20:24 -06:00
709ed8a89a Update manifest to refer to github 2023-08-20 22:16:50 -06:00
29da6a8d17 for some reason the cheveron is missing from the details so i removed the summary 2023-08-20 22:13:47 -06:00
07ddd3bf89 adding readme and previews 2023-08-20 21:59:58 -06:00
65ad532adb rewrite details in property inspector. make system sounds the default fallback behavior 2023-08-20 21:19:33 -06:00
b4f4bea0fc Delete dead js file 2023-08-20 21:18:30 -06:00
90c014e932 Add settings for fallback behavior
update action icon with padding
2023-08-20 20:52:48 -06:00
1dc8ab8a2d Add distribution tool 2023-08-06 21:55:40 -06:00
a429a435bc Rewrite how picking a matching audio session works
rewrite the UI layer to make it only send updates to the stream deck if needed
2023-08-06 21:51:04 -06:00
52 changed files with 3250 additions and 597 deletions

18
LICENSE
View File

@ -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.

View File

@ -1,2 +1,40 @@
# Focus Volume Control Plugin # 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.
![Focus volume control plugin preview](previews/1-preview.png?raw=true)
## 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

BIN
previews/1-preview.xcf Normal file

Binary file not shown.

91
src/.editorconfig Normal file
View 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

View File

@ -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; }
}
}

View File

@ -1,17 +1,164 @@
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;
public IAudioSession FindSession(List<Process> processes)
{ {
ActiveAudioSessionWrapper GetSessionForProcess(Process process) var deviceEnumerator = new MMDeviceEnumerator(Guid.NewGuid());
using var device = deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
using var manager = device.AudioSessionManager2;
var sessions = manager.Sessions;
var matchingSession = new ActiveAudioSessionWrapper();
foreach (var session in sessions)
{
var audioProcess = Process.GetProcessById((int)session.ProcessID);
if (processes.Any(x => x.Id == session.ProcessID || x.ProcessName == audioProcess?.ProcessName))
{
try
{
var displayName = audioProcess.MainModule.FileVersionInfo.FileDescription;
if (string.IsNullOrEmpty(displayName))
{
displayName = audioProcess.ProcessName;
}
matchingSession.DisplayName = displayName;
}
catch
{
matchingSession.DisplayName ??= audioProcess.ProcessName;
}
matchingSession.ExecutablePath ??= audioProcess.MainModule.FileName;
//some apps like discord have multiple volume processes.
matchingSession.AddVolume(session.SimpleAudioVolume);
}
}
return matchingSession.Any() ? matchingSession : null;
}
static object _lock = new object();
public void ResetCache()
{
lock(_lock)
{
_current = null;
}
}
public IAudioSession GetActiveSession(FallbackBehavior fallbackBehavior)
{
lock (_lock)
{
var processes = GetPossibleProcesses();
if (_currentProcesses == null || !_currentProcesses.SequenceEqual(processes))
{
_current = FindSession(processes);
}
if(_current == null)
{
if(fallbackBehavior == FallbackBehavior.SystemSounds && _current is not SystemSoundsAudioSession)
{
_current = GetSystemSounds();
}
else if(fallbackBehavior == FallbackBehavior.SystemVolume && _current is not SystemVolumeAudioSession)
{
_current = GetSystemVolume();
}
}
_currentProcesses = processes;
return _current;
}
}
/// <summary>
/// Get the list of processes that might be currently selected
/// This includes getting the child window's processes
///
/// This helps to find the audo process for windows store apps whose process is "ApplicationFrameHost.exe"
///
/// The list may optionally include a parent process, because that helps thing steam to be more reliable because the steamwebhelper (ui) is a child of steam.exe
///
/// According to deej, getting the ForegroundWindow and enumerating steam windows should work, but it doesn't seem to work for me without including the parent process
/// https://github.com/omriharel/deej/blob/master/pkg/deej/util/util_windows.go#L22
///
/// but the parent process is sometimes useless (explorer, svchost, etc) so i filter some of them out because i felt like it when i wrote the code
///
/// I also experimented with grabbing the parent process and enumerating through the windows to see if that would help, but any time the parent process was an unexpected process (explorer) it could blow up. so i decided not to bother for now
/// </summary>
/// <returns></returns>
public List<Process> GetPossibleProcesses()
{
var handle = Native.GetForegroundWindow();
if (handle == IntPtr.Zero)
{
return null;
}
var ids = Native.GetProcessesOfChildWindows(handle);
Native.GetWindowThreadProcessId(handle, out var pid);
ids.Insert(0, pid);
var processes = ids.Distinct()
.Select(x => Process.GetProcessById(x))
.ToList();
try
{
var blah = ParentProcessUtilities.GetParentProcess(pid);
if (blah != null && blah.ProcessName != "explorer" && blah.ProcessName != "svchost")
{
processes.Add(blah);
}
}
catch
{
}
return processes;
}
public void ResetAll()
{
try
{
var deviceEnumerator = new MMDeviceEnumerator(Guid.NewGuid());
using var device = deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
using var manager = device.AudioSessionManager2;
foreach (var session in manager.Sessions)
{
session.SimpleAudioVolume.MasterVolume = 1;
session.SimpleAudioVolume.Mute = false;
}
}
catch { }
}
public IAudioSession GetSystemSounds()
{ {
var deviceEnumerator = new MMDeviceEnumerator(Guid.NewGuid()); var deviceEnumerator = new MMDeviceEnumerator(Guid.NewGuid());
@ -22,42 +169,20 @@ namespace FocusVolumeControl
foreach (var session in sessions) foreach (var session in sessions)
{ {
var audioProcess = Process.GetProcessById((int)session.ProcessID); if (session.IsSystemSoundsSession)
{
return new SystemSoundsAudioSession(session.SimpleAudioVolume);
}
}
if (session.ProcessID == process.Id || audioProcess?.ProcessName == process.ProcessName)
{
var displayName = audioProcess.MainModule.FileVersionInfo.FileDescription;
var path = audioProcess.MainModule.FileName;
return new ActiveAudioSessionWrapper()
{
DisplayName = displayName,
ExecutablePath = path,
Volume = session.SimpleAudioVolume
};
}
}
return null; return null;
} }
public IAudioSession GetSystemVolume()
internal ActiveAudioSessionWrapper GetActiveSession()
{ {
const int nChars = 256; var deviceEnumerator = new MMDeviceEnumerator(Guid.NewGuid());
IntPtr handle = IntPtr.Zero;
StringBuilder Buff = new StringBuilder(nChars);
handle = Native.GetForegroundWindow();
if (handle == IntPtr.Zero) using var device = deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
{ return new SystemVolumeAudioSession(device.AudioEndpointVolume);
//todo: return system or something like that?
return null;
} }
var tid = Native.GetWindowThreadProcessId(handle, out var pid);
var process = Process.GetProcessById(pid);
return GetSessionForProcess(process);
}
}
} }

View File

@ -0,0 +1,78 @@
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 = VolumeHelpers.GetAdjustedVolume(level, step, ticks);
Volume.ForEach(x => x.MasterVolume = level);
}
public int GetVolumeLevel()
{
var level = Volume.FirstOrDefault()?.MasterVolume ?? 0;
return VolumeHelpers.GetVolumePercentage(level);
}
}

View 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();
}

View File

@ -0,0 +1,33 @@
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 = VolumeHelpers.GetAdjustedVolume(_volumeControl.MasterVolume, step, ticks);
_volumeControl.MasterVolume = level;
}
public int GetVolumeLevel() => VolumeHelpers.GetVolumePercentage(_volumeControl.MasterVolume);
}

View File

@ -0,0 +1,33 @@
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 = VolumeHelpers.GetAdjustedVolume(_volumeControl.MasterVolumeLevelScalar, step, ticks);
_volumeControl.MasterVolumeLevelScalar = level;
}
public int GetVolumeLevel() => VolumeHelpers.GetVolumePercentage(_volumeControl.MasterVolumeLevelScalar);
}

View 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);
}
}

View File

@ -1,54 +1,48 @@
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;
{
/*
todo:
link both discord processes
steam not detecting
long press reset
option for what to do when on app without sound
gitea
*/
[PluginActionId("com.dlprows.focusvolumecontrol.dialaction")] [PluginActionId("com.dlprows.focusvolumecontrol.dialaction")]
public class DialAction : EncoderBase public class DialAction : EncoderBase
{ {
private class PluginSettings private class PluginSettings
{ {
[JsonProperty("fallbackBehavior")]
public FallbackBehavior FallbackBehavior { get; set; }
[JsonProperty("stepSize")]
public int StepSize { get; set; }
public static PluginSettings CreateDefaultSettings() public static PluginSettings CreateDefaultSettings()
{ {
PluginSettings instance = new PluginSettings(); PluginSettings instance = new PluginSettings();
instance.FallbackBehavior = FallbackBehavior.SystemSounds;
return instance; return instance;
} }
} }
private PluginSettings settings; private PluginSettings settings;
//IntPtr _foregroundWindowChangedEvent;
//WinEventDelegate _delegate; IntPtr _foregroundWindowChangedEvent;
ActiveAudioSessionWrapper _currentAudioSession; Native.WinEventDelegate _delegate;
IAudioSession _currentAudioSession;
AudioHelper _audioHelper = new AudioHelper(); AudioHelper _audioHelper = new AudioHelper();
Thread _thread;
Dispatcher _dispatcher;
UIState _previousState;
public DialAction(ISDConnection connection, InitialPayload payload) : base(connection, payload) public DialAction(ISDConnection connection, InitialPayload payload) : base(connection, payload)
{ {
if (payload.Settings == null || payload.Settings.Count == 0) if (payload.Settings == null || payload.Settings.Count == 0)
@ -61,101 +55,178 @@ namespace FocusVolumeControl
settings = payload.Settings.ToObject<PluginSettings>(); settings = payload.Settings.ToObject<PluginSettings>();
} }
//_delegate = new WinEventDelegate(WinEventProc); _thread = new Thread(() =>
//_foregroundWindowChangedEvent = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, IntPtr.Zero, _delegate, 0, 0, WINEVENT_OUTOFCONTEXT); {
Logger.Instance.LogMessage(TracingLevel.DEBUG, "Registering for events");
_delegate = new Native.WinEventDelegate(WinEventProc);
_foregroundWindowChangedEvent = Native.RegisterForForegroundWindowChangedEvent(_delegate);
Logger.Instance.LogMessage(TracingLevel.DEBUG, "Starting Dispatcher");
_dispatcher = Dispatcher.CurrentDispatcher;
Dispatcher.Run();
Logger.Instance.LogMessage(TracingLevel.DEBUG, "Dispatcher Stopped");
});
_thread.SetApartmentState(ApartmentState.STA);
_thread.Start();
_currentAudioSession = settings.FallbackBehavior == FallbackBehavior.SystemSounds ? _audioHelper.GetSystemSounds() : _audioHelper.GetSystemVolume();
_ = UpdateStateIfNeeded();
}
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) public override async void DialDown(DialPayload payload)
{ {
//dial pressed down try
{
Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Down"); Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Down");
if(_currentAudioSession != null) await ToggleMuteAsync();
}
catch (Exception ex)
{ {
_currentAudioSession.Volume.Mute = !_currentAudioSession.Volume.Mute; Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unexpected Error in DialDown:\n {ex}");
var uiState = UIState.Build(_currentAudioSession);
await Connection.SetFeedbackAsync(uiState);
}
else
{
await Connection.ShowAlert();
} }
} }
public override void DialUp(DialPayload payload) { }
public override async void TouchPress(TouchpadPressPayload payload) public override async void TouchPress(TouchpadPressPayload payload)
{
try
{ {
Logger.Instance.LogMessage(TracingLevel.INFO, "Touch Press"); Logger.Instance.LogMessage(TracingLevel.INFO, "Touch Press");
if (payload.IsLongPress) if (payload.IsLongPress)
{ {
//todo: iterate through all sessions setting them back to 100 except the master volume await ResetAllAsync();
} }
else else
{ {
await ToggleMuteAsync();
if (_currentAudioSession != null)
{
_currentAudioSession.Volume.Mute = !_currentAudioSession.Volume.Mute;
var uiState = UIState.Build(_currentAudioSession);
await Connection.SetFeedbackAsync(uiState);
} }
else
{
await Connection.ShowAlert();
} }
catch (Exception ex)
{
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unexpected Error in TouchPress:\n {ex}");
} }
} }
public override async void DialRotate(DialRotatePayload payload) public override async void DialRotate(DialRotatePayload payload)
{
try
{ {
Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Rotate"); Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Rotate");
//dial rotated. ticks positive for right, negative for left //dial rotated. ticks positive for right, negative for left
if (_currentAudioSession != null) if (_currentAudioSession != null)
{ {
_currentAudioSession.Volume.MasterVolume += (0.01f) * payload.Ticks; _currentAudioSession.IncrementVolumeLevel(settings.StepSize, payload.Ticks);
await UpdateStateIfNeeded();
var uiState = UIState.Build(_currentAudioSession);
await Connection.SetFeedbackAsync(uiState);
} }
else else
{ {
await Connection.ShowAlert(); await Connection.ShowAlert();
} }
} }
catch (Exception ex)
public override void DialUp(DialPayload payload)
{ {
//dial unpressed _audioHelper.ResetCache();
Logger.Instance.LogMessage(TracingLevel.INFO, "Dial Up"); await Connection.ShowAlert();
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unable to increment volume:\n {ex}");
}
} }
public override void Dispose() async Task ResetAllAsync()
{ {
/* try
if(_foregroundWindowChangedEvent != IntPtr.Zero)
{ {
Native.UnhookWinEvent(_foregroundWindowChangedEvent); _audioHelper.ResetAll();
}
catch
{
_audioHelper.ResetCache();
await Connection.ShowAlert();
throw;
}
}
async Task ToggleMuteAsync()
{
try
{
if (_currentAudioSession != null)
{
_currentAudioSession.ToggleMute();
await UpdateStateIfNeeded();
}
else
{
await Connection.ShowAlert();
}
}
catch
{
_audioHelper.ResetCache();
await Connection.ShowAlert();
throw;
} }
*/
} }
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 try
var activeSession = _audioHelper.GetActiveSession();
if (activeSession == null)
{ {
//todo: something? //called once every 1000ms and can be used for updating the title/image of the key
} var activeSession = _audioHelper.GetActiveSession(settings.FallbackBehavior);
else
if (activeSession != null)
{ {
_currentAudioSession = activeSession; _currentAudioSession = activeSession;
} }
await UpdateStateIfNeeded();
}
catch (Exception ex)
{
_audioHelper.ResetCache();
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Exception on Tick:\n {ex}");
}
}
private async Task UpdateStateIfNeeded()
{
try
{
if (_currentAudioSession != null) if (_currentAudioSession != null)
{ {
var uiState = UIState.BuildWithImage(_currentAudioSession);
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); await Connection.SetFeedbackAsync(uiState);
_previousState = uiState;
}
}
catch (Exception ex)
{
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Failed to update screen\n {ex}");
} }
} }
@ -166,38 +237,40 @@ namespace FocusVolumeControl
public override void ReceivedSettings(ReceivedSettingsPayload payload) public override void ReceivedSettings(ReceivedSettingsPayload payload)
{
try
{ {
Tools.AutoPopulateSettings(settings, payload.Settings); Tools.AutoPopulateSettings(settings, payload.Settings);
SaveSettings(); SaveSettings();
} }
catch (Exception ex)
private Task SaveSettings()
{ {
return Connection.SetSettingsAsync(JObject.FromObject(settings)); Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unexpected Error in SaveSettings:\n {ex}");
}
} }
private async Task SaveSettings()
/*
public async void WinEventProc(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
{ {
var activeSession = _audioHelper.GetActiveSession(); try
if(activeSession == null)
{ {
//todo: something? await Connection.SetSettingsAsync(JObject.FromObject(settings));
} }
else catch (Exception ex)
{ {
_currentAudioSession = activeSession; Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unexpected Error in SaveSettings:\n {ex}");
//populate the UI
await Connection.SetTitleAsync(_currentAudioSession.DisplayName);
} }
} }
*/
}
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}");
}
}
} }

Binary file not shown.

View File

@ -0,0 +1,8 @@
namespace FocusVolumeControl;
public enum FallbackBehavior
{
SystemSounds,
PreviousApp,
SystemVolume
}

View File

@ -51,17 +51,24 @@
<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\VolumeHelpers.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 +78,39 @@
</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> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
<Content Include="Images\actionIcon%402x.png"> <Content Include="PropertyInspector\**\*.js;PropertyInspector\**\*.css">
<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 +119,4 @@
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<PostBuildEvent>
</PostBuildEvent>
</PropertyGroup>
</Project> </Project>

View File

@ -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));
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 897 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -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);
} }
}

View 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;
}
}
}

View File

@ -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
}
}

View File

@ -1,12 +1,7 @@
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)
@ -17,4 +12,3 @@ namespace FocusVolumeControl
SDWrapper.Run(args); SDWrapper.Run(args);
} }
} }
}

View File

@ -6,12 +6,41 @@
<meta name=apple-mobile-web-app-capable content=yes> <meta name=apple-mobile-web-app-capable content=yes>
<meta name=apple-mobile-web-app-status-bar-style content=black> <meta name=apple-mobile-web-app-status-bar-style content=black>
<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="./lib/sdpi.css">
<script src="https://cdn.jsdelivr.net/gh/barraider/streamdeck-easypi@latest/src/sdtools.common.js"></script> <link rel="sytlesheet" href="./lib/rangeTooltip.css">
<script src="PluginActionPI.js"></script> <script src="lib/sdtools.common.js"></script>
<script src="lib/rangeTooltip.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>
<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> </div>
</body> </body>
</html> </html>

View File

@ -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: "%";
}

View 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
);
});
}

File diff suppressed because it is too large Load Diff

View 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);
}

View 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));
}
}

View 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 };
}
}

View 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; }
}

View File

@ -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;
}
}
}

View File

@ -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.1.0",
"CodePath": "FocusVolumeControl", "CodePath": "FocusVolumeControl",
"Category": "Volume Control [dlprows]", "Category": "Volume Control [dlprows]",
"Icon": "Images/pluginIcon", "Icon": "Images/pluginIcon",

View File

@ -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>

View File

@ -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> /// <summary>
/// Interaction logic for MainWindow.xaml /// Interaction logic for MainWindow.xaml
/// </summary> /// </summary>
public partial class MainWindow : Window public partial class MainWindow : Window
{ {
AudioHelper _audioHelper;
Native.WinEventDelegate _delegate;
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
dele = new WinEventDelegate(WinEventProc); _audioHelper = new AudioHelper();
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();
delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime); if (session != null)
[DllImport("user32.dll")]
static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags);
private const uint WINEVENT_OUTOFCONTEXT = 0;
private const uint EVENT_SYSTEM_FOREGROUND = 3;
[DllImport("user32.dll")]
static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll", SetLastError = true)]
static extern uint GetWindowThreadProcessId(IntPtr hWnd, out int processId);
[DllImport("user32.dll")]
static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
private static SimpleAudioVolume GetVolumeObject(Process process)
{ {
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()); var deviceEnumerator = new MMDeviceEnumerator(Guid.NewGuid());
using var device = deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia); using var device = deviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
using var manager = device.AudioSessionManager2; using var manager = device.AudioSessionManager2;
var sessions = manager.Sessions; var sessions = manager!.Sessions;
foreach (var session in sessions) foreach (var session in sessions!)
{ {
if (session.ProcessID == process.Id)
{
return session.SimpleAudioVolume;
}
var audioProcess = Process.GetProcessById((int)session.ProcessID); var audioProcess = Process.GetProcessById((int)session.ProcessID);
if(audioProcess?.ProcessName == process.ProcessName)
{ var displayName = audioProcess!.MainModule!.FileVersionInfo.FileDescription;
Console.WriteLine(process.MainModule.FileVersionInfo.FileDescription);
return session.SimpleAudioVolume; sb.AppendLine($"pid: {audioProcess.Id}");
sb.AppendLine($"\tprocessName: {audioProcess.ProcessName}");
sb.AppendLine($"\tsession: {displayName}");
} }
}
return null; _tf2.Text = sb.ToString();
} }
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();
}
}
} }

View File

@ -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>