/ VST Home / Tutorials

Data Exchange Tutorial - How to send data from the realtime process to the edit controller

On this page:

Related pages:


You'll find the source for this tutorial in the tutorial repository

Tutorial - How to use the Data Exchange API

In this tutorial you learn how to use the Data Exchange API to send data from the realtime audio process method to the edit controller of your plug-in.

Sending data from the audio processor

Let's send data from the processor to the controller.

First, we need to add the required include so that we can use the wrapper class. Add the following include before you define your audio processor:

#include "public.sdk/source/vst/utility/dataexchange.h"

To prepare the AudioEffect class, you need to overwrite the following methods:

tresult PLUGIN_API initialize (FUnknown* context) override;
tresult PLUGIN_API connect (Vst::IConnectionPoint* other) override;
tresult PLUGIN_API disconnect (Vst::IConnectionPoint* other) override;
tresult PLUGIN_API setActive (TBool state) override;
tresult PLUGIN_API canProcessSampleSize (int32 symbolicSampleSize) override;
tresult PLUGIN_API process (Vst::ProcessData& data) override;

And we also have to add the wrapper class as a member of our audio processor class:

std::unique_ptr<Vst::DataExchangeHandler> dataExchange;

Now we can initialize and configure the dataExchange. We do this in:

tresult PLUGIN_API DataExchangeProcessor::connect (Vst::IConnectionPoint* other)
{
    auto result = Vst::AudioEffect::connect (other);
    if (result == kResultTrue)
    {
        auto configCallback = [this] (Vst::DataExchangeHandler::Config& config,
                                      const Vst::ProcessSetup& setup) {
            Vst::SpeakerArrangement arr;
            getBusArrangement (Vst::BusDirections::kInput, 0, arr);
            numChannels = static_cast<uint16_t> (Vst::SpeakerArr::getChannelCount (arr));
            auto sampleSize = sizeof (float);

            config.blockSize = setup.sampleRate * numChannels * sampleSize + sizeof (DataBlock);
            config.numBlocks = 2;
            config.alignment = 32;
            config.userContextID = 0;
            return true;
        };

        dataExchange = std::make_unique<Vst::DataExchangeHandler> (this, configCallback);
        dataExchange->onConnect (other, getHostContext ());
    }
    return result;
}

The configration is done via the configCallback. In this example, we configure the queue to have a block size to store exactly 1 second of audio data of all the channels of the configured speaker arrangement of the input bus. We choose two for numBlocks because we send one block per second, and in this case, two blocks should be enough. If the frequency you need to send the block is higher, you need to increase this value to prevent data drop outs. The configCallback is called when the audio processor is activated.

The next thing we have to do is to call the dataExchange object when the edit controller is disconnected and release the memory:

tresult PLUGIN_API DataExchangeProcessor::disconnect (Vst::IConnectionPoint* other)
{
    if (dataExchange)
    {
        dataExchange->onDisconnect (other);
        dataExchange.reset ();
    }
    return AudioEffect::disconnect (other);
}

And we also have to call the dataExchange object when the processor's state changes:

tresult PLUGIN_API DataExchangeProcessor::setActive (TBool state)
{
    if (state)
        dataExchange->onActivate (processSetup);
    else
        dataExchange->onDeactivate ();
    return AudioEffect::setActive (state);
}

Now we prepare the data that we want to send to the controller. To make this a little bit easier we define a struct how this data should look like and move this into its own header "dataexchange.h":

// dataexchange.h

#pragma once

#include "public.sdk/source/vst/utility/dataexchange.h"
#include <cstdint>

namespace Steinberg::Tutorial {

struct DataBlock
{
    uint32_t sampleRate;
    uint16_t sampleSize;
    uint16_t numChannels;
    uint32_t numSamples;
    float samples[0];
};

} // Steinberg::Tutorial

So, we want to send the sample rate, size, the number of channels and the number of samples plus the actual samples to the controller.

To actually work with this DataBlock struct, we introduce a little helper function that we also add to this header:

inline DataBlock* toDataBlock (const Vst::DataExchangeBlock& block)
{
    if (block.blockID != Vst::InvalidDataExchangeBlockID)
        return reinterpret_cast<DataBlock*> (block.data);
    return nullptr;
}

