├── .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]: "
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]: "
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 | [](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/network/members)
11 | [](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/stargazers)
12 | [](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/watchers)
13 | [](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/graphs/contributors)
14 |
15 | [](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/issues)
16 | [](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/issues?q=is%3Aissue+is%3Aclosed)
17 |
18 | [](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/pulls)
19 | [](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/pulls?q=is%3Apr+is%3Aclosed)
20 | [](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/compare)
21 |
22 |
23 | [](https://github.com/XeroxDev/Loupedeck-plugin-VoiceMeeter/releases)
24 | []()
25 |
26 | [](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 |
2 |
3 | latest
4 | $(SolutionDir)..\obj\
5 | $(SolutionDir)..\bin\
6 |
7 |
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 Actions { get; } = new();
25 | private VoiceMeeterService VmService { get; }
26 | private Boolean IsMultiAction { get; set; }
27 | private String Command { get; set; }
28 | private Subject 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 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 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 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 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 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(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(
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 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 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 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 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(
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 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 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 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 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 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(
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 | ///
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 | ///
14 | public class Levels : IDisposable, IObservable
15 | {
16 | public class Channel
17 | {
18 | public LevelType LevelType { get; set; }
19 | public Int32 ChannelNumber { get; set; }
20 | };
21 |
22 | private readonly List _channels;
23 | private readonly List> _observers = [];
24 | private readonly IObservable _timer;
25 | private IDisposable _timerSubscription;
26 | private List _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(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 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> observers, IObserver 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 | ///
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 | ///
14 | public class Parameters : IDisposable, IObservable
15 | {
16 | private readonly List> _observers = [];
17 | private readonly IObservable _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 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> observers, IObserver 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 | ///
23 | /// Gets a text value
24 | ///
25 | ///
26 | ///
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 | ///
35 | /// Set a text value
36 | ///
37 | ///
38 | ///
39 | ///
40 | public static void SetTextParameter(String parameter, String value) =>
41 | TestResult(RemoteWrapper.InternalSetParameterW(parameter, value));
42 |
43 | ///
44 | /// Get a named parameter
45 | ///
46 | /// Parameter name
47 | /// float value
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 | ///
56 | /// Set a named parameter
57 | ///
58 | /// Parameter name
59 | /// float value
60 | public static void SetParameter(String parameter, Single value) =>
61 | TestResult(RemoteWrapper.SetParameter(parameter, value));
62 |
63 |
64 | ///
65 | /// Set one or several parameters by a script
66 | ///
67 | /// One or more instructions separated by comma, semicolon or newline
68 | public static void SetParameters(String parameters) => TestResult(RemoteWrapper.SetParameters(parameters));
69 |
70 | #endregion
71 |
72 | #region Commands
73 |
74 | ///
75 | /// Start the VoiceMeeter program
76 | ///
77 | ///
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 | ///
93 | /// Shutdown the VoiceMeeter program
94 | ///
95 | public static void Shutdown() => TestResult(RemoteWrapper.SetParameter(VoicemeeterCommand.Shutdown, 1));
96 |
97 | ///
98 | /// Restart the audio engine
99 | ///
100 | public static void Restart() => TestResult(RemoteWrapper.SetParameter(VoicemeeterCommand.Restart, 1));
101 |
102 | ///
103 | /// Shows the running Voicemeeter application if minimized.
104 | ///
105 | public static void Show() => TestResult(RemoteWrapper.SetParameter(VoicemeeterCommand.Show, 1));
106 |
107 | ///
108 | /// Return if the parameters have changed since the last time this method was called.
109 | ///
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 | ///
125 | /// Eject Cassette
126 | ///
127 | public static void Eject() => TestResult(RemoteWrapper.SetParameter(VoicemeeterCommand.Eject, 1));
128 |
129 | ///
130 | /// Reset all configuration
131 | ///
132 | public static void Reset() => TestResult(RemoteWrapper.SetParameter(VoicemeeterCommand.Reset, 1));
133 |
134 | ///
135 | /// Load a configuation file name
136 | ///
137 | /// Full path to file
138 | public static void Load(String configurationFileName) =>
139 | SetTextParameter(VoicemeeterCommand.Load, configurationFileName);
140 |
141 | ///
142 | /// Save a configuration to the given file name
143 | ///
144 | /// Full path to file
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 | ///
189 | /// Logs into the Voicemeeter application. Starts the given application (Voicemeeter, Bananna, Potato) if it is not already runnning.
190 | ///
191 | /// The Voicemeeter program to run
192 | ///
193 | /// IDisposable class to dispose when finished with the remote.
194 | [SupportedOSPlatform("windows")]
195 | public static async Task 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 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
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> _observers = [];
18 |
19 | public IDisposable Subscribe(IObserver 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> observers, IObserver 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 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 |
2 |
3 |
4 | net8.0-windows
5 | enable
6 | disable
7 | Loupedeck.VoiceMeeterPlugin
8 |
9 | prompt
10 | 4
11 |
12 | false
13 | true
14 |
15 | C:\Program Files\Logi\LogiPluginService\
16 | /Applications/Utilities/LogiPluginService.app/Contents/MonoBundle/
17 |
18 | $(LocalAppData)\Logi\LogiPluginService\Plugins\
19 | ~/Library/Application\ Support/Logi/LogiPluginService/Plugins/
20 | XeroxDev
21 |
22 | 4.1.0
23 | 4.1.0
24 | 4.1.0
25 | 4.1.0
26 |
27 |
28 |
29 |
30 |
31 | $(PluginApiDir)PluginApi.dll
32 |
33 |
34 | $(PluginApiDir)SkiaSharp.dll
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
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 |
--------------------------------------------------------------------------------