Compare commits

...

4 Commits

Author SHA1 Message Date
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
28 changed files with 794 additions and 826 deletions

View File

@ -1,2 +1,39 @@
# 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.
![Focus volume control plugin preview](previews/preview.png?raw=true)
## Developing
build the solution with visual studio
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/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -1,73 +0,0 @@
using CoreAudio;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FocusVolumeControl
{
public class ActiveAudioSessionWrapper
{
public string DisplayName { get; set; }
public string ExecutablePath { get; set; }
private List<SimpleAudioVolume> Volume { get; } = new List<SimpleAudioVolume>();
public string Icon { get; set; }
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? GetMuted()
{
var muted = Volume.All(x => x.Mute);
var unmuted = Volume.All(x => !x.Mute);
if(muted == !unmuted)
{
return muted;
}
return null;
}
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);
Volume.ForEach(x => x.MasterVolume = level);
}
public int GetVolumeLevel()
{
var level = Volume.FirstOrDefault()?.MasterVolume ?? 0;
return (int)(level * 100);
}
}
}

View File

@ -1,20 +1,18 @@
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
{ {
public class AudioHelper IAudioSession _current;
{
ActiveAudioSessionWrapper _current;
List<Process> _currentProcesses; List<Process> _currentProcesses;
public ActiveAudioSessionWrapper FindSession(List<Process> processes) public IAudioSession FindSession(List<Process> processes)
{ {
var deviceEnumerator = new MMDeviceEnumerator(Guid.NewGuid()); var deviceEnumerator = new MMDeviceEnumerator(Guid.NewGuid());
@ -34,7 +32,7 @@ namespace FocusVolumeControl
try try
{ {
var displayName = audioProcess.MainModule.FileVersionInfo.FileDescription; var displayName = audioProcess.MainModule.FileVersionInfo.FileDescription;
if(string.IsNullOrEmpty(displayName)) if (string.IsNullOrEmpty(displayName))
{ {
displayName = audioProcess.ProcessName; displayName = audioProcess.ProcessName;
} }
@ -54,7 +52,11 @@ namespace FocusVolumeControl
return matchingSession.Any() ? matchingSession : null; return matchingSession.Any() ? matchingSession : null;
} }
public ActiveAudioSessionWrapper GetActiveSession() static object _lock = new object();
public IAudioSession GetActiveSession(FallbackBehavior fallbackBehavior)
{
lock (_lock)
{ {
var processes = GetPossibleProcesses(); var processes = GetPossibleProcesses();
@ -63,9 +65,22 @@ namespace FocusVolumeControl
_current = FindSession(processes); _current = FindSession(processes);
} }
if(_current == null)
{
if(fallbackBehavior == FallbackBehavior.SystemSounds)
{
_current = GetSystemSounds();
}
else if(fallbackBehavior == FallbackBehavior.SystemVolume)
{
_current = GetSystemVolume();
}
}
_currentProcesses = processes; _currentProcesses = processes;
return _current; return _current;
} }
}
/// <summary> /// <summary>
/// Get the list of processes that might be currently selected /// Get the list of processes that might be currently selected
@ -117,5 +132,49 @@ namespace FocusVolumeControl
} }
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);
}
} }

View File

@ -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/pluginIcon.png";
}
}
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);
}
}

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

View File

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

View File

