OpenViBE Documentation

Implementing a signal processing algorithm

This section of the tutorial presents a detailed implementation of the signal processing algorithm. It is made of two distinct parts : the Algorithm definition (c++ header file) and the Algorithm implementation (c++ cpp file).

Algorithm definition

Here is the file containing the algorithm definition, we will detail each line of the file later on.

#ifndef __OpenViBEPlugins_Algorithm_SignalProcessingAlgorithm_H__
#define __OpenViBEPlugins_Algorithm_SignalProcessingAlgorithm_H__

#include "../../ovp_defines.h"
#include <openvibe/ov_all.h>
#include <openvibe-toolkit/ovtk_all.h>

#define OVP_ClassId_Algorithm_SignalProcessingAlgorithm     = OpenViBE::CIdentifier(0x3B7523EC, 0x16C30A39)
#define OVP_ClassId_Algorithm_SignalProcessingAlgorithmDesc = OpenViBE::CIdentifier(0x11BE2168, 0x5B494BBB)

#define OVP_Algorithm_SignalProcessingAlgorithm_InputParameterId_Matrix  = OpenViBE::CIdentifier(0x56154223, 0x42180588)
#define OVP_Algorithm_SignalProcessingAlgorithm_OutputParameterId_Matrix = OpenViBE::CIdentifier(0x023A4450, 0x6DFD17DB)

#define OVP_Algorithm_SignalProcessingAlgorithm_InputTriggerId_Initialize  = OpenViBE::CIdentifier(0x01803B07, 0x667A69BC)
#define OVP_Algorithm_SignalProcessingAlgorithm_InputTriggerId_Process     = OpenViBE::CIdentifier(0x661A59C0, 0x12FB7F74)
#define OVP_Algorithm_SignalProcessingAlgorithm_InputTriggerId_ProcessDone = OpenViBE::CIdentifier(0x33802521, 0x785D51FD)


namespace OpenViBEPlugins
{
        namespace Samples
        {
                class CAlgorithmSignalProcessingAlgorithm : public OpenViBEToolkit::TAlgorithm < OpenViBE::Plugins::IAlgorithm >
                {
                public:

                        virtual void release(void) { delete this; }

                        virtual OpenViBE::boolean initialize(void);
                        virtual OpenViBE::boolean uninitialize(void);
                        virtual OpenViBE::boolean process(void);

                        _IsDerivedFromClass_Final_(OpenViBEToolkit::TAlgorithm < OpenViBE::Plugins::IAlgorithm >, OVP_ClassId_Algorithm_SignalProcessingAlgorithm);

                protected:

                        OpenViBE::Kernel::TParameterHandler < OpenViBE::IMatrix* > ip_pMatrix;
                        OpenViBE::Kernel::TParameterHandler < OpenViBE::IMatrix* > op_pMatrix;
                };

                class CAlgorithmSignalProcessingAlgorithmDesc : public OpenViBE::Plugins::IAlgorithmDesc
                {
                public:

                        virtual void release(void) { }

                        virtual OpenViBE::CString getName(void) const                { return OpenViBE::CString("Signal processing algorithm"); }
                        virtual OpenViBE::CString getAuthorName(void) const          { return OpenViBE::CString("Yann Renard"); }
                        virtual OpenViBE::CString getAuthorCompanyName(void) const   { return OpenViBE::CString("INRIA"); }
                        virtual OpenViBE::CString getShortDescription(void) const    { return OpenViBE::CString("This is a sample signal processing algorithm that does nothing but can be used as a quick start for creating new signal processing algorithms"); }
                        virtual OpenViBE::CString getDetailedDescription(void) const { return OpenViBE::CString(""); }
                        virtual OpenViBE::CString getCategory(void) const            { return OpenViBE::CString("Samples"); }
                        virtual OpenViBE::CString getVersion(void) const             { return OpenViBE::CString("1.0"); }
                        virtual OpenViBE::CString getStockItemName(void) const       { return OpenViBE::CString("gtk-execute"); }

