diff --git a/.gitignore b/.gitignore index 74d1994..c6fe696 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ .vs/ bin/ obj/ +Windows/packages/ +Windows/Release/ *.user \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 7ccb927..ef386cb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,7 @@ [submodule "Windows/min.NET"] path = Windows/min.NET url = https://github.com/MvRens/min.NET + +[submodule "Windows/VoicemeeterRemote"] + path = Windows/VoicemeeterRemote + url = https://github.com/MvRens/VoicemeeterRemote diff --git a/Hardware/Massive Knob housing - 2 pots, 2 buttons, under desk, Arduino Pro Micro.f3d b/Hardware/Massive Knob housing - 2 pots, 2 buttons, under desk, Arduino Pro Micro.f3d new file mode 100644 index 0000000..fafd68c Binary files /dev/null and b/Hardware/Massive Knob housing - 2 pots, 2 buttons, under desk, Arduino Pro Micro.f3d differ diff --git a/Hardware/Massive Knob housing - 3 pots, under desk, Arduino Pro Micro.f3d b/Hardware/Massive Knob housing - 3 pots, under desk, Arduino Pro Micro.f3d new file mode 100644 index 0000000..32244bb Binary files /dev/null and b/Hardware/Massive Knob housing - 3 pots, under desk, Arduino Pro Micro.f3d differ diff --git a/README.md b/README.md index aca60d5..74e7076 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,47 @@ # Massive Knob -Control audio devices using physical knobs. +Control audio devices using physical knobs. And more. -Inspired by [an article on Prusa's blog](https://blog.prusaprinters.org/3d-print-an-oversized-media-control-volume-knob-arduino-basics_30184/), this project has a slightly different set of goals: +Inspired by [an article on Prusa's blog](https://blog.prusaprinters.org/3d-print-an-oversized-media-control-volume-knob-arduino-basics_30184/), this project has a slightly different set of goals. The original requirements were: -**Must have** -1. ✔ Control multiple audio devices, one set of physical controls per device -2. ✔ Volume is set to an absolute value (potentiometer instead of a rotary encoder) +1. Control volume using a potentiometer (fixed position) instead of a rotary encoder (endless rotation) +2. Control specific audio devices, not the current default device +3. Provide means of switching the default device by pressing a button -Because of the second requirement, a simple media keys HID device does not suffice and extra software is required on the desktop. +Because of these requirements, a simple media keys HID device does not suffice and extra software is required on the desktop. This opens up a range of possibilities. + +## Features + + - 🔊 Set the volume for specific devices / send the current volume to an analog output + - 🔇 Mute / unmute specific devices / send the muted state to a digital output (*e.g. LED*) + - 🎧 Set the default device / set a digital output based on the default device + - 💬 Optional OSD (On-Screen Display) + - 🔌 VoiceMeeter (Standard, Banana & Potato) plugin to execute macros or read the current state + + +Massive Knob is basically a host for plugins. A plugin can implement a device or actions which either process signals from the device to perform an action (for example, change the volume when a knob is turned) or send signals to the device based on the system state (for example, light up an LED to indicate the default device). + +### Devices +Devices can provide the following inputs and outputs, up to 255 for each type (unless you're Look Mum No Computer I assume this will be enough): + +1. Analog input (*e.g. a potentiometer*) +2. Digital input (*e.g. a button or switch*) +3. Analog output (*e.g. a PWM output, though not yet supported by the reference Arduino implementation*) +4. Digital output (*e.g. an LED*) + +#### Serial +Connects to a compatible device on a Serial port, probably a USB device like an Arduino. The device must implement the Massive Knob protocol which uses the [MIN protocol](https://github.com/min-protocol/min) to send and receive frames. An Arduino Sketch is included with this repository which can be customized to suit your hardware layout. + +#### Emulator +Useful for development, this one emulates an actual device. The number of inputs and outputs are configurable, a popup allows changing the inputs and shows the state of the outputs. -**Nice to have** -1. Physical buttons to switch the active device - - by changing the Windows default output device - - by running a VoiceMeeter macro -2. Corresponding LEDs to indicate the currently active device -3. ✔ OSD -4. ✔ API / plugins to use extra knobs and buttons for other purposes ## Developing The hardware side uses an Arduino sketch to communicate the hardware state over the serial port. The Windows software is written in C# using .NET Framework 4.7.2 and Visual Studio 2019. +Refer to the bundled plugins for examples. Some icons courtesy of https://feathericons.com/ \ No newline at end of file diff --git a/Windows/BuildRelease.ps1 b/Windows/BuildRelease.ps1 new file mode 100644 index 0000000..3cce327 --- /dev/null +++ b/Windows/BuildRelease.ps1 @@ -0,0 +1,41 @@ +# Run this script from the Developer PowerShell found in Visual Studio 2019 +# or the start menu to get the correct msbuild version on the path +# +# GitVersion is also required and must be available on the path +# Inno Setup 5 is used to compile the setup, it's path is specified below +# +$innoSetupCompiler = "C:\Program Files (x86)\Inno Setup 5\ISCC.exe" + + + + + +$versionJson = & GitVersion | Out-String +try +{ + $version = ConvertFrom-Json $versionJson +} +catch +{ + Write-Host "Error while parsing GitVersion output: $($_.Exception.Message)" -ForegroundColor Red + Write-Host $versionJson -ForegroundColor Gray + exit 1 +} + +Write-Host "GitVersion: $($version.LegacySemVer)" +$env:BUILD_VERSION = $version.LegacySemVer + + +& msbuild MassiveKnob.sln /t:Clean /t:Build /p:Configuration=Release +if (!$?) +{ + Write-Host "MSBuild failed, aborting..." -ForegroundColor Red + exit 1 +} + +& $innoSetupCompiler "Setup\MassiveKnobSetup.iss" +if (!$?) +{ + Write-Host "Inno Setup failed, aborting..." -ForegroundColor Red + exit 1 +} \ No newline at end of file diff --git a/Windows/MassiveKnob.Plugin.CoreAudio/GetDefault/DeviceGetDefaultAction.cs b/Windows/MassiveKnob.Plugin.CoreAudio/GetDefault/DeviceGetDefaultAction.cs new file mode 100644 index 0000000..b2c921b --- /dev/null +++ b/Windows/MassiveKnob.Plugin.CoreAudio/GetDefault/DeviceGetDefaultAction.cs @@ -0,0 +1,101 @@ +using System; +using System.Windows.Controls; +using AudioSwitcher.AudioApi; +using MassiveKnob.Plugin.CoreAudio.OSD; +using Microsoft.Extensions.Logging; + +namespace MassiveKnob.Plugin.CoreAudio.GetDefault +{ + public class DeviceGetDefaultAction : IMassiveKnobAction + { + public Guid ActionId { get; } = new Guid("3c427e28-493f-489f-abb3-1a7ef23ca6c9"); + public MassiveKnobActionType ActionType { get; } = MassiveKnobActionType.OutputDigital; + public string Name { get; } = Strings.GetDefaultName; + public string Description { get; } = Strings.GetDefaultDescription; + + + public IMassiveKnobActionInstance Create(ILogger logger) + { + return new Instance(); + } + + + private class Instance : IMassiveKnobActionInstance + { + private IMassiveKnobActionContext actionContext; + private DeviceGetDefaultActionSettings settings; + private IDevice playbackDevice; + private IDisposable deviceChanged; + + + public void Initialize(IMassiveKnobActionContext context) + { + actionContext = context; + settings = context.GetSettings(); + ApplySettings(); + } + + + public void Dispose() + { + deviceChanged?.Dispose(); + } + + + private void ApplySettings() + { + if (playbackDevice != null && playbackDevice.Id == settings.DeviceId) + return; + + var coreAudioController = CoreAudioControllerInstance.Acquire(); + playbackDevice = settings.DeviceId.HasValue ? coreAudioController.GetDevice(settings.DeviceId.Value) : null; + + deviceChanged?.Dispose(); + deviceChanged = playbackDevice?.PropertyChanged.Subscribe(PropertyChanged); + + CheckActive(); + } + + + public UserControl CreateSettingsControl() + { + var viewModel = new DeviceGetDefaultActionSettingsViewModel(settings); + viewModel.PropertyChanged += (sender, args) => + { + if (!viewModel.IsSettingsProperty(args.PropertyName)) + return; + + actionContext.SetSettings(settings); + ApplySettings(); + }; + + return new DeviceGetDefaultActionSettingsView(viewModel); + } + + + private void PropertyChanged(DevicePropertyChangedArgs args) + { + if (args.ChangedType != DeviceChangedType.DefaultChanged) + return; + + CheckActive(); + + // TODO default OSD + //if (settings.OSD) + //OSDManager.Show(args.Device); + } + + + private void CheckActive() + { + if (playbackDevice == null) + return; + + var isDefault = (settings.Playback && playbackDevice.IsDefaultDevice) || + (settings.Communications && playbackDevice.IsDefaultCommunicationsDevice); + + actionContext.SetDigitalOutput(settings.Inverted ? !isDefault : isDefault); + } + } + } +} diff --git a/Windows/MassiveKnob.Plugin.CoreAudio/GetDefault/DeviceGetDefaultActionSettings.cs b/Windows/MassiveKnob.Plugin.CoreAudio/GetDefault/DeviceGetDefaultActionSettings.cs new file mode 100644 index 0000000..7ff2ea4 --- /dev/null +++ b/Windows/MassiveKnob.Plugin.CoreAudio/GetDefault/DeviceGetDefaultActionSettings.cs @@ -0,0 +1,12 @@ +using MassiveKnob.Plugin.CoreAudio.Base; + +namespace MassiveKnob.Plugin.CoreAudio.GetDefault +{ + public class DeviceGetDefaultActionSettings : BaseDeviceSettings + { + public bool Playback { get; set; } = true; + public bool Communications { get; set; } = true; + + public bool Inverted { get; set; } + } +} diff --git a/Windows/MassiveKnob.Plugin.CoreAudio/GetDefault/DeviceGetDefaultActionSettingsView.xaml b/Windows/MassiveKnob.Plugin.CoreAudio/GetDefault/DeviceGetDefaultActionSettingsView.xaml new file mode 100644 index 0000000..257cd89 --- /dev/null +++ b/Windows/MassiveKnob.Plugin.CoreAudio/GetDefault/DeviceGetDefaultActionSettingsView.xaml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + diff --git a/Windows/MassiveKnob.Plugin.CoreAudio/GetDefault/DeviceGetDefaultActionSettingsView.xaml.cs b/Windows/MassiveKnob.Plugin.CoreAudio/GetDefault/DeviceGetDefaultActionSettingsView.xaml.cs new file mode 100644 index 0000000..47aa19b --- /dev/null +++ b/Windows/MassiveKnob.Plugin.CoreAudio/GetDefault/DeviceGetDefaultActionSettingsView.xaml.cs @@ -0,0 +1,14 @@ +namespace MassiveKnob.Plugin.CoreAudio.GetDefault +{ + /// + /// Interaction logic for DeviceGetDefaultActionSettingsView.xaml + /// + public partial class DeviceGetDefaultActionSettingsView + { + public DeviceGetDefaultActionSettingsView(DeviceGetDefaultActionSettingsViewModel viewModel) + { + DataContext = viewModel; + InitializeComponent(); + } + } +} \ No newline at end of file diff --git a/Windows/MassiveKnob.Plugin.CoreAudio/GetDefault/DeviceGetDefaultActionSettingsViewModel.cs b/Windows/MassiveKnob.Plugin.CoreAudio/GetDefault/DeviceGetDefaultActionSettingsViewModel.cs new file mode 100644 index 0000000..98018eb --- /dev/null +++ b/Windows/MassiveKnob.Plugin.CoreAudio/GetDefault/DeviceGetDefaultActionSettingsViewModel.cs @@ -0,0 +1,54 @@ +using MassiveKnob.Plugin.CoreAudio.Base; + +namespace MassiveKnob.Plugin.CoreAudio.GetDefault +{ + public class DeviceGetDefaultActionSettingsViewModel : BaseDeviceSettingsViewModel + { + // ReSharper disable UnusedMember.Global - used by WPF Binding + public bool Playback + { + get => Settings.Playback; + set + { + if (value == Settings.Playback) + return; + + Settings.Playback = value; + OnPropertyChanged(); + } + } + + public bool Communications + { + get => Settings.Communications; + set + { + if (value == Settings.Communications) + return; + + Settings.Communications = value; + OnPropertyChanged(); + } + } + + public bool Inverted + { + get => Settings.Inverted; + set + { + if (value == Settings.Inverted) + return; + + Settings.Inverted = value; + OnPropertyChanged(); + } + } + // ReSharper restore UnusedMember.Global + + + // ReSharper disable once SuggestBaseTypeForParameter - by design + public DeviceGetDefaultActionSettingsViewModel(DeviceGetDefaultActionSettings settings) : base(settings) + { + } + } +} diff --git a/Windows/MassiveKnob.Plugin.CoreAudio/MassiveKnob.Plugin.CoreAudio.csproj b/Windows/MassiveKnob.Plugin.CoreAudio/MassiveKnob.Plugin.CoreAudio.csproj index 1cab7bc..802bfcb 100644 --- a/Windows/MassiveKnob.Plugin.CoreAudio/MassiveKnob.Plugin.CoreAudio.csproj +++ b/Windows/MassiveKnob.Plugin.CoreAudio/MassiveKnob.Plugin.CoreAudio.csproj @@ -17,7 +17,7 @@ true full false - $(localappdata)\MassiveKnob\Plugins\CoreAudio\ + bin\Debug\ DEBUG;TRACE prompt 4 @@ -49,6 +49,12 @@ BaseDeviceSettingsView.xaml + + + + DeviceGetDefaultActionSettingsView.xaml + + @@ -61,6 +67,12 @@ + + + + DeviceSetDefaultActionSettingsView.xaml + + @@ -120,6 +132,10 @@ Designer MSBuild:Compile + + MSBuild:Compile + Designer + MSBuild:Compile Designer @@ -136,6 +152,10 @@ MSBuild:Compile Designer + + MSBuild:Compile + Designer + MSBuild:Compile Designer @@ -150,5 +170,6 @@ Always + \ No newline at end of file diff --git a/Windows/MassiveKnob.Plugin.CoreAudio/MassiveKnobCoreAudioPlugin.cs b/Windows/MassiveKnob.Plugin.CoreAudio/MassiveKnobCoreAudioPlugin.cs index 35267ed..5896fd1 100644 --- a/Windows/MassiveKnob.Plugin.CoreAudio/MassiveKnobCoreAudioPlugin.cs +++ b/Windows/MassiveKnob.Plugin.CoreAudio/MassiveKnobCoreAudioPlugin.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using MassiveKnob.Plugin.CoreAudio.GetDefault; using MassiveKnob.Plugin.CoreAudio.GetMuted; using MassiveKnob.Plugin.CoreAudio.GetVolume; +using MassiveKnob.Plugin.CoreAudio.SetDefault; using MassiveKnob.Plugin.CoreAudio.SetMuted; using MassiveKnob.Plugin.CoreAudio.SetVolume; @@ -21,8 +23,12 @@ namespace MassiveKnob.Plugin.CoreAudio { new DeviceSetVolumeAction(), new DeviceGetVolumeAction(), + new DeviceSetMutedAction(), - new DeviceGetMutedAction() + new DeviceGetMutedAction(), + + new DeviceSetDefaultAction(), + new DeviceGetDefaultAction() }; diff --git a/Windows/MassiveKnob.Plugin.CoreAudio/SetDefault/DeviceSetDefaultAction.cs b/Windows/MassiveKnob.Plugin.CoreAudio/SetDefault/DeviceSetDefaultAction.cs new file mode 100644 index 0000000..4026741 --- /dev/null +++ b/Windows/MassiveKnob.Plugin.CoreAudio/SetDefault/DeviceSetDefaultAction.cs @@ -0,0 +1,84 @@ +using System; +using System.Threading.Tasks; +using System.Windows.Controls; +using AudioSwitcher.AudioApi; +using Microsoft.Extensions.Logging; + +namespace MassiveKnob.Plugin.CoreAudio.SetDefault +{ + public class DeviceSetDefaultAction : IMassiveKnobAction + { + public Guid ActionId { get; } = new Guid("b76f1eb7-2419-42b4-9de4-9bfe6f65a841"); + public MassiveKnobActionType ActionType { get; } = MassiveKnobActionType.InputDigital; + public string Name { get; } = Strings.SetDefaultName; + public string Description { get; } = Strings.SetDefaultDescription; + + + public IMassiveKnobActionInstance Create(ILogger logger) + { + return new Instance(); + } + + + private class Instance : IMassiveKnobDigitalAction + { + private IMassiveKnobActionContext actionContext; + private DeviceSetDefaultActionSettings settings; + private IDevice playbackDevice; + + + public void Initialize(IMassiveKnobActionContext context) + { + actionContext = context; + settings = context.GetSettings(); + ApplySettings(); + } + + + public void Dispose() + { + } + + + private void ApplySettings() + { + var coreAudioController = CoreAudioControllerInstance.Acquire(); + playbackDevice = settings.DeviceId.HasValue ? coreAudioController.GetDevice(settings.DeviceId.Value) : null; + } + + + public UserControl CreateSettingsControl() + { + var viewModel = new DeviceSetDefaultActionSettingsViewModel(settings); + viewModel.PropertyChanged += (sender, args) => + { + if (!viewModel.IsSettingsProperty(args.PropertyName)) + return; + + actionContext.SetSettings(settings); + ApplySettings(); + }; + + return new DeviceSetDefaultActionSettingsView(viewModel); + } + + + public async ValueTask DigitalChanged(bool on) + { + if (playbackDevice == null || !on) + return; + + if (settings.Playback) + await playbackDevice.SetAsDefaultAsync(); + + if (settings.Communications) + await playbackDevice.SetAsDefaultCommunicationsAsync(); + + + // TODO OSD for default device + //if (settings.OSD) + //OSDManager.Show(playbackDevice); + } + } + } +} diff --git a/Windows/MassiveKnob.Plugin.CoreAudio/SetDefault/DeviceSetDefaultActionSettings.cs b/Windows/MassiveKnob.Plugin.CoreAudio/SetDefault/DeviceSetDefaultActionSettings.cs new file mode 100644 index 0000000..1c92c00 --- /dev/null +++ b/Windows/MassiveKnob.Plugin.CoreAudio/SetDefault/DeviceSetDefaultActionSettings.cs @@ -0,0 +1,10 @@ +using MassiveKnob.Plugin.CoreAudio.Base; + +namespace MassiveKnob.Plugin.CoreAudio.SetDefault +{ + public class DeviceSetDefaultActionSettings : BaseDeviceSettings + { + public bool Playback { get; set; } = true; + public bool Communications { get; set; } = true; + } +} diff --git a/Windows/MassiveKnob.Plugin.CoreAudio/SetDefault/DeviceSetDefaultActionSettingsView.xaml b/Windows/MassiveKnob.Plugin.CoreAudio/SetDefault/DeviceSetDefaultActionSettingsView.xaml new file mode 100644 index 0000000..bbf2c9a --- /dev/null +++ b/Windows/MassiveKnob.Plugin.CoreAudio/SetDefault/DeviceSetDefaultActionSettingsView.xaml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + diff --git a/Windows/MassiveKnob.Plugin.CoreAudio/SetDefault/DeviceSetDefaultActionSettingsView.xaml.cs b/Windows/MassiveKnob.Plugin.CoreAudio/SetDefault/DeviceSetDefaultActionSettingsView.xaml.cs new file mode 100644 index 0000000..4a12a89 --- /dev/null +++ b/Windows/MassiveKnob.Plugin.CoreAudio/SetDefault/DeviceSetDefaultActionSettingsView.xaml.cs @@ -0,0 +1,14 @@ +namespace MassiveKnob.Plugin.CoreAudio.SetDefault +{ + /// + /// Interaction logic for DeviceSetDefaultActionSettingsView.xaml + /// + public partial class DeviceSetDefaultActionSettingsView + { + public DeviceSetDefaultActionSettingsView(DeviceSetDefaultActionSettingsViewModel viewModel) + { + DataContext = viewModel; + InitializeComponent(); + } + } +} diff --git a/Windows/MassiveKnob.Plugin.CoreAudio/SetDefault/DeviceSetDefaultActionSettingsViewModel.cs b/Windows/MassiveKnob.Plugin.CoreAudio/SetDefault/DeviceSetDefaultActionSettingsViewModel.cs new file mode 100644 index 0000000..be7cc94 --- /dev/null +++ b/Windows/MassiveKnob.Plugin.CoreAudio/SetDefault/DeviceSetDefaultActionSettingsViewModel.cs @@ -0,0 +1,41 @@ +using MassiveKnob.Plugin.CoreAudio.Base; + +namespace MassiveKnob.Plugin.CoreAudio.SetDefault +{ + public class DeviceSetDefaultActionSettingsViewModel : BaseDeviceSettingsViewModel + { + // ReSharper disable UnusedMember.Global - used by WPF Binding + public bool Playback + { + get => Settings.Playback; + set + { + if (value == Settings.Playback) + return; + + Settings.Playback = value; + OnPropertyChanged(); + } + } + + public bool Communications + { + get => Settings.Communications; + set + { + if (value == Settings.Communications) + return; + + Settings.Communications = value; + OnPropertyChanged(); + } + } + // ReSharper restore UnusedMember.Global + + + // ReSharper disable once SuggestBaseTypeForParameter - by design + public DeviceSetDefaultActionSettingsViewModel(DeviceSetDefaultActionSettings settings) : base(settings) + { + } + } +} diff --git a/Windows/MassiveKnob.Plugin.CoreAudio/Strings.Designer.cs b/Windows/MassiveKnob.Plugin.CoreAudio/Strings.Designer.cs index e855eab..22fb5fc 100644 --- a/Windows/MassiveKnob.Plugin.CoreAudio/Strings.Designer.cs +++ b/Windows/MassiveKnob.Plugin.CoreAudio/Strings.Designer.cs @@ -105,6 +105,24 @@ namespace MassiveKnob.Plugin.CoreAudio { } } + /// + /// Looks up a localized string similar to Sets the digital output depending on whether the selected device is the active playback or communications device.. + /// + public static string GetDefaultDescription { + get { + return ResourceManager.GetString("GetDefaultDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Is default device. + /// + public static string GetDefaultName { + get { + return ResourceManager.GetString("GetDefaultName", resourceCulture); + } + } + /// /// Looks up a localized string similar to Sets the digital output to the muted state for the selected device, regardless of the current default device.. /// @@ -159,6 +177,24 @@ namespace MassiveKnob.Plugin.CoreAudio { } } + /// + /// Looks up a localized string similar to Changes the default playback and/or communications device when the input turns on.. + /// + public static string SetDefaultDescription { + get { + return ResourceManager.GetString("SetDefaultDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Set default device. + /// + public static string SetDefaultName { + get { + return ResourceManager.GetString("SetDefaultName", resourceCulture); + } + } + /// /// Looks up a localized string similar to Toggles the muted state for the selected device, regardless of the current default device.. /// @@ -177,6 +213,42 @@ namespace MassiveKnob.Plugin.CoreAudio { } } + /// + /// Looks up a localized string similar to is the default communications device. + /// + public static string SettingGetDefaultCommunications { + get { + return ResourceManager.GetString("SettingGetDefaultCommunications", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Inverted (off when the device is the default). + /// + public static string SettingGetDefaultInverted { + get { + return ResourceManager.GetString("SettingGetDefaultInverted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to is the default playback device. + /// + public static string SettingGetDefaultPlayback { + get { + return ResourceManager.GetString("SettingGetDefaultPlayback", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Turn on when the selected device. + /// + public static string SettingGetDefaultWhen { + get { + return ResourceManager.GetString("SettingGetDefaultWhen", resourceCulture); + } + } + /// /// Looks up a localized string similar to Inverted (off when muted). /// @@ -204,6 +276,24 @@ namespace MassiveKnob.Plugin.CoreAudio { } } + /// + /// Looks up a localized string similar to Set as the default communications device. + /// + public static string SettingSetDefaultCommunications { + get { + return ResourceManager.GetString("SettingSetDefaultCommunications", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Set as the default playback device. + /// + public static string SettingSetDefaultPlayback { + get { + return ResourceManager.GetString("SettingSetDefaultPlayback", resourceCulture); + } + } + /// /// Looks up a localized string similar to Inverted (muted when off). /// diff --git a/Windows/MassiveKnob.Plugin.CoreAudio/Strings.resx b/Windows/MassiveKnob.Plugin.CoreAudio/Strings.resx index 1339126..9ba8dfb 100644 --- a/Windows/MassiveKnob.Plugin.CoreAudio/Strings.resx +++ b/Windows/MassiveKnob.Plugin.CoreAudio/Strings.resx @@ -132,6 +132,12 @@ {0} (Unplugged) + + Sets the digital output depending on whether the selected device is the active playback or communications device. + + + Is default device + Sets the digital output to the muted state for the selected device, regardless of the current default device. @@ -150,12 +156,30 @@ Windows Core Audio + + Changes the default playback and/or communications device when the input turns on. + + + Set default device + Toggles the muted state for the selected device, regardless of the current default device. Mute / unmute + + is the default communications device + + + Inverted (off when the device is the default) + + + is the default playback device + + + Turn on when the selected device + Inverted (off when muted) @@ -165,6 +189,12 @@ Playback device + + Set as the default communications device + + + Set as the default playback device + Inverted (muted when off) diff --git a/Windows/MassiveKnob.Plugin.EmulatorDevice/MassiveKnob.Plugin.EmulatorDevice.csproj b/Windows/MassiveKnob.Plugin.EmulatorDevice/MassiveKnob.Plugin.EmulatorDevice.csproj index 92e4d38..a83d3e4 100644 --- a/Windows/MassiveKnob.Plugin.EmulatorDevice/MassiveKnob.Plugin.EmulatorDevice.csproj +++ b/Windows/MassiveKnob.Plugin.EmulatorDevice/MassiveKnob.Plugin.EmulatorDevice.csproj @@ -17,7 +17,7 @@ true full false - $(localappdata)\MassiveKnob\Plugins\EmulatorDevice\ + bin\Debug\ DEBUG;TRACE prompt 4 diff --git a/Windows/MassiveKnob.Plugin.SerialDevice/MassiveKnob.Plugin.SerialDevice.csproj b/Windows/MassiveKnob.Plugin.SerialDevice/MassiveKnob.Plugin.SerialDevice.csproj index 17e0d9f..8de965f 100644 --- a/Windows/MassiveKnob.Plugin.SerialDevice/MassiveKnob.Plugin.SerialDevice.csproj +++ b/Windows/MassiveKnob.Plugin.SerialDevice/MassiveKnob.Plugin.SerialDevice.csproj @@ -17,7 +17,7 @@ true full false - $(localappdata)\MassiveKnob\Plugins\SerialDevice\ + bin\Debug\ DEBUG;TRACE prompt 4 diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/Base/BaseVoiceMeeterSettings.cs b/Windows/MassiveKnob.Plugin.VoiceMeeter/Base/BaseVoiceMeeterSettings.cs new file mode 100644 index 0000000..f5c9273 --- /dev/null +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/Base/BaseVoiceMeeterSettings.cs @@ -0,0 +1,13 @@ +using Voicemeeter; + +namespace MassiveKnob.Plugin.VoiceMeeter.Base +{ + public class BaseVoiceMeeterSettings + { + public RunVoicemeeterParam Version + { + get => InstanceRegister.Version; + set => InstanceRegister.Version = value; + } + } +} diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/Base/BaseVoiceMeeterSettingsView.xaml b/Windows/MassiveKnob.Plugin.VoiceMeeter/Base/BaseVoiceMeeterSettingsView.xaml new file mode 100644 index 0000000..279f21e --- /dev/null +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/Base/BaseVoiceMeeterSettingsView.xaml @@ -0,0 +1,15 @@ + + + + + + diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/Base/BaseVoiceMeeterSettingsView.xaml.cs b/Windows/MassiveKnob.Plugin.VoiceMeeter/Base/BaseVoiceMeeterSettingsView.xaml.cs new file mode 100644 index 0000000..31f0dc4 --- /dev/null +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/Base/BaseVoiceMeeterSettingsView.xaml.cs @@ -0,0 +1,13 @@ +namespace MassiveKnob.Plugin.VoiceMeeter.Base +{ + /// + /// Interaction logic for BaseVoiceMeeterSettingsView.xaml + /// + public partial class BaseVoiceMeeterSettingsView + { + public BaseVoiceMeeterSettingsView() + { + InitializeComponent(); + } + } +} diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/Base/BaseVoiceMeeterSettingsViewModel.cs b/Windows/MassiveKnob.Plugin.VoiceMeeter/Base/BaseVoiceMeeterSettingsViewModel.cs new file mode 100644 index 0000000..d3bf795 --- /dev/null +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/Base/BaseVoiceMeeterSettingsViewModel.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using Voicemeeter; + +namespace MassiveKnob.Plugin.VoiceMeeter.Base +{ + public class BaseVoiceMeeterSettingsViewModel : BaseVoiceMeeterSettingsViewModel where T : BaseVoiceMeeterSettings + { + protected new T Settings => (T)base.Settings; + + public BaseVoiceMeeterSettingsViewModel(T settings) : base(settings) + { + } + } + + + + public class BaseVoiceMeeterSettingsViewModel : INotifyPropertyChanged + { + protected readonly BaseVoiceMeeterSettings Settings; + public event PropertyChangedEventHandler PropertyChanged; + + // ReSharper disable UnusedMember.Global - used by WPF Binding + public IList Versions { get; } + + private VoiceMeeterVersionViewModel selectedVersion; + public VoiceMeeterVersionViewModel SelectedVersion + { + get => selectedVersion; + set + { + if (value == selectedVersion) + return; + + selectedVersion = value; + OnPropertyChanged(); + + Settings.Version = value?.Version ?? RunVoicemeeterParam.None; + } + } + // ReSharper restore UnusedMember.Global + + + public BaseVoiceMeeterSettingsViewModel(BaseVoiceMeeterSettings settings) + { + Settings = settings; + + Versions = new List + { + new VoiceMeeterVersionViewModel(RunVoicemeeterParam.Voicemeeter, "VoiceMeeter Standard"), + new VoiceMeeterVersionViewModel(RunVoicemeeterParam.VoicemeeterBanana, "VoiceMeeter Banana"), + new VoiceMeeterVersionViewModel(RunVoicemeeterParam.VoicemeeterPotato, "VoiceMeeter Potato") + }; + } + + + public virtual bool IsSettingsProperty(string propertyName) + { + // SelectedVersion already trigger a VoiceMeeterVersionChanged for all instances, + // which causes the settings to be stored + return propertyName != nameof(Versions) && propertyName != nameof(SelectedVersion); + } + + + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } + + public class VoiceMeeterVersionViewModel + { + public RunVoicemeeterParam Version { get; } + public string DisplayName { get; } + + + public VoiceMeeterVersionViewModel(RunVoicemeeterParam version, string displayName) + { + Version = version; + DisplayName = displayName; + } + } +} diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/GetParameter/VoiceMeeterGetParameterAction.cs b/Windows/MassiveKnob.Plugin.VoiceMeeter/GetParameter/VoiceMeeterGetParameterAction.cs new file mode 100644 index 0000000..afbec34 --- /dev/null +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/GetParameter/VoiceMeeterGetParameterAction.cs @@ -0,0 +1,139 @@ +using System; +using System.Threading.Tasks; +using System.Windows.Controls; +using Microsoft.Extensions.Logging; +using Voicemeeter; + +namespace MassiveKnob.Plugin.VoiceMeeter.GetParameter +{ + public class VoiceMeeterGetParameterAction : IMassiveKnobAction + { + public Guid ActionId { get; } = new Guid("4904fffb-aaec-4f19-88bb-49f6ed38c3ec"); + public MassiveKnobActionType ActionType { get; } = MassiveKnobActionType.OutputDigital; + public string Name { get; } = Strings.GetParameterName; + public string Description { get; } = Strings.GetParameterDescription; + + + public IMassiveKnobActionInstance Create(ILogger logger) + { + return new Instance(); + } + + + private class Instance : IMassiveKnobActionInstance, IVoiceMeeterAction + { + private IMassiveKnobActionContext actionContext; + private VoiceMeeterGetParameterActionSettings settings; + private Parameters parameters; + private IDisposable parameterChanged; + + + public void Initialize(IMassiveKnobActionContext context) + { + actionContext = context; + settings = context.GetSettings(); + ApplySettings(); + + InstanceRegister.Register(this); + } + + + public void Dispose() + { + InstanceRegister.Unregister(this); + parameterChanged?.Dispose(); + parameters?.Dispose(); + } + + + private void ApplySettings() + { + if (InstanceRegister.Version == RunVoicemeeterParam.None) + return; + + if (parameters == null) + parameters = new Parameters(); + + if (string.IsNullOrEmpty(settings.Parameter)) + { + parameterChanged?.Dispose(); + parameterChanged = null; + } + + if (parameterChanged == null) + parameterChanged = parameters.Subscribe(x => ParametersChanged()); + + // TODO directly update output depending on value + /* + if (playbackDevice != null) + actionContext.SetDigitalOutput(settings.Inverted ? !playbackDevice.IsMuted : playbackDevice.IsMuted); + */ + } + + + public UserControl CreateSettingsControl() + { + var viewModel = new VoiceMeeterGetParameterActionSettingsViewModel(settings); + viewModel.PropertyChanged += (sender, args) => + { + if (!viewModel.IsSettingsProperty(args.PropertyName)) + return; + + actionContext.SetSettings(settings); + ApplySettings(); + }; + + return new VoiceMeeterGetParameterActionSettingsView(viewModel); + } + + + public void VoiceMeeterVersionChanged() + { + // TODO update viewModel + // TODO reset parameterChanged subscription + + actionContext.SetSettings(settings); + } + + + private void ParametersChanged() + { + if (InstanceRegister.Version == RunVoicemeeterParam.None || string.IsNullOrEmpty(settings.Parameter)) + return; + + // TODO if another task is already running, wait / chain + // TODO only start task if not yet initialized + Task.Run(async () => + { + await InstanceRegister.InitializeVoicemeeter(); + bool on; + + if (float.TryParse(settings.Value, out var settingsFloatValue)) + { + try + { + // Even on/off values are returned as floating point "1.000" in text form, + // so try to compare in native format first + var floatValue = global::VoiceMeeter.Remote.GetParameter(settings.Parameter); + on = Math.Abs(settingsFloatValue - floatValue) < 0.001; + } + catch + { + // Fall back to text comparison + var value = global::VoiceMeeter.Remote.GetTextParameter(settings.Parameter); + on = string.Equals(value, settings.Value, StringComparison.InvariantCultureIgnoreCase); + } + } + else + { + var value = global::VoiceMeeter.Remote.GetTextParameter(settings.Parameter); + on = string.Equals(value, settings.Value, StringComparison.InvariantCultureIgnoreCase); + } + + // TODO check specific parameter for changes, not just any parameter + actionContext.SetDigitalOutput(settings.Inverted ? !on : on); + }); + } + } + } +} diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/GetParameter/VoiceMeeterGetParameterActionSettings.cs b/Windows/MassiveKnob.Plugin.VoiceMeeter/GetParameter/VoiceMeeterGetParameterActionSettings.cs new file mode 100644 index 0000000..54919df --- /dev/null +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/GetParameter/VoiceMeeterGetParameterActionSettings.cs @@ -0,0 +1,11 @@ +using MassiveKnob.Plugin.VoiceMeeter.Base; + +namespace MassiveKnob.Plugin.VoiceMeeter.GetParameter +{ + public class VoiceMeeterGetParameterActionSettings : BaseVoiceMeeterSettings + { + public string Parameter { get; set; } + public string Value { get; set; } + public bool Inverted { get; set; } + } +} diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/GetParameter/VoiceMeeterGetParameterActionSettingsView.xaml b/Windows/MassiveKnob.Plugin.VoiceMeeter/GetParameter/VoiceMeeterGetParameterActionSettingsView.xaml new file mode 100644 index 0000000..fa6b5e0 --- /dev/null +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/GetParameter/VoiceMeeterGetParameterActionSettingsView.xaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/GetParameter/VoiceMeeterGetParameterActionSettingsView.xaml.cs b/Windows/MassiveKnob.Plugin.VoiceMeeter/GetParameter/VoiceMeeterGetParameterActionSettingsView.xaml.cs new file mode 100644 index 0000000..c62f2d9 --- /dev/null +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/GetParameter/VoiceMeeterGetParameterActionSettingsView.xaml.cs @@ -0,0 +1,14 @@ +namespace MassiveKnob.Plugin.VoiceMeeter.GetParameter +{ + /// + /// Interaction logic for VoiceMeeterGetParameterActionSettingsView.xaml + /// + public partial class VoiceMeeterGetParameterActionSettingsView + { + public VoiceMeeterGetParameterActionSettingsView(VoiceMeeterGetParameterActionSettingsViewModel viewModel) + { + DataContext = viewModel; + InitializeComponent(); + } + } +} \ No newline at end of file diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/GetParameter/VoiceMeeterGetParameterActionSettingsViewModel.cs b/Windows/MassiveKnob.Plugin.VoiceMeeter/GetParameter/VoiceMeeterGetParameterActionSettingsViewModel.cs new file mode 100644 index 0000000..2959ff0 --- /dev/null +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/GetParameter/VoiceMeeterGetParameterActionSettingsViewModel.cs @@ -0,0 +1,54 @@ +using MassiveKnob.Plugin.VoiceMeeter.Base; + +namespace MassiveKnob.Plugin.VoiceMeeter.GetParameter +{ + public class VoiceMeeterGetParameterActionSettingsViewModel : BaseVoiceMeeterSettingsViewModel + { + // ReSharper disable UnusedMember.Global - used by WPF Binding + public string Parameter + { + get => Settings.Parameter; + set + { + if (value == Settings.Parameter) + return; + + Settings.Parameter = value; + OnPropertyChanged(); + } + } + + public string Value + { + get => Settings.Value; + set + { + if (value == Settings.Value) + return; + + Settings.Value = value; + OnPropertyChanged(); + } + } + + public bool Inverted + { + get => Settings.Inverted; + set + { + if (value == Settings.Inverted) + return; + + Settings.Inverted = value; + OnPropertyChanged(); + } + } + // ReSharper restore UnusedMember.Global + + + // ReSharper disable once SuggestBaseTypeForParameter - by design + public VoiceMeeterGetParameterActionSettingsViewModel(VoiceMeeterGetParameterActionSettings settings) : base(settings) + { + } + } +} diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/IVoiceMeeterAction.cs b/Windows/MassiveKnob.Plugin.VoiceMeeter/IVoiceMeeterAction.cs new file mode 100644 index 0000000..48f7419 --- /dev/null +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/IVoiceMeeterAction.cs @@ -0,0 +1,7 @@ +namespace MassiveKnob.Plugin.VoiceMeeter +{ + public interface IVoiceMeeterAction + { + void VoiceMeeterVersionChanged(); + } +} diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/InstanceRegister.cs b/Windows/MassiveKnob.Plugin.VoiceMeeter/InstanceRegister.cs new file mode 100644 index 0000000..e04e945 --- /dev/null +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/InstanceRegister.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Voicemeeter; + +namespace MassiveKnob.Plugin.VoiceMeeter +{ + public static class InstanceRegister + { + private static readonly object InstancesLock = new object(); + private static readonly HashSet Instances = new HashSet(); + private static Task initializeTask; + + + // The VoiceMeeter Remote only connects to one instance, so all actions need to be in sync + private static RunVoicemeeterParam version; + public static RunVoicemeeterParam Version + { + get => version; + set + { + if (value == version) + return; + + version = value; + Notify(action => action.VoiceMeeterVersionChanged()); + + initializeTask = Task.Run(async () => + { + await global::VoiceMeeter.Remote.Initialize(version); + }); + } + } + + + public static Task InitializeVoicemeeter() + { + return initializeTask ?? Task.CompletedTask; + } + + + public static void Register(IVoiceMeeterAction instance) + { + lock (InstancesLock) + { + Instances.Add(instance); + } + } + + + public static void Unregister(IVoiceMeeterAction instance) + { + lock (InstancesLock) + { + Instances.Remove(instance); + } + } + + + public static void Notify(Action action) + { + lock (InstancesLock) + { + foreach (var instance in Instances) + action(instance); + } + } + } +} diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/MassiveKnob.Plugin.VoiceMeeter.csproj b/Windows/MassiveKnob.Plugin.VoiceMeeter/MassiveKnob.Plugin.VoiceMeeter.csproj new file mode 100644 index 0000000..260bd28 --- /dev/null +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/MassiveKnob.Plugin.VoiceMeeter.csproj @@ -0,0 +1,110 @@ + + + + + Debug + AnyCPU + {19533600-D4F6-4BD4-82A3-C0234FDF044C} + Library + Properties + MassiveKnob.Plugin.VoiceMeeter + MassiveKnob.Plugin.VoiceMeeter + v4.7.2 + 512 + true + + + true + full + false + $(localappdata)\MassiveKnob\Plugins\VoiceMeeter\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + + + + + + + + + BaseVoiceMeeterSettingsView.xaml + + + + + + VoiceMeeterGetParameterActionSettingsView.xaml + + + + + + + + + + VoiceMeeterRunMacroActionSettingsView.xaml + + + + Strings.resx + True + True + + + + + PublicResXFileCodeGenerator + Strings.Designer.cs + + + + + {A1298BE4-1D23-416C-8C56-FC9264487A95} + MassiveKnob.Plugin + + + {f35dd8e5-91fa-403e-b6f6-8d2b4ae84198} + Voicemeeter + + + + + Always + + + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + + \ No newline at end of file diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/MassiveKnobPlugin.json b/Windows/MassiveKnob.Plugin.VoiceMeeter/MassiveKnobPlugin.json new file mode 100644 index 0000000..bac5965 --- /dev/null +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/MassiveKnobPlugin.json @@ -0,0 +1,3 @@ +{ + "EntryAssembly": "MassiveKnob.Plugin.VoiceMeeter.dll" +} diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/MassiveKnobVoiceMeeterPlugin.cs b/Windows/MassiveKnob.Plugin.VoiceMeeter/MassiveKnobVoiceMeeterPlugin.cs new file mode 100644 index 0000000..0158123 --- /dev/null +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/MassiveKnobVoiceMeeterPlugin.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using MassiveKnob.Plugin.VoiceMeeter.GetParameter; +using MassiveKnob.Plugin.VoiceMeeter.RunMacro; + +namespace MassiveKnob.Plugin.VoiceMeeter +{ + [MassiveKnobPlugin] + public class MassiveKnobVoiceMeeterPlugin : IMassiveKnobActionPlugin + { + public Guid PluginId { get; } = new Guid("cf6634f1-97e3-4a18-a4aa-289b558c0e82"); + public string Name { get; } = Strings.PluginName; + public string Description { get; } = Strings.PluginDescription; + public string Author { get; } = "Mark van Renswoude "; + public string Url { get; } = "https://www.github.com/MvRens/MassiveKnob/"; + + public IEnumerable Actions { get; } = new IMassiveKnobAction[] + { + new VoiceMeeterRunMacroAction(), + new VoiceMeeterGetParameterAction() + }; + } +} diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/Properties/AssemblyInfo.cs b/Windows/MassiveKnob.Plugin.VoiceMeeter/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..1da267e --- /dev/null +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("MassiveKnob.Plugin.VoiceMeeter")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("MassiveKnob.Plugin.VoiceMeeter")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("19533600-d4f6-4bd4-82a3-c0234fdf044c")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/RunMacro/VoiceMeeterRunMacroAction.cs b/Windows/MassiveKnob.Plugin.VoiceMeeter/RunMacro/VoiceMeeterRunMacroAction.cs new file mode 100644 index 0000000..e1d6bf1 --- /dev/null +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/RunMacro/VoiceMeeterRunMacroAction.cs @@ -0,0 +1,80 @@ +using System; +using System.Threading.Tasks; +using System.Windows.Controls; +using Microsoft.Extensions.Logging; +using Voicemeeter; + +namespace MassiveKnob.Plugin.VoiceMeeter.RunMacro +{ + public class VoiceMeeterRunMacroAction : IMassiveKnobAction + { + public Guid ActionId { get; } = new Guid("3bf41e96-9418-4a0e-ba5f-580e0b94dcce"); + public MassiveKnobActionType ActionType { get; } = MassiveKnobActionType.InputDigital; + public string Name { get; } = Strings.RunMacroName; + public string Description { get; } = Strings.RunMacroDescription; + + + public IMassiveKnobActionInstance Create(ILogger logger) + { + return new Instance(); + } + + + private class Instance : IMassiveKnobDigitalAction, IVoiceMeeterAction + { + private IMassiveKnobActionContext actionContext; + private VoiceMeeterRunMacroActionSettings settings; + + + public void Initialize(IMassiveKnobActionContext context) + { + actionContext = context; + settings = context.GetSettings(); + + InstanceRegister.Register(this); + } + + + public void Dispose() + { + InstanceRegister.Unregister(this); + } + + + public UserControl CreateSettingsControl() + { + var viewModel = new VoiceMeeterRunMacroActionSettingsViewModel(settings); + viewModel.PropertyChanged += (sender, args) => + { + if (!viewModel.IsSettingsProperty(args.PropertyName)) + return; + + actionContext.SetSettings(settings); + }; + + return new VoiceMeeterRunMacroActionSettingsView(viewModel); + } + + + public async ValueTask DigitalChanged(bool on) + { + if (!on) + return; + + if (settings.Version == RunVoicemeeterParam.None || string.IsNullOrEmpty(settings.Script)) + return; + + await InstanceRegister.InitializeVoicemeeter(); + global::VoiceMeeter.Remote.SetParameters(settings.Script); + } + + + public void VoiceMeeterVersionChanged() + { + // TODO update viewModel + + actionContext.SetSettings(settings); + } + } + } +} diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/RunMacro/VoiceMeeterRunMacroActionSettings.cs b/Windows/MassiveKnob.Plugin.VoiceMeeter/RunMacro/VoiceMeeterRunMacroActionSettings.cs new file mode 100644 index 0000000..81b084e --- /dev/null +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/RunMacro/VoiceMeeterRunMacroActionSettings.cs @@ -0,0 +1,9 @@ +using MassiveKnob.Plugin.VoiceMeeter.Base; + +namespace MassiveKnob.Plugin.VoiceMeeter.RunMacro +{ + public class VoiceMeeterRunMacroActionSettings : BaseVoiceMeeterSettings + { + public string Script { get; set; } + } +} diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/RunMacro/VoiceMeeterRunMacroActionSettingsView.xaml b/Windows/MassiveKnob.Plugin.VoiceMeeter/RunMacro/VoiceMeeterRunMacroActionSettingsView.xaml new file mode 100644 index 0000000..ffc0f23 --- /dev/null +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/RunMacro/VoiceMeeterRunMacroActionSettingsView.xaml @@ -0,0 +1,20 @@ + + + + + + + + + + diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/RunMacro/VoiceMeeterRunMacroActionSettingsView.xaml.cs b/Windows/MassiveKnob.Plugin.VoiceMeeter/RunMacro/VoiceMeeterRunMacroActionSettingsView.xaml.cs new file mode 100644 index 0000000..692c590 --- /dev/null +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/RunMacro/VoiceMeeterRunMacroActionSettingsView.xaml.cs @@ -0,0 +1,14 @@ +namespace MassiveKnob.Plugin.VoiceMeeter.RunMacro +{ + /// + /// Interaction logic for VoiceMeeterRunMacroActionSettingsView.xaml + /// + public partial class VoiceMeeterRunMacroActionSettingsView + { + public VoiceMeeterRunMacroActionSettingsView(VoiceMeeterRunMacroActionSettingsViewModel viewModel) + { + DataContext = viewModel; + InitializeComponent(); + } + } +} diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/RunMacro/VoiceMeeterRunMacroActionSettingsViewModel.cs b/Windows/MassiveKnob.Plugin.VoiceMeeter/RunMacro/VoiceMeeterRunMacroActionSettingsViewModel.cs new file mode 100644 index 0000000..8c67693 --- /dev/null +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/RunMacro/VoiceMeeterRunMacroActionSettingsViewModel.cs @@ -0,0 +1,29 @@ +using MassiveKnob.Plugin.VoiceMeeter.Base; + +namespace MassiveKnob.Plugin.VoiceMeeter.RunMacro +{ + public class VoiceMeeterRunMacroActionSettingsViewModel : BaseVoiceMeeterSettingsViewModel + { + // ReSharper disable UnusedMember.Global - used by WPF Bindingpriv + public string Script + { + get => Settings.Script; + set + { + // TODO timer for change notification + if (value == Settings.Script) + return; + + Settings.Script = value; + OnPropertyChanged(); + } + } + // ReSharper restore UnusedMember.Global + + + // ReSharper disable once SuggestBaseTypeForParameter - by design + public VoiceMeeterRunMacroActionSettingsViewModel(VoiceMeeterRunMacroActionSettings settings) : base(settings) + { + } + } +} diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/Strings.Designer.cs b/Windows/MassiveKnob.Plugin.VoiceMeeter/Strings.Designer.cs new file mode 100644 index 0000000..2cbb168 --- /dev/null +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/Strings.Designer.cs @@ -0,0 +1,172 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace MassiveKnob.Plugin.VoiceMeeter { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Strings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Strings() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MassiveKnob.Plugin.VoiceMeeter.Strings", typeof(Strings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Turns the output on if the specified parameter equals the specified value.. + /// + public static string GetParameterDescription { + get { + return ResourceManager.GetString("GetParameterDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Get parameter. + /// + public static string GetParameterName { + get { + return ResourceManager.GetString("GetParameterName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Provides actions to run VoiceMeeter macros or check the current state.. + /// + public static string PluginDescription { + get { + return ResourceManager.GetString("PluginDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to VoiceMeeter Remote. + /// + public static string PluginName { + get { + return ResourceManager.GetString("PluginName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Runs a VoiceMeeter macro when the input turns on.. + /// + public static string RunMacroDescription { + get { + return ResourceManager.GetString("RunMacroDescription", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Run macro. + /// + public static string RunMacroName { + get { + return ResourceManager.GetString("RunMacroName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Inverted (on if the parameter does not equal the value). + /// + public static string SettingGetParameterInverted { + get { + return ResourceManager.GetString("SettingGetParameterInverted", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Parameter name. + /// + public static string SettingGetParameterParameter { + get { + return ResourceManager.GetString("SettingGetParameterParameter", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Expected value. + /// + public static string SettingGetParameterValue { + get { + return ResourceManager.GetString("SettingGetParameterValue", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Script. + /// + public static string SettingRunMacroScript { + get { + return ResourceManager.GetString("SettingRunMacroScript", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Example: Strip[0].A1 = 1; Strip[0].A2 = 0; + ///Use comma or semicolons to separate commands, or put each command on a separate line. For more information see the VoiceMeeter documentation on Macro Buttons.. + /// + public static string SettingRunMacroScriptExample { + get { + return ResourceManager.GetString("SettingRunMacroScriptExample", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to VoiceMeeter version. + /// + public static string SettingVoiceMeeterVersion { + get { + return ResourceManager.GetString("SettingVoiceMeeterVersion", resourceCulture); + } + } + } +} diff --git a/Windows/MassiveKnob.Plugin.VoiceMeeter/Strings.resx b/Windows/MassiveKnob.Plugin.VoiceMeeter/Strings.resx new file mode 100644 index 0000000..f688f92 --- /dev/null +++ b/Windows/MassiveKnob.Plugin.VoiceMeeter/Strings.resx @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Turns the output on if the specified parameter equals the specified value. + + + Get parameter + + + Provides actions to run VoiceMeeter macros or check the current state. + + + VoiceMeeter Remote + + + Runs a VoiceMeeter macro when the input turns on. + + + Run macro + + + Inverted (on if the parameter does not equal the value) + + + Parameter name + + + Expected value + + + Script + + + Example: Strip[0].A1 = 1; Strip[0].A2 = 0; +Use comma or semicolons to separate commands, or put each command on a separate line. For more information see the VoiceMeeter documentation on Macro Buttons. + + + VoiceMeeter version + + \ No newline at end of file diff --git a/Windows/MassiveKnob.sln b/Windows/MassiveKnob.sln index 6a480a2..c19297a 100644 --- a/Windows/MassiveKnob.sln +++ b/Windows/MassiveKnob.sln @@ -17,6 +17,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MIN", "min.NET\MIN\MIN.cspr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MIN.SerialPort", "min.NET\MIN.SerialPort\MIN.SerialPort.csproj", "{DB8819EB-D2B7-4AAE-A699-BD200F2C113A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassiveKnob.Plugin.VoiceMeeter", "MassiveKnob.Plugin.VoiceMeeter\MassiveKnob.Plugin.VoiceMeeter.csproj", "{19533600-D4F6-4BD4-82A3-C0234FDF044C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Voicemeeter", "VoicemeeterRemote\Voicemeeter\Voicemeeter.csproj", "{F35DD8E5-91FA-403E-B6F6-8D2B4AE84198}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -51,6 +55,14 @@ Global {DB8819EB-D2B7-4AAE-A699-BD200F2C113A}.Debug|Any CPU.Build.0 = Debug|Any CPU {DB8819EB-D2B7-4AAE-A699-BD200F2C113A}.Release|Any CPU.ActiveCfg = Release|Any CPU {DB8819EB-D2B7-4AAE-A699-BD200F2C113A}.Release|Any CPU.Build.0 = Release|Any CPU + {19533600-D4F6-4BD4-82A3-C0234FDF044C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {19533600-D4F6-4BD4-82A3-C0234FDF044C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {19533600-D4F6-4BD4-82A3-C0234FDF044C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {19533600-D4F6-4BD4-82A3-C0234FDF044C}.Release|Any CPU.Build.0 = Release|Any CPU + {F35DD8E5-91FA-403E-B6F6-8D2B4AE84198}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F35DD8E5-91FA-403E-B6F6-8D2B4AE84198}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F35DD8E5-91FA-403E-B6F6-8D2B4AE84198}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F35DD8E5-91FA-403E-B6F6-8D2B4AE84198}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Windows/MassiveKnob/Core/PluginManager.cs b/Windows/MassiveKnob/Core/PluginManager.cs index 198a3eb..45bb752 100644 --- a/Windows/MassiveKnob/Core/PluginManager.cs +++ b/Windows/MassiveKnob/Core/PluginManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; @@ -59,23 +60,56 @@ namespace MassiveKnob.Core var registeredIds = new RegisteredIds(); var codeBase = Assembly.GetEntryAssembly()?.CodeBase; - if (!string.IsNullOrEmpty(codeBase)) + if (string.IsNullOrEmpty(codeBase)) { - var localPath = Path.GetDirectoryName(new Uri(codeBase).LocalPath); - if (!string.IsNullOrEmpty(localPath)) - { - var applicationPluginPath = Path.Combine(localPath, @"Plugins"); - LoadPlugins(applicationPluginPath, registeredIds, onException); - } + logger.Error("No known EntryAssembly, unable to load plugins"); + return; } + var localPath = Path.GetDirectoryName(new Uri(codeBase).LocalPath); + if (string.IsNullOrEmpty(localPath)) + { + logger.Error("EntryAssembly CodeBase does not resolve to a local path, unable to load plugins: {codeBase}", codeBase); + return; + } - var localPluginPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"MassiveKnob", @"Plugins"); + var applicationPluginPath = Path.Combine(localPath, @"Plugins"); + LoadPlugins(applicationPluginPath, registeredIds, onException); + + #if DEBUG + // For debugging, load directly from the various bin folders + // ReSharper disable once InvertIf + if (IsInPath(localPath, "MassiveKnob", "bin", "Debug")) + { + // Go up three folders, filter out lingering bin/Release builds + var solutionPath = Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(localPath))); + LoadPlugins(solutionPath, registeredIds, onException, pluginPath => IsInPath(pluginPath, "bin", "Debug")); + } + #endif + + var localPluginPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MassiveKnob", "Plugins"); LoadPlugins(localPluginPath, registeredIds, onException); } + + + private static bool IsInPath(string actualPath, params string[] expectedPathComponents) + { + if (string.IsNullOrEmpty(actualPath) || expectedPathComponents.Length == 0) + return false; + + var expectedPath = Path.Combine(expectedPathComponents); + + if (!actualPath.EndsWith(Path.DirectorySeparatorChar.ToString())) + actualPath += Path.DirectorySeparatorChar; + + if (!expectedPath.EndsWith(Path.DirectorySeparatorChar.ToString())) + expectedPath += Path.DirectorySeparatorChar; + + return actualPath.EndsWith(expectedPath, StringComparison.CurrentCultureIgnoreCase); + } - private void LoadPlugins(string path, RegisteredIds registeredIds, Action onException) + private void LoadPlugins(string path, RegisteredIds registeredIds, Action onException, Func predicate = null) { logger.Information("Checking {path} for plugins...", path); if (!Directory.Exists(path)) @@ -89,6 +123,9 @@ namespace MassiveKnob.Core var pluginPath = Path.GetDirectoryName(metadataFilename); if (string.IsNullOrEmpty(pluginPath)) continue; + + if (predicate != null && !predicate(pluginPath)) + continue; PluginMetadata pluginMetadata; try diff --git a/Windows/MassiveKnob/Resources/Analog.xaml b/Windows/MassiveKnob/Resources/Analog.xaml index d678219..21d6ab4 100644 --- a/Windows/MassiveKnob/Resources/Analog.xaml +++ b/Windows/MassiveKnob/Resources/Analog.xaml @@ -4,7 +4,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> - + diff --git a/Windows/MassiveKnob/Resources/Device.xaml b/Windows/MassiveKnob/Resources/Device.xaml index 1b4f5ed..8ae9373 100644 --- a/Windows/MassiveKnob/Resources/Device.xaml +++ b/Windows/MassiveKnob/Resources/Device.xaml @@ -4,7 +4,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> - + diff --git a/Windows/MassiveKnob/Resources/Digital.xaml b/Windows/MassiveKnob/Resources/Digital.xaml index 09faf4b..52fa31d 100644 --- a/Windows/MassiveKnob/Resources/Digital.xaml +++ b/Windows/MassiveKnob/Resources/Digital.xaml @@ -4,7 +4,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> - + diff --git a/Windows/MassiveKnob/Resources/Logging.xaml b/Windows/MassiveKnob/Resources/Logging.xaml index f6db3ce..ead1df1 100644 --- a/Windows/MassiveKnob/Resources/Logging.xaml +++ b/Windows/MassiveKnob/Resources/Logging.xaml @@ -4,7 +4,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> - + diff --git a/Windows/MassiveKnob/Resources/Startup.xaml b/Windows/MassiveKnob/Resources/Startup.xaml index fa1de3f..927367b 100644 --- a/Windows/MassiveKnob/Resources/Startup.xaml +++ b/Windows/MassiveKnob/Resources/Startup.xaml @@ -4,7 +4,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> - + diff --git a/Windows/MassiveKnob/ViewModel/SettingsViewModel.cs b/Windows/MassiveKnob/ViewModel/SettingsViewModel.cs index ff58946..0c4ff78 100644 --- a/Windows/MassiveKnob/ViewModel/SettingsViewModel.cs +++ b/Windows/MassiveKnob/ViewModel/SettingsViewModel.cs @@ -17,6 +17,8 @@ using Serilog.Events; namespace MassiveKnob.ViewModel { + // TODO (code quality) split ViewModel for individual views, create viewmodel using container + // TODO (must have) show device status public class SettingsViewModel : IDisposable, INotifyPropertyChanged { private readonly Dictionary menuItemControls = new Dictionary diff --git a/Windows/Setup/MassiveKnobSetup.iss b/Windows/Setup/MassiveKnobSetup.iss new file mode 100644 index 0000000..32f6fca --- /dev/null +++ b/Windows/Setup/MassiveKnobSetup.iss @@ -0,0 +1,197 @@ +#define AppName "Massive Knob" +#define AppVersion GetEnv("BUILD_VERSION") +#define AppPublisher "Mark van Renswoude" +#define AppURL "https://github.com/MvRens/MassiveKnob" +#define AppExeName "MassiveKnob.exe" +#define BasePath ".." + +#if AppVersion == "" + #define AppVersion "IDE build" +#endif + + +[Setup] +AppId={{6D668D73-54E5-4FE1-8028-24FE993627B8} +AppName={#AppName} +AppVersion={#AppVersion} +AppPublisher={#AppPublisher} +AppPublisherURL={#AppURL} +AppSupportURL={#AppURL} +AppUpdatesURL={#AppURL} +DefaultDirName={pf}\{#AppName} +DisableProgramGroupPage=yes +OutputDir={#BasePath}\Release +OutputBaseFilename=MassiveKnobSetup-{#AppVersion} +Compression=lzma +SolidCompression=yes +ArchitecturesInstallIn64BitMode=x64 + +[Types] +Name: "full"; Description: "Full installation" +Name: "custom"; Description: "Custom installation"; Flags: iscustom + +[Components] +Name: main; Description: "Massive Knob application"; Types: full custom; Flags: fixed +Name: essentialplugins; Description: "Essential plugins"; Types: full custom +Name: essentialplugins\serialdevice; Description: "Serial device"; Types: full custom +Name: essentialplugins\coreaudio; Description: "Windows Core Audio actions"; Types: full custom +Name: optionalplugins; Description: "Optional plugins"; Types: full custom +Name: optionalplugins\emulatordevice; Description: "Emulator device"; Types: full custom +Name: optionalplugins\voicemeeter; Description: "VoiceMeeter actions"; Types: full custom + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked + +[Files] +; Main application +Source: {#BasePath}\MassiveKnob\bin\Release\{#AppExeName}; DestDir: "{app}"; Flags: ignoreversion; Components: main +Source: {#BasePath}\MassiveKnob\bin\Release\{#AppExeName}.config; DestDir: "{app}"; Flags: ignoreversion; Components: main +Source: {#BasePath}\MassiveKnob\bin\Release\*.dll; DestDir: "{app}"; Flags: ignoreversion; Components: main + +; Serial device plugin +Source: {#BasePath}\MassiveKnob.Plugin.SerialDevice\bin\Release\MassiveKnobPlugin.json; DestDir: "{app}\Plugins\SerialDevice"; Flags: ignoreversion; Components: essentialplugins\serialdevice +Source: {#BasePath}\MassiveKnob.Plugin.SerialDevice\bin\Release\*.dll; DestDir: "{app}\Plugins\SerialDevice"; Flags: ignoreversion; Components: essentialplugins\serialdevice + +; Core Audio plugin +Source: {#BasePath}\MassiveKnob.Plugin.CoreAudio\bin\Release\MassiveKnobPlugin.json; DestDir: "{app}\Plugins\CoreAudio"; Flags: ignoreversion; Components: essentialplugins\coreaudio +Source: {#BasePath}\MassiveKnob.Plugin.CoreAudio\bin\Release\*.dll; DestDir: "{app}\Plugins\CoreAudio"; Flags: ignoreversion; Components: essentialplugins\coreaudio + +; Emulator device plugin +Source: {#BasePath}\MassiveKnob.Plugin.EmulatorDevice\bin\Release\MassiveKnobPlugin.json; DestDir: "{app}\Plugins\EmulatorDevice"; Flags: ignoreversion; Components: optionalplugins\emulatordevice +Source: {#BasePath}\MassiveKnob.Plugin.EmulatorDevice\bin\Release\*.dll; DestDir: "{app}\Plugins\EmulatorDevice"; Flags: ignoreversion; Components: optionalplugins\emulatordevice + +; VoiceMeeter plugin +Source: {#BasePath}\MassiveKnob.Plugin.VoiceMeeter\bin\Release\MassiveKnobPlugin.json; DestDir: "{app}\Plugins\VoiceMeeter"; Flags: ignoreversion; Components: optionalplugins\voicemeeter +Source: {#BasePath}\MassiveKnob.Plugin.VoiceMeeter\bin\Release\*.dll; DestDir: "{app}\Plugins\VoiceMeeter"; Flags: ignoreversion; Components: optionalplugins\voicemeeter + +[Dirs] +Name: "{localappdata}\MassiveKnob" +Name: "{localappdata}\MassiveKnob\Logs" +Name: "{localappdata}\MassiveKnob\Plugins" + +[Icons] +Name: "{commonprograms}\{#AppName}"; Filename: "{app}\{#AppExeName}" +Name: "{commondesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Tasks: desktopicon + +[Run] +;Filename: "{app}\{#AppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(AppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent + +[Code] +// .NET version detection credit goes to: +// https://www.kynosarges.org/DotNetVersion.html + +function IsDotNetDetected(version: string; service: cardinal): boolean; +// Indicates whether the specified version and service pack of the .NET Framework is installed. +// +// version -- Specify one of these strings for the required .NET Framework version: +// 'v1.1' .NET Framework 1.1 +// 'v2.0' .NET Framework 2.0 +// 'v3.0' .NET Framework 3.0 +// 'v3.5' .NET Framework 3.5 +// 'v4\Client' .NET Framework 4.0 Client Profile +// 'v4\Full' .NET Framework 4.0 Full Installation +// 'v4.5' .NET Framework 4.5 +// 'v4.5.1' .NET Framework 4.5.1 +// 'v4.5.2' .NET Framework 4.5.2 +// 'v4.6' .NET Framework 4.6 +// 'v4.6.1' .NET Framework 4.6.1 +// 'v4.6.2' .NET Framework 4.6.2 +// 'v4.7' .NET Framework 4.7 +// 'v4.7.1' .NET Framework 4.7.1 +// 'v4.7.2' .NET Framework 4.7.2 +// 'v4.8' .NET Framework 4.8 +// +// service -- Specify any non-negative integer for the required service pack level: +// 0 No service packs required +// 1, 2, etc. Service pack 1, 2, etc. required +var + key, versionKey: string; + install, release, serviceCount, versionRelease: cardinal; + success: boolean; +begin + versionKey := version; + versionRelease := 0; + + // .NET 1.1 and 2.0 embed release number in version key + if version = 'v1.1' then begin + versionKey := 'v1.1.4322'; + end else if version = 'v2.0' then begin + versionKey := 'v2.0.50727'; + end + + // .NET 4.5 and newer install as update to .NET 4.0 Full + else if Pos('v4.', version) = 1 then begin + versionKey := 'v4\Full'; + case version of + 'v4.5': versionRelease := 378389; + 'v4.5.1': versionRelease := 378675; // 378758 on Windows 8 and older + 'v4.5.2': versionRelease := 379893; + 'v4.6': versionRelease := 393295; // 393297 on Windows 8.1 and older + 'v4.6.1': versionRelease := 394254; // 394271 before Win10 November Update + 'v4.6.2': versionRelease := 394802; // 394806 before Win10 Anniversary Update + 'v4.7': versionRelease := 460798; // 460805 before Win10 Creators Update + 'v4.7.1': versionRelease := 461308; // 461310 before Win10 Fall Creators Update + 'v4.7.2': versionRelease := 461808; // 461814 before Win10 April 2018 Update + 'v4.8': versionRelease := 528040; // 528049 before Win10 May 2019 Update + end; + end; + + // installation key group for all .NET versions + key := 'SOFTWARE\Microsoft\NET Framework Setup\NDP\' + versionKey; + + // .NET 3.0 uses value InstallSuccess in subkey Setup + if Pos('v3.0', version) = 1 then begin + success := RegQueryDWordValue(HKLM, key + '\Setup', 'InstallSuccess', install); + end else begin + success := RegQueryDWordValue(HKLM, key, 'Install', install); + end; + + // .NET 4.0 and newer use value Servicing instead of SP + if Pos('v4', version) = 1 then begin + success := success and RegQueryDWordValue(HKLM, key, 'Servicing', serviceCount); + end else begin + success := success and RegQueryDWordValue(HKLM, key, 'SP', serviceCount); + end; + + // .NET 4.5 and newer use additional value Release + if versionRelease > 0 then begin + success := success and RegQueryDWordValue(HKLM, key, 'Release', release); + success := success and (release >= versionRelease); + end; + + result := success and (install = 1) and (serviceCount >= service); +end; + + +function InitializeSetup(): Boolean; +var + response: Integer; + errorCode: Integer; + +begin + if not IsDotNetDetected('v4.7.2', 0) then + begin + response := SuppressibleMsgBox('Massive Knob requires Microsoft .NET Framework 4.7.2, which does not appear to be installed.' + + 'Do you want to open the .NET download page?'#13#10#13#10 + + 'Click No to continue, although Massive Knob may not run properly afterwards, or Cancel to abort the setup.', + mbInformation, + MB_YESNOCANCEL, + IDNO); + + case response of + IDYES: + begin + ShellExecAsOriginalUser('open', 'https://dotnet.microsoft.com/download/dotnet-framework/net472', '', '', + SW_SHOWNORMAL, ewNoWait, errorCode); + Result := False; + end; + + IDNO: + Result := True; + + IDCANCEL: + Result := False; + end; + end else + Result := True; +end; \ No newline at end of file diff --git a/Windows/VoicemeeterRemote b/Windows/VoicemeeterRemote new file mode 160000 index 0000000..5d259cd --- /dev/null +++ b/Windows/VoicemeeterRemote @@ -0,0 +1 @@ +Subproject commit 5d259cdaee942029487e37a02e9a32ed9833d80c