├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── BUG-REPORT.yml │ ├── FEATURE-REQUEST.yml │ ├── QUESTION.yml │ └── config.yml └── workflows │ └── draft-release.yml ├── .gitignore ├── .release-please-manifest.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── release-please-config.json └── src ├── .editorconfig ├── Directory.Build.props ├── VoiceMeeterPlugin.sln └── VoiceMeeterPlugin ├── Actions ├── Bases │ ├── BooleanBaseCommand.cs │ ├── SingleBaseAdjustment.cs │ └── SingleBaseCommand.cs ├── BusEQCommand.cs ├── BusGainAdjustment.cs ├── BusMonoCommand.cs ├── BusMuteCommand.cs ├── BusSelCommand.cs ├── EjectCommand.cs ├── HardwareInputCompAdjustment.cs ├── HardwareInputDelayAdjustment.cs ├── HardwareInputFxAdjustment.cs ├── HardwareInputGateAdjustment.cs ├── HardwareInputMonoCommand.cs ├── HardwareInputPostDelayCommand.cs ├── HardwareInputPostFxCommand.cs ├── HardwareInputPostReverbCommand.cs ├── HardwareInputReverbAdjustment.cs ├── LevelsCommand.cs ├── LoadCommand.cs ├── RawAdjustment.cs ├── RawCommand.cs ├── ResetCommand.cs ├── RestartCommand.cs ├── ShowCommand.cs ├── ShutdownCommand.cs ├── StripACommand.cs ├── StripBCommand.cs ├── StripGainAdjustment.cs ├── StripMuteCommand.cs ├── StripPanAdjustment.cs ├── StripSoloCommand.cs └── VirtualInputEQGainAdjustment.cs ├── Enums └── ErrorCode.cs ├── Extensions ├── ColorExtensions.cs ├── EnumExtensions.cs └── PluginExtensions.cs ├── Helpers ├── ColorHelper.cs ├── DrawingHelper.cs ├── PluginLog.cs ├── PluginResources.cs └── VoiceMeeterHelper.cs ├── Library └── Voicemeeter │ ├── Defines.cs │ ├── Levels.cs │ ├── Parameters.cs │ ├── Remote.cs │ ├── RemoteWrapper.cs │ ├── VoicemeeterClient.cs │ └── Wrapper.cs ├── Resources └── Images │ └── clear.png ├── Services └── VoiceMeeterService.cs ├── VoiceMeeterApplication.cs ├── VoiceMeeterPlugin.cs ├── VoiceMeeterPlugin.csproj └── metadata ├── Icon256x256.png └── LoupedeckPackage.yaml /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @XeroxDev -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG-REPORT.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "[Bug]: " 4 | labels: ["Type: Bug"] 5 | body: 6 | - type: checkboxes 7 | attributes: 8 | label: Consent 9 | options: 10 | - label: I have searched the existing issues and verified that there is no open issue for the same subject. 11 | required: true 12 | - label: I understand that the Loupedeck VoiceMeeter plugin has NO affiliation with VoiceMeeter.com or VB-Audio 13 | required: true 14 | - type: textarea 15 | attributes: 16 | label: Current Behavior 17 | description: A concise description of what you're experiencing. 18 | validations: 19 | required: true 20 | - type: textarea 21 | attributes: 22 | label: Expected Behavior 23 | description: A concise description of what you expected to happen. 24 | validations: 25 | required: true 26 | - type: textarea 27 | attributes: 28 | label: Steps To Reproduce 29 | description: Steps to reproduce the behavior. 30 | placeholder: | 31 | 1. Go to '...' 32 | 2. Click on '....' 33 | 3. Scroll down to '....' 34 | 4. See error 35 | validations: 36 | required: true 37 | - type: input 38 | attributes: 39 | label: OS 40 | description: "Which OS are you using?" 41 | value: Windows 42 | validations: 43 | required: true 44 | - type: input 45 | attributes: 46 | label: OS Version 47 | description: "Which OS version are you on?" 48 | value: "10" 49 | validations: 50 | required: true 51 | - type: input 52 | attributes: 53 | label: Plugin Version 54 | description: "Which Version does your plugin has?" 55 | placeholder: v1.0.0 56 | validations: 57 | required: true 58 | - type: input 59 | attributes: 60 | label: VoiceMeeter Version 61 | description: "Which Version does your VoiceMeeter application has?" 62 | placeholder: v3.0.1.4 63 | validations: 64 | required: true 65 | - type: dropdown 66 | attributes: 67 | label: How did you download the plugin? 68 | options: 69 | - Loupedeck Store 70 | - GitHub 71 | - Discord 72 | validations: 73 | required: true 74 | - type: textarea 75 | attributes: 76 | label: Anything else? 77 | description: | 78 | Links? References? Anything that will give us more context about the issue you are encountering! 79 | 80 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. 81 | validations: 82 | required: false 83 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE-REQUEST.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for this project 3 | title: "[FEATURE]: <title>" 4 | labels: ["Type: Enhancement"] 5 | body: 6 | - type: checkboxes 7 | attributes: 8 | label: Consent 9 | options: 10 | - label: I have searched the existing issues and verified that there is no open issue for the same subject. 11 | required: true 12 | - label: I understand that the Loupedeck VoiceMeeter plugin has NO affiliation with VoiceMeeter.com or VB-Audio 13 | required: true 14 | - type: textarea 15 | attributes: 16 | label: Feature 17 | description: Describe the feature you'd like 18 | validations: 19 | required: true 20 | - type: textarea 21 | attributes: 22 | label: Solution 23 | description: Describe the solution you'd like 24 | validations: 25 | required: false 26 | - type: textarea 27 | attributes: 28 | label: Alternatives 29 | description: Describe alternatives you've considered 30 | validations: 31 | required: false 32 | - type: textarea 33 | attributes: 34 | label: Anything else? 35 | description: | 36 | Links? References? Anything that will give us more context about the feature you are wish to add! 37 | 38 | Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. 39 | validations: 40 | required: false 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/QUESTION.yml: -------------------------------------------------------------------------------- 1 | name: Ask a Question 2 | description: You have questions? Please let us know! 3 | title: "[QUESTION]: <title>" 4 | labels: ["Type: Question"] 5 | body: 6 | - type: checkboxes 7 | attributes: 8 | label: Consent 9 | options: 10 | - label: I have searched the existing issues and verified that there is no open issue for the same subject. 11 | required: true 12 | - label: I understand that the Loupedeck VoiceMeeter plugin has NO affiliation with VoiceMeeter.com or VB-Audio 13 | required: true 14 | - type: textarea 15 | attributes: 16 | label: Question 17 | description: What is your question? 18 | validations: 19 | required: true 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /.github/workflows/draft-release.yml: -------------------------------------------------------------------------------- 1 | name: Draft Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: write 10 | pull-requests: write 11 | 12 | jobs: 13 | draft-release: 14 | if: "!contains(github.event.head_commit.message, 'chore(main): release')" 15 | runs-on: windows-latest 16 | steps: 17 | - name: Check out Git repository 18 | uses: actions/checkout@v4 19 | with: 20 | path: './' 21 | fetch-depth: 0 22 | - name: release-please 23 | uses: googleapis/release-please-action@v4 24 | id: release 25 | with: 26 | target-branch: main -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.tlog 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 298 | *.vbp 299 | 300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 301 | *.dsw 302 | *.dsp 303 | 304 | # Visual Studio 6 technical files 305 | *.ncb 306 | *.aps 307 | 308 | # Visual Studio LightSwitch build output 309 | **/*.HTMLClient/GeneratedArtifacts 310 | **/*.DesktopClient/GeneratedArtifacts 311 | **/*.DesktopClient/ModelManifest.xml 312 | **/*.Server/GeneratedArtifacts 313 | **/*.Server/ModelManifest.xml 314 | _Pvt_Extensions 315 | 316 | # Paket dependency manager 317 | .paket/paket.exe 318 | paket-files/ 319 | 320 | # FAKE - F# Make 321 | .fake/ 322 | 323 | # CodeRush personal settings 324 | .cr/personal 325 | 326 | # Python Tools for Visual Studio (PTVS) 327 | __pycache__/ 328 | *.pyc 329 | 330 | # Cake - Uncomment if you are using it 331 | # tools/** 332 | # !tools/packages.config 333 | 334 | # Tabs Studio 335 | *.tss 336 | 337 | # Telerik's JustMock configuration file 338 | *.jmconfig 339 | 340 | # BizTalk build output 341 | *.btp.cs 342 | *.btm.cs 343 | *.odx.cs 344 | *.xsd.cs 345 | 346 | # OpenCover UI analysis results 347 | OpenCover/ 348 | 349 | # Azure Stream Analytics local run output 350 | ASALocalRun/ 351 | 352 | # MSBuild Binary and Structured Log 353 | *.binlog 354 | 355 | # NVidia Nsight GPU debugger configuration file 356 | *.nvuser 357 | 358 | # MFractors (Xamarin productivity tool) working folder 359 | .mfractor/ 360 | 361 | # Local History for Visual Studio 362 | .localhistory/ 363 | 364 | # Visual Studio History (VSHistory) files 365 | .vshistory/ 366 | 367 | # BeatPulse healthcheck temp database 368 | healthchecksdb 369 | 370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 371 | MigrationBackup/ 372 | 373 | # Ionide (cross platform F# VS Code tools) working folder 374 | .ionide/ 375 | 376 | # Fody - auto-generated XML schema 377 | FodyWeavers.xsd 378 | 379 | # VS Code files for those working on multiple tools 380 | .vscode/* 381 | !.vscode/settings.json 382 | !.vscode/tasks.json 383 | !.vscode/launch.json 384 | !.vscode/extensions.json 385 | *.code-workspace 386 | 387 | # Local History for Visual Studio Code 388 | .history/ 389 | 390 | # Windows Installer files from build outputs 391 | *.cab 392 | *.msi 393 | *.msix 394 | *.msm 395 | *.msp 396 | 397 | # JetBrains Rider 398 | *.sln.iml 399 | .idea_modules/ 400 | .idea/ 401 | !.idea/codeStyles 402 | !.idea/runConfigurations -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "4.1.0" 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [4.1.0](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/compare/v4.0.1...v4.1.0) (2025-03-25) 6 | 7 | 8 | ### Features 9 | 10 | * add level meters display ([dd016ef](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/commit/dd016ef4a85bab7c459b566ec7abdac6b90f12ba)) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * actions don't get loaded due to api changes ([a9de069](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/commit/a9de0693930b7d4339fa518e7f2623b427f53efe)) 16 | 17 | 18 | ### Performance Improvements 19 | 20 | * optimize performance for image drawing and levels update ([0dbcfcb](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/commit/0dbcfcbe803f3b28421c4c549130eed656f5c708)) 21 | 22 | ## [4.0.1](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/compare/v4.0.0...v4.0.1) (2024-10-03) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * raw command doesn't update from outside sources ([72cb55f](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/commit/72cb55fa810c037d6c1ad2af794e6a4e15665c9a)) 28 | 29 | ## [4.0.0](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/compare/v3.0.0...v4.0.0) (2024-09-28) 30 | 31 | 32 | ### ⚠ BREAKING CHANGES 33 | 34 | * migration to v6 ([#24](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/issues/24)) 35 | 36 | ### Features 37 | 38 | * add raw commands and adjustments ([#27](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/issues/27)) ([676b152](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/commit/676b1525b5835a775441cab64e3a161dc6a07087)) 39 | * migration to v6 ([#24](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/issues/24)) ([56b05fd](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/commit/56b05fd36c396e538b7b496fc84528b8e04dd60a)) 40 | 41 | ## v3.0.0 (2023-02-26) 42 | 43 | ### BREAKING CHANGE 44 | 45 | - Due to the structure change, all actions have to be added again. We're sorry for 46 | the inconvenience! 47 | 48 | ### Feat 49 | 50 | - add multi-state possibility 51 | - remoddeled volume bar 52 | - add command name to the bottom of adjustment bars 53 | - add bus sel command 54 | - named adjustment commands with mute indicator 55 | - Load command support (#13) 56 | - Pan_x, Pan_y adjustment support (#11) 57 | 58 | ### Fix 59 | 60 | - volume bar just shows 'Gain' 61 | - strips/buses recognition breaks due to faulty characters 62 | - Fix a bug that could cause InvalidOperationException in foreach statements. (#14) 63 | - give every icon the right number (#10) 64 | 65 | ### Refactor 66 | 67 | - replace fontSize with cmdSize 68 | - update plugin to latest structure 69 | - change volume bar text to be at the top 70 | - replace .Drawings with SkiaSharp 71 | 72 | ## v2.0.1 (2022-05-16) 73 | 74 | ### Fix 75 | 76 | - give every icon the right color 77 | - icons update again 78 | 79 | ## [2.0.0](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/compare/v1.0.0...v2.0.0) (2022-04-04) 80 | 81 | 82 | ### ⚠ BREAKING CHANGES 83 | 84 | * This code will only run in Loupedeck 5 due to api changes 85 | 86 | ### Features 87 | 88 | * Loupedeck 5 Beta support ([72811bd](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/commit/72811bd594e6d49bcf7c80f73013ee5b24bc7031)) 89 | * min / max value limiting for encoders ([#8](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/issues/8)) ([c8df8d8](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/commit/c8df8d8d56dc11053ad03e8170c53b947ce7ade0)) 90 | * only trigger updates for the specific values that change ([#6](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/issues/6)) ([36d2476](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/commit/36d2476dfcfa316b7afcd5ecb1d9c65b6311da40)) 91 | * the icons are reflecting now the color scheme from VoiceMeeter ([8246d01](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/commit/8246d01050efcc10699bf25b75a56f7340462631)) 92 | 93 | 94 | ### Bug Fixes 95 | 96 | * better detection of voicemeeter version if setup exe was differently named ([#9](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/issues/9)) ([cec3232](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/commit/cec3232754a93103b258ff83a652d2c0373e5522)) 97 | * volume sliders should look normal again ([b9ae829](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/commit/b9ae829d4cfeb33070dba785f8838226a1ee1b70)) 98 | 99 | ## 1.0.0 (2021-12-08) 100 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Dominic "XeroxDev" Ris 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 1. Table of content 2 | - [1. Table of content](#1-table-of-content) 3 | - [2. Badges](#2-badges) 4 | - [3. What is this Plugin?](#3-what-is-this-plugin) 5 | - [4. Support / Feedback](#4-support--feedback) 6 | - [5. How to use it?](#5-how-to-use-it) 7 | - [6. How to contribute?](#6-how-to-contribute) 8 | 9 | # 2. Badges 10 | [![Forks](https://img.shields.io/github/forks/XeroxDev/Loupedeck-plugin-VoiceMeeter?color=blue&style=for-the-badge)](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/network/members) 11 | [![Stars](https://img.shields.io/github/stars/XeroxDev/Loupedeck-plugin-VoiceMeeter?color=yellow&style=for-the-badge)](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/stargazers) 12 | [![Watchers](https://img.shields.io/github/watchers/XeroxDev/Loupedeck-plugin-VoiceMeeter?color=lightgray&style=for-the-badge)](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/watchers) 13 | [![Contributors](https://img.shields.io/github/contributors/XeroxDev/Loupedeck-plugin-VoiceMeeter?color=green&style=for-the-badge)](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/graphs/contributors) 14 | 15 | [![Issues](https://img.shields.io/github/issues/XeroxDev/Loupedeck-plugin-VoiceMeeter?color=yellow&style=for-the-badge)](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/issues) 16 | [![Issues closed](https://img.shields.io/github/issues-closed/XeroxDev/Loupedeck-plugin-VoiceMeeter?color=yellow&style=for-the-badge)](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/issues?q=is%3Aissue+is%3Aclosed) 17 | 18 | [![Issues-pr](https://img.shields.io/github/issues-pr/XeroxDev/Loupedeck-plugin-VoiceMeeter?color=yellow&style=for-the-badge)](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/pulls) 19 | [![Issues-pr closed](https://img.shields.io/github/issues-pr-closed/XeroxDev/Loupedeck-plugin-VoiceMeeter?color=yellow&style=for-the-badge)](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/pulls?q=is%3Apr+is%3Aclosed) 20 | [![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge)](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/compare) 21 | 22 | <!-- [![Build](https://img.shields.io/github/workflow/status/XeroxDev/Loupedeck-plugin-VoiceMeeter/CI-CD?style=for-the-badge)](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/actions?query=workflow%3A%22CI-CD%22) --> 23 | [![Release](https://img.shields.io/github/release/XeroxDev/Loupedeck-plugin-VoiceMeeter?color=black&style=for-the-badge)](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/releases) 24 | [![Downloads](https://img.shields.io/github/downloads/XeroxDev/Loupedeck-plugin-VoiceMeeter/total.svg?color=cyan&style=for-the-badge&logo=github)]() 25 | 26 | [![Awesome Badges](https://img.shields.io/badge/badges-awesome-green?style=for-the-badge)](https://shields.io) 27 | 28 | # 3. What is this Plugin? 29 | This Loupedeck Plugin allows you to control the [VoiceMeeter](https://voicemeeter.com) application 30 | 31 | # 4. Support / Feedback 32 | You found a bug? You have a feature request? I would love to hear about it [here](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/issues/new/choose) or click on the "Issues" tab here on the GitHub repositorie! 33 | 34 | You can also join my discord [here](https://s.tswi.me/discord) 35 | 36 | # 5. How to use it? 37 | 38 | 1. Install the [VoiceMeeter](https://voicemeeter.com) App 39 | 2. Install the Plugin 40 | 3. Add Actions to Loupedeck 41 | 4. Have fun controlling your voicemeeter 42 | 43 | # 6. How to contribute? 44 | 45 | Just fork the repository and create PR's. 46 | 47 | > [!NOTE] 48 | > We're using [release-please](https://github.com/googleapis/release-please) to optimal release the library. 49 | > release-please is following the [conventionalcommits](https://www.conventionalcommits.org) specification. -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": { 3 | ".": { 4 | "package-name": "VoiceMeeterPlugin", 5 | "component": "VoiceMeeterPlugin", 6 | "changelog-path": "CHANGELOG.md", 7 | "release-type": "simple", 8 | "draft": true, 9 | "prerelease": false, 10 | "include-component-in-tag": false, 11 | "extra-files": [ 12 | "src/VoiceMeeterPlugin/VoiceMeeterPlugin.csproj", 13 | "src/VoiceMeeterPlugin/metadata/LoupedeckPackage.yaml" 14 | ] 15 | } 16 | }, 17 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json" 18 | } -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | # Remove the line below if you want to inherit .editorconfig settings from higher directories 2 | root = true 3 | 4 | # C# files 5 | [*.cs] 6 | 7 | #### Core EditorConfig Options #### 8 | 9 | # Indentation and spacing 10 | indent_size = 4 11 | indent_style = space 12 | tab_width = 4 13 | 14 | # New line preferences 15 | end_of_line = crlf 16 | insert_final_newline = false 17 | 18 | #### .NET Coding Conventions #### 19 | 20 | # Organize usings 21 | dotnet_separate_import_directive_groups = true 22 | dotnet_sort_system_directives_first = true 23 | 24 | # this. and Me. preferences 25 | dotnet_style_qualification_for_event = true:warning 26 | dotnet_style_qualification_for_field = true:warning 27 | dotnet_style_qualification_for_method = true:warning 28 | dotnet_style_qualification_for_property = true:warning 29 | 30 | # Language keywords vs BCL types preferences 31 | dotnet_style_predefined_type_for_locals_parameters_members = false:warning 32 | dotnet_style_predefined_type_for_member_access = false:warning 33 | 34 | # Parentheses preferences 35 | dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:warning 36 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning 37 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:warning 38 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning 39 | 40 | # Modifier preferences 41 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent 42 | 43 | # Expression-level preferences 44 | csharp_style_deconstructed_variable_declaration = true:suggestion 45 | csharp_style_inlined_variable_declaration = true:warning 46 | csharp_style_throw_expression = true:warning 47 | dotnet_style_coalesce_expression = true:warning 48 | dotnet_style_collection_initializer = true:warning 49 | dotnet_style_explicit_tuple_names = true:warning 50 | dotnet_style_null_propagation = true:warning 51 | dotnet_style_object_initializer = false:suggestion 52 | dotnet_style_prefer_auto_properties = true:warning 53 | dotnet_style_prefer_compound_assignment = true:warning 54 | dotnet_style_prefer_conditional_expression_over_assignment = true:warning 55 | dotnet_style_prefer_conditional_expression_over_return = true:warning 56 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 57 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 58 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning 59 | 60 | # Field preferences 61 | dotnet_style_readonly_field = true:warning 62 | 63 | # Parameter preferences 64 | dotnet_code_quality_unused_parameters = all:warning 65 | 66 | #### C# Coding Conventions #### 67 | 68 | # var preferences 69 | csharp_style_var_elsewhere = true:silent 70 | csharp_style_var_for_built_in_types = true:warning 71 | csharp_style_var_when_type_is_apparent = true:warning 72 | 73 | # Expression-bodied members 74 | csharp_style_expression_bodied_accessors = when_on_single_line:suggestion 75 | csharp_style_expression_bodied_constructors = when_on_single_line:suggestion 76 | csharp_style_expression_bodied_indexers = when_on_single_line:suggestion 77 | csharp_style_expression_bodied_lambdas = when_on_single_line:suggestion 78 | csharp_style_expression_bodied_local_functions = when_on_single_line:suggestion 79 | csharp_style_expression_bodied_methods = when_on_single_line:suggestion 80 | csharp_style_expression_bodied_operators = when_on_single_line:suggestion 81 | csharp_style_expression_bodied_properties = when_on_single_line:suggestion 82 | 83 | # Pattern matching preferences 84 | csharp_style_pattern_matching_over_as_with_null_check = true:warning 85 | csharp_style_pattern_matching_over_is_with_cast_check = true:warning 86 | 87 | # Null-checking preferences 88 | csharp_style_conditional_delegate_call = true:warning 89 | 90 | # Modifier preferences 91 | csharp_prefer_static_local_function = false:warning 92 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async 93 | 94 | # Code-block preferences 95 | csharp_prefer_braces = true:warning 96 | csharp_prefer_simple_using_statement = false:warning 97 | 98 | # Expression-level preferences 99 | csharp_prefer_simple_default_expression = true:warning 100 | csharp_style_pattern_local_over_anonymous_function = true:warning 101 | csharp_style_prefer_index_operator = true:suggestion 102 | csharp_style_prefer_range_operator = true:suggestion 103 | csharp_style_unused_value_assignment_preference = discard_variable:suggestion 104 | csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion 105 | 106 | # 'using' directive preferences 107 | csharp_using_directive_placement = inside_namespace:warning 108 | 109 | #### C# Formatting Rules #### 110 | 111 | # New line preferences 112 | csharp_new_line_before_catch = true 113 | csharp_new_line_before_else = true 114 | csharp_new_line_before_finally = true 115 | csharp_new_line_before_members_in_anonymous_types = true 116 | csharp_new_line_before_members_in_object_initializers = true 117 | csharp_new_line_before_open_brace = all 118 | csharp_new_line_between_query_expression_clauses = true 119 | 120 | # Indentation preferences 121 | csharp_indent_block_contents = true 122 | csharp_indent_braces = false 123 | csharp_indent_case_contents = true 124 | csharp_indent_case_contents_when_block = false 125 | csharp_indent_labels = one_less_than_current 126 | csharp_indent_switch_labels = true 127 | 128 | # Space preferences 129 | csharp_space_after_cast = false 130 | csharp_space_after_colon_in_inheritance_clause = true 131 | csharp_space_after_comma = true 132 | csharp_space_after_dot = false 133 | csharp_space_after_keywords_in_control_flow_statements = true 134 | csharp_space_after_semicolon_in_for_statement = true 135 | csharp_space_around_binary_operators = before_and_after 136 | csharp_space_around_declaration_statements = false 137 | csharp_space_before_colon_in_inheritance_clause = true 138 | csharp_space_before_comma = false 139 | csharp_space_before_dot = false 140 | csharp_space_before_open_square_brackets = false 141 | csharp_space_before_semicolon_in_for_statement = false 142 | csharp_space_between_empty_square_brackets = false 143 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 144 | csharp_space_between_method_call_name_and_opening_parenthesis = false 145 | csharp_space_between_method_call_parameter_list_parentheses = false 146 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 147 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 148 | csharp_space_between_method_declaration_parameter_list_parentheses = false 149 | csharp_space_between_parentheses = false 150 | csharp_space_between_square_brackets = false 151 | 152 | # Wrapping preferences 153 | csharp_preserve_single_line_blocks = true 154 | csharp_preserve_single_line_statements = false 155 | 156 | #### Naming styles #### 157 | 158 | # Naming rules 159 | 160 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion 161 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface 162 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i 163 | 164 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion 165 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types 166 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case 167 | 168 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion 169 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members 170 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case 171 | 172 | # Symbol specifications 173 | 174 | dotnet_naming_symbols.interface.applicable_kinds = interface 175 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal 176 | dotnet_naming_symbols.interface.required_modifiers = 177 | 178 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum 179 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal 180 | dotnet_naming_symbols.types.required_modifiers = 181 | 182 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 183 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal 184 | dotnet_naming_symbols.non_field_members.required_modifiers = 185 | 186 | # Naming styles 187 | 188 | dotnet_naming_style.pascal_case.required_prefix = 189 | dotnet_naming_style.pascal_case.required_suffix = 190 | dotnet_naming_style.pascal_case.word_separator = 191 | dotnet_naming_style.pascal_case.capitalization = pascal_case 192 | 193 | dotnet_naming_style.begins_with_i.required_prefix = I 194 | dotnet_naming_style.begins_with_i.required_suffix = 195 | dotnet_naming_style.begins_with_i.word_separator = 196 | dotnet_naming_style.begins_with_i.capitalization = pascal_case 197 | 198 | #### Diagnostic configuration #### 199 | 200 | dotnet_diagnostic.CA1028.severity = none 201 | dotnet_diagnostic.CA1031.severity = none 202 | dotnet_diagnostic.CA1054.severity = none 203 | dotnet_diagnostic.CA1056.severity = none 204 | dotnet_diagnostic.CA1060.severity = none 205 | dotnet_diagnostic.CA1303.severity = none 206 | dotnet_diagnostic.CA1308.severity = none 207 | dotnet_diagnostic.CA1716.severity = none 208 | dotnet_diagnostic.CA1720.severity = none 209 | dotnet_diagnostic.CA2101.severity = none 210 | dotnet_diagnostic.CA2234.severity = none 211 | dotnet_diagnostic.CA5350.severity = none 212 | dotnet_diagnostic.CA9998.severity = none 213 | 214 | dotnet_diagnostic.CS1591.severity = none 215 | 216 | dotnet_diagnostic.IDE0002.severity = none 217 | dotnet_diagnostic.IDE0021.severity = warning 218 | dotnet_diagnostic.IDE0022.severity = warning 219 | dotnet_diagnostic.IDE0058.severity = none 220 | dotnet_diagnostic.IDE0059.severity = none 221 | 222 | dotnet_diagnostic.SA1108.severity = none 223 | dotnet_diagnostic.SA1117.severity = none 224 | dotnet_diagnostic.SA1121.severity = none 225 | dotnet_diagnostic.SA1122.severity = none 226 | dotnet_diagnostic.SA1131.severity = none 227 | dotnet_diagnostic.SA1201.severity = none 228 | dotnet_diagnostic.SA1202.severity = none 229 | dotnet_diagnostic.SA1203.severity = none 230 | dotnet_diagnostic.SA1204.severity = none 231 | dotnet_diagnostic.SA1214.severity = none 232 | dotnet_diagnostic.SA1309.severity = none 233 | dotnet_diagnostic.SA1512.severity = none 234 | dotnet_diagnostic.SA1513.severity = none 235 | dotnet_diagnostic.SA1600.severity = none 236 | dotnet_diagnostic.SA1601.severity = none 237 | dotnet_diagnostic.SA1602.severity = none 238 | dotnet_diagnostic.SA1629.severity = none 239 | dotnet_diagnostic.SA1633.severity = none 240 | 241 | dotnet_diagnostic.SX1309.severity = warning 242 | dotnet_diagnostic.SX1309S.severity = warning 243 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | <Project> 2 | <PropertyGroup> 3 | <LangVersion>latest</LangVersion> 4 | <BaseIntermediateOutputPath>$(SolutionDir)..\obj\</BaseIntermediateOutputPath> 5 | <BaseOutputPath>$(SolutionDir)..\bin\</BaseOutputPath> 6 | </PropertyGroup> 7 | </Project> 8 | -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.8.34408.163 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VoiceMeeterPlugin", "VoiceMeeterPlugin\VoiceMeeterPlugin.csproj", "{CAB60883-9DB0-4C86-817C-A9F5E8B7389B}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {CAB60883-9DB0-4C86-817C-A9F5E8B7389B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {CAB60883-9DB0-4C86-817C-A9F5E8B7389B}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {CAB60883-9DB0-4C86-817C-A9F5E8B7389B}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {CAB60883-9DB0-4C86-817C-A9F5E8B7389B}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {0E15B716-1D2C-45CB-B731-E5A3A4741D44} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/Bases/BooleanBaseCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Actions.Bases 2 | { 3 | using System.Reactive.Linq; 4 | using System.Reactive.Subjects; 5 | 6 | using Extensions; 7 | 8 | using Helpers; 9 | 10 | using Library.Voicemeeter; 11 | 12 | using Services; 13 | 14 | using SkiaSharp; 15 | 16 | public class BooleanBaseCommand : PluginMultistateDynamicCommand 17 | { 18 | private enum VMStates 19 | { 20 | Off, 21 | On 22 | } 23 | 24 | private Dictionary<Int32, Boolean[]> Actions { get; } = new(); 25 | private VoiceMeeterService VmService { get; } 26 | private Boolean IsMultiAction { get; set; } 27 | private String Command { get; set; } 28 | private Subject<Boolean> OnDestroy { get; } = new(); 29 | public Boolean IsRealClass { get; set; } 30 | private Boolean IsStrip { get; } 31 | private Int32 Offset { get; set; } 32 | private SKColor ActiveColor { get; } 33 | private SKColor InactiveColor { get; } 34 | protected Boolean Loaded { get; set; } 35 | 36 | public BooleanBaseCommand(Boolean isRealClass, Boolean isStrip, SKColor? activeColor = null, SKColor? inactiveColor = null) 37 | { 38 | foreach (var state in Enum.GetValues(typeof(VMStates))) 39 | { 40 | this.AddState(state.ToString(), $"If the action is {state}"); 41 | } 42 | 43 | this.IsRealClass = isRealClass; 44 | this.IsStrip = isStrip; 45 | this.ActiveColor = activeColor ?? ColorHelper.Active; 46 | this.InactiveColor = inactiveColor ?? ColorHelper.Inactive; 47 | if (!this.IsRealClass) 48 | { 49 | return; 50 | } 51 | 52 | this.VmService = VoiceMeeterService.Instance; 53 | } 54 | 55 | public async Task CreateCommands(Int32 stripCount, String cmd, Int32 specialCount, Int32 offset, 56 | String displayName = null) 57 | { 58 | this.IsMultiAction = true; 59 | this.Command = cmd; 60 | this.Offset = offset; 61 | this.DisplayName = displayName ?? cmd; 62 | while (!this.VmService.Connected) 63 | { 64 | await Task.Delay(1000); 65 | } 66 | 67 | for (var hi = 0; hi < stripCount; hi++) 68 | { 69 | this.Actions.Add(hi, new Boolean[specialCount]); 70 | for (var special = 1; special <= specialCount; special++) 71 | { 72 | var name = Remote.GetTextParameter($"{(this.IsStrip ? "Strip" : "Bus")}[{hi + this.Offset}].Label"); 73 | var groupName = String.IsNullOrEmpty(name) ? this.IsStrip ? "Strip" : "Bus" : name; 74 | this.AddParameter( 75 | GetActionParameterName(hi, cmd, special), 76 | $"{this.DisplayName}{special}", 77 | $"{groupName} ({hi + 1 + this.Offset})" 78 | ); 79 | } 80 | } 81 | 82 | this.GetNewSettings(); 83 | } 84 | 85 | public async Task CreateCommands(Int32 stripCount, String cmd, Int32 offset, String displayName = null) 86 | { 87 | this.IsMultiAction = false; 88 | this.Command = cmd; 89 | this.Offset = offset; 90 | this.DisplayName = displayName ?? cmd; 91 | while (!this.VmService.Connected) 92 | { 93 | await Task.Delay(1000); 94 | } 95 | 96 | this.Actions.Add(0, new Boolean[stripCount]); 97 | for (var hi = 0; hi < stripCount; hi++) 98 | { 99 | var name = Remote.GetTextParameter($"{(this.IsStrip ? "Strip" : "Bus")}[{hi + this.Offset}].Label"); 100 | var groupName = String.IsNullOrEmpty(name) ? this.IsStrip ? "Strip" : "Bus" : name; 101 | this.AddParameter( 102 | GetActionParameterName(hi, cmd), 103 | this.DisplayName, 104 | $"{groupName} ({hi + 1 + this.Offset})" 105 | ); 106 | } 107 | 108 | this.GetNewSettings(); 109 | } 110 | 111 | private static String GetActionParameterName(Int32 stripNumber, String cmd, Int32? specialNumber = null) => 112 | specialNumber == null ? $"VM-Strip{stripNumber}-{cmd}2147483647" : $"VM-Strip{stripNumber}-{cmd}{specialNumber.Value}"; 113 | 114 | 115 | private void GetNewSettings() 116 | { 117 | if (this.IsMultiAction) 118 | { 119 | for (var hiIndex = 0; hiIndex < this.Actions.Count; hiIndex++) 120 | { 121 | var hi = this.Actions[hiIndex]; 122 | for (var index = 1; index <= hi.Length; index++) 123 | { 124 | var oldValue = hi[index - 1]; 125 | var newValue = (Int32)Remote.GetParameter($"{(this.IsStrip ? "Strip" : "Bus")}[{hiIndex + this.Offset}].{this.Command}{index}") == 1; 126 | hi[index - 1] = newValue; 127 | 128 | if (!this.Loaded || oldValue == hi[index - 1]) 129 | { 130 | continue; 131 | } 132 | 133 | var actionParameter = GetActionParameterName(hiIndex, this.Command, index); 134 | this.SetCurrentState(actionParameter, newValue ? VMStates.On.ToInt() : VMStates.Off.ToInt()); 135 | this.ActionImageChanged(actionParameter); 136 | } 137 | } 138 | } 139 | else 140 | { 141 | var hi = this.Actions[0]; 142 | for (var index = 0; index < hi.Length; index++) 143 | { 144 | var oldValue = hi[index]; 145 | var newValue = (Int32)Remote.GetParameter($"{(this.IsStrip ? "Strip" : "Bus")}[{index + this.Offset}].{this.Command}") == 1; 146 | hi[index] = newValue; 147 | if (!this.Loaded || oldValue == hi[index]) 148 | { 149 | continue; 150 | } 151 | 152 | var actionParameter = GetActionParameterName(index, this.Command); 153 | this.SetCurrentState(actionParameter, newValue ? VMStates.On.ToInt() : VMStates.Off.ToInt()); 154 | this.ActionImageChanged(actionParameter); 155 | } 156 | } 157 | } 158 | 159 | protected override Boolean OnLoad() 160 | { 161 | this.Loaded = true; 162 | if (!this.IsRealClass) 163 | { 164 | return base.OnLoad(); 165 | } 166 | 167 | this.VmService.Parameters 168 | .TakeUntil(this.OnDestroy) 169 | .Subscribe(_ => this.GetNewSettings()); 170 | 171 | return base.OnLoad(); 172 | } 173 | 174 | protected override Boolean OnUnload() 175 | { 176 | if (!this.IsRealClass) 177 | { 178 | return base.OnLoad(); 179 | } 180 | 181 | this.OnDestroy.OnNext(true); 182 | return base.OnUnload(); 183 | } 184 | 185 | protected override void RunCommand(String actionParameter) 186 | { 187 | if (!this.IsRealClass) 188 | { 189 | return; 190 | } 191 | 192 | this.GetButton(actionParameter, out var mainIndex, out var action, out var actionIndex); 193 | 194 | if (mainIndex == -1 || action == -1) 195 | { 196 | return; 197 | } 198 | 199 | var state = this.Actions[mainIndex][actionIndex] ? VMStates.Off : VMStates.On; 200 | 201 | Remote.SetParameter( 202 | this.IsMultiAction 203 | ? $"{(this.IsStrip ? "Strip" : "Bus")}[{mainIndex + this.Offset}].{this.Command}{action}" 204 | : $"{(this.IsStrip ? "Strip" : "Bus")}[{action + this.Offset}].{this.Command}", 205 | state.ToInt()); 206 | 207 | this.SetCurrentState(actionParameter, state.ToInt()); 208 | this.ActionImageChanged(actionParameter); 209 | } 210 | 211 | protected override String GetCommandDisplayName(String actionParameter, Int32 state, PluginImageSize imageSize) 212 | { 213 | if (!this.IsRealClass || actionParameter == null) 214 | { 215 | return null; 216 | } 217 | 218 | return $"{this.DisplayName}\n{(VMStates.On.CompareInt(state) ? "On" : "Off")}"; 219 | } 220 | 221 | protected override BitmapImage GetCommandImage(String actionParameter, Int32 state, PluginImageSize imageSize) 222 | { 223 | if (!this.IsRealClass) 224 | { 225 | return null; 226 | } 227 | 228 | this.GetButton(actionParameter, out var mainIndex, out var action, out _); 229 | 230 | if (mainIndex == -1 || action == -1) 231 | { 232 | return null; 233 | } 234 | 235 | var actionString = this.IsMultiAction 236 | ? $"{(this.IsStrip ? "Strip" : "Bus")}[{mainIndex + this.Offset}]" 237 | : $"{(this.IsStrip ? "Strip" : "Bus")}[{action + this.Offset}]"; 238 | var name = Remote.GetTextParameter($"{actionString}.Label"); 239 | if (String.IsNullOrEmpty(name)) 240 | { 241 | name = this.IsMultiAction 242 | ? $"{(this.IsStrip ? "Strip" : "Bus")} {mainIndex + 1 + this.Offset}" 243 | : $"{(this.IsStrip ? "Strip" : "Bus")} {action + 1 + this.Offset}"; 244 | } 245 | 246 | return DrawingHelper.DrawDefaultImage(this.IsMultiAction ? $"{this.DisplayName}{action}" : this.DisplayName, name, VMStates.On.CompareInt(state) ? this.ActiveColor : this.InactiveColor); 247 | } 248 | 249 | private void GetButton(String actionParameter, out Int32 mainIndex, out Int32 action, out Int32 actionIndex) 250 | { 251 | var splitted = actionParameter.Replace("VM-Strip", "").Replace(this.Command, "").Split('-'); 252 | 253 | var firstSplitTruthy = Int32.TryParse(splitted[0], out var first); 254 | var secondSplitTruthy = Int32.TryParse(splitted[1], out var second); 255 | if (!firstSplitTruthy || !secondSplitTruthy) 256 | { 257 | mainIndex = -1; 258 | action = -1; 259 | actionIndex = -1; 260 | } 261 | 262 | if (second != 2147483647) 263 | { 264 | mainIndex = first; 265 | action = second; 266 | actionIndex = second - 1; 267 | } 268 | else 269 | { 270 | mainIndex = 0; 271 | action = first; 272 | actionIndex = first; 273 | } 274 | } 275 | } 276 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/Bases/SingleBaseAdjustment.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Actions.Bases 2 | { 3 | using System.Reactive.Linq; 4 | using System.Reactive.Subjects; 5 | 6 | using Extensions; 7 | 8 | using Helpers; 9 | 10 | using Library.Voicemeeter; 11 | 12 | using Services; 13 | 14 | public class SingleBaseAdjustment : PluginDynamicAdjustment 15 | { 16 | private AdjustmentItem[] Actions { get; set; } 17 | private VoiceMeeterService VmService { get; } 18 | private String Command { get; set; } 19 | private Subject<Boolean> OnDestroy { get; } = new(); 20 | private Int32 Offset { get; set; } 21 | private Boolean IsStrip { get; } 22 | private Int32 MaxValue { get; } 23 | private Int32 MinValue { get; } 24 | private Int32 ScaleFactor { get; } 25 | public Boolean IsRealClass { get; set; } 26 | protected Boolean Loaded { get; set; } 27 | 28 | public SingleBaseAdjustment(Boolean hasRestart, Boolean isRealClass, Boolean isStrip, Int32 minValue = 0, 29 | Int32 maxValue = 10, Int32 scaleFactor = 1) : base(hasRestart) 30 | { 31 | this.IsRealClass = isRealClass; 32 | this.IsStrip = isStrip; 33 | 34 | this.MinValue = minValue; 35 | this.MaxValue = maxValue; 36 | this.ScaleFactor = scaleFactor; 37 | 38 | if (!this.IsRealClass) 39 | { 40 | return; 41 | } 42 | 43 | this.VmService = VoiceMeeterService.Instance; 44 | } 45 | 46 | public async Task CreateCommands(Int32 stripCount, String cmd, Int32 offset, String displayName = null) 47 | { 48 | this.Command = cmd; 49 | this.Offset = offset; 50 | this.DisplayName = displayName ?? cmd; 51 | while (!this.VmService.Connected) 52 | { 53 | await Task.Delay(1000); 54 | } 55 | 56 | this.Actions = new AdjustmentItem[stripCount]; 57 | for (var hi = 0; hi < stripCount; hi++) 58 | { 59 | var name = Remote.GetTextParameter($"{(this.IsStrip ? "Strip" : "Bus")}[{hi + this.Offset}].Label"); 60 | var groupName = String.IsNullOrEmpty(name) ? this.IsStrip ? "Strip" : "Bus" : name; 61 | this.AddParameter( 62 | GetActionParameterName(hi, cmd), 63 | this.DisplayName, 64 | $"{groupName} ({hi + 1 + this.Offset})" 65 | ); 66 | } 67 | 68 | this.GetNewSettings(); 69 | } 70 | 71 | private static String GetActionParameterName(Int32 stripNumber, String cmd) => $"VM-Strip{stripNumber}-{cmd}"; 72 | 73 | private void GetNewSettings() 74 | { 75 | for (var hiIndex = 0; hiIndex < this.Actions.Length; hiIndex++) 76 | { 77 | var oldItem = this.Actions[hiIndex] ?? new AdjustmentItem(); 78 | 79 | var param = $"{(this.IsStrip ? "Strip" : "Bus")}[{hiIndex + this.Offset}]"; 80 | 81 | var newItem = new AdjustmentItem 82 | { 83 | Value = (Int32)(Remote.GetParameter($"{param}.{this.Command}") * this.ScaleFactor), 84 | Name = Remote.GetTextParameter($"{param}.Label"), 85 | IsMuted = Remote.GetParameter($"{param}.Mute") > 0, 86 | }; 87 | 88 | this.Actions[hiIndex] = newItem; 89 | 90 | if (this.Loaded && (oldItem.Value != newItem.Value || oldItem.Name != newItem.Name || oldItem.IsMuted != newItem.IsMuted)) 91 | { 92 | this.AdjustmentValueChanged(GetActionParameterName(hiIndex, this.Command)); 93 | } 94 | } 95 | } 96 | 97 | protected override Boolean OnLoad() 98 | { 99 | this.Loaded = true; 100 | if (!this.IsRealClass) 101 | { 102 | return base.OnLoad(); 103 | } 104 | 105 | this.VmService.Parameters 106 | .TakeUntil(this.OnDestroy) 107 | .Subscribe(_ => this.GetNewSettings()); 108 | 109 | return base.OnLoad(); 110 | } 111 | 112 | protected override Boolean OnUnload() 113 | { 114 | if (!this.IsRealClass) 115 | { 116 | return base.OnLoad(); 117 | } 118 | 119 | this.OnDestroy.OnNext(true); 120 | return base.OnUnload(); 121 | } 122 | 123 | protected override void RunCommand(String actionParameter) 124 | { 125 | if (!this.IsRealClass || String.IsNullOrEmpty(actionParameter)) 126 | { 127 | return; 128 | } 129 | 130 | var index = this.GetButton(actionParameter); 131 | 132 | if (index == -1) 133 | { 134 | return; 135 | } 136 | 137 | Remote.SetParameter($"{(this.IsStrip ? "Strip" : "Bus")}[{index + this.Offset}].{this.Command}", 0); 138 | 139 | this.AdjustmentValueChanged(actionParameter); 140 | } 141 | 142 | protected override void ApplyAdjustment(String actionParameter, Int32 diff) 143 | { 144 | if (!this.IsRealClass || String.IsNullOrEmpty(actionParameter)) 145 | { 146 | return; 147 | } 148 | 149 | var index = this.GetButton(actionParameter); 150 | 151 | if (index == -1 || this.Actions[index] is null) 152 | { 153 | return; 154 | } 155 | 156 | var newVal = this.Actions[index].Value + diff; 157 | if (newVal < this.MinValue) 158 | { 159 | newVal = this.MinValue; 160 | } 161 | else if (newVal > this.MaxValue) 162 | { 163 | newVal = this.MaxValue; 164 | } 165 | 166 | 167 | Remote.SetParameter($"{(this.IsStrip ? "Strip" : "Bus")}[{index + this.Offset}].{this.Command}", 168 | newVal / this.ScaleFactor); 169 | 170 | this.AdjustmentValueChanged(actionParameter); 171 | } 172 | 173 | protected override BitmapImage GetAdjustmentImage(String actionParameter, PluginImageSize imageSize) 174 | { 175 | if (!this.IsRealClass || String.IsNullOrEmpty(actionParameter)) 176 | { 177 | return null; 178 | } 179 | 180 | var index = this.GetButton(actionParameter); 181 | 182 | if (this.Actions[index] is null || index == -1) 183 | { 184 | return base.GetAdjustmentImage(actionParameter, imageSize); 185 | } 186 | 187 | var (value, name, isMuted) = this.Actions[index]; 188 | var backgroundColor = !isMuted ? this.Actions[index].Value > 0 ? ColorHelper.Danger : ColorHelper.Active : ColorHelper.Inactive; 189 | 190 | return DrawingHelper.DrawVolumeBar(imageSize, backgroundColor.ToBitmapColor(), BitmapColor.White, value, this.MinValue, this.MaxValue, this.ScaleFactor, this.Command, name); 191 | } 192 | 193 | protected override BitmapImage GetCommandImage(String actionParameter, PluginImageSize imageSize) => this.GetAdjustmentImage(actionParameter, imageSize); 194 | 195 | protected override Double? GetAdjustmentMinValue(String actionParameter) => this.MinValue; 196 | 197 | protected override Double? GetAdjustmentMaxValue(String actionParameter) => this.MaxValue; 198 | 199 | private Int32 GetButton(String actionParameter) 200 | { 201 | var number = actionParameter.Replace("VM-Strip", "").Replace($"-{this.Command}", ""); 202 | 203 | if (Int32.TryParse(number, out var index)) 204 | { 205 | return index; 206 | } 207 | else 208 | { 209 | return -1; 210 | } 211 | } 212 | 213 | private class AdjustmentItem 214 | { 215 | public Single Value { get; set; } 216 | public String Name { get; set; } 217 | public Boolean IsMuted { get; set; } 218 | 219 | public void Deconstruct(out Single value, out String name, out Boolean isMuted) 220 | { 221 | value = this.Value; 222 | name = this.Name; 223 | isMuted = this.IsMuted; 224 | } 225 | } 226 | } 227 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/Bases/SingleBaseCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Actions.Bases 2 | { 3 | using Helpers; 4 | 5 | public class SingleBaseCommand(String actionName, String description, String groupName, Action action) 6 | : PluginDynamicCommand(actionName, description, groupName) 7 | { 8 | private String ActionName { get; } = actionName; 9 | private Action Action { get; } = action; 10 | 11 | protected override BitmapImage GetCommandImage(String actionParameter, PluginImageSize imageSize) 12 | => DrawingHelper.DrawDefaultImage(this.ActionName, "", ColorHelper.Inactive); 13 | 14 | protected override void RunCommand(String actionParameter) => this.Action(); 15 | } 16 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/BusEQCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Actions 2 | { 3 | using Bases; 4 | 5 | using Helpers; 6 | 7 | public class BusEqCommand : BooleanBaseCommand 8 | { 9 | public BusEqCommand() : base(true, false) => 10 | this.CreateCommands( 11 | VoiceMeeterHelper.GetHardwareInputCount() + VoiceMeeterHelper.GetVirtualInputCount(), 12 | "EQ.on", 13 | 0, 14 | "EQ" 15 | ).ConfigureAwait(false); 16 | } 17 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/BusGainAdjustment.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Actions 2 | { 3 | using Bases; 4 | 5 | using Helpers; 6 | 7 | public class BusGainAdjustment : SingleBaseAdjustment 8 | { 9 | public BusGainAdjustment() : base(true, true, false, -60, 12) => 10 | this.CreateCommands( 11 | VoiceMeeterHelper.GetHardwareInputCount() + VoiceMeeterHelper.GetVirtualInputCount(), 12 | "Gain", 13 | 0 14 | ).ConfigureAwait(false); 15 | } 16 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/BusMonoCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Actions 2 | { 3 | using Bases; 4 | 5 | using Helpers; 6 | 7 | public class BusMonoCommand : BooleanBaseCommand 8 | { 9 | public BusMonoCommand() : base(true, false) => 10 | this.CreateCommands( 11 | VoiceMeeterHelper.GetHardwareInputCount() + VoiceMeeterHelper.GetVirtualInputCount(), 12 | "Mono", 13 | 0 14 | ).ConfigureAwait(false); 15 | } 16 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/BusMuteCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Actions 2 | { 3 | using Bases; 4 | 5 | using Helpers; 6 | 7 | public class BusMuteCommand : BooleanBaseCommand 8 | { 9 | public BusMuteCommand() : base(true, false, ColorHelper.Danger) => 10 | this.CreateCommands( 11 | VoiceMeeterHelper.GetHardwareInputCount() + VoiceMeeterHelper.GetVirtualInputCount(), 12 | "Mute", 13 | 0 14 | ).ConfigureAwait(false); 15 | } 16 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/BusSelCommand.cs: -------------------------------------------------------------------------------- 1 | // This file is part of the VoiceMeeterPlugin project. 2 | // 3 | // Copyright (c) 2022 Dominic Ris 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | namespace Loupedeck.VoiceMeeterPlugin.Actions 24 | { 25 | using Bases; 26 | 27 | using Helpers; 28 | 29 | public class BusSelCommand : BooleanBaseCommand 30 | { 31 | public BusSelCommand() : base(true, false, ColorHelper.SelActive) => 32 | this.CreateCommands( 33 | VoiceMeeterHelper.GetHardwareInputCount() + VoiceMeeterHelper.GetVirtualInputCount(), 34 | "Sel", 35 | 0 36 | ).ConfigureAwait(false); 37 | } 38 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/EjectCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Actions 2 | { 3 | using Bases; 4 | 5 | using Library.Voicemeeter; 6 | 7 | public class EjectCommand : SingleBaseCommand 8 | { 9 | public EjectCommand() : base("Eject", "Eject Cassette", "Special", Remote.Eject) 10 | { 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/HardwareInputCompAdjustment.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Actions 2 | { 3 | using Bases; 4 | 5 | using Helpers; 6 | 7 | public class HardwareInputCompAdjustment : SingleBaseAdjustment 8 | { 9 | public HardwareInputCompAdjustment() : base(true, true, true) => 10 | this.CreateCommands( 11 | VoiceMeeterHelper.GetHardwareInputCount(), 12 | "Comp", 13 | 0 14 | ).ConfigureAwait(false); 15 | } 16 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/HardwareInputDelayAdjustment.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Actions 2 | { 3 | using Bases; 4 | 5 | using Helpers; 6 | 7 | using Library.Voicemeeter; 8 | 9 | public class HardwareInputDelayAdjustment : SingleBaseAdjustment 10 | { 11 | public HardwareInputDelayAdjustment() : base(true, true, true) 12 | { 13 | if (Remote.Version != RunVoicemeeterParam.VoicemeeterPotato) 14 | { 15 | this.IsRealClass = false; 16 | return; 17 | } 18 | 19 | this.CreateCommands( 20 | VoiceMeeterHelper.GetHardwareInputCount(), 21 | "Delay", 22 | 0 23 | ).ConfigureAwait(false); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/HardwareInputFxAdjustment.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Actions 2 | { 3 | using Bases; 4 | 5 | using Helpers; 6 | 7 | using Library.Voicemeeter; 8 | 9 | public class HardwareInputFx1Adjustment : SingleBaseAdjustment 10 | { 11 | public HardwareInputFx1Adjustment() : base(true, true, true) 12 | { 13 | if (Remote.Version != RunVoicemeeterParam.VoicemeeterPotato) 14 | { 15 | this.IsRealClass = false; 16 | return; 17 | } 18 | 19 | this.CreateCommands( 20 | VoiceMeeterHelper.GetHardwareInputCount(), 21 | "Fx1", 22 | 0 23 | ).ConfigureAwait(false); 24 | } 25 | } 26 | 27 | public class HardwareInputFx2Adjustment : SingleBaseAdjustment 28 | { 29 | public HardwareInputFx2Adjustment() : base(true, true, true) 30 | { 31 | if (Remote.Version != RunVoicemeeterParam.VoicemeeterPotato) 32 | { 33 | this.IsRealClass = false; 34 | return; 35 | } 36 | 37 | this.CreateCommands( 38 | VoiceMeeterHelper.GetHardwareInputCount(), 39 | "Fx2", 40 | 0 41 | ).ConfigureAwait(false); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/HardwareInputGateAdjustment.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Actions 2 | { 3 | using Bases; 4 | 5 | using Helpers; 6 | 7 | public class HardwareInputGateAdjustment : SingleBaseAdjustment 8 | { 9 | public HardwareInputGateAdjustment() : base(true, true, true) => 10 | this.CreateCommands( 11 | VoiceMeeterHelper.GetHardwareInputCount(), 12 | "Gate", 13 | 0 14 | ).ConfigureAwait(false); 15 | } 16 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/HardwareInputMonoCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Actions 2 | { 3 | using Bases; 4 | 5 | using Helpers; 6 | 7 | public class HardwareInputMonoCommand : BooleanBaseCommand 8 | { 9 | public HardwareInputMonoCommand() : base(true, true) => 10 | this.CreateCommands( 11 | VoiceMeeterHelper.GetHardwareInputCount(), 12 | "Mono", 13 | 0 14 | ).ConfigureAwait(false); 15 | } 16 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/HardwareInputPostDelayCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Actions 2 | { 3 | using Bases; 4 | 5 | using Helpers; 6 | 7 | using Library.Voicemeeter; 8 | 9 | public class HardwareInputPostDelayCommand : BooleanBaseCommand 10 | { 11 | public HardwareInputPostDelayCommand() : base(true, true) 12 | { 13 | if (Remote.Version != RunVoicemeeterParam.VoicemeeterPotato) 14 | { 15 | this.IsRealClass = false; 16 | return; 17 | } 18 | 19 | this.CreateCommands( 20 | VoiceMeeterHelper.GetHardwareInputCount(), 21 | "PostDelay", 22 | 0 23 | ).ConfigureAwait(false); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/HardwareInputPostFxCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Actions 2 | { 3 | using Bases; 4 | 5 | using Helpers; 6 | 7 | using Library.Voicemeeter; 8 | 9 | public class HardwareInputPostFx1Command : BooleanBaseCommand 10 | { 11 | public HardwareInputPostFx1Command() : base(true, true) 12 | { 13 | if (Remote.Version != RunVoicemeeterParam.VoicemeeterPotato) 14 | { 15 | this.IsRealClass = false; 16 | return; 17 | } 18 | 19 | this.CreateCommands( 20 | VoiceMeeterHelper.GetHardwareInputCount(), 21 | "PostFx1", 22 | 0 23 | ).ConfigureAwait(false); 24 | } 25 | } 26 | 27 | public class HardwareInputPostFx2Command : BooleanBaseCommand 28 | { 29 | public HardwareInputPostFx2Command() : base(true, true) 30 | { 31 | if (Remote.Version != RunVoicemeeterParam.VoicemeeterPotato) 32 | { 33 | this.IsRealClass = false; 34 | return; 35 | } 36 | 37 | this.CreateCommands( 38 | VoiceMeeterHelper.GetHardwareInputCount(), 39 | "PostFx2", 40 | 0 41 | ).ConfigureAwait(false); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/HardwareInputPostReverbCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Actions 2 | { 3 | using Bases; 4 | 5 | using Helpers; 6 | 7 | using Library.Voicemeeter; 8 | 9 | public class HardwareInputPostReverbCommand : BooleanBaseCommand 10 | { 11 | public HardwareInputPostReverbCommand() : base(true, true) 12 | { 13 | if (Remote.Version != RunVoicemeeterParam.VoicemeeterPotato) 14 | { 15 | this.IsRealClass = false; 16 | return; 17 | } 18 | 19 | this.CreateCommands( 20 | VoiceMeeterHelper.GetHardwareInputCount(), 21 | "PostReverb", 22 | 0 23 | ).ConfigureAwait(false); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/HardwareInputReverbAdjustment.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Actions 2 | { 3 | using Bases; 4 | 5 | using Helpers; 6 | 7 | using Library.Voicemeeter; 8 | 9 | public class HardwareInputReverbAdjustment : SingleBaseAdjustment 10 | { 11 | public HardwareInputReverbAdjustment() : base(true, true, true) 12 | { 13 | if (Remote.Version != RunVoicemeeterParam.VoicemeeterPotato) 14 | { 15 | this.IsRealClass = false; 16 | return; 17 | } 18 | 19 | this.CreateCommands( 20 | VoiceMeeterHelper.GetHardwareInputCount(), 21 | "Reverb", 22 | 0 23 | ).ConfigureAwait(false); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/LevelsCommand.cs: -------------------------------------------------------------------------------- 1 | // This file is part of the VoiceMeeterPlugin project. 2 | // 3 | // Copyright (c) 2024 Dominic Ris 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | namespace Loupedeck.VoiceMeeterPlugin.Actions; 24 | 25 | using System.Reactive.Linq; 26 | using System.Reactive.Subjects; 27 | 28 | using Extensions; 29 | 30 | using Helpers; 31 | 32 | using Library.Voicemeeter; 33 | 34 | using Services; 35 | 36 | using SkiaSharp; 37 | 38 | public class LevelsCommand : ActionEditorCommand 39 | { 40 | private VoiceMeeterService VmService { get; } 41 | private Subject<Boolean> OnDestroy { get; } = new(); 42 | 43 | public LevelsCommand() 44 | { 45 | this.DisplayName = "Level Display"; 46 | this.Description = "Displays specific Levels"; 47 | 48 | this.ActionEditor.AddControlEx( 49 | new ActionEditorTextbox("name", "Display Name", "Name displayed on the device itself").SetRequired() 50 | ); 51 | this.ActionEditor.AddControlEx( 52 | new ActionEditorSlider("channel_number", "Channel Number", "The Channel Number to display").SetRequired().SetValues(0, 100, 0, 1) 53 | ); 54 | this.ActionEditor.AddControlEx( 55 | new ActionEditorListbox("channel_type", "Channel Type", "The Channel Type to display").SetRequired() 56 | ); 57 | this.ActionEditor.AddControlEx( 58 | 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})$") 59 | ); 60 | this.ActionEditor.AddControlEx( 61 | 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})$") 62 | ); 63 | 64 | this.ActionEditor.ListboxItemsRequested += (_, e) => 65 | { 66 | // iterate over LevelType enum values and add them (their name) to the listbox 67 | foreach (var value in Enum.GetValues(typeof(LevelType))) 68 | { 69 | e.Items.Add(new ActionEditorListboxItem("channel_type_" + value, value.ToString(), "")); 70 | } 71 | }; 72 | 73 | this.VmService = VoiceMeeterService.Instance; 74 | } 75 | 76 | protected override Boolean OnLoad() 77 | { 78 | this.VmService.Levels 79 | .TakeUntil(this.OnDestroy) 80 | .Subscribe(_ => this.ActionImageChanged()); 81 | 82 | return base.OnLoad(); 83 | } 84 | 85 | protected override Boolean OnUnload() 86 | { 87 | this.OnDestroy.OnNext(true); 88 | return base.OnUnload(); 89 | } 90 | 91 | protected override String GetCommandDisplayName(ActionEditorActionParameters actionParameters) 92 | { 93 | Tuple<String, Levels.Channel, SKColor, SKColor> parameters; 94 | try 95 | { 96 | parameters = GetParameters(actionParameters); 97 | } 98 | catch (Exception) 99 | { 100 | return null; 101 | } 102 | 103 | if (parameters == null) 104 | { 105 | return null; 106 | } 107 | 108 | var (name, channel, _, _) = parameters; 109 | 110 | this.VmService.Levels.AddChannel(channel); 111 | 112 | var currentValue = 0f; 113 | 114 | try 115 | { 116 | currentValue = Remote.GetLevel(channel.LevelType, channel.ChannelNumber); 117 | } 118 | catch (Exception) 119 | { 120 | // ignore 121 | } 122 | 123 | currentValue = (Single)Math.Round(currentValue, 10); 124 | 125 | return $"{name} - {currentValue:P0}"; 126 | } 127 | 128 | protected override BitmapImage GetCommandImage(ActionEditorActionParameters actionParameters, Int32 imageWidth, Int32 imageHeight) 129 | { 130 | Tuple<String, Levels.Channel, SKColor, SKColor> parameters; 131 | try 132 | { 133 | parameters = GetParameters(actionParameters); 134 | } 135 | catch (Exception) 136 | { 137 | return null; 138 | } 139 | 140 | if (parameters == null) 141 | { 142 | return null; 143 | } 144 | 145 | var (name, channel, bgColor, fgColor) = parameters; 146 | 147 | this.VmService.Levels.AddChannel(channel); 148 | 149 | var currentValue = 0f; 150 | 151 | try 152 | { 153 | currentValue = Remote.GetLevel(channel.LevelType, channel.ChannelNumber); 154 | } 155 | catch (Exception) 156 | { 157 | // ignore 158 | } 159 | 160 | currentValue = (Single)Math.Round(currentValue, 10); 161 | 162 | if (currentValue < 0) 163 | { 164 | currentValue = 0; 165 | } 166 | 167 | if (currentValue > 1) 168 | { 169 | currentValue = 1; 170 | } 171 | 172 | return DrawingHelper.DrawVolumeBar(PluginImageSize.Width60, bgColor.ToBitmapColor(), fgColor.ToBitmapColor(), currentValue, 0, 1, 1, "", name, false); 173 | } 174 | 175 | private static Tuple<String, Levels.Channel, SKColor, SKColor> GetParameters(ActionEditorActionParameters actionParameters) 176 | { 177 | actionParameters.TryGetString("name", out var name); 178 | actionParameters.TryGetInt32("channel_number", out var channelNumber); 179 | actionParameters.TryGetString("channel_type", out var channelType); 180 | actionParameters.TryGetString("bgcolor", out var bgColor); 181 | actionParameters.TryGetString("fgcolor", out var fgColor); 182 | 183 | // for the channel type, we first have to remove the prefix 184 | var type = channelType.Replace("channel_type_", ""); 185 | if (!Enum.TryParse<LevelType>(type, out var levelType)) 186 | { 187 | return null; 188 | } 189 | 190 | var channel = new Levels.Channel { LevelType = levelType, ChannelNumber = channelNumber }; 191 | 192 | return new Tuple<String, Levels.Channel, SKColor, SKColor>( 193 | String.IsNullOrEmpty(name) ? "Unknown" : name, 194 | channel, 195 | SKColor.TryParse(bgColor, out var bg) ? bg : ColorHelper.Inactive, 196 | SKColor.TryParse(fgColor, out var fg) ? fg : SKColors.White); 197 | } 198 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/LoadCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Actions 2 | { 3 | using Helpers; 4 | 5 | using Library.Voicemeeter; 6 | 7 | public class LoadCommand : PluginDynamicCommand 8 | { 9 | public LoadCommand() : base("Load", "Load settings", "Special") => this.MakeProfileAction("text;Path:"); 10 | 11 | protected override BitmapImage GetCommandImage(String actionParameter, PluginImageSize imageSize) 12 | => DrawingHelper.DrawDefaultImage("Load", Path.GetFileNameWithoutExtension(actionParameter) ?? "", ColorHelper.Inactive); 13 | 14 | protected override void RunCommand(String actionParameter) => Remote.Load(actionParameter); 15 | } 16 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/RawAdjustment.cs: -------------------------------------------------------------------------------- 1 | // This file is part of the VoiceMeeterPlugin project. 2 | // 3 | // Copyright (c) 2024 Dominic Ris 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | namespace Loupedeck.VoiceMeeterPlugin.Actions; 24 | 25 | using System.Globalization; 26 | using System.Reactive.Linq; 27 | using System.Reactive.Subjects; 28 | 29 | using Extensions; 30 | 31 | using Helpers; 32 | 33 | using Library.Voicemeeter; 34 | 35 | using Services; 36 | 37 | using SkiaSharp; 38 | 39 | public class RawAdjustment : ActionEditorAdjustment 40 | { 41 | private VoiceMeeterService VmService { get; } 42 | private Subject<Boolean> OnDestroy { get; } = new(); 43 | 44 | public RawAdjustment() : base(false) 45 | { 46 | this.DisplayName = "Raw Adjustment"; 47 | this.Description = "Adjusts a raw value"; 48 | 49 | this.ActionEditor.AddControlEx( 50 | new ActionEditorTextbox("name", "Display Name", "Name displayed on the device itself").SetRequired() 51 | ); 52 | this.ActionEditor.AddControlEx( 53 | new ActionEditorTextbox("api", "API", "The \"API\" to adjust, example: Strip[0].Gain").SetRequired() 54 | ); 55 | this.ActionEditor.AddControlEx( 56 | new ActionEditorTextbox("steps", "Steps", "The steps to adjust the API by").SetRequired().SetFormat(ActionEditorTextboxFormat.Integer).SetRegex(@"^-?\d+(\.\d+)?$") 57 | ); 58 | this.ActionEditor.AddControlEx( 59 | new ActionEditorTextbox("min", "Min Value", "The minimum value that can be reached").SetRequired().SetFormat(ActionEditorTextboxFormat.Integer).SetRegex(@"^-?\d+(\.\d+)?$") 60 | ); 61 | this.ActionEditor.AddControlEx( 62 | new ActionEditorTextbox("max", "Max Value", "The maximum value that can be reached").SetRequired().SetFormat(ActionEditorTextboxFormat.Integer).SetRegex(@"^-?\d+(\.\d+)?$") 63 | ); 64 | this.ActionEditor.AddControlEx( 65 | new ActionEditorTextbox("bgcolor", "Background Color", "The color it should use in hex (#rrggbb example: #FF0000 = red)").SetRegex("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$") 66 | ); 67 | 68 | this.ActionEditor.AddControlEx( 69 | new ActionEditorTextbox("fgcolor", "Foreground Color", "The color it should use in hex (#rrggbb example: #FF0000 = red)").SetRegex("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$") 70 | ); 71 | 72 | this.VmService = VoiceMeeterService.Instance; 73 | } 74 | 75 | protected override Boolean OnLoad() 76 | { 77 | this.VmService.Parameters 78 | .TakeUntil(this.OnDestroy) 79 | .Subscribe(_ => this.AdjustmentValueChanged()); 80 | 81 | return base.OnLoad(); 82 | } 83 | 84 | protected override Boolean OnUnload() 85 | { 86 | this.OnDestroy.OnNext(true); 87 | return base.OnUnload(); 88 | } 89 | 90 | protected override BitmapImage GetCommandImage(ActionEditorActionParameters actionParameters, Int32 imageWidth, Int32 imageHeight) => 91 | this.GetAdjustmentImage(actionParameters, imageWidth, imageHeight); 92 | 93 | protected override BitmapImage GetAdjustmentImage(ActionEditorActionParameters actionParameters, Int32 imageWidth, Int32 imageHeight) 94 | { 95 | Tuple<String, String, Single, Int32, Int32, SKColor, SKColor> parameters; 96 | try 97 | { 98 | parameters = GetParameters(actionParameters); 99 | } 100 | catch (Exception) 101 | { 102 | return null; 103 | } 104 | 105 | if (parameters == null) 106 | { 107 | return null; 108 | } 109 | 110 | var (name, api, steps, min, max, bgColor, fgColor) = parameters; 111 | 112 | var currentValue = -9999f; 113 | 114 | try 115 | { 116 | currentValue = Remote.GetParameter(api); 117 | } 118 | catch (Exception) 119 | { 120 | // ignore 121 | } 122 | 123 | var decimalPlaces = GetDecimalPlaces(steps); 124 | currentValue = (Single)Math.Round(currentValue, decimalPlaces); 125 | 126 | 127 | return DrawingHelper.DrawVolumeBar(PluginImageSize.Width60, bgColor.ToBitmapColor(), fgColor.ToBitmapColor(), currentValue, min, max, 1, "", name); 128 | } 129 | 130 | protected override Boolean ApplyAdjustment(ActionEditorActionParameters actionParameters, Int32 diff) 131 | { 132 | Tuple<String, String, Single, Int32, Int32, SKColor, SKColor> parameters; 133 | try 134 | { 135 | parameters = GetParameters(actionParameters); 136 | } 137 | catch (Exception) 138 | { 139 | return false; 140 | } 141 | 142 | if (parameters == null) 143 | { 144 | return false; 145 | } 146 | 147 | var (_, api, steps, min, max, _, _) = parameters; 148 | 149 | try 150 | { 151 | var currentValue = Remote.GetParameter(api); 152 | var newValue = currentValue + diff * steps; 153 | if (newValue < min) 154 | { 155 | newValue = min; 156 | } 157 | else if (newValue > max) 158 | { 159 | newValue = max; 160 | } 161 | Remote.SetParameter(api, newValue); 162 | } 163 | catch (Exception) 164 | { 165 | return false; 166 | } 167 | 168 | return true; 169 | } 170 | 171 | private static Tuple<String, String, Single, Int32, Int32, SKColor, SKColor> GetParameters(ActionEditorActionParameters actionParameters) 172 | { 173 | actionParameters.TryGetString("name", out var name); 174 | actionParameters.TryGetString("api", out var api); 175 | actionParameters.TryGetString("steps", out var value); 176 | actionParameters.TryGetString("min", out var min); 177 | actionParameters.TryGetString("max", out var max); 178 | actionParameters.TryGetString("bgcolor", out var bgColor); 179 | actionParameters.TryGetString("fgcolor", out var fgColor); 180 | 181 | return new Tuple<String, String, Single, Int32, Int32, SKColor, SKColor>( 182 | String.IsNullOrEmpty(name) ? "Unknown" : name, 183 | String.IsNullOrEmpty(api) ? "Strip[1].Gain" : api, 184 | Single.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out var val) ? val : 1, 185 | Int32.TryParse(min, NumberStyles.Any, CultureInfo.InvariantCulture, out var minValue) ? minValue : 0, 186 | Int32.TryParse(max, NumberStyles.Any, CultureInfo.InvariantCulture, out var maxValue) ? maxValue : 100, 187 | SKColor.TryParse(bgColor, out var bg) ? bg : ColorHelper.Inactive, 188 | SKColor.TryParse(fgColor, out var fg) ? fg : SKColors.White); 189 | } 190 | 191 | private static Int32 GetDecimalPlaces(Single steps) 192 | { 193 | var str = steps.ToString(CultureInfo.InvariantCulture); 194 | var index = str.IndexOf('.'); 195 | return index == -1 ? 0 : str.Length - index - 1; 196 | } 197 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/RawCommand.cs: -------------------------------------------------------------------------------- 1 | // This file is part of the VoiceMeeterPlugin project. 2 | // 3 | // Copyright (c) 2024 Dominic Ris 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | namespace Loupedeck.VoiceMeeterPlugin.Actions; 24 | 25 | using System.Reactive.Linq; 26 | using System.Reactive.Subjects; 27 | 28 | using Helpers; 29 | 30 | using Library.Voicemeeter; 31 | 32 | using Services; 33 | 34 | using SkiaSharp; 35 | 36 | public class RawCommand : MultistateActionEditorCommand 37 | { 38 | private VoiceMeeterService VmService { get; } 39 | private Subject<Boolean> OnDestroy { get; } = new(); 40 | 41 | public RawCommand() 42 | { 43 | this.DisplayName = "Raw Command"; 44 | this.Description = "Toggle or execute an action"; 45 | 46 | this.ActionEditor.AddControlEx( 47 | new ActionEditorTextbox("name", "Display Name", "Name displayed on the device itself").SetRequired() 48 | ); 49 | this.ActionEditor.AddControlEx( 50 | new ActionEditorTextbox("api", "API", "The \"API\" to adjust, example: Strip[0].Gain").SetRequired() 51 | ); 52 | this.ActionEditor.AddControlEx( 53 | new ActionEditorTextbox("oncolor", "On Color", "The color it should use in hex (#rrggbb example: #FF0000 = red)").SetRegex("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$") 54 | ); 55 | this.ActionEditor.AddControlEx( 56 | new ActionEditorTextbox("offcolor", "Off Color", "The color it should use in hex (#rrggbb example: #FF0000 = red)").SetRegex("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$") 57 | ); 58 | 59 | this.VmService = VoiceMeeterService.Instance; 60 | this.AddState("Off", "If the action is off"); 61 | this.AddState("On", "If the action is on"); 62 | } 63 | 64 | protected override Boolean OnLoad() 65 | { 66 | this.VmService.Parameters 67 | .TakeUntil(this.OnDestroy) 68 | .Subscribe(_ => this.ActionImageChanged()); 69 | 70 | return base.OnLoad(); 71 | } 72 | 73 | protected override Boolean OnUnload() 74 | { 75 | this.OnDestroy.OnNext(true); 76 | return base.OnUnload(); 77 | } 78 | 79 | protected override String GetCommandDisplayName(ActionEditorActionParameters actionParameters, Int32 stateIndex) 80 | { 81 | Tuple<String, String, SKColor, SKColor> parameters; 82 | try 83 | { 84 | parameters = GetParameters(actionParameters); 85 | } 86 | catch (Exception) 87 | { 88 | return "Unknown"; 89 | } 90 | 91 | if (parameters == null) 92 | { 93 | return "Unknown"; 94 | } 95 | 96 | var (name, _, _, _) = parameters; 97 | 98 | return $"{name} - {(stateIndex == 0 ? "Off" : "On")}"; 99 | } 100 | 101 | protected override BitmapImage GetCommandImage(ActionEditorActionParameters actionParameters, Int32 stateIndex, Int32 imageWidth, Int32 imageHeight) 102 | { 103 | Tuple<String, String, SKColor, SKColor> parameters; 104 | try 105 | { 106 | parameters = GetParameters(actionParameters); 107 | } 108 | catch (Exception) 109 | { 110 | return null; 111 | } 112 | 113 | if (parameters == null) 114 | { 115 | return null; 116 | } 117 | 118 | var (name, api, onColor, offColor) = parameters; 119 | 120 | var currentValue = false; 121 | 122 | try 123 | { 124 | currentValue = (Int32)Remote.GetParameter(api) == 1; 125 | } 126 | catch (Exception) 127 | { 128 | // ignore 129 | } 130 | 131 | 132 | return DrawingHelper.DrawDefaultImage(name, "", currentValue ? onColor : offColor); 133 | } 134 | 135 | protected override Boolean RunCommand(ActionEditorActionParameters actionParameters) 136 | { 137 | Tuple<String, String, SKColor, SKColor> parameters; 138 | try 139 | { 140 | parameters = GetParameters(actionParameters); 141 | } 142 | catch (Exception) 143 | { 144 | return false; 145 | } 146 | 147 | if (parameters == null) 148 | { 149 | return false; 150 | } 151 | 152 | var (_, api, _, _) = parameters; 153 | 154 | try 155 | { 156 | var currentValue = (Int32)Remote.GetParameter(api) == 1; 157 | Remote.SetParameter(api, currentValue ? 0 : 1); 158 | this.SetCurrentState(actionParameters, currentValue ? 0 : 1); 159 | } 160 | catch (Exception) 161 | { 162 | return false; 163 | } 164 | 165 | return true; 166 | } 167 | 168 | private static Tuple<String, String, SKColor, SKColor> GetParameters(ActionEditorActionParameters actionParameters) 169 | { 170 | actionParameters.TryGetString("name", out var name); 171 | actionParameters.TryGetString("api", out var api); 172 | actionParameters.TryGetString("oncolor", out var onColor); 173 | actionParameters.TryGetString("offcolor", out var offColor); 174 | 175 | return new Tuple<String, String, SKColor, SKColor>( 176 | String.IsNullOrEmpty(name) ? "Unknown" : name, 177 | String.IsNullOrEmpty(api) ? "Strip[1].Gain" : api, 178 | SKColor.TryParse(onColor, out var on) ? on : ColorHelper.Active, 179 | SKColor.TryParse(offColor, out var off) ? off : ColorHelper.Inactive); 180 | } 181 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/ResetCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Actions 2 | { 3 | using Bases; 4 | 5 | using Library.Voicemeeter; 6 | 7 | public class ResetCommand : SingleBaseCommand 8 | { 9 | public ResetCommand() : base("Reset", "Reset ALL configuration of VoiceMeeter", "Special", Remote.Reset) 10 | { 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/RestartCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Actions 2 | { 3 | using Bases; 4 | 5 | using Library.Voicemeeter; 6 | 7 | public class RestartCommand : SingleBaseCommand 8 | { 9 | public RestartCommand() : base("Restart", "Restart audio engine", "Special", Remote.Restart) 10 | { 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/ShowCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Actions 2 | { 3 | using Bases; 4 | 5 | using Library.Voicemeeter; 6 | 7 | public class ShowCommand : SingleBaseCommand 8 | { 9 | public ShowCommand() : base("Show", "Show VoiceMeeter", "Special", Remote.Show) 10 | { 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/ShutdownCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Actions 2 | { 3 | using Bases; 4 | 5 | using Library.Voicemeeter; 6 | 7 | public class ShutdownCommand : SingleBaseCommand 8 | { 9 | public ShutdownCommand() : base("Shutdown", "Shutdown VoiceMeeter", "Special", Remote.Shutdown) 10 | { 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/StripACommand.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Actions 2 | { 3 | using Bases; 4 | 5 | using Helpers; 6 | 7 | public class StripACommand : BooleanBaseCommand 8 | { 9 | public StripACommand() : base(true, true) => 10 | this.CreateCommands( 11 | VoiceMeeterHelper.GetHardwareInputCount() + VoiceMeeterHelper.GetVirtualInputCount(), 12 | "A", 13 | VoiceMeeterHelper.GetStripACount(), 14 | 0 15 | ).ConfigureAwait(false); 16 | } 17 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/StripBCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Actions 2 | { 3 | using Bases; 4 | 5 | using Helpers; 6 | 7 | public class StripBCommand : BooleanBaseCommand 8 | { 9 | public StripBCommand() : base(true, true) => 10 | this.CreateCommands( 11 | VoiceMeeterHelper.GetHardwareInputCount() + VoiceMeeterHelper.GetVirtualInputCount(), 12 | "B", 13 | VoiceMeeterHelper.GetStripBCount(), 14 | 0 15 | ).ConfigureAwait(false); 16 | } 17 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/StripGainAdjustment.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Actions 2 | { 3 | using Bases; 4 | 5 | using Helpers; 6 | 7 | public class StripGainAdjustment : SingleBaseAdjustment 8 | { 9 | public StripGainAdjustment() : base(true, true, true, -60, 12) => 10 | this.CreateCommands( 11 | VoiceMeeterHelper.GetHardwareInputCount() + VoiceMeeterHelper.GetVirtualInputCount(), 12 | "Gain", 13 | 0 14 | ).ConfigureAwait(false); 15 | } 16 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/StripMuteCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Actions 2 | { 3 | using Bases; 4 | 5 | using Helpers; 6 | 7 | public class StripMuteCommand : BooleanBaseCommand 8 | { 9 | public StripMuteCommand() : base(true, true, ColorHelper.Danger) => 10 | this.CreateCommands( 11 | VoiceMeeterHelper.GetHardwareInputCount() + VoiceMeeterHelper.GetVirtualInputCount(), 12 | "Mute", 13 | 0 14 | ).ConfigureAwait(false); 15 | } 16 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/StripPanAdjustment.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Actions 2 | { 3 | using Bases; 4 | 5 | using Helpers; 6 | 7 | public class StripPanXAdjustment : SingleBaseAdjustment 8 | { 9 | public StripPanXAdjustment() : base(true, true, true, -5, 5, 10) => 10 | this.CreateCommands( 11 | VoiceMeeterHelper.GetHardwareInputCount() + VoiceMeeterHelper.GetVirtualInputCount(), 12 | "Pan_x", 13 | 0 14 | ).ConfigureAwait(false); 15 | } 16 | 17 | public class HardwareInputPanYAdjustment : SingleBaseAdjustment 18 | { 19 | public HardwareInputPanYAdjustment() : base(true, true, true, 0, 10, 10) => 20 | this.CreateCommands( 21 | VoiceMeeterHelper.GetHardwareInputCount(), 22 | "Pan_y", 23 | 0 24 | ).ConfigureAwait(false); 25 | } 26 | 27 | public class VirtualInputPanYAdjustment : SingleBaseAdjustment 28 | { 29 | public VirtualInputPanYAdjustment() : base(true, true, true, -5, 5, 10) => 30 | this.CreateCommands( 31 | VoiceMeeterHelper.GetVirtualInputCount(), 32 | "Pan_y", 33 | VoiceMeeterHelper.GetHardwareInputCount() 34 | ).ConfigureAwait(false); 35 | } 36 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/StripSoloCommand.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Actions 2 | { 3 | using Bases; 4 | 5 | using Helpers; 6 | 7 | public class StripSoloCommand : BooleanBaseCommand 8 | { 9 | public StripSoloCommand() : base(true, true) => 10 | this.CreateCommands( 11 | VoiceMeeterHelper.GetHardwareInputCount() + VoiceMeeterHelper.GetVirtualInputCount(), 12 | "Solo", 13 | 0 14 | ).ConfigureAwait(false); 15 | } 16 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Actions/VirtualInputEQGainAdjustment.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Actions 2 | { 3 | using Bases; 4 | 5 | using Helpers; 6 | 7 | using Library.Voicemeeter; 8 | 9 | public class VirtualInputEqGain1Adjustment : SingleBaseAdjustment 10 | { 11 | public VirtualInputEqGain1Adjustment() : base(true, true, true, -12, 12) => 12 | this.CreateCommands( 13 | VoiceMeeterHelper.GetVirtualInputCount(), 14 | "EQGain1", 15 | VoiceMeeterHelper.GetHardwareInputCount() 16 | ).ConfigureAwait(false); 17 | } 18 | 19 | public class VirtualInputEqGain2Adjustment : SingleBaseAdjustment 20 | { 21 | public VirtualInputEqGain2Adjustment() : base(true, true, true, -12, 12) => 22 | this.CreateCommands( 23 | VoiceMeeterHelper.GetVirtualInputCount(), 24 | "EQGain2", 25 | VoiceMeeterHelper.GetHardwareInputCount() 26 | ).ConfigureAwait(false); 27 | } 28 | 29 | public class VirtualInputEqGain3Adjustment : SingleBaseAdjustment 30 | { 31 | public VirtualInputEqGain3Adjustment() : base(true, true, true, -12, 12) 32 | { 33 | if (Remote.Version != RunVoicemeeterParam.VoicemeeterPotato) 34 | { 35 | this.IsRealClass = false; 36 | return; 37 | } 38 | 39 | this.CreateCommands( 40 | VoiceMeeterHelper.GetVirtualInputCount(), 41 | "EQGain3", 42 | VoiceMeeterHelper.GetHardwareInputCount() 43 | ).ConfigureAwait(false); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Enums/ErrorCode.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Enums 2 | { 3 | public enum ErrorCode : UInt16 4 | { 5 | None, 6 | NotConnected = 100, 7 | NotInstalled, 8 | ChannelOutOfRange, 9 | ParameterError, 10 | ParameterNotFound, 11 | StructureMismatch, 12 | } 13 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Extensions/ColorExtensions.cs: -------------------------------------------------------------------------------- 1 | // This file is part of the VoiceMeeterPlugin project. 2 | // 3 | // Copyright (c) 2022 Dominic Ris 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | namespace Loupedeck.VoiceMeeterPlugin.Extensions 24 | { 25 | using SkiaSharp; 26 | 27 | public static class ColorExtensions 28 | { 29 | public static BitmapColor ToBitmapColor(this SKColor color) => new(color.Red, color.Green, color.Blue, color.Alpha); 30 | } 31 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Extensions/EnumExtensions.cs: -------------------------------------------------------------------------------- 1 | // This file is part of the VoiceMeeterPlugin project. 2 | // 3 | // Copyright (c) 2023 Dominic Ris 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | namespace Loupedeck.VoiceMeeterPlugin.Extensions 24 | { 25 | public static class EnumExtensions 26 | { 27 | public static Boolean CompareInt(this Enum value, int compareValue) => Convert.ToInt32(value) == compareValue; 28 | 29 | public static Int32 ToInt(this Enum value) => Convert.ToInt32(value); 30 | } 31 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Extensions/PluginExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Extensions 2 | { 3 | using System.Text.RegularExpressions; 4 | 5 | using Enums; 6 | 7 | public static partial class PluginExtensions 8 | { 9 | public static void SetStatus(this Plugin plugin, PluginStatus status, ErrorCode errorCode = ErrorCode.None) 10 | { 11 | ArgumentNullException.ThrowIfNull(plugin); 12 | 13 | if (status == PluginStatus.Error) 14 | { 15 | if (errorCode == 0) 16 | { 17 | throw new ArgumentNullException(nameof(errorCode)); 18 | } 19 | } 20 | 21 | var message = LettersRegex().Replace(errorCode.ToString(), m => $"{m.Value[0]} {Char.ToLower(m.Value[1])}"); 22 | 23 | if (plugin.PluginStatus.Status == status && plugin.PluginStatus.Message == message) 24 | { 25 | return; 26 | } 27 | 28 | plugin.OnPluginStatusChanged( 29 | status, 30 | message, 31 | $"https://help.xeroxdev.de/en/loupedeck/voicemeeter/error/{(UInt16)errorCode}", 32 | $"Error {(UInt16)errorCode}" 33 | ); 34 | 35 | // reset the status after 5 seconds 36 | if (status == PluginStatus.Error) 37 | { 38 | Task.Delay(5000).ContinueWith(_ => plugin.ResetStatus()); 39 | } 40 | } 41 | 42 | public static void ResetStatus(this Plugin plugin) => plugin.SetStatus(PluginStatus.Normal); 43 | 44 | [GeneratedRegex("[a-z][A-Z]")] 45 | private static partial Regex LettersRegex(); 46 | } 47 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Helpers/ColorHelper.cs: -------------------------------------------------------------------------------- 1 | // This file is part of the VoiceMeeterPlugin project. 2 | // 3 | // Copyright (c) 2022 Dominic Ris 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | namespace Loupedeck.VoiceMeeterPlugin.Helpers 24 | { 25 | using SkiaSharp; 26 | 27 | public static class ColorHelper 28 | { 29 | public static SKColor Inactive => new(59, 75, 85); 30 | public static SKColor Active => new(82, 144, 112); 31 | public static SKColor Danger => new(196, 78, 61); 32 | public static SKColor SelActive => new(203, 174, 130); 33 | } 34 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Helpers/DrawingHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Helpers 2 | { 3 | using System.Globalization; 4 | 5 | using SkiaSharp; 6 | 7 | public static class DrawingHelper 8 | { 9 | private static readonly String RESOURCE_PATH = "Loupedeck.VoiceMeeterPlugin.Resources"; 10 | 11 | public static BitmapImage ReadImage(String imageName, String ext = "png", String addPath = "Images") 12 | => EmbeddedResources.ReadImage($"{RESOURCE_PATH}.{addPath}.{imageName}.{ext}"); 13 | 14 | public static BitmapBuilder LoadBitmapBuilder(String imageName = "clear", String text = null, BitmapColor? textColor = null, String ext = "png", String addPath = "Images") 15 | => LoadBitmapBuilder(ReadImage(imageName, ext, addPath), text, textColor); 16 | 17 | public static BitmapBuilder LoadBitmapBuilder(BitmapImage image, String text = null, BitmapColor? textColor = null) 18 | { 19 | var builder = new BitmapBuilder(80, 80); 20 | builder.Clear(BitmapColor.Black); 21 | builder.DrawImage(image); 22 | 23 | return text is null ? builder : builder.AddTextOutlined(text, textColor: textColor); 24 | } 25 | 26 | public static BitmapImage LoadBitmapImage(String imageName = "clear", String text = null, BitmapColor? textColor = null, String ext = "png", String addPath = "Images") 27 | => LoadBitmapBuilder(imageName, text, textColor, ext, addPath).ToImage(); 28 | 29 | public static BitmapImage LoadBitmapImage(BitmapImage image, String text = null, BitmapColor? textColor = null) 30 | => LoadBitmapBuilder(image, text, textColor).ToImage(); 31 | 32 | public static BitmapBuilder AddTextOutlined(this BitmapBuilder builder, String text, BitmapColor? outlineColor = null, BitmapColor? textColor = null, Int32 fontSize = 12) 33 | { 34 | builder.DrawText(text, 0, -30, 80, 80, textColor, fontSize, 0, 0); 35 | return builder; 36 | } 37 | 38 | public static BitmapImage DrawDefaultImage(String innerText, String outerText, SKColor brushColor, Int32 width = 80, Int32 height = 80) 39 | { 40 | SKTypeface font = SKTypeface.FromFamilyName("Arial", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright); 41 | var info = new SKImageInfo(width, height); 42 | using var surface = SKSurface.Create(info); 43 | var canvas = surface.Canvas; 44 | using var paint = new SKPaint(); 45 | paint.Color = brushColor; 46 | paint.IsAntialias = true; 47 | paint.Typeface = font; 48 | 49 | var rect = new SKRect(5, 20, width - 5, height - 20); 50 | 51 | paint.TextSize = GetOptimalFontSize(innerText, paint, rect); 52 | 53 | var cornerRadius = Math.Min(width, height) / 2; 54 | paint.Style = SKPaintStyle.Stroke; 55 | paint.StrokeWidth = 2; 56 | canvas.DrawRoundRect(rect, cornerRadius, cornerRadius, paint); 57 | paint.Style = SKPaintStyle.Fill; 58 | 59 | paint.TextAlign = SKTextAlign.Center; 60 | canvas.DrawText(innerText, rect.MidX, rect.MidY - (paint.FontMetrics.Descent + paint.FontMetrics.Ascent) / 2, paint); 61 | 62 | var image = surface.Snapshot(); 63 | var data = image.Encode(SKEncodedImageFormat.Png, 100); 64 | 65 | return LoadBitmapImage(BitmapImage.FromArray(data.ToArray()), outerText); 66 | } 67 | 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) 69 | { 70 | var dim = imageSize.GetDimension(); 71 | var percentage = (currentValue - minValue) / (maxValue - minValue); 72 | var height = (Int32)(dim * 0.9); 73 | var width = (Int32)(dim * 0.6); 74 | var calculatedHeight = (Int32)(height * percentage); 75 | var xCenter = dim / 2 - width / 2; 76 | var yCenter = dim / 2 + height / 2; 77 | using var builder = new BitmapBuilder(dim, dim); 78 | 79 | builder.Clear(BitmapColor.Black); 80 | builder.DrawRectangle(xCenter, yCenter, width, -height, backgroundColor); 81 | builder.FillRectangle(xCenter, yCenter, width, -calculatedHeight, backgroundColor); 82 | 83 | if (drawValue) 84 | { 85 | builder.DrawText((currentValue / scaleFactor).ToString(CultureInfo.CurrentCulture), foregroundColor); 86 | } 87 | 88 | var cmdSize = GetOptimalFontSize(cmd, dim: dim); 89 | builder.DrawText(cmd, 0, dim / 2 - cmdSize / 2, dim, dim, foregroundColor, cmdSize, 0, 0); 90 | 91 | if (String.IsNullOrWhiteSpace(name)) 92 | { 93 | return builder.ToImage(); 94 | } 95 | 96 | var nameSize = GetOptimalFontSize(name, dim: dim); 97 | builder.DrawText(name, 0, dim / 2 * -1 + nameSize / 2, dim, dim, foregroundColor, nameSize, 0, 0); 98 | 99 | return builder.ToImage(); 100 | } 101 | 102 | private static Int32 GetOptimalFontSize(String text, SKPaint paint = null, SKRect? rect = null, Int32? dim = null) 103 | { 104 | var minFontSize = 1; 105 | var maxFontSize = 16; 106 | SKRect textBounds = new SKRect(); 107 | 108 | if (paint is null) 109 | { 110 | paint = new SKPaint { IsAntialias = true }; 111 | } 112 | 113 | while (minFontSize <= maxFontSize) 114 | { 115 | var midFontSize = (minFontSize + maxFontSize) / 2; 116 | paint.TextSize = midFontSize; 117 | 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 | } 138 | } 139 | 140 | return maxFontSize; 141 | } 142 | 143 | 144 | 145 | private static Int32 GetDimension(this PluginImageSize size) => 146 | size switch 147 | { 148 | PluginImageSize.Width60 => 50, 149 | PluginImageSize.Width90 => 80, 150 | PluginImageSize.Width116 => 116, 151 | _ => 80, 152 | }; 153 | } 154 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Helpers/PluginLog.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Helpers 2 | { 3 | // A helper class that enables logging from the plugin code. 4 | 5 | internal static class PluginLog 6 | { 7 | private static PluginLogFile _pluginLogFile; 8 | 9 | public static void Init(PluginLogFile pluginLogFile) 10 | { 11 | pluginLogFile.CheckNullArgument(nameof(pluginLogFile)); 12 | _pluginLogFile = pluginLogFile; 13 | } 14 | 15 | public static void Verbose(String text) => _pluginLogFile?.Verbose(text); 16 | 17 | public static void Verbose(Exception ex, String text) => _pluginLogFile?.Verbose(ex, text); 18 | 19 | public static void Info(String text) => _pluginLogFile?.Info(text); 20 | 21 | public static void Info(Exception ex, String text) => _pluginLogFile?.Info(ex, text); 22 | 23 | public static void Warning(String text) => _pluginLogFile?.Warning(text); 24 | 25 | public static void Warning(Exception ex, String text) => _pluginLogFile?.Warning(ex, text); 26 | 27 | public static void Error(String text) => _pluginLogFile?.Error(text); 28 | 29 | public static void Error(Exception ex, String text) => _pluginLogFile?.Error(ex, text); 30 | } 31 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Helpers/PluginResources.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Helpers 2 | { 3 | using System.Reflection; 4 | 5 | // A helper class for managing plugin resources. 6 | // Note that the resource files handled by this class must be embedded in the plugin assembly at compile time. 7 | // That is, the Build Action of the files must be "Embedded Resource" in the plugin project. 8 | 9 | internal static class PluginResources 10 | { 11 | private static Assembly _assembly; 12 | 13 | public static void Init(Assembly assembly) 14 | { 15 | assembly.CheckNullArgument(nameof(assembly)); 16 | _assembly = assembly; 17 | } 18 | 19 | // Retrieves the names of all the resource files in the specified folder. 20 | // The parameter `folderName` must be specified as a full path, for example, `Loupedeck.VoiceMeeterPlugin.Resources`. 21 | // Returns the full names of the resource files, for example, `Loupedeck.VoiceMeeterPlugin.Resources.Resource.txt`. 22 | public static String[] GetFilesInFolder(String folderName) => _assembly.GetFilesInFolder(folderName); 23 | 24 | // Finds the first resource file with the specified file name. 25 | // Returns the full name of the found resource file. 26 | // Throws `FileNotFoundException` if the resource file is not found. 27 | public static String FindFile(String fileName) => _assembly.FindFileOrThrow(fileName); 28 | 29 | // Finds all the resource files that match the specified regular expression pattern. 30 | // Returns the full names of the found resource files. 31 | // Example: 32 | // `PluginResources.FindFiles(@"\w+\.txt$")` returns all the resource files with the extension `.txt`. 33 | public static String[] FindFiles(String regexPattern) => _assembly.FindFiles(regexPattern); 34 | 35 | // Finds the first resource file with the specified file name, and returns the file as a stream. 36 | // Throws `FileNotFoundException` if the resource file is not found. 37 | public static Stream GetStream(String resourceName) => _assembly.GetStream(FindFile(resourceName)); 38 | 39 | // Reads content of the specified text file, and returns the file content as a string. 40 | // Throws `FileNotFoundException` if the resource file is not found. 41 | public static String ReadTextFile(String resourceName) => _assembly.ReadTextFile(FindFile(resourceName)); 42 | 43 | // Reads content of the specified binary file, and returns the file content as bytes. 44 | // Throws `FileNotFoundException` if the resource file is not found. 45 | public static Byte[] ReadBinaryFile(String resourceName) => _assembly.ReadBinaryFile(FindFile(resourceName)); 46 | 47 | // Reads content of the specified image file, and returns the file content as a bitmap image. 48 | // Throws `FileNotFoundException` if the resource file is not found. 49 | public static BitmapImage ReadImage(String resourceName) => _assembly.ReadImage(FindFile(resourceName)); 50 | 51 | // Extracts the specified resource file to the given file path in the file system. 52 | // Throws `FileNotFoundException` if the resource file is not found, or a system exception if the output file cannot be written. 53 | public static void ExtractFile(String resourceName, String filePathName) 54 | => _assembly.ExtractFile(FindFile(resourceName), filePathName); 55 | } 56 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Helpers/VoiceMeeterHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Helpers 2 | { 3 | using Library.Voicemeeter; 4 | 5 | public static class VoiceMeeterHelper 6 | { 7 | public static Int32 GetHardwareInputCount() 8 | { 9 | var version = Remote.Version; 10 | return version switch 11 | { 12 | RunVoicemeeterParam.VoicemeeterPotato => 5, 13 | _ => 3 14 | }; 15 | } 16 | 17 | public static Int32 GetVirtualInputCount() 18 | { 19 | var version = Remote.Version; 20 | return version switch 21 | { 22 | RunVoicemeeterParam.VoicemeeterPotato => 3, 23 | _ => 2 24 | }; 25 | } 26 | 27 | public static Int32 GetMasterSectionCount() 28 | { 29 | var version = Remote.Version; 30 | return version switch 31 | { 32 | RunVoicemeeterParam.VoicemeeterPotato => 8, 33 | _ => 5 34 | }; 35 | } 36 | 37 | public static Int32 GetStripACount() 38 | { 39 | var version = Remote.Version; 40 | return version switch 41 | { 42 | RunVoicemeeterParam.VoicemeeterPotato => 5, 43 | _ => 3 44 | }; 45 | } 46 | 47 | public static Int32 GetStripBCount() 48 | { 49 | var version = Remote.Version; 50 | return version switch 51 | { 52 | RunVoicemeeterParam.VoicemeeterPotato => 3, 53 | _ => 2 54 | }; 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Library/Voicemeeter/Defines.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Library.Voicemeeter 2 | { 3 | public enum RunVoicemeeterParam 4 | { 5 | None = 0, 6 | Voicemeeter = 1, 7 | VoicemeeterBanana = 2, 8 | VoicemeeterPotato = 3, 9 | }; 10 | 11 | public enum LevelType 12 | { 13 | PreFaderInput = 0, 14 | PostFaderInput = 1, 15 | PostMuteInput = 2, 16 | Output = 3 17 | }; 18 | 19 | public enum LoginResponse 20 | { 21 | AlreadyLoggedIn = -2, 22 | NoClient = -1, 23 | Ok = 0, 24 | VoicemeeterNotRunning = 1, 25 | } 26 | 27 | public static class InputChannel 28 | { 29 | public const Int32 Strip1Left = 0; 30 | public const Int32 Strip1Right = 1; 31 | public const Int32 Strip2Left = 2; 32 | public const Int32 Strip2Right = 3; 33 | public const Int32 Strip3Left = 4; 34 | public const Int32 Strip3Right = 5; 35 | public const Int32 VaioLeft = 6; 36 | public const Int32 VaioRight = 7; 37 | public const Int32 AuxLeft = 8; 38 | public const Int32 AuxRight = 9; 39 | } 40 | 41 | public static class OutputChannel 42 | { 43 | public const Int32 A1Left = 0; 44 | public const Int32 A1Right = 1; 45 | public const Int32 A2Left = 2; 46 | public const Int32 A2Right = 3; 47 | public const Int32 A3Left = 4; 48 | public const Int32 A3Right = 5; 49 | public const Int32 Bus1Left = 6; 50 | public const Int32 Bus1Right = 7; 51 | public const Int32 Bus2Left = 8; 52 | public const Int32 Bus2Right = 9; 53 | } 54 | 55 | public static class VoicemeeterCommand 56 | { 57 | public static String Shutdown = "Command.Shutdown"; 58 | public static String Show = "Command.Show"; 59 | public static String Restart = "Command.Restart"; 60 | public static String Eject = "Command.Eject"; 61 | public static String Reset = "Command.Reset"; 62 | public static String Save = "Command.Save"; 63 | public static String Load = "Command.Load"; 64 | } 65 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Library/Voicemeeter/Levels.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Library.Voicemeeter 2 | { 3 | using System.Reactive.Linq; 4 | 5 | /// <summary> 6 | /// Observable levels monitor. Use Rx to subscribe to the levels of the selected Channels. 7 | /// Usage: 8 | /// using System.Reactive.Linq; 9 | /// var levels = new Voicemeeter.Levels(channels, 20); 10 | /// var subscription = levels.Subscribe(x => DoSomethingWithFloatArray(x)); 11 | /// ... 12 | /// subscription.Dispose(); levels.Dispose(); 13 | /// </summary> 14 | public class Levels : IDisposable, IObservable<Single[]> 15 | { 16 | public class Channel 17 | { 18 | public LevelType LevelType { get; set; } 19 | public Int32 ChannelNumber { get; set; } 20 | }; 21 | 22 | private readonly List<Channel> _channels; 23 | private readonly List<IObserver<Single[]>> _observers = []; 24 | private readonly IObservable<Int32> _timer; 25 | private IDisposable _timerSubscription; 26 | private List<Single> _oldValues; 27 | 28 | public Levels(Int32 milliseconds = 20) 29 | { 30 | this._channels = []; 31 | this._timer = Observable.Interval(TimeSpan.FromMilliseconds(milliseconds)).Select(_ => 1); 32 | this.Watch(); 33 | } 34 | 35 | public void AddChannel(Channel channel) 36 | { 37 | // first check if there's already a channel with the same LevelType and ChannelNumber 38 | if (this._channels.Any(c => c.LevelType == channel.LevelType && c.ChannelNumber == channel.ChannelNumber)) 39 | { 40 | return; 41 | } 42 | 43 | this._channels.Add(channel); 44 | } 45 | 46 | private void Watch() => 47 | this._timerSubscription = this._timer.Subscribe(_ => 48 | { 49 | if (this._channels.Count == 0) 50 | { 51 | return; 52 | } 53 | var values = new List<Single>(this._channels.Count); 54 | values.AddRange(this._channels.Select(channel => Remote.GetLevel(channel.LevelType, channel.ChannelNumber))); 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; 63 | this.Notify(values.ToArray()); 64 | }); 65 | 66 | public IDisposable Subscribe(IObserver<Single[]> observer) 67 | { 68 | if (!this._observers.Contains(observer)) 69 | { 70 | this._observers.Add(observer); 71 | } 72 | 73 | return new Unsubscriber(this._observers, observer); 74 | } 75 | 76 | private void Notify(Single[] values) 77 | { 78 | foreach (var observer in this._observers) 79 | { 80 | observer.OnNext(values); 81 | } 82 | } 83 | 84 | public void Dispose() => this._timerSubscription?.Dispose(); 85 | 86 | private sealed class Unsubscriber(List<IObserver<Single[]>> observers, IObserver<Single[]> observer) : IDisposable 87 | { 88 | public void Dispose() 89 | { 90 | if (observer != null && observers.Contains(observer)) 91 | { 92 | observers.Remove(observer); 93 | } 94 | } 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Library/Voicemeeter/Parameters.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Library.Voicemeeter 2 | { 3 | using System.Reactive.Linq; 4 | 5 | /// <summary> 6 | /// Observable parameters monitor. Use Rx to subscribe to parameter changes. 7 | /// Usage: 8 | /// using System.Reactive.Linq; 9 | /// var parameters = new Voicemeeter.Parameters(); 10 | /// var subscription = parameters.Parameters(x => DoSomethingTheParametersChanged(x)); 11 | /// ... 12 | /// subscription.Dispose(); levels.Dispose(); 13 | /// </summary> 14 | public class Parameters : IDisposable, IObservable<Int32> 15 | { 16 | private readonly List<IObserver<Int32>> _observers = []; 17 | private readonly IObservable<Int32> _timer; 18 | private IDisposable _timerSubscription; 19 | 20 | public Parameters(Int32 milliseconds = 20) 21 | { 22 | this._timer = Observable.Interval(TimeSpan.FromMilliseconds(milliseconds)).Select(_ => 1); 23 | this.Watch(); 24 | } 25 | 26 | private void Watch() => 27 | this._timerSubscription = this._timer.Subscribe(_ => 28 | { 29 | var response = Remote.IsParametersDirty(); 30 | if (response > 0) 31 | { 32 | this.Notify(response); 33 | } 34 | }); 35 | 36 | public IDisposable Subscribe(IObserver<Int32> observer) 37 | { 38 | if (!this._observers.Contains(observer)) 39 | { 40 | lock (this._observers) 41 | { 42 | this._observers.Add(observer); 43 | } 44 | } 45 | 46 | return new Unsubscriber(this._observers, observer); 47 | } 48 | 49 | private void Notify(Int32 value) 50 | { 51 | lock (this._observers) 52 | { 53 | foreach (var observer in this._observers) 54 | { 55 | observer.OnNext(value); 56 | } 57 | } 58 | } 59 | 60 | public void Dispose() => this._timerSubscription?.Dispose(); 61 | 62 | private sealed class Unsubscriber(List<IObserver<Int32>> observers, IObserver<Int32> observer) : IDisposable 63 | { 64 | public void Dispose() 65 | { 66 | if (observer != null && observers.Contains(observer)) 67 | { 68 | observers.Remove(observer); 69 | } 70 | } 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Library/Voicemeeter/Remote.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Library.Voicemeeter 2 | { 3 | using System.Runtime.Versioning; 4 | using System.Text; 5 | 6 | using Enums; 7 | 8 | using Extensions; 9 | 10 | using Microsoft.Win32; 11 | 12 | public static class Remote 13 | { 14 | // Don't keep loading the DLL 15 | private static IntPtr? _handle; 16 | private static ClientApplication ClientApplication { get; set; } 17 | 18 | public static RunVoicemeeterParam Version { get; set; } 19 | 20 | #region Parameters 21 | 22 | /// <summary> 23 | /// Gets a text value 24 | /// </summary> 25 | /// <param name="parameter"></param> 26 | /// <returns></returns> 27 | public static String GetTextParameter(String parameter) 28 | { 29 | var buffer = new StringBuilder(255); 30 | var code = RemoteWrapper.InternalGetParameterW(parameter, buffer); 31 | return code == 0 ? buffer.ToString() : null; 32 | } 33 | 34 | /// <summary> 35 | /// Set a text value 36 | /// </summary> 37 | /// <param name="parameter"></param> 38 | /// <param name="value"></param> 39 | /// <returns></returns> 40 | public static void SetTextParameter(String parameter, String value) => 41 | TestResult(RemoteWrapper.InternalSetParameterW(parameter, value)); 42 | 43 | /// <summary> 44 | /// Get a named parameter 45 | /// </summary> 46 | /// <param name="parameter">Parameter name</param> 47 | /// <returns>float value</returns> 48 | public static Single GetParameter(String parameter) 49 | { 50 | Single value = 0; 51 | TestResult(RemoteWrapper.GetParameter(parameter, ref value)); 52 | return value; 53 | } 54 | 55 | /// <summary> 56 | /// Set a named parameter 57 | /// </summary> 58 | /// <param name="parameter">Parameter name</param> 59 | /// <param name="value">float value</param> 60 | public static void SetParameter(String parameter, Single value) => 61 | TestResult(RemoteWrapper.SetParameter(parameter, value)); 62 | 63 | 64 | /// <summary> 65 | /// Set one or several parameters by a script 66 | /// </summary> 67 | /// <param name="parameters">One or more instructions separated by comma, semicolon or newline</param> 68 | public static void SetParameters(String parameters) => TestResult(RemoteWrapper.SetParameters(parameters)); 69 | 70 | #endregion 71 | 72 | #region Commands 73 | 74 | /// <summary> 75 | /// Start the VoiceMeeter program 76 | /// </summary> 77 | /// <param name="voicemeterType"></param> 78 | public static void Start(RunVoicemeeterParam voicemeterType) 79 | { 80 | switch (RemoteWrapper.InternalRunVoicemeeter((Int32)voicemeterType)) 81 | { 82 | case 0: return; 83 | case -1: 84 | SendError(ErrorCode.NotInstalled); 85 | return; 86 | default: 87 | SendError(); 88 | return; 89 | } 90 | } 91 | 92 | /// <summary> 93 | /// Shutdown the VoiceMeeter program 94 | /// </summary> 95 | public static void Shutdown() => TestResult(RemoteWrapper.SetParameter(VoicemeeterCommand.Shutdown, 1)); 96 | 97 | /// <summary> 98 | /// Restart the audio engine 99 | /// </summary> 100 | public static void Restart() => TestResult(RemoteWrapper.SetParameter(VoicemeeterCommand.Restart, 1)); 101 | 102 | /// <summary> 103 | /// Shows the running Voicemeeter application if minimized. 104 | /// </summary> 105 | public static void Show() => TestResult(RemoteWrapper.SetParameter(VoicemeeterCommand.Show, 1)); 106 | 107 | /// <summary> 108 | /// Return if the parameters have changed since the last time this method was called. 109 | /// </summary> 110 | public static Int32 IsParametersDirty() 111 | { 112 | try 113 | { 114 | return RemoteWrapper.IsParametersDirty(); 115 | } 116 | catch (Exception) 117 | { 118 | // TODO: Figure out the Memory Exception when calling the API 119 | } 120 | 121 | return 0; 122 | } 123 | 124 | /// <summary> 125 | /// Eject Cassette 126 | /// </summary> 127 | public static void Eject() => TestResult(RemoteWrapper.SetParameter(VoicemeeterCommand.Eject, 1)); 128 | 129 | /// <summary> 130 | /// Reset all configuration 131 | /// </summary> 132 | public static void Reset() => TestResult(RemoteWrapper.SetParameter(VoicemeeterCommand.Reset, 1)); 133 | 134 | /// <summary> 135 | /// Load a configuation file name 136 | /// </summary> 137 | /// <param name="configurationFileName">Full path to file</param> 138 | public static void Load(String configurationFileName) => 139 | SetTextParameter(VoicemeeterCommand.Load, configurationFileName); 140 | 141 | /// <summary> 142 | /// Save a configuration to the given file name 143 | /// </summary> 144 | /// <param name="configurationFileName">Full path to file</param> 145 | public static void Save(String configurationFileName) => 146 | SetTextParameter(VoicemeeterCommand.Load, configurationFileName); 147 | 148 | #endregion 149 | 150 | #region Rx 151 | 152 | public static Single GetLevel(LevelType type, Int32 channel) 153 | { 154 | Single value = 0; 155 | TestLevelResult(RemoteWrapper.GetLevel((Int32)type, channel, ref value)); 156 | return value; 157 | } 158 | 159 | private static void TestLevelResult(Int32 result) 160 | { 161 | // 0: OK(no error). 162 | // -1: error 163 | // -2: no server. 164 | // -3: no level available 165 | // -4: out of range 166 | switch (result) 167 | { 168 | case 0: return; 169 | case -1: 170 | SendError(); 171 | return; 172 | case -2: 173 | SendError(ErrorCode.NotConnected); 174 | return; 175 | case -3: 176 | return; 177 | case -4: 178 | SendError(ErrorCode.ChannelOutOfRange); 179 | return; 180 | default: 181 | SendError(); 182 | return; 183 | } 184 | } 185 | 186 | #endregion 187 | 188 | /// <summary> 189 | /// Logs into the Voicemeeter application. Starts the given application (Voicemeeter, Bananna, Potato) if it is not already runnning. 190 | /// </summary> 191 | /// <param name="voicemeeterType">The Voicemeeter program to run</param> 192 | /// <param name="application"></param> 193 | /// <returns>IDisposable class to dispose when finished with the remote.</returns> 194 | [SupportedOSPlatform("windows")] 195 | public static async Task<IDisposable> Initialize(RunVoicemeeterParam voicemeeterType, 196 | ClientApplication application) 197 | { 198 | ClientApplication = application; 199 | if (_handle.HasValue == false) 200 | { 201 | // Find current version from the registry 202 | const String key = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"; 203 | const String key32 = 204 | @"HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"; 205 | const String uninstKey = "VB:Voicemeeter {17359A74-1236-5467}"; 206 | // if not windows, throw exception 207 | var voicemeeter = Registry.GetValue($"{key}\\{uninstKey}", "UninstallString", null); 208 | 209 | if (voicemeeter == null && Environment.Is64BitProcess) 210 | { 211 | // Fall back to 32-bits registry 212 | voicemeeter = Registry.GetValue($"{key32}\\{uninstKey}", "UninstallString", null); 213 | } 214 | 215 | if (voicemeeter == null) 216 | { 217 | SendError(ErrorCode.NotInstalled); 218 | return null; 219 | } 220 | 221 | var voicemeeterString = voicemeeter.ToString()?.Split('\\').Last(); 222 | var directoryName = Path.GetDirectoryName(voicemeeter.ToString()); 223 | if (directoryName == null) 224 | { 225 | SendError(ErrorCode.NotInstalled); 226 | return null; 227 | } 228 | 229 | if (voicemeeterString.ContainsNoCase('8')) 230 | { 231 | Version = RunVoicemeeterParam.VoicemeeterPotato; 232 | } 233 | else if (voicemeeterString.ContainsNoCase("pro")) 234 | { 235 | Version = RunVoicemeeterParam.VoicemeeterBanana; 236 | } 237 | else 238 | { 239 | //Maybe their install EXE was named something different:) 240 | var potatoPanelFile = Path.Combine(directoryName, "VBVMVAIO3_ControlPanel.exe"); 241 | var bananaPanelFile = Path.Combine(directoryName, "VBVMAUX_ControlPanel.exe"); 242 | if (File.Exists(potatoPanelFile)) 243 | { 244 | Version = RunVoicemeeterParam.VoicemeeterPotato; 245 | } 246 | else if (File.Exists(bananaPanelFile)) 247 | { 248 | Version = RunVoicemeeterParam.VoicemeeterBanana; 249 | } 250 | else 251 | { 252 | Version = RunVoicemeeterParam.Voicemeeter; 253 | } 254 | } 255 | 256 | 257 | _handle = Wrapper.LoadLibrary( 258 | Path.Combine(directoryName, 259 | Environment.Is64BitProcess ? "VoicemeeterRemote64.dll" : "VoicemeeterRemote.dll")); 260 | } 261 | 262 | var startVoiceMeeter = voicemeeterType != RunVoicemeeterParam.None; 263 | 264 | if (await Login(voicemeeterType, startVoiceMeeter).ConfigureAwait(false)) 265 | { 266 | return new VoicemeeterClient(); 267 | } 268 | 269 | return null; 270 | } 271 | 272 | public static async Task<Boolean> Login(RunVoicemeeterParam voicemeeterType, Boolean retry = true) 273 | { 274 | while (true) 275 | { 276 | switch ((LoginResponse)RemoteWrapper.LoginVoicemeeter()) 277 | { 278 | case LoginResponse.Ok: 279 | case LoginResponse.AlreadyLoggedIn: 280 | return true; 281 | 282 | case LoginResponse.VoicemeeterNotRunning: 283 | if (retry) 284 | { 285 | // Run voicemeeter program 286 | Start(voicemeeterType); 287 | 288 | await Task.Delay(2000).ConfigureAwait(false); 289 | retry = false; 290 | continue; 291 | } 292 | 293 | break; 294 | } 295 | 296 | return false; 297 | } 298 | } 299 | 300 | private static void TestResult(Int32 result) 301 | { 302 | //0: OK(no error). 303 | //-1: error 304 | //-2: no server. 305 | //-3: unknown parameter 306 | //-5: structure mismatch 307 | switch (result) 308 | { 309 | case 0: return; 310 | case -1: 311 | SendError(ErrorCode.ParameterError); 312 | return; 313 | case -2: 314 | SendError(ErrorCode.NotConnected); 315 | return; 316 | case -3: 317 | SendError(ErrorCode.ParameterNotFound); 318 | return; 319 | case -5: 320 | SendError(ErrorCode.StructureMismatch); 321 | return; 322 | default: 323 | SendError(); 324 | return; 325 | } 326 | } 327 | 328 | private static void SendError(ErrorCode errorCode = ErrorCode.None) 329 | => ClientApplication?.Plugin.SetStatus(PluginStatus.Error, errorCode); 330 | } 331 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Library/Voicemeeter/RemoteWrapper.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Library.Voicemeeter 2 | { 3 | using System.Runtime.InteropServices; 4 | using System.Text; 5 | 6 | internal static class RemoteWrapper 7 | { 8 | internal static Int32 LoginVoicemeeter() => 9 | Environment.Is64BitProcess 10 | ? RemoteWrapper64.LoginVoicemeeter() 11 | : RemoteWrapper32.LoginVoicemeeter(); 12 | 13 | internal static Int32 Logout() => 14 | Environment.Is64BitProcess 15 | ? RemoteWrapper64.Logout() 16 | : RemoteWrapper32.Logout(); 17 | 18 | internal static Int32 InternalRunVoicemeeter(Int32 voicemeterType) => 19 | Environment.Is64BitProcess 20 | ? RemoteWrapper64.InternalRunVoicemeeter(voicemeterType) 21 | : RemoteWrapper32.InternalRunVoicemeeter(voicemeterType); 22 | 23 | internal static Int32 GetParameter(String szParamName, ref Single value) => 24 | Environment.Is64BitProcess 25 | ? RemoteWrapper64.GetParameter(szParamName, ref value) 26 | : RemoteWrapper32.GetParameter(szParamName, ref value); 27 | 28 | internal static Int32 SetParameter(String szParamName, Single value) => 29 | Environment.Is64BitProcess 30 | ? RemoteWrapper64.SetParameter(szParamName, value) 31 | : RemoteWrapper32.SetParameter(szParamName, value); 32 | 33 | internal static Int32 InternalGetParameterW(String szParamName, StringBuilder value) => 34 | Environment.Is64BitProcess 35 | ? RemoteWrapper64.InternalGetParameterW(szParamName, value) 36 | : RemoteWrapper32.InternalGetParameterW(szParamName, value); 37 | 38 | internal static Int32 InternalSetParameterW(String szParamName, String value) => 39 | Environment.Is64BitProcess 40 | ? RemoteWrapper64.InternalSetParameterW(szParamName, value) 41 | : RemoteWrapper32.InternalSetParameterW(szParamName, value); 42 | 43 | internal static Int32 IsParametersDirty() => 44 | Environment.Is64BitProcess 45 | ? RemoteWrapper64.IsParametersDirty() 46 | : RemoteWrapper32.IsParametersDirty(); 47 | 48 | internal static Int32 GetLevel(Int32 nType, Int32 nuChannel, ref Single value) => 49 | Environment.Is64BitProcess 50 | ? RemoteWrapper64.GetLevel(nType, nuChannel, ref value) 51 | : RemoteWrapper32.GetLevel(nType, nuChannel, ref value); 52 | 53 | public static Int32 SetParameters(String szParameters) => 54 | Environment.Is64BitProcess 55 | ? RemoteWrapper64.SetParameters(szParameters) 56 | : RemoteWrapper32.SetParameters(szParameters); 57 | } 58 | 59 | 60 | internal static class RemoteWrapper32 61 | { 62 | [DllImport("VoicemeeterRemote.dll", EntryPoint = "VBVMR_Login", CallingConvention = CallingConvention.StdCall)] 63 | internal static extern Int32 LoginVoicemeeter(); 64 | 65 | [DllImport("VoicemeeterRemote.dll", EntryPoint = "VBVMR_Logout")] 66 | internal static extern Int32 Logout(); 67 | 68 | [DllImport("VoicemeeterRemote.dll", EntryPoint = "VBVMR_RunVoicemeeter")] 69 | internal static extern Int32 InternalRunVoicemeeter(Int32 voicemeterType); 70 | 71 | // Get/Set Parameters return codes 72 | // returns 0: OK (no error). 73 | // -1: error 74 | // -2: no server. 75 | // -3: unknown parameter 76 | // -5: structure mismatch 77 | 78 | // long __stdcall VBVMR_GetParameterFloat(char * szParamName, float * pValue); 79 | [DllImport("VoicemeeterRemote.dll", EntryPoint = "VBVMR_GetParameterFloat")] 80 | internal static extern Int32 GetParameter(String szParamName, ref Single value); 81 | 82 | // long __stdcall VBVMR_SetParameterFloat(char * szParamName, float Value); 83 | [DllImport("VoicemeeterRemote.dll", EntryPoint = "VBVMR_SetParameterFloat")] 84 | internal static extern Int32 SetParameter(String szParamName, Single value); 85 | 86 | //long __stdcall VBVMR_GetParameterStringA(char* szParamName, char* szString); 87 | //long __stdcall VBVMR_GetParameterStringW(char* szParamName, unsigned short* wszString); 88 | [DllImport("VoicemeeterRemote.dll", EntryPoint = "VBVMR_GetParameterStringW", 89 | CallingConvention = CallingConvention.StdCall)] 90 | internal static extern Int32 InternalGetParameterW( 91 | [MarshalAs(UnmanagedType.LPStr)] String szParamName, // char* 92 | [MarshalAs(UnmanagedType.LPWStr)] StringBuilder value); // unsigned short* 93 | 94 | // long __stdcall VBVMR_SetParameterStringA(char* szParamName, char* szString); 95 | // long __stdcall VBVMR_SetParameterStringW(char* szParamName, unsigned short* wszString); 96 | [DllImport("VoicemeeterRemote.dll", EntryPoint = "VBVMR_SetParameterStringW", 97 | CallingConvention = CallingConvention.StdCall)] 98 | internal static extern Int32 InternalSetParameterW( 99 | [MarshalAs(UnmanagedType.LPStr)] String szParamName, // char* 100 | [MarshalAs(UnmanagedType.LPWStr)] String value); // unsigned short* 101 | 102 | // Check if parameters have changed. 103 | // Call this function periodically (typically every 10 or 20ms). 104 | // (this function must be called from one thread only) 105 | // returns: 0: no new paramters. 106 | // 1: New parameters -> update your display. 107 | // -1: error(unexpected) 108 | // -2: no server. 109 | // long __stdcall VBVMR_IsParametersDirty(void); 110 | [DllImport("VoicemeeterRemote.dll", EntryPoint = "VBVMR_IsParametersDirty")] 111 | internal static extern Int32 IsParametersDirty(); 112 | 113 | // long __stdcall VBVMR_GetLevel(long nType, long nuChannel, float* pValue); 114 | [DllImport("VoicemeeterRemote.dll", EntryPoint = "VBVMR_GetLevel")] 115 | internal static extern Int32 GetLevel(Int32 nType, Int32 nuChannel, ref Single value); 116 | 117 | // long __stadcall VBVMR_SetParameters(char* szParameters) 118 | // long __stadcall VBVMR_SetParameters(unsigned short* szParameters) 119 | [DllImport("VoicemeeterRemote.dll", EntryPoint = "VBVMR_SetParametersW", 120 | CallingConvention = CallingConvention.StdCall)] 121 | public static extern Int32 SetParameters([MarshalAs(UnmanagedType.LPWStr)] String szParameters); 122 | } 123 | 124 | 125 | internal static class RemoteWrapper64 126 | { 127 | [DllImport("VoicemeeterRemote64.dll", EntryPoint = "VBVMR_Login", 128 | CallingConvention = CallingConvention.StdCall)] 129 | internal static extern Int32 LoginVoicemeeter(); 130 | 131 | [DllImport("VoicemeeterRemote64.dll", EntryPoint = "VBVMR_Logout")] 132 | internal static extern Int32 Logout(); 133 | 134 | [DllImport("VoicemeeterRemote64.dll", EntryPoint = "VBVMR_RunVoicemeeter")] 135 | internal static extern Int32 InternalRunVoicemeeter(Int32 voicemeterType); 136 | 137 | // Get/Set Parameters return codes 138 | // returns 0: OK (no error). 139 | // -1: error 140 | // -2: no server. 141 | // -3: unknown parameter 142 | // -5: structure mismatch 143 | 144 | // long __stdcall VBVMR_GetParameterFloat(char * szParamName, float * pValue); 145 | [DllImport("VoicemeeterRemote64.dll", EntryPoint = "VBVMR_GetParameterFloat")] 146 | internal static extern Int32 GetParameter(String szParamName, ref Single value); 147 | 148 | // long __stdcall VBVMR_SetParameterFloat(char * szParamName, float Value); 149 | [DllImport("VoicemeeterRemote64.dll", EntryPoint = "VBVMR_SetParameterFloat")] 150 | internal static extern Int32 SetParameter(String szParamName, Single value); 151 | 152 | //long __stdcall VBVMR_GetParameterStringA(char* szParamName, char* szString); 153 | //long __stdcall VBVMR_GetParameterStringW(char* szParamName, unsigned short* wszString); 154 | [DllImport("VoicemeeterRemote64.dll", EntryPoint = "VBVMR_GetParameterStringW", 155 | CallingConvention = CallingConvention.StdCall)] 156 | internal static extern Int32 InternalGetParameterW( 157 | [MarshalAs(UnmanagedType.LPStr)] String szParamName, // char* 158 | [MarshalAs(UnmanagedType.LPWStr)] StringBuilder value); // unsigned short* 159 | 160 | // long __stdcall VBVMR_SetParameterStringA(char* szParamName, char* szString); 161 | // long __stdcall VBVMR_SetParameterStringW(char* szParamName, unsigned short* wszString); 162 | [DllImport("VoicemeeterRemote64.dll", EntryPoint = "VBVMR_SetParameterStringW", 163 | CallingConvention = CallingConvention.StdCall)] 164 | internal static extern Int32 InternalSetParameterW( 165 | [MarshalAs(UnmanagedType.LPStr)] String szParamName, // char* 166 | [MarshalAs(UnmanagedType.LPWStr)] String value); // unsigned short* 167 | 168 | // Check if parameters have changed. 169 | // Call this function periodically (typically every 10 or 20ms). 170 | // (this function must be called from one thread only) 171 | // returns: 0: no new paramters. 172 | // 1: New parameters -> update your display. 173 | // -1: error(unexpected) 174 | // -2: no server. 175 | // long __stdcall VBVMR_IsParametersDirty(void); 176 | [DllImport("VoicemeeterRemote64.dll", EntryPoint = "VBVMR_IsParametersDirty")] 177 | internal static extern Int32 IsParametersDirty(); 178 | 179 | // long __stdcall VBVMR_GetLevel(long nType, long nuChannel, float* pValue); 180 | [DllImport("VoicemeeterRemote64.dll", EntryPoint = "VBVMR_GetLevel")] 181 | internal static extern Int32 GetLevel(Int32 nType, Int32 nuChannel, ref Single value); 182 | 183 | // long __stadcall VBVMR_SetParameters(char* szParameters) 184 | // long __stadcall VBVMR_SetParameters(unsigned short* szParameters) 185 | [DllImport("VoicemeeterRemote64.dll", EntryPoint = "VBVMR_SetParametersW", 186 | CallingConvention = CallingConvention.StdCall)] 187 | public static extern Int32 SetParameters([MarshalAs(UnmanagedType.LPWStr)] String szParameters); 188 | } 189 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Library/Voicemeeter/VoicemeeterClient.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Library.Voicemeeter 2 | { 3 | public sealed class VoicemeeterClient : IDisposable, IObservable<Single> 4 | { 5 | public void Dispose() 6 | { 7 | try 8 | { 9 | RemoteWrapper.Logout(); 10 | } 11 | catch (Exception) 12 | { 13 | // ignored 14 | } 15 | } 16 | 17 | private readonly List<IObserver<Single>> _observers = []; 18 | 19 | public IDisposable Subscribe(IObserver<Single> observer) 20 | { 21 | if (!this._observers.Contains(observer)) 22 | { 23 | this._observers.Add(observer); 24 | } 25 | 26 | return new Unsubscriber(this._observers, observer); 27 | } 28 | 29 | private void Notify(Single value) 30 | { 31 | foreach (var observer in this._observers) 32 | { 33 | observer.OnNext(value); 34 | } 35 | } 36 | 37 | private sealed class Unsubscriber(List<IObserver<Single>> observers, IObserver<Single> observer) : IDisposable 38 | { 39 | public void Dispose() 40 | { 41 | if (observer != null && observers.Contains(observer)) 42 | { 43 | observers.Remove(observer); 44 | } 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Library/Voicemeeter/Wrapper.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Library.Voicemeeter 2 | { 3 | using System.Runtime.InteropServices; 4 | 5 | internal static class Wrapper 6 | { 7 | // Load the DLL to pull it into the running process 8 | [DllImport("kernel32.dll")] 9 | internal static extern IntPtr LoadLibrary(String dllToLoad); 10 | } 11 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Resources/Images/clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/82c13aaefcf2cd3d3ea6ea162931536c9320f3d9/src/VoiceMeeterPlugin/Resources/Images/clear.png -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/Services/VoiceMeeterService.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin.Services 2 | { 3 | using Library.Voicemeeter; 4 | 5 | public sealed class VoiceMeeterService 6 | { 7 | public static VoiceMeeterService Instance => Lazy.Value; 8 | 9 | private static readonly Lazy<VoiceMeeterService> Lazy = new(() => new VoiceMeeterService()); 10 | 11 | public Parameters Parameters { get; set; } 12 | public Levels Levels { get; set; } 13 | public Boolean Connected { get; set; } 14 | 15 | public async Task StartService(ClientApplication application) 16 | { 17 | await Remote.Initialize(RunVoicemeeterParam.None, application); 18 | 19 | this.Connected = true; 20 | this.Parameters = new Parameters(); 21 | this.Levels = new Levels(); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/VoiceMeeterApplication.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin 2 | { 3 | using Services; 4 | 5 | // This class can be used to connect the Loupedeck plugin to an application. 6 | 7 | public class VoiceMeeterApplication : ClientApplication 8 | { 9 | public VoiceMeeterApplication() => VoiceMeeterService.Instance.StartService(this).ConfigureAwait(true); 10 | 11 | // This method can be used to link the plugin to a Windows application. 12 | protected override String GetProcessName() => ""; 13 | 14 | // This method can be used to link the plugin to a macOS application. 15 | protected override String GetBundleName() => ""; 16 | 17 | // This method can be used to check whether the application is installed or not. 18 | public override ClientApplicationStatus GetApplicationStatus() => ClientApplicationStatus.Unknown; 19 | 20 | protected override Boolean IsProcessNameSupported(String processName) => 21 | processName.ContainsNoCase("VB-AUDIO Virtual Audi Device") || processName.ContainsNoCase("VoiceMeeter"); 22 | } 23 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/VoiceMeeterPlugin.cs: -------------------------------------------------------------------------------- 1 | namespace Loupedeck.VoiceMeeterPlugin 2 | { 3 | using Helpers; 4 | 5 | using Library.Voicemeeter; 6 | 7 | // This class contains the plugin-level logic of the Loupedeck plugin. 8 | 9 | public class VoiceMeeterPlugin : Plugin 10 | { 11 | // Gets a value indicating whether this is an API-only plugin. 12 | public override Boolean UsesApplicationApiOnly => true; 13 | 14 | // Gets a value indicating whether this is a Universal plugin or an Application plugin. 15 | public override Boolean HasNoApplication => true; 16 | 17 | // Initializes a new instance of the plugin class. 18 | public VoiceMeeterPlugin() 19 | { 20 | // Initialize the plugin log. 21 | PluginLog.Init(this.Log); 22 | 23 | // Initialize the plugin resources. 24 | PluginResources.Init(this.Assembly); 25 | } 26 | 27 | // This method is called when the plugin is loaded. 28 | public override void Load() 29 | { 30 | } 31 | 32 | // This method is called when the plugin is unloaded. 33 | public override void Unload() 34 | { 35 | try 36 | { 37 | RemoteWrapper.Logout(); 38 | } 39 | catch 40 | { 41 | // ignored 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/VoiceMeeterPlugin.csproj: -------------------------------------------------------------------------------- 1 | <Project Sdk="Microsoft.NET.Sdk"> 2 | 3 | <PropertyGroup> 4 | <TargetFramework>net8.0-windows</TargetFramework> 5 | <ImplicitUsings>enable</ImplicitUsings> 6 | <Nullable>disable</Nullable> 7 | <RootNamespace>Loupedeck.VoiceMeeterPlugin</RootNamespace> 8 | 9 | <ErrorReport>prompt</ErrorReport> 10 | <WarningLevel>4</WarningLevel> 11 | 12 | <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> 13 | <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> 14 | 15 | <PluginApiDir Condition="$(OS) == 'Windows_NT'">C:\Program Files\Logi\LogiPluginService\</PluginApiDir> 16 | <PluginApiDir Condition="$(OS) != 'Windows_NT'">/Applications/Utilities/LogiPluginService.app/Contents/MonoBundle/</PluginApiDir> 17 | 18 | <PluginDir Condition="$(OS) == 'Windows_NT'">$(LocalAppData)\Logi\LogiPluginService\Plugins\</PluginDir> 19 | <PluginDir Condition="$(OS) != 'Windows_NT'">~/Library/Application\ Support/Logi/LogiPluginService/Plugins/</PluginDir> 20 | <Company>XeroxDev</Company> 21 | <!-- x-release-please-start-version --> 22 | <AssemblyVersion>4.1.0</AssemblyVersion> 23 | <FileVersion>4.1.0</FileVersion> 24 | <Version>4.1.0</Version> 25 | <PackageVersion>4.1.0</PackageVersion> 26 | <!-- x-release-please-end --> 27 | </PropertyGroup> 28 | 29 | <ItemGroup> 30 | <Reference Include="PluginApi"> 31 | <HintPath>$(PluginApiDir)PluginApi.dll</HintPath> 32 | </Reference> 33 | <Reference Include="SkiaSharp"> 34 | <HintPath >$(PluginApiDir)SkiaSharp.dll</HintPath> 35 | </Reference> 36 | </ItemGroup> 37 | 38 | <ItemGroup> 39 | <Folder Include="Resources\" /> 40 | </ItemGroup> 41 | 42 | <ItemGroup> 43 | <PackageReference Include="System.Reactive" Version="6.0.1" /> 44 | </ItemGroup> 45 | 46 | <Target Name="PostBuild" AfterTargets="PostBuildEvent"> 47 | <Exec Condition="$(OS) == 'Windows_NT'" Command="echo $(OutputPath) > "$(PluginDir)$(ProjectName).link"" /> 48 | <Exec Condition="$(OS) != 'Windows_NT'" Command="echo $(OutputPath) > $(PluginDir)$(ProjectName).link" /> 49 | </Target> 50 | 51 | <Target Name="CopyMetadata" AfterTargets="PostBuildEvent"> 52 | <ItemGroup> 53 | <MetadataFiles Include="metadata\*.*" /> 54 | </ItemGroup> 55 | <Copy SourceFiles="@(MetadataFiles)" DestinationFolder="$(OutputPath)metadata" /> 56 | </Target> 57 | 58 | <Target Name="PluginClean" AfterTargets="CoreClean"> 59 | <Delete Condition="$(OS) == 'Windows_NT'" Files="$(PluginDir)$(ProjectName).link" /> 60 | <Exec Condition="$(OS) != 'Windows_NT'" Command="rm -f $(PluginDir)$(ProjectName).link" /> 61 | 62 | <RemoveDir Directories="$(OutputPath)metadata" /> 63 | </Target> 64 | 65 | </Project> 66 | -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/metadata/Icon256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/82c13aaefcf2cd3d3ea6ea162931536c9320f3d9/src/VoiceMeeterPlugin/metadata/Icon256x256.png -------------------------------------------------------------------------------- /src/VoiceMeeterPlugin/metadata/LoupedeckPackage.yaml: -------------------------------------------------------------------------------- 1 | type: plugin4 2 | name: VoiceMeeter 3 | displayName: VoiceMeeter 4 | description: Control your VoiceMeeter audio mixer with your Loupedeck device. 5 | pluginFileName: VoiceMeeterPlugin.dll 6 | version: 4.1.0 7 | author: XeroxDev 8 | copyright: Copyright © 2022-2024 Dominic Ris. All rights reserved. 9 | pluginFolderWin: . 10 | supportedDevices: 11 | - LoupedeckCtFamily 12 | pluginCapabilities: 13 | - HasApplication 14 | minimumLoupedeckVersion: 6.0.0 15 | license: MIT 16 | licenseUrl: https://opensource.org/licenses/MIT 17 | supportPageUrl: https://s.tswi.me/discord 18 | homePageUrl: https://help.xeroxdev.de/en/loupedeck/voicemeeter/home 19 | --------------------------------------------------------------------------------