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/
test*
test/
test-*

182
README.md
View File

@ -1,127 +1,189 @@
# 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
- Banana 2.1.1.9
- Potato 3.1.1.9
> A command-line interface for controlling Voicemeeter
## `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
.\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.
- `v`: Print the version of vmrcli.
- `i`: Enable interactive mode, use (-I) to disable the '>>' prompt.
- If set, any api commands passed on the command line will be ignored.
- `f`: Do not split input on spaces.
- `k`: The kind of Voicemeeter (basic, banana or potato). Use this to launch the GUI.
- `l`: Set log level, must be one of TRACE, DEBUG, INFO, WARN, ERROR, or FATAL
- `e`: Enable extra console output (toggle, set messages)
- `c`: Load a user configuration (give the full file path)
- `m`: Launch the MacroButtons application
- `s`: Launch the StreamerView application
| Option | Description | Example |
|--------|-------------|----------|
| `-h` | Print help message | `vmrcli.exe -h` |
| `-v` | Show version information | `vmrcli.exe -v` |
| `-i` | Enable interactive mode | `vmrcli.exe -i` |
| `-I` | Interactive mode without prompt | `vmrcli.exe -I` |
| `-f` | Don't split input on spaces | `vmrcli.exe -f` |
| `-k <type>` | Launch Voicemeeter GUI | `-kbasic`, `-kbanana`, `-kpotato` |
| `-l <level>` | Set log level | `-lDEBUG`, `-lINFO`, `-lWARN` |
| `-e` | Enable extra console output | `vmrcli.exe -e` |
| `-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`
- Commands starting with `!` will be toggled, use it with boolean parameters.
- Commands containing `=` will set a value. (use `+=` and `-=` to increment/decrement)
- All other commands with get a value.
### Command Types
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
.\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
.\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`
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.
#### **Device Configuration**
*Configure hardware devices with complex names*
```powershell
.\vmrcli.exe -lDEBUG -f bus[1].device.wdm='"Realtek Digital Output (Realtek(R) Audio)"'
.\vmrcli.exe -lDEBUG -f strip[0].label='"My Podmic"'
.\vmrcli.exe -lDEBUG bus[2].mute=1 bus[2].mute 'bus[2].device.wdm="Realtek Digital Output (Realtek(R) Audio)"'
```
#### `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
- `unlock`: command.lock=0
- `show`: command.show=1
- `hide`: command.show=0
- `restart`: command.restart=1
### Quick Commands
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
.\vmrcli.exe -i
```
Will open an interactive prompt:
**Interactive prompt:**
```powershell
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
.\vmrcli.exe -lDEBUG $(Get-Content .\example_commands.txt)
```
You may also pipe a scripts contents to the CLI:
**Via pipeline:**
```powershell
$(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
[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+).\""
{{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/interface.c -f src/util.c -f src/vmrcli.c -f src/wrapper.c -p \"@version (\d+\.\d+\.\d+)\""
{{end}}

View File

@ -1,5 +1,5 @@
# 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].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);
bool is_comment(char *s);
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__ */

View File

@ -3,7 +3,7 @@
* @author Vincent Burel, Onyx and Iris (code@onyxandiris.online)
* @brief Functions for initializing the iVMR 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
*
* @copyright Vincent Burel(c)2015-2021 All Rights Reserved

View File

@ -2,7 +2,7 @@
* @file util.c
* @author Onyx and Iris (code@onyxandiris.online)
* @brief Utility functions.
* @version 0.11.0
* @version 0.13.0
* @date 2024-07-06
*
* @copyright Copyright (c) 2024
@ -125,3 +125,67 @@ struct quickcommand *command_in_quickcommands(const char *command_key, const str
}
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
* @author Onyx and Iris (code@onyxandiris.online)
* @brief A Voicemeeter Remote Command Line Interface
* @version 0.11.0
* @version 0.13.0
* @date 2024-07-06
*
* @copyright Copyright (c) 2024
@ -11,6 +11,8 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <getopt.h>
#include <windows.h>
#include "interface.h"
@ -35,7 +37,7 @@
#define RES_SZ 512 /* Size of the buffer passed to VBVMR_GetParameterStringW */
#define COUNT_OF(x) (sizeof(x) / sizeof(x[0]))
#define DELIMITERS " \t;,"
#define VERSION "0.12.0"
#define VERSION "0.13.0"
/**
* @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
* Walks through each line split by " \t;," delimiters.
* Each token is passed to parse_command()
*
* @brief Parse each input line into separate commands and execute them.
* Commands are split based on the delimiters argument, but quoted strings are preserved as single commands.
* 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 input Each input line, from stdin or CLI args
* @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))
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 (token != NULL)
while (*current != '\0')
{
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);
token = strtok_r(NULL, delimiters, &p);
}
}
@ -341,8 +408,7 @@ static void parse_command(PT_VMR vmr, char *command)
if (qc_ptr != NULL)
{
set_parameters(vmr, qc_ptr->fullcommand);
if (eflag)
{
if (eflag) {
printf("Setting %s\n", qc_ptr->fullcommand);
}
return;
@ -359,8 +425,7 @@ static void parse_command(PT_VMR vmr, char *command)
if (res.val.f == 1 || res.val.f == 0)
{
set_parameter_float(vmr, command, 1 - res.val.f);
if (eflag)
{
if (eflag) {
printf("Toggling %s\n", command);
}
}
@ -372,10 +437,18 @@ static void parse_command(PT_VMR vmr, char *command)
if (strchr(command, '=') != NULL) /* set */
{
set_parameters(vmr, command);
if (eflag)
char quoted_command[MAX_LINE];
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 */

View File

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