5 Commits

Author SHA1 Message Date
9b7acf5bb6 add context struct, use it to pass around config + vmr object.
add length bound formatting to the token trace logging.

patch bump
2026-04-08 21:25:37 +01:00
99756a7928 add support for long options
update README

minor bump
2026-04-08 03:17:21 +01:00
3823e0c497 include pattern in output 2026-03-06 01:48:55 +00:00
9bb7b32f7b upd example_commands 2026-03-04 19:13:17 +00:00
23146cf1a0 fix label example 2026-03-04 18:23:59 +00:00
7 changed files with 147 additions and 101 deletions

View File

@@ -27,19 +27,19 @@
### Command Line Options ### Command Line Options
| Option | Description | Example | | Option | Long Option | Description | Example |
|--------|-------------|----------| |--------|-------------|-------------|---------|
| `-h` | Print help message | `vmrcli.exe -h` | | `-h` | `--help` | Print help message | `vmrcli.exe -h` |
| `-v` | Show version information | `vmrcli.exe -v` | | `-v` | `--version` | Show version information | `vmrcli.exe -v` |
| `-i` | Enable interactive mode | `vmrcli.exe -i` | | `-i` | `--interactive` | Enable interactive mode | `vmrcli.exe -i` |
| `-I` | Interactive mode without prompt | `vmrcli.exe -I` | | `-I` | `--no-prompt` | Interactive mode without prompt | `vmrcli.exe -I` |
| `-f` | Don't split input on spaces | `vmrcli.exe -f` | | `-f` | `--full-line` | Don't split input on spaces | `vmrcli.exe -f` |
| `-k <type>` | Launch Voicemeeter GUI | `-kbasic`, `-kbanana`, `-kpotato` | | `-k <type>` | `--kind <type>` | Launch Voicemeeter GUI | `--kind basic`, `--kind banana`, `--kind potato` |
| `-l <level>` | Set log level | `-lDEBUG`, `-lINFO`, `-lWARN` | | `-l <level>` | `--log-level <level>` | Set log level | `--log-level DEBUG`, `--log-level WARN` |
| `-e` | Enable extra console output | `vmrcli.exe -e` | | `-e` | `--extra-output` | Enable extra console output | `vmrcli.exe -e` |
| `-c <path>` | Load user configuration | `-c "C:\config.txt"` | | `-c <path>` | `--config <path>` | Load user configuration | `--config "C:\config.txt"` |
| `-m` | Launch MacroButtons app | `vmrcli.exe -m` | | `-m` | `--macrobuttons` | Launch MacroButtons app | `vmrcli.exe -m` |
| `-s` | Launch StreamerView app | `vmrcli.exe -s` | | `-s` | `--streamerview` | Launch StreamerView app | `vmrcli.exe -s` |
> **Note:** When using interactive mode (`-i`), command line API commands are ignored. > **Note:** When using interactive mode (`-i`), command line API commands are ignored.
@@ -69,10 +69,10 @@
``` ```
#### **Setting Labels with Spaces** #### **Setting Labels with Spaces**
*Set device labels and retrieve values* *Set labels and print them back*
```powershell ```powershell
.\vmrcli.exe -kbanana -lDEBUG 'strip[0].label="my podmic"' strip[2].label .\vmrcli.exe -kbanana -lDEBUG 'strip[0].label="my podmic"' strip[0].label
``` ```
#### **Device Configuration** #### **Device Configuration**

View File