                        virtual OpenViBE::CIdentifier getCreatedClass(void) const    { return OVP_ClassId_Algorithm_SignalProcessingAlgorithm; }
                        virtual OpenViBE::Plugins::IPluginObject* create(void)       { return new OpenViBEPlugins::Samples::CAlgorithmSignalProcessingAlgorithm; }

                        virtual OpenViBE::boolean getAlgorithmPrototype(
                                OpenViBE::Kernel::IAlgorithmProto& rAlgorithmPrototype) const
                        {
                                rAlgorithmPrototype.addInputParameter (OVP_Algorithm_SignalProcessingAlgorithm_InputParameterId_Matrix,     "Matrix", OpenViBE::Kernel::ParameterType_Matrix);
                                rAlgorithmPrototype.addOutputParameter(OVP_Algorithm_SignalProcessingAlgorithm_OutputParameterId_Matrix,    "Matrix", OpenViBE::Kernel::ParameterType_Matrix);

                                rAlgorithmPrototype.addInputTrigger   (OVP_Algorithm_SignalProcessingAlgorithm_InputTriggerId_Initialize,   "Initialize");
                                rAlgorithmPrototype.addInputTrigger   (OVP_Algorithm_SignalProcessingAlgorithm_InputTriggerId_Process,      "Process");
                                rAlgorithmPrototype.addOutputTrigger  (OVP_Algorithm_SignalProcessingAlgorithm_OutputTriggerId_ProcessDone, "Process done");

                                return true;
                        }

                        _IsDerivedFromClass_Final_(OpenViBE::Plugins::IAlgorithmDesc, OVP_ClassId_Algorithm_SignalProcessingAlgorithmDesc);
                };
        };
};

#endif // __OpenViBEPlugins_Algorithm_SignalProcessingAlgorithm_H__

First of all, we will include every identifier / define needed for this plugin to work. The OpenViBE toolkit will help in the implementation so we include it as well.

#include "../../ovp_defines.h"
#include <openvibe/ov_all.h>
#include <openvibe-toolkit/ovtk_all.h>

The next part defines all the unique CIdentifier objects that identify the algorithm, the algorithm descriptor, and all the inputs, outputs and triggers. These random identifiers can be generated randomly using the dedicated tool id-generator, provided by the OpenViBE software platform. Please be sure to use unique identifiers.

#define OVP_ClassId_Algorithm_SignalProcessingAlgorithm     = OpenViBE::CIdentifier(0x3B7523EC, 0x16C30A39)
#define OVP_ClassId_Algorithm_SignalProcessingAlgorithmDesc = OpenViBE::CIdentifier(0x11BE2168, 0x5B494BBB)

#define OVP_Algorithm_SignalProcessingAlgorithm_InputParameterId_Matrix  = OpenViBE::CIdentifier(0x56154223, 0x42180588)
#define OVP_Algorithm_SignalProcessingAlgorithm_OutputParameterId_Matrix = OpenViBE::CIdentifier(0x023A4450, 0x6DFD17DB)

#define OVP_Algorithm_SignalProcessingAlgorithm_InputTriggerId_Initialize  = OpenViBE::CIdentifier(0x01803B07, 0x667A69BC)
#define OVP_Algorithm_SignalProcessingAlgorithm_InputTriggerId_Process     = OpenViBE::CIdentifier(0x661A59C0, 0x12FB7F74)
#define OVP_Algorithm_SignalProcessingAlgorithm_InputTriggerId_ProcessDone = OpenViBE::CIdentifier(0x33802521, 0x785D51FD)

In order to avoid name collisions, it is a safe practice to include all classes in a namespace. All standard OpenViBE plugins are defined in sub namespaces of OpenViBEPlugins. Here we are going to build a new box for example purposes, so we narrow down the scope to the Samples sub namespace.


namespace OpenViBEPlugins
{
        namespace Samples
        {

Now that we are in the OpenViBEPlugins::Samples namespace, we have to define the classes making up the algorithm :

