AnyWave:DeveloperCorner

From WikiMEG
Revision as of 11:23, 30 March 2020 by Bruno (Talk | contribs)

Jump to: navigation, search

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


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.

Linux

Build from sources following the instructions on our Gitlab.
Considering the default installation path, the requires folders to build a plugin are:

  • /usr/local/AnyWave/include
  • /usr/local/AnyWave/lib

Mac OS

The required folders to build a plugin are:

  • /Applications/AnyWave.app/Contents/include
  • /Applications/AnyWave.app/Contents/dylibs

Windows

The required folders to build a plugin are:

  • 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.
Download Qt and Qt Creator here: Get Qt and Qt Creator
Note about Windows:
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.
Technically speaking you can build a plugin with QtCreator using another compiler like gcc or clang.
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.
Building an AnyWave plugin requires that the paths to AnyWave headers and libraries must be set.
A good practice is to set an environment variable called AW_ROOT that points to the root folder of your AnyWave installation.
Example:

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
}

You may use this file as .pri that you could include in every project you plan to develop:
Get plugins.pri file

Build a reader plugin

As a tutorial, we are going to develop here a reader plugin called "My Reader".
Each Qt project starts with a .pro file which qmake use to compile and link a Qt project.
A Reader plugin is a Qt shared library that should be placed in the Plugins directory of AnyWave.

Step 1: Prepare the folder

Let's create a folder in which we will put the files required to build our plugin:

mkdir MyReader && cd MyReader

Step 2: create the project file

Now we can create the .pro file for our project:

# 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

Step 3: Let's code

The header file:

#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)
};

The header file declares two classes: One is the reader object itself and the other one is the plugin object.
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.

constructor/destructor (init our plugin)

Let's have a closer look at methods we have to implement:

explicit MyReader(const QString& fileName);
~MyReader();

The constructor/destructor methods. This is where you initialise the member variables and handle the destruction by releasing allocated memory and open files.
Let's see the code for our constructor:

#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.
}

Cleaning up our reader:

///
/// 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();  
}

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:
IMPORTANT NOTICE
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).
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.
First, we'll code the canRead method:

#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.
}

Now we must implement the openFile method. This is where we'll inform AnyWave about what is available in the file (channels, markers):

#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;
}

Now the important part: reading the data.

#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;
}

Build a signal processing plug-in

How to write a MATLAB plug-in

MATLAB API

How to write a Python Scripted plug-in