The Open Master Hearing Aid (openMHA)  openMHA
Open community platform for hearing aid algorithm research
Writing openMHA Plugins. A step-by-step tutorial

Plugins are C++ code that is compiled and linked against the openMHA library. The compiler needs be instructed on how to find the openMHA headers and library and to link against the openMHA library. There are two possible options: One can compile openMHA and then create a copy of an example plugin directory and customize from there. See COMPILATION.md for more information on how to compile openMHA.

On Ubuntu is is also possible to install the libopenmha-dev package and include config.mk into the user's Makefile. Example 21 provides an example plugin and Makefile for this scenario.

On the personal hearing lab (PHL) running mahalia, the required packages for compiling openMHA plugins are already installed since version 4.17.0-r1. Example 21 can also be used here.

openMHA contains a small number of example plugins as C++ source code. They are meant to help developers in understanding the concepts of openMHA plugin programming starting from the simplest example and increasing in complexity. This tutorial explains the basic parts of the example files.

example1.cpp

The example plugin file example1.cpp demonstrates the easiest way to implement an openMHA Plugin. It attenuates the sound signal in the first channel by multiplying the sound samples with a factor. The plugin class MHAPlugin::plugin_t exports several methods, but only two of them need a non-empty implementation:

#include "mha_plugin.hh"
class example1_t : public MHAPlugin::plugin_t<int> {
public:
example1_t(MHA_AC::algo_comm_t & iac, const std::string & configured_name)
: MHAPlugin::plugin_t<int>("",iac)
{(void)configured_name;/* ignore 2nd parameter */}
void release(void)
{/* Do nothing in release */}
The template class for C++ openMHA plugins.
Definition: mha_plugin.hh:214
plugin_t(const std::string &, MHA_AC::algo_comm_t &)
Constructor of plugin template base class.
Definition: mha_plugin.hh:466
Algorithm communication variable space interface.
Definition: mha_algo_comm.hh:430
This C++ class implements the simplest example plugin for the step-by-step tutorial.
Definition: example1.cpp:31
example1_t(MHA_AC::algo_comm_t &iac, const std::string &configured_name)
Do-nothing constructor.
Definition: example1.cpp:36
void release(void)
Release may be empty.
Definition: example1.cpp:41
Header file for MHA C++ plugin class templates.
Namespace for openMHA plugin class templates and thread-safe runtime configurations.
Definition: mha_plugin.hh:45

Every plugin implementation should include the 'mha_plugin.hh' header file. C++ helper classes for plugin development are declared in this header file, and most header files needed for plugin development are included by mha_plugin.hh.

The class example1_t inherits from the class MHAPlugin::plugin_t, which in turn inherits from MHAParser::parser_t – the configuration language interface in the method "parse". Our plugin class therefore inherits the "parse" method from MHAParser::parser_t, which integrates the plugin into the global openMHA configuration tree.

The constructor has to accept two parameters of types algo_comm_t and std::string, respectively. In this simple example, we do not make use of them.

The release() method is used to free resources after signal processing. In this simple example, we do not allocate resources, so there is no need to free them.

The prepare method

void prepare(mhaconfig_t & signal_info)
{
if (signal_info.domain != MHA_WAVEFORM)
throw MHA_Error(__FILE__, __LINE__,
"This plugin can only process waveform signals.");
if (signal_info.channels < 1)
throw MHA_Error(__FILE__,__LINE__,
"This plugin requires at least one input channel.");
}
Error reporting exception class.
Definition: mha_error.hh:31
#define MHA_WAVEFORM
Definition: mha.hh:76
MHA prepare configuration structure.
Definition: mha.hh:247
unsigned int channels
Number of audio channels.
Definition: mha.hh:248
unsigned int domain
Signal domain (MHA_WAVEFORM or MHA_SPECTRUM)
Definition: mha.hh:249
Parameters
signal_infoContains information about the input signal's dimensions, see mhaconfig_t.

The prepare() method of the plugin is called before the signal processing starts, when the input signal dimensions like domain, number of channels, frames per block, and sampling rate are known. The prepare() method can check these values and raise an exception if the plugin cannot cope with them, as is done here. The plugin can also change these values if the signal processing performed in the plugin results in an output signal with different parameters. This plugin does not change the signal's parameters, therefore they are not modified here.

The signal processing method

mha_wave_t * process(mha_wave_t * signal)
{
unsigned int channel = 0; // channels and frames counting starts with 0
float factor = 0.1f;
unsigned int frame;
// Scale channel number "channel" by "factor":
for(frame = 0; frame < signal->num_frames; frame++) {
// Waveform channels are stored interleaved.
signal->buf[signal->num_channels * frame + channel] *= factor;
}
// Algorithms may process data in-place and return the input signal
// structure as their output signal:
return signal;
}
};
Waveform signal structure.
Definition: mha.hh:165
mha_real_t * buf
signal buffer
Definition: mha.hh:166
unsigned int num_channels
number of channels
Definition: mha.hh:167
unsigned int num_frames
number of frames in each channel
Definition: mha.hh:168
Parameters
signalPointer to the input signal structure mha_wave_t.
Returns
Pointer to the output signal structure. The input signal structure may be reused if the signal has the same domain and dimensions.

