Right, it's time to actually get a working user interface setup. We'll implement a basic dial and inform the host of changes to the parameter, including the gesture begin and end events. We'll also implement the timer extension so when the host automates the parameter, the dial can be automatically redrawn with the new value.

I assume that you'll be likely replacing the entire GUI code once you've finished the tutorial, so I'm going to keep it as minimal as possible.

We begin by adding a function to draw a rectangle, and implement PluginPaint.

static void PluginPaintRectangle(MyPlugin *plugin, uint32_t *bits, uint32_t l, uint32_t r, uint32_t t, uint32_t b, uint32_t border, uint32_t fill) {
    for (uint32_t i = t; i < b; i++) {
        for (uint32_t j = l; j < r; j++) {
            bits[i * GUI_WIDTH + j] = (i == t || i == b - 1 || j == l || j == r - 1) ? border : fill;
        }
    }
}

static void PluginPaint(MyPlugin *plugin, uint32_t *bits) {
    // Draw the background.
    PluginPaintRectangle(plugin, bits, 0, GUI_WIDTH, 0, GUI_HEIGHT, 0xC0C0C0, 0xC0C0C0);

    // Draw the parameter, using the parameter value owned by the main thread.
    PluginPaintRectangle(plugin, bits, 10, 40, 10, 40, 0x000000, 0xC0C0C0);
    PluginPaintRectangle(plugin, bits, 10, 40, 10 + 30 * (1.0f - plugin->mainParameters[P_VOLUME]), 40, 0x000000, 0x000000);
}

The next step is to allow the user to interact with the dial. First, we'll need to query the host's implementation of the parameters extension so that we can notify it when some parameters have been modified. These will later be picked up on the audio thread and sent to the host via the output events queue. This will either happen in the audio process callback, or in the extensionParams.flush function.

To the MyPlugin structure, add the field const clap_host_params_t *hostParams;. Then in pluginClass.init, set the field using plugin->hostParams = (const clap_host_params_t *) plugin->host->get_extension(plugin->host, CLAP_EXT_PARAMS);.

We're going to maintain an array of gesture events that need to be sent to the host. Gesture events are used to indicate when the user starts and stop modifying a parameter on the plugin's GUI. They aren't necessary to send, but implementing them for continuous controls like dials will ensure the best user experience the host can provide. To the MyPlugin structure we add the following fields:

// Gesture start and end events that need to be sent.
bool gestureStart[P_COUNT], gestureEnd[P_COUNT];

// Information about the parameter that is currently being modified.
bool mouseDragging;
uint32_t mouseDraggingParameter;
int32_t mouseDragOriginX, mouseDragOriginY;
float mouseDragOriginValue;

Let's implement PluginProcessMousePress.

static void PluginProcessMousePress(MyPlugin *plugin, int32_t x, int32_t y) {
    // If the cursor is inside the dial...
    if (x >= 10 && x < 40 && y >= 10 && y < 40) {
        // Start dragging.
        plugin->mouseDragging = true;
        plugin->mouseDraggingParameter = P_VOLUME;
        plugin->mouseDragOriginX = x;
        plugin->mouseDragOriginY = y;
        plugin->mouseDragOriginValue = plugin->mainParameters[P_VOLUME];

        // Inform the audio thread to send a gesture start event.
        MutexAcquire(plugin->syncParameters);
        plugin->gestureStart[plugin->mouseDraggingParameter] = true;
        MutexRelease(plugin->syncParameters);

        // Inform the host that either the pluginClass.process or extensionParams.flush
        // callback should be executed at some point in the near future so that we
        // can send the host the gesture and parameter value events.
        if (plugin->hostParams && plugin->hostParams->request_flush) {
            plugin->hostParams->request_flush(plugin->host);
        }
    }
}

In PluginProcessMouseRelease we perform the opposite actions.

static void PluginProcessMouseRelease(MyPlugin *plugin) {
    if (plugin->mouseDragging) {
        // Inform the audio thread to send a gesture end event.
        MutexAcquire(plugin->syncParameters);
        plugin->gestureEnd[plugin->mouseDraggingParameter] = true;
        MutexRelease(plugin->syncParameters);

        // As before.
        if (plugin->hostParams && plugin->hostParams->request_flush) {
            plugin->hostParams->request_flush(plugin->host);
        }

        // Dragging has stopped.
        plugin->mouseDragging = false;
    }
}

Finally, we implement PluginProcessMouseDrag. We compute the new value of the parameter, update our array and indicate to the audio thread that it has changed.

