Compare commits

..

No commits in common. "9bb7b32f7bc6d6d79a0c06ac13f5a9319fd79960" and "4e5801541101df6c05d7ce5d9d4c4f80c1ec61ab" have entirely different histories.

10 changed files with 82 additions and 387 deletions

View File

@ -1,102 +0,0 @@
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,6 +56,4 @@ dkms.conf
.vscode/
test/
test-*
test*

182
README.md
View File

@ -1,189 +1,127 @@
# Voicemeeter Remote Command Line Utility
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Platform](https://img.shields.io/badge/platform-Windows-blue)](#requirements)
## `Tested against`
> A command-line interface for controlling Voicemeeter
- Basic 1.1.1.9
- Banana 2.1.1.9
- Potato 3.1.1.9
## Compatibility
## `Requirements`
| Voicemeeter Version | Status |
|-------------------|--------|
| Basic 1.1.2.2 | ✅ Tested |
| Banana 2.1.2.2 | ✅ Tested |
| Potato 3.1.2.2 | ✅ Tested |
- [Voicemeeter](https://voicemeeter.com/)
## Requirements
- **[Voicemeeter](https://voicemeeter.com/)** - Any version (Basic, Banana, or Potato)
- **Windows** operating system
- **Command line environment** (PowerShell, CMD, or Terminal)
## Usage
## `Use`
```powershell
.\vmrcli.exe [OPTIONS] <api_commands>
.\vmrcli.exe [-h] [-v] [-i|-I] [-f] [-k] [-l] [-e] [-c] [-m] [-s] <api commands>
```
### Command Line Options
Where:
| 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.
- `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
## `API Commands`
### Command Types
- 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.
| 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` |
Examples:
> **Tip:** Use quotes around values containing spaces: `'strip[0].label="my device"'`
---
### Examples
#### **Basic Operations**
*Toggle mute, get values, and adjust gain*
Launch basic GUI, set log level to INFO, Toggle Strip 0 Mute, print its new value, then decrease Bus 0 Gain by 3.8
```powershell
.\vmrcli.exe -kbasic -lINFO !strip[0].mute strip[0].mute bus[0].gain-=3.8
```
#### **Setting Labels with Spaces**
*Set labels and print them back*
Launch banana GUI, set log level to DEBUG, set Strip 0 label to podmic then print Strip 2 label
```powershell
.\vmrcli.exe -kbanana -lDEBUG 'strip[0].label="my podmic"' strip[0].label
.\vmrcli.exe -kbanana -lDEBUG strip[0].label=podmic strip[2].label
```
#### **Device Configuration**
*Configure hardware devices with complex names*
#### `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.
```powershell
.\vmrcli.exe -lDEBUG bus[2].mute=1 bus[2].mute 'bus[2].device.wdm="Realtek Digital Output (Realtek(R) Audio)"'
.\vmrcli.exe -lDEBUG -f bus[1].device.wdm='"Realtek Digital Output (Realtek(R) Audio)"'
.\vmrcli.exe -lDEBUG -f strip[0].label='"My Podmic"'
```
#### **Batch Operations**
*Multiple strip configurations in one command*
#### `Quick Commands`
```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
```
A short list of quick commands are available:
### Quick Commands
- `lock`: command.lock=1
- `unlock`: command.lock=0
- `show`: command.show=1
- `hide`: command.show=0
- `restart`: command.restart=1
*Convenient shortcuts for common Voicemeeter operations*
They may be used in direct or 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 |
## `Interactive Mode`
> **Available in both direct and interactive modes**
Running the following command in Powershell:
## Interactive Mode
*Real-time command interface for live audio control*
**Start interactive session:**
```powershell
.\vmrcli.exe -i
```
**Interactive prompt:**
Will open an interactive prompt:
```powershell
Interactive mode enabled. Enter 'Q' to exit.
>>
```
> **Important:** Command line API arguments are ignored when using `-i`
API commands follow the same rules as listed above. Entering `Q` or `q` will exit the program.
## Script Files
## `Script files`
*Automate complex audio setups with script files*
Scripts can be loaded from text files, for example in Powershell:
### Loading Scripts
**From file content:**
```powershell
.\vmrcli.exe -lDEBUG $(Get-Content .\example_commands.txt)
```
**Via pipeline:**
You may also pipe a scripts contents to the CLI:
```powershell
$(Get-Content .\example_commands.txt) | .\vmrcli.exe -lDEBUG -I
```
### Script Format Rules
Multiple API commands can be in a single line, they may be separated by space, `;` or `,`.
| 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` |
Lines starting with `#` will be interpreted as comments.
## Build Instructions
## `Build`
*Compile from source using GNU Make*
Run the included `makefile` with [GNU Make](https://www.gnu.org/software/make/).
### Prerequisites
- [GNU Make](https://www.gnu.org/software/make/)
- GCC compiler (recommended)
- Windows development environment
The binary in [Releases][releases] is compiled with coloured logging enabled. To disable this you can override the `LOG_USE_COLOR` variable, for example:
### Build Commands
`make LOG_USE_COLOR=no`
```bash
# Standard build
make
## `Official Documentation`
# Disable colored logging
make LOG_USE_COLOR=no
- [Voicemeeter Remote C API][remoteapi-docs]
# Clean build artifacts
make clean
```
## `Special Thanks`
> **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.
- [rxi][rxi-user] for writing the [log.c][log-c] package
[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,5 +81,4 @@ 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="my podmic" strip[0].label
strip[0].mute !strip[0].mute strip[0].mute strip[0].gain strip[0].label=podmic strip[0].label
# Strip 1
strip[1].mute=1 strip[1].mute strip[1].limit-=8

View File

@ -20,6 +20,5 @@ 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.13.0
* @version 0.11.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.13.0
* @version 0.11.0
* @date 2024-07-06
*
* @copyright Copyright (c) 2024
@ -125,67 +125,3 @@ 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.13.0
* @version 0.11.0
* @date 2024-07-06
*
* @copyright Copyright (c) 2024
@ -11,8 +11,6 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <getopt.h>
#include <windows.h>
#include "interface.h"
@ -37,7 +35,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.13.0"
#define VERSION "0.12.0"
/**
* @enum The kind of values a get call may return.
@ -296,35 +294,11 @@ 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 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
* @brief Returns early if input is a comment
* Walks through each line split by " \t;," delimiters.
* Each token is passed to parse_command()
*
* @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
@ -334,54 +308,13 @@ static void parse_input(PT_VMR vmr, char *input, char *delimiters)
if (is_comment(input))
return;
char *current = input;
char token[MAX_LINE];
size_t token_length = 0;
bool inside_quotes = false;
char quote_char = '\0';
char *token, *p;
while (*current != '\0')
token = strtok_r(input, delimiters, &p);
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);
token = strtok_r(NULL, delimiters, &p);
}
}
@ -408,7 +341,8 @@ 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;
@ -425,7 +359,8 @@ 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);
}
}
@ -437,18 +372,10 @@ static void parse_command(PT_VMR vmr, char *command)
if (strchr(command, '=') != NULL) /* set */
{
char quoted_command[MAX_LINE];
if (add_quotes_if_needed(command, quoted_command, MAX_LINE))
set_parameters(vmr, command);
if (eflag)
{
set_parameters(vmr, quoted_command);
if (eflag) {
printf("Setting %s\n", command);
}
}
else
{
log_error("Command too long after adding quotes");
printf("Setting %s\n", command);
}
}
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.13.0
* @version 0.11.0
* @date 2024-07-06
*
* @copyright Copyright (c) 2024