One thing remains to be done before we can start sending the data: We need a member variable of the Vst::DataExchangeBlock struct where we store the actual block we work with while processing the audio. So we add this to our processor definition:

class DataExchangeProcessor : public Vst::AudioEffect
{
private:
    Vst::DataExchangeBlock currentExchangeBlock {InvalidDataExchangeBlock};
}

Now let's start to implement the processing:

tresult PLUGIN_API DataExchangeProcessor::process (Vst::ProcessData& processData)
{
    if (processData.numSamples <= 0)
        return kResultTrue;
    return kResultTrue;
}

When there are no samples in the processData we jump out of the method directly.

Now the first thing we need to do is to acquire a new block from the dataExchange object:

tresult PLUGIN_API DataExchangeProcessor::process (Vst::ProcessData& processData)
{
    // ...
    if (currentExchangeBlock.blockID == Vst::InvalidDataExchangeBlockID)
        acquireNewExchangeBlock ();
    // ...
}

We only want to acquire a new block if we have not already done so. Therefore, we need to check this before we call the acquireNewExchangeBlock method that does this:

void DataExchangeProcessor::acquireNewExchangeBlock ()
{
    currentExchangeBlock = dataExchange->getCurrentOrNewBlock ();
    if (auto block = toDataBlock (currentExchangeBlock))
    {
        block->sampleRate = static_cast<uint32_t> (processSetup.sampleRate);
        block->numChannels = numChannels;
        block->sampleSize = sizeof (float);
        block->numSamples = 0;
    }
}

We ask the dataExchange object for a new block with getCurrentOrNewBlock (), check if it is valid with a call to the previously defined function toDataBlock and fill it with the sample rate, the sample size and the number of channels.

Now back to our process function. We now write the samples from the input buffer into our block until the block is filled with 1 second of audio data:

tresult PLUGIN_API DataExchangeProcessor::process (Vst::ProcessData& processData)
{
    // ...
    auto input = processData.inputs[0];
    auto output = processData.outputs[0];

    if (auto block = toDataBlock (currentExchangeBlock))
    {
        auto numSamples = static_cast<uint32> (processData.numSamples);
        while (numSamples > 0)
        {
            uint32 numSamplesFreeInBlock = block->sampleRate - block->numSamples;
            uint32 numSamplesToCopy = std::min<uint32> (numSamplesFreeInBlock, numSamples);
            for (auto channel = 0; channel < input.numChannels; ++channel)
            {
                auto blockChannelData = &block->samples[0] + block->numSamples;
                auto inputChannel =
                    input.channelBuffers32[channel] + (processData.numSamples - numSamples);
                memcpy (blockChannelData, inputChannel, numSamplesToCopy * sizeof (float));
            }
            block->numSamples += numSamplesToCopy;
            if (block->numSamples == block->sampleRate)
            {
                dataExchange->sendCurrentBlock ();
                acquireNewExchangeBlock ();
                block = toDataBlock (currentExchangeBlock);
                if (block == nullptr)
                    break;
            }
            numSamples -= numSamplesToCopy;
        }
    }
    // ...
}

When it is filled with 1 second of audio data we send the block with dataExchange->sendCurrentBlock() and directly acquire a new block afterwards.

Finally, we need to copy back the input audio buffers to the output audio buffers. The whole method looks like this then:

