Programming Reference:GenericFilter Class

From BCI2000 Wiki
Jump to: navigation, search

The GenericFilter class

GenericFilter is a base class that provides a programming interface for all user code inside the core modules of this BCI2000 implementation. Programming your own data acquisition module, your own filter inside Signal Processing, or your own application, implies deriving your own class from GenericFilter. GenericFilter's member functions represent the various initialization and processing events that occur during system startup and operation (see Technical Reference:States of Operation). Your own filter code must implement its own versions of some of these member functions:

Required Members


The Publish function publishes the parameters and states your filter wants to introduce into the system (the BEGIN_... and END_... macros handle the actual function calls):

     "MySection int MyParam= 1 "
       "0 0 3 // This is range-checked between 0 and 3",
     "MySection float MyOtherParam= 0.1 "
       "0 % % // This is not automatically range-checked",

     "MyState 1 0 0 0",

The data between the BEGIN_... and END_... macros are lists of comma-separated C-strings, which are additionally broken across lines to improve readability. For the compiler, anything is fine here that is allowed between the curly braces in

const char* array[] = { "entry 1", "entry 2", "entry 3", };

For successful filter instantiation at runtime, the strings must also be valid parameter and state definitions.

NOTE: Traditionally, parameters and states have been published from inside a filter's constructor. For more flexibility, a separate Publish() function has been introduced. For backward compatibility, parameters and states may still be published from the constructor, but this is deprecated for new code.


The Preflight function checks whether the preconditions for successful operation are met. This function is called whenever parameter values are re-applied, i.e., whenever the user presses "Set Config" in the operator window. If Preflight does not report an error, this counts as a statement that Initialize and Process will work properly with the current parameters. The first argument to Preflight will inform you about what kind of input signal your filter is going to receive, and your filter is expected to report the properties of its output signal via the second parameter:

 void MyFilter::Preflight( const SignalProperties& Input,
                                 SignalProperties& Output ) const
   PreflightCondition( Parameter( "MyOtherParam" ) > 0.0 );
   PreflightCondition( Input.Channels() > 0 );
   Output = Input;

Note that the const keyword following the function argument list forbids altering any data member of your filter object. This avoids diffusion of initialization code from Initialize into Preflight. If you have your own sub-objects instantiated and maintained by your filter, you should provide them with their own Preflight() const member functions, and call these from your filter's Preflight.

In the same way as other BCI2000 code does, the Preflight() function reports errors by writing descriptive text into the bcierr output stream.


Initialize is called after a successful Preflight. Thus, it may safely omit all checks related to parameter consistency. In Initialize, your filter's data members are set to the values implied by the user's choices, or to the initial values required for the filter's operation:

 void MyFilter::Initialize( const SignalProperties& Input,
                            const SignalProperties& Output )
   mMyParam = Parameter( "MyParam" );
   mMyOtherParam = Parameter( "MyOtherParam" );
   mCount = 0;


The Process function is called once for each block of EEG data. It receives an input in its first argument, and sets its output signal to values resulting from filter operation. In the current BCI2000 implementation, there is a single chain of filters--one filter's output signal is the next filter's input. A filter which does not perform any modification to the signal (e.g., a statistics filter) needs to copy its input signal into the output signal, as in the example:

 void MyFilter::Process( const GenericSignal& Input,
                               GenericSignal& Output )
   if( Input( 0, 0 ) > mMyOtherParam )
   Output = Input;

The Process function should not acquire or release resources (opening/closing files, allocating memory, etc). Natural places for such operations are the Initialize, StartRun, and StopRun member functions.

Optional Member Functions

Other member functions are optional; you may decide whether you override their default implementation with your own version, depending on what your filter needs to do:


AutoConfig is called each time the configuration has changed. It allows you to set parameter values according to your component's needs, and to perform initialization that is required in order to determine parameter values.

AutoConfig may be implemented by any component, but it is typically used by components that interface to hardware devices, such as EEG amplifiers. In AutoConfig, the component may set up a persistent connection to its device, and obtain information about the device's configuration. Then, this information may be used to adapt the component's parameter settings accordingly.

From AutoConfig, only such parameters may be adapted that have been published in the component's own Publish method. Further, AutoConfig parameter changes will be silently ignored unless a parameter's value is set to "auto", allowing user choices to override auto configuration. Typically, a component's auto-configurable parameters should be published with "auto" as a value (which should be the only entry in case of list- or matrix-valued parameters).

