Tutorial 2: Implementing an algorithm and using it in boxes

  • NB: Document updated for OpenViBE 3.3.0 (27.Apr.2022).

Introduction

This tutorial will cover the implementation of Algorithms, the generic processing objects that can be embedded in boxes.

The objectives are to:

  • Allow more than one box to use the same processing based on a generic component
  • Simplify the code of boxes by dividing the complexity between algorithms and box

This tutorial consists in:

  1. Implementing a matrix processing algorithm that computes the maximum vector of a 2-dimensional matrix. The i-th element of the output vector will be the maximum value of the i-th column of the matrix.
  2. Embbed this algorithm in  a data-driven box.
  3. Test the box on a multi channel signal to output the maximum channel

The algorithm and the box will be placed in the Signal Processing project, respectively under extras/contrib/plugins/processing/signal-processing/src/algorithms and extras/contrib/plugins/processing/signal-processing/src/box-algorithms.

Design

As usual, it’s better to think about the design we want to achieve before diving into the code. We will need an algorithm with:

  • 1 input : the matrix with 2 dimensions (we will need to check that)
  • 1 output : the matrix with only one vector
  • 2 input triggers : “Initialize” and “Process”
  • 1 output trigger : “Process Done”

The Box will be very simple as box implementation has been covered in a previous tutorial.

  • 1 input : signal
  • 1 output : signal
  • No settings

The Matrix Maximum Algorithm

The algorithm is made of 2 files, the header and the implementation.

The skeleton-generator is not currently capable of creating algorithm skeletons.

Define the CIdentifiers

The Algorithm and its inputs, outputs and triggers are all identified by a unique CIdentifier. The developer tool id-generator is capable of generating random CIdentifiers for you to copy-paste in your code.

To use an algorithm, the box just needs to know its identifiers. That’s all ! You will do everything using the AlgorithmProxy or ParameterHandler generic classes, and you don’t need the algorithm specification.

We will need 7 identifiers, for:

  • Algorithm
  • Algorithm Descriptor
  • Matrix Input
  • Matrix Output
  • “Initialize” input trigger
  • “Process” input trigger
  • “Process Done” output trigger

If we want to be able to use the algorithm in any other openvibe box (in any plugin subproject) easily, we can declare all the identifiers in the global define file of the project ovp_defines.h.

Doing so can simplify the implementation of boxes relying on algorithms, as they just need to include the ovp_defines.h file in order to be able to use any algorithm whose identifiers are declared in it.

Of course, it’s also possible to declare the CIdentifiers directly in the Algorithm header, but every box using your algorithm will then have to include the algorithm’s header.

Here are the CIdentifier used by our algorithm; they are defined in ovp_defines.h:

...

#define OVP_ClassId_Algorithm_MatrixMaximum                     OpenViBE::CIdentifier(0x3B7723EC, 0x16C30A39)
#define OVP_ClassId_Algorithm_MatrixMaximumDesc                 OpenViBE::CIdentifier(0x11BE2168, 0x5B444BBB)
#define OVP_Algorithm_MatrixMaximum_InputParameterId_Matrix     OpenViBE::CIdentifier(0x56254223, 0x42180588)
#define OVP_Algorithm_MatrixMaximum_OutputParameterId_Matrix    OpenViBE::CIdentifier(0x025A4450, 0x6DFD17DB)
#define OVP_Algorithm_MatrixMaximum_InputTriggerId_Initialize   OpenViBE::CIdentifier(0x41803B07, 0x667A69BC)
#define OVP_Algorithm_MatrixMaximum_InputTriggerId_Process      OpenViBE::CIdentifier(0x641A59C0, 0x12FB7F74)
#define OVP_Algorithm_MatrixMaximum_OutputTriggerId_ProcessDone OpenViBE::CIdentifier(0x37802521, 0x785D51FD)

...

Header

 File: algorithms/ovpCAlgorithmMatrixMaximum.h(click here to show content)
 File: algorithms/ovpCAlgorithmMatrixMaximum.h(click here to hide content)

#pragma once

#include "../ovp_defines.h"
#include <toolkit/ovtk_all.h>

