Compare commits

..

8 Commits

10 changed files with 387 additions and 82 deletions

102
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,102 @@
name: Build vmrcli
on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:
jobs:
build-windows:
runs-on: windows-latest
permissions:
contents: write # Required to create releases
actions: read # Required to download artifacts
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup MSYS2
uses: msys2/setup-msys2@v2
with:
msystem: MINGW64
update: true
install: >-
mingw-w64-x86_64-gcc
mingw-w64-x86_64-make
make
- name: Add MSYS2 to PATH
run: |
echo "${{ runner.temp }}\msys64\mingw64\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
echo "${{ runner.temp }}\msys64\usr\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
shell: pwsh
- name: Verify GCC installation
run: |
gcc --version
make --version
shell: pwsh
- name: Clean previous builds
run: make clean
shell: pwsh
continue-on-error: true
- name: Build vmrcli
run: make all
shell: pwsh
env:
LOG_USE_COLOR: yes
- name: Verify build output
run: |
if (Test-Path "bin/vmrcli.exe") {
Write-Host "✅ Build successful - vmrcli.exe created"
Get-Item "bin/vmrcli.exe" | Format-List Name, Length, LastWriteTime
} else {
Write-Host "❌ Build failed - vmrcli.exe not found"
exit 1
}
shell: pwsh
- name: Test executable
run: |
if (Test-Path "bin/vmrcli.exe") {
Write-Host "Testing vmrcli.exe..."
& ".\bin\vmrcli.exe" -h
}
shell: pwsh
continue-on-error: true
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: vmrcli-windows
path: |
bin/vmrcli.exe
retention-days: 30
- name: Upload build logs (on failure)
if: failure()
uses: actions/upload-artifact@v4
with:
name: build-logs
path: |
obj/
*.log
retention-days: 7
- name: Create GitHub Release
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v1
with:
files: |
bin/vmrcli.exe
tag_name: ${{ github.ref_name }}
name: vmrcli ${{ github.ref_name }}
draft: false
prerelease: false
generate_release_notes: true
token: ${{ secrets.GITHUB_TOKEN }}

4
.gitignore vendored
View File

@ -56,4 +56,6 @@ dkms.conf
.vscode/ .vscode/
test* test/
test-*

182
README.md
View File

