Advantages/disadvantages of using interface classes

Post Reply
mandolini_
Posts: 15
Joined: Fri Feb 14, 2020 1:50 am

Advantages/disadvantages of using interface classes

Post by mandolini_ » Mon Jul 20, 2020 11:20 pm

Hi,

I'm having trouble deciphering whether I should use two separate classes (runtime config class and signal processing class) or just a single, signal processing class for my plugins. It seems that you are able to poll your current config file (*.cfg) and modify any MHAParser variables if you use a runtime configuration class as well. Is this correct?

More generally, what are the pros/cons of using interface classes as shown in example5? Perhaps an explanation of what a "runtime configuration" is would be helpful.

Thanks

mandolini_
Posts: 15
Joined: Fri Feb 14, 2020 1:50 am

Re: Advantages/disadvantages of using interface classes

Post by mandolini_ » Tue Jul 21, 2020 7:07 pm

Furthermore, what is the role of the release() method in the 2-class topology? It doesn't look like there are release methods to free memory that is created in the prepare() method. Should allocating memory now occur in the constructor, and freeing memory occur in the destructor of the signal processing class?

tobiasherzke
Posts: 27
Joined: Mon Jun 24, 2019 12:51 pm

Re: Advantages/disadvantages of using runtime configuration classes

Post by tobiasherzke » Wed Jul 22, 2020 8:13 pm

You need an interface class (which implements the interface functions prepare(), release(), process(), constructor(),...) in every plugin. Whether you should use an additional runtime configuration class or not is the question.

Advantages of using runtime configuration classes: You can allow configuration changes while the signal processing is running while benefiting from thread safety and consistency without using locks for synchronization, which is important for real-time signal processing.

Disadvantages of using runtime configuration classes: The amount of code to write increases.

I realize that these answers may be too abstract. More details are in the documentation about example5 in section 2.2.5 in http://www.openmha.org/docs/openMHA_dev ... manual.pdf and in the sections referenced from there.

prepare, release, and memory: I believe you are referring this code line in method update() in example5.cpp, which is called from prepare or when a configuration variable changes:

Code: Select all

        push_config(new example5_t(scale_ch.data,tftype.channels,factor.data));
And you ask because no corresponding delete can be found in example5.cpp. For explanation, refer to the documentation of method push_config, either in the same PDF or here:
Parameters
ncfg
A pointer to the new runtime configuration object. This object must have been created on the heap by the configuration thread with operator new. By passing the pointer to this method, client code gives up ownership. The object will be deleted in a future invocation of push_config, or on destruction of this config_t instance.
Polling of configuration files (*.cfg): Configuration files are not polled continuously. A configuration file is read once when the MHA is told to read it, and after that not used anymore. Live configuration updates happen through the TCP port that MHA opens. Default is port 33337 on network interface "localhost", this can be altered by invoking mha with parameters --port=... --interface=...

mandolini_
Posts: 15
Joined: Fri Feb 14, 2020 1:50 am

Re: Advantages/disadvantages of using interface classes

Post by mandolini_ » Thu Jul 23, 2020 7:37 pm

Thanks for the reply. Part of my confusion stemmed from not realizing that the interface class and signal processing class are the same thing; the runtime configuration class (which the interface/SP class inherits from) is where updates to the configuration occur by creating a new config each time a parameter gets updated.

Some follow-up questions:

1a)
I have written a plugin already with a single interface class, and now I would like to be able to update some of the algorithm's parameters at runtime. How do I know what should stay in that class and what should be moved to the runtime config class?

1b)
Specifically, some of the parameters I would like to change directly impact the memory allocation of objects in my algorithm. Where should this memory allocation occur? The prepare method of the interface class? The constructor of the runtime configuration class? A separate prepare method of the runtime config class? This is mostly a question about backend execution.


2)
What is the significance of having a process() method in an interface class that points to the process() method of the runtime config class? Specifically referencing this line in example5:

Code: Select all

mha_spec_t* plugin_interface_t::process(mha_spec_t* spec)
{
    poll_config();
    return cfg->process(spec);
}
Sorry for all the questions. Many thanks for your patience!

tobiasherzke
Posts: 27
Joined: Mon Jun 24, 2019 12:51 pm

Runtime configuration questions

Post by tobiasherzke » Sun Jul 26, 2020 7:13 pm

mandolini_ wrote:
Thu Jul 23, 2020 7:37 pm
1a)
I have written a plugin already with a single interface class, and now I would like to be able to update some of the algorithm's parameters at runtime. How do I know what should stay in that class and what should be moved to the runtime config class?
You do not actually move the parameters, but copy them in some form. If you share your code here I am sure we can give you some specific recommendations.
1b)
Specifically, some of the parameters I would like to change directly impact the memory allocation of objects in my algorithm. Where should this memory allocation occur? The prepare method of the interface class? The constructor of the runtime configuration class? A separate prepare method of the runtime config class? This is mostly a question about backend execution.
You need to make sure to only allocate and deallocate memory in the configuration thread. Only performing memory (de-) allocations in the constructor (destructor) of a runtime configuration class is one way to ensure that.

