Difference between revisions of "AnyWave:BuildReader"

From WikiMEG
Jump to: navigation, search
(Define the C++ classes)
Line 332: Line 332:
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]
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]

Revision as of 14:23, 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.

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:

#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.
	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.
// 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
	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)
// 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)
	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
	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.
	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());
					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);
		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:
		case AwChannel::SEEG:
		case AwChannel::MEG:
			inserted->setGain((float)1E-12);    // IMPORTANT: MEG channels are expected to be expressed in pT. 
	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.
	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"));
	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
					QString value = line.section('\t', 1, 1);
					if (value.isEmpty())
					QString position = line.section('\t', 2, 2);
					if (position.isEmpty())
					QString duration = line.section('\t', 3, 3);
					AwMarker m;  // create marker object.
					m.setLabel(label);   // set marker attribudes (label, value, position and duration).
					if (!duration.isEmpty())
					block->addMarker(m);  // add the marker to the current block of data.
         // 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];
        // done demultiplexing data for channels, so delete allocated buffer.
	delete[] buf;
	return read;  // returns the number of bytes read 
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