Skip to content

Commit

Permalink
feat: add level meters display
Browse files Browse the repository at this point in the history
  • Loading branch information
XeroxDev committed Oct 11, 2024
1 parent 34761e6 commit dd016ef
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 46 deletions.
189 changes: 189 additions & 0 deletions src/VoiceMeeterPlugin/Actions/LevelsCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
// This file is part of the VoiceMeeterPlugin project.
//
// Copyright (c) 2024 Dominic Ris
//
// 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 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 NON-INFRINGEMENT. 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.

namespace Loupedeck.VoiceMeeterPlugin.Actions;

using System.Reactive.Linq;
using System.Reactive.Subjects;

using Extensions;

using Helpers;

using Library.Voicemeeter;

using Services;

using SkiaSharp;

public class LevelsCommand : ActionEditorCommand
{
private VoiceMeeterService VmService { get; }
private Subject<Boolean> OnDestroy { get; } = new();

public LevelsCommand()
{
this.DisplayName = "Level Display";
this.Description = "Displays specific Levels";

this.ActionEditor.AddControlEx(
new ActionEditorTextbox("name", "Display Name", "Name displayed on the device itself").SetRequired()
);
this.ActionEditor.AddControlEx(
new ActionEditorSlider("channel_number", "Channel Number", "The Channel Number to display").SetRequired().SetValues(0, 100, 0, 1)
);
this.ActionEditor.AddControlEx(
new ActionEditorListbox("channel_type", "Channel Type", "The Channel Type to display").SetRequired()
);
this.ActionEditor.AddControlEx(
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})$")
);
this.ActionEditor.AddControlEx(
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})$")
);

this.ActionEditor.ListboxItemsRequested += (_, e) =>
{
// iterate over LevelType enum values and add them (their name) to the listbox
foreach (var value in Enum.GetValues(typeof(LevelType)))
{
e.Items.Add(new ActionEditorListboxItem("channel_type_" + value, value.ToString(), ""));
}
};

this.VmService = VoiceMeeterService.Instance;
}

protected override Boolean OnLoad()
{
this.VmService.Levels
.TakeUntil(this.OnDestroy)
.Subscribe(_ => this.ActionImageChanged());

return base.OnLoad();
}

protected override Boolean OnUnload()
{
this.OnDestroy.OnNext(true);
return base.OnUnload();
}

protected override String GetCommandDisplayName(ActionEditorActionParameters actionParameters)
{
Tuple<String, Levels.Channel, SKColor, SKColor> parameters;
try
{
parameters = GetParameters(actionParameters);
}
catch (Exception)
{
return null;
}

if (parameters == null)
{
return null;
}

var (name, channel, bgColor, fgColor) = parameters;

this.VmService.Levels.AddChannel(channel);

var currentValue = 0f;

try
{
currentValue = Remote.GetLevel(channel.LevelType, channel.ChannelNumber);
}
catch (Exception)
{
// ignore
}

currentValue = (Single)Math.Round(currentValue, 10);

return $"{name} - {currentValue:P0}";
}

protected override BitmapImage GetCommandImage(ActionEditorActionParameters actionParameters, Int32 imageWidth, Int32 imageHeight)
{
Tuple<String, Levels.Channel, SKColor, SKColor> parameters;
try
{
parameters = GetParameters(actionParameters);
}
catch (Exception)
{
return null;
}

if (parameters == null)
{
return null;
}

var (name, channel, bgColor, fgColor) = parameters;

this.VmService.Levels.AddChannel(channel);

var currentValue = 0f;

try
{
currentValue = Remote.GetLevel(channel.LevelType, channel.ChannelNumber);
}
catch (Exception)
{
// ignore
}

currentValue = (Single)Math.Round(currentValue, 10);


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

private static Tuple<String, Levels.Channel, SKColor, SKColor> GetParameters(ActionEditorActionParameters actionParameters)
{
actionParameters.TryGetString("name", out var name);
actionParameters.TryGetInt32("channel_number", out var channelNumber);
actionParameters.TryGetString("channel_type", out var channelType);
actionParameters.TryGetString("bgcolor", out var bgColor);
actionParameters.TryGetString("fgcolor", out var fgColor);

// for the channel type, we first have to remove the prefix
var type = channelType.Replace("channel_type_", "");
if (!Enum.TryParse<LevelType>(type, out var levelType))
{
return null;
}

var channel = new Levels.Channel { LevelType = levelType, ChannelNumber = channelNumber };

return new Tuple<String, Levels.Channel, SKColor, SKColor>(
String.IsNullOrEmpty(name) ? "Unknown" : name,
channel,
SKColor.TryParse(bgColor, out var bg) ? bg : ColorHelper.Inactive,
SKColor.TryParse(fgColor, out var fg) ? fg : SKColors.White);
}
}
7 changes: 5 additions & 2 deletions src/VoiceMeeterPlugin/Helpers/DrawingHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ public static BitmapImage DrawDefaultImage(String innerText, String outerText, S
}