@ -1,46 +1,28 @@
using BarRaider.SdTools; using BarRaider.SdTools;
using BarRaider.SdTools.Payloads; using BarRaider.SdTools.Payloads;
using CoreAudio; using FocusVolumeControl.AudioSessions;
using FocusVolumeControl.UI; 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.ServiceModel.Description;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Threading; using System.Windows.Threading;
namespace FocusVolumeControl namespace FocusVolumeControl;
[PluginActionId("com.dlprows.focusvolumecontrol.dialaction")]
public class DialAction : EncoderBase
{ {
/*
todo:
link both discord processes
steam not detecting
long press reset
option for what to do when on app without sound
*/
[PluginActionId("com.dlprows.focusvolumecontrol.dialaction")]
public class DialAction : EncoderBase
{
private class PluginSettings private class PluginSettings
{ {
[JsonProperty("fallbackBehavior")]
public FallbackBehavior FallbackBehavior { 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;
} }
} }
@ -50,7 +32,7 @@ namespace FocusVolumeControl
IntPtr _foregroundWindowChangedEvent; IntPtr _foregroundWindowChangedEvent;
Native.WinEventDelegate _delegate; Native.WinEventDelegate _delegate;
ActiveAudioSessionWrapper _currentAudioSession; IAudioSession _currentAudioSession;
AudioHelper _audioHelper = new AudioHelper(); AudioHelper _audioHelper = new AudioHelper();
Thread _thread; Thread _thread;
@ -84,6 +66,8 @@ namespace FocusVolumeControl
_thread.SetApartmentState(ApartmentState.STA); _thread.SetApartmentState(ApartmentState.STA);
_thread.Start(); _thread.Start();
_currentAudioSession = settings.FallbackBehavior == FallbackBehavior.SystemSounds ? _audioHelper.GetSystemSounds() : _audioHelper.GetSystemVolume();
} }
public override async void DialDown(DialPayload payload) public override async void DialDown(DialPayload payload)
@ -98,7 +82,7 @@ namespace FocusVolumeControl
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 _audioHelper.ResetAll();
} }
else else
{ {
@ -108,6 +92,9 @@ namespace FocusVolumeControl
async Task ToggleMuteAsync() async Task ToggleMuteAsync()
{ {
try
{
if (_currentAudioSession != null) if (_currentAudioSession != null)
{ {
_currentAudioSession.ToggleMute(); _currentAudioSession.ToggleMute();
@ -118,11 +105,19 @@ namespace FocusVolumeControl
await Connection.ShowAlert(); await Connection.ShowAlert();
} }
} }
catch (Exception ex)
{
await Connection.ShowAlert();
Logger.Instance.LogMessage(TracingLevel.ERROR, $"Unable to toggle mute: {ex.Message}");
}
}
public override async void DialRotate(DialRotatePayload payload) public override async void DialRotate(DialRotatePayload payload)
{ {
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
try
{
if (_currentAudioSession != null) if (_currentAudioSession != null)
{ {
_currentAudioSession.IncrementVolumeLevel(1, payload.Ticks); _currentAudioSession.IncrementVolumeLevel(1, payload.Ticks);
@ -133,6 +128,12 @@ namespace FocusVolumeControl
await Connection.ShowAlert(); 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)
{ {
@ -152,14 +153,10 @@ namespace FocusVolumeControl
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?
}
else
{ {
_currentAudioSession = activeSession; _currentAudioSession = activeSession;
} }
@ -172,7 +169,7 @@ namespace FocusVolumeControl
if (_currentAudioSession != null) if (_currentAudioSession != null)
{ {
var uiState = UIState.Build(_currentAudioSession); var uiState = new UIState(_currentAudioSession);
if ( _previousState != null && uiState != null && if ( _previousState != null && uiState != null &&
uiState.Title == _previousState.Title && uiState.Title == _previousState.Title &&
@ -216,6 +213,4 @@ namespace FocusVolumeControl
} }
}
} }

View File

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

View File

@ -54,13 +54,16 @@
<Reference Include="WindowsBase" /> <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="ParentProcessUtilities.cs" /> <Compile Include="ParentProcessUtilities.cs" />
<Compile Include="PluginAction.cs" />
<Compile Include="Program.cs" /> <Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="UI\UIState.cs" /> <Compile Include="UI\UIState.cs" />
@ -74,42 +77,15 @@
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="Images\categoryIcon%402x.png"> <Content Include="$(SolutionDir)..\previews\**\*" 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">
@ -139,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>

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: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1,14 +1,11 @@
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
{ {
public class Native
{
public delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime); public delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);
[DllImport("user32.dll")] [DllImport("user32.dll")]
@ -59,6 +56,7 @@ namespace FocusVolumeControl
return ids; return ids;
} }
[DllImport("ntdll.dll")]
public static extern int NtQueryInformationProcess(IntPtr processHandle, int processInformationClass, ref ParentProcessUtilities processInformation, int processInformationLength, out int returnLength);
}
} }

View File

@ -1,19 +1,15 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace FocusVolumeControl namespace FocusVolumeControl;
/// <summary>
/// A utility class to determine a process parent.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct ParentProcessUtilities
{ {
/// <summary>
/// A utility class to determine a process parent.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct ParentProcessUtilities
{
// These members must match PROCESS_BASIC_INFORMATION // These members must match PROCESS_BASIC_INFORMATION
internal IntPtr Reserved1; internal IntPtr Reserved1;
internal IntPtr PebBaseAddress; internal IntPtr PebBaseAddress;
@ -22,8 +18,6 @@ namespace FocusVolumeControl
internal IntPtr UniqueProcessId; internal IntPtr UniqueProcessId;
internal IntPtr InheritedFromUniqueProcessId; internal IntPtr InheritedFromUniqueProcessId;
[DllImport("ntdll.dll")]
private static extern int NtQueryInformationProcess(IntPtr processHandle, int processInformationClass, ref ParentProcessUtilities processInformation, int processInformationLength, out int returnLength);
/// <summary> /// <summary>
/// Gets the parent process of specified process. /// Gets the parent process of specified process.
@ -44,7 +38,7 @@ namespace FocusVolumeControl
public static Process GetParentProcess(Process process) public static Process GetParentProcess(Process process)
{ {
var data = new ParentProcessUtilities(); var data = new ParentProcessUtilities();
int status = NtQueryInformationProcess(process.Handle, 0, ref data, Marshal.SizeOf(data), out var returnLength); int status = Native.NtQueryInformationProcess(process.Handle, 0, ref data, Marshal.SizeOf(data), out var returnLength);
if (status != 0) if (status != 0)
{ {
return null; return null;
@ -60,6 +54,4 @@ namespace FocusVolumeControl
} }
} }
}
} }

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,14 +1,9 @@
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 // Uncomment this line of code to allow for debugging
@ -16,5 +11,4 @@ namespace FocusVolumeControl
SDWrapper.Run(args); SDWrapper.Run(args);
} }
}
} }

