Skip to content

Commit 0dbcfcb

Browse files
committed
perf: optimize performance for image drawing and levels update
1 parent dd016ef commit 0dbcfcb

File tree

3 files changed

+72
-103
lines changed

3 files changed

+72
-103
lines changed

src/VoiceMeeterPlugin/Actions/LevelsCommand.cs

+11-2
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ protected override String GetCommandDisplayName(ActionEditorActionParameters act
105105
return null;
106106
}
107107

108-
var (name, channel, bgColor, fgColor) = parameters;
108+
var (name, channel, _, _) = parameters;
109109

110110
this.VmService.Levels.AddChannel(channel);
111111

@@ -158,7 +158,16 @@ protected override BitmapImage GetCommandImage(ActionEditorActionParameters acti
158158
}
159159

160160
currentValue = (Single)Math.Round(currentValue, 10);
161-
161+
162+
if (currentValue < 0)
163+
{
164+
currentValue = 0;
165+
}
166+
167+
if (currentValue > 1)
168+
{
169+
currentValue = 1;
170+
}
162171

163172
return DrawingHelper.DrawVolumeBar(PluginImageSize.Width60, bgColor.ToBitmapColor(), fgColor.ToBitmapColor(), currentValue, 0, 1, 1, "", name, false);
164173
}

src/VoiceMeeterPlugin/Helpers/DrawingHelper.cs

+52-100
Original file line numberDiff line numberDiff line change
@@ -6,190 +6,142 @@
66