  • the algorithm itself, and
  • its associated descriptor.

The descriptor is retrieved by the kernel upon startup. It provides information about the algorithm and also allows to create instances of this algorithm.

We start by declaring the algorithm class.

First off, we must choose an appropriate class name. Since this example algorithm is meant to be a general signal processing example, we use a general name. In practice, the name of an algorithm should always convey a rough idea of what kind of signal processing it performs.

The interface common to all algorithms is IAlgorithm. Our algorithm must therefore inherit from it to be identified as a proper algorithm. However, algorithms implementation is made easier by the OpenViBEToolkit::TAlgorithm template, which implements several functionalities common to all algorithms (e.g. direct access to managers). Therefore, we inherit from this template, using the required IAlgorithm interface as the template argument.

The first method to implement is release, which deletes the algorithm.

                class CAlgorithmSignalProcessingAlgorithm : public OpenViBEToolkit::TAlgorithm < OpenViBE::Plugins::IAlgorithm >
                {
                public:

                        virtual void release(void) { delete this; }

The gist of an algorithm implementation lies in three methods : initialize, uninitialize and process. Here is the life cycle of an algorithm :

  • the algorithm is created (using the algorithm descriptor)
  • initialize is called once
  • process is called an indefinite number of times
  • uninitialize is called once
  • the algorithm is deleted

Each of these methods returns a boolean reflecting whether they operated successfully. If any of them returns false, the caller should assume the algorithm failed and should stop calling this algorithm any further.

                        virtual OpenViBE::boolean initialize(void);
                        virtual OpenViBE::boolean uninitialize(void);
                        virtual OpenViBE::boolean process(void);

All OpenViBE classes have their own class identifier. Once a new identifier has been generated, one should associate it to the algorithm class using the _IsDerivedFromClass_Final_ macro. The first argument of the macro is the name of the parent class, the second is the algorithm identifier.

                        _IsDerivedFromClass_Final_(OpenViBEToolkit::TAlgorithm < OpenViBE::Plugins::IAlgorithm >, OVP_ClassId_Algorithm_SignalProcessingAlgorithm);

                protected:

In this simple example, we are going to design an algorithm that takes a single input parameter (a matrix) and produces a single output parameter (another matrix).

In order to facilitate parameters usage, the algorithm should embed its parameters in parameter handlers. Parameter handlers provide transparent access to embedded parameters using an interface similar to the parameter interface.

The algorithm definition is now complete, and we move on to its descriptor. The algorithm descriptor provides information as to the role of its associated algorithm, its author, version, category and so on.

The interface common to all algorithm descriptors is IAlgorithmDesc, which we inherit from.

The release function can be left empty since a single instance of any given descriptor is created by the plugin which contains it, at plugin loading time, and eventually destroyed by the plugin when it is unloaded.

A number of methods return description strings which can be used to get a general idea of what the algorithm consists in.

                class CAlgorithmSignalProcessingAlgorithmDesc : public OpenViBE::Plugins::IAlgorithmDesc
                {
                public:

                        virtual void release(void) { }

                        virtual OpenViBE::CString getName(void) const                { return OpenViBE::CString("Signal processing algorithm"); }
                        virtual OpenViBE::CString getAuthorName(void) const          { return OpenViBE::CString("Yann Renard"); }
                        virtual OpenViBE::CString getAuthorCompanyName(void) const   { return OpenViBE::CString("INRIA"); }
                        virtual OpenViBE::CString getShortDescription(void) const    { return OpenViBE::CString("This is a sample signal processing algorithm that does nothing but can be used as a quick start for creating new signal processing algorithms"); }
                        virtual OpenViBE::CString getDetailedDescription(void) const { return OpenViBE::CString(""); }
                        virtual OpenViBE::CString getCategory(void) const            { return OpenViBE::CString("Samples"); }
                        virtual OpenViBE::CString getVersion(void) const             { return OpenViBE::CString("1.0"); }
                        virtual OpenViBE::CString getStockItemName(void) const       { return OpenViBE::CString("gtk-execute"); }

The descriptor specifies what kind of algorithm it is able to create thanks to the getCreatedClass method, which returns the name of the algorithm class. Actual instances of the algorithm may be created using the create method.