The plugin works with time domain input signal (indicated by the data type mha_wave_t of the process method's parameter). It scales the first channel by a factor of 0.1. The output signal reuses the structure that previously contained the input signal (in-place processing).

Connecting the C++ class with the C plugin interface

Plugins have to export C functions as their interface (to avoid C++ name-mangling issues and other incompatibilities when mixing plugins compiled with different C++ compilers).

MHAPLUGIN_CALLBACKS(example1,example1_t,wave,wave)
#define MHAPLUGIN_CALLBACKS(plugname, classname, indom, outdom)
C++ wrapper macro for the plugin interface.
Definition: mha_plugin.hh:729

This macro takes care of accessing the C++ class from the C functions required as the plugin's interface. It implements the C funtions and calls the corresponding C++ instance methods. Plugin classes should be derived from the template class MHAPlugin::plugin_t to be compatible with the C interface wrapper.

This macro also catches C++ exceptions of type MHA_Error, when raised in the methods of the plugin class, and reports the error using an error flag as the return value of the underlying C function. It is therefore important to note that only C++ exceptions of type MHA_Error may be raised by your plugin. If your code uses different Exception classes, you will have to catch them yourself before control leaves your plugin class, and maybe report the error by throwing an instance of MHA_Error. This is important, because: (1) C++ exceptions cannot cross the plugin interface, which is in C, and (2) there is no error handling code for your exception classes in the openMHA framework anyways.

example2.cpp

This is another simple example of openMHA plugin written in C++. This plugin also scales one channel of the input signal, working in the time domain. The scale factor and which channel to scale (index number) are made accessible to the configuration language.

The algorithm is again implemented as a C++ class.

class example2_t : public MHAPlugin::plugin_t<int> {
public:
example2_t(MHA_AC::algo_comm_t & iac, const std::string & configured_name);
void prepare(mhaconfig_t & signal_info);
void release(void);
};
Variable with float value.
Definition: mha_parser.hh:487
Variable with integer value.
Definition: mha_parser.hh:450
This C++ class implements the second example plugin for the step-by-step tutorial.
Definition: example2.cpp:32
void prepare(mhaconfig_t &signal_info)
Plugin preparation.
Definition: example2.cpp:82
mha_wave_t * process(mha_wave_t *signal)
Signal processing performed by the plugin.
Definition: example2.cpp:104
example2_t(MHA_AC::algo_comm_t &iac, const std::string &configured_name)
This constructor initializes the configuration language variables and inserts them into the openMHA c...
Definition: example2.cpp:66
MHAParser::int_t scale_ch
Index of audio channel to scale.
Definition: example2.cpp:34
MHAParser::float_t factor
The scaling factor applied to the selected channel.
Definition: example2.cpp:36
void release(void)
Undo restrictions posed in prepare.
Definition: example2.cpp:99
Parameters
scale_ch– the channel number to be scaled
factor– the scale factor of the scaling.

This class again inherits from the template class MHAPlugin::plugin_t for intergration with the openMHA configuration language. The two data members serve as externally visible configuration variables. All methods of this class have a non-empty implementation.

Constructor

const std::string & configured_name)
: MHAPlugin::plugin_t<int>("This plugin multiplies the sound signal"
" in one audio channel by a factor",iac),
scale_ch("Index of audio channel to scale. Indices start from 0.",
"0",
"[0,["),
factor("The scaling factor that is applied to the selected channel.",
"0.1",
"[0,[")
{
insert_item("channel", &scale_ch);
insert_item("factor", &factor);
(void)configured_name; // Ignore 2nd parameter
}

The constructor invokes the superclass constructor with a string parameter. This string parameter serves as the help text that describes the functionality of the plugin. The constructor registers configuration variables with the openMHA configuration tree and sets their default values and permitted ranges. The minimum permitted value for both variables is zero, and there is no maximum limit (apart from the limitations of the underlying C data type). The configuration variables have to be registered with the parser node instance using the MHAParser::parser_t::insert_item method.

The prepare method

void example2_t::prepare(mhaconfig_t & signal_info)
{
if (signal_info.domain != MHA_WAVEFORM)
throw MHA_Error(__FILE__, __LINE__,
"This plugin can only process waveform signals.");
// The user may have configured scale_ch before prepare is called.
// Check that the configured channel is present in the input signal.
if (signal_info.channels <= unsigned(scale_ch.data))
throw MHA_Error(__FILE__,__LINE__,
"This plugin requires at least %d input channels.",
scale_ch.data + 1);
// Adjust the range of the channel configuration variable so that it
// cannot be set to an out-of-range value during processing.
scale_ch.set_range("[0," + val2str(int(signal_info.channels)) + "[");
}
int data
Data field.
Definition: mha_parser.hh:478
void set_range(const std::string &r)
Change the valid range of a variable.
Definition: mha_parser.cpp:2320
std::string val2str(const bool &)
Convert to string.
Definition: mha_parser.cpp:1238
Parameters
signal_info– contains information about the input signal's parameters, see mhaconfig_t.