@@ -80,6 +80,6 @@ tasks:
{{if eq .CLI_ARGS "show"}} {{if eq .CLI_ARGS "show"}}
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+).\" -pp"
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+)\"" 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+)\" -pp"
{{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

@@ -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.13.0 * @version 0.14.1
* @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.13.0 * @version 0.14.1
* @date 2024-07-06 * @date 2024-07-06
* *
* @copyright Copyright (c) 2024 * @copyright Copyright (c) 2024

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.13.0 * @version 0.14.1
* @date 2024-07-06 * @date 2024-07-06
* *
* @copyright Copyright (c) 2024 * @copyright Copyright (c) 2024
@@ -22,22 +22,23 @@
#define USAGE "Usage: .\\vmrcli.exe [-h] [-v] [-i|-I] [-f] [-k] [-l] [-e] [-c] [-m] [-s] <api commands>\n" \ #define USAGE "Usage: .\\vmrcli.exe [-h] [-v] [-i|-I] [-f] [-k] [-l] [-e] [-c] [-m] [-s] <api commands>\n" \
"Where: \n" \ "Where: \n" \
"\th: Print the help message\n" \ "\t-h, --help: Print the help message\n" \
"\tv: Print the version number\n" \ "\t-v, --version: Print the version number\n" \
"\ti: Enable interactive mode, use (-I) to disable the '>>' prompt\n" \ "\t-i, --interactive: Enable interactive mode\n" \
"\tf: Do not split input on spaces\n" \ "\t-I, --no-prompt: Enable interactive mode without the '>>' prompt\n" \
"\tk: The kind of Voicemeeter (basic, banana, potato)\n" \ "\t-f, --full-line: Do not split input on spaces\n" \
"\tl: Set log level, must be one of TRACE, DEBUG, INFO, WARN, ERROR, or FATAL\n" \ "\t-k, --kind: The kind of Voicemeeter (basic, banana, potato)\n" \
"\te: Enable extra console output (toggle, set messages)\n" \ "\t-l, --log-level: Set log level, must be one of TRACE, DEBUG, INFO, WARN, ERROR, or FATAL\n" \
"\tc: Load a user configuration (give the full file path)\n" \ "\t-e, --extra-output: Enable extra console output (toggle, set messages)\n" \
"\tm: Launch the MacroButtons application\n" \ "\t-c, --config: Load a user configuration (give the full file path)\n" \
"\ts: Launch the StreamerView application" "\t-m, --macrobuttons: Launch the MacroButtons application\n" \
"\t-s, --streamerview: Launch the StreamerView application"
#define OPTSTR ":hvk:msc:iIfl:e" #define OPTSTR ":hvk:msc:iIfl:e"
#define MAX_LINE 4096 /* Size of the input buffer */ #define MAX_LINE 4096 /* Size of the input buffer */
#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.13.0" #define VERSION "0.14.1"
/** /**
* @enum The kind of values a get call may return. * @enum The kind of values a get call may return.
@@ -63,38 +64,70 @@ struct result
} val; } val;
}; };
static bool eflag = false; /**
* @struct A struct to hold the program configuration, set by CLI args
*/
struct config_t {
bool mflag;
bool sflag;
bool cflag;
char *cvalue;
bool iflag;
bool with_prompt;
bool fflag;
bool eflag;
int log_level;
enum kind kind;
};
/**
* @struct A struct to hold the program context, including the config and the iVMR interface pointer
*/
struct context_t {
struct config_t config;
PT_VMR vmr;
};
static void terminate(PT_VMR vmr, char *msg); static void terminate(PT_VMR vmr, char *msg);
static void usage(); static void usage();
static enum kind set_kind(char *kval); static enum kind set_kind(char *kval);
static void interactive(PT_VMR vmr, bool with_prompt, char *delimiters); static void interactive(const struct context_t *context, char *delimiters);
static void parse_input(PT_VMR vmr, char *input, char *delimiters); static void parse_input(const struct context_t *context, char *input, char *delimiters);
static void parse_command(PT_VMR vmr, char *command); static void parse_command(const struct context_t *context, char *command);
static void get(PT_VMR vmr, char *command, struct result *res); static void get(PT_VMR vmr, char *command, struct result *res);
int main(int argc, char *argv[]) int get_options(struct config_t *config, int argc, char *argv[])
{ {
bool iflag = false, static const struct option options[] =
mflag = false, {
sflag = false, {"help", no_argument, 0, 'h'},
cflag = false, {"version",no_argument, 0, 'v'},
fflag = false, {"kind", required_argument, 0, 'k'},
with_prompt = true; {"macrobuttons", no_argument, 0, 'm'},
int opt; {"streamerview", no_argument, 0, 's'},
int log_level = LOG_WARN; {"config", required_argument, 0, 'c'},
char *cvalue; {"interactive", no_argument, 0, 'i'},
enum kind kind = BANANAX64; {"no-prompt", no_argument, 0, 'I'},
{"full-line", no_argument, 0, 'f'},
{"log-level", required_argument,0, 'l'},
{"extra-output", no_argument, 0, 'e'},
{NULL, 0, NULL, 0 }
};
config->with_prompt = true;
config->log_level = LOG_WARN;
config->kind = BANANAX64;
if (argc == 1) if (argc == 1)
{ {
usage(); usage();
} }
log_set_level(log_level); log_set_level(config->log_level);
opterr = 0; opterr = 0;
while ((opt = getopt(argc, argv, OPTSTR)) != -1) int opt;
while ((opt = getopt_long(argc, argv, OPTSTR, options, NULL)) != -1)
{ {
switch (opt) switch (opt)
{ {
@@ -102,37 +135,37 @@ int main(int argc, char *argv[])
printf("vmrcli version %s\n", VERSION); printf("vmrcli version %s\n", VERSION);
exit(EXIT_SUCCESS); exit(EXIT_SUCCESS);
case 'k': case 'k':
kind = set_kind(optarg); config->kind = set_kind(optarg);
if (kind == UNKNOWN) if (config->kind == UNKNOWN)
{ {
log_fatal("Unknown Voicemeeter kind '%s'", optarg); log_fatal("Unknown Voicemeeter kind '%s'", optarg);
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
} }
break; break;
case 'm': case 'm':
mflag = true; config->mflag = true;
break; break;
case 's': case 's':
sflag = true; config->sflag = true;
break; break;
case 'c': case 'c':
cflag = true; config->cflag = true;
cvalue = optarg; config->cvalue = optarg;
break; break;
case 'I': case 'I':
with_prompt = false; config->with_prompt = false;
[[fallthrough]]; [[fallthrough]];
case 'i': case 'i':
iflag = true; config->iflag = true;
break; break;
case 'f': case 'f':
fflag = true; config->fflag = true;
break; break;
case 'l': case 'l':
log_level = log_level_from_string(optarg); config->log_level = log_level_from_string(optarg);
if (log_level != -1) if (config->log_level != -1)
{ {
log_set_level(log_level); log_set_level(config->log_level);
} }
else else
{ {
@@ -142,7 +175,7 @@ int main(int argc, char *argv[])
} }
break; break;
case 'e': case 'e':
eflag = true; config->eflag = true;
break; break;
case '?': case '?':
log_fatal("unknown option -- '%c'\n" log_fatal("unknown option -- '%c'\n"
@@ -160,69 +193,78 @@ int main(int argc, char *argv[])
usage(); usage();
} }
} }
return optind;
}
PT_VMR vmr = create_interface(); int main(int argc, char *argv[])
if (vmr == NULL) {
struct context_t context = {0};
int optind = get_options(&context.config, argc, argv);
log_set_level(context.config.log_level);
context.vmr = create_interface();
if (context.vmr == NULL)
{ {
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
} }
long rep = login(vmr, kind); long rep = login(context.vmr, context.config.kind);
if (rep != 0) if (rep != 0)
{ {
if (rep == -2) if (rep == -2)
terminate(vmr, "Timeout logging into the API."); terminate(context.vmr, "Timeout logging into the API.");
else else
terminate(vmr, "Error logging into the Voicemeeter API"); terminate(context.vmr, "Error logging into the Voicemeeter API");
} }
if (mflag) if (context.config.mflag)
{ {
run_voicemeeter(vmr, MACROBUTTONS); run_voicemeeter(context.vmr, MACROBUTTONS);
log_info("MacroButtons app launched"); log_info("MacroButtons app launched");
} }
if (sflag) if (context.config.sflag)
{ {
run_voicemeeter(vmr, STREAMERVIEW); run_voicemeeter(context.vmr, STREAMERVIEW);
log_info("StreamerView app launched"); log_info("StreamerView app launched");
} }
if (cflag) if (context.config.cflag)
{ {
set_parameter_string(vmr, "command.load", cvalue); set_parameter_string(context.vmr, "command.load", context.config.cvalue);
log_info("Profile %s loaded", cvalue); log_info("Profile %s loaded", context.config.cvalue);
Sleep(300); Sleep(300);
clear(vmr, is_pdirty); clear(context.vmr, is_pdirty);
} }
char *delimiter_ptr = DELIMITERS; char *delimiter_ptr = DELIMITERS;
if (fflag) if (context.config.fflag)
{ {
delimiter_ptr++; /* skip space delimiter */ delimiter_ptr++; /* skip space delimiter */
} }
if (iflag) if (context.config.iflag)
{ {
puts("Interactive mode enabled. Enter 'Q' to exit."); puts("Interactive mode enabled. Enter 'Q' to exit.");
interactive(vmr, with_prompt, delimiter_ptr); interactive(&context, delimiter_ptr);
} }
else else
{ {
for (int i = optind; i < argc; ++i) for (int i = optind; i < argc; ++i)
{ {
parse_input(vmr, argv[i], delimiter_ptr); parse_input(&context, argv[i], delimiter_ptr);
} }
} }
rep = logout(vmr); rep = logout(context.vmr);
if (rep != 0) if (rep != 0)
{ {
terminate(vmr, "Error logging out of the Voicemeeter API"); terminate(context.vmr, "Error logging out of the Voicemeeter API");
} }
log_info("Successfully logged out of the Voicemeeter API"); log_info("Successfully logged out of the Voicemeeter API");
free(vmr); free(context.vmr);
return EXIT_SUCCESS; return EXIT_SUCCESS;
} }
@@ -276,12 +318,12 @@ static enum kind set_kind(char *kval)
* @param with_prompt If true, prints the interactive prompt '>>' * @param with_prompt If true, prints the interactive prompt '>>'
* @param delimiters A string of delimiter characters to split each input line * @param delimiters A string of delimiter characters to split each input line
*/ */
static void interactive(PT_VMR vmr, bool with_prompt, char *delimiters) static void interactive(const struct context_t *context, char *delimiters)
{ {
char input[MAX_LINE]; char input[MAX_LINE];
size_t len; size_t len;
if (with_prompt) if (context->config.with_prompt)
printf(">> "); printf(">> ");
while (fgets(input, MAX_LINE, stdin) != NULL) while (fgets(input, MAX_LINE, stdin) != NULL)
{ {
@@ -289,9 +331,9 @@ static void interactive(PT_VMR vmr, bool with_prompt, char *delimiters)
if (len == 1 && toupper(input[0]) == 'Q') if (len == 1 && toupper(input[0]) == 'Q')
break; break;
parse_input(vmr, input, delimiters); parse_input(context, input, delimiters);
if (with_prompt) if (context->config.with_prompt)
printf(">> "); printf(">> ");
} }
} }
@@ -329,7 +371,7 @@ static bool add_char_to_token(char *token, size_t *token_len, char c, size_t max
* @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
*/ */
static void parse_input(PT_VMR vmr, char *input, char *delimiters) static void parse_input(const struct context_t *context, char *input, char *delimiters)
{ {
if (is_comment(input)) if (is_comment(input))
return; return;
@@ -363,7 +405,7 @@ static void parse_input(PT_VMR vmr, char *input, char *delimiters)
if (token_length > 0) if (token_length > 0)
{ {
token[token_length] = '\0'; token[token_length] = '\0';
parse_command(vmr, token); parse_command(context, token);
token_length = 0; token_length = 0;
} }
@@ -372,8 +414,12 @@ static void parse_input(PT_VMR vmr, char *input, char *delimiters)
} }
else else
{ {
add_char_to_token(token, &token_length, *current, MAX_LINE); if (!add_char_to_token(token, &token_length, *current, MAX_LINE))
log_trace("Added char '%c' to token, current token: '%s'", *current, token); {
log_error("Input token exceeds maximum length of %d characters", MAX_LINE - 1);
return;
}
log_trace("Added char '%c' to token, current token: '%.*s'", *current, (int)token_length, token);
} }
current++; current++;
} }
@@ -381,7 +427,7 @@ static void parse_input(PT_VMR vmr, char *input, char *delimiters)
if (token_length > 0) if (token_length > 0)
{ {
token[token_length] = '\0'; token[token_length] = '\0';
parse_command(vmr, token); parse_command(context, token);
} }
} }
@@ -393,7 +439,7 @@ static void parse_input(PT_VMR vmr, char *input, char *delimiters)
* @param vmr Pointer to the iVMR interface * @param vmr Pointer to the iVMR interface
* @param command Each token from the input line as its own command string * @param command Each token from the input line as its own command string
*/ */
static void parse_command(PT_VMR vmr, char *command) static void parse_command(const struct context_t *context, char *command)
{ {
log_debug("Parsing %s", command); log_debug("Parsing %s", command);
@@ -407,8 +453,8 @@ static void parse_command(PT_VMR vmr, char *command)
struct quickcommand *qc_ptr = command_in_quickcommands(command, quickcommands, (int)COUNT_OF(quickcommands)); struct quickcommand *qc_ptr = command_in_quickcommands(command, quickcommands, (int)COUNT_OF(quickcommands));
if (qc_ptr != NULL) if (qc_ptr != NULL)
{ {
set_parameters(vmr, qc_ptr->fullcommand); set_parameters(context->vmr, qc_ptr->fullcommand);
if (eflag) { if (context->config.eflag) {
printf("Setting %s\n", qc_ptr->fullcommand); printf("Setting %s\n", qc_ptr->fullcommand);
} }
return; return;
@@ -419,13 +465,13 @@ static void parse_command(PT_VMR vmr, char *command)
command++; command++;
struct result res = {.type = FLOAT_T}; struct result res = {.type = FLOAT_T};
get(vmr, command, &res); get(context->vmr, command, &res);
if (res.type == FLOAT_T) if (res.type == FLOAT_T)
{ {
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(context->vmr, command, 1 - res.val.f);
if (eflag) { if (context->config.eflag) {
printf("Toggling %s\n", command); printf("Toggling %s\n", command);
} }
} }
@@ -441,8 +487,8 @@ static void parse_command(PT_VMR vmr, char *command)
if (add_quotes_if_needed(command, quoted_command, MAX_LINE)) if (add_quotes_if_needed(command, quoted_command, MAX_LINE))
{ {
set_parameters(vmr, quoted_command); set_parameters(context->vmr, quoted_command);
if (eflag) { if (context->config.eflag) {
printf("Setting %s\n", command); printf("Setting %s\n", command);
} }
} }
@@ -455,7 +501,7 @@ static void parse_command(PT_VMR vmr, char *command)
{ {
struct result res = {.type = FLOAT_T}; struct result res = {.type = FLOAT_T};
get(vmr, command, &res); get(context->vmr, command, &res);
switch (res.type) switch (res.type)
{ {
case FLOAT_T: case FLOAT_T:

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.13.0 * @version 0.14.1
* @date 2024-07-06 * @date 2024-07-06
* *
* @copyright Copyright (c) 2024 * @copyright Copyright (c) 2024