namespace OpenViBE
{
    namespace Plugins
    {
        namespace SignalProcessing
        {
            class CAlgorithmMatrixMaximum : public Toolkit::TAlgorithm<IAlgorithm>
            {
                public:
                    virtual void release(void) { delete this; }

                    virtual bool initialize(void);
                    virtual bool uninitialize(void);
                    virtual bool process(void);

                    _IsDerivedFromClass_Final_(Toolkit::TAlgorithm<IAlgorithm>, OVP_ClassId_Algorithm_MatrixMaximum);

                protected:
                    OpenViBE::Kernel::TParameterHandler < OpenViBE::IMatrix* > ip_pMatrix; // input matrix
                    OpenViBE::Kernel::TParameterHandler < OpenViBE::IMatrix* > op_pMatrix; // output matrix
            };

            class CAlgorithmMatrixMaximumDesc : public OpenViBE::Plugins::IAlgorithmDesc
            {
                public:
                    virtual void release(void) { }

                    virtual OpenViBE::CString getName(void) const { return OpenViBE::CString("Signal maximum algorithm"); }
                    virtual OpenViBE::CString getAuthorName(void) const { return OpenViBE::CString("Laurent Bonnet"); }
                    virtual OpenViBE::CString getAuthorCompanyName(void) const { return OpenViBE::CString("INRIA"); }
                    virtual OpenViBE::CString getShortDescription(void) const { return OpenViBE::CString("Computes the maximum vector of a 2-dimension matrix"); }
                    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_MatrixMaximum; }
                    virtual IPluginObject* create(void) { return new CAlgorithmMatrixMaximum(); }

                    virtual bool getAlgorithmPrototype(Kernel::IAlgorithmProto& rAlgorithmPrototype) const
                    {
                        rAlgorithmPrototype.addInputParameter(OVP_Algorithm_MatrixMaximum_InputParameterId_Matrix, "Matrix", Kernel::ParameterType_Matrix);
                        rAlgorithmPrototype.addOutputParameter(OVP_Algorithm_MatrixMaximum_OutputParameterId_Matrix, "Matrix", Kernel::ParameterType_Matrix);

                        rAlgorithmPrototype.addInputTrigger(OVP_Algorithm_MatrixMaximum_InputTriggerId_Initialize, "Initialize");
                        rAlgorithmPrototype.addInputTrigger(OVP_Algorithm_MatrixMaximum_InputTriggerId_Process, "Process");
                        rAlgorithmPrototype.addOutputTrigger(OVP_Algorithm_MatrixMaximum_OutputTriggerId_ProcessDone, "Process done");

                        return true;
                    }

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

First, we include everything we need :

  • ovp_defines.h: it’s where our identifiers are defined
  • ovtk_all.h: to be able to use algorithm proxy and other utility classes

The header declares 2 classes:

  • CAlgorithmMatrixMaximum: the algorithm, derived from the toolkit template Toolkit::TAlgorithm<IAlgorithm>
  • CAlgorithmMatrixMaximumDesc: the algorithm descriptor used by the kernel to the algorithm structure

Like in any other algorithm, 3 functions must be implemented:

  • bool initialize(void): to initialize the input and output parameters
  • bool uninitialize(void): to cleanup the i/o parameters
  • bool process(void): where the processing is actually performed and output triggers produced, according to input triggers

We need 2 parameters:

  • OpenViBE::Kernel::TParameterHandler < OpenViBE::IMatrix* > ip_pMatrix : the NxM input matrix
  • OpenViBE::Kernel::TParameterHandler < OpenViBE::IMatrix* > op_pMatrix : the output matrix with the maximum vector

The Algorithm Descriptor is relatively close to a Box Descriptor. You need to provide some generic information (author, category, description, etc.) along with the algorithm prototype in getAlgorithmPrototype.

Note that when adding new input and output to your algorithm, you must provide the corresponding unique identifier along with name and type:

rAlgorithmPrototype.addInputParameter (OVP_Algorithm_MatrixMaximum_InputParameterId_Matrix,     "Matrix",
                                       Kernel::ParameterType_Matrix);
rAlgorithmPrototype.addOutputParameter(OVP_Algorithm_MatrixMaximum_OutputParameterId_Matrix,    "Matrix",
                                       Kernel::ParameterType_Matrix);

rAlgorithmPrototype.addInputTrigger   (OVP_Algorithm_MatrixMaximum_InputTriggerId_Initialize,   "Initialize");
rAlgorithmPrototype.addInputTrigger   (OVP_Algorithm_MatrixMaximum_InputTriggerId_Process,      "Process");
rAlgorithmPrototype.addOutputTrigger  (OVP_Algorithm_MatrixMaximum_OutputTriggerId_ProcessDone, "Process done");

Implementation

