Let's add parameters to our plugin. For demonstration purposes we'll only be adding one parameter, volume, but it should be clear how to extend the plugin yourself. We'll support non-destructive polyphonic modulation, as well as synchronizing the parameters between the main and audio threads, so that we can serialize our plugin's state.

We begin by defining a few things: a mutex object, a helper function to clamp a float between 0 and 1, and the enumeration values for our parameters.

#ifdef _WIN32
#include <windows.h>
typedef HANDLE Mutex;
#define MutexAcquire(mutex) WaitForSingleObject(mutex, INFINITE)
#define MutexRelease(mutex) ReleaseMutex(mutex)
#define MutexInitialise(mutex) (mutex = CreateMutex(nullptr, FALSE, nullptr))
#define MutexDestroy(mutex) CloseHandle(mutex)
#else
#include <pthread.h>
typedef pthread_mutex_t Mutex;
#define MutexAcquire(mutex) pthread_mutex_lock(&(mutex))
#define MutexRelease(mutex) pthread_mutex_unlock(&(mutex))
#define MutexInitialise(mutex) pthread_mutex_init(&(mutex), nullptr)
#define MutexDestroy(mutex) pthread_mutex_destroy(&(mutex))
#endif

static float FloatClamp01(float x) {
    return x >= 1.0f ? 1.0f : x <= 0.0f ? 0.0f : x;
}

// Parameters.
#define P_VOLUME (0)
#define P_COUNT (1)

Although for the sake of simplicity we will use mutex objects for this tutorial, in due course it would be a good idea to replace them with lock-free structures in your codebase.

Next, we update the Voice structure to include an array of parameter offsets specific to that value. The host will send these values to us via events on the audio thread, and we will use them for polyphonic modulation.

struct Voice {
    bool held;
    int32_t noteID;
    int16_t channel, key;

    float phase;
    float parameterOffsets[P_COUNT]; // New!
};

Now we add the arrays of parameters to the MyPlugin structure. We have parameters, the array for the audio thread, and mainParameters, the array for the main thread. We also add two boolean arrays: changed indicating for each parameter whether the value has been changed on the audio thread and needs synchronization to the main thread; and mainChanged for the opposite direction of synchronization. Finally, we add a syncParameters mutex. If a thread wants to write to an array, or read from an array of the opposite thread, they should do so with this mutex acquired.

struct MyPlugin {
    clap_plugin_t plugin;
    const clap_host_t *host;
    float sampleRate;
    Array<Voice> voices;
    float parameters[P_COUNT], mainParameters[P_COUNT]; // New!
    bool changed[P_COUNT], mainChanged[P_COUNT]; // New!
    Mutex syncParameters; // New!
};

With the structures all setup, we can begin modifying the actual plugin code. We start in PluginRenderAudio, where we read the volume parameter. We read from the parameters array in MyPlugin, since we are on the audio thread. We add to this the value in parameterOffsets of the Voice and clamp it, giving the polyphonically modulated value.

static void PluginRenderAudio(MyPlugin *plugin, uint32_t start, uint32_t end, float *outputL, float *outputR) {
    for (uint32_t index = start; index < end; index++) {
        float sum = 0.0f;

        for (int i = 0; i < plugin->voices.Length(); i++) {
            Voice *voice = &plugin->voices[i];
            if (!voice->held) continue;

            // New!
            float volume = FloatClamp01(plugin->parameters[P_VOLUME] + voice->parameterOffsets[P_VOLUME]);
            sum += sinf(voice->phase * 2.0f * 3.14159f) * 0.2f * volume;

            voice->phase += 440.0f * exp2f((voice->key - 57.0f) / 12.0f) / plugin->sampleRate;
            voice->phase -= floorf(voice->phase);
        }

        outputL[index] = sum;
        outputR[index] = sum;
    }
}

Next, we update PluginProcessEvent to handle the CLAP_EVENT_PARAM_VALUE and CLAP_EVENT_PARAM_MOD events. The former is sent when the host wants to change the actual value of a parameter; for example, when the user modifies a dial for one of the plugin's parameters in the host's UI. The latter is sent for polyphonic modulation; the event specifies a voice or set of voices to which the parameter offset applies.

For the CLAP_EVENT_PARAM_VALUE, we store the value into the parameters array (since we are on the audio thread), and mark the corresponding changed boolean, so that the main thread knows the audio thread wants to update the value. We make sure that the operation is done under lock of the syncParameters mutex, since we are modifying the arrays.

if (event->type == CLAP_EVENT_PARAM_VALUE) {
    const clap_event_param_value_t *valueEvent = (const clap_event_param_value_t *) event;
    uint32_t i = (uint32_t) valueEvent->param_id;
    MutexAcquire(plugin->syncParameters);
    plugin->parameters[i] = valueEvent->value;
    plugin->changed[i] = true;
    MutexRelease(plugin->syncParameters);
}

For the CLAP_EVENT_PARAM_MOD, we iterate through the voices, and for any that match the query, we update the parameter offset. Since this is non-destructive modulation, the main thread doesn't need to know about this, since it won't be using the values for serialization.