static void PluginProcessMouseDrag(MyPlugin *plugin, int32_t x, int32_t y) {
    if (plugin->mouseDragging) {
        // Compute the new value of the parameter based on the mouse's position.
        float newValue = FloatClamp01(plugin->mouseDragOriginValue + (plugin->mouseDragOriginY - y) * 0.01f);

        // Under the syncParameters mutex, update the main thread's parameters array,
        // and inform the audio thread it should read the value into its array.
        MutexAcquire(plugin->syncParameters);
        plugin->mainParameters[plugin->mouseDraggingParameter] = newValue;
        plugin->mainChanged[plugin->mouseDraggingParameter] = true;
        MutexRelease(plugin->syncParameters);

        // As before.
        if (plugin->hostParams && plugin->hostParams->request_flush) {
            plugin->hostParams->request_flush(plugin->host);
        }
    }
}

We now need to actually send the gesture events to the host. This takes place in PluginSyncMainToAudio, where we send the CLAP_EVENT_PARAM_VALUE events to the host. We send the CLAP_EVENT_PARAM_GESTURE_BEGIN events first, then the CLAP_EVENT_PARAM_VALUE events, and finally the CLAP_EVENT_PARAM_GESTURE_END events. This ensures that the parameter values events are never sent outside of a gesture.

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->gestureStart[i]) {
            plugin->gestureStart[i] = false;

            clap_event_param_gesture_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_GESTURE_BEGIN;
            event.header.flags = 0;
            event.param_id = i;
            out->try_push(out, &event.header);
        }

        if (plugin->mainChanged[i]) {
            // ... unchanged ...
        }

        if (plugin->gestureEnd[i]) {
            plugin->gestureEnd[i] = false;

            clap_event_param_gesture_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_GESTURE_END;
            event.header.flags = 0;
            event.param_id = i;
            out->try_push(out, &event.header);
        }
    }

    MutexRelease(plugin->syncParameters);
}

Okay, the last thing to do is to implement the timer extension. This will give us a regular callback on the main thread, which will give us an opportunity to periodically synchronize the parameter values from the audio thread to the main thread and repaint the user interface. This will mean that parameters automated by the host will display correctly in the plugin's GUI.

static const clap_plugin_timer_support_t extensionTimerSupport = {
    .on_timer = [] (const clap_plugin_t *_plugin, clap_id timerID) {
        MyPlugin *plugin = (MyPlugin *) _plugin->plugin_data;

        // If the GUI is open and at least one parameter value has changed...
        if (plugin->gui && PluginSyncAudioToMain(plugin)) {
            // Repaint the GUI.
            GUIPaint(plugin, true);
        }
    },
};

And we update pluginClass.get_extension:

.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;
    if (0 == strcmp(id, CLAP_EXT_GUI             )) return &extensionGUI;
    if (0 == strcmp(id, CLAP_EXT_POSIX_FD_SUPPORT)) return &extensionPOSIXFDSupport;
    if (0 == strcmp(id, CLAP_EXT_TIMER_SUPPORT   )) return &extensionTimerSupport;
    if (0 == strcmp(id, CLAP_EXT_STATE           )) return &extensionState;
    return nullptr;
},

All that's left to do now is to register and unregister the timer when the plugin is initialized and destroyed. We add the following fields to MyPlugin:

const clap_host_timer_support_t *hostTimerSupport;
clap_id timerID;

In pluginClass.init, add the following. The timer ID is saved as we will use it to unregister the timer.

plugin->hostTimerSupport = (const clap_host_timer_support_t *) plugin->host->get_extension(plugin->host, CLAP_EXT_TIMER_SUPPORT);

if (plugin->hostTimerSupport && plugin->hostTimerSupport->register_timer) {
    plugin->hostTimerSupport->register_timer(plugin->host, 200 /* every 200 milliseconds */, &plugin->timerID);
}

Finally, in pluginClass.destroy add:

if (plugin->hostTimerSupport && plugin->hostTimerSupport->register_timer) {
    plugin->hostTimerSupport->unregister_timer(plugin->host, plugin->timerID);
}

And that's all there is to it! This is the end of the tutorial. We've built a simple CLAP audio plugin that plays sine waves polyphonically with automatable and non-destructively modulatable parameters, as well as implemented a properly synchronized GUI and state serialization. That's all for now, folks.

clap-tutorial-part-4-plugin.cpp

Part 1: Basics.

Part 2: Parameters.

Part 3: GUI.

Part 4: You are here!.