AnyWave:BuildReader

From WikiMEG
Jump to: navigation, search

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:

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