Skip to content

Commit 676b152

Browse files
authored
feat: add raw commands and adjustments (#27)
This adds a way to add fully configurable commands and adjustments resolves #18 This should also, in theory, resolve #22 because the Macro Buttons don't do anything else as the RawCommand. It also resolves #23 (with the raw adjustment, this can me done without any issues)
1 parent 5543467 commit 676b152

File tree

5 files changed

+403
-8
lines changed

5 files changed

+403
-8
lines changed

src/VoiceMeeterPlugin/Actions/Bases/SingleBaseAdjustment.cs

+6
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,12 @@ protected override BitmapImage GetAdjustmentImage(String actionParameter, Plugin
191191
return DrawingHelper.DrawVolumeBar(imageSize, backgroundColor.ToBitmapColor(), BitmapColor.White, value, this.MinValue, this.MaxValue, this.ScaleFactor, this.Command, name);
192192
}
193193

194+
protected override BitmapImage GetCommandImage(String actionParameter, PluginImageSize imageSize) => this.GetAdjustmentImage(actionParameter, imageSize);
195+
196+
protected override Double? GetAdjustmentMinValue(String actionParameter) => this.MinValue;
197+
198+
protected override Double? GetAdjustmentMaxValue(String actionParameter) => this.MaxValue;
199+
194200
private Int32 GetButton(String actionParameter)
195201
{
196202
var number = actionParameter.Replace("VM-Strip", "").Replace($"-{this.Command}", "");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
// This file is part of the VoiceMeeterPlugin project.
2+
//
3+
// Copyright (c) 2024 Dominic Ris
4+
//
5+
// Permission is hereby granted, free of charge, to any person obtaining a copy
6+
// of this software and associated documentation files (the "Software"), to deal
7+
// in the Software without restriction, including without limitation the rights
8+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
// copies of the Software, and to permit persons to whom the Software is
10+
// furnished to do so, subject to the following conditions:
11+
//
12+
// The above copyright notice and this permission notice shall be included in all
13+
// copies or substantial portions of the Software.
14+
//
15+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
18+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
// SOFTWARE.
22+
23+
namespace Loupedeck.VoiceMeeterPlugin.Actions;
24+
25+
using System.Globalization;
26+
using System.Reactive.Linq;
27+
using System.Reactive.Subjects;
28+
29+
using Extensions;
30+
31+
using Helpers;
32+
33+
using Library.Voicemeeter;
34+
35+
using Services;
36+
37+
using SkiaSharp;
38+
39+
public class RawAdjustment : ActionEditorAdjustment
40+
{
41+
private VoiceMeeterService VmService { get; }
42+
private Subject<Boolean> OnDestroy { get; } = new();
43+
44+
public RawAdjustment() : base(false)
45+
{
46+
this.DisplayName = "Raw Adjustment";
47+
this.Description = "Adjusts a raw value";
48+
49+
this.ActionEditor.AddControlEx(
50+
new ActionEditorTextbox("name", "Display Name", "Name displayed on the device itself").SetRequired()
51+
);
52+
this.ActionEditor.AddControlEx(
53+
new ActionEditorTextbox("api", "API", "The \"API\" to adjust, example: Strip[0].Gain").SetRequired()
54+
);
55+
this.ActionEditor.AddControlEx(
56+
new ActionEditorTextbox("steps", "Steps", "The steps to adjust the API by").SetRequired().SetFormat(ActionEditorTextboxFormat.Integer).SetRegex(@"^-?\d+(\.\d+)?$")
57+
);
58+
this.ActionEditor.AddControlEx(
59+
new ActionEditorTextbox("min", "Min Value", "The minimum value that can be reached").SetRequired().SetFormat(ActionEditorTextboxFormat.Integer).SetRegex(@"^-?\d+(\.\d+)?$")
60+
);
61+
this.ActionEditor.AddControlEx(
62+
new ActionEditorTextbox("max", "Max Value", "The maximum value that can be reached").SetRequired().SetFormat(ActionEditorTextboxFormat.Integer).SetRegex(@"^-?\d+(\.\d+)?$")
63+
);
64+
this.ActionEditor.AddControlEx(
65+
new ActionEditorTextbox("bgcolor", "Background Color", "The color it should use in hex (#rrggbb example: #FF0000 = red)").SetRegex("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$")
66+
);
67+
68+
this.ActionEditor.AddControlEx(
69+
new ActionEditorTextbox("fgcolor", "Foreground Color", "The color it should use in hex (#rrggbb example: #FF0000 = red)").SetRegex("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$")
70+
);
71+
72+
this.VmService = VoiceMeeterService.Instance;
73+
}
74+
75+
protected override Boolean OnLoad()
76+
{
77+
this.VmService.Parameters
78+
.TakeUntil(this.OnDestroy)
79+
.Subscribe(_ => this.AdjustmentValueChanged());
80+
81+
return base.OnLoad();
82+
}
83+
84+
protected override Boolean OnUnload()
85+
{
86+
this.OnDestroy.OnNext(true);
87+
return base.OnUnload();
88+
}
89+
90+
protected override BitmapImage GetCommandImage(ActionEditorActionParameters actionParameters, Int32 imageWidth, Int32 imageHeight) =>
91+
this.GetAdjustmentImage(actionParameters, imageWidth, imageHeight);
92+
93+
protected override BitmapImage GetAdjustmentImage(ActionEditorActionParameters actionParameters, Int32 imageWidth, Int32 imageHeight)
94+
{
95+
Tuple<String, String, Single, Int32, Int32, SKColor, SKColor> parameters;
96+
try
97+
{
98+
parameters = GetParameters(actionParameters);
99+
}
100+
catch (Exception)
101+
{
102+
return null;
103+
}
104+
105+
if (parameters == null)
106+
{
107+
return null;
108+
}
109+
110+
var (name, api, steps, min, max, bgColor, fgColor) = parameters;
111+
112+
var currentValue = -9999f;
113+
114+
try
115+
{
116+
currentValue = Remote.GetParameter(api);
117+
}
118+
catch (Exception)
119+
{
120+
// ignore
121+
}
122+
123+
var decimalPlaces = GetDecimalPlaces(steps);
124+
currentValue = (Single)Math.Round(currentValue, decimalPlaces);
125+
126+
127+
return DrawingHelper.DrawVolumeBar(PluginImageSize.Width60, bgColor.ToBitmapColor(), fgColor.ToBitmapColor(), currentValue, min, max, 1, "", name);
128+
}
129+
130+
protected override Boolean ApplyAdjustment(ActionEditorActionParameters actionParameters, Int32 diff)
131+
{
132+
Tuple<String, String, Single, Int32, Int32, SKColor, SKColor> parameters;
133+
try
134+
{
135+
parameters = GetParameters(actionParameters);
136+
}
137+
catch (Exception)
138+
{
139+
return false;
140+
}
141+
142+
if (parameters == null)
143+
{
144+
return false;
145+
}
146+
147+
var (_, api, steps, min, max, _, _) = parameters;
148+
149+
try
150+
{
151+
var currentValue = Remote.GetParameter(api);
152+
var newValue = currentValue + diff * steps;
153+
if (newValue < min)
154+
{
155+
newValue = min;
156+
}
157+
else if (newValue > max)
158+
{
159+
newValue = max;
160+
}
161+
Remote.SetParameter(api, newValue);
162+
}
163+
catch (Exception)
164+
{
165+
return false;
166+
}
167+
168+
return true;
169+
}
170+
171+
private static Tuple<String, String, Single, Int32, Int32, SKColor, SKColor> GetParameters(ActionEditorActionParameters actionParameters)
172+
{
173+
actionParameters.TryGetString("name", out var name);
174+
actionParameters.TryGetString("api", out var api);
175+
actionParameters.TryGetString("steps", out var value);
176+
actionParameters.TryGetString("min", out var min);
177+
actionParameters.TryGetString("max", out var max);
178+
actionParameters.TryGetString("bgcolor", out var bgColor);
179+
actionParameters.TryGetString("fgcolor", out var fgColor);
180+
181+
return new Tuple<String, String, Single, Int32, Int32, SKColor, SKColor>(
182+
String.IsNullOrEmpty(name) ? "Unknown" : name,
183+
String.IsNullOrEmpty(api) ? "Strip[1].Gain" : api,
184+
Single.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var val) ? val : 1,
185+
Int32.TryParse(min, NumberStyles.Any, CultureInfo.InvariantCulture, out var minValue) ? minValue : 0,
186+
Int32.TryParse(max, NumberStyles.Any, CultureInfo.InvariantCulture, out var maxValue) ? maxValue : 100,
187+
SKColor.TryParse(bgColor, out var bg) ? bg : ColorHelper.Inactive,
188+
SKColor.TryParse(fgColor, out var fg) ? fg : SKColors.White);
189+
}
190+
191+
private static Int32 GetDecimalPlaces(Single steps)
192+
{
193+
var str = steps.ToString(CultureInfo.InvariantCulture);
194+
var index = str.IndexOf('.');
195+
return index == -1 ? 0 : str.Length - index - 1;
196+
}
197+
}
+181
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
// This file is part of the VoiceMeeterPlugin project.
2+
//
3+
// Copyright (c) 2024 Dominic Ris
4+
//
5+
// Permission is hereby granted, free of charge, to any person obtaining a copy
6+
// of this software and associated documentation files (the "Software"), to deal
7+
// in the Software without restriction, including without limitation the rights
8+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
// copies of the Software, and to permit persons to whom the Software is
10+
// furnished to do so, subject to the following conditions:
11+
//
12+
// The above copyright notice and this permission notice shall be included in all
13+
// copies or substantial portions of the Software.
14+
//
15+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
18+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
// SOFTWARE.
22+
23+
namespace Loupedeck.VoiceMeeterPlugin.Actions;
24+
25+
using System.Reactive.Linq;
26+
using System.Reactive.Subjects;
27+
28+
using Helpers;
29+
30+
using Library.Voicemeeter;
31+
32+
using Services;
33+
34+
using SkiaSharp;
35+
36+
public class RawCommand : MultistateActionEditorCommand
37+
{
38+
private VoiceMeeterService VmService { get; }
39+
private Subject<Boolean> OnDestroy { get; } = new();
40+
41+
public RawCommand()
42+
{
43+
this.DisplayName = "Raw Command";
44+
this.Description = "Toggle or execute an action";
45+
46+
this.ActionEditor.AddControlEx(
47+
new ActionEditorTextbox("name", "Display Name", "Name displayed on the device itself").SetRequired()
48+
);
49+
this.ActionEditor.AddControlEx(
50+
new ActionEditorTextbox("api", "API", "The \"API\" to adjust, example: Strip[0].Gain").SetRequired()
51+
);
52+
this.ActionEditor.AddControlEx(
53+
new ActionEditorTextbox("oncolor", "On Color", "The color it should use in hex (#rrggbb example: #FF0000 = red)").SetRegex("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$")
54+
);
55+
this.ActionEditor.AddControlEx(
56+
new ActionEditorTextbox("offcolor", "Off Color", "The color it should use in hex (#rrggbb example: #FF0000 = red)").SetRegex("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$")
57+
);
58+
59+
this.VmService = VoiceMeeterService.Instance;
60+
this.AddState("Off", "If the action is off");
61+
this.AddState("On", "If the action is on");
62+
}
63+
64+
protected override Boolean OnLoad()
65+
{
66+
this.VmService.Parameters
67+
.TakeUntil(this.OnDestroy)
68+
.Subscribe(_ => this.AdjustmentValueChanged());
69+
70+
return base.OnLoad();
71+
}
72+
73+
protected override Boolean OnUnload()
74+
{
75+
this.OnDestroy.OnNext(true);
76+
return base.OnUnload();
77+
}
78+
79+
protected override String GetCommandDisplayName(ActionEditorActionParameters actionParameters, Int32 stateIndex)
80+
{
81+
Tuple<String, String, SKColor, SKColor> parameters;
82+
try
83+
{
84+
parameters = GetParameters(actionParameters);
85+
}
86+
catch (Exception)
87+
{
88+
return "Unknown";
89+
}
90+
91+
if (parameters == null)
92+
{
93+
return "Unknown";
94+
}
95+
96+
var (name, _, _, _) = parameters;
97+
98+
return $"{name} - {(stateIndex == 0 ? "Off" : "On")}";
99+
}
100+
101+
protected override BitmapImage GetCommandImage(ActionEditorActionParameters actionParameters, Int32 stateIndex, Int32 imageWidth, Int32 imageHeight)
102+
{
103+
Tuple<String, String, SKColor, SKColor> parameters;
104+
try
105+
{
106+
parameters = GetParameters(actionParameters);
107+
}
108+
catch (Exception)
109+
{
110+
return null;
111+
}
112+
113+
if (parameters == null)
114+
{
115+
return null;
116+
}
117+
118+
var (name, api, onColor, offColor) = parameters;
119+
120+
var currentValue = false;
121+
122+
try
123+
{
124+
currentValue = (Int32)Remote.GetParameter(api) == 1;
125+
}
126+
catch (Exception)
127+
{
128+
// ignore
129+
}
130+
131+
132+
return DrawingHelper.DrawDefaultImage(name, "", currentValue ? onColor : offColor);
133+
}
134+
135+
protected override Boolean RunCommand(ActionEditorActionParameters actionParameters)
136+
{
137+
Tuple<String, String, SKColor, SKColor> parameters;
138+
try
139+
{
140+
parameters = GetParameters(actionParameters);
141+
}
142+
catch (Exception)
143+
{
144+
return false;
145+
}
146+
147+
if (parameters == null)
148+
{
149+
return false;
150+
}
151+
152+
var (_, api, _, _) = parameters;
153+
154+
try
155+
{
156+
var currentValue = (Int32)Remote.GetParameter(api) == 1;
157+
Remote.SetParameter(api, currentValue ? 0 : 1);
158+
this.SetCurrentState(actionParameters, currentValue ? 0 : 1);
159+
}
160+
catch (Exception)
161+
{
162+
return false;
163+
}
164+
165+
return true;
166+
}
167+
168+
private static Tuple<String, String, SKColor, SKColor> GetParameters(ActionEditorActionParameters actionParameters)
169+
{
170+
actionParameters.TryGetString("name", out var name);
171+
actionParameters.TryGetString("api", out var api);
172+
actionParameters.TryGetString("oncolor", out var onColor);
173+
actionParameters.TryGetString("offcolor", out var offColor);
174+
175+
return new Tuple<String, String, SKColor, SKColor>(
176+
String.IsNullOrEmpty(name) ? "Unknown" : name,
177+
String.IsNullOrEmpty(api) ? "Strip[1].Gain" : api,
178+
SKColor.TryParse(onColor, out var on) ? on : ColorHelper.Active,
179+
SKColor.TryParse(offColor, out var off) ? off : ColorHelper.Inactive);
180+
}
181+
}

0 commit comments

Comments
 (0)