                        virtual OpenViBE::CIdentifier getCreatedClass(void) const    { return OVP_ClassId_Algorithm_SignalProcessingAlgorithm; }
                        virtual OpenViBE::Plugins::IPluginObject* create(void)       { return new OpenViBEPlugins::Samples::CAlgorithmSignalProcessingAlgorithm; }

The algorithm prototype may be retrieved using getAlgorithmPrototype. The prototype gives information about inputs, outputs and triggers.

Inputs and outputs are parameters and can easily be manipulated with parameter handlers. Triggers are related to the algorithm state : input triggers specify how the algorithm should execute when called, while output triggers give an indication of what state the algorithm ended in when done with processing.

                        virtual OpenViBE::boolean getAlgorithmPrototype(
                                OpenViBE::Kernel::IAlgorithmProto& rAlgorithmPrototype) const
                        {

This signal processing algorithm has two parameters : one input containing the signal matrix to process and one output containing the processed signal. Each of these parameters is identified by an identifier, a name and a type.

                                rAlgorithmPrototype.addInputParameter (OVP_Algorithm_SignalProcessingAlgorithm_InputParameterId_Matrix,     "Matrix", OpenViBE::Kernel::ParameterType_Matrix);
                                rAlgorithmPrototype.addOutputParameter(OVP_Algorithm_SignalProcessingAlgorithm_OutputParameterId_Matrix,    "Matrix", OpenViBE::Kernel::ParameterType_Matrix);

This signal processing algorithm also has three triggers.

The first one is an input trigger and is used to notify the algorithm that the initialization step could be performed. This initialization is different from the one done in initialize : when the latter method is called, the algorithm itself is initialized, whereas the former method initializes data the algorithm will process later on. Preconditions that must be validated before activating this trigger include :

  • The algorithm initialize method returned successfully
  • The input matrix description was filled

The second trigger also is an input trigger. When set, it requests the actual processing of the input matrix and results in the production of an output matrix.

The last trigger is an output trigger used to tell the caller about the outcome of the signal processing stage. When and only when an output matrix is successfully produced, the algorithm activates this output trigger.

                                rAlgorithmPrototype.addInputTrigger   (OVP_Algorithm_SignalProcessingAlgorithm_InputTriggerId_Initialize,   "Initialize");
                                rAlgorithmPrototype.addInputTrigger   (OVP_Algorithm_SignalProcessingAlgorithm_InputTriggerId_Process,      "Process");
                                rAlgorithmPrototype.addOutputTrigger  (OVP_Algorithm_SignalProcessingAlgorithm_OutputTriggerId_ProcessDone, "Process done");

Finally, the method returns true, notifying the caller (the kernel) that the box prototype was successfully retrieved.

                                return true;
                        }

Similarly to how we associated an identifier to the algorithm class, we specify the identifier of the algorithm descriptor using the _IsDerivedFromClass_Final_ macro. The first argument is algorithm descriptor's parent interface and the second is the actual descriptor identifier.

