Interfacing with Voicemeeter on the Command Line

I recently picked up the book C Programming A Modern Approach 2e by K. N. King. Although I've dabbled with C over the years this is the first time I've committed any period of time to learning it. I took this opportunity to do some programming with the Voicemeeter SDK while reading the book.

My goal was to create a CLI program that implements the following features:


First step, have the CLI accept instructions as arguments and execute each in turn. Then define an -i flag to enable interactive mode. This allows a user to operate the program in two distinct modes.

    while ((opt = getopt(argc, argv, OPTSTR)) != -1)
    {
        switch (opt)
        {
        case 'i':
            iflag = true;
            break;
        case 'h':
            [[fallthrough]];
        default:
            usage();
        }
    }

    if (iflag)
    {
        puts("Interactive mode enabled. Enter 'Q' to exit.");
        interactive(vmr);
    }
    else
    {
        for (int i = optind; i < argc; i++)
        {
            parse_input(vmr, argv[i]);
        }
    }

Interactive mode should read repeatedly from stdin:

void interactive(PT_VMR vmr)
{
    ...

    while (fgets(input, MAX_LINE, stdin) != NULL)
    {
        input[(len = strcspn(input, "\n"))] = 0;
        if (len == 1 && toupper(input[0]) == 'Q')
            break;

        parse_input(vmr, input);
        
        ...
    }
}

Allowing a user to enter commands until an exit instruction is given:

Interactive Mode


Since reading from script files may use either of the CLI's modes we must parse the input in both cases. Consider that a single line in a script may contain multiple instructions, for example:

strip[0].gain=5 strip[1].comp+=4.8 strip[2].label=podmic

So it's important that we split lines into separate instructions:

void parse_input(PT_VMR vmr, char *input)
{
    ...

    token = strtok_r(input, DELIMITERS, &p);
    while (token != NULL)
    {
        parse_command(vmr, token);
        token = strtok_r(NULL, DELIMITERS, &p);
    }
}

Here is an example run with verbose output enabled:

Script File Example


The CLI application should correctly handle get, set and toggle operations.

void parse_command(PT_VMR vmr, char *command)
{
    log_debug("Parsing %s", command);

    if (command[0] == '!') /* toggle */
    {
        ...

        return;
    }

    if (strchr(command, '=') != NULL) /* set */
    {
        ...
    }
    else /* get */
    {
        ...
    }
}

Set is trivial enough, we can simply use the VBVMR_SetParameters api call. This handles both float and string parameters.

Get is a little tricker because C is statically typed, meaning the compiler must be made aware of parameter and return types. To avoid having to explicitly track which commands are expected to return which type of response we can use the fact that a failed get_parameter_float will return an error code and then try get_parameter_string.

void get(PT_VMR vmr, char *command, struct result *res)
{
    clear_dirty(vmr);
    if (get_parameter_float(vmr, command, &res->val.f) != 0)
    {
        res->type = STRING_T;
        if (get_parameter_string(vmr, command, res->val.s) != 0)
        {
            res->val.s[0] = 0;
            log_error("Unknown parameter '%s'", command);
        }
    }
}

As well as that, C offers Unions for building mixed data structures (covered in Chapter 16 of C Programming A Modern Approach). By defining a Struct with a Union member I was able to store and track the result.

struct result
{
    enum restype type;
    union val
    {
        float f;
        wchar_t s[RES_SZ];
    } val;
};

Toggle is then simply an implementation of a get into a set. The only noteworth detail is that we should guard against unsafe gain changes. I handled this by first testing if the response was of type float, and then testing it against 1 and 0. Strictly speaking this doesn't guarantee a boolean parameter, but it does protect against dangerous operations such as Strip 0 Gain = (1 - (-18)) which could be hazardous to health or audio equipment.

        if (res.type == FLOAT_T)
        {
            if (res.val.f == 1 || res.val.f == 0)
            {
                set_parameter_float(vmr, command, 1 - res.val.f);
            }
            else
            {
                ...
            }
        }

I decided to use the log.c package by rxi to offer various levels of logging. Here is a demonstration of the CLI run in direct mode with TRACE logging enabled.

Trace Logging

As you can see, it gives a low level perspective of the API calls.


This has been a very fun project to tackle, it's easy to see why people fall in love with programming in C.

I have made public the full source code for this package.

Further Notes:

Subscribe to this blog's RSS feed