Difference between revisions of "AnyWave:DeveloperCorner"

From WikiMEG
Jump to: navigation, search
 
(14 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:menu_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]]==
 

Latest revision as of 16:14, 21 April 2020

Welcome

This part of the Wiki is dedicated to developers who would like to implement their own plug-ins for AnyWave.

Quick Navigation
Write a MATLAB Plugin Write a c++ Plugin
Make your plugin batchable Make your plugin compatible with the batch GUI of AnyWave