The user may have changed the configuration variables before preparing the openMHA plugin. A consequence of this is that it is not sufficient any more to check if the input signal has at least 1 audio channel.

Instead, this prepare method checks that the input signal has enough channels so that the current value of scale_ch.data is a valid channel index, i.e. 0 $\le$ scale_ch.data < signal_info.channels. The prepare method does not have to check that 0 $\le$ scale_ch.data, since this is guaranteed by the valid range setting of the configuration variable.

The prepare method then modifies the valid range of the scale_ch variable, it modifies the upper bound so that the user cannot set the variable to a channel index higher than the available channels. Setting the range is done using a string parameter. The prepare method contatenates a string of the form "[0,n[". n is the number of channels in the input signal, and is used here as an exclusive upper boundary. To convert the number of channels into a string, a helper function for string conversion from the openMHA Toolbox is used. This function is overloaded and works for several data types.

It is safe to assume that the value of configuration variables does not change while the prepare method executes, since openMHA preparation is triggered from a configuration language command, and the openMHA configuration language parser is busy and cannot accept other commands until all openMHA plugins are prepared (or one of them stops the process by raising an exception). As we will see later in this tutorial, the same assumption cannot be made for the process method.

The release method

{
}

The release method should undo the state changes that were performed by the prepare method. In this example, the prepare method has reduced the valid range of the scale_ch, so that only valid channels could be selected during signal processing.

The release method reverts this change by setting the valid range back to its original value, "[0,[".

The signal processing method

{
unsigned int frame;
for(frame = 0; frame < signal->num_frames; frame++)
value(signal,frame,scale_ch.data) *= factor.data;
return signal;
}
float data
Data field.
Definition: mha_parser.hh:516
mha_real_t & value(mha_wave_t *s, unsigned int fr, unsigned int ch)
Access an element of a waveform structure.
Definition: mha_signal.hh:541

The processing function uses the current values of the configuration variables to scale every frame in the selected audio channel.

Note that the value of each configuration variable can change while the processing method executes, since the process method usually executes in a different thread than the configuration interface.

For this simple plugin, this is not a problem, but for more advanced plugins, it has to be taken into consideration. The next section takes a closer look at the problem.

Consistency

Assume that one thread reads the value stored in a variable while another thread writes a new value to that variable concurrently. In this case, you may have a consistency problem. You would perhaps expect that the value retrieved from the variable either (a) the old value, or (b) the new value, but not (c) something else. Yet generally case (c) is a possibility.

Fortunately, for some data types on PC systems, case (c) cannot happen. These are 32bit wide data types with a 4-byte alignment. Therefore, the values in MHAParser::int_t and MHAParser::float_t are always consistent, but this is not the case for vectors, strings, or complex values. With these, you can get a mixture of the bit patterns of old and new values, or you can even cause a memory access violation in case a vector or string grows and has to be reallocated to a different memory address.

There is also a consistency problem if you take the combination of two "safe" datatypes. The openMHA provides a mechanism that can cope with these types of problems. This thread-safe runtime configuration update mechanism is introduced in example 5.

example3.cpp

This example introduces the openMHA Event mechanism. Plugins that provide configuration variable can receive a callback from the parser base class when a configuration variable is accessed through the configuration language interface.

The third example performes the same processing as before, but now only even channel indices are permitted when selecting the audio channel to scale. This restriction cannot be ensured by setting the range of the channel index configuration variable. Instead, the event mechanism of openMHA configuration variables is used. Configuration variables emit 4 different events, and your plugin can connect callback methods that are called when the events are triggered. These events are:

writeaccess

valuechanged

readaccess

prereadaccess

All of these callbacks are executed in the configuration thread. Therefore, the callback implementation does not have to be realtime-safe. No other updates of configuration language variables through the configuration language can happen in parallel, but your processing method can execute in parallel and may change values.

Data member declarations

