AnyWave:BuildReader
One important thing with AnyWave is to be able to read a data file.
Although that AnyWave is able to read some common EEG or MEG file formats, you might need to read a particular data file.
The only way to achieve that is to build a Reader plug-in for AnyWave.
This will require implementing a C++ plug-in using the SDK.
See the previous sections of the Developer's corner to see how to build the SDK and use it to build a new plug-in.
Let's start with the basic cmake project that we will modify to suit our needs.
ADES reader as example
AnyWave is able to read .ades file format which is a simple file format built upon a text header file and a binary data file.
This 'plug-in' is embedded within AnyWave but we are going to implement it as an external plug-in, for the sake of demonstration.
Define the C++ classes
A C++ plug-in is defined by two classes:
- A class that describes the plug-in to AnyWave.
- A class that describes the core mechanism of the plug-in.
That is true for all kind of plug-ins in AnyWave.
A good knowledge of the Qt Framework is required as plug-in are build as Qt Plugins.
It's a good start to read the Get Used with AnyWave C++ objects section.
AwReaderPlugin and AwFileReader classes
As mentioned above, two C++ classes must be described and implemented to build a Reader plug-in.
Here are the two classes, described within one header file:
#ifndef DESREADER_H #define DESREADER_H #include <AwReaderInterface.h> #include <QtCore> #include <QDataStream> class ADesReader : public AwFileReader // our class must derived from AwFileReader { Q_OBJECT // Q_OBJECT and Q_INTERFACES macro are specific to the Qt Framework. Q_INTERFACES(AwFileReader) // They indicate that the object is also derived from QObject and implements the Qt signals and slots mechanism. public: ADesReader(const QString& filename); ~ADesReader() { cleanUpAndClose(); } // The three methods above need to be implemented. There are virtuals method defined in AwFileReader. long readDataFromChannels(float start, float duration, QList<AwChannel *> &channelList); // This method will get data from the file and fill channels with them. FileStatus openFile(const QString &path); // This method will open the file and return a status. FileStatus canRead(const QString &path); // This method will check if the file format is correct and return a status. // The method above is optional but it is a good practice to implement it. void cleanUpAndClose(); // A cleanup method to clean memory and close the file. private: // Here we define variables and methods required by our plug-in. QFile m_headerFile; // A QFile to handle the text header file. QFile m_binFile; // A QFile to handle the binary data file. QTextStream m_headerStream; // A stream object to read the content of the header file. QDataStream m_binStream; // A stream object to read the content of the data file. float m_samplingRate; // A variable to store the global sampling rate. int m_nSamples; // A variable to store the number of samples. QString m_binPath; // A variable to store the file path to the binary data file. }; class ADesReaderPlugin : public AwReaderPlugin // The plugin class must derived from AwReaderPlugin { Q_OBJECT // Define the object as a QObject. The interface for the plugin must be AwReaderPlugin Q_INTERFACES(AwReaderPlugin) public: ADesReaderPlugin(); // The constructor ADesReader *newInstance(const QString& filename) { return new ADesReader(filename); } // MANDATORY method that must instantiate the AwFileReader derived class for our plug-in. }; #endif // DESREADER_H
Now the implementation:
#include "ADesReader.h" // Plugin constructor ADesReaderPlugin::ADesReaderPlugin() : AwReaderPlugin() { name = "ADES Reader"; // give a name to our plugin. The name must be unique. description = QString(tr("Open .ades files")); // give a description about what format the plug-in will load. version = QString("1.0"); // Version information, not used for now. fileExtensions << "*.ades"; // Add extension filter four our format. m_flags = Aw::ReaderHasExtension; // Set flags to tell AnyWave about some features of our plug-in. Here we just inform that the plugin can handle file extensions. } // FileReader constructor ADesReader::ADesReader(const QString& path) : AwFileReader(path) // This is where we can initialize our FileReader derived object. { m_binStream.setVersion(QDataStream::Qt_4_4); // Qt specific: setting the version of Qt to 4.4 for stream objects. m_samplingRate = 0; // init variables. m_nSamples = 0; } // The cleanUp Method void ADesReader::cleanUpAndClose() { m_headerFile.close(); // very simple: we close the two open files (header and data) m_binFile.close(); } // canRead() // This method must check if the file path given is a valid file. // Because some file formats are using the same file extensions (.eeg, for example), we must be sure that our plug-in will open a compatible file. ADesReader::FileStatus ADesReader::canRead(const QString &path) { cleanUpAndClose(); // Cleanup first (just in case) m_headerFile.setFileName(path); if (!m_headerFile.open(QIODevice::ReadOnly | QIODevice::Text)) // Try to open the header file. return AwFileReader::FileAccess; // If failed, return FileAcess error status. m_headerStream.setDevice(&m_headerFile); // Setting up stream object to read the header file content. QString line = m_headerStream.readLine(); // Read the first line of header file m_headerFile.close(); if (line.toUpper().startsWith("#ADES")) // ADES format expects the first line to be #ADES return AwFileReader::NoError; // if the line is correct, return the NoError status. return AwFileReader::WrongFormat; // The header file is invalid. } // openFile() // This method will open the file path given as parameter. // Normally the file had been checked before by canRead() so we are sure to open a ADES file. ADesReader::FileStatus ADesReader::openFile(const QString &path) { cleanUpAndClose(); // clean up before, just in case. m_headerFile.setFileName(path); m_headerStream.setDevice(&m_headerFile); if (!m_headerFile.open(QIODevice::ReadOnly)) // open header file return AwFileReader::FileAccess; // should not happen QList<QPair<QString, int> > labels; // build a list of pairs. A channel is identified by a name and a type. Name will be stored as QString as type will be stored as integer. while (!m_headerStream.atEnd()) // Read header file until its end. { QString line = m_headerStream.readLine(); // get a line QStringList tokens = line.split("="); // split the line around "=" symbol if (!tokens.isEmpty() && !line.startsWith("#")) // Skip the line if it is empty or begins with the "#" symbol. { QString key = tokens.at(0); // Parsing lines QString val; if (key.trimmed().toUpper() == "SAMPLINGRATE") // extract sampling rate keyword m_samplingRate = tokens.at(1).toDouble(); else if (key.trimmed().toUpper() == "NUMBEROFSAMPLES") // extract number of samples keyword m_nSamples = tokens.at(1).toInt(); else // it is a channel // Extract channel informat (Remember that channels are described by: channelName = Type { QPair<QString, int> pair; pair.first = tokens.at(0).trimmed(); if (tokens.size() == 2) pair.second = AwChannel::stringToType(tokens.at(1).trimmed()); else pair.second = AwChannel::EEG; labels << pair; // add a new channel pair to the list. } } } if (labels.isEmpty() || m_samplingRate == 0. || m_nSamples == 0) // if key values are missing or invalid, returns error status. return AwFileReader::WrongFormat; for (int i = 0; i < labels.size(); i++) // Now parsing the channel pairs list. { AwChannel chan; QPair<QString, int> pair = labels.at(i); chan.setName(pair.first); AwChannel *inserted = infos.addChannel(chan); // insert a new AwChannel object into infos. inserted->setType(pair.second); // setting the type inserted->setSamplingRate(m_samplingRate); // setting the sampling rate (AnyWave expects that all channels have a sampling rate). switch (pair.second) // setting gain and unit depending on channel type. { case AwChannel::EEG: inserted->setGain(150); inserted->setUnit("µV"); break; case AwChannel::SEEG: inserted->setGain(300); inserted->setUnit("µV"); break; case AwChannel::MEG: inserted->setGain((float)1E-12); // IMPORTANT: MEG channels are expected to be expressed in pT. inserted->setUnit("pT"); break; } } AwBlock *block = infos.newBlock(); // Now that we add all the channels to the infos class of our plug-in we must add a block of data. // AnyWave handle data by blocks. For now, only continous data, (only 1 block) are visualized by AnyWave. Anywave, it is already possible to internally handle multiple blocks. // Future versions of AnyWave will permit to visualize epoched data. block->setDuration((float)m_nSamples / m_samplingRate); // A block must have a duration in seconds and a number of samples. block->setSamples(m_nSamples); m_headerFile.close(); // We have done reading the header file. Close it. m_binPath = path; // set the path to the binary file: the extension must be .dat m_binPath.replace(QString(".ades"), QString(".dat")); m_binFile.setFileName(m_binPath); m_binStream.setDevice(&m_binFile); if (!m_binFile.open(QIODevice::ReadOnly)) // trying to open binary file. return AwFileReader::FileAccess; // An optional marker file could be present QString markerPath = path; markerPath.replace(QString(".ades"), QString(".mrk")); // Setting path name for marker file (extension must be .mrk) if (QFile::exists(markerPath)) // Does the .mrk file exist? { // Yes, so read it. QFile markerFile(markerPath); QTextStream stream(&markerFile); // .mrk file is text file with tab separated values describing markers. if (markerFile.open(QIODevice::ReadOnly | QIODevice::Text)) { AwMarkerList markers; // create a list of markers while (!stream.atEnd()) // read the .mrk file til the end. { QString line = stream.readLine(); line = line.trimmed(); // processing line and skip line starting with // if (!line.startsWith("//")) { QString label = line.section('\t', 0, 0); if (label.isEmpty()) // no label => skip line continue; QString value = line.section('\t', 1, 1); if (value.isEmpty()) continue; QString position = line.section('\t', 2, 2); if (position.isEmpty()) continue; QString duration = line.section('\t', 3, 3); AwMarker m; // create marker object. m.setLabel(label); // set marker attribudes (label, value, position and duration). m.setValue((qint16)value.toInt()); m.setStart(position.toFloat()); if (!duration.isEmpty()) m.setDuration(duration.toFloat()); block->addMarker(m); // add the marker to the current block of data. } } markerFile.close(); } } // done return AwFileReader::NoError; } // // readDataFromChannels() // This is the main method AnyWave will call to read data from the file. // AnyWave expects data to be represented by AwChannel objects. Each channel contains its own vector of data. // Therefore, to read data we must handle a list of channels, a position in the file and the amount of data to read. // Position and duration are expressed as seconds, not samples. long ADesReader::readDataFromChannels(float start, float duration, AwChannelList &channelList) { if (channelList.isEmpty()) // Ask to read an empty list => do nothing return 0; if (duration <= 0) // Duration of data is invalid => do nothing. return 0; // number of samples to read qint64 nSamples = (qint64)floor(duration * m_samplingRate); // starting sample in channel. qint64 nStart = (qint64)floor(start * m_samplingRate); // total number of channels in file. qint32 nbChannels = infos.channelsCount(); // starting sample in file. qint64 startSample = nStart * nbChannels; if (nSamples <= 0) return 0; if (nStart > infos.totalSamples()) // infos.totalSamples() will return the total number of samples in the file for one channel. return 0; // The binary file of ADES format contains multiplexed data with one sample as a 32bit float. // That means we must first read all the channels data into a temporary buffer and then de-multiplexed the data to retrieve the vector data for a channel. float *buf = NULL; // Buffer variable used to read data. int totalSize = nSamples * nbChannels; // compute the total number of bytes to read into the buffer. buf = new float[totalSize]; // allocate buffer m_binFile.seek(startSample * sizeof(float)); int read = m_binStream.readRawData((char *)buf, totalSize * sizeof(float)); // Read data read /= sizeof(float); if (read == 0) // check if read function succeeded or not. { delete [] buf; return 0; } read /= nbChannels; // Browse the channel list foreach (AwChannel *c, channelList) { int index = infos.indexOfChannel(c->name()); // infos.indexOfChannel() will give the index of channel in data based on its name. float *data = c->newData(read); // create a new vector of data for the channel. int count = 0; while (count < read) // copy data from buffer to the channel. { *data++ = (float)buf[index + count * nbChannels]; count++; } } // done demultiplexing data for channels, so delete allocated buffer. delete[] buf; return read; // returns the number of bytes read } // IMPORTANT: Q_EXPORT_PLUGIN2(ADesReader, ADesReaderPlugin) // Add this macro so the Qt Framework will correctly build a Qt Plugin. // Typically the parameters are the name of the FileReader and the name of the plugin object. // It is up to the developer to name them correctly.