                        _IsDerivedFromClass_Final_(OpenViBE::Plugins::IAlgorithmDesc, OVP_ClassId_Algorithm_SignalProcessingAlgorithmDesc);
                };
        };
};

Algorithm implementation

Here is the file containing the algorithm implementation, we will detail each line of the file later on.

#include "ovpCAlgorithmSignalProcessingAlgorithm.h"

using namespace OpenViBE;
using namespace OpenViBE::Kernel;
using namespace OpenViBE::Plugins;

using namespace OpenViBEPlugins;
using namespace OpenViBEPlugins::Samples;

boolean CAlgorithmSignalProcessingAlgorithm::initialize(void)
{
        ip_pMatrix.initialize(this->getInputParameter(OVP_Algorithm_SignalProcessingAlgorithm_InputParameterId_Matrix));
        op_pMatrix.initialize(this->getOutputParameter(OVP_Algorithm_SignalProcessingAlgorithm_OutputParameterId_Matrix));
        return true;
}

boolean CAlgorithmSignalProcessingAlgorithm::uninitialize(void)
{
        op_pMatrix.uninitialize();
        ip_pMatrix.uninitialize();
        return true;
}

boolean CAlgorithmSignalProcessingAlgorithm::process(void)
{
        IMatrix* l_pInputMatrix=ip_pMatrix;
        IMatrix* l_pOutputMatrix=op_pMatrix;

        if(this->isInputTriggerActive(OVP_Algorithm_SignalProcessingAlgorithm_InputTriggerId_Initialize))
        {
                OpenViBEToolkit::Tools::Matrix::copyDescription(*l_pOutputMatrix, *l_pInputMatrix);
        }

        if(this->isInputTriggerActive(OVP_Algorithm_SignalProcessingAlgorithm_InputTriggerId_Process))
        {
                OpenViBEToolkit::Tools::Matrix::copyContent(*l_pOutputMatrix, *l_pInputMatrix);
                for(uint32 i=0; i<l_pInputMatrix->getDimensionSize(0); i++)
                {
                        l_pOutputMatrix->getBuffer()[i*l_pInputMatrix->getDimensionSize(1)]=0;
                }

                this->activateOutputTrigger(OVP_Algorithm_SignalProcessingAlgorithm_OutputTriggerId_ProcessDone, true);
        }

        return true;
}

First off, for easier development, it is common practice to declare using several common OpenViBE namespaces at the beginning of the file, after including the required header files. A good rule of thumb is to never use "using" directives in header files, and to use them sparsely in implementation files. Considering OpenViBE does not contain conflicting classes (no two classes are named identically among all its files), it is acceptable to declare using OpenViBE namespaces.

#include "ovpCAlgorithmSignalProcessingAlgorithm.h"

using namespace OpenViBE;
using namespace OpenViBE::Kernel;
using namespace OpenViBE::Plugins;

Similarly, the OpenViBEPlugins project does not contain conflicting classes and makes it acceptable to declare using some of its namespaces.

using namespace OpenViBEPlugins;
using namespace OpenViBEPlugins::Samples;

On to the real meat of the implementation : there are three algorithm methods two implement,and the good news is that two of them are simple :)

In this example, initialize and uninitialize simply consist in connecting or disconnecting our parameter handlers to the actual parameters. These handlers will be used later to ease parameter value manipulation.

Let's initialize our parameters.

boolean CAlgorithmSignalProcessingAlgorithm::initialize(void)
{

ip_pMatrix is the input matrix pointer used by this algorithm. This parameter was declared in the algorithm descriptor as being of type OV_TypeId_Matrix and identified as OVP_Algorithm_SignalProcessingAlgorithm_InputParameterId_Matrix. The parameter can be retrieved with the getInputParameter function and passed to the handler using its initialize method.

Past this point, ip_pMatrix can be used as the object it encapsulates, that is, as an IMatrix* pointer using the -> operator.

        ip_pMatrix.initialize(this->getInputParameter(OVP_Algorithm_SignalProcessingAlgorithm_InputParameterId_Matrix));

Similarly, op_pMatrix is the output matrix pointer of this algorithm. The parameter can be retrieved with the getOutputParameter function and given to the handler. Past this point, ip_pMatrix can be used as an IMatrix* pointer using the -> operator.

        op_pMatrix.initialize(this->getOutputParameter(OVP_Algorithm_SignalProcessingAlgorithm_OutputParameterId_Matrix));

Returning true at the end of this initialize function tells the kernel everything went on nicely and no error occurred. If false is returned here, the kernel will consider this algorithm failed to initialize and won't call it anymore in the future.

        return true;
}

Uninitialize should be called to terminate an algorithm which won't be used anymore. Thus, every initialized member should be freed and the whole environment should be left as it was before initialize was called. In this simple example, all that is needed is to disconnect our parameter handlers from their respective parameters.