Another way is to only allocate in prepare and deallocate in release but when your memory requirements can change with configuration variable updates then you cannot allow configuration updates at run time with this method. If you need to allow them during run time, then use the method from the previous paragraph.
2)
What is the significance of having a process() method in an interface class that points to the process() method of the runtime config class? Specifically referencing this line in example5:

Code: Select all

mha_spec_t* plugin_interface_t::process(mha_spec_t* spec)
{
    poll_config();
    return cfg->process(spec);
}
In my opinion it is only a matter of taste. You can either do all the processing in the interface class but then you need to always reference the data from the runtime configuration with

Code: Select all

cfg->some_field_or_other
and they all need to be public or have public accessors. Forwarding the processing may give better encapsulation. I have done both ways in the past and would accept both ways in a code review. Do whatever you are more comfortable with.

mandolini_
Posts: 15
Joined: Fri Feb 14, 2020 1:50 am

Re: Advantages/disadvantages of using interface classes

Post by mandolini_ » Sat Aug 01, 2020 12:20 am

Thanks for your reply. The memory allocation aspect seems the most difficult to topple. I will share a bit of my code and I invite any recommendation as how to best approach this.

Code: Select all

// Runtime configuration class
class filter_t {
    public:
        filter_t(MHAParser::bool_t filter_signal);
        mha_spec_t* process(mha_spec_t*);
    protected:
        /* different signal representation that gets converted in the copyDataTo...() function methods */
        ComplexFloatArray2D* m_pInStreamCplx; // create pointer to class object
        ComplexFloatArray2D* m_pOutStreamCplx;

        /* Filter block */
        FilterBlock* m_pFilter;
    private:
        bool filter_signal;
        /* convert between signal conventions/objects */
        void copyDataToFilterType(const mha_spec_t *mha_signal) {...};
        void copyDataToMHA(mha_spec_t *mha_signal) {...};
};


// Runtime cfg constructor
filter_t::filter_t(MHAParser::bool_t filter_signal)
      : m_pInStreamCplx(NULL),
        filter_signal(filter_signal.data)
{

}


mha_spec_t* filter_t::process(mha_spec_t *mha_signal)
{
    if (filter_signal)    // <-- Would like to update this parameter at runtime (as a sort of on/off switch for the filter)
    {
        copyDataToFilterType(mha_signal); //gets converted to m_pInStreamCplx

        m_pFilter->processFilter(m_pInStreamCplx, m_pOutStreamCplx); //filter incoming signal

        copyDataToMHA(mha_signal); //m_pOutStreamCplx gets converted to mha_spec_t
    }
    return mha_signal;
}



// Interface class
class filter_interface_t : public MHAPlugin::plugin_t<filter_t> {
    public:
        filter_interface_t(const algo_comm_t&, const std::string&, const std::string&);
        mha_spec_t* process(mha_spec_t*);
        void prepare(mhaconfig_t&);
        void release(void);
    private:
        void update_cfg();
        MHAParser::bool_t filter_signal;
        // The Event connector
        MHAEvents::patchbay_t<filter_interface_t> patchbay;
};


// Interface class constructor
filter_interface_t::filter_interface_t(const algo_comm_t &ac,
                                       const std::string &chain_name,
                                       const std::string &algo_name)
    : MHAPlugin::plugin_t<filter_t>("Description here.",ac),
      filter_signal("Either process the input signal or don't. The choice is yours.", "yes"),
{
    insert_item("process", &filter_signal);
    patchbay.connect(&filter_signal.writeaccess, this, &filter_interface_t::update_cfg);  // <-- should this be &filter_signal.valuechanged instead?  
}


mha_spec_t* filter_interface_t::process(mha_spec_t* mha_signal)
{
    poll_config();
    return cfg->process(mha_signal);
}


void filter_interface_t::prepare(mhaconfig_t &signal_info)
{
    if (signal_info.domain != MHA_SPECTRUM) {
        throw MHA_Error(__FILE__, __LINE__,
                        "This plugin can only process spectrum signals.");
    }

    unsigned int nbins = NBINS(signal_info.fftlen);
    if (nbins != (signal_info.fftlen / 2 + 1)) {
        throw MHA_Error(__FILE__, __LINE__,
                        "Incorrect number of frequency bins. Should be %d\n"
                        "Window length == %d\n"
                        "fragsize == %d\n"
                        "fftlen == %d\n",
                        nbins, signal_info.wndlen, signal_info.fragsize, signal_info.fftlen);
    }

    m_pInStreamCplx = new ComplexFloatArray2D(signal_info.channels, nbins);
    assert(m_pInStreamCplx);
    m_pOutStreamCplx = new ComplexFloatArray2D(signal_info.channels, nbins);
    assert(m_pInStreamCplx);
    m_pFilter = new FilterBlock(signal_info.channels, nbins);
    assert(m_pFilter);

    /* remember the transform configuration (i.e. channel numbers): */
    tftype = signal_info;
    /* make sure that a valid runtime configuration exists: */
    update_cfg();
}


