Difference between revisions of "AnyWave:BuildReader"
(22 intermediate revisions by one user not shown) | |||
Line 5: | Line 5: | ||
See the previous sections of the [[AnyWave:DeveloperCorner|Developer's corner]] to see how to build the SDK and use it to build a new plug-in.<br /> | See the previous sections of the [[AnyWave:DeveloperCorner|Developer's corner]] to see how to build the SDK and use it to build a new plug-in.<br /> | ||
− | Let's start with the [http://meg.univ-amu.fr/AnyWave/tuto/CMakeLists.txt basic cmake project] | + | Let's start with the [http://meg.univ-amu.fr/AnyWave/tuto/CMakeLists.txt basic cmake project] that we will modify to suit our needs. |
=ADES reader as example= | =ADES reader as example= | ||
Line 11: | Line 11: | ||
This 'plug-in' is embedded within AnyWave but we are going to implement it as an external plug-in, for the sake of demonstration. | 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:<br /> | ||
+ | * 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.<br /> | ||
+ | |||
+ | It's a good start to read the [[AnyWave:AwObjects|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.<br /> | ||
+ | Here are the two classes, described within one header file: | ||
+ | <syntaxhighlight lang="cpp"> | ||
+ | #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 | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Now the implementation:<br /> | ||
+ | <syntaxhighlight lang="cpp"> | ||
+ | #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. | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | You can download a zip file containing all the required files to build our plug-in by clicking [http://meg.univ-amu.fr/AnyWave/tuto/BuildReader/ades_reader.zip here] | ||
+ | |||
+ | ==Editing cmake project to build our reader== | ||
+ | Starting with the basic cmake project file, only few modifications are required to build our reader plug-in: | ||
+ | |||
+ | <syntaxhighlight lang="cmake"> | ||
+ | |||
+ | SET(SRCS | ||
+ | ${CMAKE_SOURCE_DIR}/ades_reader.cpp) # Add our implementation file | ||
+ | |||
+ | SET(MOCS | ||
+ | ${CMAKE_SOURCE_DIR}/ades_reader.h) # Remember that objects are derived from QObject and thus require the MOC tool to parse them. | ||
+ | |||
+ | qt4_wrap_cpp(ADES_MOCS ${MOCS}) # cmake will call Qt MOC tool and wrap moc file into variable ADES_MOCS | ||
+ | |||
+ | |||
+ | add_library(ADESReader SHARED ${SRCS} ${ADES_MOCS}) # Add sources to the make rule. | ||
+ | target_link_libraries(ADESReader AwCoreLib AwReadWriteLib ${QT_LIBRARIES}) # Add libraries to linker | ||
+ | |||
+ | </syntaxhighlight> | ||
+ | |||
+ | If the SDK is correctly installed an configured, just type '''cmake .''' in the source folder and our plug-in should build and install into the SDK folder. |
Latest revision as of 14:28, 24 March 2015
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.
Contents
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.
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.
You can download a zip file containing all the required files to build our plug-in by clicking here
Editing cmake project to build our reader
Starting with the basic cmake project file, only few modifications are required to build our reader plug-in:
SET(SRCS ${CMAKE_SOURCE_DIR}/ades_reader.cpp) # Add our implementation file SET(MOCS ${CMAKE_SOURCE_DIR}/ades_reader.h) # Remember that objects are derived from QObject and thus require the MOC tool to parse them. qt4_wrap_cpp(ADES_MOCS ${MOCS}) # cmake will call Qt MOC tool and wrap moc file into variable ADES_MOCS add_library(ADESReader SHARED ${SRCS} ${ADES_MOCS}) # Add sources to the make rule. target_link_libraries(ADESReader AwCoreLib AwReadWriteLib ${QT_LIBRARIES}) # Add libraries to linker
If the SDK is correctly installed an configured, just type cmake . in the source folder and our plug-in should build and install into the SDK folder.