 File: algorithms/ovpCAlgorithmMatrixMaximum.cpp(click here to show content)
 File: algorithms/ovpCAlgorithmMatrixMaximum.cpp(click here to hide content)

#include "ovpCAlgorithmMatrixMaximum.h"

namespace OpenViBE {
    namespace Plugins {
        namespace SignalProcessing {

            bool CAlgorithmMatrixMaximum::initialize(void)
            {
                ip_pMatrix.initialize(this->getInputParameter(OVP_Algorithm_MatrixMaximum_InputParameterId_Matrix));
                op_pMatrix.initialize(this->getOutputParameter(OVP_Algorithm_MatrixMaximum_OutputParameterId_Matrix));
                return true;
            }

            bool CAlgorithmMatrixMaximum::uninitialize(void)
            {
                op_pMatrix.uninitialize();
                ip_pMatrix.uninitialize();
                return true;
            }

            bool CAlgorithmMatrixMaximum::process(void)
            {
                if (this->isInputTriggerActive(OVP_Algorithm_MatrixMaximum_InputTriggerId_Initialize))
                {
                    if (ip_pMatrix->getDimensionCount() != 2)
                    {
                        this->getLogManager() << Kernel::LogLevel_Error << "The input matrix must have 2 dimensions";
                        return false;
                    }
                    op_pMatrix->setDimensionCount(2);
                    op_pMatrix->setDimensionSize(0, 1); // only one - maximum - vector
                    op_pMatrix->setDimensionSize(1, ip_pMatrix->getDimensionSize(1)); // same number of elements in the vector
                }

                if (this->isInputTriggerActive(OVP_Algorithm_MatrixMaximum_InputTriggerId_Process))
                {
                    // we iterate over the columns (second dimension)
                    for (uint32_t i = 0; i < ip_pMatrix->getDimensionSize(1); i++)
                    {
                        double l_f64Maximum = ip_pMatrix->getBuffer()[i];
                        // and try to find the maximum among the values on column i
                        for (uint32_t j = 1; j < ip_pMatrix->getDimensionSize(0); j++)
                        {
                            if (l_f64Maximum < ip_pMatrix->getBuffer()[i + j * ip_pMatrix->getDimensionSize(1)])
                            {
                                l_f64Maximum = ip_pMatrix->getBuffer()[i + j * ip_pMatrix->getDimensionSize(1)];
                            }
                        }
                        op_pMatrix->getBuffer()[i] = l_f64Maximum;
                    }
                    this->activateOutputTrigger(OVP_Algorithm_MatrixMaximum_OutputTriggerId_ProcessDone, true);
                }

                return true;
            }
        };
    };
};

Initialize

We simply initialize the matrix parameters to handle the input and output of the algorithm, using the corresponding identifiers:

ip_pMatrix.initialize(this->getInputParameter(OVP_Algorithm_MatrixMaximum_InputParameterId_Matrix));
op_pMatrix.initialize(this->getOutputParameter(OVP_Algorithm_MatrixMaximum_OutputParameterId_Matrix));

Uninitialize

Uninitializing the parameters is done easily using:

op_pMatrix.uninitialize();
ip_pMatrix.uninitialize();

Process

The process function is basically a switch between different behaviors, according to the input triggers.The triggers can be checked using the isInputTriggerActive(trigger_id) function.

In the case of input trigger “Initialize”, the goal is to verify that the input matrices will have 2 dimensions (NxM) and to prepare the output matrix structure (2 dimensions, 1xM):