boolean CAlgorithmSignalProcessingAlgorithm::uninitialize(void)
{

op_pMatrix can be disconnected from its parameter thanks to the uninitialize method. Past this point, using operator -> will throw an exception and cause a crash.

        op_pMatrix.uninitialize();

The same can be done with ip_pMatrix.

        ip_pMatrix.uninitialize();

As in initialize, returning true notifies the kernel everything ran smoothly.

        return true;
}

Now all that is left to implement is the core of the algorithm, the process method which does the actual signal processing. This method is called each time operations should be performed by this algorithm.

What operations the algorithm should execute may be specified using input triggers. Every time process is called, the algorithm should check the state of its input triggers and deduce what computations to perform. At the end of the processing, the algorithm can reflect its status by setting the appropriate output triggers.

boolean CAlgorithmSignalProcessingAlgorithm::process(void)
{

First of all, we will retrieve input and output matrix pointers from the parameter handlers. Since our handler instances were templated with IMatrix*, there is an automatic cast operator that returns a pointer to the actual IMatrix* value.

        IMatrix* l_pInputMatrix=ip_pMatrix;
        IMatrix* l_pOutputMatrix=op_pMatrix;

Now, we have to examine each of the input triggers to know what to process. Those input triggers are declared in the algorithm prototype. In this case, there are two of them, namely OVP_Algorithm_SignalProcessingAlgorithm_InputTriggerId_Initialize and OVP_Algorithm_SignalProcessingAlgorithm_InputTriggerId_Process. Let's check whether the first one is activated

        if(this->isInputTriggerActive(OVP_Algorithm_SignalProcessingAlgorithm_InputTriggerId_Initialize))
        {

When the initialize trigger is active, we know that the input matrix contains a complete description of the input matrices that will be forwarded to this algorithm (number of dimensions, dimension sizes, labels...).

Since our processing function only changes some matrix values, the description of its output matrix is identical to that of its input matrix. Therefore, we copy it using the copyDescription helper function of the toolkit. Past this point, the output matrix will have the same aspect as the input matrix. However, its contents are undefined for now.

                OpenViBEToolkit::Tools::Matrix::copyDescription(*l_pOutputMatrix, *l_pInputMatrix);
        }

Let's now test whether the process input trigger is set.

        if(this->isInputTriggerActive(OVP_Algorithm_SignalProcessingAlgorithm_InputTriggerId_Process))
        {

When this particular trigger is active, we know that the input matrix is completely filled, including its buffer (the part of the matrix that contains values). This is where the actual processing can take place. As an example, we demonstrate how to copy the input signal buffer, slightly modifying it by changing only the first sample of each channel by setting it to 0.

Input matrices are assumed to be two dimensional, and the samples of a given channel are listed in a row before moving on to the next channel.

  • the first dimension (index 0) of the matrix represents the number of channels
  • the second dimension (index 1) is made up of the samples of any given channel

The getBuffer method returns a pointer to the first value of the matrix, that is, the first sample of the first channel. To access the jth sample of the ith channel, one must offset this pointer by the following formula :

i * sampleCount + j

where sampleCount is the number of samples per channel, given by the size of the second dimension (index 1).

In this example, we iterate over channels (i) and set their first sample (j == 0) to 0.

                OpenViBEToolkit::Tools::Matrix::copyContent(*l_pOutputMatrix, *l_pInputMatrix);
                for(uint32 i=0; i<l_pInputMatrix->getDimensionSize(0); i++)
                {
                        l_pOutputMatrix->getBuffer()[i*l_pInputMatrix->getDimensionSize(1)]=0;
                }

Now that the processing is done, this can be reflected to the caller by activating the "process done" output trigger, whose state will be checked by the caller. Therefore, the OVP_Algorithm_SignalProcessingAlgorithm_OutputTriggerId_ProcessDone output trigger is activated.

                this->activateOutputTrigger(OVP_Algorithm_SignalProcessingAlgorithm_OutputTriggerId_ProcessDone, true);
        }

Finally, we should return true to notify the kernel that no error occurred during this processing step. If false is returned, the kernel won't ever call this algorithm anymore.

        return true;
}

Now please go on to the final step : Implementing a Box