tresult PLUGIN_API DataExchangeProcessor::process (Vst::ProcessData& processData)
{
    if (processData.numSamples <= 0)
        return kResultTrue;

    if (currentExchangeBlock.blockID == Vst::InvalidDataExchangeBlockID)
        acquireNewExchangeBlock ();

    auto input = processData.inputs[0];
    auto output = processData.outputs[0];

    if (auto block = toDataBlock (currentExchangeBlock))
    {
        auto numSamples = static_cast<uint32> (processData.numSamples);
        while (numSamples > 0)
        {
            uint32 numSamplesFreeInBlock = block->sampleRate - block->numSamples;
            uint32 numSamplesToCopy = std::min<uint32> (numSamplesFreeInBlock, numSamples);
            for (auto channel = 0; channel < input.numChannels; ++channel)
            {
                auto blockChannelData = &block->samples[0] + block->numSamples;
                auto inputChannel =
                    input.channelBuffers32[channel] + (processData.numSamples - numSamples);
                memcpy (blockChannelData, inputChannel, numSamplesToCopy * sizeof (float));
            }
            block->numSamples += numSamplesToCopy;
            if (block->numSamples == block->sampleRate)
            {
                dataExchange->sendCurrentBlock ();
                acquireNewExchangeBlock ();
                block = toDataBlock (currentExchangeBlock);
                if (block == nullptr)
                    break;
            }
            numSamples -= numSamplesToCopy;
        }
    }
    for (auto channel = 0; channel < input.numChannels; ++channel)
    {
        if (output.channelBuffers32[channel] != input.channelBuffers32[channel])
        {
            memcpy (output.channelBuffers32[channel], input.channelBuffers32[channel],
                    processData.numSamples * sizeof (float));
        }
        output.silenceFlags = input.silenceFlags;
    }

    return kResultOk;
}

Receiving data in the edit controller

We prepare our edit controller by adding the inheritance of Vst::IDataExchangeReceiver and by providing a few methods and adding a new Vst::DataExchangeReceiverHandler member:

class DataExchangeController : public Vst::EditControllerEx1,
                               public Vst::IDataExchangeReceiver
{
public:
    // ...
    tresult PLUGIN_API notify (Vst::IMessage* message) override;
    void PLUGIN_API queueOpened (Vst::DataExchangeUserContextID userContextID,
                                 uint32 blockSize,
                                 TBool& dispatchOnBackgroundThread) override;
    void PLUGIN_API queueClosed (Vst::DataExchangeUserContextID userContextID) override;
    void PLUGIN_API onDataExchangeBlocksReceived (Vst::DataExchangeUserContextID userContextID, 
                                                  uint32 numBlocks,
                                                  Vst::DataExchangeBlock* blocks, 
                                                  TBool onBackgroundThread) override;

    DEFINE_INTERFACES
        DEF_INTERFACE (Vst::IDataExchangeReceiver)
    END_DEFINE_INTERFACES (EditController)
    DELEGATE_REFCOUNT (EditController)
private:
    Vst::DataExchangeReceiverHandler dataExchange {this};
}

First, we need to forward messages to the DataExchangeReceiverHandler so that it can process the data exchange messages in case the host does not support the native API:

tresult PLUGIN_API DataExchangeController::notify (Vst::IMessage* message)
{
    if (dataExchange.onMessage (message))
        return kResultTrue;
    return EditControllerEx1::notify (message);
}

In the next step, we can implement the IDataExchangeReceiver methods:

void PLUGIN_API DataExchangeController::queueOpened (Vst::DataExchangeUserContextID userContextID,
                                                     uint32 blockSize,
                                                     TBool& dispatchOnBackgroundThread)
{
    FDebugPrint ("Data Exchange Queue opened.\n");
}

void PLUGIN_API DataExchangeController::queueClosed (Vst::DataExchangeUserContextID userContextID)
{
    FDebugPrint ("Data Exchange Queue closed.\n");
}

void PLUGIN_API DataExchangeController::onDataExchangeBlocksReceived (
    Vst::DataExchangeUserContextID userContextID, uint32 numBlocks, Vst::DataExchangeBlock* blocks,
    TBool onBackgroundThread)
{
    for (auto index = 0u; index < numBlocks; ++index)
    {
        auto dataBlock = toDataBlock (blocks[index]);
        FDebugPrint (
            "Received Data Block: SampleRate: %d, SampleSize: %d, NumChannels: %d, NumSamples: %d\n",
            dataBlock->sampleRate, static_cast<uint32_t> (dataBlock->sampleSize),
            static_cast<uint32_t> (dataBlock->numChannels),
            static_cast<uint32_t> (dataBlock->numSamples));
    }
}

The onDataExchangeBlocksReceived() method will be called each time the processor has sent a block. You can now do whatever you want with the data.

If you want the data to be dispatched on a background thread you need to set the dispatchOnBackgroundThread variable to true in the queueOpened method.