In case of parameters which are declared "readonly" (from the user's perspective), changes during AutoConfig will also persist if the original parameter value was empty, i.e. those parameters need not be set to "auto" explicitly.

When there are multiple auto-configurable parameters that depend upon one another, the number of cases to be dealt with will grow exponentially with the number of parameters: each single parameter may have a value assigned by the user, or may have been set to auto-configured, resulting in (2^N)-1 distinct auto-configuration cases for N such parameters. Thus, code that needs to deal with each of those cases explicitly would be difficult to write and maintain. To improve scaling of effort, parameter access in AutoConfig follows rules which may be difficult to grasp at first but greatly simplify the task of writing AutoConfig code:

  • Parameter assignment persists throughout the AutoConfig function, as if the parameter in question had been set to "auto" by the user.
  • An additional ActualParameter() accessor function provides read-only access to a parameter's current value, i.e. the user's setting, "auto", or a previously assigned auto-configured value.

As an example, consider two interdependent parameters, "MyParam" and "MyOtherParam". If "MyParam" has been user-configured, but "MyOtherParam" is set to "auto", "MyOtherParam's" value needs to be set to a value that depends on the value of "MyParam", and vice versa. Altogether, four cases are possible, and three of those require auto configuration. Using the rules above, we may write AutoConfig code that only grows linearly with the number of parameters involved, rather than exponentially, in the following way:

  • First, assign sane and consistent initial values to all parameters,
  • then, assign each parameter again as if the assigned parameter were the only one to be auto-configured, using the remaining parameters' actual values.
Parameter("MyParam") = ...;
// omit redundant initial assignment of MyOtherParam
Parameter("MyOtherParam") = SomeFunction( ActualParameter("MyParam") );
Parameter("MyParam") = InverseOfSomeFunction( ActualParameter("MyOtherParam") );

In case of three interdependent parameters, we have:

Parameter("MyParam1") = ...;
Parameter("MyParam2") = ...;
// omit redundant initial assignment of MyParam3
Parameter("MyParam3") = SomeFunction12( ActualParameter("MyParam1"), ActualParameter("MyParam2") );
Parameter("MyParam1") = SomeFunction23( ActualParameter("MyParam2"), ActualParameter("MyParam3") );
Parameter("MyParam2") = SomeFunction13( ActualParameter("MyParam1"), ActualParameter("MyParam3") );


StartRun is called when the system enters the running state. As opposed to Initialize, which is the place for tasks that need to be performed on each parameter change, StartRun is provided for tasks that need to be performed each time the user clicks "Run" or "Resume" in the operator window. As a canonical example, the DataIOFilter opens a new .dat file from its StartRun member function.


StopRun is called each time the system leaves the running state, entering the suspended state. Typically, this happens whenever the user clicks "Suspend" in the operator window. The DataIOFilter provides an example for its usage: This filter closes the current .dat file from its StopRun member function.

StopRun is also the only function from which a filter may change a parameter value. Any parameter changes inside StopRun will propagate to the other modules without any explicit request from your side.


Resting is called instead of Process while the system is in suspended state. Typically, Resting is called repeatedly for filters inside source modules; in the remaining modules, Resting is called once when the system enters suspended state. Except that it is called at least once in suspended state, you should not make any assumption how often Resting is called.


The Halt function is called before any reconfiguration of the system takes place. If your filter initiates asynchronous operations such as playing a sound file, acquiring EEG data, or executing threads, its Halt member function should terminate all such operations immediately. Failure to do so might result in a non-responding module, or in other errors difficult to track down. For descendants of GenericADC, implementing the Halt function is mandatory.


Your filter's Destructor should free all resources acquired in the Constructor or in Initialize. In many cases, freeing of resources will be done automatically if you use direct members instead of pointers, removing the need for a destructor. However, if your filter has a non-empty Halt function, it needs a destructor that calls Halt - this can not be done from the base class destructor because overridden virtual functions cannot be called from base class constructors or destructors.



Unlike the previous members of GenericFilter, this function represents a property rather than an event. Through its return value, a filter can decide whether its output may be offered for visualization by the framework, i.e. whether it appears in the Processing Stages section in the operator's Visualize tab.

AllowsVisualization is called once at instantiation, and then after each Initialize() that occurs. If it returns false at instantiation, no visualization parameter is created for the filter. If it returns false at initialization time, no default visualization is displayed, independently of the visualization parameter's value. The filter itself may still maintain its own visualization parameters and GenericVisualization object.

Rules for Parameter and State access

For consistent and robust overall system operation, it is crucial that individual filters actually check parameter values and state accessibility from their Preflight() function. To support this idea, the BCI2000 framework imposes a set of rules to parameter and state access:

  • Parameters and states accessed during initialization or processing must be accessed during the preflight phase. Otherwise, an error message will be displayed when the first access occurs.
  • To avoid pointless cludging of the Preflight() function, there are some exceptions to this rule:
    • States defined in a filter's constructor are known to be present, and thus need not be accessed from that filter's Preflight() function.
    • Parameters defined in a filter's Publish() function will be range-checked automatically if any of their LowRange or HighRange properties is set, and need not be accessed explicitly from Preflight().
    • To disable automatic range checking, set both these properties to the empty string in your parameter definition line:
      MySection int MyParam= 1 0 % % // a parameter that is not automatically range-checked
      This implies that you need to implement your own range-check in Preflight(). If there are no actual constraints on the parameter's value, just add a line Parameter( "MyParam" ); to the filter's Preflight() function.
  • Expressions may involve state names, and thus need to be evaluated in order to check whether referred-to states are present. Just add a line
    Expression( Parameter( "MyExpressionParam" ) ).Evaluate();
    to the filter's Preflight() function.