    if(this->isInputTriggerActive(OVP_Algorithm_MatrixMaximum_InputTriggerId_Initialize))
    {
        if( ip_pMatrix->getDimensionCount() != 2)
        {
            // we return false so the box which called process knows something went wrong
            this->getLogManager() << Kernel::LogLevel_Error << "The input matrix must have 2 dimensions";
            return false;
        }
        op_pMatrix->setDimensionCount(2);
        op_pMatrix->setDimensionSize(0,1); // only one - maximum - vector
        op_pMatrix->setDimensionSize(1,ip_pMatrix->getDimensionSize(1)); // same number of elements in the vector
    }

If a “Process” has been requested, we can compute the maximum vector over the incoming matrix. When the process is finally done, we can activate the corresponding output trigger:

    if(this->isInputTriggerActive(OVP_Algorithm_MatrixMaximum_InputTriggerId_Process))
    {
        // we iterate over the columns (second dimension)
        for(uint32_t i=0; i<ip_pMatrix->getDimensionSize(1); i++)
        {
            double l_f64Maximum = ip_pMatrix->getBuffer()[i];
            // and try to find the maximum among the values on column i
            for(uint32_t j=1; j<ip_pMatrix->getDimensionSize(0); j++)
            {
                if(l_f64Maximum < ip_pMatrix->getBuffer()[i+j*ip_pMatrix->getDimensionSize(1)])
                {
                    l_f64Maximum = ip_pMatrix->getBuffer()[i+j*ip_pMatrix->getDimensionSize(1)];
                }
            }
            op_pMatrix->getBuffer()[i]= l_f64Maximum;
        }
        this->activateOutputTrigger(OVP_Algorithm_MatrixMaximum_OutputTriggerId_ProcessDone, true);
    }

The Signal Maximum Box

For detailed help on how to implement a box, see the previous tutorial.

Header

 File: box-algorithms/ovpCBoxAlgorithmSignalMaximum.h(click here to show content)
 File: box-algorithms/ovpCBoxAlgorithmSignalMaximum.h(click here to hide content)

#pragma once

#include "../ovp_defines.h"

#include <openvibe/ov_all.h>
#include <toolkit/ovtk_all.h>

#define OVP_ClassId_BoxAlgorithm_SignalMaximum OpenViBE::CIdentifier(0x6A12346A, 0x6758940)
#define OVP_ClassId_BoxAlgorithm_SignalMaximumDesc OpenViBE::CIdentifier(0x6AAEAB12, 0x67124560)

namespace OpenViBE
{
    namespace Plugins
    {
        namespace SignalProcessing
        {
            class CBoxAlgorithmSignalMaximum : virtual public Toolkit::TBoxAlgorithm < IBoxAlgorithm >
            {
                public:
                    virtual void release(void) { delete this; }

                    virtual bool initialize(void);
                    virtual bool uninitialize(void);

                    virtual bool processInput(const size_t index) override;

                    virtual bool process(void);

                    _IsDerivedFromClass_Final_(Toolkit::TBoxAlgorithm < IBoxAlgorithm >, OVP_ClassId_BoxAlgorithm_SignalMaximum);

                protected:
                    Toolkit::TSignalDecoder < CBoxAlgorithmSignalMaximum > m_oSignalDecoder;

                    OpenViBE::Kernel::IAlgorithmProxy* m_pMatrixMaximumAlgorithm;
                    OpenViBE::Kernel::TParameterHandler < OpenViBE::IMatrix* > ip_pMatrixMaximumAlgorithm_Matrix;
                    OpenViBE::Kernel::TParameterHandler < OpenViBE::IMatrix* > op_pMatrixMaximumAlgorithm_Matrix;

                    Toolkit::TSignalEncoder < CBoxAlgorithmSignalMaximum > m_oSignalEncoder;
            };

            class CBoxAlgorithmSignalMaximumDesc : virtual public IBoxAlgorithmDesc
            {
                public:

                    virtual void release(void) { }

