|
|
(15 intermediate revisions by one user not shown) |
Line 1: |
Line 1: |
| + | __NOTOC__ |
| =Welcome= | | =Welcome= |
| This part of the Wiki is dedicated to developers who would like to implement their own plug-ins for AnyWave.<br /> | | This part of the Wiki is dedicated to developers who would like to implement their own plug-ins for AnyWave.<br /> |
− |
| |
| {| style="text-align: center; margin: auto;" | | {| style="text-align: center; margin: auto;" |
| |+ Quick Navigation | | |+ Quick Navigation |
| |- | | |- |
− | | [[File:Write maltab plugin.png|400px|link=AnyWave:WriteMatlabScripted|'''Write a MATLAB Plugin''']] | + | | [[File:Menu_matlab_plugin.png|400px|link=AnyWave:MATLAB_Plugin|'''Write a MATLAB Plugin''']] || [[File:Menu_cpp_plugin.png|400px|link=AnyWave:Cpp_Plugin|'''Write a c++ Plugin''']] |
| + | |- |
| + | | [[File:Menu_matlab_batch.png|400px|link=AnyWave:Plugin_Batch|'''Make your plugin batchable''']] || [[File:Menu_matlab_batch_gui_compatible.png|400px|link=AnyWave:MATLAB_Batch_GUI|'''Make your plugin compatible with the batch GUI of AnyWave''']] |
| |} | | |} |
− |
| |
− |
| |
− | AnyWave is written using the Qt Framework and the Qt plugin mechanism, so a good knowledge of the Qt Framework is required.
| |
− |
| |
− | =The SDK=
| |
− | If you have installed AnyWave on your system, you will find all the required files in the installation folder.<br />
| |
− | ==Linux==
| |
− | Build from sources following the instructions on our [https://gitlab.thevirtualbrain.org/anywave Gitlab].<br />
| |
− | Considering the default installation path, the requires folders to build a plugin are:<br />
| |
− | * '''/usr/local/AnyWave/include'''
| |
− | * '''/usr/local/AnyWave/lib'''
| |
− | ==Mac OS==
| |
− | The required folders to build a plugin are:<br />
| |
− | * '''/Applications/AnyWave.app/Contents/include'''
| |
− | * '''/Applications/AnyWave.app/Contents/dylibs'''
| |
− |
| |
− | ==Windows==
| |
− | The required folders to build a plugin are:<br />
| |
− | * '''AnyWave\include'''
| |
− | * '''AnyWave\lib'''
| |
− |
| |
− | =Basic requirements to build a plugin=
| |
− | We strongly recommend QtCreator as the tool to use which is available along with the Qt open source package you will need to build a plugin. <br/>
| |
− | Download Qt and Qt Creator here: [https://www.qt.io/download Get Qt and Qt Creator] <br/>
| |
− | '''Note about Windows:'''<br/>
| |
− | The Windows version of AnyWave is built with Visual Studio 2017 though, so, if you plan to build a plugin for Windows, consider using Visual Studio along with the Qt VS Addin.<br/>
| |
− | Technically speaking you can build a plugin with QtCreator using another compiler like '''gcc''' or '''clang'''.<br/>
| |
− | You will need to put the runtime DLL files of the chosen compiler into the AnyWave folder after you copied the plugin into the Plugins subdirectory.
| |
− |
| |
− | ==Prepare a project==
| |
− | Every Qt project starts with a .pro file which is the format used by qmake, the tool for building Qt projects.<br/>
| |
− | Building an AnyWave plugin requires that the paths to AnyWave headers and libraries must be set.<br/>
| |
− | A good practice is to set an environment variable called '''AW_ROOT''' that points to the root folder of your AnyWave installation.<br />
| |
− | Example:<br/>
| |
− | <syntaxhighlight lang="text">
| |
− | CONFIG += release warn_off c++11 plugin
| |
− | ROOT=$$(AW_ROOT)
| |
− | # on Mac OS the required folders are located inside the application bundle.
| |
− | macx{
| |
− | ROOT = $$(AW_ROOT)/Contents
| |
− | }
| |
− | INCLUDEPATH += $$ROOT/include
| |
− | LIBS += -L$$ROOT/lib
| |
− | DESTDIR = $$ROOT/Plugins
| |
− |
| |
− | # Mac OS application has a separated folder for Plugins.
| |
− | macx {
| |
− | DESTDIR = $$ROOT/../../Anywave_Plugins
| |
− | # shared libs on Mac are located in the application bundle.
| |
− | LIBS += -F/Library/Frameworks -L$$ROOT/dylibs
| |
− | }
| |
− | </syntaxhighlight>
| |
− | You may use this file as .pri that you could include in every project you plan to develop:<br/>
| |
− | [http://meg.univ-amu.fr/AnyWave/sdk/plugins.pri Get plugins.pri file]
| |
− |
| |
− | =Build a reader plugin=
| |
− | As a tutorial, we are going to develop here a reader plugin called "My Reader".<br/>
| |
− | Each Qt project starts with a .pro file which qmake use to compile and link a Qt project.<br/>
| |
− | A Reader plugin is a Qt shared library that should be placed in the Plugins directory of AnyWave.<br/>
| |
− | ==Step 1: Prepare the folder==
| |
− | Let's create a folder in which we will put the files required to build our plugin:<br/>
| |
− | <syntaxhighlight lang="bash">
| |
− | mkdir MyReader && cd MyReader
| |
− | </syntaxhighlight>
| |
− |
| |
− | ==Step 2: create the project file==
| |
− | Now we can create the .pro file for our project:<br>
| |
− | <syntaxhighlight lang="text">
| |
− | # We assume that the plugins.pri file is also in our plugin directory.
| |
− | include(plugins.pri)
| |
− | # The name of our plugin
| |
− | TARGET=MyReader
| |
− | # Our project must be a library
| |
− | TEMPLATE = lib
| |
− | # Just to avoid unwanted libs, Qt GUI is not usefull for a reader plugin (no widgets/no graphics items).
| |
− | QT -= gui
| |
− |
| |
− | # We must link with some AnyWave libs
| |
− | # AwRW is the AnyWave ReadWrite lib.
| |
− | unix{
| |
− | LIBS += -lAwRW
| |
− | }
| |
− | win32{
| |
− | LIBS += AwReadWriteLib.lib
| |
− | }
| |
− |
| |
− | HEADERS += MyReader.h
| |
− | SOURCES += MyReader.cpp
| |
− | </syntaxhighlight>
| |
− |
| |
− | ==Step 3: Let's code==
| |
− | The header file:<br/>
| |
− | <syntaxhighlight lang="cpp">
| |
− | #pragma once
| |
− | #include <AwFileIO.h> // AnyWave base class header
| |
− | #include <QFile> // Qt QFile object
| |
− |
| |
− | class Q_DECL_EXPORT MyReader : public AwFileIO
| |
− | {
| |
− | Q_OBJECT
| |
− | Q_INTERFACES(AwFileIO)
| |
− | public:
| |
− | explicit MyReader(const QString& fileName);
| |
− | ~MyReader();
| |
− |
| |
− | qint64 readDataFromChannels(float start, float duration, QList<AwChannel *>& channels) override;
| |
− | AwFileIO::FileStatus openFile(const QString &path) override;
| |
− | AwFileIO::FileStatus canRead(const QString &path) override;
| |
− | void cleanUpAndClose() override;
| |
− | protected:
| |
− | // keep a file object to read data
| |
− | QFile m_dataFile;
| |
− | float m_samplingRate; // keep the global sampling rate value.
| |
− | };
| |
− |
| |
− | class Q_DECL_EXPORT MyReaderPlugin : public AwFileIOPlugin
| |
− | {
| |
− | Q_OBJECT
| |
− | Q_INTERFACES(AwFileIOPlugin)
| |
− | Q_PLUGIN_METADATA(IID AwFileIOInterfacePlugin_IID)
| |
− | public:
| |
− | MyReaderPlugin();
| |
− |
| |
− | /** IMPORTANT: use the following MACRO. **/
| |
− | AW_INSTANTIATE_PLUGIN(MyReader)
| |
− | };
| |
− |
| |
− | </syntaxhighlight>
| |
− | The header file declares two classes: One is the reader object itself and the other one is the plugin object.<br/>
| |
− | The methods declared with the override keyword MUST be implemented otherwise the compiler will complain and your plugin won't be a valid AnyWave plugin.<br/>
| |
− | ===constructor/destructor (init our plugin)===
| |
− | Let's have a closer look at methods we have to implement:<br/>
| |
− | <syntaxhighlight lang="cpp">
| |
− | explicit MyReader(const QString& fileName);
| |
− | ~MyReader();
| |
− | </syntaxhighlight>
| |
− | The constructor/destructor methods. This is where you initialise the member variables and handle the destruction by releasing allocated memory and open files.<br/>
| |
− | Let's see the code for our constructor:<br/>
| |
− | <syntaxhighlight lang="cpp">
| |
− | #include "MyReader.h"
| |
− |
| |
− | // constructor for reader object
| |
− | MyReader::MyReader(const QString& fileName) : AwFileIO(fileName)
| |
− | {
| |
− | // do some inits here.
| |
− | }
| |
− |
| |
− | // constructor of Plugin object
| |
− | // Here we will define the name of our Plugin and it's properties/flags for AnyWave
| |
− | MyReaderPlugin::MyReaderPlugin() : AwFileIOPlugin()
| |
− | {
| |
− | // define the name (must be unique in AnyWave)
| |
− | name = QString("MyReader Plugin");
| |
− | // short description of what the plugin does.
| |
− | description = QString("Read My Format");
| |
− | // info about manufacturer/author
| |
− | manufacturer = QString("Myself");
| |
− | // version string
| |
− | version = QString("1.0");
| |
− | // the file extensions the reader can open.
| |
− | fileExtensions = { ".myr" };
| |
− | // Flags to define the behavior in AnyWave
| |
− | m_flags = Aw::HasExtension|Aw::CanRead; // Here we tell AnyWave that the plugin handle file extension (mostly the case)
| |
− | // and we also specify CanRead flag to tell AnyWave that this is a READER plugin.
| |
− | }
| |
− | </syntaxhighlight>
| |
− | Cleaning up our reader:
| |
− | <syntaxhighlight lang="cpp">
| |
− |
| |
− | ///
| |
− | /// cleanUpAndClose must be implemented in a reader.
| |
− | /// This is where you close the files, destroy objects, etc.
| |
− | void MyReader::cleanUpAndClose()
| |
− | {
| |
− | // Here we simply make sure that the binary file is closed.
| |
− | m_dataFile.close();
| |
− | }
| |
− |
| |
− | </syntaxhighlight>
| |
− |
| |
− | ===Open and read the data===
| |
− | Now we have to implement the three methods that will allow our plugin to open and read data from a file:<br/>
| |
− | '''IMPORTANT NOTICE'''<br/>
| |
− | We assume the data are stored as real amplitudes values, in float32. The expected unit for EEG/SEEG is microVolt where the expected units for MEG is pT (pico Tesla).<br/>
| |
− | These are the units AnyWave expects, so if you have to write a reader plugin for a format in which units are different, implement the unit conversion in the readDataFromChannels method.<br/>
| |
− | First, we'll code the canRead method:<br/>
| |
− | <syntaxhighlight lang="cpp">
| |
− | #include "MyReader.h"
| |
− | #include <QFile>
| |
− |
| |
− | ///
| |
− | /// canRead : use to open a file and check if the format is ok.
| |
− | /// Returns a flag informing AnyWave that we can or can't read that file.
| |
− | AwFileIO::FileStatus MyReader::canRead(const QString& fileName)
| |
− | {
| |
− | // Here we won't really open the file, we are just going to check if we can.
| |
− |
| |
− | // Let's suppose or .myr format is a text header file where the first line is a string with the following keyword "MYFORMAT".
| |
− |
| |
− | QFile file(fileName);
| |
− | if (file.open(QIODevice::ReadOnly|QIODevice::Text) // try to open as a text file.
| |
− | {
| |
− | QString line = file.readLine(); // read the first line.
| |
− | file.close();
| |
− | if (line == "MYFORMAT") {
| |
− | return AwFileIO::NoError; // it's OUR format!
| |
− | }
| |
− | }
| |
− | return AwFileIO::WrongFormat; // We don't know this file format.
| |
− | }
| |
− | </syntaxhighlight>
| |
− | Now we must implement the openFile method. This is where we'll inform AnyWave about what is available in the file (channels, markers): <br/>
| |
− | <syntaxhighlight lang="cpp">
| |
− | #include "MyReader.h"
| |
− | #include <QFile>
| |
− |
| |
− | AwFileIO::FileStatus MyReader::openFile(const QString& fileName)
| |
− | {
| |
− | // open the text header file and get informations about what is present in the data file.
| |
− | // We suppose that a binary data file is associated to the header file (like in the ADES format).
| |
− | QFile file(fileName);
| |
− | if (!file.open(QIODevice::ReadOnly|QIODevice::Text) {
| |
− | // failed to open the header file: provide an error message and quit.
| |
− | m_error = QString("Failed to open the file.");
| |
− | return AwFileStatus::FileAccess;
| |
− | }
| |
− | // read first line (remember MYFORMAT tag)
| |
− | file.readLine();
| |
− | // the second line contains the global sampling rate of data
| |
− | // We store that information in a member variable for further use in readDataFromChannels.
| |
− | m_samplingRate = (float)file.readLine().toDouble();
| |
− | // the next line contains the total number of samples per channel.
| |
− | qint64 nSamples = file.readLine().toInt();
| |
− | // the next line contains the number of channels and their order in the data.
| |
− | int nChannels = file.readLine().toInt();
| |
− | // the following lines contains all the electrode labels of the corresponding channels.
| |
− | // Use a list to store them
| |
− | QStringList labels, types;
| |
− | for (auto i = 0; i < nChannels; i++) // now get all the channels label
| |
− | labels.append(file.readLine());
| |
− | // the following lines (nChannels) contains the types of the channels (EEG, SEEG, Other, ECG, ...)
| |
− | // We suppose the types are compatible with the ones AnyWave can handle.
| |
− | for (auto i = 0; i < nChannels; i++)
| |
− | types.append(file.readLine());
| |
− |
| |
− | // ok now we can add the channels to the info object of our plugin:
| |
− | for (auto i = 0; i < nChannels; i++) {
| |
− | AwChannel channel;
| |
− | channel.setName(labels.at(i));
| |
− | channel.setType(AwChannel::stringToType(types.at(i)));
| |
− | channel.setSamplingRate(samplingRate);
| |
− | // adjust the unit and default gain depending on type:
| |
− | if (channel.isEEG() || channel.isSEEG())
| |
− | channel.setUnit(QString::fromLatin1("µv"));
| |
− | if (channel.isMEG() || channel.isReference())
| |
− | channel.setUnit("pT");
| |
− |
| |
− | infos.addChannel(channel);
| |
− | }
| |
− | // A Block is the basic data set object AnyWave uses.
| |
− | AwBlock *block = infos.newBlock(); // instantiate a new data set using the infos object (a member of AwFileIO).
| |
− | // set the number of samples in the block
| |
− | block->setSamples(nSamples);
| |
− | // set also the duration in seconds
| |
− | block->setDuration(nSamples / samplingRate);
| |
− |
| |
− | // Done
| |
− | file.close();
| |
− | // we suppose the binary file has got he same filename but a .dat extension.
| |
− | // So open our binary file for further use (in the readDataFromChannels method).
| |
− | // replace .myr extension with .dat
| |
− | QString dataFileName = fileName;
| |
− | dataFileName.replace(QString(".myr"), QString(".dat");
| |
− |
| |
− | if (!m_dataFile.open(QIODevice::ReadOnly)) {
| |
− | m_error = "Could not open the binary file.";
| |
− | return AwFileIO::FileAcess
| |
− | }
| |
− |
| |
− | // we're ok the binary file is open.
| |
− | return AwFileIO::NoError;
| |
− | }
| |
− | </syntaxhighlight>
| |
− |
| |
− | Now the important part: reading the data.<br/>
| |
− | <syntaxhighlight lang="cpp">
| |
− | #include "MyReader.h"
| |
− |
| |
− | ///
| |
− | /// readDataFromChannels expects a position in the file (start) and a duration in seconds (duration).
| |
− | /// It will read the data for the requested channels (channels).
| |
− | /// Returns the number of samples read for a channel.
| |
− | /// Returns 0 if no data could be loaded.
| |
− | qin64 MyReader::readDataFromChannels(float start, float duration, AwChannelList& channels)
| |
− | {
| |
− | // basic checking
| |
− | if (channels.isEmpty())
| |
− | return;
| |
− |
| |
− | // number of samples to read
| |
− | qint64 nSamples = (qint64)floor(duration * m_samplingRate);
| |
− | // offset sample in channel
| |
− | qint64 nStart = (qint64)floor(start * m_samplingRate);
| |
− | // total number of channels in the file:
| |
− | auto nChannels = infos.channelsCount();
| |
− | // Starting sample in the file.
| |
− | qint64 startSample = nStart * nChannels;
| |
− |
| |
− | // check positions and samples
| |
− | if (nSamples <= 0 || nStart > infos.totalSamples())
| |
− | return 0;
| |
− |
| |
− | // check if the length requested goes beyond the file limit:
| |
− | if (nStart + nSamples > infos.totalSamples())
| |
− | nSamples = info.totalSamples() - nStart; // reduce the number of samples to read to match the file limit.
| |
− |
| |
− | // total number of samples to read
| |
− | qint64 totalSize = nSamples * nChannels;
| |
− |
| |
− | // instantiate a buffer to read samples
| |
− | float *buf = new float[totalSize];
| |
− | // Set the file position
| |
− | m_dataFile.seek(startSample * sizeof(float));
| |
− | // read data
| |
− | qint64 = m_dataFile.read((char *)buf, totalSize * sizeof(float));
| |
− | if (read <= 0) {
| |
− | delete[] buf;
| |
− | return 0;
| |
− | }
| |
− |
| |
− | // compute the real number of samples successfully read.
| |
− | read /= sizeof(float);
| |
− | read /= nChannels;
| |
− |
| |
− | // ok now fill the data vector of the requested channels:
| |
− | for (auto c : channels) {
| |
− | int index = infos.indexOfChannel(c->name());
| |
− | float *data = c->newData(read); // replace the current data vector of the channel by a new one. The size of vector is the number of samples read.
| |
− | qint64 count = 0;
| |
− | while (count++ < read) // get the data from the buffer for the current channel.
| |
− | *data++ = (float)buf[index + count * nChannels];
| |
− | }
| |
− | // Done, free the buffer and finish.
| |
− | delete[] buf;
| |
− | return read;
| |
− | }
| |
− |
| |
− | </syntaxhighlight>
| |
− |
| |
− | =Build a signal processing plug-in=
| |
− | ==[[AnyWave:WriteMatlabScripted|How to write a MATLAB plug-in]]==
| |
− | ===[[AnyWave:MATLAB_API|MATLAB API]]===
| |
− |
| |
− | ==[[AnyWave:WritePythonScripted|How to write a Python Scripted plug-in]]==
| |
This part of the Wiki is dedicated to developers who would like to implement their own plug-ins for AnyWave.