View File

@ -8,10 +8,24 @@
<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>
<summary>Fallback Details</summary>
<p>If you look at windows volume mixer, you will see that not all applications can have their volume controlled. The fallback behavior controls what happens when you are in an application that doesn't show up in the volume mixer</p>
<p>* System Sounds - Switch to system sounds. This will control windows sound effects such as when an error sound plays. If you're in an application that is making beeping sounds, this will often allow you to control those sounds while leaving things like your music/videos alone</p>
<p>* Previous App - Use the last app that had a volume control. This can result in the stream deck not changing after you have quit an application.</p>
<p>* 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,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

@ -8,11 +8,12 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using BarRaider.SdTools; using BarRaider.SdTools;
using Newtonsoft.Json; using Newtonsoft.Json;
using FocusVolumeControl.AudioSessions;
namespace FocusVolumeControl.UI namespace FocusVolumeControl.UI;
internal class UIState
{ {
internal class UIState
{
[JsonProperty("title")] [JsonProperty("title")]
public string Title { get; private init; } public string Title { get; private init; }
@ -25,41 +26,16 @@ namespace FocusVolumeControl.UI
[JsonProperty("icon")] [JsonProperty("icon")]
public ValueWithOpacity<string> icon { get; private init; } public ValueWithOpacity<string> icon { get; private init; }
public static UIState Build(ActiveAudioSessionWrapper session) public UIState(IAudioSession session)
{ {
var volume = session.GetVolumeLevel(); var volume = session.GetVolumeLevel();
var opacity = session.IsMuted() ? 0.5f : 1;
var iconData = session.GetIcon();
var opacity = session.GetMuted() != true ? 1 : 0.5f; Title = session.DisplayName;
Value = new() { Value = $"{volume}%", Opacity = opacity };
var iconData = ""; Indicator = new() { Value = volume, Opacity = opacity };
icon = new() { Value = iconData, Opacity = opacity };
if (session.Icon != null)
{
iconData = session.Icon;
}
else
{
try
{
var icon = Icon.ExtractAssociatedIcon(session.ExecutablePath);
iconData = Tools.ImageToBase64(icon.ToBitmap(), true);
}
catch
{
iconData = "Image/pluginIcon.png";
}
session.Icon = iconData;
} }
return new UIState()
{
Title = session.DisplayName,
Value = new() { Value = $"{volume}%", Opacity = opacity },
Indicator = new() { Value = volume, Opacity = opacity },
icon = new() { Value = iconData, Opacity = opacity },
};
}
}
} }

View File

@ -1,20 +1,13 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Data.SqlTypes;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace FocusVolumeControl.UI namespace FocusVolumeControl.UI;
internal class ValueWithOpacity<T>
{ {
internal class ValueWithOpacity<T>
{
[JsonProperty("value")] [JsonProperty("value")]
public required T Value { get; init; } public required T Value { get; init; }
[JsonProperty("opacity")] [JsonProperty("opacity")]
public required float Opacity { get; init; } public required float Opacity { get; init; }
}
} }

View File

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

View File

@ -1,31 +1,17 @@
using CoreAudio; using CoreAudio;
using FocusVolumeControl; using FocusVolumeControl;
using System; using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Management;
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
/// </summary>
public partial class MainWindow : Window
{
AudioHelper _audioHelper; AudioHelper _audioHelper;
Native.WinEventDelegate _delegate; Native.WinEventDelegate _delegate;
@ -91,7 +77,6 @@ namespace SoundBrowser
sb.AppendLine("picked the following best match"); sb.AppendLine("picked the following best match");
sb.AppendLine($"\tsession: {session.DisplayName}"); sb.AppendLine($"\tsession: {session.DisplayName}");
sb.AppendLine($"\tvolume: {session.GetVolumeLevel()}"); sb.AppendLine($"\tvolume: {session.GetVolumeLevel()}");
sb.AppendLine($"\tcount: {session.Count}");
} }
else else
{ {
@ -130,6 +115,4 @@ namespace SoundBrowser
} }
}
} }

View File

@ -1,65 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace SoundBrowser
{
/// <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;
[DllImport("ntdll.dll")]
private static extern int NtQueryInformationProcess(IntPtr processHandle, int processInformationClass, ref ParentProcessUtilities processInformation, int processInformationLength, out int returnLength);
/// <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)
{
Process process = Process.GetProcessById(id);
return GetParentProcess(process.Handle);
}
/// <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(IntPtr handle)
{
ParentProcessUtilities pbi = new ParentProcessUtilities();
int returnLength;
int status = NtQueryInformationProcess(handle, 0, ref pbi, Marshal.SizeOf(pbi), out returnLength);
if (status != 0)
throw new Win32Exception(status);
try
{
return Process.GetProcessById(pbi.InheritedFromUniqueProcessId.ToInt32());
}
catch (ArgumentException)
{
// not found
return null;
}
}
}
}