if (event->type == CLAP_EVENT_PARAM_MOD) {
    const clap_event_param_mod_t *modEvent = (const clap_event_param_mod_t *) event;

    for (int i = 0; i < plugin->voices.Length(); i++) {
        Voice *voice = &plugin->voices[i];

        if ((modEvent->key == -1 || voice->key == modEvent->key)
                && (modEvent->note_id == -1 || voice->noteID == modEvent->note_id)
                && (modEvent->channel == -1 || voice->channel == modEvent->channel)) {
            voice->parameterOffsets[modEvent->param_id] = modEvent->amount;
            break;
        }
    }
}

Let us now take the opportunity to initialize the parameter arrays and the mutex. In pluginClass.init, we add the following code:

.init = [] (const clap_plugin *_plugin) -> bool {
    MyPlugin *plugin = (MyPlugin *) _plugin->plugin_data;

    MutexInitialise(plugin->syncParameters);

    for (uint32_t i = 0; i < P_COUNT; i++) {
        clap_param_info_t information = {};
        extensionParams.get_info(_plugin, i, &information);
        plugin->mainParameters[i] = plugin->parameters[i] = information.default_value;
    }

    return true;
}

We have not defined extensionParams yet, but we will soon enough. And in pluginClass.destroy, we add the code to destroy the mutex:

.destroy = [] (const clap_plugin *_plugin) {
    MyPlugin *plugin = (MyPlugin *) _plugin->plugin_data;
    plugin->voices.Free();
    MutexDestroy(plugin->syncParameters);
    free(plugin);
}

We're going to implementing two new extensions, the parameters extension and the state extension. Before we can get there though, we need to add functions to synchronize parameters from the main thread to the audio thread and another for the opposite direction. These functions work under thelock of the syncParameters mutex since they will be reading from the arrays owned by the opposite thread as well as modifying their own arrays.

To synchronize from the main thread to audio thread, for each of the changed parameters, we copy the new parameter value over and then construct a CLAP_EVENT_PARAM_VALUE event to send to the host. The event is pushed to a clap_output_events_t structure. Note that we also processing incoming CLAP_EVENT_PARAM_VALUE events from the host; it works very similar in the direction from the plugin to the host.

static void PluginSyncMainToAudio(MyPlugin *plugin, const clap_output_events_t *out) {
    MutexAcquire(plugin->syncParameters);

    for (uint32_t i = 0; i < P_COUNT; i++) {
        if (plugin->mainChanged[i]) {
            plugin->parameters[i] = plugin->mainParameters[i];
            plugin->mainChanged[i] = false;

            clap_event_param_value_t event = {};
            event.header.size = sizeof(event);
            event.header.time = 0;
            event.header.space_id = CLAP_CORE_EVENT_SPACE_ID;
            event.header.type = CLAP_EVENT_PARAM_VALUE;
            event.header.flags = 0;
            event.param_id = i;
            event.cookie = NULL;
            event.note_id = -1;
            event.port_index = -1;
            event.channel = -1;
            event.key = -1;
            event.value = plugin->parameters[i];
            out->try_push(out, &event.header);
        }
    }

    MutexRelease(plugin->syncParameters);
}

To synchronize from the audio thread to the main thread, there is less to do. We only need to copy the new values into the array. We also compute a boolean indicating whether any parameter had changed during the call; this will be useful later when we implement a GUI.

static bool PluginSyncAudioToMain(MyPlugin *plugin) {
    bool anyChanged = false;
    MutexAcquire(plugin->syncParameters);

    for (uint32_t i = 0; i < P_COUNT; i++) {
        if (plugin->changed[i]) {
            plugin->mainParameters[i] = plugin->parameters[i];
            plugin->changed[i] = false;
            anyChanged = true;
        }
    }

    MutexRelease(plugin->syncParameters);
    return anyChanged;
}

With these functions in place, we're almost ready to implement the new extension. But first let's make sure that the PluginSyncMainToAudio function is called in pluginClass.process:

.process = [] (const clap_plugin *_plugin, const clap_process_t *process) -> clap_process_status {
    MyPlugin *plugin = (MyPlugin *) _plugin->plugin_data;

    PluginSyncMainToAudio(plugin, process->out_events);

    // ... as before ...

    return CLAP_PROCESS_CONTINUE;
}

Finally, we can begin work on the extensions. First, the parameters extension. This tells the host about the properties of our parameters (.count, .get_info), gives it a way to query the current value of parameters (.get_value), gives it a way to transform values to and from text (.value_to_text, .text_to_value), and also provides a mechanism for parameter synchronization when the plugin isn't processing audio (.flush).