77
public static class DrawingHelper
88
{
9-
private static String RESOURCE_PATH = "Loupedeck.VoiceMeeterPlugin.Resources";
10-
11-
public static SKPath RoundedRect(SKRect bounds, Int32 radius)
12-
{
13-
var diameter = radius * 2;
14-
SKRect arc = new SKRect(bounds.Left, bounds.Top, bounds.Left + diameter, bounds.Top + diameter);
15-
SKPath path = new SKPath();
16-
17-
if (radius == 0)
18-
{
19-
path.AddRect(bounds);
20-
return path;
21-
}
22-
23-
// top left arc
24-
path.ArcTo(arc, 180, 90, false);
25-
26-
// top right arc
27-
arc = new SKRect(bounds.Right - diameter, bounds.Top, bounds.Right, bounds.Top + diameter);
28-
path.ArcTo(arc, 270, 90, false);
29-
30-
// bottom right arc
31-
arc = new SKRect(bounds.Right - diameter, bounds.Bottom - diameter, bounds.Right, bounds.Bottom);
32-
path.ArcTo(arc, 0, 90, false);
33-
34-
// bottom left arc
35-
arc = new SKRect(bounds.Left, bounds.Bottom - diameter, bounds.Left + diameter, bounds.Bottom);
36-
path.ArcTo(arc, 90, 90, false);
37-
38-
path.Close();
39-
return path;
40-
}
9+
private static readonly String RESOURCE_PATH = "Loupedeck.VoiceMeeterPlugin.Resources";
4110

4211
public static BitmapImage ReadImage(String imageName, String ext = "png", String addPath = "Images")
4312
=> EmbeddedResources.ReadImage($"{RESOURCE_PATH}.{addPath}.{imageName}.{ext}");
4413

45-
public static BitmapBuilder LoadBitmapBuilder
46-
(String imageName = "clear", String text = null, BitmapColor? textColor = null, String ext = "png",
47-
String addPath = "Images")
14+
public static BitmapBuilder LoadBitmapBuilder(String imageName = "clear", String text = null, BitmapColor? textColor = null, String ext = "png", String addPath = "Images")
4815
=> LoadBitmapBuilder(ReadImage(imageName, ext, addPath), text, textColor);
4916

50-
public static BitmapBuilder LoadBitmapBuilder
51-
(BitmapImage image, String text = null, BitmapColor? textColor = null)
17+
public static BitmapBuilder LoadBitmapBuilder(BitmapImage image, String text = null, BitmapColor? textColor = null)
5218
{
5319
var builder = new BitmapBuilder(80, 80);
54-
5520
builder.Clear(BitmapColor.Black);
5621
builder.DrawImage(image);
5722

5823
return text is null ? builder : builder.AddTextOutlined(text, textColor: textColor);
5924
}
6025

61-
public static BitmapImage LoadBitmapImage
62-
(String imageName = "clear", String text = null, BitmapColor? textColor = null, String ext = "png",
63-
String addPath = "Images")
26+
public static BitmapImage LoadBitmapImage(String imageName = "clear", String text = null, BitmapColor? textColor = null, String ext = "png", String addPath = "Images")
6427
=> LoadBitmapBuilder(imageName, text, textColor, ext, addPath).ToImage();
6528

6629
public static BitmapImage LoadBitmapImage(BitmapImage image, String text = null, BitmapColor? textColor = null)
6730
=> LoadBitmapBuilder(image, text, textColor).ToImage();
6831

69-
public static BitmapBuilder AddTextOutlined(this BitmapBuilder builder, String text,
70-
BitmapColor? outlineColor = null,
71-
BitmapColor? textColor = null, Int32 fontSize = 12)
32+
public static BitmapBuilder AddTextOutlined(this BitmapBuilder builder, String text, BitmapColor? outlineColor = null, BitmapColor? textColor = null, Int32 fontSize = 12)
7233
{
73-
// TODO: Make it outline
7434
builder.DrawText(text, 0, -30, 80, 80, textColor, fontSize, 0, 0);
7535
return builder;
7636
}
7737

7838
public static BitmapImage DrawDefaultImage(String innerText, String outerText, SKColor brushColor, Int32 width = 80, Int32 height = 80)
7939
{
80-
// Set the dimensions and font
8140
SKTypeface font = SKTypeface.FromFamilyName("Arial", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright);
82-
var fontSize = 20;
83-
84-
// Create the canvas and paint
8541
var info = new SKImageInfo(width, height);
86-
var surface = SKSurface.Create(info);
42+
using var surface = SKSurface.Create(info);
8743
var canvas = surface.Canvas;
88-
var paint = new SKPaint { Color = brushColor, IsAntialias = true, Typeface = font };
44+
using var paint = new SKPaint();
45+
paint.Color = brushColor;
46+
paint.IsAntialias = true;
47+
paint.Typeface = font;
8948

90-
// Calculate the dimensions of the rounded rectangle outline
9149
var rect = new SKRect(5, 20, width - 5, height - 20);
9250

93-
// Adjust the font size if necessary to fit the inner text within the dimensions of the rounded rectangle outline
94-
while (true)
95-
{
96-
paint.TextSize = fontSize;
97-
SKRect tb = new SKRect();
98-
paint.MeasureText(innerText, ref tb);
99-
if (tb.Width < rect.Width - 5 && tb.Height < rect.Height)
100-
{
101-
break;
102-
}
103-
104-
fontSize--;
105-
}
51+
paint.TextSize = GetOptimalFontSize(innerText, paint, rect);
10652

107-
// Draw the rounded rectangle outline
10853
var cornerRadius = Math.Min(width, height) / 2;
10954
paint.Style = SKPaintStyle.Stroke;
11055
paint.StrokeWidth = 2;
11156
canvas.DrawRoundRect(rect, cornerRadius, cornerRadius, paint);
11257
paint.Style = SKPaintStyle.Fill;
11358

114-
// Draw the inner text centered within the rounded rectangle outline
11559
paint.TextAlign = SKTextAlign.Center;
116-
paint.TextSize = fontSize;
11760
canvas.DrawText(innerText, rect.MidX, rect.MidY - (paint.FontMetrics.Descent + paint.FontMetrics.Ascent) / 2, paint);
11861

119-
// Save the image to memory and return the memory streams
12062
var image = surface.Snapshot();
12163
var data = image.Encode(SKEncodedImageFormat.Png, 100);
12264

12365
return LoadBitmapImage(BitmapImage.FromArray(data.ToArray()), outerText);
12466
}
12567

126-
public static BitmapImage DrawVolumeBar(PluginImageSize imageSize, BitmapColor backgroundColor, BitmapColor foregroundColor, Single currentValue, Int32 minValue, Int32 maxValue,
127-
Int32 scaleFactor, String cmd, String name = "", Boolean drawValue = true)
68+
public static BitmapImage DrawVolumeBar(PluginImageSize imageSize, BitmapColor backgroundColor, BitmapColor foregroundColor, Single currentValue, Int32 minValue, Int32 maxValue, Int32 scaleFactor, String cmd, String name = "", Boolean drawValue = true)
12869
{
129-
// Prepare variables
13070
var dim = imageSize.GetDimension();
131-
var percentage = (currentValue - minValue) / (maxValue - minValue) * 100;
71+
var percentage = (currentValue - minValue) / (maxValue - minValue);
13272
var height = (Int32)(dim * 0.9);
13373
var width = (Int32)(dim * 0.6);
134-
var calculatedHeight = (Int32)(height * percentage / 100);
74+
var calculatedHeight = (Int32)(height * percentage);
13575
var xCenter = dim / 2 - width / 2;
13676
var yCenter = dim / 2 + height / 2;
137-
var builder = new BitmapBuilder(dim, dim);
77+
using var builder = new BitmapBuilder(dim, dim);
13878

139-
// Reset to black
14079
builder.Clear(BitmapColor.Black);
141-
142-
// Draw volume bar and border
14380
builder.DrawRectangle(xCenter, yCenter, width, -height, backgroundColor);
14481
builder.FillRectangle(xCenter, yCenter, width, -calculatedHeight, backgroundColor);
14582

146-
// Draw value text at the center
14783
if (drawValue)
14884
{
14985
builder.DrawText((currentValue / scaleFactor).ToString(CultureInfo.CurrentCulture), foregroundColor);
15086
}
15187

152-
const Int32 fontSize = 16;
153-
154-
var cmdSize = GetFontSize(fontSize, cmd, dim);
155-
156-
// Draw cmd text at the bottom
88+
var cmdSize = GetOptimalFontSize(cmd, dim: dim);
15789
builder.DrawText(cmd, 0, dim / 2 - cmdSize / 2, dim, dim, foregroundColor, cmdSize, 0, 0);
15890

159-
// if name is available, draw it over the volume bar
160-
if (String.IsNullOrEmpty(name))
91+
if (String.IsNullOrWhiteSpace(name))
16192
{
16293
return builder.ToImage();
16394
}
16495

165-
var nameSize = GetFontSize(fontSize, name, dim);
166-
167-
// draw the text using the calculated font size
96+
var nameSize = GetOptimalFontSize(name, dim: dim);
16897
builder.DrawText(name, 0, dim / 2 * -1 + nameSize / 2, dim, dim, foregroundColor, nameSize, 0, 0);
16998

17099
return builder.ToImage();
171100
}
172101

173-
private static Int32 GetFontSize(Int32 fontSize, String text, Int32 dim)
102+
private static Int32 GetOptimalFontSize(String text, SKPaint paint = null, SKRect? rect = null, Int32? dim = null)
174103
{
175-
// create a SKPaint object for measuring the text
176-
var paint = new SKPaint { TextSize = fontSize, IsAntialias = true };
104+
var minFontSize = 1;
105+
var maxFontSize = 16;
106+
SKRect textBounds = new SKRect();
177107

178-
// measure the size of the text
179-
var textBounds = new SKRect();
180-
paint.MeasureText(text, ref textBounds);
108+
if (paint is null)
109+
{
110+
paint = new SKPaint { IsAntialias = true };
111+
}
181112

182-
// adjust the font size until the text fits within the bounds of the image
183-
while (textBounds.Width > dim || textBounds.Height > dim)
113+
while (minFontSize <= maxFontSize)
184114
{
185-
fontSize -= 1;
186-
paint.TextSize = fontSize;
115+
var midFontSize = (minFontSize + maxFontSize) / 2;
116+
paint.TextSize = midFontSize;
187117
paint.MeasureText(text, ref textBounds);
118+
119+
var fits = false;
120+
121+
if (rect.HasValue)
122+
{
123+
fits = textBounds.Width <= rect.Value.Width && textBounds.Height <= rect.Value.Height;
124+
}
125+
else if (dim.HasValue)
126+
{
127+
fits = textBounds.Width <= dim.Value && textBounds.Height <= dim.Value;
128+
}
129+
130+
if (fits)
131+
{
132+
minFontSize = midFontSize + 1;
133+
}
134+
else
135+
{
136+
maxFontSize = midFontSize - 1;
137+
}
188138
}
189139

190-
return fontSize;
140+
return maxFontSize;
191141
}
192142

143+
144+
193145
private static Int32 GetDimension(this PluginImageSize size) =>
194146
size switch
195147
{

src/VoiceMeeterPlugin/Library/Voicemeeter/Levels.cs

+9-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public class Channel
2323
private readonly List<IObserver<Single[]>> _observers = [];
2424
private readonly IObservable<Int32> _timer;
2525
private IDisposable _timerSubscription;
26+
private List<Single> _oldValues;
2627

2728
public Levels(Int32 milliseconds = 20)
2829
{
@@ -51,7 +52,14 @@ private void Watch() =>
5152
}
5253
var values = new List<Single>(this._channels.Count);
5354
values.AddRange(this._channels.Select(channel => Remote.GetLevel(channel.LevelType, channel.ChannelNumber)));
54-
55+
56+
// This is maybe to harsh, but this will prevent the same values to be sent multiple times, which is good for performance.
57+
if (this._oldValues != null && (this._oldValues.SequenceEqual(values) || this._oldValues.Sum() == values.Sum()))
58+
{
59+
return;
60+
}
61+
62+
this._oldValues = values;
5563
this.Notify(values.ToArray());
5664
});
5765

0 commit comments

Comments
 (0)