@ -1,127 +1,189 @@
# Voicemeeter Remote Command Line Utility # Voicemeeter Remote Command Line Utility
## `Tested against` [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Platform](https://img.shields.io/badge/platform-Windows-blue)](#requirements)
- Basic 1.1.1.9 > A command-line interface for controlling Voicemeeter
- Banana 2.1.1.9
- Potato 3.1.1.9
## `Requirements` ## Compatibility
- [Voicemeeter](https://voicemeeter.com/) | Voicemeeter Version | Status |
|-------------------|--------|
| Basic 1.1.2.2 | ✅ Tested |
| Banana 2.1.2.2 | ✅ Tested |
| Potato 3.1.2.2 | ✅ Tested |
## `Use` ## Requirements
- **[Voicemeeter](https://voicemeeter.com/)** - Any version (Basic, Banana, or Potato)
- **Windows** operating system
- **Command line environment** (PowerShell, CMD, or Terminal)
## Usage
```powershell ```powershell
.\vmrcli.exe [-h] [-v] [-i|-I] [-f] [-k] [-l] [-e] [-c] [-m] [-s] <api commands> .\vmrcli.exe [OPTIONS] <api_commands>
``` ```
Where: ### Command Line Options
- `h`: Print the help message. | Option | Description | Example |
- `v`: Print the version of vmrcli. |--------|-------------|----------|
- `i`: Enable interactive mode, use (-I) to disable the '>>' prompt. | `-h` | Print help message | `vmrcli.exe -h` |
- If set, any api commands passed on the command line will be ignored. | `-v` | Show version information | `vmrcli.exe -v` |
- `f`: Do not split input on spaces. | `-i` | Enable interactive mode | `vmrcli.exe -i` |
- `k`: The kind of Voicemeeter (basic, banana or potato). Use this to launch the GUI. | `-I` | Interactive mode without prompt | `vmrcli.exe -I` |
- `l`: Set log level, must be one of TRACE, DEBUG, INFO, WARN, ERROR, or FATAL | `-f` | Don't split input on spaces | `vmrcli.exe -f` |
- `e`: Enable extra console output (toggle, set messages) | `-k <type>` | Launch Voicemeeter GUI | `-kbasic`, `-kbanana`, `-kpotato` |
- `c`: Load a user configuration (give the full file path) | `-l <level>` | Set log level | `-lDEBUG`, `-lINFO`, `-lWARN` |
- `m`: Launch the MacroButtons application | `-e` | Enable extra console output | `vmrcli.exe -e` |
- `s`: Launch the StreamerView application | `-c <path>` | Load user configuration | `-c "C:\config.txt"` |
| `-m` | Launch MacroButtons app | `vmrcli.exe -m` |
| `-s` | Launch StreamerView app | `vmrcli.exe -s` |
> **Note:** When using interactive mode (`-i`), command line API commands are ignored.
## `API Commands` ## `API Commands`
- Commands starting with `!` will be toggled, use it with boolean parameters. ### Command Types
- Commands containing `=` will set a value. (use `+=` and `-=` to increment/decrement)
- All other commands with get a value.
Examples: | Syntax | Action | Example |
|--------|---------|---------|
| `!command` | **Toggle** boolean values | `!strip[0].mute` |
| `command=value` | **Set** a parameter | `strip[0].gain=2.5` |
| `command+=value` | **Increment** a parameter | `bus[0].gain+=1.2` |
| `command-=value` | **Decrement** a parameter | `bus[0].gain-=3.8` |
| `command` | **Get** current value | `strip[0].label` |
Launch basic GUI, set log level to INFO, Toggle Strip 0 Mute, print its new value, then decrease Bus 0 Gain by 3.8 > **Tip:** Use quotes around values containing spaces: `'strip[0].label="my device"'`
---
### Examples
#### **Basic Operations**
*Toggle mute, get values, and adjust gain*
```powershell ```powershell
.\vmrcli.exe -kbasic -lINFO !strip[0].mute strip[0].mute bus[0].gain-=3.8 .\vmrcli.exe -kbasic -lINFO !strip[0].mute strip[0].mute bus[0].gain-=3.8
``` ```
Launch banana GUI, set log level to DEBUG, set Strip 0 label to podmic then print Strip 2 label #### **Setting Labels with Spaces**
*Set labels and print them back*
```powershell ```powershell
.\vmrcli.exe -kbanana -lDEBUG strip[0].label=podmic strip[2].label .\vmrcli.exe -kbanana -lDEBUG 'strip[0].label="my podmic"' strip[0].label
``` ```
#### `String Commands With Spaces` #### **Device Configuration**
*Configure hardware devices with complex names*
It may be desirable to send a string request containing spaces, for example to change an output device. By default the CLI splits such strings, to avoid this pass the `-f` flag. It's probably best to use this with single commands only due to its effect on how the CLI parses strings. Also note the inclusion of the double quotation marks, it seems the C API requires them.
```powershell ```powershell
.\vmrcli.exe -lDEBUG -f bus[1].device.wdm='"Realtek Digital Output (Realtek(R) Audio)"' .\vmrcli.exe -lDEBUG bus[2].mute=1 bus[2].mute 'bus[2].device.wdm="Realtek Digital Output (Realtek(R) Audio)"'
.\vmrcli.exe -lDEBUG -f strip[0].label='"My Podmic"'
``` ```
#### `Quick Commands` #### **Batch Operations**
*Multiple strip configurations in one command*
A short list of quick commands are available: ```powershell
.\vmrcli.exe `
'strip[0].label="my podmic"' strip[0].label !strip[0].mute `
'strip[1].label="my wavemic"' strip[1].label !strip[1].mute
```
- `lock`: command.lock=1 ### Quick Commands
- `unlock`: command.lock=0
- `show`: command.show=1
- `hide`: command.show=0
- `restart`: command.restart=1
They may be used in direct or interactive mode. *Convenient shortcuts for common Voicemeeter operations*
## `Interactive Mode` | Command | API Equivalent | Description |
|---------|----------------|-------------|
| `lock` | `command.lock=1` | 🔒 Lock Voicemeeter parameters |
| `unlock` | `command.lock=0` | 🔓 Unlock Voicemeeter parameters |
| `show` | `command.show=1` | 👁️ Show Voicemeeter interface |
| `hide` | `command.show=0` | 🙈 Hide Voicemeeter interface |
| `restart` | `command.restart=1` | 🔄 Restart Voicemeeter engine |
Running the following command in Powershell: > **Available in both direct and interactive modes**
## Interactive Mode
*Real-time command interface for live audio control*
**Start interactive session:**
```powershell ```powershell
.\vmrcli.exe -i .\vmrcli.exe -i
``` ```
Will open an interactive prompt: **Interactive prompt:**
```powershell ```powershell
Interactive mode enabled. Enter 'Q' to exit. Interactive mode enabled. Enter 'Q' to exit.
>> >>
``` ```
API commands follow the same rules as listed above. Entering `Q` or `q` will exit the program. > **Important:** Command line API arguments are ignored when using `-i`
## `Script files` ## Script Files
Scripts can be loaded from text files, for example in Powershell: *Automate complex audio setups with script files*
### Loading Scripts
**From file content:**
```powershell ```powershell
.\vmrcli.exe -lDEBUG $(Get-Content .\example_commands.txt) .\vmrcli.exe -lDEBUG $(Get-Content .\example_commands.txt)
``` ```
You may also pipe a scripts contents to the CLI: **Via pipeline:**
```powershell ```powershell
$(Get-Content .\example_commands.txt) | .\vmrcli.exe -lDEBUG -I $(Get-Content .\example_commands.txt) | .\vmrcli.exe -lDEBUG -I
``` ```
Multiple API commands can be in a single line, they may be separated by space, `;` or `,`. ### Script Format Rules
Lines starting with `#` will be interpreted as comments. | Feature | Syntax | Example |
|---------|--------|----------|
| **Multiple commands per line** | Space, `;`, or `,` separated | `strip[0].mute=1;bus[0].gain+=2` |
| **Comments** | Lines starting with `#` | `# This is a comment` |
## `Build` ## Build Instructions
Run the included `makefile` with [GNU Make](https://www.gnu.org/software/make/). *Compile from source using GNU Make*
The binary in [Releases][releases] is compiled with coloured logging enabled. To disable this you can override the `LOG_USE_COLOR` variable, for example: ### Prerequisites
- [GNU Make](https://www.gnu.org/software/make/)
- GCC compiler (recommended)
- Windows development environment
`make LOG_USE_COLOR=no` ### Build Commands
## `Official Documentation` ```bash
# Standard build
make
- [Voicemeeter Remote C API][remoteapi-docs] # Disable colored logging
make LOG_USE_COLOR=no
## `Special Thanks` # Clean build artifacts
make clean
```
- [rxi][rxi-user] for writing the [log.c][log-c] package > **Pre-built binaries** are available in [Releases][releases] with coloured logging enabled
---
## Resources
### Official Documentation
- [Voicemeeter Remote C API][remoteapi-docs] - Complete API reference
### Acknowledgments
- **[rxi][rxi-user]** - Creator of the excellent [log.c][log-c] logging library
---
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
[releases]: https://github.com/onyx-and-iris/vmrcli/releases [releases]: https://github.com/onyx-and-iris/vmrcli/releases
[remoteapi-docs]: https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/main/VoicemeeterRemoteAPI.pdf [remoteapi-docs]: https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/main/VoicemeeterRemoteAPI.pdf

View File

@ -81,4 +81,5 @@ tasks:
pwsh -c "bump show -f src/vmrcli.c -p \"#define VERSION .(\d+\.\d+\.\d+).\"" pwsh -c "bump show -f src/vmrcli.c -p \"#define VERSION .(\d+\.\d+\.\d+).\""
{{else}} {{else}}
pwsh -c "bump {{.CLI_ARGS}} -w -f src/vmrcli.c -p \"#define VERSION .(\d+\.\d+\.\d+).\"" pwsh -c "bump {{.CLI_ARGS}} -w -f src/vmrcli.c -p \"#define VERSION .(\d+\.\d+\.\d+).\""
pwsh -c "bump {{.CLI_ARGS}} -w -f src/interface.c -f src/util.c -f src/vmrcli.c -f src/wrapper.c -p \"@version (\d+\.\d+\.\d+)\""
{{end}} {{end}}

View File

@ -1,5 +1,5 @@
# Strip 0 # Strip 0
strip[0].mute !strip[0].mute strip[0].mute strip[0].gain strip[0].label=podmic strip[0].label strip[0].mute !strip[0].mute strip[0].mute strip[0].gain strip[0].label="my podmic" strip[0].label
# Strip 1 # Strip 1
strip[1].mute=1 strip[1].mute strip[1].limit-=8 strip[1].mute=1 strip[1].mute strip[1].limit-=8

View File

@ -20,5 +20,6 @@ char *kind_as_string(char *s, int kind, int n);
char *version_as_string(char *s, long v, int n); char *version_as_string(char *s, long v, int n);
bool is_comment(char *s); bool is_comment(char *s);
struct quickcommand *command_in_quickcommands(const char *command, const struct quickcommand *quickcommands, int n); struct quickcommand *command_in_quickcommands(const char *command, const struct quickcommand *quickcommands, int n);
bool add_quotes_if_needed(const char *command, char *output, size_t max_len);
#endif /* __UTIL_H__ */ #endif /* __UTIL_H__ */

View File

@ -3,7 +3,7 @@
* @author Vincent Burel, Onyx and Iris (code@onyxandiris.online) * @author Vincent Burel, Onyx and Iris (code@onyxandiris.online)
* @brief Functions for initializing the iVMR interface. * @brief Functions for initializing the iVMR interface.
* Defines a single public function that returns a pointer to the interface. * Defines a single public function that returns a pointer to the interface.
* @version 0.11.0 * @version 0.13.0
* @date 2024-07-06 * @date 2024-07-06
* *
* @copyright Vincent Burel(c)2015-2021 All Rights Reserved * @copyright Vincent Burel(c)2015-2021 All Rights Reserved

View File

@ -2,7 +2,7 @@
* @file util.c * @file util.c
* @author Onyx and Iris (code@onyxandiris.online) * @author Onyx and Iris (code@onyxandiris.online)
* @brief Utility functions. * @brief Utility functions.
* @version 0.11.0 * @version 0.13.0
* @date 2024-07-06 * @date 2024-07-06
* *
* @copyright Copyright (c) 2024 * @copyright Copyright (c) 2024
@ -125,3 +125,67 @@ struct quickcommand *command_in_quickcommands(const char *command_key, const str
} }
return NULL; return NULL;
} }
/**
* @brief Adds quotes around the value part of a command if it contains spaces or tabs
*
* @param command The input command string (parameter=value format)
* @param output Buffer to store the result
* @param max_len Maximum length of the output buffer
* @return true if quotes were added or command was copied successfully
* @return false if the command is too long or invalid
*/
bool add_quotes_if_needed(const char *command, char *output, size_t max_len)
{
const char *equals_pos = strchr(command, '=');
// No '=' found, copy command as-is
if (equals_pos == NULL) {
if (strlen(command) >= max_len)
return false;
strcpy(output, command);
return true;
}
const char *value = equals_pos + 1;
// Value doesn't contain spaces or tabs, copy command as-is
if (strchr(value, ' ') == NULL && strchr(value, '\t') == NULL) {
if (strlen(command) >= max_len)
return false;
strcpy(output, command);
return true;
}
// Value needs quotes - calculate required buffer size
size_t param_len = equals_pos - command;
size_t value_len = strlen(value);
size_t quotes_len = 2;
size_t required_len = param_len + 1 + quotes_len + value_len + 1; // param + '=' + '"' + value + '"' + '\0'
if (required_len > max_len)
return false;
/**
* Construct the output string in the format: parameter="value"
* - Copy the parameter part (up to the '=')
* - Append '=' and opening quote
* - Append the value
* - Append closing quote and null terminator
*/
char *pos = output;
strncpy(pos, command, param_len);
pos += param_len;
*pos++ = '=';
*pos++ = '"';
strcpy(pos, value);
pos += value_len;
*pos++ = '"';
*pos = '\0';
return true;
}

View File

@ -2,7 +2,7 @@
* @file vmrcli.c * @file vmrcli.c
* @author Onyx and Iris (code@onyxandiris.online) * @author Onyx and Iris (code@onyxandiris.online)
* @brief A Voicemeeter Remote Command Line Interface * @brief A Voicemeeter Remote Command Line Interface
* @version 0.11.0 * @version 0.13.0
* @date 2024-07-06 * @date 2024-07-06
* *
* @copyright Copyright (c) 2024 * @copyright Copyright (c) 2024
@ -11,6 +11,8 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <getopt.h> #include <getopt.h>
#include <windows.h> #include <windows.h>
#include "interface.h" #include "interface.h"
@ -35,7 +37,7 @@
#define RES_SZ 512 /* Size of the buffer passed to VBVMR_GetParameterStringW */ #define RES_SZ 512 /* Size of the buffer passed to VBVMR_GetParameterStringW */
#define COUNT_OF(x) (sizeof(x) / sizeof(x[0])) #define COUNT_OF(x) (sizeof(x) / sizeof(x[0]))
#define DELIMITERS " \t;," #define DELIMITERS " \t;,"
#define VERSION "0.12.0" #define VERSION "0.13.0"
/** /**
* @enum The kind of values a get call may return. * @enum The kind of values a get call may return.
@ -294,11 +296,35 @@ static void interactive(PT_VMR vmr, bool with_prompt, char *delimiters)
} }
} }
/* Helper functions for parse_input */
static inline bool is_quote_char(char c) {
return (c == '"' || c == '\'');
}
static inline bool is_delimiter_char(char c, const char *delimiters) {
return strchr(delimiters, c) != NULL;
}
static char* skip_consecutive_delimiters(char *p, const char *delimiters) {
while (*p != '\0' && is_delimiter_char(*p, delimiters)) {
p++;
}
return p;
}
static bool add_char_to_token(char *token, size_t *token_len, char c, size_t max_len) {
if (*token_len < max_len - 1) {
token[(*token_len)++] = c;
return true;
}
return false; // Buffer would overflow
}
/** /**
* @brief Returns early if input is a comment * @brief Parse each input line into separate commands and execute them.
* Walks through each line split by " \t;," delimiters. * Commands are split based on the delimiters argument, but quoted strings are preserved as single commands.
* Each token is passed to parse_command() * See the test cases for examples of how input lines are parsed:
* * https://github.com/onyx-and-iris/vmrcli?tab=readme-ov-file#api-commands
* @param vmr Pointer to the iVMR interface * @param vmr Pointer to the iVMR interface
* @param input Each input line, from stdin or CLI args * @param input Each input line, from stdin or CLI args
* @param delimiters A string of delimiter characters to split each input line * @param delimiters A string of delimiter characters to split each input line
@ -308,13 +334,54 @@ static void parse_input(PT_VMR vmr, char *input, char *delimiters)
if (is_comment(input)) if (is_comment(input))
return; return;
char *token, *p; char *current = input;
char token[MAX_LINE];
size_t token_length = 0;
bool inside_quotes = false;
char quote_char = '\0';
token = strtok_r(input, delimiters, &p); while (*current != '\0')
while (token != NULL)
{ {
if (!inside_quotes && is_quote_char(*current))
{
inside_quotes = true;
quote_char = *current;
current++;
log_trace("Entering quotes with char '%c'", quote_char);
continue;
}
else if (inside_quotes && *current == quote_char)
{
inside_quotes = false;
quote_char = '\0';
current++;
log_trace("Exiting quotes");
continue;
}
else if (!inside_quotes && is_delimiter_char(*current, delimiters))
{
if (token_length > 0)
{
token[token_length] = '\0';
parse_command(vmr, token);
token_length = 0;
}
current = skip_consecutive_delimiters(current, delimiters);
continue;
}
else
{
add_char_to_token(token, &token_length, *current, MAX_LINE);
log_trace("Added char '%c' to token, current token: '%s'", *current, token);
}
current++;
}
if (token_length > 0)
{
token[token_length] = '\0';
parse_command(vmr, token); parse_command(vmr, token);
token = strtok_r(NULL, delimiters, &p);
} }
} }
@ -341,8 +408,7 @@ static void parse_command(PT_VMR vmr, char *command)
if (qc_ptr != NULL) if (qc_ptr != NULL)
{ {
set_parameters(vmr, qc_ptr->fullcommand); set_parameters(vmr, qc_ptr->fullcommand);
if (eflag) if (eflag) {
{
printf("Setting %s\n", qc_ptr->fullcommand); printf("Setting %s\n", qc_ptr->fullcommand);
} }
return; return;
@ -359,8 +425,7 @@ static void parse_command(PT_VMR vmr, char *command)
if (res.val.f == 1 || res.val.f == 0) if (res.val.f == 1 || res.val.f == 0)
{ {
set_parameter_float(vmr, command, 1 - res.val.f); set_parameter_float(vmr, command, 1 - res.val.f);
if (eflag) if (eflag) {
{
printf("Toggling %s\n", command); printf("Toggling %s\n", command);
} }
} }
@ -372,10 +437,18 @@ static void parse_command(PT_VMR vmr, char *command)
if (strchr(command, '=') != NULL) /* set */ if (strchr(command, '=') != NULL) /* set */
{ {
set_parameters(vmr, command); char quoted_command[MAX_LINE];
if (eflag)
if (add_quotes_if_needed(command, quoted_command, MAX_LINE))
{ {
printf("Setting %s\n", command); set_parameters(vmr, quoted_command);
if (eflag) {
printf("Setting %s\n", command);
}
}
else
{
log_error("Command too long after adding quotes");
} }
} }
else /* get */ else /* get */

View File

@ -2,7 +2,7 @@
* @file wrapper.c * @file wrapper.c
* @author Onyx and Iris (code@onyxandiris.online) * @author Onyx and Iris (code@onyxandiris.online)
* @brief Provides public functions that wrap the iVMR calls * @brief Provides public functions that wrap the iVMR calls
* @version 0.11.0 * @version 0.13.0
* @date 2024-07-06 * @date 2024-07-06
* *
* @copyright Copyright (c) 2024 * @copyright Copyright (c) 2024