BCI2000
BCI2000
BCI2000
This tutorial walks you through the process of obtaining the BCI2000 source distribution, and using it to
build and test your own custom filters, implemented in C++ inside your own custom core module. It
assumes that you have a good working knowledge of the C++ language, and basic familiarity with the
compiler/IDE that you are going to use.
Many of the specific instructions below will assume that you are on a 32-bit Windows system, are using
Microsoft's free Visual C++ 2010 Express compiler, and have checked out the BCI2000 distribution to a
location C:\BCI2000\3.x on your hard-drive. However, the same steps are valid for other supported
setups, with the appropriate setup-specific changes.
Contents
[hide]
1 Prerequisites
2 Setup
7 Exercises
8 Further reading
Prerequisites
1. First, you need a complete BCI2000 distribution that includes the src and build directories. If
you already have this, use SVN to make sure it is updated to a recent stable version (at least, for
this tutorial to make sense, you will need revision 3279 or later / May 2011 or later). If you do not
already have the source distribution, here is how you obtain it:
1. If you do not already have a bci2000.org username, get one here. Your password will be
mailed to you.
2. To download the BCI2000 source-code, you will need an SVN client. If you do not have
one, for Windows we recommend TortoiseSVN, which can be downloadedhere. The
BCI2000 wiki contains more documentation about how to set up and use SVN.
3. Use SVN, with your username and password, to check out the latest BCI2000 source-
code-included distribution, the location of which
ishttp://www.bci2000.org/svn/tags/releases/current (note this is the current release
version, not the current source code -- for the current source code, check out
from http://www.bci2000.org/svn/trunk). We will assume that you checked out to a
location C:\BCI2000\3.x on your hard-drive. Wherever you see this path below,
adjust it so that it reflects the location you actually used. More information about the
layout of the resulting distribution can be found here.
2. Next you will need to download and install CMake if you do not already have it (at least version
2.8.3 is recommended). CMake can be downloaded here. Make sure you select the option add to
PATH for all users when installing.
3. Finally you will need a C++ compiler. We will proceed on the assumption that you are on 32-bit
Windows and using the free Visual C++ 2010 Express environment which can be
downloaded here (choose either the Visual C++ web-installer or, for something that can be
installed offline, the option marked "Visual Studio 2010 Express All-in-One ISO" - this will give you
an .iso file which can be mounted as a virtual drive using a tool like Virtual Clone Drive). Consult
the documentation for the BCI2000 build system for information on other supported compilers,
and more about CMake.
You should now have everything you need in order to build and customize BCI2000.
Prerequisites (Linux)
1. C++ compiler
2. Subversion
3. CMake
Prerequisites (OS X)
3. Qt libraries (You only need the libraries, not the full SDK. We have performed successful builds
with v4.7.2. Your mileage may vary according to the version you use.)
Then follow the "unix command-line equivalent" instructions from the Terminal.app command-line.
NOTE: CMake may be used to create an XCode project file for BCI2000, using the appropriate generator
option. However, it seems that XCode builds of BCI2000 do not always succeed, even if there is no
problem with Unix makefile builds on the same machine. Thus, you should always try a command-line build
when building fails under XCode.
$ cd ~ # or wherever
$ svn checkout http://www.bci2000.org/svn/trunk bci2000 # (or call it
whatever you want)
Setup
1. Use CMake to build a project file. This is done by going to C:\BCI2000\3.x\build (which is
the main workbench from which all build operations occur) and launching one of the " Make ...
.bat" batch-files. If you are using Visual C++ 2010 Express, the appropriate batch file is Make
VS10 Project Files.bat . It may ask you several questions: feel free to answer "y" to
building the "tools", "contrib" and "BCPy2000" components. For now, for simplicity, you may wish
to say no to framework extensions. Also say no to modules that require MFC (unless you are
using a commercial version of Visual C++ Studio that includes MFC). If successful, you should
see a long list of sub-projects being created, and after several seconds of this, "Configuring done"
followed by "Generating done".
2. Open the resulting solution file, BCI2000.sln , in Visual C++ Express. The file should be located
in C:\BCI2000\3.x\build .
3. Ensure that you are in "Release" mode rather than "Debug" (in Visual C++, this is a drop-down
menu in a toolbar near the top)
4. You probably do not need to build all of BCI2000 at once. To use BCI2000, the minimum you will
need consists of the Operator, plus at least one SignalSource module, at least one
SignalProcessing module, and at least one Application module. To build a single module in Visual
C++ Express, right-click on the module's name in the list on the left, and select "Build". The initial
build will typically take a few minutes per module. If you do not intend to modify a given module,
you can always skip building it by copying the corresponding ready-built binary (for
example, Operator.exe) from the prog subdirectory of a BCI2000 v.2 or v.3 binary
distribution, and paste it into the corresponding directory of the distribution in which you are
working, i.e. C:\BCI2000\3.x\prog . (To download the latest binary distribution, you will need
to supply the same username and password that you used for SVN download.) Let's assume you
have built, or copied, the following
modules: SignalGenerator, ARSignalProcessing,CursorTask and Operator, as well as the
necessary OperatorLib shared library.
5. Build the NewBCI2000Module, NewBCI2000Filter and NewBCI2000FilterTool targets for use in
the subsequent steps. The result will be new
executables,NewBCI2000Module.exe, NewBCI2000Filter.exe and NewBCI2000Filte
rTool.exe, in the C:\BCI2000\3.x\build directory.
$ cd ~/bci2000 # or wherever
$ cd build
$ ./Make\ Unix\ Makefiles.sh
$ make NewBCI2000Module NewBCI2000Filter NewBCI2000FilterTool
1. Navigate to C:\BCI2000\3.x\build
2. Find NewBCI2000Module.exe there, assuming you have already built it (see "Setup", above).
1. What type of module are you creating? Enter "1" for a SignalSource module, "2" for a
SignalProcessing module, or "3" for an Application module.
2. What should be the name of the new module? This should be single word, without spaces
or punctuation (For example: VeryNiceSignalProcessing. Or, at your option, something
even more informative.) This name will be used as the name of a new directory that will
contain the project source information, as well as for the resulting binary (.exe file).
3. Where (i.e. inside what parent directory) should the new project directory be created? You
can express this as a relative path, in which case it is interpreted relative to
the build directory where NewBCI2000Module.exe itself is running. To accept the
default answer, which is ..\src\custom, just press return. This location (or some
subdirectory of it) is a good choice: it maps to C:\BCI2000\3.x\src\custom which
is an area reserved for your own projects.
4. Now re-run CMake (see step 1 of "Setup", above). You should see your new project being added
at the end of the long list of projects. Re-open the project file (step 2 of "Setup") and you should
see it listed in the alphabetical list of projects.
5. If you have just created a new SignalSource project, a specialized source-acquisition filter (called
an Analogue-to-Digital Converter or ADC) will already have been added to the project. If your
project was called Foo or FooSource this will be instantiated in files
called FooADC.cpp and FooADC.h. You can find this by expanding the tree (click on the plus
sign) next to your project name in Visual C++. Find the files under "Source" (or "Headers") and
then "Project". Double-click them to edit. You should already be able to compile the module: try it.
6. If you have created a SignalProcessing or Application project, you may be able to compile it, but it
will not do anything until you add at least one filter to it (see next section).
$ cd ~/bci2000 # or wherever
$ cd build
$ ./NewBCI2000Module 2 VeryNiceSignalProcessing ../src/custom
$ ./Make\ Unix\ Makefiles.sh
$ make VeryNiceSignalProcessing
1. Navigate to C:\BCI2000\3.x\build
2. Find NewBCI2000Filter.exe there, assuming you have already built it (see "Setup", above).
1. What type of filter are you creating? Enter "1" for a subclass of BufferedADC (although if
you created a SignalSource project with the NewBCI2000Module tool, an ADC of this
kind will already have been created for you), enter "2" for a subclass of GenericFilter, or
"3" for a subclass of ApplicationBase.
2. What should be the name of the filter class? This should be a legal name for a new C++
class (no spaces or punctuation). If you enter, for example, "FooFilter", then a class of
this name will be implemented in two new files, FooFilter.cpp and FooFilter.h .
3. To which project (i.e. in which project directory, relative to build) should the filter be
added? For example: ..\src\custom\VeryNiceSignalProcessing (as in all of
the BCI2000 framework, directory slashes are allowed to go this / way or that \ way
regardless of whether you are on Windows or not)
5. Find your new files under the Source/Project and Headers/Project trees by clicking on the + sign
next to the project name. Double-click them to edit. Read the comments in the file for help as to
how to flesh out the various filter methods.
6. Try building the project (right-click on the project and select "build"). SignalSource and Application
projects may already build successfully. SignalProcessing projects, or any project to which you
have added a new GenericFilter subclass, will have at least one deliberate #error in the code.
When the build attempt finishes, double-click on the error message to go to the offending line.
Read the comments in the file, and you will see that the error is there to force you to think about
the ordering of the filters in your module. Once you have resolved this issue, simply remove the
#error line and select "build" again (the process will be quicker this time: only previously unbuilt
files or newly modified files will be compiled).
$ cd ~/bci2000 # or wherever
$ cd build
$ ./NewBCI2000Filter 2 FooFilter ../src/custom/VeryNiceSignalProcessing
$ ./Make\ Unix\ Makefiles.sh
$ make VeryNiceSignalProcessing
1. Locate successfully built modules, which will appear as .exe files inside the top-
level prog directory of the BCI2000 distribution, for example C:\BCI2000\3.x\prog
2. The most basic way to launch BCI2000 is to double-click on each module in turn. Start
with Operator.exe, then launch one SignalSource module, one SignalProcessing module, and
one Application module (your new module will fill one of these three roles, but all three are
required).
3. If you have a firewall running on your machine, dialogs may open for any modules that the firewall
has never seen before. If so, click "unblock" to proceed, for each one. You should only ever need
to do this once per newly-built module. (Check that your firewall is not configured to block network
connections without telling you.)
4. In the Config dialog, under the Visualize tab, you should see a check-box for visualizing the
output of all the filters in the chain, including your newly created filter. You may find it useful,
whenever you are developing a new filter, to visualize your filter input and output simultaneously
(i.e. visualize both the new filter and the filter that immediately precedes it in the chain).
Configuring your new filter for offline use
This is an optional step which many developers of new custom filters will find useful. Filters may be
compiled singly as standalone executables to allow them to be tested and used for data analysis offline.
The resulting "filter tools" can be used either from the DOS/Unix command-line or, more comfortably, from
the Matlab command-line.
1. Navigate to C:\BCI2000\3.x\build
2. Find NewBCI2000FilterTool.exe there, assuming you have already built it (see "Setup",
above).
3. Launch it, and tell it which existing C++ file contains the filter definition (as always, specify the path
relative to the build directory—for
example,..\src\custom\VeryNiceSignalProcessing\FooFilter.cpp)
4. The tool will perform for you the necessary alterations to the CMakeLists files. (The procedure,
details of which can be found here, involves creating a subdirectory calledcmdline inside your
project directory, because specifying a new executable target in the same CMakeLists.txt file
as your module would cause a misconfiguration problems for either the module or the filter-tool. )
5. Close BCI2000.sln, re-run CMake, and re-open BCI2000.sln. You should now have a new
target, whose name is simply the name of the filter (e.g. FooFilter).
6. Right-click on the new target and select "build". The resulting binary will appear as (for
example) ..\tools\cmdline\FooFilter.exe
$ cd ~/bci2000 # or wherever
$ cd build
$ ./NewBCI2000FilterTool
../src/custom/VeryNiceSignalProcessing/FooFilter.cpp
$ ./Make\ Unix\ Makefiles.sh
$ make FooFilter
$ ../tools/cmdline/FooFilter --help
$ make bci_dat2stream bci_stream2mat # you'll also need these
Exercises
Exercise 1: RMSFilter
2. Use NewBCI2000Filter to create a new filter called RMSFilter in your new module.
3. NewBCI2000Module and NewBCI2000Filter report that they are creating and altering various files
and directories. Write down what you think is the purpose of each new directory, new file, or
alteration to an existing file.
4. Write a filter called RMSFilter, which takes in multiple signal channels, and outputs a single
channel containing the root-mean-square signal across all input channels. (Advanced variant:
introduce a parameter which allows the user to specify groups of channels; then output one RMS
signal per group.) Test the filter using theSignalGenerator module as an input, configuring the
SignalGenerator to respond to mouse movement as described in the documentation. First
visualize just the input of your new filter (the output of the previous filter, which will presumably be
the TransmissionFilter). Then draw on paper what you would expect to see as an output of your
filter in response to different mouse positions/actions. Finally, visualize your filter's output in order
to verify that it matches your expectations.
5. Visualization allows you to test your code's behaviour qualitatively to some extent. In what various
different ways could you verify, in a more precise, quantitative way, that your implementation is
correct? Under what circumstances should you spend the extra time to do this? (hint: the answer
rhymes with "hallways")
Exercise 2: Debugging
1. Write a batch file to launch your combination of modules. You may wish to use one of the existing
files in the top-level batch directory as a template. A batch file will allow you to go around each
edit-compile-debug cycle much faster and more reliably. It will also allow you to pass useful
command-line flags to the modules as you start them. And finally, if you need to set parameters in
a certain way on each launch, we strongly recommended that you take advantage of operator
scripting in your batch file to ensure that the required parameters are loaded automatically from a
file on each launch. Inconsistent behaviour from one debug cycle to the next can often be
attributed to having forgotten to perform the menial task of loading parameters manually.
1. In your batch file, comment out or remove the line that launches the module you want to
debug.
2. Again in your batch file, assuming you are using the SignalGenerator, FilePlayback, or
some other source module which does not have to run in real-time, add the flag --
EvaluateTiming=0 to the call that launches the source module (see example snippet
below). Putting a debug breakpoint in your Process() method will slow the system to
below real-time, and we do not want the framework's real-time check to terminate the
debug session for this reason.
3. Launch the batch file, thereby launching all the necessary modules except one, and
loading any parameters needed for the debug session.
4. Note that there are different "build modes" for the BCI2000 solution, with names like
"Release" and "Debug". In order to debug a particular module, you will need to ensure
that the module is built in either "Debug" mode or "RelWithDebInfo" mode. If you are in
the wrong mode for debugging, select the correct build mode from the drop-down menu
on Visual C++ Express's toolbar, then build your module again. Note that the BCPy2000
modules (PythonSource, PythonSignalProcessing andPythonApplication) cannot be built
in Debug mode. Also, Debug mode can sometimes mask dangerous bugs, so you may
experience the frustration of trying to investigate a crash only for it to stop happening
when you switch from Release to Debug. Finally, a BCI2000 module built in Debug mode
may exhibit poorer timing performance. For these reasons it is not generally advisable to
use Debug mode as the default mode of your project, and RelWithDebInfo is often
advisable when debugging (even though the compiler optimizations may lead to a less
logical-seeming debugging experience).
5. In Visual C++, set a breakpoint in the source file you want to debug.
6. Although CMake directs Visual C++ to create the module in the correct
directory, C:\BCI2000\3.x\prog, it is unable to set the working directory in which
the debug instance runs to the same value. Therefore, if your module needs to load
resources like image or sound files, and expects to find these at the end of a path that is
expressed relative to prog, you will need to set the prog directory as the module's
working directory by hand: Right-click on the module -> Properties -> Configuration
Properties -> Debugging -> Working Directory
7. In Visual C++, right-click on the module, then select "Debug" followed by "Start New
Instance" (if you find this too fiddly to do with the mouse, right-click, then press "g", then
press "s" ). If the build is not up-to-date, Visual Studio will prompt you: in this case, allow
it to build the module.
8. You are now debugging BCI2000. Verify that the debugger stops your module at the
breakpoint you specified and at the time that you expected. Browse the available
variables and their properties.
1. Write a filter called DiffFilter, in which each output channel contains the numerical derivative, with
respect to time, of the corresponding input channel. Note that your Process() method only sees
one discrete chunk (or SampleBlock) of signal at a time. Is this a problem? Use private member
variables of your filter instance as a "scratch-pad" where necessary.
2. Assemble the pre-existing ExpressionFilter, followed by your DiffFilter, followed by your RMSFilter
(from exercise 1), in that order within one SignalProcessing module. The ExpressionFilter
implementation is already part of the framework, so all you need to do is uncomment
the #include and Filter() statements that correspond to it inPipeDefinition.cpp.
Read the comments in PipeDefinition.cpp, and
the Programming_Reference:Filter_Chain wiki page, for more information about linking filters in a
chain.
3. Make sure your SignalSource module is started with the --LogMouse=1 flag (see the code
snippet above).
4. Set the Expressions parameter (found in the Filtering tab of the Config dialog) such that it has
two rows and one column, and contains the expressions "MousePosX" and "MousePosY".
5. Write down a full description of how you think the SignalProcessing module will process the signal,
from start to finish, and exactly how you expect it to respond to different kinds of mouse
movement.
6. Verify that the module behaves the way you expect, under all relevant input conditions.
Exercise 4: Offline filter-chain reconstruction in Matlab
1. Take one or more of the filters you have written (RMSFilter, DiffFilter), or create a new one which
also does something whose numerical output can be checked very easily (examples: squaring the
input signal, or doubling it, or taking the absolute value).
4. Learn how to use the matlab function bci2000chain and the supporting Matlab tools.
5. Use bci2000chain to run your filter on some example data, or even on some toy data that you
create using create_bcidat. Then, separately, write a Matlab function for performing the same
numerical operation that you believe your filter performs. Compare the two outputs and verify that
your filter does exactly what it is intended to do, at least to within some very small numerical
tolerance. The maximum absolute difference between the two outputs should be very small (say,
1e-10 or less, depending on the operations and on the magnitude of the input signal).
Contents
[hide]
1 Introduction
2 Supported Environments
6 How To Test
7 Known Issues
8 Conclusions
o 10.1 Linux
o 10.2 OS X
11 See also
Introduction
Previous versions of BCI2000 (2.x and earlier) were dependent on the visual components library (VCL) by
Borland and could only be built using the Borland compiler. For version 3.0, we made BCI2000 compiler-
independent, and therefore had to choose a portable replacement for VCL. Qt was chosen to replace VCL
because it is not only compiler-independent, but also platform-independent.
Supported Environments
Macintosh OSX: Compiles and passes standard tests on 10.5 and newer systems
Supported Compilers
Visual Studio 9 (2008) and 10, download free Express version from here (although some BCI2000
modules require MFC, most modules may be built using the Express version)
Supported IDEs
Qt Creator
Other IDEs supported by CMake generators (Eclipse CDT, ...), as long as these use the compilers
listed above
Setting up MinGW to build BCI2000
This section only applies when you want to use MinGW as a compiler to build BCI2000. If you are using a
different compiler/IDE, please proceed to the next section.
If you don't care whether your BCI2000 executables are statically linked against Qt libraries, you may
use any recent version of MinGW. You will need to install Qt separately from BCI2000, and you will
need to follow the instructions in the next section, "Building against a Qt distribution outside the
BCI2000 source tree".
If you want to statically link BCI2000 against Qt libraries using any version of MinGW, you will need to
install Qt separately from BCI2000, and recompile it from source with the "static" configuration flag as
described here. Then, follow the instructions in the next section, "Building against a Qt distribution
outside the BCI2000 source tree".
If you want to use the pre-compiled static Qt libraries in the BCI2000 source tree, you will need to
install a compatible version of MinGW. Currently, this is MinGW with gcc 4.4.0, which can be
downloaded from [1]. Extract this file into any path on your local hard drive, and add its "bin"
subdirectory to your system path variable so MinGW commands are recognized when entering them
from the command line.
How To Build Using CMake
1. Ensure that your compiler and IDE are installed on the computer. This means that Visual Studio is
installed if you intend to use Visual Studio, or that MinGW and Code::Blocks are installed if you
intend to use Code::Blocks as your IDE and MinGW as your compiler.
2. Download and install cmake (Version 2.8.2 or higher!) from http://www.cmake.org/ - Add to path
for all users. -- If you experience a problem with a version of cmake newer than 2.8.3, download
cmake 2.8.3 from here.
3. Go to the BCI2000/build directory and double-click the "Make ... .bat" file which best describes
your intended platform. (for example, if you plan to use Visual Studio 2008, you would run the
"Make VS2008 Project Files.bat"). These batch files will ask you a few questions about which
parts of the BCI2000 distribution you want to make, and will then call CMake with the appropriate
options.
4. Wait while cmake examines your computer, finds Qt and your compiler, and generates applicable
project files for your system
5. Open the generated project/solution (for Visual Studio, this is called "BCI2000.sln") in the IDE of
your choice. Or, if you are using MinGW make, run the make command from the command
prompt using the Makefile in the top-level "build" directory. Refer to your IDE's help for IDE-
specific instructions on how to build an application.
6. Even though CMake must always be run from one top-level place (the "build" directory), and will
spend several seconds generating a Makefile or project file for the whole BCI2000 tree at once,
this does not mean that you have to compile all of BCI2000 at once. In the next step, if you're
using MinGW-make on the command-line, you could for example type "make Operator" or "make
SignalGenerator" to build only selected modules (again, you should be in the top-level "build"
directory when you do this). Visual IDEs usually have their own equivalent of this: for example, in
Visual Studio 2008, you can right-click on a particular module and select "Build".
7. If you build the targets"NewBCI2000Module" and "NewBCI2000Filter", this will create two new
binary executables with these names. They will be found in the top-level "build" directory (and, in
common with CMake and make, they should be launched only from this location). Launch
NewBCI2000Module to create a new project in order to compile your own custom module. Launch
NewBCI2000Filter to create a new filter and add it to a project you have already created. In either
case, you may need to re-run CMake (step 3) in order to ensure these changes are reflected in
the project file.
Building against a Qt distribution outside the BCI2000 source tree
BCI2000 comes with a stripped-down, precompiled version of Qt, which is downloaded by CMake at
configuration time. This is a static version of the Qt libraries, and allows to distribute the resulting
executable without hindsight to DLL dependencies. The complex and annoying implications of such
dependencies has been termed "DLL hell" for good reasons, so we strongly recommend linking against the
BCI2000-specific Qt libraries for Windows builds.
When running CMake, it determines the current compiler/OS combination, and tries to download an
appropriate version of the BCI2000-specific Qt build. If no such build is available, it expects a local Qt
installation to be available, and fails with an error message if this is not the case.
If a BCI2000-specific Qt build is available for the current configuration, you will still have the option of using
a locally installed version of Qt. This is a CMake optionUSE_EXTERNAL_QT appearing in
the CMakeCache.txt file after CMake has been run for the first time. Such options may be changed
using the CMake GUI, or a text editor. After changing the option, run CMake again to apply the change to
the created project files.
How To Test
It is important - especially if you're using an unsupported compiler/IDE - to test your executables once
they've been built to make sure they function properly.
To run testing:
1. Ensure that the entire BCI2000 project has built successfully and that executables exist in
the BCI2000/prog directory, the BCI2000/tools/cmdline directory, and
the BCI2000/build/buildutils/test directory.
A Note on found differences: differences may not indicate that you have a broken build. Sometimes
different compilers handle the precision of floating point numbers differently than others. These can
account for small differences in calculated signal or state values. The default reference files were
generated using an MSVC build on 32 bit Windows. If your compiler is different, this may not be a problem.
Known Issues
MinGW, Borland and other single configuration generators within CMake only generate one
configuration at CMake Run-time. By default, this is set to the release configuration. It can be set -
along with specific compiler options -
in BCI2000/build/cmake/BuildConfigurations.cmake. The Visual Studio generator will
ignore settings in this file. To turn on a debug build in a single configuration generator, run cmake -
i in the build directory and set CMAKE_BUILD_TYPE to "Debug" when prompted.
All Compilers handle non standard characters, such as umlauts and characters with accents or tildes,
differently. Because BCI2000 currently has no standardized way of handling non standard characters
in a cross-compiler environment, it is strongly recommended that - for the time being - special
characters are not used in localizations during the development of BCI2000 Ver 3.0.
Conclusions
Now that BCI2000 is open to a number of platforms, and compilers, support may not exist for every
possible compiler/platform available. Certain compilers do not optimize code as well as others, and this
behavior may lead to poor system latencies during BCI2000 experiments. The supported compilers have
been rigorously tested and confirmed to be adequate for compiling the BCI2000 sources. If you wish to use
a different compiler, be sure to run tools/BCI2000Certification in order to confirm your setup. CMake is a
powerful tool, but in the end, ability to compile the sources is completely up to the IDE/compiler choice. If
your IDE/compiler choice is not listed above, it is strongly urged that you to consider using one which is
supported. If you run into problems using an unsupported IDE/compiler combination, you can try to find
help at the BBS -http://www.bci2000.org/phpbb/index.php. BCI2000 should compile as effortlessly as
possible on supported platforms.
Note that the Qt libraries provided in the BCI2000 source tree are for Windows only, so you need to
separately install Qt on your system before compiling BCI2000.
Linux
Executable tests are passed on x86 and amd64 architectures running Debian Squeeze (currently "Stable")
and Wheezy (currently "Testing").
OS X
BCI2000 builds successfully in OS X Leopard and Snow Leopard using the CMake generating script
at build/Make Unix Makefiles.sh. Executable tests run successfully on OSX, both in 32 and 64 bit
mode.
2 Filter Instantiation
For each module, the filter chain is documented in its own FilterChain parameter, which is located in
the System/Configuration section.
Filter Instantiation
Creating instances of all filter classes inside a module, and building the filter chain, is handled by the
framework. However, it needs a hint to determine the sequence in which filters are to be arranged. In
general, this hint consists of a single statement placed inside your filter's .cpp file:
In principle, the above scheme allows to you add filters to a module's filter chain without modification to the
existing source code, simply by adding a .cpp file with aRegisterFilter statement to the project.
However for Signal Processing modules, it is more desirable to have an explicit representation of the entire
filter chain centralized in one place. So, for each individual Signal Processing module there is one
file PipeDefinition.cpp that defines the filter chain as a sequence of Filter statements
(seeProgramming Tutorial:Implementing a Signal Processing Filter for an example). In other modules,
filters' default position will be suitable in general. Still, you may add aPipeDefinition.cpp with a
sequence of Filter statements to the module. In this case, positions defined
in PipeDefinition.cpp will take precedence over the filters' default positions.
The BCI2000 command-line tools can be used to recreate a filter-chain offline, from a system command-
line. The Matlab function bci2000chain, part of BCI2000's suite of Matlab tools, provides a convenient
interface to this from within Matlab.
This page gives an overview over the class hierarchies which matter to you when doing programming work
in BCI2000. It does not describe individual self-contained classes such as
the SignalProperties or GenericSignal; rather, it explains classes that are part of hierarchies, each playing a
certain role in its hierarchy.
Contents
[hide]
1 Types of Classes
2 Class Hierarchies
o 2.1 GenericFilter
o 2.2 EnvironmentExtension
o 2.3 ApplicationBase
o 2.5 Stimulus
3 See Also
Types of Classes
In BCI2000, class hierarchies consist of three types of classes: Interface class, mix-in classes, and client
classes.
Interface Classes
Interface classes, which provide an abstract interface for functionality that is then implemented by classes
derived from those interface classes. Due to the common interface, the BCI2000 framework can deal with a
great number of derived classes, serving their needs without even knowing which derived classes exist and
what they do.
Interface classes typically declare a number of so-called virtual functions. In the interface class, these
virtual functions do nothing specific; in most cases, they are not even defined, such that a derived class is
forced to implement its own version of that function. An interface class' virtual functions provide an interface
to their derived classes. In addition to virtual functions, an interface class also has plain member functions;
these provide an interface to classes that use objects of the interface class, rather than inheriting from it.
In BCI2000, there are a large number of interface classes that represent programming interfaces for filters,
data acquisition interfaces, file writers, application modules, and graphic objects. The most important
interface class is the GenericFilter class, which defines the programming interface for all BCI2000 filter
components that are part of the filter chain.
Most often, interfaces are built around the notion of an "event". When an event happens, it requires an
action. Actions are defined by client classes that implement an interface. Each virtual function in the
interface corresponds to an event, and derived classes implement those functions, thus providing event
"handlers". As an example, the GraphObjectinterface class declares a virtual function called OnPaint().
Individual graphic objects inherit from the GraphObject class, and implement the associated interface by
providing their own OnPaint() function. Whenever a window needs redrawing, graphic objects are asked to
handle the Paint event, i.e. for all graphic objects tied to the respective window, theOnPaint() event handler
is called. In its OnPaint() function, a GraphObject descendant class that represents a circle will provide
code that draws a circle, while a GraphObjectdescendant representing a text field will implement
an OnPaint() event handler that draws text.
Most often, classes directly inherit from a single interface class only, e.g. when you write a BCI2000 filter
component, it inherits from the GenericFilter interface class. However, it is not uncommon that there is a
hierarchy of interface classes which all build upon each other, where the line of inheritage represents
increasing specialization. As an example, all BCI2000 filters inherit from the GenericFilter interface class.
However, data acquisition filters do not inherit from GenericFilter directly; rather, they inherit from a class
calledGenericADC, which in turn inherits from GenericFilter, i.e. it specializes the GenericFilter interface for
data acquisition components. In addition, there exists an interface class called BufferedADC which
provides buffering, and an interface for data acquisition components that write their acquired data into a
ring buffer. A class that inherits the BufferedADCinterface thus implements an interface that has been
specialized all the way down from GenericADC and GenericFilter to BufferedADC.
Mix-in Classes
Unlike interface classes, which provide a more-or-less empty framework to be filled in by descendant
classes, mix-in classes do actually implement functionality. This functionality is contained in non-virtual
functions which may be declared "protected" to make them accessible only to classes that inherit from the
mix-in class. An example of such a mix-in class in BCI2000, is the Environment class, which provides
access to the Parameters and States that exist in the system. As GenericFilter inherits from Environment,
most BCI2000 components inherit from Environment without explicitly asking for it. However, any class that
wants to access parameters or states may inherit from the Environmentclass. Another example of a mix-in
class is the ApplicationWindowClient class. To its inheritants, it provides access to application windows,
taking care of window creation and deletion, and allowing filters in the application module to share windows
for drawing.
Client Classes
Client classes are at the bottom of the inheritance hierarchy. They implement inherited interfaces, and use
the functionality provided by mix-in classes, to actually get something done. E.g., the LinearClassifier class
implements the GenericFilter interface, using the mix-in functionality of the Environment class, resulting in a
BCI2000 component that applies a linear classifier to a stream of data.
The StimulusPresentationTask class implements the StimulusTask interface, which in turn is a
specialization of the GenericFilter interface, and inherits functionality from the ApplicationBase base class.
Class Hierarchies
GenericFilter
GenericFilter is an interface class that serves as a base class for all BCI2000 filter components, i.e.
components that process signals in the filter chain. GenericFilter also inherits from the Environment class,
and thus provides the ability to access parameters and states to its descendants. Specializations to
GenericFilter comprise GenericADC, and BufferedADC.
EnvironmentExtension
EnvironmentExtension is an interface class for BCI2000 components that do not process signals, and are
not part of the filter chain, but need to be notified of BCI2000 events such as Preflight, Initialize, or
StartRun. Typical descendants of EnvironmentExtension are utility classes such as LogFile or
RandomGenerator.
ApplicationBase
This class inherits from GenericFilter, and serves as a base class for application task filters in application
modules. To these, it provides a logging facility in form of the AppLog member object, and a random
number generator in its RandomNumberGenerator member object. Two specializations of ApplicationBase
exist that provide specialized interfaces to certain types of task filters: FeedbackTask, and StimulusTask.
These two interface classes define events that are specific to task filters proving feedback, and doing
stimulus presentation, respectively. E.g., the FeedbackTask defines an event FeedbackBegin which should
be handled by displaying, say, a feedback cursor on a feedback screen, while the StimulusTask defines an
event StimulusBegin, which is triggered when a stimulus is being displayed.
Stimulus
In stimulus-based application modules, an interface is needed for classes that represent stimuli. The
interface defines that stimuli may be presented, or hidden. A Stimulus descendant implements this
interface by providing appropriate code to present itself (showing an image on a display, or playing a
sound).
GenericSignal Class
Location
src/shared/types
Synopsis
Many classes in both Data Acquisition and Signal Processing work on signals, i.e., continuously flowing
data organized into channels and samples. The GenericSignal class contains floating point data
organized as a matrix of channels and "elements" (a generalized notion of samples -- e.g., spectrally
analyzed data might contain the spectrum of each channel as a list of "elements").
Methods
GenericSignal(int Channels, int Elements, SignalType=int16)
Initializes a GenericSignal object to the specified number of channels and elements. The signal type
argument may be one of
SignalType::int16,
SignalType::int32,
SignalType::float32,
SignalType::float24.
GenericSignal(SignalProperties)
Initializes a GenericSignal object to the properties defined by the SignalProperties object.
WriteToStream(ostream)/ReadFromStream(istream)
Read/write from/to std::iostream streams in human-readable ASCII format.
WriteBinary(ostream)/ReadBinary(istream)
Read/write from/to std::iostream streams as a BCI2000 signal message.
Properties
SignalProperties Properties (rw)
The signal's properties, contained in a SignalProperties object.
SignalProperties Class
Location
src/shared/types
Synopsis
Sometimes, the number of channels, elements, and bytes required for storing values, are referred to as
"Signal Properties". There is a separate class, SignalProperties, for expressing those values, and
determining whether a given signal "fits" into another one, i.e., whether the values contained in one signal
may be copied into another signal without loss of information. GenericSignal class uses
a SignalProperties member to maintain information about its properties.
Methods
SignalProperties(Channels, Elements, SignalType=int16)
Initializes a SignalProperties object to the specified number of channels and elements. The signal
type argument may be one of
SignalType::int16,
SignalType::int32,
SignalType::float32,
SignalType::float24.
bool Accommodates(SignalProperties)
True if a signal of the argument's properties can be represented by a signal with the object's properties
without data loss, i.e. the number of channels and elements must at least match the argument's, and the
numeric range defined by the signal's type must at least equal the argument's.
These interpretations are tried in the above order; if none matches, a negative index is returned, indicating
failure to interpret the address.
This function is provided to simplify conversion of parameter values into indices, allowing users to use
labels, physical units, or one-based indices as appropriate for the signal that is to be addressed.
Properties
int Channels, Elements (rw)
The number of channels and elements.
SignalType Type (rw)
The signal's numeric type.
bool IsEmpty
True if at least one of channels and elements is zero.
Handling Errors
Types of Errors
We assume that all errors we need to consider fall into one of the following categories, each of which
implies a different type of approach to error avoidance/error handling:
In BCI2000, this translates into a thorough parameter check done by each module before any parameter
settings are actually applied to the system.
Range and Consistency checks, whereby ranges generally depend on the values of other parameters;
Signal property checks: Does the output signal of one filter meet the next filter's requirements for its
input signal?
Resource availability checks:
Are needed system resources available? (E.g., is it possible to open a required sound output
device?)
Do output files have legal file names? Are output files writeable? (We could even check whether
there is enough space left to write the EEG file, but this is not practical because a concurrent
process might use up the space while our system runs.)
In each of these cases, the user should get appropriate feedback guiding her towards fixing the problem.
Whenever the system tries to fix a parameter setup error by using some default set of parameters, it should
do so only if
it presents the user with a warning that tells her what it did and why it did so, and if
the automatically fixed parameters are treated as if changed by the user, i.e. with a parameter check
performed on them.
Otherwise, people may unknowingly using a system that doesn't do what they want it to, or have a system
that creates new parameter inconsistencies when trying to fix others.
Runtime Errors
Definition of the Term
This category covers everything that can go wrong in the course of running an application program, insofar
as that malfunction is due to a lack of resources in the underlying system required for proper operation (i.e.,
not due to a programming error). Assuming that parameter checking has been implemented properly as
outlined above, we can narrow the term `Runtime Error' to cases for which the following statement holds: A
runtime error occurs whenever the system runs out of resources that were still available during parameter
checking.
Runtime errors, when unhandled, become logic errors because the code implies assumptions that no
longer hold once a runtime error has occurred.
Strategies
In a properly designed and implemented system, runtime errors, in the restricted sense described above,
will not occur frequently. However, as they are caused by undesired circumstances outside the scope of
the application program itself, it seems important to provide information to the user that is as detailed as
possible in order to enable her to prevent this type of situation in the future, and to make her aware of the
fact that the application program depends on her willingness to provide a smooth operating environment.
After displaying a message of this kind, it seems appropriate to simply abort execution altogether, while
trying to avoid a loss of the data acquired up to that time.
Logic Errors
Definition of the Term
Logic, or programming, errors in general can be due to a programmer who, in his or her code, implicitly or
explicitly makes assumptions that do not always hold.
Strategies
Programming errors are not supposed to occur at all in a tested version of an application. Therefore,
instead of trying to 'handle' them, it is important to make them show up as close to their point of origin in the
code as possible, by frequently and explicitly checking whether implicit assumptions actually hold, and
aborting execution with an error message if this is not the case.
Typically, such checks use an "assertion" facility provided by the programming language. BCI2000
provides its own bciassert macro in BCIAssert.h to make sure that failed assertions result in
messages that are displayed by the BCI2000 operator module. Unlike the standard assert macro, BCI2000
assertions do not evaluate to empty in release builds.
Aside from that, writing code as explicit, general, and simple as possible greatly reduces the possibility of
making logic errors in the first place.
#include "BCIError.h"
...
using namespace std;
...
ofstream outputStream( fileName );
if( !outputStream.is_open() )
{
bcierr << "Cannot open the file \""
<< fileName
<< "\" for output"
<< endl;
}
Furthermore, for handling runtime errors from which it is difficult to recover, code may throw an exception
that will abort execution and eventually lead to an error message being sent to the operator module (for
framework related details see the section entitled "Implementation on the Framework Side"):
#include "BCIException.h"
...
if( ernie.find( bert ) != ernie.end() )
throw bciexception( "Ernie just ate Bert. I don't know how to tell the
story." );
tellMyStory( ernie, bert );
...
Checking Parameters
Checking parameters is done in a separate member function of the filter base class which, similar to the
member function that does the actual processing, takes input and output signal representatives as
parameters, thus allowing for signal property checking.
or
Output = SignalProperties( 0, 0 );
The const declaration for its this pointer prohibits initialization functionality
from GenericFilter::Initialize() entering into Preflight(). Such behavior is unwanted
because it would corrupt the idea of performing a complete parameter check before actually altering the
state of a filter object.
A necessary condition for a correct implementation of the Preflight() function is that any parameter,
as well as any state that will be accessed during the processing phase, be accessed
from Preflight() at least once. For parameters and states defined by the filter itself (i.e. inside its
constructor), range and accessibility checks are automatically performed by the framework; parameters
and states defined by other filters must be explicitly accessed from Preflight(). If
a GenericFilter descendant fails to access an externally defined parameter or state
during Preflight(), the first access during the processing phase will result in a runtime error.
As an example,
delete Parameters;
Parameters = new ParamList;
Convenient Access to Environment Objects is possible through a number of symbols which offer built-in
checking and error reporting:
Examples:
State(Name)
This symbol allows for reading a state's value from the state vector and setting a state's value in the state
vector. Trying to access a state that is not accessible will result in an error reported via bcierr.
Examples:
PreflightCondition(Condition)
This symbol is meant to be used inside implementations of GenericFilter::Preflight(). If the
boolean condition given as its argument is false, it will output an error message into bcierr containing the
condition given in its argument.
Example:
If TransmitCh is greater than SourceCh, a message will be sent to bcierr and displayed to the user,
stating:
During the preflight phase, errors are Configuration Errors. A module's framework code
behind bcierr just collects error messages; on return from the preflight function, it sends those messages
to the operator module which then, from the contents of the message (i.e., whether it was empty or not),
determines whether the preflight was successful; on not receiving any message after some timeout it
assumes a broken connection or a crashed module.
(For now, a simple timeout scheme with a fixed timeout interval of 5s seems appropriate. In the future, one
might consider a module requesting additional timeout periods if it expects lengthy calculations.)
During all other phases, the code behind bcierr immediately (i.e., on flushing the std::ostream)
sends its message buffer to a log file as well as to the operator module, indicating a Runtime Error to the
operator module which will, in turn, halt the system,shut down the other modules, and display the message
to the user.
In addition, the top level exception handling code of each module contains similar functionality, sending an
exception's associated description string into a log file and to the operator module, if possible, then quitting
the module in which the exception occurred. This not only ensures a proper general handling of exceptions
within the framework but also allows a programmer to handle Runtime Errors by raising her own
exceptions, eliminating the need to take care of the error condition in the code following the detection of an
error.
Introduction
As BCI2000 modules are being used in various countries, there emerges some need to adapt user
interface elements to regional customs. Most notably, text that gives information to the subject requires
translation into the subject's native language, as often a knowledge of English cannot be assumed. Even
with a knowledge of English, reading text in a foreign language is a distracting factor that makes the
already demanding task of operating a BCI even more difficult.
This document explains the approach taken to manage localized (translated) user interface strings in
BCI2000 application modules, trying to meet the following goals:
Flexibility: For the end user (operator), it should be possible to add translations into a additional
languages, and to modify translations he does not feel to be appropriate. This should be achievable
without making changes to the source code and recompiling the application module.
Separability: For the user, switching languages should be done at a central place, i.e., by changing a
single parameter. For the programmer, adding localization capabilities to existing modules should be
possible with minimal changes to existing code. When writing new application modules, there should
be no need to consider localization issues in advance.
Documentation: Documenting and providing a collection of existing translations into various
languages should be possible inside a module's source code.
Strategies
For a number of text values that have always been set from parameters (such as the Speller
module's TextToSpell parameter that holds the text the user is told to spell in copy spelling mode),
localization is not an issue because the user may just change the value as seems appropriate.
The remaining text items, according to the way they are specified, fall into two categories:
For both categories, translations are kept in a single matrix type parameter that uses string labels, i.e., row
and column titles. Each column title represents the native English version of a text; row titles represent
languages for which translations exist. The user chooses amongst languages by specifying the desired
language in a second parameter; the BCI2000 framework will then use this table to look up a text's
translation, matching the text itself against column labels, and the target language against row labels. If no
match is found, it will leave the text unchanged.
For string literals in *.cpp files, the strategy is to simply put a function layer around the string, i.e., to send
it through a function that checks for a translation and exchanges the text if there is a match. Introducing
localizability into existing code implies only a very small amount of change compared to, e.g., introducing a
separate parameter for each string. In the former case, one needs only wrap the string literal into a function
call in-place; in the latter case, one would have to add a parameter line to a possibly remote filter class
constructor, read that parameter from inside that filter's Initialize() call, and put it into the object that
actually holds the string which might require introduction of additional accessor functions.
For strings specified in GUI resource files, the strategy is to supply a function that, very generally,
examines string properties of GUI objects, replacing them with their localized versions if applicable. This
function needs to be called explicitly from inside the Initialize() function of
the GenericFilter descendant that determines the GUI object's behavior (in most cases, this is the
application module's TTask class).
Implementation
The implementation is centered around a Localization class declared
in Application/shared/Localization.h.
Language defines the language into which strings are translated; if its value matches one of
the LocalizedStrings row labels, translations will be taken from that row; otherwise, strings will
not be translated. A value of Default results in all strings keeping their original values.
LocalizedStrings defines string translations. Strings that do not appear as a column label will not
be translated. Also, strings with an empty translation entry inLocalizedStrings will not be
translated.
Interface to the programmer
The LocalizedStrings parameter is empty by default. Although a user may add translations in desired
languages to the empty matrix by manually using the matrix editor of the operator module, translations will
preferably be provided in a filter constructor by listing them as in the following example:
#include "Localization.h"
...
TTask::TTask()
{
...
LANGUAGES "Italian",
"French",
BEGIN_LOCALIZED_STRINGS
"Yes",
"Si",
"Oui",
"No",
"No",
"Non",
END_LOCALIZED_STRINGS
...
}
If the LocalizedStrings matrix was empty before the TTask constructor gets called, it will have these
entries after execution of the constructor:
Yes No
Italian Si No
French Oui Non
There may be any number of translation tables inside filter constructors, with their entries being added to
the existing ones, or overriding entries that already exist.
Once entered, the translations contained in LocalizedStrings are applied via two mechanisms:
The function LocalizableString() takes a string as an argument and returns the appropriate
entry from LocalizedStrings, or the unmodified string if no entry can be found. For example,
instead of
TellUser( "Well done!" );
one would write
#include "Localization.h"
...
TellUser( LocalizableString( "Well done!" ) );
to have a translation for "Well done!" looked up in LocalizedStrings.
The function ApplyLocalizations() takes a pointer to a GUI object (for the Borland VCL, this
would be a TForm*) and translates all localizable text contained within it. This function must be called
during GenericFilter:: Initialize for each GUI object generated from a resource file.
Further implementation details
You should not use LocalizableString() on string constants used before the first call
to GenericFilter::Initialize() or for initializing static or global objects of any kind because
localization information used will not be available at global initialization time, and local static variables,
once initialized, will not be updated appropriately.
Language names are case-insensitive. You may use any string for a language name but as a
convention we suggest its most common English name, as in Italian, Dutch, French,
German, with international country abbreviations as optional regional qualifiers as in EnglishUS,
EnglishGB, GermanA, GermanCH if necessary.
table
Encoding of non-ASCII characters follows the UTF8 convention. To ensure platform independent
readability of source code files, there are macros that define HTML character names to their UTF8
encoded strings. This allows you to write
"Sm" oslash "rrebr" oslash "d"
for "Smørrebrød" (cf. table 1).
Functions specific to the Library
int BCI2000Release( void* )
purpose: Releases a string buffer, or other object allocated by the library. arguments: String pointer, or
handle to object allocated by one of the library functions. returns: 1 if successful, 0 otherwise.
enum
{
BCI2000Remote_Invisible = 0,
BCI2000Remote_Visible = 1,
BCI2000Remote_NoChange = 2,
};
This is to stress the importance of a proper implementation of Halt for smooth system operation. More
often than not, a source module will rely on asynchronous activity in a hardware driver, or a separate data
acquisition thread maintained by the GenericADC descendant itself. Such asynchronous activity must be
halted from the Halt function in order to ensure proper behavior of the source module at reconfiguration
and shutdown time.
Empty Output
For GenericADC descendants, the output signal's dimensions should be specified as "empty" in
the Preflight function.
The GenericFileWriter interface is identical to the GenericFilter interface, except for the
following:
void
BCI2000FileWriter::Publish() const
{
FileWriterBase::Publish();
BEGIN_PARAMETER_DEFINITIONS
"Storage string StorageTime= % % % % "
"// time of beginning of data storage",
END_PARAMETER_DEFINITIONS
}
Contents
[hide]
1 Methods
o 1.1 BCI2000FileReader([file name])
o 1.2 Open(file name, [buffer size=50kB])
o 1.3 RawValue(channel, sample)
o 1.4 CalibratedValue(channel, sample)
o 1.5 ReadStateVector(sample)
2 Properties
o 2.1 IsOpen (r)
o 2.2 ErrorState (r)
o 2.3 NumSamples (r)
o 2.4 SamplingRate (r)
o 2.5 SignalProperties (r)
o 2.6 FileFormatVersion (r)
o 2.7 HeaderLength (r)
o 2.8 StateVectorLength (r)
o 2.9 Parameters (r)
o 2.10 Parameter (r)
o 2.11 States (r)
o 2.12 State (r)
o 2.13 StateVector (r)
3 Performance Considerations
4 See also
Methods
BCI2000FileReader([file name])
With a file name given as an argument, the constructor will try to open the specified file.
RawValue(channel, sample)
Sets the current sample position to the value specified in the second argument, and returns the raw sample
value for the channel specified in the first argument.
CalibratedValue(channel, sample)
Sets the current sample position to the value specified in the second argument, and returns the calibrated
sample value for the channel specified in the first argument. The calibrated sample value is obtained by
applying channel-specific sample offsets and gains as present in the
file's SourceChOffset and SourceChGain parameters.
ReadStateVector(sample)
Sets the current sample position to the specified value, and reads state vector data.
Properties
IsOpen (r)
true when a file has been opened successfully, false otherwise.
ErrorState (r)
One of
NoError,
FileOpenError,
MalformedHeader.
NumSamples (r)
The number of samples in the currently opened data file.
SamplingRate (r)
The currently opened data file's sampling rate.
SignalProperties (r)
A SignalProperties object describing the current file's data format, number of channels, sample block
size (in its Elements property), and physical units.
FileFormatVersion (r)
A string describing the file's format as specified by the file header. Currently, this is "1.0" or "1.1".
HeaderLength (r)
The data file's header length in bytes.
StateVectorLength (r)
The state vector's length in bytes.
Parameters (r)
A ParamList object that contains all parameters present in the file.
Parameter (r)
Access to an individual parameter, following the syntax provided by the Environment class.
States (r)
A StateList object containing all state variables present in the file.
State (r)
Access to an individual state variable, following the syntax provided by the Environment class. The
returned value will match the state variable's value at the current sample position, as specified to a
previous call to one of the RawValue, CalibratedValue, or ReadStateVector methods.
StateVector (r)
Access to the full state vector, with state values matching those at the current sample position.
Performance Considerations
To improve performance when reading data, BCI2000FileReader uses an internal buffer which is 50kB
in size by default but may be set to a different value using an optional second argument
to BCI2000FileReader::Open().
Buffering strategy is optimized for sequential forward access, which is supposed to be the most relevant
use case. Buffering is non-overlapping, with the requested sample always the first one in the buffer in case
the buffer needs updating. In case of sequential access in reverse direction, this buffering scheme breaks
down and becomes maximally inefficient because the entire buffer is updated on each access.
In a BCI2000 data file, data is organized such that, for each sample, values from all channels and states
are stored, followed with all values from the next sample point, etc. When reading data from multiple
channels, this implies that it is favorable to iterate over channels in an inner loop, and over samples in
an outer loop; otherwise, buffering will become less efficient by a factor identical to the number of channels
read.
Location
BCI2000/src/shared/modules/signalprocessing
Synopsis
The IIRFilterBase class is a base class for BCI2000 filters that provide digital filters. Classes derived
from IIRFilterBase are supposed to implementIIRFilterBase::DesignFilter(), specifying
filter behavior by means of a transfer function. Differently from typical IIR filter implementations, which
expect the transfer function in terms of filter coefficients, the BCI2000 IIRFilterBase class expects the
transfer function in terms of its numerator's roots ("zeros") and denominator's roots ("poles"); this approach
improves numerical stability at higher filter orders.
FilterDesign Library
While any method may be used to determine the transfer function, the FilterDesign library provides a
convenient way to compute a transfer function from desired filter properties. It is located
at BCI2000/src/extlib/math/FilterDesign, and was written for BCI2000 based on public-
domain code by A.J. Fisher; it provides a number of classic filter design methods in form of C++ classes.
To use such a class, instantiate it, use its member functions to configure parameters, and obtain
its TransferFunction property (see the example section below).
CAVEAT: When specifying frequencies, the FilterDesign library expects them normalized to the sampling
rate, such that the Nyquist frequency becomes 0.5. This is different from Matlab, where filter design
functions expect frequencies normalized to the Nyquist frequency, such that the Nyquist frequency
corresponds to 1.0.
FilterDesign::Butterworth
Order(order)
Set the filter order to the specified value.
Lowpass(corner frequency)
Design a low pass with the specified corner frequency, i.e. suppress signals above the given frequency.
Frequencies are specified in terms of the sampling rate, and expected to be below 0.5 (the Nyquist
frequency).
Highpass(corner frequency)
Design a high pass with the specified corner frequency, i.e. suppress signals below the given frequency.
TransferFunction
Returns a transfer function as a rational polynomial with complex roots as declared
in BCI2000/src/extlib/math/Polynomials.h. From the Ratpoly object returned, numerator
and denominator polynomials may be extracted
using Ratpoly::Numerator() and Ratpoly::Denominator(), respectively. Both functions return
a polynomial, from which roots may be obtained using Polynomial::Roots(), and coefficients may be
obtained using Polynomial::Coefficients().
FilterDesign::Chebychev
The Chebychev class provides an interface identical to the Butterworth class, and an additional
function to specify ripple amplitude.
Ripple_dB(amplitude)
Set the amplitude of the filter's passband ripples in dB.
FilterDesign::Resonator
Rather than through corner frequencies, the Resonator class is parameterized specifying a resonant
frequency in conjunction with a quality factor (which is inversely proportional to the resonance peak's
width).
QFactor(q)
Set the resonator's quality factor to the specified value.
Bandpass(center frequency)
Design a bandpass around the specified center frequency.
Bandstop(center frequency)
Design a bandstop around the specified center frequency.
Allpass(center frequency)
Design a filter with a constant frequency response but altering phase at the center frequency.
Remarks
You may obtain a serial combination of filters by multiplying their individual transfer functions prior to
extracting gain, zeros, and poles.
The actual IIR filter implementation uses a sequence of order-one filter stages corresponding to pairs
of zeros and poles. Thus, there is no limitation to the filter order, as it would be the case for
implementations based on coefficients, due to numerical instability.
Roots common to both numerator and denominator will be automatically removed from the overall
transfer function, so the combined filter may be of an order which is lower than the sum of individual
filter orders.
Background Information
The effect of a linear filter may be viewed in terms of a transfer function. When transforming both the filter
and its input signal from the time domain into a more convenient representation (applying a Z transform),
the input is transformed into a function over the complex plane, and the effect of the linear filter is
transformed into multiplication with another rational transfer function defined over the complex
plane, i.e. is a fraction consisting of a polynomial in as its numerator, and another fraction
consisting of a polynomial in as its denominator (for more information,
see http://en.wikipedia.org/wiki/Linear_filter#Mathematics_of_filter_design):
where the and are called coefficients, the are called zeros because H assumes a value of
zero whenever , and the are called poles because H goes to infinity wherever ,
and is a gain factor.
Importantly, the effect of applying filters in series is represented by multiplication of the two transfer
functions. In reverse, the effect of a single filter may be decomposed into a series of filters by writing H as a
product of transfer functions.
Filter design methods, such as Butterworth's or Chebychev's, define a filter by placing H's zeros and
poles in the complex plane, with the number of zeros and the number of poles corresponding to twice the
filter's order.
Often then, filters are implemented in terms of a series of delays for input and output samples, and
coefficients associated with those delays, such that the current output sample is computed as the
difference of past input samples, weighted with input coefficients , and past output samples, weighted
with output coefficients . Comparing such a filter's action to that of a filter with transfer function H, one
finds that input coefficients correspond to the polynomial coefficients of H's numerator, and output
coefficients ai correspond to the polynomial coefficients of H's denominator. A standard procedure in filter
design is thus to take a filter's poles and zeros, and convert them into coefficients and by
expanding numerator and denominator polynomial into coefficient form, and dividing both by to
obtain the correct order of coefficients.
While this approach is computationally most efficient, it suffers from a lack of computational accuracy, and
resulting instability. The cause of this instability is the fact that arbitrarily large terms need to cancel out in
the difference between weighted input and output samples, but may fail to do so due to roundoff errors.
In BCI2000, a different approach is taken. Taking raw zeros and poles, the transfer function is first
simplified by removing factors that cancel out. Then, it is taken apart into a product of transfer functions
with a single zero and a single pole each, corresponding to a series of filters, each of order 1. Thus, in
BCI2000 IIR filters may be of arbitrary order without risk of instability (provided the filter's poles and zeros
meet the criteria for stability in the general sense, of course).
Examples
Creating a BCI2000 Filter
To implement a 2nd order Butterworth low pass filter, you might derive a
class ButterworthLP from IIRFilterBase, with its declaration being
ButterworthLP::ButterworthLP()
{
BEGIN_PARAMETER_DEFINITIONS
"Filtering float ButterworthLPCorner= 30Hz % % % // Low pass corner
frequency",
END_PARAMETER_DEFINITIONS
}
In the DesignFilter method, we use the FilterDesign library to obtain poles and zeros from parameters:
void
ButterworthLP::DesignFilter( const SignalProperties& inSignalProperties,
IIRFilterBase::Real& outGain,
IIRFilterBase::ComplexVector& outZeros,
IIRFilterBase::ComplexVector& outPoles )
const
{
float corner = MeasurementUnits::ReadAsFreq( Parameter(
"ButterworthLPCorner" ) )
/ inSignalProperties.Elements() * Parameter(
"SampleBlockSize" );
if( corner < 0.0 || corner > 0.5 )
bcierr << "ButterworthLPCorner exceeds range" << endl;
Ratpoly<FilterDesign::Complex> tf = FilterDesign::Butterworth()
.Order( 2 )
.Lowpass( corner )
.TransferFunction();
outGain = 1.0 / abs( tf.Evaluate( 1.0 ) ); // make sure that LF gain is
unity
outZeros = tf.Numerator().Roots();
outPoles = tf.Denominator().Roots();
}
For an example that combines a notch filter with a high pass filter, please refer to the SourceFilter's source
code.
Ratpoly<FilterDesign::Complex> tf = FilterDesign::Butterworth()
.Order( 2 )
.Lowpass( 10 / 500 )
.TransferFunction();
FilterDesign::ComplexVector a, b;
FilterDesign::ComputeCoefficients( tf, b, a );
NOTE: When comparing filter coefficients with Matlab's, consider that Matlab's convention for specifying
frequencies differs from that of FilterDesign. For more information, see theCaveat above.
[hide]
1 Location
2 Synopsis
3 Usage
4 Access to Application Windows
5 RandomNumberGenerator
6 Application Log
7 Remarks
8 See also
Location
BCI2000/src/shared/modules/application
Synopsis
The ApplicationBase class bundles base functionality which is useful for any BCI2000
application module. This base functionality comprises
MyTaskFilter::MyTaskFilter()
: mrWindow( ApplicationWindowClient::Window() )
{
...
}
MyTaskFilter::Initialize( ... )
{
...
ImageStimulus* pImageStimulus = new ImageStimulus( mrWindow );
...
}
You may also test windows for existence from the Preflight() function, and only use them
when they already exist. This way, your code can adapt to the presence of certain filters in
the application module. For more details, see
the ApplicationWindowClient class reference page.
RandomNumberGenerator
The ApplicationBase class provides an object of type RandomGenerator which may be
used to obtain integer pseudo-random numbers uniformly distributed over a specified range:
will assign a number between 0 and 9 to the variable named "rnd". Compared with other
options to obtain pseudo-random numbers, the advantage of
usingApplicationBase::RandomNumberGenerator object is that its behavior is governed
by a global random seed value; this allows consistent, predictable BCI2000 behavior, e.g. for
testing purposes. For more information, refer to the RandomGenerator reference page.
Application Log
Typically, a BCI2000 application module displays messages to the operator user to indicate
that a new trial has started, a target has been hit or missed, or to display statistics about the
current run. Often, this is information, in conjunction with additional, more detailed
information is written into a log file, and that log file is maintained side-by-side with recorded
data in the current session directory.
The ApplicationBase class provides an object named AppLog as a convenient interface to
write messages to screen, log file, or both at the same time. That AppLog object uses
a LogFile object to write into a log file; it uses a GenericVisualization object to write
messages into an operator window.
The AppLog object is a <std::ostream>, and may be used as in the following examples:
Make sure to include the "endl" as nothing is written until the AppLog receives a newline
command.
The application log file is located in the current session directory, and named after the
current session; it carries an .applog extension.
Operator log window messages have APLG as their visualization source ID.
Remarks
Typically, a BCI2000 application module will implement a paradigm that belongs to one of
two categories:
Programming Reference:ApplicationWindowClient
Class
ApplicationWindowClient is a mix-in class that provides its inheritants with access to application windows,
which are instances of the ApplicationWindow class.ApplicationWindow instances have names,
with the default name being "Application". A window's name may be used to obtain
a DisplayWindow reference to the window, e.g.
If you don't want to create an application window, but only access an existing one, you need to declare
access to that window during the preflight phase. In your Preflight()member function, write:
Window( "Application" );
When no such window exists, a preflight error will be reported. Unless the window has been accessed
during the construction phase, not declaring access to the window during the preflight phase will result in
an error message when calling ApplicationWindowClient::Window() from elsewhere.
1 Location
2 Synopsis
3 Methods
o 3.1 GraphDisplay()
o 3.2 ~GraphDisplay()
o 3.3 DeleteObjects()
o 3.4 Invalidate()
o 3.5 InvalidateRect(GUI::Rect)
o 3.6 Update()
o 3.7 GUI::Rect NormalizedToPixelCoordinates(GUI::Rect)
o 3.8 GUI::Rect PixelToNormalizedCoordinates(GUI::Rect)
o 3.9 Paint(RegionHandle=NULL)
o 3.10 Change()
o 3.11 Click(int x, int y)
o 3.12 QueueOfGraphObjects ObjectsClicked()
o 3.13 ClearClicks()
o 3.14 BitmapImage BitmapData(width=0, height=0)
4 Properties
o 4.1 GUI::DrawContext Context (rw)
o 4.2 RGBColor Color (rw)
5 See also
Location
src/shared/gui
Synopsis
An object of type GraphDisplay refers to a graphical output rectangle, and a set of graphical objects that
are displayed within that rectangle.
GraphDisplay does not make assumptions on output device (screen, printer) or whether it refers to an
entire window, or a rectangle within a larger window.
The GraphDisplay class is contained within the GUI namespace. To use it, either qualify its
name: GUI::GraphDisplay (recommended for header files); or put a usingstatement on top of the file
(recommended for cpp files):
Methods
GraphDisplay()
Initializes a GraphDisplay object such that it is not bound to any device or window.
~GraphDisplay()
The GraphDisplay destructor deletes all GraphObjects within the display.
DeleteObjects()
Removes and deletes all GraphObject instances contained in the GraphDisplay.
Invalidate()
Marks the entire GraphDisplay area for repainting.
InvalidateRect(GUI::Rect)
Marks a rectangle within the GraphDisplay for repainting. The rectangle is given in screen pixel
coordinates.
NOTE: The use of pixel coordinates has been introduced in rev 3599 to avoid artifacts from converting forth
and back between normalized and pixel coordinates. Originally, the rectangle was specified in normalized
coordinates.
Update()
Forces an immediate repaint of all invalidated areas in the GraphDisplay, making sure that changes
in GraphObject state are reflected in the device's frame buffer. In BCI2000 application modules, this
function should be called immediately before setting the StimulusTime time stamp.
GUI::Rect NormalizedToPixelCoordinates(GUI::Rect)
GUI::Rect PixelToNormalizedCoordinates(GUI::Rect)
These functions are used to convert between pixel/device coordinates, and normalized coordinates. Note
that, from application code, GraphObjects are positioned and sized using normalized coordinates
relative to the GraphDisplay's screen rectangle; in their OnPaint handlers, they are provided with pixel
coordinates.
Paint(RegionHandle=NULL)
Triggers a repaint of the GraphDisplay's contents. When a region handle is specified, it is used as a clip
during the paint operation, saving redraw of the entire GraphDisplayarea when only a subregion has
become invalid.
Change()
Triggers a Change event for all GraphObjects that are part of the display. For modifications in size or
draw context, this function is called automatically.
Click(int x, int y)
Triggers a Click event for all GraphObjects that are part of the display.
Unlike GraphObject::OnClick(), coordinates are pixel coordinates.
QueueOfGraphObjects ObjectsClicked()
When the user clicks inside a GraphDisplay, all objects' Click events are called to determine which of
them were below the mouse pointer when clicking. Objects that consider themselves clicked are added to a
queue, which is of type std::queue<GraphObject*>. This queue is accessible from
the ObjectsClicked function; from the queue, clicked objects may be retrieved using
its pop() member function.
ClearClicks()
Empties the queue of clicked objects.
Properties
GUI::DrawContext Context (rw)
The draw context into which a GraphDisplay is supposed to render the GraphObjects it contains. A
draw context consists of a handle to an output device context (HDC in Windows), and a rectangle within
that device context's area.
1 Location
2 Synopsis
3 Methods
o 3.1 GraphObject(GraphDisplay, int zOrder)
o 3.2 Show()
o 3.3 Hide()
o 3.4 Invalidate()
o 3.5 Paint()
o 3.6 Change()
o 3.7 bool Click(GUI::Point)
4 Properties
o 4.1 GraphDisplay Display (r)
o 4.2 bool Visible (r)
o 4.3 float ZOrder (rw)
o 4.4 enum AspectRatioMode (rw)
o 4.5 GUI::Rect DisplayRect (rw)
5 Events
o 5.1 OnPaint(GUI::DrawContext)
o 5.2 OnChange(GUI::DrawContext)
o 5.3 bool OnClick(GUI::Point)
6 Descendants
7 See also
Location
src/shared/gui
Synopsis
The GraphObject class defines an interface for graphical objects displayed by a BCI2000
application. GraphObjects are tied to a GraphDisplay, which typically represents an application
window.
GraphObjects possess a z-order position that determines how they are drawn on top of each other. You
do not instantiate GraphObjects directly; rather, you create objects of classes that inherit
the GraphObject interface.
The GraphObject class is contained within the GUI namespace. To use it, either qualify its
name: GUI::GraphObject (recommended for header files); or put a usingstatement on top of the file
(recommended for cpp files):
Methods
GraphObject(GraphDisplay, int zOrder)
The constructor of a graph object requires a reference to a GraphDisplay, and an integer that specifies
the object's z order, where lower values correspond to objects that are drawn on top of objects with larger z
order values.
Show()
Makes the object visible. Initially, objects are created in visible state.
Hide()
Makes the object invisible.
Invalidate()
Invalidates the object's bounding rectangle, i.e. marks it as needing to be repainted. Typically, this function
is called from a derived class, indicating that a change in object properties has occurred that requires a
repaint.
Paint()
Asks an object to paint itself by calling its OnPaint event handler.
Change()
Notifies an object of a change in display properties by calling its OnChange event handler.
bool Click(GUI::Point)
Tests whether the specified point is inside an object's bounding rectangle, and calls its OnClick event
handler if this is the case.
Properties
GraphDisplay Display (r)
The GraphDisplay object that was specified when the object was created.
bool Visible (r)
True if the object is visible, false if it is hidden. Use the Hide() and Show() methods to set this property.
AdjustNone
No adjustment is made.
AdjustWidth
The object's width is adapted to its contents, while keeping its height constant.
AdjustHeight
The object's height is adapted to its contents, keeping its width constant.
AdjustBoth
Both the object's height and width are adjusted to its content.
The exact behavior of aspect ratio adjustment depends on the object's type. E.g., for
bitmap images, AdjustBoth will size the image such that one image pixel corresponds to
one screen pixel; AdjustHeight and AdjustWidth will adjust such that the original aspect
ratio is preserved.
Events
OnPaint(GUI::DrawContext)
From its OnPaint event handler, a GraphObject renders itself into a device context
and rectangle specified in its DrawContext argument. The DrawContext consists of an
OS device context handle, and a rectangle in device coordinates. Implementing this
function is mandatory for classes inheriting from GraphObject.
OnChange(GUI::DrawContext)
The OnChange event handler is called to notify the object that a change in size, position,
or output device has occurred. E.g., if the object maintains an internal bitmap buffer that
it uses to speed up paint operations, it should delete and re-create that buffer from
its OnChange handler to ensure consistency with the output draw context. Objects that
render themselves directly do not need to override the (empty) default handler.
bool OnClick(GUI::Point)
The OnClick event handler receives a point in normalized coordinates, and returns
whether it considers itself clicked. The default handler does nothing, and returns true.
Descendants
The GraphObject class is parent to the following class hierarchy:
You provide only specific code. This is in a function that waits for and reads A/D data (line 3 in the pseudo
code shown at Technical Reference:Core Modules), together with some helper functions that perform
initialization and cleanup tasks. Together these functions form a class derived from GenericADC.
Contents
[hide]
1 Example Scenario
2 Writing the ADC Header File
3 ADC Implementation
4 ADC Initialization
5 Data Acquisition
6 Adding the SourceFilter
7 Finished
Example Scenario
Your Tachyon Corporation A/D card comes with a C-style software interface declared in a header
file "TachyonLib.h" that consists of three functions
#define TACHYON_NO_ERROR 0
int TachyonStart( int inSamplingRate, int inNumberOfChannels );
int TachyonStop( void );
int TachyonWaitForData( short** outBuffer, int inCount );
From the library help file, you learn that TachyonStart configures the card and starts acquisition to some
internal buffer; that TachyonStop stops acquisition to the buffer, and that TachyonWaitForData will
block execution until the specified amount of data has been acquired, and that it will return a pointer to a
buffer containing the data in its first argument. Each of the functions will return zero if everything went well,
otherwise some error value will be returned. Luckily, Tachyon Corporation gives you just what you need for
a BCI2000 source module, so implementing the ADC class is quite straightforward.
#ifndef TACHYON_ADC_H
#define TACHYON_ADC_H
#include "GenericADC.h"
void Publish();
void AutoConfig( const SignalProperties& );
void Preflight( const SignalProperties&, SignalProperties& ) const;
void Initialize( const SignalProperties&, const SignalProperties& );
void Process( const GenericSignal&, GenericSignal& );
void Halt();
private:
void* mHandle;
int mSourceCh,
mSampleBlockSize,
mSamplingRate;
};
#endif // TACHYON_ADC_H
ADC Implementation
In the .cpp file, you will need some #includes, and a filter registration:
#include "TachyonADC.h"
#include "Tachyon/TachyonLib.h"
#include "BCIError.h"
RegisterFilter( TachyonADC, 1 );
From the constructor, you request parameters and states that your ADC needs; from the destructor, you
call Halt to make sure that your board stops acquiring data whenever your class instance gets destructed:
TachyonADC::TachyonADC()
: mSourceCh( 0 ),
mSampleBlockSize( 0 ),
mSamplingRate( 0 )
{
BEGIN_PARAMETER_DEFINITIONS
"Source int SourceCh= 64 64 1 128 "
"// this is the number of digitized channels",
"Source int SampleBlockSize= 16 5 1 128 "
"// this is the number of samples transmitted at a time",
"Source int SamplingRate= 128 128 1 4000 "
"// this is the sample rate",
END_PARAMETER_DEFINITIONS
}
TachyonADC::~TachyonADC()
{
Halt();
}
ADC Initialization
Your Preflight function will check whether the board works with the parameters requested, and
communicate the dimensions of its output signal:
Here, the last argument of the SignalProperties constructor determines not only the type of the signal
propagated to the BCI2000 filters but also the format of the dat file written by the source module.
The actual Initialize function will only be called if Preflight did not report any errors. Thus, you
may skip any further checks, and write
Balancing the TachyonStart call in the Initialize function, your Halt function should stop all
asynchronous activity that your ADC code initiates:
void TachyonADC::Halt()
{
TachyonStop();
}
Data Acquisition
Note that the Process function may not return unless the output signal is filled with data, so it is crucial
that TachyonWaitForData is a blocking function. (If your card does not provide such a function, and
you need to poll for data, don't forget to call Sleep( 0 ) inside your polling loop to avoid tying up the
CPU.)
Finished
You are done! Use your TachyonADC.cpp to replace the GenericADC descendant in an existing source
module, add the TachyonADC.lib shipped with your card to the project, compile, and link.
Contents
[hide]
#ifndef LP_FILTER_H
#define LP_FILTER_H
#include "GenericFilter.h"
#include "MeasurementUnits.h"
#include "BCIError.h"
#include <vector>
#include <cmath>
We introduce these members into the class declaration, adding the following lines after
the Process declaration:
private:
double mDecayFactor;
std::vector<double> mPreviousOutput;
The next step is to initialize these member variables, introducing filter parameters as needed. This is done
in the Initialize member function -- we write it down without considering possible error conditions:
Now this version is quite inconvenient for a user going to configure our filter -- the time constant is given in
units of a sample's duration, resulting in a need to re-configure each time the sampling rate is changed. A
better idea is to let the user choose whether to give the time constant in seconds or in sample blocks. To
achieve this, there is a utility classMeasurementUnits that has a member ReadAsTime(), returning
values in units of sample blocks which is the natural time unit in a BCI2000 system. Writing a number
followed by an "s" will allow the user to specify a time value in seconds; writing a number without the "s" will
be interpreted as sample blocks. Thus, our user friendly version ofInitialize reads
1. The time constant is not zero -- otherwise, a division by zero will occur.
2. The time constant is not negative -- otherwise, the output signal is no longer guaranteed to be
finite, and a numeric overflow may occur.
3. The output signal is assumed to hold at least as much data as the input signal contains.
The first two assumptions may be violated if a user enters an illegal value into the LPTimeConstant
parameter; we need to make sure that an error is reported, and no code is executed that depends on these
two assumptions. For the last assumption, we request an appropriate output signal from
the Preflight function. Thus, the Preflight code reads
LPFilter::LPFilter()
: mDecayFactor( 0 ),
mPreviousOutput( 0 )
{
BEGIN_PARAMETER_DEFINITIONS
"Filtering float LPTimeConstant= 16s"
" 16s % % // time constant for the low pass filter in blocks or
seconds",
END_PARAMETER_DEFINITIONS
}
LPFilter::~LPFilter()
{
}
Filter instantiation
To have our filter instantiated in a signal processing module, we add a line containing a Filter statement
to the module's PipeDefinition.cpp. This statement expects a string parameter which is used to
determine the filter's position in the filter chain. If we want to use the filter in the AR Signal Processing
module, and place it after theSpatialFilter, we add
#include "LPFilter.h"
...
Filter( LPFilter, 2.B1 );
#include "GenericVisualization.h"
...
class LPFilter : public GenericFilter
{
public:
...
virtual bool AllowsVisualization() const { return false; }
private:
...
GenericVisualization mSignalVis;
};
...
LPFilter::LPFilter()
: mDecayFactor( 0 ),
mPreviousOutput( 0 ),
mSignalVis( "LPFLT" )
{
BEGIN_PARAMETER_DEFINITIONS
"Filtering float LPTimeConstant= 16s"
" 16s % % // time constant for the low pass filter in blocks or
seconds",
"Visualize int VisualizeLowPass= 1"
" 1 0 1 // visualize low pass output signal (0=no, 1=yes)",
END_PARAMETER_DEFINITIONS
}
In Initialize, we add
Finally, to update the display in regular intervals, we add the following at the end of Process:
GenericVisualization mTaskLogVis;
initializing it with
LPFilter::LPFilter()
: ...
mTaskLogVis( SourceID::TaskLog )
{
...
}
The advantage of the FieldTripBuffer interface is that you have all control in MATLAB that you are used to.
You can write your MATLAB code for offline-analysis (i.e. reading data from a file) and apply exactly the
same code to online analysis (i.e. reading from BCI2000). Of course for the online analysis to make some
sense, your analysis script has to be meaningful and has to work with relatively small data fragments (e.g.
one second or less), otherwise the MATLAB code would not really run in real-time. Another interesting
feature is that in MATLAB you can use the profiler (type "help profile") to determine which parts of your
code take a long time to execute and speed those parts up.
The remainder of this page gives an example of how to get the data into MATLAB, plot the data using
standard MATLAB code, and how to close the BCI loop by writing an event back to BCI2000. The example
below does not do any useful processing, it is up to you to decide how you want to process the data in
MATLAB. A number of realtime applications are included in the realtime module of the FieldTrip toolbox.
Additional documentation for that can be found on the FieldTrip website, under development->realtime.
To use the FieldTrip buffer, you start BCI2000 with the FieldTripBuffer as the Signal Processing application.
Subsequently you start MATLAB yourself, i.e. your MATLAB session is a normal standalone application.
You should have a recent copy of the FieldTrip toolbox installed, or at least a copy of the FieldTrip fileio
module. The FieldTrip toolbox and its components are available for download
from http://www.ru.nl/neuroimaging/fieldtrip. Please make sure that the correct version of the fileio module
is on your MATLAB search path.
Subsequently you can do something like the following code below. You should be able to copy and paste
the code into the MATLAB command window and get a real-time updating MATLAB figure with the data
from BCI2000.
filename = 'buffer://localhost:1972';
% read the header for the first time to determine number of channels and
sampling rate
hdr = read_header(filename, 'cache', true);
count = 0;
prevSample = 0
blocksize = hdr.Fs;
chanindx = 1:hdr.nChans;
while true
% determine number of samples available in buffer
hdr = read_header(filename, 'cache', true);
if newsamples>=blocksize
% determine the samples to process
begsample = prevSample+1;
endsample = prevSample+blocksize ;
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%
% subsequently the data can be processed, here it is only plotted
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%
% plot the data just like a standard FieldTrip raw data strucute
plot(time, dat);
event.type = 'Signal';
event.sample = 1;
event.offset = 0;
event.duration = 1;
event_up = event;
event_up.value = 1;
event_down = event;
event_down.value = -1;
event_null = event;
event_null.value = 0;
You can write the events to the buffer according to the following example code:
filename = 'buffer://localhost:1972';
The control signal in BCI2000 remains at a constant value as long as you don't write another event with
another control signal.
Contents
[hide]
1 Overview
2 Implementation
o 2.1 Device API
o 2.2 Event Interface
o 2.3 Thread Interface
o 2.4 EnvironmentExtension Class
3 Finished
4 See also
Overview
In BCI2000, input logging can be done with per-sample resolution. Typically, BCI2000 data acquisition,
signal processing, and application feedback code runs in a pipesynchronously, being called once
per BCI2000 sample block, and cannot detect state changes in an input device more frequently than that.
To support input logging with per-sample resolution, BCI2000 allows code to post so-
called events asynchronously from a separate thread, which are time-stamped internally and matched
against the current data block's time stamp in order to associate them with individual samples.
In this tutorial, we will discuss how to implement input Logging for a device by polling its state in regular
intervals. Generally, relying on OS events to detect changes in device state is preferred over polling;
however, whether and how device state is available via OS events depends strongly on the device's driver
software, and it is thus difficult to provide a valid example. Readers interested in input logging via OS
events should read this tutorial first, and then proceed to the key logger component's source code for a
non-polling example.
Implementation
An input logger component consists of a combination of a few existing software components, which are all
provided by BCI2000 except the device API itself.
Device API
The device API provides functions that allow to read, or manipulate, the state of the input device. Typically,
it consists of a library (DLL), and an associated header file.
For the sake of this tutorial, we will assume that the device has the shape of thumb wheel, and has one
continuous degree of freedom. Its header file, ThumbWheel.h, provides a C-style interface:
In order to connect to the thumb wheel, we call ThumbWheelInit(), receiving ThumbOK if everything is
fine. The ThumbWheelGetPos() function will return the wheel's current position, as an integer between
zero and THUMB_WHEEL_MAX_POS.
Event Interface
Using the BCI2000 event interface, device state may be written into BCI2000 states asynchronously. We
will use the event interface to record the wheel's position into a state called ThumbWheelPos, writing
#include "BCIEvent.h"
...
bcievent << "ThumbWheelPos " << ThumbWheelGetPos();
Thread Interface
In order to observe the wheel's state independently of BCI2000's processing of data blocks, we create a
thread that polls wheel state in regular intervals. We will use BCI2000'sOSThread class to implement that
separate thread.
#include "OSThread.h"
#include "ThumbWheel.h"
Note that we avoid sending events if there is no change in position. Otherwise, the event queue will grow
very large, increasing overall processing and memory load even if there is no information to record.
EnvironmentExtension Class
The EnvironmentExtension Class is a base class for BCI2000 components ("extensions") that are
not filters. Such extensions do not process signals but still have access toBCI2000 parameters and state
variables, and are notified of system events such as Preflight, Initialize, and StartRun.
#ifndef THUMBWHEEL_LOGGER_H
#define THUMBWHEEL_LOGGER_H
#include "Environment.h"
#include "ThumbThread.h"
private:
bool mLogThumbWheel;
ThumbWheelThread* mpThumbWheelThread;
};
#endif // THUMBWHEEL_LOGGER_H
In our extension component's Publish() member function, we test for a parameter LogThumbWheel,
and only request the "ThumbWheelPos" state variable if logging is actually enabled.
The LogThumbWheel parameter will be available if the module has been started up with --
LogThumbWheel=1 specified on the command line; this way, logging may be enabled and disabled, with
no state variable allocated when logging is disabled. Note that we request the LogThumbWheel parameter
even if it already exists; this has the effect of providing appropriate auxiliary information about that
parameter, i.e. its section, type, and comment fields.
void ThumbWheelLogger::Publish()
{
if( OptionalParameter( "LogThumbWheel" ) > 0 )
{
BEGIN_PARAMETER_DEFINITIONS
"Source:Log%20Input int LogThumbWheel= 1 0 0 1 "
" // record thumb wheel to state (boolean)",
END_PARAMETER_DEFINITIONS
BEGIN_EVENT_DEFINITIONS
"ThumbWheelPos 15 0 0 0",
END_EVENT_DEFINITIONS
}
}
From the Preflight() member function, we check whether the thumb wheel is available:
void ThumbWheelLogger::Initialize()
{
mLogThumbWheel = ( OptionalParameter( "LogThumbWheel" ) > 0 );
}
From the component's StartRun() member function, we instantiate the thumb wheel thread class
declared above, thereby running its Execute() member in a new thread:
void ThumbWheelLogger::StartRun()
{
if( mLogThumbWheel )
{
mpThumbWheelThread = new ThumbWheelThread;
mpThumbWheelThread->Start();
}
}
void ThumbWheelLogger::StopRun()
{
if( mpThumbWheelThread != NULL )
{
OSEvent terminateEvent;
mpThumbWheelThread->Terminate( &terminateEvent );
terminateEvent->Wait();
delete mpThumbWheelThread;
mpThumbWheelThread = NULL;
}
}
We also forward StopRun() functionality to the Halt() member to ensure appropriate halting of
asynchronous activity:
void ThumbWheelLogger::Halt()
{
StopRun();
}
Finally, to make sure there exists an object of our ThumbWheelLogger class, we use
the Extension macro at the top of its .cpp file:
Extension( ThumbWheelLogger );
Finished
Now, when we add the ThumbWheelLogger.cpp file to a source module, then the module will contain
an object of our newly created class, and it will listen to the --LogThumbWheel=1 command line option.
[hide]
1 Location
2 Synopsis
3 Introduction
o 3.1 Presentation of stimuli
o 3.2 Grouping of stimuli
o 3.3 Sequencing
o 3.4 Classification vs Target Selection
3.4.1 Specifying Stimulus-Target Relations
3.4.2 Accumulation and Combination of Evidence
o 3.5 Using a Different Classifier with StimulusTask
3.5.1 Converting a Classifier's Output into a Classification Score
3.5.1.1 Classifier Error Rates
3.5.1.2 Measures of Significance
3.5.2 Transmitting Classification Scores
4 Events
o 4.1 Events Summary
o 4.2 Stimulus Code Event
4.2.1 int OnNextStimulusCode
o 4.3 Events forwarded from the GenericFilter interface
4.3.1 OnPreflight(SignalProperties (r))
4.3.2 OnInitialize(SignalProperties (r))
4.3.3 OnStartRun
4.3.4 OnStopRun
4.3.5 OnHalt
o 4.4 Phase transition Events
4.4.1 OnPreSequence
4.4.2 OnSequenceBegin
4.4.3 OnSequenceEnd
4.4.4 OnStimulusBegin(int stimulusCode (r))
4.4.5 OnStimulusEnd(int stimulusCode (r))
4.4.6 OnPostRun
o 4.5 Input Events
4.5.1 DoPreRun(GenericSignal (r), bool doProgress (rw))
4.5.2 DoPreSequence(GenericSignal (r), bool doProgress (rw))
4.5.3 DoStimulus(GenericSignal (r), bool doProgress (rw))
4.5.4 DoISI(GenericSignal (r), bool doProgress (rw))
4.5.5 DoPostSequence(GenericSignal (r), bool doProgress (rw))
4.5.6 DoPostRun(GenericSignal (r), bool doProgress (rw))
o 4.6 Classification Events
4.6.1 OnClassInput(stimulusCode (r), GenericSignal (r))
4.6.2 Target* OnClassResult(ClassResult (r))
5 Properties
o 5.1 AssociationMap Associations (rw)
o 5.2 Target* AttendedTarget (rw)
o 5.3 GUI::GraphDisplay Display (r)
o 5.4 ostream AppLog, AppLog.File, AppLog.Screen (w)
o 5.5 RandomGenerator RandomNumberGenerator (rw)
6 Methods
o 6.1 DisplayMessage(string)
7 Parameters
o 7.1 WindowBackgroundColor
o 7.2 PreRunDuration
o 7.3 PostRunDuration
o 7.4 PreSequenceDuration
o 7.5 PostSequenceDuration
o 7.6 StimulusDuration
o 7.7 EarlyOffsetExpression
o 7.8 ISIMinDuration, ISIMaxDuration
o 7.9 InterpretMode
o 7.10 DisplayResults
o 7.11 MinimumEvidence
o 7.12 AccumulateEvidence
8 States
o 8.1 StimulusCode
o 8.2 StimulusType
o 8.3 StimulusBegin
o 8.4 PhaseInSequence
o 8.5 PauseApplication
9 Timeline
10 See also
Location
BCI2000/src/shared/modules/application
Synopsis
The StimulusTask class is a base class for application modules that present a sequence of
stimuli. You do not use objects of type StimulusTask directly; rather, you implement your
own class that inherits from it, and implements specialized behavior building on the base
functionality provided by the StimulusTask class.
Introduction
The StimulusTask class is a base class for applications that present stimuli to the user, and
optionally perform selection of a selection target based on a real-time evaluation of the
user's brain signals generated in response to stimulus presentations. Straightforward as this
seems, complication arises from the fact that there is not always a 1-to-1 relation between
stimuli to present, and targets to choose from (e.g., the P3SpellerTask will present rows
and columns of the selection matrix, but the final choice will be a single matrix entry rather
than a row or column). Also, due to the low signal-to-noise ratio in individual evoked
response signals, it is generally necessary to perform multiple stimulus presentations before
a target may be selected with reasonable accuracy.
Through its interface to descendant classes, the StimulusTask base class allows for
efficient implementation of various kinds of such applications. In detail, the following basic
concepts are involved in the interaction between StimulusTask, and a descendant class.
Presentation of stimuli
Individual stimuli are instances of stimulus classes, which derive from a
common Stimulus base class. There are classes for visual and auditory stimuli available.
AStimulusTask descendant class may instantiate any number of stimuli
during OnInitialize(). Each stimulus must be made known to the StimulusTask class by
adding it to a so-called "Association" (see below). The StimulusTask class will then take
care of presenting those stimuli at appropriate points in time, according to user configuration.
Grouping of stimuli
Stimuli are presented as groups called "Associations". Each Association corresponds to a
single StimulusCode value. No Associations exist when
a StimulusCodedescendant's OnInitialize() function is called. As a coding example, the
descendant may add a stimulus to an Association with StimulusCode 23 by calling
SomeStimulusClass* pStimulus = new SomeStimulusClass;
... // set stimulus properties
Associations()[23].Add( pStimulus );
There, a new Association object for StimulusCode 23 will be created if it does not exist.
Stimulus codes are positive integers. They need not be continuous, may be specified in any
order, and chosen as deemed convenient. A zero StimulusCode represents absence of a
stimulus, so 0 cannot be chosen as a StimulusCode index for an association. Still, an
Association object with index 0 may be created, but it will be ignored during stimulus
presentation.
Note that an empty Association object is created on any access to a non-existing index, both
read and write accesses. Thus, you will need to take care to avoid unwanted empty
Associations, i.e., don't use an Association index unless you want that Association to exist. If
an empty Association exists, its StimulusCode may appear in the presentation sequence,
taking up a presentation time slot without any stimulus being presented. In case such
behavior is actually desired, an empty Association may be created by calling
Associations()[index];
Whenever the StimulusCode state becomes nonzero, all stimuli contained in the
corresponding Association are presented simultaneously. An Association may contain a
single stimulus only (e.g., when presenting images in the StimulusPresentation module), or it
may contain multiple stimuli (e.g., matrix speller rows and columns are made up of multiple
stimuli, where each stimulus is a single matrix entry). Also, Associations may contain
different kinds of stimuli (e.g., when presenting both images and audio files with
StimulusPresentation, or when using auditory stimuli to announce rows or columns in the
P3Speller).
Sequencing
Presentation sequences consist of Associations, represented by their StimulusCodes.
The StimulusTask base class does not do any sequencing by itself. Rather, each time
before a presentation happens, the OnNextStimulusCode() function is called, which must
be implemented by a StimulusTask descendant. The descendant class indicates the end of
a sequence by returning zero as a StimulusCode. Further, it indicates the end of a run by
returning a zero StimulusCode twice in a row (i.e., an empty sequence). The information
about existing StimulusCode values will be known by a descendant because it is responsible
for populating the Associations object, but to avoid duplication of information, it may be
appropriate to either
In the case of evoked brain potentials (ERPs, e.g., P300 wave), a linear classifier may be
used to classify between the two cases "ERP occurred" vs "no ERP occurred". Quite
favorably, it may be shown that in this case the linear classifier's output is a linear function of
the log-likelihood ratio representing evidence in favor of the "ERP occurred" case, and
against the "no ERP occurred" case. As a result, combined resp. accumulated evidence in
favor of single selection targets can be accurately computed from the classifier's output for
individual brain responses, by using Association information about how stimuli were grouped
in presentation. This allows classification to be performed entirely by the StimulusTask class,
omitting the need for classification code within a descendant class. Still, a descendant class
may implement its own classification by overriding the OnClassResult() function, or it may
modify the default classification result by overriding OnClassResult(), and calling
StimulusTask::OnClassResult() within its own implementation of that function.
Using a Different Classifier with StimulusTask
In general, linear classification may be considered optimal for detection of evoked potential
responses, and the P3SignalProcessing module performs such linear classification.
Still, you might want to use a different classification algorithm in order to classify the brain's
response to a stimulus, e.g. you might want to use oscillatory signal features in addition to or
in place of evoked potentials. In such cases, combination and accumulation of evidence may
not function properly, unless you transform classification output into the kind of data which is
expected by StimulusTask-based applications such as the P3Speller application module.
where ln stands for the natural logarithm, which is often termed log when provided as a
function in programming environments.
Using the above formula, the output of a binary classifier may be converted into a log-
likelihood ratio by first determining the probability against the existence of a response.
Depending on the nature of your classification algorithm, you may need to do some analysis
by your own in order to determine that probability. In many cases, however, may be
obtained from classifier output by one of the following methods:
For a classifier with binary output , if the false positive rate is estimated to be , and
the false negative rate is estimated to be , you will have
Measures of Significance
A -value from a statistical test is the probability for the null hypothesis to be true. The null
hypothesis is "there was no ERP", and thus a -value represents the desired probability
against an ERP.
Although a -value may not be directly available, other measures of significance, such as
a -value, may be converted into a -value. Formulae relating the -value to other
measures of significance may be found in statistics textbooks, or web resources.
Transmitting Classification Scores
The StimulusTask class expects to receive a control signal with a single channel, and a
single element. Whenever the StimulusCodeRes state is nonzero, it will treat the single
value of its control signal as an update to the classification score associated with the
StimulusCode sent in the StimulusCodeRes state.
Classification score updates may be sent any time during sequence or post-sequence
(PhaseInSequence state is 2 or 3). Though the order in which classification scores are sent
does not matter, StimulusTask expects to receive exactly one classification result for each
StimulusCode that has been presented since the last classification score update was sent.
Only a single classification score can be sent for each signal data block. If your classifier
operates by comparison of multiple brain responses, rather than each response individually,
you should store classification scores in a stack-like data structure, and send only the top of
the stack at each data block.
Events marked with * may occur multiple times in a row. Events given in square brackets
depend on the signal processing module, and may not occur at all; however, when they
occur, their place in the temporal sequence will be as specified in the table.
Progress from one application state to the next will occur according to the sequencing
parameters, or if requested by a handler via its doProgress output argument (see Input
events below).
OnPreflight
OnInitialize
DoPreRun* PreRun
Loop {
DoPreSequence* PreSequence
OnSequenceBegin
Loop {
DoStimulus* Stimulus
DoISI* ISI
OnSequenceEnd
DoPostSequence* PostSequence
OnPostRun
DoPostRun* PostRun
OnHalt
The OnClassResult event handler is supposed to determine a selection target from these
classification values. Classification values are provided in a ClassResult object;
theOnClassResult handler returns a pointer to a Target object representing the chosen
selection target, or a null pointer to indicate that no target has been chosen. On return from
theOnClassResult event handler, and in case of a non-null pointer, the target
object's Select() method is then called, which typically results in some action associated
with the target object in question, such as entering a letter into a speller.
The default OnClassResult handler calls AssociationMap::ClassifyTargets() to
translate classification values into selection targets; you may override this behavior by
providing your own handler.
Properties
All properties are protected, i.e. intended for use from descendant classes only.
AssociationMap Associations (rw)
An object of type AssociationMap, representing sets of stimuli and selection targets
associated with a given stimulus code.
Typically, a stimulus presentation application populates the Associations object with stimuli
and targets in its OnInitialize event handler, and relies on
theStimulusPresentationTask default mechanisms for stimulus presentation, and target
classification.
Often, but not always, there is a 1-to-1-to-1 correspondence between stimuli, stimulus
codes, and selection targets; for a detailed discussion of these terms, and how to use the
related objects in your own application, refer to Programming Reference:AssociationMap
Class.
Target* AttendedTarget (rw)
A pointer that refers to a selection target object, or a null pointer. When non-null,
the StimulusType state variable is set based on the AttendedTarget property such
thatStimulusType is set to 1 whenever the current StimulusCode state variable refers to
an Association that contains the specified target.
For a discussion of how stimuli, stimulus codes, and selection targets are related to each
other via Association objects, refer to Programming Reference:AssociationMap
Class#AssociationMap Class.
GUI::GraphDisplay Display (r)
The Display property provides access to the GUI::GraphDisplay object representing the
application module's output window.
ostream AppLog, AppLog.File, AppLog.Screen (w)
Inheriting from ApplicationBase, descendants of StimulusTask have access to
the AppLog, AppLog.File, and AppLog.Screen streams which are members
ofApplicationBase. These streams allow convenient output into an application log file
(AppLog.File), an application log window (AppLog.Screen), and both simultaneously
(AppLog).
Methods
Methods are declared protected, i.e. for use by descendants only.
DisplayMessage(string)
Displays a text message in the application window. This function is provided for convenience
and consistency of appearance; for detailed control over appearance of text messages, add
your own TextField object to the GUI::GraphDisplay represented by the Display property.
Parameters
WindowBackgroundColor
The window's background color, given as an RGB value. For convenience, RGB values may
be entered in hexadecimal notation, e.g. 0xff0000 for red.
PreRunDuration
The duration of the pause preceding the first sequence. Given in sample blocks, or in time
units when immediately followed with 's', 'ms', or similar.
PostRunDuration
Duration of the pause following last sequence. Given in sample blocks, or in time units when
immediately followed with 's', 'ms', or similar.
PreSequenceDuration
Duration of the pause preceding sequences (or sets of intensifications). Given in sample
blocks, or in time units when immediately followed with 's', 'ms', or similar.
When used in conjunction with the P3TemporalFilter, this value needs to be larger than
the EpochLength parameter. This allows classification to complete before the next sequence
of stimuli is presented.
StimulusDuration
For visual stimuli, the duration of stimulus presentation. For auditory stimuli, the maximum
duration, i.e. playback of audio extending above the specified duration will be muted. Given
in sample blocks, or in time units when immediately followed with 's', 'ms', or similar.
EarlyOffsetExpression
Allows the specification of an Expression that is constantly monitored during stimulus
presentation. When the value of the Expression transitions from zero to non-zero, the
stimulus is aborted early, even if the StimulusDuration has not yet elapsed. For example, set
this Expression to KeyDown==32 and start your source module with the --
LogKeyboard=1 flag: then the subject will be able to advance the stimulus sequence
manually by pressing the space key.
ISIMinDuration, ISIMaxDuration
Minimum and maximum duration of the inter-stimulus interval. During the inter-stimulus
interval, the screen is blank, and audio is muted.
Actual inter-stimulus intervals vary randomly between minimum and maximum value, with
uniform probability for all intermediate values. Given in sample blocks, or in time units when
immediately followed with 's', 'ms', or similar. Note that temporal resolution is limited to a
single sample block.
InterpretMode
An enumerated value selecting on-line classification of evoked responses:
By default, target selection is performed without considering the actual amount of evidence
that favors the selected target over other targets. Although the selected target will always be
a target with maximum classification score (i.e., evidence), other targets may have the same
or a similar score. It may be useful to omit classification in such situations altogether, by
specifying a minimum amount of evidence that must exist in favor of the selected target,
when compared to the next-best target. When used together with
theAccumulateEvidence option, this allows to dynamically control the number of stimulus
presentations, by simply repeating stimulus sequences until a sufficient amount of evidence
has been collected.
In classifier training, classifier weights may be normalized such that within-class variance is
1 (this is done by recent versions of the P300Classifier tool). In this case, you may use the
following equations to convert between the MinimumEvidence parameter , and the correct
classification chance :
For large , this relationship may be approximated and expressed in terms of error
probability :
Thus, the evidence value roughly corresponds to twice the number of leading zeros in the
desired error probability, if classifier weights are normalized. Some values are provided in
the following table:
5% 3
1% 4.6
0.5% 5.3
0.1% 6.9
0.05% 7.6
0.01% 9.2
AccumulateEvidence
By default, only those classification scores are used which have been received from the
signal processing module immediately prior to classification. When AccumulateEvidence is
set, classification scores are accumulated until a selection is actually performed. Typically,
accumulated classification scores will have higher evidence values, such that a selection
threshold set with MinimumEvidence will be eventually crossed after scores have been
accumulated for some time.
This allows for dynamically choosing the number of stimulus repetitions in a P300 paradigm,
by setting the number of stimulus repetitions to 1, and setting
the MinimumEvidenceparameter to a value greater zero.
In addition, accumulated overall evidence will not increase if there is no consistent evidence
in favor of a certain target. Thus, it is possible to operate a P300 BCI in a quasi-
asynchronous mode by using AccumulateEvidence, and choosing
a MinimumEvidence value that is large enough to make accidental selection unlikely. In this
configuration, no selection will be made unless the BCI user is actually concentrating on a
target for a number of stimulus presentations, resulting in consistently accumulating
evidence for that target.
NOTE: If you are using your own classifier, this feature will not work properly unless your
classifier's output matches certain criteria. Make sure to read these notes on how to use a
different classifier.
States
StimulusCode
The numerical ID of the stimulus being presented (16 bit).
StimulusType
This state is 1 during presentation of an attended stimulus, and 0 otherwise. The notion of an
"attended" stimulus requires data recording in copy mode.
StimulusBegin
This state is 1 during the first block of stimulus presentation, and 0 otherwise.
PhaseInSequence
This state is 1 during pre-sequence, 2 during sequence and 3 during post-sequence
(see Timeline).
PauseApplication
While this state is set to 1, no task processing occurs, i.e. the task is paused, and may be
resumed by setting PauseApplication to 0.
Timeline
Programming Reference:Stimulus Class
Contents
[hide]
1 Stimulus Class
o 1.1 Location
o 1.2 Synopsis
o 1.3 Properties
1.3.1 int Tag (rw)
o 1.4 Methods
1.4.1 Present()
1.4.2 Conceal()
o 1.5 Events
1.5.1 OnPresent()
1.5.2 OnConceal()
o 1.6 Descendants
2 SetOfStimuli Class
o 2.1 Location
o 2.2 Synopsis
o 2.3 Methods
2.3.1 Add(Stimulus pointer)
2.3.2 Remove(Stimulus pointer)
2.3.3 Clear()
2.3.4 DeleteObjects()
2.3.5 bool Contains(Stimulus pointer)
2.3.6 bool Intersects(SetOfStimuli)
2.3.7 Present()
2.3.8 Conceal()
3 See also
Stimulus Class
Location
src/shared/modules/application/stimuli
Synopsis
The Stimulus class is a virtual base class which defines an event handling interface for stimuli. There, a
"Stimulus" is defined as "an object that can present (and possibly conceal) itself."
The SetOfStimuli class allows to define sets of stimuli, and to present or conceal all of the stimuli it
contains.
Properties
int Tag (rw)
An arbitrary integer that may be used to encode information about a given stimulus (e.g., its corresponding
row in a configuration matrix).
Methods
Present()
Prompts a stimulus to present itself by calling its OnPresent() event handler.
Conceal()
Prompts a stimulus to conceal itself by calling its OnConceal() event handler.
Events
OnPresent()
In its OnPresent event handler, a stimulus is supposed to present itself, e.g., to make itself visible, play
itself if it is a sound or a movie, or highlight itself if it is a P300 matrix element.
OnConceal()
In its OnConceal event handler, a stimulus is supposed to conceal itself, e.g., make itself invisible, or
switch back to normal mode. For non-visual stimuli, this handler is typically empty.
This event is called Conceal rather than Hide because "Hide" is already used as a name for
a GraphObject's function that makes it invisible.
Descendants
The Stimulus class is parent to a class hierarchy containing a number of auditory and visual stimuli:
SetOfStimuli Class
Location
SetOfStimuli is declared in the Stimulus class header file.
Synopsis
SetOfStimuli is a helper class that allows to prompt presentation, concealing, and destruction of a
group of stimuli.
Methods
Add(Stimulus pointer)
Adds a stimulus object to the set.
Remove(Stimulus pointer)
Removes a given stimulus object from the set; nothing happens when the specified stimulus is not a
member of the set.
Clear()
Clears the set. Stimuli that were represented in the set in form of pointers are unaffected.
DeleteObjects()
Deletes all stimulus objects that are in the set, and clears the set. To avoid dangling pointers, and multiple
deletion, you should make sure that the set's members are not part of any other SetOfStimuli by the
time you call SetOfStimuli::DeleteObjects().
Present()
Prompts each element of the set to present itself by calling its Present() method.
Conceal()
Prompts each element of the set to conceal itself by calling its Conceal() method.
1 Target Class
o 1.1 Location
o 1.2 Synopsis
o 1.3 Properties
1.3.1 int Tag (rw)
o 1.4 Methods
1.4.1 Select()
o 1.5 Events
1.5.1 OnSelect()
o 1.6 Descendants
2 SetOfTargets Class
o 2.1 Location
o 2.2 Synopsis
o 2.3 Methods
2.3.1 Add(Target pointer)
2.3.2 Remove(Target pointer)
2.3.3 Clear()
2.3.4 DeleteObjects()
2.3.5 bool Contains(Target pointer)
2.3.6 bool Intersects(SetOfTargets)
2.3.7 Select()
3 See also
Target Class
Location
src/shared/modules/application/stimuli
Synopsis
The Target class is a virtual base class which defines an event handling interface for targets. There, a
"Target" is defined as "an object that performs an action when selected."
A Target descendant class will typically override its empty OnSelect() event handlers with a function
that performs an action. An application uses the publicTarget::Select() function to control its
behavior; this function, in turn, calls the virtual event handler.
The SetOfTargets class allows to define sets of targets, and to select all of the targets it contains.
Properties
int Tag (rw)
An arbitrary integer that may be used to encode information about a given target (e.g., its corresponding
row in a configuration matrix).
Methods
Select()
Prompts a target to perform its selection action by calling its OnSelect() event handler.
Events
OnSelect()
In its OnSelect event handler, a target performs an action, e.g. entering a letter into a speller.
Descendants
The Target class is parent to the SpellerTarget and AudioSpellerTarget classes.
SetOfTargets Class
Location
SetOfTargets is declared in the Target class header file.
Synopsis
SetOfTargets is a helper class that provides selection and destruction of a group of stimuli.
Methods
Add(Target pointer)
Adds a target object to the set.
Remove(Target pointer)
Removes a given target object from the set; nothing happens when the specified target is not a member of
the set.
Clear()
Clears the set. Targets that were represented in the set in form of pointers are unaffected.
DeleteObjects()
Deletes all target objects that are in the set, and clears the set. To avoid dangling pointers, and multiple
deletion, you should make sure that the set's members are not part of any other SetOfTargets by the
time you call SetOfTargets::DeleteObjects().
bool Intersects(SetOfTargets)
Returns true if any of the elements of the specified set is part of the set on which Intersects is called,
and false otherwise.
Select()
Calls each element's Select() method.
1 Location
2 Synopsis
3 Association Class
o 3.1 Properties
3.1.1 int StimulusDuration (rw)
3.1.2 int ISIMinDuration (rw)
3.1.3 int ISIMaxDuration (rw)
3.1.4 SetOfStimuli Stimuli (rw)
3.1.5 SetOfTargets Targets (rw)
o 3.2 Methods
3.2.1 Clear()
3.2.2 DeleteObjects()
3.2.3 Add(Stimulus pointer|Target pointer)
3.2.4 Remove(Stimulus pointer|Target pointer)
3.2.5 bool Contains(Stimulus pointer|Target pointer)
3.2.6 bool Intersects(SetOfStimuli|SetOfTargets)
3.2.7 Present()
3.2.8 Conceal()
3.2.9 Select()
4 AssociationMap Class
o 4.1 Methods
4.1.1 bool StimuliIntersect(StimulusCode1, StimulusCode2)
4.1.2 bool TargetsIntersect(StimulusCode1, StimulusCode2)
4.1.3 SetOfStimuli TargetToStimuli(Target pointer)
4.1.4 TargetClassification ClassifyTargets(ClassResult)
5 ClassResult Class
6 TargetClassification Class
o 6.1 Methods
6.1.1 Target pointer MostLikelyTarget()
7 See also
Location
src/shared/modules/application/stimuli/Association
Synopsis
This page describes four closely related classes declared in a common header file, all related to stimulus
presentation and target classification.
The Association class serves to associate stimuli with each other, targets with each other, stimuli with
targets, and to store sequencing information about such groups.
Properties
int StimulusDuration (rw)
The duration of stimulus presentation, given in sample blocks.
Methods
Clear()
Clears both stimulus and target sets.
DeleteObjects()
Deletes all stimulus and target objects that are part of the association, and clears the association.
NB: You should not call this function for an Association object unless you can be sure that no stimulus
is part of multiple associations. Otherwise, you will risk multiple deallocation of stimulus pointers. Typically,
you will keep track of existing stimuli in a separate SetOfStimuli object, and call that
set's DeleteObjects() member function in order to dispose of stimulus objects.
Add(Stimulus pointer|Target pointer)
Adds a stimulus or target object to the association.
bool Intersects(SetOfStimuli|SetOfTargets)
Returns true if any of the elements of the specified set is part of the association, and false otherwise.
Present()
Prompts each stimulus in the association to present itself by calling its Present() method.
Conceal()
Prompts each stimulus in the association to conceal itself by calling its Conceal() method.
Select()
For each target contained in the association, calls its Select() method.
AssociationMap Class
An AssocationMap links Associations, or groups of stimuli and targets, to stimulus codes. Each
stimulus code maps to a single Association object, and vice versa.
This way, the AssociationMap controls how stimuli are presented in groups, and sequences of groups,
in a stimulus task, and it provides the information required to determine a selected target from a user
response.
Methods
AssociationMap inherits publicly from std::map<int, Association, and provides all its member
functions. In a StimulusTask descendant, the Associations property provides access to the
single AssociationMap object stored inside the StimulusTask object.
Typically, only the subscript operator [] is required to assign stimulus codes to associations:
Similarly, association information may be read when the subscript operator appears on the right hand side
of an assignment:
int stimulusDuration = Associations()[ myStimulusCode ].StimulusDuration();
TargetClassification ClassifyTargets(ClassResult)
Translates a ClassResult object into TargetClassification object. A ClassResult object
contains classification values per stimulus code; these values are translated into classification values per
target.
ClassResult Class
A mapping of stimulus codes to classification values. Classification values are computed by
the P3TemporalFilter in conjunction with the LinearClassifier, and accumulated by theStimulusTask
class inside a ClassResult object before translated into a target selection.
TargetClassification Class
A TargetClassification object represents per-target classification values. Such an object is returned
from AssociationMap::ClassifyTargets().
Methods
Target pointer MostLikelyTarget()
Returns a pointer to the target associated with the largets classification value, or NULL if there are no
targets stored in the TargetClassification object.
1 Location
2 Synopsis
3 Speller Class
o 3.1 Methods
3.1.1 Enter(string)
3.1.2 Add(SpellerTarget pointer)
3.1.3 Remove(SpellerTarget pointer)
3.1.4 DeleteObjects()
3.1.5 SpellerTarget pointer SuggestTarget(string from, string to)
o 3.2 Events
3.2.1 OnEnter(string)
4 SpellerTarget Class
o 4.1 Methods
4.1.1 SpellerTarget(Speller)
4.1.2 Select()
o 4.2 Properties
4.2.1 string EntryText (rw)
5 See also
Location
src/shared/modules/application/speller
Synopsis
The Speller class provides an abstract interface for "Spellers". A "Speller" is an object that responds to
an OnEnter event, and holds a set of SpellerTarget objects. Holding a set
of SpellerTarget objects allows to suggest a "next target" based on a desired text output, and an
existing text output.
A SpellerTarget is a Target that is linked to a Speller such that selecting the target results in
entering text into the speller.
Speller Class
Methods
Enter(string)
Prompts the speller to react to the information provided in the argument string. The Speller class
interface makes no assumption about the encoding used, i.e. it does not interpret the argument string in
any way.
Add(SpellerTarget pointer)
Adds the specified SpellerTarget to the speller's set of targets.
Remove(SpellerTarget pointer)
Removes the specified SpellerTarget from the speller's set of targets.
DeleteObjects()
Deletes all SpellerTarget objects that are part of the speller's set of targets.
Events
OnEnter(string)
In its OnEnter event handler, a Speller descendant should perform actions that correspond to entering
the text given as an argument.
SpellerTarget Class
Methods
SpellerTarget(Speller)
When constructing a speller target, specify the Speller object it is associated with.
The SpellerTarget object will add itself to the Speller object's set of targets, and may be deleted
using Speller::DeleteObjects() when appropriate.
Select()
Will enter the text contained in the EntryText property into the speller specified when creating the object.
Properties
string EntryText (rw)
The text entered into the speller object when the target is selected.