class example3_t : public MHAPlugin::plugin_t<int> {
Monitor variable with int value.
Definition: mha_parser.hh:615
A Plugin class using the openMHA Event mechanism.
Definition: example3.cpp:33
MHAEvents::patchbay_t< example3_t > patchbay
The Event connector.
Definition: example3.cpp:42
MHAParser::float_t factor
The scaling factor applied to the selected channel.
Definition: example3.cpp:37
MHAParser::int_mon_t prepared
Keep Track of the prepare/release calls.
Definition: example3.cpp:39
MHAParser::int_t scale_ch
Index of audio channel to scale.
Definition: example3.cpp:35

This plugin exposes another configuration variable, "prepared", that keeps track of the prepared state of the plugin. This is a read-only (monitor) integer variable, i.e. its value can only be changed by your plugin's C++ code. When using the configuration language interface, the value of this variable can only be read, but not changed.

The patchbay member is an instance of a connector class that connects event sources with callbacks.

Method declarations

/* Callbacks triggered by Events */
void on_scale_ch_writeaccess();
void on_scale_ch_valuechanged();
void on_scale_ch_readaccess();
void on_prereadaccess();
public:
example3_t(MHA_AC::algo_comm_t & iac, const std::string & configured_name);
void prepare(mhaconfig_t & signal_info);
void release(void);
mha_wave_t * process(mha_wave_t * signal);
};

This plugin exposes 4 callback methods that are triggered by events. Multiple events (from the same or different configuration variables) can be connected to the same callback method, if desired.

This example plugin uses the valuechanged event to check that the scale_ch configuration variable is only set to valid values.

The other callbacks only cause log messages to stdout, but the comments in the logging callbacks give a hint when listening on the events would be useful.

Example 3 constructor