                    virtual OpenViBE::CString getName(void) const { return OpenViBE::CString("Signal Maximum"); }
                    virtual OpenViBE::CString getAuthorName(void) const { return OpenViBE::CString("Laurent Bonnet"); }
                    virtual OpenViBE::CString getAuthorCompanyName(void) const { return OpenViBE::CString("INRIA"); }
                    virtual OpenViBE::CString getShortDescription(void) const { return OpenViBE::CString("Max channel !"); }
                    virtual OpenViBE::CString getDetailedDescription(void) const { return OpenViBE::CString("Output a one channel signal corresponding to the maximum of each input channel for each sample index."); }
                    virtual OpenViBE::CString getCategory(void) const { return OpenViBE::CString("Signal processing"); }
                    virtual OpenViBE::CString getVersion(void) const { return OpenViBE::CString("1.0"); }
                    virtual OpenViBE::CString getStockItemName(void) const { return OpenViBE::CString("gtk-add"); }

                    virtual OpenViBE::CIdentifier getCreatedClass(void) const { return OVP_ClassId_BoxAlgorithm_SignalMaximum; }
                    virtual OpenViBE::Plugins::IPluginObject* create(void) { return new CBoxAlgorithmSignalMaximum; }

                    virtual bool getBoxPrototype(Kernel::IBoxProto& rBoxAlgorithmPrototype) const
                    {
                        rBoxAlgorithmPrototype.addInput("Signal", OV_TypeId_Signal);

                        rBoxAlgorithmPrototype.addOutput("Processed Signal", OV_TypeId_Signal);

                        rBoxAlgorithmPrototype.addFlag(OV_AttributeId_Box_FlagIsUnstable);

                        return true;

                    }
                    _IsDerivedFromClass_Final_(IBoxAlgorithmDesc, OVP_ClassId_BoxAlgorithm_SignalMaximumDesc);
            };
        };
    };
};

The interesting point here is the cohabitation of the algorithms provided by the codec toolkit and our new signal processing algorithm:

Toolkit::TSignalDecoder < CBoxAlgorithmSignalMaximum > m_oSignalDecoder;

OpenViBE::Kernel::IAlgorithmProxy* m_pMatrixMaximumAlgorithm;
OpenViBE::Kernel::TParameterHandler < OpenViBE::IMatrix* > ip_pMatrixMaximumAlgorithm_Matrix;
OpenViBE::Kernel::TParameterHandler < OpenViBE::IMatrix* > op_pMatrixMaximumAlgorithm_Matrix;

Toolkit::TSignalEncoder < CBoxAlgorithmSignalMaximum > m_oSignalEncoder;

Without using the codec toolkit, this is what your encoders and decoders would also look like : an algorithm proxy, and the input and outputs parameter handlers.

Implementation