public static BitmapImage DrawVolumeBar(PluginImageSize imageSize, BitmapColor backgroundColor, BitmapColor foregroundColor, Single currentValue, Int32 minValue, Int32 maxValue,
Int32 scaleFactor, String cmd, String name = "")
Int32 scaleFactor, String cmd, String name = "", Boolean drawValue = true)
{
// Prepare variables
var dim = imageSize.GetDimension();
Expand All @@ -144,7 +144,10 @@ public static BitmapImage DrawVolumeBar(PluginImageSize imageSize, BitmapColor b
builder.FillRectangle(xCenter, yCenter, width, -calculatedHeight, backgroundColor);

// Draw value text at the center
builder.DrawText((currentValue / scaleFactor).ToString(CultureInfo.CurrentCulture), foregroundColor);
if (drawValue)
{
builder.DrawText((currentValue / scaleFactor).ToString(CultureInfo.CurrentCulture), foregroundColor);
}

const Int32 fontSize = 16;

Expand Down
39 changes: 21 additions & 18 deletions src/VoiceMeeterPlugin/Library/Voicemeeter/Levels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,37 @@ public class Channel
};

private readonly List<Channel> _channels;
private readonly List<IObserver<Single[]>> _observers = new();
private readonly List<IObserver<Single[]>> _observers = [];
private readonly IObservable<Int32> _timer;
private IDisposable _timerSubscription;

public Levels(Channel[] channels, Int32 milliseconds = 20)
public Levels(Int32 milliseconds = 20)
{
this._channels = new List<Channel>(channels);
this._channels = [];
this._timer = Observable.Interval(TimeSpan.FromMilliseconds(milliseconds)).Select(_ => 1);
this.Watch();
}

public void AddChannel(Channel channel)
{
// first check if there's already a channel with the same LevelType and ChannelNumber
if (this._channels.Any(c => c.LevelType == channel.LevelType && c.ChannelNumber == channel.ChannelNumber))
{
return;
}

this._channels.Add(channel);
}

private void Watch() =>
this._timerSubscription = this._timer.Subscribe(_ =>
{
var values = new List<Single>(this._channels.Count);
foreach (var channel in this._channels)
if (this._channels.Count == 0)
{
values.Add(Remote.GetLevel(channel.LevelType, channel.ChannelNumber));
return;
}
var values = new List<Single>(this._channels.Count);
values.AddRange(this._channels.Select(channel => Remote.GetLevel(channel.LevelType, channel.ChannelNumber)));
this.Notify(values.ToArray());
});
Expand All @@ -63,22 +75,13 @@ private void Notify(Single[] values)

public void Dispose() => this._timerSubscription?.Dispose();

private sealed class Unsubscriber : IDisposable
private sealed class Unsubscriber(List<IObserver<Single[]>> observers, IObserver<Single[]> observer) : IDisposable
{
private readonly List<IObserver<Single[]>> _observers;
private readonly IObserver<Single[]> _observer;

public Unsubscriber(List<IObserver<Single[]>> observers, IObserver<Single[]> observer)
{
this._observers = observers;
this._observer = observer;
}

public void Dispose()
{
if (this._observer != null && this._observers.Contains(this._observer))
if (observer != null && observers.Contains(observer))
{
this._observers.Remove(this._observer);
observers.Remove(observer);
}
}
}
Expand Down
17 changes: 4 additions & 13 deletions src/VoiceMeeterPlugin/Library/Voicemeeter/Parameters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
/// </summary>
public class Parameters : IDisposable, IObservable<Int32>
{
private readonly List<IObserver<Int32>> _observers = new();
private readonly List<IObserver<Int32>> _observers = [];
private readonly IObservable<Int32> _timer;
private IDisposable _timerSubscription;

Expand Down Expand Up @@ -59,22 +59,13 @@ private void Notify(Int32 value)

public void Dispose() => this._timerSubscription?.Dispose();

private sealed class Unsubscriber : IDisposable
private sealed class Unsubscriber(List<IObserver<Int32>> observers, IObserver<Int32> observer) : IDisposable
{
private readonly List<IObserver<Int32>> _observers;
private readonly IObserver<Int32> _observer;

public Unsubscriber(List<IObserver<Int32>> observers, IObserver<Int32> observer)
{
this._observers = observers;
this._observer = observer;
}

public void Dispose()
{
if (this._observer != null && this._observers.Contains(this._observer))
if (observer != null && observers.Contains(observer))
{
this._observers.Remove(this._observer);
observers.Remove(observer);
}
}
}
Expand Down
17 changes: 4 additions & 13 deletions src/VoiceMeeterPlugin/Library/Voicemeeter/VoicemeeterClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public void Dispose()
}
}

private readonly List<IObserver<Single>> _observers = new();
private readonly List<IObserver<Single>> _observers = [];

public IDisposable Subscribe(IObserver<Single> observer)
{
Expand All @@ -34,22 +34,13 @@ private void Notify(Single value)
}
}

private sealed class Unsubscriber : IDisposable
private sealed class Unsubscriber(List<IObserver<Single>> observers, IObserver<Single> observer) : IDisposable
{
private readonly List<IObserver<Single>> _observers;
private readonly IObserver<Single> _observer;

public Unsubscriber(List<IObserver<Single>> observers, IObserver<Single> observer)
{
this._observers = observers;
this._observer = observer;
}

public void Dispose()
{
if (this._observer != null && this._observers.Contains(this._observer))
if (observer != null && observers.Contains(observer))
{
this._observers.Remove(this._observer);
observers.Remove(observer);
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/VoiceMeeterPlugin/Services/VoiceMeeterService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public sealed class VoiceMeeterService
private static readonly Lazy<VoiceMeeterService> Lazy = new(() => new VoiceMeeterService());

public Parameters Parameters { get; set; }
public Levels Levels { get; set; }
public Boolean Connected { get; set; }

public async Task StartService(ClientApplication application)
Expand All @@ -17,6 +18,7 @@ public async Task StartService(ClientApplication application)

this.Connected = true;
this.Parameters = new Parameters();
this.Levels = new Levels();
}
}
}

0 comments on commit dd016ef

Please sign in to comment.