static const clap_plugin_params_t extensionParams = {
    .count = [] (const clap_plugin_t *plugin) -> uint32_t {
        return P_COUNT;
    },

    .get_info = [] (const clap_plugin_t *_plugin, uint32_t index, clap_param_info_t *information) -> bool {
        if (index == P_VOLUME) {
            memset(information, 0, sizeof(clap_param_info_t));
            information->id = index;
            // These flags enable polyphonic modulation.
            information->flags = CLAP_PARAM_IS_AUTOMATABLE | CLAP_PARAM_IS_MODULATABLE | CLAP_PARAM_IS_MODULATABLE_PER_NOTE_ID;
            information->min_value = 0.0f;
            information->max_value = 1.0f;
            information->default_value = 0.5f;
            strcpy(information->name, "Volume");
            return true;
        } else {
            return false;
        }
    },

    .get_value = [] (const clap_plugin_t *_plugin, clap_id id, double *value) -> bool {
        MyPlugin *plugin = (MyPlugin *) _plugin->plugin_data;
        uint32_t i = (uint32_t) id;
        if (i >= P_COUNT) return false;

        // get_value is called on the main thread, but should return the value of the parameter according to the audio thread,
        // since the value on the audio thread is the one that host communicates with us via CLAP_EVENT_PARAM_VALUE events.
        // Since we're accessing the opposite thread's arrays, we must acquire the syncParameters mutex.
        // And although we need to check the mainChanged array, we mustn't actually modify the parameters array,
        // since that can only be done on the audio thread. Don't worry -- it'll pick up the changes eventually.
        MutexAcquire(plugin->syncParameters);
        *value = plugin->mainChanged[i] ? plugin->mainParameters[i] : plugin->parameters[i];
        MutexRelease(plugin->syncParameters);
        return true;
    },

    .value_to_text = [] (const clap_plugin_t *_plugin, clap_id id, double value, char *display, uint32_t size) {
        uint32_t i = (uint32_t) id;
        if (i >= P_COUNT) return false;
        snprintf(display, size, "%f", value);
        return true;
    },

    .text_to_value = [] (const clap_plugin_t *_plugin, clap_id param_id, const char *display, double *value) {
        // TODO Implement this.
        return false;
    },

    .flush = [] (const clap_plugin_t *_plugin, const clap_input_events_t *in, const clap_output_events_t *out) {
        MyPlugin *plugin = (MyPlugin *) _plugin->plugin_data;
        const uint32_t eventCount = in->size(in);

        // For parameters that have been modified by the main thread, send CLAP_EVENT_PARAM_VALUE events to the host.
        PluginSyncMainToAudio(plugin, out);

        // Process events sent to our plugin from the host.
        for (uint32_t eventIndex = 0; eventIndex < eventCount; eventIndex++) {
            PluginProcessEvent(plugin, in->get(in, eventIndex));
        }
    },
};

The other extension is the state extension. This lets our plugin save and restore its state. We assume that the stream accessor functions won't result in short reads or writes, but this is not guaranteed; in a production plugin, you should modify these implementation to handle short reads and writes.

static const clap_plugin_state_t extensionState = {
    .save = [] (const clap_plugin_t *_plugin, const clap_ostream_t *stream) -> bool {
        MyPlugin *plugin = (MyPlugin *) _plugin->plugin_data;

        // Synchronize any changes from the audio thread (that is, parameter values sent to us by the host)
        // before we save the state of the plugin.
        PluginSyncAudioToMain(plugin);

        return sizeof(float) * P_COUNT == stream->write(stream, plugin->mainParameters, sizeof(float) * P_COUNT);
    },

    .load = [] (const clap_plugin_t *_plugin, const clap_istream_t *stream) -> bool {
        MyPlugin *plugin = (MyPlugin *) _plugin->plugin_data;

        // Since we're modifying a parameter array, we need to acquire the syncParameters mutex.
        MutexAcquire(plugin->syncParameters);
        bool success = sizeof(float) * P_COUNT == stream->read(stream, plugin->mainParameters, sizeof(float) * P_COUNT);
        // Make sure that the audio thread will pick up upon the modified parameters next time pluginClass.process is called.
        for (uint32_t i = 0; i < P_COUNT; i++) plugin->mainChanged[i] = true;
        MutexRelease(plugin->syncParameters);

        return success;
    },
};

Finally, we must update pluginClass.get_extension to include the new extensions we've implemented.

.get_extension = [] (const clap_plugin *plugin, const char *id) -> const void * {
    if (0 == strcmp(id, CLAP_EXT_NOTE_PORTS )) return &extensionNotePorts;
    if (0 == strcmp(id, CLAP_EXT_AUDIO_PORTS)) return &extensionAudioPorts;
    if (0 == strcmp(id, CLAP_EXT_PARAMS     )) return &extensionParams; // New!
    if (0 == strcmp(id, CLAP_EXT_STATE      )) return &extensionState; // New!
    return nullptr;
},

Fantastic, we've now got parameters support in our plugin. Recompile and load it in Bitwig. You should be able to assign modulators to the volume parameter and try out polyphonic modulation!

clap-tutorial-part-2-plugin.cpp

Part 1: Basics.

Part 2: You are here!

Part 3: GUI.

Part 4: Gestures.