 File: box-algorithms/ovpCBoxAlgorithmSignalMaximum.cpp(click here to show content)
 File: box-algorithms/ovpCBoxAlgorithmSignalMaximum.cpp(click here to hide content)

#include "ovpCBoxAlgorithmSignalMaximum.h"

using namespace OpenViBE;
using namespace Kernel;
using namespace Plugins;
using namespace SignalProcessing;

bool CBoxAlgorithmSignalMaximum::initialize(void)
{
    m_oSignalDecoder.initialize(*this, 0);

    m_pMatrixMaximumAlgorithm = &this->getAlgorithmManager().getAlgorithm(this->getAlgorithmManager().createAlgorithm(OVP_ClassId_Algorithm_MatrixMaximum));
    m_pMatrixMaximumAlgorithm->initialize();
    ip_pMatrixMaximumAlgorithm_Matrix.initialize(m_pMatrixMaximumAlgorithm->getInputParameter(OVP_Algorithm_MatrixMaximum_InputParameterId_Matrix));
    op_pMatrixMaximumAlgorithm_Matrix.initialize(m_pMatrixMaximumAlgorithm->getOutputParameter(OVP_Algorithm_MatrixMaximum_OutputParameterId_Matrix));

    // Connect the encoder to the output 0
    m_oSignalEncoder.initialize(*this, 0);

    // we connect the algorithms
    // the Signal Maximum algorithm will take the matrix coming from the signal decoder:
    ip_pMatrixMaximumAlgorithm_Matrix.setReferenceTarget(m_oSignalDecoder.getOutputMatrix());
    // The Signal Encoder will take the sampling rate from the Signal Decoder:
    m_oSignalEncoder.getInputSamplingRate().setReferenceTarget(m_oSignalDecoder.getOutputSamplingRate());
    // And the matrix from the Signal Maximum algorithm:
    m_oSignalEncoder.getInputMatrix().setReferenceTarget(op_pMatrixMaximumAlgorithm_Matrix);
    return true;
}

/*******************************************************************************/

bool CBoxAlgorithmSignalMaximum::uninitialize(void)
{
    m_oSignalDecoder.uninitialize();

    op_pMatrixMaximumAlgorithm_Matrix.uninitialize();
    ip_pMatrixMaximumAlgorithm_Matrix.uninitialize();
    m_pMatrixMaximumAlgorithm->uninitialize();
    this->getAlgorithmManager().releaseAlgorithm(*m_pMatrixMaximumAlgorithm);

    m_oSignalEncoder.uninitialize();

    return true;
}

/*******************************************************************************/

bool CBoxAlgorithmSignalMaximum::processInput(const size_t index)
{
    getBoxAlgorithmContext()->markAlgorithmAsReadyToProcess();
    return true;
}

/*******************************************************************************/

bool CBoxAlgorithmSignalMaximum::process(void)
{
    IBoxIO& l_rDynamicBoxContext = this->getDynamicBoxContext();

    //we decode the input signal chunks
    for (uint32_t i = 0; i < l_rDynamicBoxContext.getInputChunkCount(0); i++)
    {
        m_oSignalDecoder.decode(i);

        if (m_oSignalDecoder.isHeaderReceived())
        {
            if (!m_pMatrixMaximumAlgorithm->process(OVP_Algorithm_MatrixMaximum_InputTriggerId_Initialize)) return false;

            m_oSignalEncoder.encodeHeader();
            l_rDynamicBoxContext.markOutputAsReadyToSend(0, l_rDynamicBoxContext.getInputChunkStartTime(0, i), l_rDynamicBoxContext.getInputChunkEndTime(0, i));
        }

        if (m_oSignalDecoder.isBufferReceived())
        {
            // we process the signal matrice with our algorithm
            m_pMatrixMaximumAlgorithm->process(OVP_Algorithm_MatrixMaximum_InputTriggerId_Process);

            // If the process is done successfully, we can encode the buffer
            if (m_pMatrixMaximumAlgorithm->isOutputTriggerActive(OVP_Algorithm_MatrixMaximum_OutputTriggerId_ProcessDone))
            {
                m_oSignalEncoder.encodeBuffer();
                l_rDynamicBoxContext.markOutputAsReadyToSend(0, l_rDynamicBoxContext.getInputChunkStartTime(0, i), l_rDynamicBoxContext.getInputChunkEndTime(0, i));
            }
        }

        if (m_oSignalDecoder.isEndReceived())
        {
            m_oSignalEncoder.encodeEnd();
            l_rDynamicBoxContext.markOutputAsReadyToSend(0, l_rDynamicBoxContext.getInputChunkStartTime(0, i), l_rDynamicBoxContext.getInputChunkEndTime(0, i));
        }
    }
    return true;
}

Initialize

We need to initiliaze the codecs as usual, and the algorithm proxy and parameter handlers of the signal processing algorithm.

The interesting point here is the connection between algorithm through setReferenceTarget calls:

// we connect the algorithms
// the Signal Maximum algorithm will take the matrix coming from the signal decoder:
ip_pMatrixMaximumAlgorithm_Matrix.setReferenceTarget(m_oSignalDecoder.getOutputMatrix());
// The Signal Encoder will take the sampling rate from the Signal Decoder:
m_oSignalEncoder.getInputSamplingRate().setReferenceTarget(m_oSignalDecoder.getOutputSamplingRate());
// And the matrix from the Signal Maximum algorithm:
m_oSignalEncoder.getInputMatrix().setReferenceTarget(op_pMatrixMaximumAlgorithm_Matrix);

As you can see codec toolkit and old-school algorithm are totally compatible, as they manipulate the same generic objects : algorithm proxy and parameter handlers.

After this moment the algorithms are linked together, thus when decoding a new chunk the outputed sampling frequency will be directly set as encoder input, and the matrix given to the Signal Maximum algorithm.

Uninitialize

We free the parameter handlers and algorithms as usual.

m_oSignalDecoder.uninitialize();

op_pMatrixMaximumAlgorithm_Matrix.uninitialize();
ip_pMatrixMaximumAlgorithm_Matrix.uninitialize();
m_pMatrixMaximumAlgorithm->uninitialize();
this->getAlgorithmManager().releaseAlgorithm(*m_pMatrixMaximumAlgorithm);

m_oSignalEncoder.uninitialize();

Process

We iterate over all signal input chunks, and decode it:

for (uint32_t i = 0; i < l_rDynamicBoxContext.getInputChunkCount(0); i++)
{
    m_oSignalDecoder.decode(i);

As usual 3 cases are possible : Header, Buffer or End of stream. In case of header, we need to trigger the initialization of the algorithm. This process will initialize the input matrix of the signal encoder, as we set its reference target to the algorithm output matrix. If the algorithm process returned in an erroneous state, we must deactivate the box by returning false in the box process.

    if (m_oSignalDecoder.isHeaderReceived())
    {
        if (!m_pMatrixMaximumAlgorithm->process(OVP_Algorithm_MatrixMaximum_InputTriggerId_Initialize))
            return false;

        m_oSignalEncoder.encodeHeader();
        l_rDynamicBoxContext.markOutputAsReadyToSend(0, l_rDynamicBoxContext.getInputChunkStartTime(0, i), l_rDynamicBoxContext.getInputChunkEndTime(0, i));
    }

If a buffer is received, we simply trigger the “Process” of the algorithm, and check if the output trigger “Process Done” is raised:

    if (m_oSignalDecoder.isBufferReceived())
    {
        // we process the signal matrice with our algorithm
        m_pMatrixMaximumAlgorithm->process(OVP_Algorithm_MatrixMaximum_InputTriggerId_Process);

        // If the process is done successfully, we can encode the buffer
        if (m_pMatrixMaximumAlgorithm->isOutputTriggerActive(OVP_Algorithm_MatrixMaximum_OutputTriggerId_ProcessDone))
        {
            m_oSignalEncoder.encodeBuffer();
            l_rDynamicBoxContext.markOutputAsReadyToSend(0, l_rDynamicBoxContext.getInputChunkStartTime(0, i), l_rDynamicBoxContext.getInputChunkEndTime(0, i));
        }
    }

Finally if an end is received we forward that information to the output:

    if (m_oSignalDecoder.isEndReceived())
    {
        m_oSignalEncoder.encodeEnd();
        l_rDynamicBoxContext.markOutputAsReadyToSend(0, l_rDynamicBoxContext.getInputChunkStartTime(0, i), l_rDynamicBoxContext.getInputChunkEndTime(0, i));
    }
} // end of for loop

Include your algorithm and box into the project

In the ovp_main.cpp file:

  • Include the algorithm and the box

...
#include "algorithms/ovpCAlgorithmMatrixMaximum.h"
#include "box-algorithms/ovpCBoxAlgorithmSignalMaximum.h"
...

  • Declare the descriptors

...
OVP_Declare_New(CAlgorithmMatrixMaximumDesc);
OVP_Declare_New(CBoxAlgorithmSignalMaximumDesc);
...

Test the Box

We test this box with the following scenario:

Testing the Signal Maximum Box

We produce a 4-channel signal with the Sinus Oscillator, a 1-channel linear signal with the Time Signal. The Signal Merger box will merge the two inputs to produce a 5-channel signal (last channel is the time signal). To do this job properly, the 2 sampling boxes must produce a signal with the same sampling frequency and number of samples per buffer.

The Signal Maximum is computed and displayed as shown below. Once the time signal is above the maximum amplitude of the generated sinusoids, its samples are naturally chosen as the maximum.

Running the scenario

Conclusion

This advanced tutorial covered the implementation of algorithms and how to use them in boxes.

The algorithm mechanism contributes to the high modularity of OpenViBE. It’s also a powerful tool to lower the complexity of a box. For example, the Classifier Trainer box is able to use different classification methods (e.g. SVM, LDA). These methods are implemented in dedicated algorithms, and the trainer box manipulates these algorithms, without knowing what’s inside them. Thus the box is relatively simple. And it’s quiet easy to add more classification algorithms…

If any questions remain, please contact us on the regular channels to get help from experienced programmers!

This entry was posted in Box plugins and tagged , , . Bookmark the permalink.