void filter_interface_t::release(void)
{
    /* free resources here, if any are allocated */
    delete m_pInStreamCplx;
    m_pInStreamCplx = NULL;
    delete m_pOutStreamCplx;
    m_pOutStreamCplx = NULL;
    delete m_pFilter;
    m_pFilter = NULL;
}


void filter_interface_t::update_cfg()
{
    push_config(new filter_t(filter_signal));
}
I don't believe I need to change how much memory is allocated for the m_pInStreamCplx and m_pOutStreamCplx objects each call of push_config(). I really only want to change whether it processes the signal or not (based on toggling the filter_signal variable).

tobiasherzke
Posts: 27
Joined: Mon Jun 24, 2019 12:51 pm

Re: Advantages/disadvantages of using interface classes

Post by tobiasherzke » Sat Aug 08, 2020 8:52 am

mandolini_ wrote:
Sat Aug 01, 2020 12:20 am
I will share a bit of my code and I invite any recommendation as how to best approach this.


I understand that you are in the process of modifying your code to add a runtime configuration class to your plugin, because you want to switch processing on and off during runtime with a boolean switch. And you are not yet finished, there are data members declared in one class but initialized and used in the other, etc.

Maybe this boolean switch is only the first mutable parameter that you introduce and more will follow in future?

I ask because, if this will be the only runtime-changeable parameter in your plugin, then a separate runtime configuration class is overkill in my view, and I would implement all processing in the interface class, either querying the current value of filter_signal.data once at the beginning of the process method without using patchbay/update, or I would declare "bool" as my runtime configurations class:

Code: Select all

class filter_interface_t : public MHAPlugin::plugin_t<bool> {
        void update_cfg() {push_config(new bool(filter_signal.data));}
But if there were more parameters that can change during processing, then I would add a runtime configuration class like you began here. Some advice for this case inspired from your code:
  • Do not pass around instances of MHAParser::bool_t and other similar types in namespace MHAParser by value. Instead, pass only their data member.
  • Allocate the memory in the constructor of the runtime configuration class. Add a destructor that deallocates the memory to the runtime configuration class, so that when MHA deletes the runtime config instance, also their memory is deleted.
  • From your prepare method, call the update method.
  • In the update method, check that MHA is prepared with is_prepared. (This is set to true just before the prepare method is called.) When this is false, then your update method will just return without doing anything. This is necessary if you need to access the signal dimensions, which are only known when prepare is called.
  • Check signal dimensions either in the update method or in the runtime configuration class constructor.
  • In your update method, use input_cfg() to access the signal dimensions
  • Regarding valuechanged vs writeaccess: When your filter object lives in the runtime configuration and you update any parameter, then a new runtime configuration object is created and you lose the previous filter's state. This can be desirable or undesirable. Using writeaccess instead of valuechanged gives your users a tool to reset the filter's state without changing the processing mode.

mandolini_
Posts: 15
Joined: Fri Feb 14, 2020 1:50 am

Re: Advantages/disadvantages of using interface classes

Post by mandolini_ » Mon Aug 10, 2020 7:19 pm

Maybe this boolean switch is only the first mutable parameter that you introduce and more will follow in future?
That is correct. This is the first, and simplest mutable parameter I would like to change. I've only shown this one to get a general idea for how the rest should work.


The advice for [de]allocating memory in the constructor [destructor] was helpful, and was probably the biggest piece that I was missing.

Regarding valuechanged vs writeaccess: When your filter object lives in the runtime configuration and you update any parameter, then a new runtime configuration object is created and you lose the previous filter's state. This can be desirable or undesirable. Using writeaccess instead of valuechanged gives your users a tool to reset the filter's state without changing the processing mode.
So you are saying basically that writeaccess allows for the filter state to remain the same (which is desirable in my case), and valuechanged will reset its state after each update?

tobiasherzke
Posts: 27
Joined: Mon Jun 24, 2019 12:51 pm

Re: Advantages/disadvantages of using interface classes

Post by tobiasherzke » Tue Aug 11, 2020 4:11 pm

mandolini_ wrote:
Mon Aug 10, 2020 7:19 pm
So you are saying basically that writeaccess allows for the filter state to remain the same (which is desirable in my case), and valuechanged will reset its state after each update?
No, this is not what I meant to say. Sorry for not being clear enough.

If the initial state is filter=no, then your users can submit configuration command sequences like

filter=yes
# wait 5 seconds
filter=yes

If you register a writeaccess callback, then the second setting will replace the filter and reset the filter state.
If you register a valuechanged callback, then the second setting will have no effect.

By choosing writeaccess instead of valuechanged, you give your users the possibility to reset the filter state without changing the filter settings.

(If resetting the filter state needs to be avoided, then there are ways to avoid it, at least most of the times, but this is an advanced topic. First, finish implementing your runtime configuration class.)

Post Reply