example3_t::example3_t(MHA_AC::algo_comm_t & iac, const std::string &)
: MHAPlugin::plugin_t<int>("This plugin multiplies the sound signal"
" in one audio channel by a factor",iac),
scale_ch("Index of audio channel to scale. Indices start from 0."
" Only channels with even indices may be scaled.",
"0",
"[0,["),
factor("The scaling factor that is applied to the selected channel.",
"0.1",
"[0,["),
prepared("State of this plugin: 0 = unprepared, 1 = prepared")
{
insert_item("channel", &scale_ch);
insert_item("factor", &factor);
prepared.data = 0;
insert_item("prepared", &prepared);
patchbay.connect(&scale_ch.writeaccess, this,
patchbay.connect(&scale_ch.valuechanged, this,
patchbay.connect(&scale_ch.readaccess, this,
patchbay.connect(&scale_ch.prereadaccess, this,
patchbay.connect(&factor.prereadaccess, this,
patchbay.connect(&prepared.prereadaccess, this,
}
void on_scale_ch_writeaccess()
Definition: example3.cpp:139
example3_t(MHA_AC::algo_comm_t &iac, const std::string &configured_name)
This constructor initializes the configuration language variables and inserts them into the openMHA c...
Definition: example3.cpp:80
void on_scale_ch_readaccess()
Definition: example3.cpp:157
void on_scale_ch_valuechanged()
Definition: example3.cpp:148
void on_prereadaccess()
Definition: example3.cpp:163

The constructor of a monitor variable does not require a parameter for setting the initial value. The only parameter here is the help text describing the contents of the read-only variable. If the initial value should differ from 0, then the .data member of the configuration variable has to be set to the initial value in the plugin constructor's body explicitly, as is done here for demonstration although the initial value of this monitor variable is 0.

Events and callback methods are then connected using the patchbay member variable.

The prepare method

void example3_t::prepare(mhaconfig_t & signal_info)
{
if (signal_info.domain != MHA_WAVEFORM)
throw MHA_Error(__FILE__, __LINE__,
"This plugin can only process waveform signals.");
// The user may have configured scale_ch before prepare is called.
// Check that the configured channel is present in the input signal.
if (signal_info.channels <= unsigned(scale_ch.data))
throw MHA_Error(__FILE__,__LINE__,
"This plugin requires at least %d input channels.",
scale_ch.data + 1);
// bookkeeping
}
int data
Data field.
Definition: mha_parser.hh:620
void prepare(mhaconfig_t &signal_info)
Plugin preparation.
Definition: example3.cpp:111

The prepare method checks wether the current setting of the scale_ch variable is possible with the input signal dimension. It does not adjust the range of the variable, since the range alone is not sufficient to ensure all future settings are also valid: The scale channel index has to be even.

The release method

{
}
void release(void)
Bookkeeping only.
Definition: example3.cpp:126

The release method is needed for tracking the prepared state only in this example.

The signal processing method

{
unsigned int frame;
for(frame = 0; frame < signal->num_frames; frame++)
value(signal,frame,scale_ch.data) *= factor.data;
return signal;
}
mha_wave_t * process(mha_wave_t *signal)
Signal processing performed by the plugin.
Definition: example3.cpp:131

The signal processing member function is the same as in example 2.

The callback methods

{
printf("Write access: Attempt to set scale_ch=%d.\n", scale_ch.data);
// Can be used to track any writeaccess to the configuration, even
// if it does not change the value. E.g. setting the name of the
// sound file in a string configuration variable can cause a sound
// file player plugin to start playing the sound file from the
// beginning.
}
{
if (scale_ch.data & 1)
throw MHA_Error(__FILE__,__LINE__,
"Attempt to set scale_ch to non-even value %d",
// Can be used to recompute a runtime configuration only if some
// configuration variable actually changed.
}
{
printf("scale_ch has been read.\n");
// A configuration variable used as an accumulator can be reset
// after it has been read.
}
{
printf("A configuration language variable is about to be read.\n");
// Can be used to compute the value on demand.
}
MHAPLUGIN_CALLBACKS(example3,example3_t,wave,wave)

When the writeaccess or valuechanged callbacks throw an MHAError exception, then the change made to the value of the configuration variable is reverted.

If multiple event sources are connected to a single callback method, then it is not possible to determine which event has caused the callback to execute. Often, this information is not crucial, i.e. when the answer to a change of any variable in a set of variables is the same, e.g. the recomputation of a new runtime configuration that takes all variables of this set as input.

example4.cpp

This plugin is the same as example 3 except that it works on the spectral domain (STFT).

The Prepare method

void example4_t::prepare(mhaconfig_t & signal_info)
{
if (signal_info.domain != MHA_SPECTRUM)
throw MHA_Error(__FILE__, __LINE__,
"This plugin can only process spectrum signals.");
// The user may have configured scale_ch before prepare is called.
// Check that the configured channel is present in the input signal.
if (signal_info.channels <= unsigned(scale_ch.data))
throw MHA_Error(__FILE__,__LINE__,
"This plugin requires at least %d input channels.",
scale_ch.data + 1);
// bookkeeping
}
void prepare(mhaconfig_t &signal_info)
Plugin preparation.
Definition: example4.cpp:112
MHAParser::int_mon_t prepared
Keep Track of the prepare/release calls.
Definition: example4.cpp:39
MHAParser::int_t scale_ch
Index of audio channel to scale.
Definition: example4.cpp:35
#define MHA_SPECTRUM
Definition: mha.hh:77

The prepare method now checks that the signal domain is MHA_SPECTRUM.

The signal processing method

{
unsigned int bin;
// spectral signal is stored non-interleaved.
mha_complex_t * channeldata =
signal->buf + signal->num_frames * scale_ch.data;
for(bin = 0; bin < signal->num_frames; bin++)
channeldata[bin] *= factor.data;
return signal;
}
MHAParser::float_t factor
The scaling factor applied to the selected channel.
Definition: example4.cpp:37
mha_spec_t * process(mha_spec_t *signal)
Signal processing performed by the plugin.
Definition: example4.cpp:132
Type for complex floating point values.
Definition: mha.hh:100
Definition: mha.hh:197
unsigned int num_frames
number of frames in each channel
Definition: mha.hh:200
mha_complex_t * buf
signal buffer
Definition: mha.hh:198

The signal processing member function works on the spectral signal instead of the wave signal as before.

The mha_spec_t instance stores the complex (mha_complex_t) spectral signal for positive frequences only (since the waveform signal is always real). The num_frames member of mha_spec_t actually denotes the number of STFT bins.

Please note that different from mha_wave_t, a multichannel signal in mha_spec_t is stored non-interleaved in the signal buffer.

Some arithmetic operations are defined on struct mha_complex_t to facilitate efficient complex computations. The *= operator used here (defined for real and for complex arguments) is one of them.

Connecting the C++ class with the C plugin interface

MHAPLUGIN_CALLBACKS(example4,example4_t,spec,spec)
A Plugin class using the spectral signal.
Definition: example4.cpp:33

When connecting a class that performs spectral processing with the C interface, use spec instead of wave as the domain indicator.

example5.cpp

Many algorithms use complex operations to transform the user space variables into run time configurations. If this takes a noticeable time (e.g. more than 100-500 $\mu$ sec), the update of the runtime configuration can not take place in the real time processing thread. Furthermore, the parallel access to complex structures may cause unpredictable results if variables are read while only parts of them are written to memory (cf. section Consistency). To handle these situations, a special C++ template class MHAPlugin::plugin_t was designed. This class helps keeping all access to the configuration language variables in the configuration thread rather than in the processing thread.

The runtime configuration class example5_t is the parameter of the template class MHAPlugin::plugin_t. Its constructor converts the user variables into a runtime configuration. Because the constructor executes in the configuration thread, there is no harm if the constructor takes a long time. All other member functions and data members of the runtime configurations are accessed only from the signal processing thread (real-time thread).

class example5_t {
public:
example5_t(unsigned int,unsigned int,mha_real_t);
private:
unsigned int channel;
};
Definition: example5.cpp:48
mha_spec_t * process(mha_spec_t *)
Definition: example5.cpp:108
example5_t(unsigned int, unsigned int, mha_real_t)
Definition: example5.cpp:92
unsigned int channel
Definition: example5.cpp:53
mha_real_t scale
Definition: example5.cpp:54
float mha_real_t
openMHA type for real numbers
Definition: mha.hh:94

The plugin interface class inherits from the plugin template class MHAPlugin::plugin_t, parameterised by the runtime configuration. Configuration changes (write access to the variables) will emit a write access event of the changed variables. These events can be connected to member functions of the interface class by the help of a MHAEvents::patchbay_t instance.

class plugin_interface_t : public MHAPlugin::plugin_t<example5_t> {
public:
const std::string & configured_name);
private:
void update_cfg();
/* integer variable of MHA-parser: */
/* float variable of MHA-parser: */
/* patch bay for connecting configuration parser
events with local member functions: */
};
Definition: example5.cpp:68
MHAParser::int_t scale_ch
Definition: example5.cpp:77
mha_spec_t * process(mha_spec_t *)
Definition: example5.cpp:146
void update_cfg()
Definition: example5.cpp:177
plugin_interface_t(MHA_AC::algo_comm_t &iac, const std::string &configured_name)
Definition: example5.cpp:121
MHAParser::float_t factor
Definition: example5.cpp:79
MHAEvents::patchbay_t< plugin_interface_t > patchbay
Definition: example5.cpp:82
void prepare(mhaconfig_t &)
Definition: example5.cpp:161

The constructor of the runtime configuration analyses and validates the user variables. If the configuration is invalid, an exception of type MHA_Error is thrown. This will cause the openMHA configuration language command which caused the change to fail: The modified configuration language variable is then reset to its original value, and the error message will contain the message string of the MHA_Error exception.

example5_t::example5_t(unsigned int ichannel,
unsigned int numchannels,
mha_real_t iscale)
: channel(ichannel),scale(iscale)
{
if( channel >= numchannels )
throw MHA_Error(__FILE__,__LINE__,
"Invalid channel number %u (only %u channels configured).",
channel,numchannels);
}
void scale(mha_spec_t *dest, const mha_wave_t *src)
Definition: mha_signal.cpp:808

In this example, the run time configuration class example5_t has a signal processing member function. In this function, the selected channel is scaled by the given scaling factor.

{
/* Scale channel number "scale_ch" by "factor": */
for(unsigned int fr = 0; fr < spec->num_frames; fr++){
spec->buf[fr + channel * spec->num_frames].re *= scale;
spec->buf[fr + channel * spec->num_frames].im *= scale;
}
mha_real_t re
Real part.
Definition: mha.hh:101
mha_real_t im
Imaginary part.
Definition: mha.hh:102
return spec;
}

The constructor of the example plugin class is similar to the previous examples. A callback triggered on write access to the variables is registered using the MHAEvents::patchbay_t instance.

const std::string &)
: MHAPlugin::plugin_t<example5_t>("example plugin scaling a spectral signal",iac),
/* initialzing variable 'scale_ch' with MHAParser::int_t(char* name, .... ) */
scale_ch("channel number to be scaled","0","[0,["),
/* initialzing variable 'factor' with MHAParser::float_t(char* name, .... ) */
factor("scale factor","1.0","[0,2]")
{
/* Register variables to the configuration parser: */
insert_item("channel",&scale_ch);
insert_item("factor",&factor);
/*
* On write access to the parser variables a notify callback of
* this class will be called. That funtion will update the runtime
* configuration.
*/
patchbay.connect(&scale_ch.writeaccess,this,&plugin_interface_t::update_cfg);
patchbay.connect(&factor.writeaccess,this,&plugin_interface_t::update_cfg);
}

The processing function can gather the latest valid runtime configuration by a call of poll_config. On success, the class member cfg points to this configuration. On error, if there is no usable runtime configuration instance, an exception is thrown. In this example, the prepare method ensures that there is a valid runtime configuration, so that in this example, no error can be raised at this point. The prepare method is always executed before the process method is called. The runtime configuration class in this example provides a signal processing method. The process method of the plugin interface calls the process method of this instance to perform the actual signal processing.

{
return cfg->process(spec);
}
runtime_cfg_t * poll_config()
Receive the latest run time configuration.
Definition: mha_plugin.hh:360
runtime_cfg_t * cfg
Pointer to the runtime configuration currently used by the signal processing thread.
Definition: mha_plugin.hh:162

The prepare method ensures that a valid runtime configuration exists by creating a new runtime configuration from the current configuration language variables. If the configuraion is invalid, then an exception of type MHA_Error is raised and the preparation of the openMHA fails with an error message.

{
if( tfcfg.domain != MHA_SPECTRUM )
throw MHA_Error(__FILE__,__LINE__,
"Example5: Only spectral processing is supported.");
/* remember the transform configuration (i.e. channel numbers): */
tftype = tfcfg;
/* make sure that a valid runtime configuration exists: */
}
mhaconfig_t tftype
Member for storage of plugin interface configuration.
Definition: mha_plugin.hh:233

The update_cfg member function is called when the value of a configuration language variable changes, or from the prepare method. It allocates a new runtime configuration and registers it for later access from the real time processing thread. The function push_config stores the configuration in a FiFo queue of runtime configurations. Once they are inserted in the FiFo, the MHAPlugin::plugin_t template is responsible for deleting runtime configuration instances stored in the FiFo. You don't need to keep track of the created instances, and you must not delete them yourself.

{
}
void push_config(runtime_cfg_t *ncfg)
Push a new run time configuration into the configuration fifo.
Definition: mha_plugin.hh:410

In the end of the example code file, the macro MHAPLUGIN_CALLBACKS defines all ANSI-C interface functions and passes them to the corresponding C++ class member functions (partly defined by the MHAPlugin::plugin_t template class). All exceptions of type MHA_Error are caught and transformed into an appropriate error code and error message.

example6.cpp

This example is the same as the previous one, except that it additionally creates an 'Algorithm Communication Variable' (AC variable). It calculates the RMS level of a given channel and stores it into this variable. The variable can be accessed by any other algorithm in the same chain. To store the data onto disk, the 'acsave' plugin can be used. 'acmon' is a plugin which converts AC variables into parsable monitor variables.

In the constructor of the plugin class the variable rmsdb is registered under the name example6_rmslev as a one-dimensional AC variable of type float. For registration of other types, read access and other detailed informations please see Communication between algorithms.

example6_t::example6_t(MHA_AC::algo_comm_t & iac, const std::string &)
: MHAPlugin::plugin_t<cfg_t>("Example rms level meter plugin",iac),
/* initialzing variable 'channel_no' with MHAParser::int_t(char* name, .... ) */
channel_no("channel in which the RMS level is measured","0","[0,[")
{
/* Register variables to the configuration parser: */
insert_item("channel",&channel_no);
/*
* On write access to the parser variables a notify callback of
* this class will be called. That funtion will update the runtime
* configuration.
*/
patchbay.connect(&channel_no.writeaccess,this,&example6_t::update_cfg);
/*
* Propagate the level variable to all algorithms in the
* processing chain. If multiple instances of this algorithm are
* required, than it is necessary to use different names for this
* variable (i.e. prefixing the name with the algorithm name
* passed to MHAInit).
*/
ac.insert_var_float("example6_rmslev", &rmsdb );
}
Definition: complex_scale_channel.cpp:46
void update_cfg()
Definition: example6.cpp:182
example6_t(MHA_AC::algo_comm_t &iac, const std::string &configured_name)
Definition: example6.cpp:108

Debugging openMHA plugins

Suppose you would want to step through the code of your openMHA plugin with a debugger. This example details how to use the GDB debugger to inspect the example6_t::prepare() and example6_t::process() routines of example6.cpp example 6.

First, make sure that your plugin is compiled with the compiler option to include debugging symbols: Apply the -ggdb switch to all gcc, g++ invocations.

Once the plugin is compiled with debugging symbols, create a test configuration. For example 6, assuming there is an audio file named input.wav in your working directory, you could create a configuration file named ‘debugexample6.cfg’, with the following content:

# debugexample6.cfg
fragsize = 64
srate = 44100
nchannels_in = 2
iolib = MHAIOFile

io.in = input.wav
io.out = output.wav
mhalib = example6
mha.channel = 1
cmd=start

Assuming all your binaries and shared-object libraries are in your ‘bin’ directory (see README.md), you could start gdb using

$ export MHA_LIBRARY_PATH=$PWD/bin 
$ gdb $MHA_LIBRARY_PATH/mha

Set breakpoints in prepare and process methods, and start execution. Note that specifying the breakpoint by symbol (example6_t::prepare) does not yet work, as the symbol lives in the openMHA plugin that has not yet been loaded. Specifying by line number works, however. Specifying the breakpoint by symbol also works once the plugin is loaded (i.e. when the debugger stops in the first break point). You can set the breakpoints like this (example shown here is run in gdb version 7.11.1):

(gdb) run ?read:debugexample6.cfg
Starting program: {openMHA_directory}/bin/mha ?read:debugexample6.cfg
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
The Open Master Hearing Aid (openMHA) server
Copyright (c) 2005-2021 HoerTech gGmbH, D-26129 Oldenburg, Germany

This program comes with ABSOLUTELY NO WARRANTY; for details see file COPYING.
This is free software, and you are welcome to redistribute it 
under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE, Version 3; 
for details see file COPYING.


Breakpoint 1, example6_t::prepare (this=0x6478b0, tfcfg=...)
    at example6.cpp:192
192        if( tfcfg.domain != MHA_WAVEFORM )
(gdb) b example6.cpp:162
Breakpoint 2 at 0x7ffff589744a: file example6.cpp, line 162.
(gdb) c
Continuing.

Where ‘{openMHA_directory}’ is the directory where openMHA is located (which should also be your working directory in this case). Next stop is the process() method. You can now examine and change the variables, step through the program as needed (using, for example ‘n’ to step in the next line):

Breakpoint 2, example6_t::process (this=0x7ffff6a06c0d, wave=0x10a8b550)
    at example6.cpp:162
162     {
(gdb) n
163        poll_config();
(gdb)

Writing unit tests for openMHA plugins

This section introduces how to test a plugin with C++ unit tests using the Googletest framework. In order to execute the tests, navigate to the openMHA root directory and run make unit-tests in your terminal. Afterwards you may execute make unit-tests in the plugin directory in order to only execute the very test you are working on.

example7

As an example, unit tests for plugin example7.cpp are written, which is functionally the same as plugin example1.cpp (see section example1.cpp). In order to write unit tests for your plugin it must have its class/function declarations in a header file (.hh) so you can include it in the unit test file. The class/function definitions are contained in the respective source file (.cpp).
The unit tests are written using a test fixture class (here: example7_testing) which will be inherited by the individual tests (TEST_F). This enables us to use the members in example7_testing in multiple tests without the need for redundant declarations.

#include "mha_algo_comm.hh"
#include "mha_signal.hh"
#include "example7.hh"
#include <gtest/gtest.h>
class example7_testing : public ::testing::Test {
public:
MHA_AC::algo_comm_t & ac = {acspace};
mhaconfig_t signal_properties {
.channels = 2U,
.domain = MHA_WAVEFORM,
.fragsize = 10U,
.wndlen = 0U,
.fftlen = 0U,
.srate = 44100.0f
};
example7_t ex7 = {ac,"algo"};
MHASignal::waveform_t wave_input{signal_properties.fragsize,signal_properties.channels};
};
signal processing class for waveform data (based on mha_wave_t)
Definition: mha_signal.hh:892
AC variable space implementation.
Definition: mha_algo_comm.hh:610
Definition: example7.hh:24
Header file for Algorithm Communication.
Header file for audio signal handling and processing classes.

The test fixture class is derived from the ::testing::Test class declared in gtest.h. The constructor of example7_t needs three parameters, namely a handle to the algorithm communication variable space and two strings. A container for audio signals for repeatedly passing blocks of the input signal to the plugin under test is also allocated by the test fixture class. It is defined as an instance of MHASignal::waveform_t with the name wave_input and its values are zero upon initialization.

TEST_F(example7_testing,test_state_methods){
EXPECT_FALSE(ex7.is_prepared());
ex7.prepare_(signal_properties);
acspace.set_prepared(true);
EXPECT_TRUE(ex7.is_prepared());
acspace.set_prepared(false);
ex7.release_();
EXPECT_FALSE(ex7.is_prepared());
}

The first test checks whether the state methods work as expected. Next to the actual processing there are often certain variables in each individual openMHA plugin that need to be allocated beforehand or wiped from memory afterwards. The methods that are used to do this are prepare() and release(). In order to assert that they were called and that we switched states accordingly we use the methods prepare_() and release_() (Note: the underscore!) that are defined in the plugin base class mha_plugin_t<>. These methods keep track of the state, call prepare() and release() and do additional bookkeeping. To ensure that the state methods work as expected the Googletest methods EXPECT_FALSE and EXPECT_TRUE are used.

TEST_F(example7_testing,test_functionality){
ex7.prepare_(signal_properties);
acspace.set_prepared(true);
wave_input.assign(1.0f);
EXPECT_FLOAT_EQ(1.0f,value(wave_input,4,0));
EXPECT_FLOAT_EQ(1.0f,value(wave_input,5,0));
EXPECT_FLOAT_EQ(1.0f,value(wave_input,4,1));
EXPECT_FLOAT_EQ(1.0f,value(wave_input,5,1));
ex7.process(&wave_input);
EXPECT_FLOAT_EQ(0.1f,value(wave_input,4,0));
EXPECT_FLOAT_EQ(0.1f,value(wave_input,5,0));
EXPECT_FLOAT_EQ(1.0f,value(wave_input,4,1));
EXPECT_FLOAT_EQ(1.0f,value(wave_input,5,1));
acspace.set_prepared(false);
ex7.release_();
}

In this test the goal is to assess the main feature of the plugin (example7_t), which is the same as in example1_t, namely altering the signal's first channel by a constant factor of 0.1. The variable wave_input is the signal that will be processed by the plugin. In order to assert the success, the elements in wave_input are set to a constant value of 1, because they are 0 upon initialization. During the process() function all elements of the first channel of wave_input are multiplied by the factor 0.1. Before process() is called the value assigned to wave_input is checked via the method EXPECT_FLOAT_EQ provided by Googletest. The values of wave_input are retrieved by the method value() by passing the desired sample position and channel number as second and third input parameter, respectively. Here, we checked the values of two frames in each channel to show the difference before and after processing; the frame indices were chosen randomly. After calling process(), the values contained in wave_input are checked again to make sure that the plugin worked as intended.