This commit is contained in:
2021-10-14 13:47:35 +02:00
commit 6625a8dfaa
4026 changed files with 844291 additions and 0 deletions
@@ -0,0 +1,440 @@
#include "ovpCAlgorithmBrainampFileReader.h"
#include <system/ovCMemory.h>
#include <sstream>
#include <cstdlib>
namespace OpenViBE {
namespace Plugins {
namespace FileIO {
//
bool CAlgorithmBrainampFileReader::initialize()
{
ip_filename.initialize(getInputParameter(OVP_Algorithm_BrainampFileReader_InputParameterId_Filename));
ip_epochDuration.initialize(getInputParameter(OVP_Algorithm_BrainampFileReader_InputParameterId_EpochDuration));
ip_seekTime.initialize(getInputParameter(OVP_Algorithm_BrainampFileReader_InputParameterId_SeekTime));
ip_convertStimuli.initialize(getInputParameter(OVP_Algorithm_BrainampFileReader_InputParameterId_ConvertStimuli));
op_startTime.initialize(getOutputParameter(OVP_Algorithm_BrainampFileReader_OutputParameterId_CurrentStartTime));
op_endTime.initialize(getOutputParameter(OVP_Algorithm_BrainampFileReader_OutputParameterId_CurrentEndTime));
op_sampling.initialize(getOutputParameter(OVP_Algorithm_BrainampFileReader_OutputParameterId_Sampling));
op_signalMatrix.initialize(getOutputParameter(OVP_Algorithm_BrainampFileReader_OutputParameterId_SignalMatrix));
op_stimulations.initialize(getOutputParameter(OVP_Algorithm_BrainampFileReader_OutputParameterId_Stimulations));
// Default value
ip_convertStimuli = true;
m_buffer = nullptr;
return true;
}
bool CAlgorithmBrainampFileReader::uninitialize()
{
if (m_dataFile.is_open()) { m_dataFile.close(); }
op_stimulations.uninitialize();
op_signalMatrix.uninitialize();
op_sampling.uninitialize();
op_endTime.uninitialize();
op_startTime.uninitialize();
ip_convertStimuli.uninitialize();
ip_seekTime.uninitialize();
ip_epochDuration.uninitialize();
ip_filename.uninitialize();
delete [] m_buffer;
m_buffer = nullptr;
return true;
}
bool CAlgorithmBrainampFileReader::process()
{
if (this->isInputTriggerActive(OVP_Algorithm_BrainampFileReader_InputTriggerId_Open))
{
std::string markerFilename;
std::string dataFilename;
m_binaryFormat = EBinaryFormat::Integer16;
m_nChannel = 0;
m_startSampleIdx = 0;
m_endSampleIdx = 0;
m_sampleCountPerBuffer = 0;
m_channelScales.clear();
delete [] m_buffer;
m_buffer = nullptr;
m_endianness = EEndianness::LittleEndian;
m_headerFile.open(ip_filename->toASCIIString(), std::ios::binary);
if (!m_headerFile.good())
{
getLogManager() << Kernel::LogLevel_Error << "Could not open file [" << *ip_filename << "]\n";
return false;
}
getLogManager() << Kernel::LogLevel_Trace << "Opening " << *ip_filename << " succeeded\n";
op_signalMatrix->setDimensionCount(2);
size_t channelIdx = 0;
EStatus status = EStatus::Nothing;
do
{
std::string what;
std::getline(m_headerFile, what, '\n');
getLogManager() << Kernel::LogLevel_Debug << what << "\n";
// optionally removes ending carriage return for windows / linux compatibility
if (what.length() != 0) { if (what[what.length() - 1] == '\r') { what.erase(what.length() - 1, 1); } }
if (what.length() != 0)
{
std::string::size_type equalPos;
if (what[0] == ';') // comments
{ }
else if (what.length() > 2 && what[0] == '[') // section start
{
std::string name;
name.assign(what, 1, what.length() - 2);
if (name == "Common Infos")
{
getLogManager() << Kernel::LogLevel_Trace << "Found section " << what << "\n";
status = EStatus::CommonInfos;
}
else if (name == "Binary Infos")
{
getLogManager() << Kernel::LogLevel_Trace << "Found section " << what << "\n";
status = EStatus::BinrayInfos;
}
else if (name == "Channel Infos")
{
getLogManager() << Kernel::LogLevel_Trace << "Found section " << what << "\n";
status = EStatus::ChannelInfos;
}
else if (name == "Comment")
{
getLogManager() << Kernel::LogLevel_Trace << "Found section " << what << "\n";
status = EStatus::Comment;
}
else
{
getLogManager() << Kernel::LogLevel_Warning << "{" << what <<
"} looked like a new section in the header file but is not know of this parser. Therefore anything after this line will be skipped until a new section is found\n";
status = EStatus::Nothing;
}
}
else if ((equalPos = what.find('=')) != std::string::npos && status != EStatus::Comment) // Option value
{
std::string name;
std::string value;
name.assign(what, 0, equalPos);
value.assign(what, equalPos + 1, what.length() - equalPos - 1);
getLogManager() << Kernel::LogLevel_Trace << "| Found option " << name << " with value " << CString(
value.c_str()) << "\n";
switch (status)
{
case EStatus::CommonInfos:
if (name == "DataFormat")
{
if (value != "BINARY") { getLogManager() << Kernel::LogLevel_ImportantWarning << "Only binary data is supported\n"; }
}
else if (name == "DataOrientation")
{
if (value != "MULTIPLEXED") { getLogManager() << Kernel::LogLevel_ImportantWarning << "Only multiplexed data is supported\n"; }
}
else if (name == "DataType")
{
if (value != "TIMEDOMAIN") { getLogManager() << Kernel::LogLevel_ImportantWarning << "Only time domain data is supported\n"; }
}
else if (name == "Codepage")
{
if (value != "ANSI")
{
getLogManager() << Kernel::LogLevel_Warning << "Header specifies code page as " << value
<< " but it will be forced back to " << CString("ANSI") << "\n";
}
}
else if (name == "DataFile") { dataFilename = value; }
else if (name == "MarkerFile") { markerFilename = value; }
else if (name == "NumberOfChannels")
{
m_nChannel = atoi(value.c_str());
m_channelScales.clear();
m_channelScales.resize(m_nChannel, 1);
op_signalMatrix->setDimensionSize(0, m_nChannel);
}
else if (name == "SamplingInterval")
{
double samplingInterval = atof(value.c_str());
op_sampling = uint64_t(0.5 + 1000000.0 / samplingInterval); // +0.5 for rounding
m_sampleCountPerBuffer = int64_t(ip_epochDuration * op_sampling); // $$$ Casted in (int64_t) because of Ubuntu 7.10 crash !
op_signalMatrix->setDimensionSize(1, uint32_t(m_sampleCountPerBuffer));
// TODO warn if approximated sampling rate
getLogManager() << Kernel::LogLevel_Trace << "| -> Calculated sampling frequency " << op_sampling << "Hz\n";
}
else { getLogManager() << Kernel::LogLevel_Warning << "Skipped option " << name << " with value " << value << "\n"; }
break;
case EStatus::BinrayInfos:
if (name == "BinaryFormat")
{
if (value == "INT_16") { m_binaryFormat = EBinaryFormat::Integer16; }
else if (value == "UINT_16") { m_binaryFormat = EBinaryFormat::UnsignedInteger16; }
else if (value == "FLOAT_32" || value == "IEEE_FLOAT_32") { m_binaryFormat = EBinaryFormat::Float32; }
else
{
m_binaryFormat = EBinaryFormat::Integer16;
getLogManager() << Kernel::LogLevel_ImportantWarning << "Unsupported binary format option value \""
<< value << "\"... Switched back to 16 bits integer (default)\n";
}
}
else if (name == "UseBigEndianOrder")
{
if (value == "YES") { m_endianness = EEndianness::BigEndian; }
else if (value == "NO") { m_endianness = EEndianness::LittleEndian; }
else
{
m_endianness = EEndianness::LittleEndian;
getLogManager() << Kernel::LogLevel_ImportantWarning << "Unsupported use big endian order option value \""
<< value << "\"... Switched back to little endian (default)\n";
}
}
else { getLogManager() << Kernel::LogLevel_Warning << "Skipped option " << name << " with value " << value << "\n"; }
break;
case EStatus::ChannelInfos:
{
std::stringstream ss(value);
std::string channelName, referenceChannelName, resolutionInUnit, unitName;
std::getline(ss, channelName, ',');
std::getline(ss, referenceChannelName, ',');
std::getline(ss, resolutionInUnit, ',');
std::getline(ss, unitName, ',');
op_signalMatrix->setDimensionLabel(0, channelIdx, channelName.c_str());
m_channelScales[channelIdx] = atof(resolutionInUnit.c_str());
channelIdx++;
}
break;
default: break;
}
}
}
} while (m_headerFile.good());
// Changing file location
std::string fullPath = ip_filename->toASCIIString();
std::string path;
std::string::size_type slashPos = fullPath.rfind('/');
if (slashPos != std::string::npos)
{
path.assign(fullPath, 0, slashPos + 1);
markerFilename = path + markerFilename;
dataFilename = path + dataFilename;
}
// Opens data file
m_dataFile.open(dataFilename.c_str(), std::ios::binary);
if (!m_dataFile.good()) { getLogManager() << Kernel::LogLevel_Error << "Could not open file [" << dataFilename << "]\n"; }
else { getLogManager() << Kernel::LogLevel_Trace << "Opening " << dataFilename << " succeeded\n"; }
// Opens marker file
m_markerFile.open(markerFilename.c_str(), std::ios::binary);
if (!m_markerFile.good()) { getLogManager() << Kernel::LogLevel_Error << "Could not open file [" << markerFilename << "]\n"; }
else
{
getLogManager() << Kernel::LogLevel_Trace << "Opening " << markerFilename << " succeeded\n";
status = EStatus::Nothing;
do
{
std::string what;
std::getline(m_markerFile, what, '\n');
getLogManager() << Kernel::LogLevel_Debug << what << "\n";
// optionally removes ending carriage return for windows / linux compatibility
if (what.length() != 0) { if (what[what.length() - 1] == '\r') { what.erase(what.length() - 1, 1); } }
if (what.length() != 0)
{
std::string::size_type equalPos;
if (what[0] == ';') // comments
{ }
else if (what.length() > 2 && what[0] == '[') // section start
{
std::string name;
name.assign(what, 1, what.length() - 2);
if (name == "Common Infos")
{
getLogManager() << Kernel::LogLevel_Trace << "Found section " << what << "\n";
status = EStatus::CommonInfos;
}
else if (name == "Marker Infos")
{
getLogManager() << Kernel::LogLevel_Trace << "Found section " << what << "\n";
status = EStatus::MarkerInfos;
}
else
{
getLogManager() << Kernel::LogLevel_Warning << "{" << what <<
"} looked like a new section in the marker file but is not know of this parser. Therefore anything after this line will be skipped until a new section is found\n";
status = EStatus::Nothing;
}
}
else if ((equalPos = what.find('=')) != std::string::npos && status != EStatus::Comment) // Option value
{
std::string name, value;
name.assign(what, 0, equalPos);
value.assign(what, equalPos + 1, what.length() - equalPos - 1);
getLogManager() << Kernel::LogLevel_Trace << "| Found option " << name << " with value " << value << "\n";
switch (status)
{
case EStatus::CommonInfos:
break;
case EStatus::MarkerInfos:
{
std::stringstream ss(value);
std::string type, desc, pos, duration, idx, date;
std::getline(ss, type, ',');
std::getline(ss, desc, ',');
std::getline(ss, pos, ',');
std::getline(ss, duration, ',');
std::getline(ss, idx, ',');
std::getline(ss, date, ',');
getLogManager() << Kernel::LogLevel_Trace << "| -> Found marker " << type << "," << desc << "," << pos << "," << duration << ","
<< idx << "," << date << "\n";
if (type == "Stimulus" && desc.length() > 0)
{
if (idx != "0")
{
getLogManager() << Kernel::LogLevel_Warning << "Marker [" << type << ":" << desc <<
"] is not marked on channel 0 and OpenViBE only supports global scope stimulations. Therefore this marker will be considered as global\n";
}
uint64_t id;
if (desc[0] == 'S') { id = atoi(desc.substr(1, std::string::npos).c_str()); }
else { id = atoi(desc.c_str()); }
if (ip_convertStimuli)
{
getLogManager() << Kernel::LogLevel_Trace << "Pre-conversion stimulation is " << desc <<
" at sample index [" << pos << "]\n";
id = OVTK_StimulationId_Label(id);
}
stimulation_t stim;
stim.id = id;
stim.startIdx = atoi(pos.c_str());
stim.duration = atoi(duration.c_str());
stim.name = desc;
m_stimulations.push_back(stim);
getLogManager() << Kernel::LogLevel_Trace << "Found stimulation " << stim.id << " at sample index [" << stim.startIdx << ":"
<< stim.duration << "]\n";
}
else
{
getLogManager() << Kernel::LogLevel_Warning << "Marker [" << type << ":" << desc <<
"] is not supported. Therefore it will be ignored\n";
}
}
break;
default: break;
}
}
}
} while (m_markerFile.good());
}
}
if (this->isInputTriggerActive(OVP_Algorithm_BrainampFileReader_InputTriggerId_Seek))
{
getLogManager() << Kernel::LogLevel_ImportantWarning << "This has not been implemented yet\n";
}
if (this->isInputTriggerActive(OVP_Algorithm_BrainampFileReader_InputTriggerId_Next))
{
double* buffer = op_signalMatrix->getBuffer();
#define DO_IT_WITH_TYPE(T) \
{ \
if (!m_buffer) { m_buffer = new uint8_t[op_signalMatrix->getBufferElementCount() * sizeof(T)]; } \
uint8_t* fileBuffer = m_buffer; \
T tValue; \
m_dataFile.read((char*)fileBuffer, op_signalMatrix->getBufferElementCount() * sizeof(T)); \
if (m_dataFile.eof()) { memset(fileBuffer, 0, op_signalMatrix->getBufferElementCount() * sizeof(T)); } \
bool (*fiileToHost)(const uint8_t*, T*); \
if (m_endianness == EEndianness::LittleEndian) { fiileToHost = System::Memory::littleEndianToHost; } \
else { fiileToHost = System::Memory::bigEndianToHost; } \
for (uint32_t j = 0; j < m_sampleCountPerBuffer; ++j) \
{ \
for (uint32_t i = 0; i < m_nChannel; i++, fileBuffer += sizeof(T)) \
{ \
(*fiileToHost)(fileBuffer, &tValue); \
buffer[i * m_sampleCountPerBuffer + j] = m_channelScales[i] * tValue; \
} \
} \
}
if (m_binaryFormat == EBinaryFormat::Integer16) { DO_IT_WITH_TYPE(int16_t); }
if (m_binaryFormat == EBinaryFormat::UnsignedInteger16) { DO_IT_WITH_TYPE(uint16_t); }
if (m_binaryFormat == EBinaryFormat::Float32) { DO_IT_WITH_TYPE(float); }
m_startSampleIdx = m_endSampleIdx;
m_endSampleIdx += m_sampleCountPerBuffer;
op_startTime = CTime(op_sampling, m_startSampleIdx).time();
op_endTime = CTime(op_sampling, m_endSampleIdx).time();
// find stimulations in this range
uint64_t nStim = 0;
for (const auto& s : m_stimulations) { if (m_startSampleIdx <= s.startIdx && s.startIdx < m_endSampleIdx) { nStim++; } }
op_stimulations->setStimulationCount(nStim);
nStim = 0;
for (const auto& s : m_stimulations)
{
if (m_startSampleIdx <= s.startIdx && s.startIdx < m_endSampleIdx)
{
uint64_t date = CTime(op_sampling, s.startIdx).time();
uint64_t duration = CTime(op_sampling, s.duration).time();
op_stimulations->setStimulationIdentifier(nStim, s.id);
op_stimulations->setStimulationDate(nStim, date);
op_stimulations->setStimulationDuration(nStim, duration);
nStim++;
}
}
this->activateOutputTrigger(OVP_Algorithm_BrainampFileReader_OutputTriggerId_DataProduced, true);
}
if (this->isInputTriggerActive(OVP_Algorithm_BrainampFileReader_InputTriggerId_Close))
{
delete [] m_buffer;
m_buffer = nullptr;
m_markerFile.close();
m_dataFile.close();
m_headerFile.close();
}
return true;
}
} // namespace FileIO
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,111 @@
#pragma once
#include "../../ovp_defines.h"
#include <openvibe/ov_all.h>
#include <toolkit/ovtk_all.h>
#include <fstream>
namespace OpenViBE {
namespace Plugins {
namespace FileIO {
class CAlgorithmBrainampFileReader final : public Toolkit::TAlgorithm<IAlgorithm>
{
public:
void release() override { delete this; }
bool initialize() override;
bool uninitialize() override;
bool process() override;
_IsDerivedFromClass_Final_(Toolkit::TAlgorithm<IAlgorithm>, OVP_ClassId_Algorithm_BrainampFileReader)
protected:
enum class EStatus { Nothing, CommonInfos, BinrayInfos, ChannelInfos, MarkerInfos, Comment };
enum class EBinaryFormat { Integer16, UnsignedInteger16, Float32 };
enum class EEndianness { LittleEndian, BigEndian };
typedef struct
{
uint64_t id;
uint64_t startIdx;
uint64_t duration;
std::string name;
} stimulation_t;
Kernel::TParameterHandler<CString*> ip_filename;
Kernel::TParameterHandler<double> ip_epochDuration;
Kernel::TParameterHandler<uint64_t> ip_seekTime;
Kernel::TParameterHandler<bool> ip_convertStimuli;
Kernel::TParameterHandler<uint64_t> op_startTime;
Kernel::TParameterHandler<uint64_t> op_endTime;
Kernel::TParameterHandler<uint64_t> op_sampling;
Kernel::TParameterHandler<CMatrix*> op_signalMatrix;
Kernel::TParameterHandler<IStimulationSet*> op_stimulations;
CString m_filename;
EBinaryFormat m_binaryFormat = EBinaryFormat::Integer16;
EEndianness m_endianness = EEndianness::LittleEndian;
uint32_t m_nChannel = 0;
uint64_t m_startSampleIdx = 0;
uint64_t m_endSampleIdx = 0;
uint64_t m_sampleCountPerBuffer = 0;
uint8_t* m_buffer = nullptr;
std::vector<double> m_channelScales;
std::vector<stimulation_t> m_stimulations;
std::ifstream m_headerFile;
std::ifstream m_dataFile;
std::ifstream m_markerFile;
};
class CAlgorithmBrainampFileReaderDesc final : public IAlgorithmDesc
{
public:
void release() override { }
CString getName() const override { return CString("Brainamp file reader"); }
CString getAuthorName() const override { return CString("Yann Renard"); }
CString getAuthorCompanyName() const override { return CString("INRIA/IRISA"); }
CString getShortDescription() const override { return CString("Reads input having the BrainAmp file format"); }
CString getDetailedDescription() const override { return CString(""); }
CString getCategory() const override { return CString("File reading and writing/Brainamp"); }
CString getVersion() const override { return CString("1.1"); }
CIdentifier getCreatedClass() const override { return OVP_ClassId_Algorithm_BrainampFileReader; }
IPluginObject* create() override { return new CAlgorithmBrainampFileReader; }
bool getAlgorithmPrototype(Kernel::IAlgorithmProto& prototype) const override
{
prototype.addInputParameter(OVP_Algorithm_BrainampFileReader_InputParameterId_Filename, "Filename", Kernel::ParameterType_String);
prototype.addInputParameter(OVP_Algorithm_BrainampFileReader_InputParameterId_EpochDuration, "Epoch duration", Kernel::ParameterType_Float);
prototype.addInputParameter(OVP_Algorithm_BrainampFileReader_InputParameterId_SeekTime, "Seek time", Kernel::ParameterType_Integer);
prototype.addInputParameter(OVP_Algorithm_BrainampFileReader_InputParameterId_ConvertStimuli, "Convert stimuli", Kernel::ParameterType_Boolean);
prototype.addOutputParameter(OVP_Algorithm_BrainampFileReader_OutputParameterId_CurrentStartTime, "Current start time", Kernel::ParameterType_Integer);
prototype.addOutputParameter(OVP_Algorithm_BrainampFileReader_OutputParameterId_CurrentEndTime, "Current end time", Kernel::ParameterType_Integer);
prototype.addOutputParameter(OVP_Algorithm_BrainampFileReader_OutputParameterId_Sampling, "Sampling rate", Kernel::ParameterType_Integer);
prototype.addOutputParameter(OVP_Algorithm_BrainampFileReader_OutputParameterId_SignalMatrix, "Signal samples", Kernel::ParameterType_Matrix);
prototype.addOutputParameter(OVP_Algorithm_BrainampFileReader_OutputParameterId_Stimulations, "Stimulations", Kernel::ParameterType_StimulationSet);
prototype.addInputTrigger(OVP_Algorithm_BrainampFileReader_InputTriggerId_Open, "Open");
prototype.addInputTrigger(OVP_Algorithm_BrainampFileReader_InputTriggerId_Seek, "Seek");
prototype.addInputTrigger(OVP_Algorithm_BrainampFileReader_InputTriggerId_Next, "Next");
prototype.addInputTrigger(OVP_Algorithm_BrainampFileReader_InputTriggerId_Close, "Close");
prototype.addOutputTrigger(OVP_Algorithm_BrainampFileReader_OutputTriggerId_Error, "Error");
prototype.addOutputTrigger(OVP_Algorithm_BrainampFileReader_OutputTriggerId_DataProduced, "Data produced");
return true;
}
_IsDerivedFromClass_Final_(IAlgorithmDesc, OVP_ClassId_Algorithm_BrainampFileReaderDesc)
};
} // namespace FileIO
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,256 @@
#include <iostream>
#include <fstream>
#include <sstream>
#include <cstdlib>
#include <boost/regex.hpp>
#include <boost/detail/endian.hpp>
#include "ovpCBCI2000ReaderHelper.h"
namespace BCI2000 {
void CBCI2000ReaderHelper::printInfo(std::ostream& os) const
{
if (m_good)
{
os << "version: " << m_bci2000Version << std::endl;
os << "header length: " << m_headerLength << std::endl;
os << "source channels: " << m_nSrcChannel << std::endl;
os << "state vector length: " << m_stateVectorLength << std::endl;
os << "data format: " << m_dataFormat << std::endl;
os << "samples: " << m_nSamples << std::endl;
os << "samples left: " << m_samplesLeft << std::endl;
os << "sample size: " << m_sampleSize << std::endl;
os << "rate: " << getRate() << std::endl;
}
else { os << "bad file (unreadable or bad header)" << std::endl; }
}
bool CBCI2000ReaderHelper::parseMeta(OpenViBE::CString& meta)
{
static const boost::regex RE_META("BCI2000V= ([0-9.]+) HeaderLen= ([0-9]+) SourceCh= ([0-9]+) StateVectorLen(gth)?= ([0-9]+) DataFormat= ([a-z0-9]+)\r?$",
boost::regex::perl | boost::regex::icase);
static const boost::regex RE_OLD_META("HeaderLen= +([0-9]+) SourceCh= +([0-9]+) StatevectorLen= +([0-9]+)\r$", boost::regex::perl | boost::regex::icase);
boost::smatch match;
if (regex_match(std::string(meta), match, RE_META))
{
m_bci2000Version = float(atof(match.str(1).c_str()));
m_headerLength = int(atoi(match.str(2).c_str()));
m_nSrcChannel = int(atoi(match.str(3).c_str()));
// 4 is dropped (could be used for len/length syntax check)
m_stateVectorLength = int(atoi(match.str(5).c_str()));
m_dataFormat = match.str(6).c_str();
}
else
{
if (regex_match(std::string(meta), match, RE_OLD_META))
{
m_bci2000Version = 1.0;
m_headerLength = int(atoi(match.str(1).c_str()));
m_nSrcChannel = int(atoi(match.str(2).c_str()));
m_stateVectorLength = int(atoi(match.str(3).c_str()));
m_dataFormat = "int16_t";
}
else
{
std::cerr << " *** bci2000helper error: cannot parse meta information header" << std::endl;
return false;
}
}
return true;
}
bool CBCI2000ReaderHelper::parseHeader(std::istream& is)
{
static const boost::regex REGEXP_SECTION("\\[ *(.*[^ ]) *\\].*");
static const boost::regex REGEXP_PARAMETER("([^ ]+ [^ ]+ [^ ]+)= (.*)$"); // (section) (type) (name)= (value)
static const boost::regex REGEXP_FIELD("([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+) ([^ ]+)$"); // Name Length Value ByteLocation BitLocation
std::string section = "NONAME";
std::string line;
boost::smatch match;
std::getline(is, line);
while (line.length() > 2)
{
// is it a section name ?
if (regex_match(line, match, REGEXP_SECTION)) { section = match[1]; }
else // not section, parse if interesting
{
if (section == "Parameter Definition")
{
if (regex_match(line, match, REGEXP_PARAMETER))
{
m_parameters[match[1].str().c_str()] = match[2].str().c_str();
//std::cout << "parameter added : \n\t\t" << match[1].str() << " \n\t\t" << match[2].str() << "\n";
}
else
{
// should never happen: malformed file
std::cerr << " *** bci2000helper error: cannot parse Parameters" << std::endl;
return false;
}
}
if (section == "State Vector Definition")
{
if (regex_match(line, match, REGEXP_FIELD))
{
const int length = atoi(match.str(2).c_str());
const int value = atoi(match.str(3).c_str());
const int bytePos = atoi(match.str(4).c_str());
const int bitPos = atoi(match.str(5).c_str());
m_bitfield.addField(bytePos, bitPos, length, match.str(1).c_str(), value);
}
else
{
// should never happen: malformed file
std::cerr << " *** bci2000helper error: cannot parse Parameters" << std::endl;
return false;
}
}
}
std::getline(is, line);
}
return true;
}
float CBCI2000ReaderHelper::getRate() const
{
// Warning: the SamplingRate field is not clearly defined
// in the BCI2000 doc; it could be an int, or a float;
// it should be in Section Source, but Source:Garbage:Stuff is possible
// in some case, there is a pending "Hz" after the numbers
// Conclusion: we should not fix this until it's clear...
OpenViBE::CString key = "Source int SamplingRate";
if (m_parameters.count(key) == 1)
{
std::istringstream is(m_parameters.find(key)->second.toASCIIString());
float rate;
is >> rate;
return rate;
}
key = "Source:Signal%20Properties:DataIOFilter int SamplingRate";
if (m_parameters.count(key) == 1)
{
std::istringstream is(m_parameters.find(key)->second.toASCIIString());
float rate;
is >> rate;
return rate;
}
return -1.0; // not found :-(
}
OpenViBE::CString CBCI2000ReaderHelper::getChannelName(const size_t index) const
{
// To be checked on the different version of format.
const OpenViBE::CString key = "Source:Signal%20Properties:DataIOFilter list ChannelNames";
if (m_parameters.count(key) == 1)
{
std::istringstream is(static_cast<const char*>(m_parameters.find(key)->second));
std::string token;
for (size_t i = 0; i <= index + 1; ++i) //+1 because the channel count is in the parameter on first position (parameter:list)
{
token.clear();
is >> token;
}
return token.c_str();
}
return ("Channel " + std::to_string(index + 1)).c_str();
}
CBCI2000ReaderHelper::CBCI2000ReaderHelper(const char* filename)
{
m_file.open(filename, std::ios::binary);
if (!m_file.good())
{
m_good = false;
return;
}
std::stringbuf buffer;
m_file.get(buffer);
OpenViBE::CString meta = buffer.str().c_str();
m_file.seekg(0, std::ios::end);
const int fileSize = int(m_file.tellg());
m_file.seekg(0, std::ios::beg);
m_good = parseMeta(meta);
if (!m_good) { return; }
std::map<OpenViBE::CString, int> sizesOfMap;
sizesOfMap["float"] = 4;
sizesOfMap["int"] = 4;
sizesOfMap["int16_t"] = 2;
m_nSamples = (fileSize - m_headerLength) / (sizesOfMap[m_dataFormat] * m_nSrcChannel + m_stateVectorLength);
m_sampleSize = sizesOfMap[m_dataFormat] * m_nSrcChannel + m_stateVectorLength;
m_samplesLeft = m_nSamples;
m_good = parseHeader(m_file);
}
std::vector<float> CBCI2000ReaderHelper::readSample()
{
std::vector<float> samples;
if (m_samplesLeft < 1)
{
return samples; // nothing to read, empty vector returned
}
char* data = new char[m_sampleSize];
m_file.read(data, m_sampleSize);
float* dataAsFloat = reinterpret_cast<float*>(data);
for (int i = 0; i < m_nSrcChannel; ++i) { samples.push_back(dataAsFloat[i]); }
delete[] data;
m_samplesLeft--;
return samples;
}
template <class TFrom, class TTo>
int CBCI2000ReaderHelper::readSamplesInternal(TTo* samples, uint32_t* states, int n)
{
if (n > m_samplesLeft) { n = m_samplesLeft; }
if (n < 1) { return 0; }
char* data = new char[m_sampleSize * n];
m_file.read(data, m_sampleSize * n);
for (int i = 0; i < n; ++i)
{
if (samples != nullptr)
{
for (int j = 0; j < m_nSrcChannel; ++j)
{
TFrom sample = *reinterpret_cast<TFrom*>(data + i * m_sampleSize + j * sizeof(TFrom));
samples[i * m_nSrcChannel + j] = sample;
// check endianess ?
}
}
if (states != nullptr)
{
unsigned char* state = reinterpret_cast<unsigned char*>(data) + i * m_sampleSize + m_nSrcChannel * sizeof(TFrom);
m_bitfield.getFields(state, states + i * m_bitfield.size());
}
//std::copy(l_pData+i*m_i32SampleSize,l_pData+i*m_i32SampleSize+m_nSrcChannel*sizeof(T),samples+i*m_nSrcChannel);
}
delete[] data;
m_samplesLeft -= n;
return n;
}
int CBCI2000ReaderHelper::readSamples(double* samples, uint32_t* states, const int n)
{
if (m_dataFormat == OpenViBE::CString("float")) { return readSamplesInternal<float, double>(samples, states, n); }
#if defined(BOOST_LITTLE_ENDIAN)
if (m_dataFormat == OpenViBE::CString("int")) { return readSamplesInternal<int, double>(samples, states, n); }
if (m_dataFormat == OpenViBE::CString("int16_t")) { return readSamplesInternal<int16_t, double>(samples, states, n); }
#else
std::cerr << "*** bci2000helper error: read_samples from int16_t or int is not implemented yet on bigendian machines" << std::endl;
exit(EXIT_FAILURE);
#endif
return -1; // should never happen... TODO: error checking
}
} // namespace BCI2000
@@ -0,0 +1,70 @@
#pragma once
#include <fstream>
#include <ostream>
#include <vector>
#include <map>
#include "ovpCBitfield.h"
namespace BCI2000 {
/**
* \class CBCI2000ReaderHelper
* \author Olivier Rochel (INRIA)
* \brief BCI2000 file format parser and utilities. Uses the m_oBitfield utility class.
**/
class CBCI2000ReaderHelper
{
protected:
std::ifstream m_file;
float m_bci2000Version = 0; // file version.
int m_headerLength = 0; // header size (inc. meta)
int m_nSrcChannel = 0; // number of channels
int m_stateVectorLength = 0; // size of state field
OpenViBE::CString m_dataFormat; // data format (float, int16_t...)
std::vector<OpenViBE::CString> m_channelNames;
int m_nSamples = 0;
int m_sampleSize = 0;
int m_samplesLeft = 0;
bool m_good = false; // m_bGood is true if file open, header looks m_bGood (may
// still be truncated or broken in a silly way)
std::map<OpenViBE::CString, OpenViBE::CString> m_parameters;
// state vector
CBitfield m_bitfield;
// helpers
bool parseMeta(OpenViBE::CString& meta);
bool parseHeader(std::istream& is);
private:
template <class TFrom, class TTo>
int readSamplesInternal(TTo* samples, uint32_t* states, int n);
public:
/**
* Constructor from a BCI2000 file.
* \param filename BCI2000 file name.
**/
explicit CBCI2000ReaderHelper(const char* filename);
~CBCI2000ReaderHelper() { if (m_file) { m_file.close(); } }
void printInfo(std::ostream& os) const;
float getRate() const;
OpenViBE::CString getChannelName(size_t index) const;
std::vector<float> readSample();
int readSamples(double* samples, uint32_t* states, int n);
// getters
int getNbSamples() const { return m_nSamples; }
int getSampleSize() const { return m_sampleSize; }
int getChannels() const { return m_nSrcChannel; }
int getSamplesLeft() const { return m_samplesLeft; }
bool isGood() const { return m_good; }
size_t getStateVectorSize() const { return m_bitfield.size(); }
const OpenViBE::CString& getStateName(const int i) const { return m_bitfield.getFieldName(i); }
};
} // namespace BCI2000
@@ -0,0 +1,41 @@
#include "ovpCBitfield.h"
namespace BCI2000 {
uint32_t* CBitfield::getFields(unsigned char* data)
{
const size_t nFields = m_fields.size();
uint32_t* fields = new uint32_t[nFields];
getFields(data, fields);
return fields;
}
void CBitfield::getFields(unsigned char* data, uint32_t* fields)
{
const size_t nFields = m_fields.size();
for (uint32_t i = 0; i < nFields; ++i)
{
const int space = (m_fields[i].m_Length - 1 + m_fields[i].m_BitPos) / 8 + 1;
unsigned char* p = data + m_fields[i].m_BytePos;
uint32_t field = (*p) >> m_fields[i].m_BitPos;
for (uint32_t j = 1; j < uint32_t(space); ++j)
{
p++;
field += uint32_t(*p) << (j * 8 - m_fields[i].m_BitPos);
}
field &= 0xFFFFFFFF >> (32 - m_fields[i].m_Length);
fields[i] = field;
}
}
bool CBitfield::addField(const int bytePosition, const int bitPosition, const int length, const OpenViBE::CString& name, const int value)
{
if (length > 32) { return false; } // doc says len is 32 bits max
if (bitPosition > 7) { return false; }
m_fields.push_back(CField(bytePosition, bitPosition, length, name, value));
return true;
}
} // namespace BCI2000
@@ -0,0 +1,43 @@
#pragma once
#include <openvibe/ov_all.h>
#include <vector>
namespace BCI2000 {
class CBitfield
{
protected:
class CField
{
public:
int m_BytePos = 0; // starting byte
int m_BitPos = 0; // starting bit within starting byte
int m_Length = 0; // field length (in bits)
OpenViBE::CString m_Name;
int m_InitialValue = 0; // initial value. Not updated when reading files
CField(const int bytePos, const int bitPos, const int length, const OpenViBE::CString& name, const int initialValue)
: m_BytePos(bytePos), m_BitPos(bitPos), m_Length(length), m_Name(name), m_InitialValue(initialValue) {}
};
std::vector<CField> m_fields;
public:
// extract values from compressed data; returned array of size this->size() to
// be deleted[] by the user.
uint32_t* getFields(unsigned char* data);
// extract values from compressed data
// extractedFields must be allocated and of size this->size()
void getFields(unsigned char* data, uint32_t* fields);
// add a new field. Returns false when invalid paramaters entered, true otherwise.
bool addField(const int bytePosition, const int bitPosition, const int length, const OpenViBE::CString& name, const int value);
// returns the number of m_fields in the structure
// (not to be confused with the actual memory footprint)
uint32_t size() const { return m_fields.size(); }
const OpenViBE::CString& getFieldName(const size_t index) const { return m_fields[index].m_Name; }
int getInitialValue(const size_t index) const { return m_fields[index].m_InitialValue; }
};
} // namespace BCI2000
@@ -0,0 +1,155 @@
#include "ovpCBoxAlgorithmBCI2000Reader.h"
#include <iostream>
#include <sstream>
namespace OpenViBE {
namespace Plugins {
namespace FileIO {
bool CBoxAlgorithmBCI2000Reader::initialize()
{
const CString filename = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 0);
m_helper = new BCI2000::CBCI2000ReaderHelper(filename);
if (!m_helper->isGood())
{
this->getLogManager() << Kernel::LogLevel_ImportantWarning << "Could not open file [" << filename << "]\n";
m_helper = nullptr;
return false;
}
std::stringstream ss;
m_helper->printInfo(ss);
this->getLogManager() << Kernel::LogLevel_Trace << "Metadata from [" << filename << "] :\n" << ss.str() << "\n";
m_headerSent = false;
m_nChannel = m_helper->getChannels();
m_nSamplePerBuffer = size_t(uint64_t(FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 1)));
m_samplesSent = 0;
if (m_nSamplePerBuffer == 0)
{
this->getLogManager() << Kernel::LogLevel_Error << "SampleCountPerBuffer is 0, this will not work\n";
return false;
}
m_buffer.resize(m_nChannel * m_nSamplePerBuffer);
m_states.resize(m_helper->getStateVectorSize() * m_nSamplePerBuffer);
m_rate = size_t(m_helper->getRate());
if (int(m_rate) == -1)
{
this->getLogManager() << Kernel::LogLevel_Error << "Sampling rate could not be extracted from the file.\n";
return false;
}
if (m_rate == 0)
{
this->getLogManager() << Kernel::LogLevel_Error << "Sampling rate of 0 is not supported.\n";
return false;
}
m_signalEncoder.initialize(*this, 0);
m_oSignalMatrix = m_signalEncoder.getInputMatrix();
m_oSignalMatrix->resize(m_nChannel, m_nSamplePerBuffer);
for (size_t i = 0; i < m_nChannel; ++i) { m_oSignalMatrix->setDimensionLabel(0, i, m_helper->getChannelName(i)); }
m_stateEncoder.initialize(*this, 1);
m_oStateMatrix = m_stateEncoder.getInputMatrix();
m_oStateMatrix->resize(m_helper->getStateVectorSize(), m_nSamplePerBuffer);
for (size_t i = 0; i < m_helper->getStateVectorSize(); ++i) { m_oStateMatrix->setDimensionLabel(0, i, m_helper->getStateName(i)); }
m_signalEncoder.getInputSamplingRate() = m_rate;
m_stateEncoder.getInputSamplingRate() = m_rate;
for (size_t i = 0; i < m_helper->getStateVectorSize(); ++i)
{
this->getLogManager() << Kernel::LogLevel_Trace << "BCI2000 state var " << i << " is : " << m_helper->getStateName(i) << "\n";
}
return true;
}
bool CBoxAlgorithmBCI2000Reader::uninitialize()
{
delete m_helper;
m_buffer.clear();
m_states.clear();
m_signalEncoder.uninitialize();
m_stateEncoder.uninitialize();
return true;
}
bool CBoxAlgorithmBCI2000Reader::processClock(Kernel::CMessageClock& /*msg*/)
{
if (m_helper->getSamplesLeft() > 0)
{
getBoxAlgorithmContext()->markAlgorithmAsReadyToProcess();
return true;
}
return false;
}
uint64_t CBoxAlgorithmBCI2000Reader::getClockFrequency()
{
// Intentional parameter swap to get the frequency
return CTime(m_nSamplePerBuffer, m_rate).time();
}
void CBoxAlgorithmBCI2000Reader::sendHeader()
{
m_signalEncoder.encodeHeader();
m_stateEncoder.encodeHeader();
m_headerSent = true;
getDynamicBoxContext().markOutputAsReadyToSend(0, 0, 0);
getDynamicBoxContext().markOutputAsReadyToSend(1, 0, 0);
}
bool CBoxAlgorithmBCI2000Reader::process()
{
if (!m_headerSent) { sendHeader(); }
//prepare data
const int nRead = m_helper->readSamples(m_buffer.data(), m_states.data(), m_nSamplePerBuffer);
if (nRead > 0)
{
// padding. TODO: is it necessary ? or even dangerous ?
for (uint32_t i = nRead; i < m_nSamplePerBuffer; ++i) { for (uint32_t j = 0; j < m_nChannel; ++j) { m_buffer[i * m_nChannel + j] = 0.0; } }
// transpose (yeah, I know... ugly)
for (uint32_t i = 0; i < m_nSamplePerBuffer; ++i)
{
for (uint32_t j = 0; j < m_nChannel; ++j) { m_oSignalMatrix->getBuffer()[j * m_nSamplePerBuffer + i] = m_buffer[i * m_nChannel + j]; }
}
m_signalEncoder.encodeBuffer();
const uint64_t start = CTime(m_rate, m_samplesSent).time();
const uint64_t end = CTime(m_rate, m_samplesSent + m_nSamplePerBuffer).time();
m_samplesSent += nRead;
if (m_helper->getSamplesLeft() == 0) { m_signalEncoder.encodeEnd(); }
getDynamicBoxContext().markOutputAsReadyToSend(0, start, end);
// padding. TODO: is it necessary ? or even dangerous ?
for (size_t i = nRead; i < m_nSamplePerBuffer; ++i)
{
for (size_t j = 0; j < m_helper->getStateVectorSize(); ++j) { m_states[i * m_helper->getStateVectorSize() + j] = 0; }
}
// transpose (yeah, I know... ugly)
for (size_t i = 0; i < m_nSamplePerBuffer; ++i)
{
for (size_t j = 0; j < m_helper->getStateVectorSize(); ++j)
{
m_oStateMatrix->getBuffer()[j * m_nSamplePerBuffer + i] = m_states[i * m_helper->getStateVectorSize() + j];
}
}
m_stateEncoder.encodeBuffer();
if (m_helper->getSamplesLeft() == 0) { m_signalEncoder.encodeEnd(); }
getDynamicBoxContext().markOutputAsReadyToSend(1, start, end);
}
else
{
this->getLogManager() << Kernel::LogLevel_Error << "An error occurred while trying to get new samples from file. The file may be corrupted.\n";
return false;
}
return true;
}
} // namespace FileIO
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,103 @@
#pragma once
#include "ovpCBCI2000ReaderHelper.h"
#include "../../ovp_defines.h"
#include <openvibe/ov_all.h>
#include <toolkit/ovtk_all.h>
#define OVP_ClassId_BoxAlgorithm_BCI2000Reader OpenViBE::CIdentifier(0xFF78DAF4, 0xC41544B8)
#define OVP_ClassId_BoxAlgorithm_BCI2000ReaderDesc OpenViBE::CIdentifier(0xFF53D107, 0xC31144B8)
namespace OpenViBE {
namespace Plugins {
namespace FileIO {
/**
* \class CBoxAlgorithmBCI2000Reader
* \author Olivier Rochel (INRIA)
* \date Tue Jun 21 11:11:04 2011
* \brief The class CBoxAlgorithmBCI2000Reader describes the box BCI2000 Reader.
*
*/
class CBoxAlgorithmBCI2000Reader final : virtual public Toolkit::TBoxAlgorithm<IBoxAlgorithm>
{
public:
void release() override { delete this; }
bool initialize() override;
bool uninitialize() override;
bool processClock(Kernel::CMessageClock& msg) override;
uint64_t getClockFrequency() override;
bool process() override;
_IsDerivedFromClass_Final_(Toolkit::TBoxAlgorithm<IBoxAlgorithm>, OVP_ClassId_BoxAlgorithm_BCI2000Reader)
protected:
bool m_headerSent = false;
Toolkit::TSignalEncoder<CBoxAlgorithmBCI2000Reader> m_signalEncoder;
Toolkit::TSignalEncoder<CBoxAlgorithmBCI2000Reader> m_stateEncoder;
// These 2 were from the time the matrices were built, not given by the encoders.
// They could be removed, but for now make the code a bit easier to read - that's
// why they're still there.
CMatrix* m_oSignalMatrix = nullptr;
CMatrix* m_oStateMatrix = nullptr;
size_t m_rate = 0;
size_t m_nChannel = 0;
size_t m_nSamplePerBuffer = 0;
std::vector<double> m_buffer; // temporary buffer as we'll have to transpose data for signal_out
std::vector<uint32_t> m_states; // state variables, to be converted too;
uint64_t m_samplesSent = 0;
BCI2000::CBCI2000ReaderHelper* m_helper = nullptr;
// helpers
void sendHeader();
};
/**
* \class CBoxAlgorithmBCI2000ReaderDesc
* \author Olivier Rochel (INRIA)
* \date Tue Jun 21 11:11:04 2011
* \brief Descriptor of the box BCI2000 Reader.
*
*/
class CBoxAlgorithmBCI2000ReaderDesc final : virtual public IBoxAlgorithmDesc
{
public:
void release() override { }
CString getName() const override { return CString("BCI2000 File Reader"); }
CString getAuthorName() const override { return CString("Olivier Rochel"); }
CString getAuthorCompanyName() const override { return CString("INRIA"); }
CString getShortDescription() const override { return CString("Reads BCI2000 .dat files."); }
CString getDetailedDescription() const override { return CString("The box reads EEG/States signals from a BCI2000 file (.dat)"); }
CString getCategory() const override { return CString("File reading and writing/BCI2000"); }
CString getVersion() const override { return CString("1.3"); }
CString getStockItemName() const override { return CString("gtk-open"); }
CIdentifier getCreatedClass() const override { return OVP_ClassId_BoxAlgorithm_BCI2000Reader; }
IPluginObject* create() override { return new CBoxAlgorithmBCI2000Reader; }
bool getBoxPrototype(Kernel::IBoxProto& prototype) const override
{
prototype.addOutput("Signal",OV_TypeId_Signal);
prototype.addOutput("State",OV_TypeId_Signal);
prototype.addSetting("File name",OV_TypeId_Filename, "");
prototype.addSetting("Samples per buffer",OV_TypeId_Integer, "16");
prototype.addFlag(OV_AttributeId_Box_FlagIsUnstable); // meuh non !
return true;
}
_IsDerivedFromClass_Final_(IBoxAlgorithmDesc, OVP_ClassId_BoxAlgorithm_BCI2000ReaderDesc)
};
} // namespace FileIO
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,163 @@
#include "ovpCBoxAlgorithmBrainampFileReader.h"
namespace OpenViBE {
namespace Plugins {
namespace FileIO {
uint64_t CBoxAlgorithmBrainampFileReader::getClockFrequency()
{
// Brainamp file reader parameters
Kernel::TParameterHandler<CString*> ip_filename(m_reader->getInputParameter(OVP_Algorithm_BrainampFileReader_InputParameterId_Filename));
Kernel::TParameterHandler<double> ip_epochDuration(m_reader->getInputParameter(OVP_Algorithm_BrainampFileReader_InputParameterId_EpochDuration));
Kernel::TParameterHandler<uint64_t> ip_seekTime(m_reader->getInputParameter(OVP_Algorithm_BrainampFileReader_InputParameterId_SeekTime));
Kernel::TParameterHandler<uint64_t> op_startTime(m_reader->getOutputParameter(OVP_Algorithm_BrainampFileReader_OutputParameterId_CurrentStartTime));
Kernel::TParameterHandler<uint64_t> op_endTime(m_reader->getOutputParameter(OVP_Algorithm_BrainampFileReader_OutputParameterId_CurrentEndTime));
Kernel::TParameterHandler<uint64_t> op_sampling(m_reader->getOutputParameter(OVP_Algorithm_BrainampFileReader_OutputParameterId_Sampling));
Kernel::TParameterHandler<CMatrix*> op_signalMatrix(m_reader->getOutputParameter(OVP_Algorithm_BrainampFileReader_OutputParameterId_SignalMatrix));
Kernel::TParameterHandler<IStimulationSet*> op_stimulations(m_reader->getOutputParameter(OVP_Algorithm_BrainampFileReader_OutputParameterId_Stimulations));
return uint64_t((1LL << 32) / ip_epochDuration);
}
bool CBoxAlgorithmBrainampFileReader::initialize()
{
// Creates algorithms
m_reader = &getAlgorithmManager().
getAlgorithm(getAlgorithmManager().createAlgorithm(OVP_ClassId_Algorithm_BrainampFileReader));
m_experimentInfoEncoder = &getAlgorithmManager().getAlgorithm(
getAlgorithmManager().createAlgorithm(OVP_GD_ClassId_Algorithm_ExperimentInfoEncoder));
m_signalEncoder = &getAlgorithmManager().getAlgorithm(getAlgorithmManager().createAlgorithm(OVP_GD_ClassId_Algorithm_SignalEncoder));
m_stimEncoder = &getAlgorithmManager().getAlgorithm(getAlgorithmManager().createAlgorithm(OVP_GD_ClassId_Algorithm_StimulationEncoder));
m_reader->initialize();
m_experimentInfoEncoder->initialize();
m_signalEncoder->initialize();
m_stimEncoder->initialize();
// Brainamp file reader parameters
Kernel::TParameterHandler<CString*> ip_filename(m_reader->getInputParameter(OVP_Algorithm_BrainampFileReader_InputParameterId_Filename));
Kernel::TParameterHandler<double> ip_epochDuration(m_reader->getInputParameter(OVP_Algorithm_BrainampFileReader_InputParameterId_EpochDuration));
Kernel::TParameterHandler<uint64_t> ip_seekTime(m_reader->getInputParameter(OVP_Algorithm_BrainampFileReader_InputParameterId_SeekTime));
Kernel::TParameterHandler<bool> ip_convertStimuli(m_reader->getInputParameter(OVP_Algorithm_BrainampFileReader_InputParameterId_ConvertStimuli));
Kernel::TParameterHandler<uint64_t> op_startTime(m_reader->getOutputParameter(OVP_Algorithm_BrainampFileReader_OutputParameterId_CurrentStartTime));
Kernel::TParameterHandler<uint64_t> op_endTime(m_reader->getOutputParameter(OVP_Algorithm_BrainampFileReader_OutputParameterId_CurrentEndTime));
Kernel::TParameterHandler<uint64_t> op_sampling(m_reader->getOutputParameter(OVP_Algorithm_BrainampFileReader_OutputParameterId_Sampling));
Kernel::TParameterHandler<CMatrix*> op_signalMatrix(m_reader->getOutputParameter(OVP_Algorithm_BrainampFileReader_OutputParameterId_SignalMatrix));
Kernel::TParameterHandler<IStimulationSet*> op_stimulations(m_reader->getOutputParameter(OVP_Algorithm_BrainampFileReader_OutputParameterId_Stimulations));
// Signal stream encoder parameters
Kernel::TParameterHandler<uint64_t> ip_sampling(m_signalEncoder->getInputParameter(OVP_GD_Algorithm_SignalEncoder_InputParameterId_Sampling));
Kernel::TParameterHandler<CMatrix*> ip_signalMatrix(m_signalEncoder->getInputParameter(OVP_GD_Algorithm_SignalEncoder_InputParameterId_Matrix));
// Stimulation stream encoder parameters
Kernel::TParameterHandler<IStimulationSet*> ip_stimulations(
m_stimEncoder->getInputParameter(OVP_GD_Algorithm_StimulationEncoder_InputParameterId_StimulationSet));
// Connect parameters together
ip_sampling.setReferenceTarget(op_sampling);
ip_signalMatrix.setReferenceTarget(op_signalMatrix);
ip_stimulations.setReferenceTarget(op_stimulations);
// Configures settings according to box
*ip_filename = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 0);
ip_epochDuration = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 1);
ip_convertStimuli = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 2);
m_headerSent = false;
return true;
}
bool CBoxAlgorithmBrainampFileReader::uninitialize()
{
m_reader->process(OVP_Algorithm_BrainampFileReader_InputTriggerId_Close);
m_reader->uninitialize();
m_stimEncoder->uninitialize();
m_signalEncoder->uninitialize();
m_experimentInfoEncoder->uninitialize();
getAlgorithmManager().releaseAlgorithm(*m_reader);
getAlgorithmManager().releaseAlgorithm(*m_stimEncoder);
getAlgorithmManager().releaseAlgorithm(*m_signalEncoder);
getAlgorithmManager().releaseAlgorithm(*m_experimentInfoEncoder);
return true;
}
bool CBoxAlgorithmBrainampFileReader::processClock(Kernel::CMessageClock& /*msg*/)
{
getBoxAlgorithmContext()->markAlgorithmAsReadyToProcess();
return true;
}
bool CBoxAlgorithmBrainampFileReader::process()
{
Kernel::IBoxIO& boxContext = this->getDynamicBoxContext();
// Brainamp file reader parameters
Kernel::TParameterHandler<CString*> ip_filename(m_reader->getInputParameter(OVP_Algorithm_BrainampFileReader_InputParameterId_Filename));
Kernel::TParameterHandler<double> ip_epochDuration(m_reader->getInputParameter(OVP_Algorithm_BrainampFileReader_InputParameterId_EpochDuration));
Kernel::TParameterHandler<uint64_t> ip_seekTime(m_reader->getInputParameter(OVP_Algorithm_BrainampFileReader_InputParameterId_SeekTime));
Kernel::TParameterHandler<uint64_t> op_startTime(m_reader->getOutputParameter(OVP_Algorithm_BrainampFileReader_OutputParameterId_CurrentStartTime));
Kernel::TParameterHandler<uint64_t> op_endTime(m_reader->getOutputParameter(OVP_Algorithm_BrainampFileReader_OutputParameterId_CurrentEndTime));
Kernel::TParameterHandler<uint64_t> op_sampling(m_reader->getOutputParameter(OVP_Algorithm_BrainampFileReader_OutputParameterId_Sampling));
Kernel::TParameterHandler<CMatrix*> op_signalMatrix(m_reader->getOutputParameter(OVP_Algorithm_BrainampFileReader_OutputParameterId_SignalMatrix));
Kernel::TParameterHandler<IStimulationSet*> op_stimulations(m_reader->getOutputParameter(OVP_Algorithm_BrainampFileReader_OutputParameterId_Stimulations));
// Signal stream encoder parameters
Kernel::TParameterHandler<IMemoryBuffer*>
op_signalBuffer(m_signalEncoder->getOutputParameter(OVP_GD_Algorithm_SignalEncoder_OutputParameterId_EncodedMemoryBuffer));
// Stimulation stream encoder parameters
Kernel::TParameterHandler<IMemoryBuffer*> op_stimulationBuffer(
m_stimEncoder->getOutputParameter(OVP_GD_Algorithm_StimulationEncoder_OutputParameterId_EncodedMemoryBuffer));
// Experiment information
Kernel::TParameterHandler<IMemoryBuffer*> op_experimentInfoBuffer(
m_experimentInfoEncoder->getOutputParameter(OVP_GD_Algorithm_ExperimentInfoEncoder_OutputParameterId_EncodedMemoryBuffer));
// Connects parameters to memory buffer
op_experimentInfoBuffer = boxContext.getOutputChunk(0);
op_signalBuffer = boxContext.getOutputChunk(1);
op_stimulationBuffer = boxContext.getOutputChunk(2);
if (!m_headerSent)
{
// Opens file
if (!m_reader->process(OVP_Algorithm_BrainampFileReader_InputTriggerId_Open)) { return false; }
// Produces header
m_experimentInfoEncoder->process(OVP_GD_Algorithm_ExperimentInfoEncoder_InputTriggerId_EncodeHeader);
m_stimEncoder->process(OVP_GD_Algorithm_StimulationEncoder_InputTriggerId_EncodeHeader);
m_signalEncoder->process(OVP_GD_Algorithm_SignalEncoder_InputTriggerId_EncodeHeader);
// Sends header
boxContext.markOutputAsReadyToSend(0, 0, 0);
boxContext.markOutputAsReadyToSend(1, 0, 0);
boxContext.markOutputAsReadyToSend(2, 0, 0);
// Turn flag off
m_headerSent = true;
}
if (!m_reader->process(OVP_Algorithm_BrainampFileReader_InputTriggerId_Next)) { return false; }
if (m_reader->isOutputTriggerActive(OVP_Algorithm_BrainampFileReader_OutputTriggerId_DataProduced))
{
// Produces buffer
m_experimentInfoEncoder->process(OVP_GD_Algorithm_ExperimentInfoEncoder_InputTriggerId_EncodeBuffer);
m_stimEncoder->process(OVP_GD_Algorithm_StimulationEncoder_InputTriggerId_EncodeBuffer);
m_signalEncoder->process(OVP_GD_Algorithm_SignalEncoder_InputTriggerId_EncodeBuffer);
// Sends buffer
boxContext.markOutputAsReadyToSend(0, op_startTime, op_endTime);
boxContext.markOutputAsReadyToSend(1, op_startTime, op_endTime);
boxContext.markOutputAsReadyToSend(2, op_startTime, op_endTime);
}
return true;
}
} // namespace FileIO
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,71 @@
#pragma once
#include "../../ovp_defines.h"
#include <openvibe/ov_all.h>
#include <toolkit/ovtk_all.h>
namespace OpenViBE {
namespace Plugins {
namespace FileIO {
class CBoxAlgorithmBrainampFileReader final : public Toolkit::TBoxAlgorithm<IBoxAlgorithm>
{
public:
void release() override { delete this; }
uint64_t getClockFrequency() override;
bool initialize() override;
bool uninitialize() override;
bool processClock(Kernel::CMessageClock& msg) override;
bool process() override;
_IsDerivedFromClass_Final_(Toolkit::TBoxAlgorithm<IBoxAlgorithm>, OVP_ClassId_BoxAlgorithm_BrainampFileReader)
protected:
Kernel::IAlgorithmProxy* m_reader = nullptr;
Kernel::IAlgorithmProxy* m_experimentInfoEncoder = nullptr;
Kernel::IAlgorithmProxy* m_signalEncoder = nullptr;
Kernel::IAlgorithmProxy* m_stimEncoder = nullptr;
bool m_headerSent = false;
};
class CBoxAlgorithmBrainampFileReaderDesc final : public IBoxAlgorithmDesc
{
public:
void release() override { }
CString getName() const override { return CString("BrainVision Format file reader"); }
CString getAuthorName() const override { return CString("Yann Renard"); }
CString getAuthorCompanyName() const override { return CString("INRIA/IRISA"); }
CString getShortDescription() const override { return CString("Reads input having the BrainAmp file format"); }
CString getDetailedDescription() const override { return CString(""); }
CString getCategory() const override { return CString("File reading and writing/BrainVision Format"); }
CString getVersion() const override { return CString("1.1"); }
CString getStockItemName() const override { return CString("gtk-open"); }
CIdentifier getCreatedClass() const override { return OVP_ClassId_BoxAlgorithm_BrainampFileReader; }
IPluginObject* create() override { return new CBoxAlgorithmBrainampFileReader; }
bool getBoxPrototype(Kernel::IBoxProto& prototype) const override
{
// Adds box outputs
prototype.addOutput("Experiment information", OV_TypeId_ExperimentInfo);
prototype.addOutput("EEG stream", OV_TypeId_Signal);
prototype.addOutput("Stimulations", OV_TypeId_Stimulations);
// Adds settings
prototype.addSetting("Filename (header)", OV_TypeId_Filename, "");
prototype.addSetting("Epoch size (in sec)", OV_TypeId_Float, "0.0625");
prototype.addSetting("Convert stimuli to OpenViBE labels", OV_TypeId_Boolean, "true");
return true;
}
_IsDerivedFromClass_Final_(IBoxAlgorithmDesc, OVP_ClassId_BoxAlgorithm_BrainampFileReaderDesc)
};
} // namespace FileIO
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,310 @@
#include "ovpCBoxAlgorithmBrainampFileWriter.h"
#include <fs/Files.h>
#include <cstring>
#include <iomanip>
#include <fstream>
#include <string>
#include <sstream>
#if defined TARGET_OS_Linux || defined TARGET_OS_MacOS
#include <strings.h>
#define _strcmpi strcasecmp
#endif
namespace OpenViBE {
namespace Plugins {
namespace FileIO {
bool CBoxAlgorithmBrainampFileWriter::initialize()
{
// Creates algorithms
m_signalDecoder = &getAlgorithmManager().getAlgorithm(getAlgorithmManager().createAlgorithm(OVP_GD_ClassId_Algorithm_SignalDecoder));
m_stimulationDecoder = &getAlgorithmManager().getAlgorithm(getAlgorithmManager().createAlgorithm(OVP_GD_ClassId_Algorithm_StimulationDecoder));
m_signalDecoder->initialize();
m_stimulationDecoder->initialize();
// Signal stream encoder parameters
op_sampling.initialize(m_signalDecoder->getOutputParameter(OVP_GD_Algorithm_SignalDecoder_OutputParameterId_Sampling));
// Connect parameters together
ip_signalBuffer.initialize(m_signalDecoder->getInputParameter(OVP_GD_Algorithm_StreamedMatrixDecoder_InputParameterId_MemoryBufferToDecode));
ip_stimulationsBuffer.initialize(m_stimulationDecoder->getInputParameter(OVP_GD_Algorithm_StimulationDecoder_InputParameterId_MemoryBufferToDecode));
op_matrix.initialize(m_signalDecoder->getOutputParameter(OVP_GD_Algorithm_StreamedMatrixDecoder_OutputParameterId_Matrix));
op_stimSet.initialize(m_stimulationDecoder->getOutputParameter(OVP_GD_Algorithm_StimulationDecoder_OutputParameterId_StimulationSet));
// Configures settings according to box
m_filePath = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 0);
m_dictionaryFilename = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 1);
m_transformStimulations = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 2);
m_shouldWriteFullFilenames = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 3);
// Markers inside the output file are numbered, we need to keep trace of how many of them we have already written
m_markersWritten = 0;
m_wasMarkerHeaderWritten = false;
// Create header, EEG and marker files
char parentPath[1024];
// Add the extension if it wasn't done
#if defined TARGET_OS_Linux || defined TARGET_OS_MacOS
if((m_filePath.length() < 5) || (strcasecmp(m_filePath.toASCIIString() + m_filePath.length() - 5, ".vhdr") != 0))
#else
if ((m_filePath.length() < 5) || (_strcmpi(m_filePath.toASCIIString() + m_filePath.length() - 5, ".vhdr") != 0))
#endif
{
m_filePath = m_filePath + ".vhdr";
}
if (!FS::Files::getParentPath(m_filePath.toASCIIString(), parentPath))
{
this->getLogManager() << Kernel::LogLevel_Error << "Cannot access " << m_filePath << "\n";
return false;
}
if (!FS::Files::directoryExists(parentPath) && !FS::Files::createParentPath(m_filePath))
{
this->getLogManager() << Kernel::LogLevel_Error << "Directory [" << parentPath << "] cannot be created\n";
return false;
}
// Create header file
char filenameWithoutExtension[1024];
FS::Files::getFilenameWithoutExtension(m_filePath.toASCIIString(), filenameWithoutExtension);
FS::Files::openOFStream(m_headerFStream, m_filePath.toASCIIString());
if (m_headerFStream.fail())
{
this->getLogManager() << Kernel::LogLevel_Error << "Cannot open " << m_filePath << " : " << strerror(errno) << "\n";
return false;
}
// Create EEG file
std::string eegFilePath = std::string(parentPath) + "/" + std::string(filenameWithoutExtension) + ".eeg";
FS::Files::openOFStream(m_eegFStream, eegFilePath.c_str(), std::ios::out | std::ios::binary);
if (m_eegFStream.fail())
{
this->getLogManager() << Kernel::LogLevel_Error << "Cannot open " << eegFilePath << " : " << strerror(errno) << "\n";
return false;
}
// Create Markers file
std::string markerFilePath = std::string(parentPath) + "/" + std::string(filenameWithoutExtension) + ".vmrk";
FS::Files::openOFStream(m_markerFStream, markerFilePath.c_str());
if (m_markerFStream.fail())
{
this->getLogManager() << Kernel::LogLevel_Error << "Cannot open " << markerFilePath << " : " << strerror(errno) << "\n";
return false;
}
// Opens Marker to OpenViBE Stimulation dictionary file
if (m_dictionaryFilename != CString(""))
{
std::ifstream ifs;
FS::Files::openIFStream(ifs, m_dictionaryFilename.toASCIIString());
if (!ifs.good()) { getLogManager() << Kernel::LogLevel_Error << "Could not open dictionary file [" << m_dictionaryFilename << "]\n"; }
else
{
getLogManager() << Kernel::LogLevel_Trace << "Opening " << m_dictionaryFilename << " succeeded\n";
// read the values from the dictionar, they are specified in format
// <Marker Type> , <Marker Name> , <OpenViBE Stimulation>
// Stimulations can be either written as decimal numbers, or as strings
do
{
std::string line;
std::getline(ifs, line, '\n');
// optionally removes ending carriage return for windows / linux compatibility
if (line.length() != 0) { if (line[line.length() - 1] == '\r') { line.erase(line.length() - 1, 1); } }
if (line.length() != 0)
{
if (line[0] == ';') // comments
{ }
else
{
std::string type, name, stimulationID;
std::stringstream ss(line);
std::getline(ss, type, ',');
std::getline(ss, name, ',');
std::getline(ss, stimulationID);
uint64_t id = this->getTypeManager().getEnumerationEntryValueFromName(OVTK_TypeId_Stimulation, stimulationID.c_str());
m_stimulationToMarkers[id] = type + "," + name;
}
}
} while (ifs.good());
}
}
return true;
}
bool CBoxAlgorithmBrainampFileWriter::uninitialize()
{
m_stimulationDecoder->uninitialize();
m_signalDecoder->uninitialize();
this->getAlgorithmManager().releaseAlgorithm(*m_stimulationDecoder);
this->getAlgorithmManager().releaseAlgorithm(*m_signalDecoder);
if (m_eegFStream.is_open()) { m_eegFStream.close(); }
if (m_markerFStream.is_open()) { m_markerFStream.close(); }
return true;
}
bool CBoxAlgorithmBrainampFileWriter::processInput(const size_t /*index*/)
{
this->getBoxAlgorithmContext()->markAlgorithmAsReadyToProcess();
return true;
}
bool CBoxAlgorithmBrainampFileWriter::process()
{
Kernel::IBoxIO& boxContext = this->getDynamicBoxContext();
// Signal
for (size_t i = 0; i < boxContext.getInputChunkCount(0); i++)
{
ip_signalBuffer = boxContext.getInputChunk(0, i);
m_signalDecoder->process();
if (m_signalDecoder->isOutputTriggerActive(OVP_GD_Algorithm_SignalDecoder_OutputTriggerId_ReceivedHeader))
{
// BrainAmp format uses sampling interval (between two samples) in microseconds
const size_t samplingInterval = 1000000 / size_t(op_sampling);
const size_t nChannels = op_matrix->getDimensionSize(0);
if (m_headerFStream.is_open())
{
std::string auxFilename = "$b";
if (m_shouldWriteFullFilenames)
{
char filenameWithoutExtension[1024];
FS::Files::getFilenameWithoutExtension(m_filePath.toASCIIString(), filenameWithoutExtension);
auxFilename = filenameWithoutExtension;
}
m_headerFStream << "Brain Vision Data Exchange Header File Version 1.0" << std::endl;
m_headerFStream << "[Common Infos]" << std::endl;
m_headerFStream << "DataFile=" << auxFilename << ".eeg" << std::endl;
m_headerFStream << "MarkerFile=" << auxFilename << ".vmrk" << std::endl;
m_headerFStream << "DataFormat=BINARY" << std::endl;
m_headerFStream << "DataOrientation=MULTIPLEXED" << std::endl;
m_headerFStream << "NumberOfChannels=" << nChannels << std::endl;
m_headerFStream << "SamplingInterval=" << samplingInterval << std::endl;
m_headerFStream << "[Binary Infos]" << std::endl;
m_headerFStream << "BinaryFormat=IEEE_FLOAT_32" << std::endl; // TODO_JL extend this to more formats
m_headerFStream << "[Channel Infos]" << std::endl;
for (size_t c = 0; c < nChannels; c++)
{
// OpenViBE format does not specify any units
m_headerFStream << "Ch" << (c + 1) << "=" << op_matrix->getDimensionLabel(0, c) << ",,1" << std::endl;
}
m_headerFStream.close();
}
else
{
this->getLogManager() << Kernel::LogLevel_Error << "Cannot open " << m_filePath << "\n";
return false;
}
}
if (m_signalDecoder->isOutputTriggerActive(OVP_GD_Algorithm_SignalDecoder_OutputTriggerId_ReceivedBuffer))
{
if (m_eegFStream.good())
{
for (uint32_t j = 0; j < op_matrix->getDimensionSize(1); ++j)
{
for (uint32_t c = 0; c < op_matrix->getDimensionSize(0); c++)
{
float value = float(op_matrix->getBuffer()[c * op_matrix->getDimensionSize(1) + j]);
m_eegFStream.write(reinterpret_cast<char*>(&value), sizeof(float));
}
}
}
}
boxContext.markInputAsDeprecated(0, i);
}
// Stimulations
for (uint32_t i = 0; i < boxContext.getInputChunkCount(1); i++)
{
ip_stimulationsBuffer = boxContext.getInputChunk(1, i);
m_stimulationDecoder->process();
// Handle Header
if (!m_wasMarkerHeaderWritten && m_stimulationDecoder->isOutputTriggerActive(
OVP_GD_Algorithm_StimulationDecoder_OutputTriggerId_ReceivedHeader))
{
std::string auxFilename = "$b";
if (m_shouldWriteFullFilenames)
{
char filenameWithoutExtension[1024];
FS::Files::getFilenameWithoutExtension(m_filePath.toASCIIString(), filenameWithoutExtension);
auxFilename = filenameWithoutExtension;
}
m_markerFStream << "Brain Vision Data Exchange Marker File, Version 1.0" << std::endl;
m_markerFStream << "[Common Infos]" << std::endl;
m_markerFStream << "DataFile=" << auxFilename << ".eeg" << std::endl;
m_markerFStream << "[Marker Infos]" << std::endl;
m_wasMarkerHeaderWritten = true;
}
if (m_stimulationDecoder->isOutputTriggerActive(OVP_GD_Algorithm_StimulationDecoder_OutputTriggerId_ReceivedBuffer))
{
for (uint32_t j = 0; j < op_stimSet->getStimulationCount(); j++)
{
const uint64_t id = op_stimSet->getStimulationIdentifier(j);
const uint64_t date = op_stimSet->getStimulationDate(j);
const uint64_t duration = op_stimSet->getStimulationDuration(j);
std::string writtenMarker;
bool writeStimulation = false;
if (m_stimulationToMarkers.count(id))
{
writtenMarker = m_stimulationToMarkers[id];
writeStimulation = true;
}
else if (m_transformStimulations)
{
std::string str = this->getTypeManager().getEnumerationEntryNameFromValue(OVTK_TypeId_Stimulation, id).toASCIIString();
writtenMarker = "Stimulus," + (str.empty() ? std::to_string(id) : str);
writeStimulation = true;
}
if (writeStimulation && m_markerFStream.good())
{
m_markersWritten++;
m_markerFStream << "Mk" << m_markersWritten << "=" << writtenMarker << ",";
// Calclulate the index of the sample to which the marker is attached
const uint64_t sampleIdx = CTime(date).toSampleCount(op_sampling);
m_markerFStream << sampleIdx << ",";
// Minimal duration of markers inside BrainAmp format seems to be 1 sample
const uint64_t durationInSamples = CTime(duration).toSampleCount(op_sampling) + 1;
m_markerFStream << durationInSamples << ",";
// 0 means that the stimulation is attached to all channels
m_markerFStream << "0" << std::endl;
}
}
}
boxContext.markInputAsDeprecated(1, i);
}
return true;
}
} // namespace FileIO
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,92 @@
#pragma once
#include "../../ovp_defines.h"
#include <openvibe/ov_all.h>
#include <toolkit/ovtk_all.h>
#include <fstream>
#include <map>
namespace OpenViBE {
namespace Plugins {
namespace FileIO {
class CBoxAlgorithmBrainampFileWriter final : public Toolkit::TBoxAlgorithm<IBoxAlgorithm>
{
public:
void release() override { delete this; }
bool initialize() override;
bool uninitialize() override;
bool processInput(const size_t index) override;
bool process() override;
_IsDerivedFromClass_Final_(Toolkit::TBoxAlgorithm<IBoxAlgorithm>, OVP_ClassId_BoxAlgorithm_BrainampFileWriter)
protected:
Kernel::IAlgorithmProxy* m_signalDecoder = nullptr;
Kernel::IAlgorithmProxy* m_stimulationDecoder = nullptr;
Kernel::TParameterHandler<const IMemoryBuffer*> ip_signalBuffer;
Kernel::TParameterHandler<const IMemoryBuffer*> ip_stimulationsBuffer;
Kernel::TParameterHandler<CMatrix*> op_matrix;
Kernel::TParameterHandler<IStimulationSet*> op_stimSet;
Kernel::TParameterHandler<uint64_t> op_sampling;
CString m_filePath;
CString m_dictionaryFilename;
bool m_transformStimulations = false;
bool m_shouldWriteFullFilenames = false;
private:
std::ofstream m_headerFStream;
std::ofstream m_eegFStream;
std::ofstream m_markerFStream;
std::map<uint64_t, std::string> m_stimulationToMarkers;
uint64_t m_markersWritten = 0;
bool m_wasMarkerHeaderWritten = false;
};
class CBoxAlgorithmBrainampFileWriterDesc final : public IBoxAlgorithmDesc
{
public:
void release() override { }
CString getName() const override { return CString("BrainVision Format File Writer"); }
CString getAuthorName() const override { return CString("Jozef Legeny"); }
CString getAuthorCompanyName() const override { return CString("Mensia Technologies"); }
CString getShortDescription() const override { return CString("Writes its input into a BrainVision format file"); }
CString getDetailedDescription() const override { return CString("This box allows to write the input signal under BrainVision file format."); }
CString getCategory() const override { return CString("File reading and writing/BrainVision Format"); }
CString getVersion() const override { return CString("1.1"); }
CString getStockItemName() const override { return CString("gtk-save"); }
CIdentifier getCreatedClass() const override { return OVP_ClassId_BoxAlgorithm_BrainampFileWriter; }
IPluginObject* create() override { return new CBoxAlgorithmBrainampFileWriter; }
bool getBoxPrototype(Kernel::IBoxProto& prototype) const override
{
// Adds box outputs
prototype.addInput("EEG stream", OV_TypeId_Signal);
prototype.addInput("Stimulations", OV_TypeId_Stimulations);
// Adds settings
prototype.addSetting("Filename (header in vhdr format)", OV_TypeId_Filename, "record-[$core{date}-$core{time}].vhdr");
prototype.addSetting("Marker to OV Stimulation dictionary", OV_TypeId_Filename, "");
prototype.addSetting("Convert OpenViBE Stimulations to markers", OV_TypeId_Boolean, "true");
prototype.addSetting("Use full data and marker file names in header", OV_TypeId_Boolean, "false");
return true;
}
_IsDerivedFromClass_Final_(IBoxAlgorithmDesc, OVP_ClassId_BoxAlgorithm_BrainampFileWriterDesc)
};
} // namespace FileIO
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,336 @@
#include "ovpCBCICompetitionIIIbReader.h"
#include <iostream>
#include <cmath>
namespace OpenViBE {
namespace Plugins {
namespace FileIO {
#define BCICompetitionIIIbReader_UndefinedClass 0xFFFFFFFFFFLL
bool CBCICompetitionIIIbReader::initialize()
{
m_signalEncoder.initialize(*this, 0);
m_stimEncoder.initialize(*this, 1);
// Parses box settings to find filename
const CString file = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 0);
//opens the file
if (file) { m_file.open(file); }
if (!m_file.good())
{
this->getLogManager() << Kernel::LogLevel_ImportantWarning << "Could not open file [" << file << "]\n";
return false;
}
m_file.seekg(0, std::ios::end);
m_fileSize = size_t(m_file.tellg());
m_file.seekg(0, std::ios::beg);
m_trialLength = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 10);
m_cueDisplayStart = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 11);
m_feedbackStart = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 12);
readTriggers();
readLabels();
readArtifacts();
readTrueLabels();
// Gets the size of output buffers
m_samplesPerBuffer = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 5);
//Offline/Online
const bool offline = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 6);
if (!offline)
{
//computes clock frequency
if (m_samplesPerBuffer <= m_sampling)
{
if (m_sampling % m_samplesPerBuffer != 0)
{
getBoxAlgorithmContext()->getPlayerContext()->getLogManager() << Kernel::LogLevel_Warning <<
"The sampling rate isn't a multiple of the buffer size\n" <<
"Please consider adjusting the BCI Competition IIIb reader settings to correct this!\n";
}
if (m_samplesPerBuffer == 0)
{
getBoxAlgorithmContext()->getPlayerContext()->getLogManager() << Kernel::LogLevel_Error <<
"SamplesPerBuffer is 0, this will not work\n";
return false;
}
// Intentional parameter swap to get the frequency
m_clockFrequency = CTime(m_samplesPerBuffer, m_sampling).time();
}
}
//Test/Training
m_keepTrainingSamples = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 7);
m_keepTestSamples = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 8);
m_keepArtifactSamples = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 9);
writeSignalInformation();
m_signalEncoder.encodeHeader();
m_stimEncoder.encodeHeader();
getBoxAlgorithmContext()->getDynamicBoxContext()->markOutputAsReadyToSend(0, 0, 0);
getBoxAlgorithmContext()->getDynamicBoxContext()->markOutputAsReadyToSend(1, 0, 0);
m_buffer = m_signalEncoder.getInputMatrix();
return true;
}
bool CBCICompetitionIIIbReader::uninitialize()
{
m_stimEncoder.uninitialize();
m_signalEncoder.uninitialize();
if (m_file) { m_file.close(); }
return true;
}
bool CBCICompetitionIIIbReader::processClock(Kernel::CMessageClock& /*msg*/)
{
if (!m_endOfFile) { getBoxAlgorithmContext()->markAlgorithmAsReadyToProcess(); }
return true;
}
bool CBCICompetitionIIIbReader::process()
{
if (m_errorOccurred) { return false; }
Kernel::IBoxIO* boxIO = getBoxAlgorithmContext()->getDynamicBoxContext();
//reading signal
//reset vector
double* buffer = m_buffer->getBuffer();
for (uint32_t i = 0; i < m_buffer->getBufferElementCount(); ++i) { buffer[i] = 0; }
std::istringstream ss;
std::string line;
double sample;
uint32_t count = 0;
for (; count < m_samplesPerBuffer && !m_endOfFile; ++count)
{
if (!getline(m_file, line)) { m_endOfFile = true; }
ss.clear();
ss.str(line);
ss >> sample;
if (std::isnan(sample)) { buffer[count] = (count != 0) ? buffer[count - 1] : 0.0; }
else { buffer[count] = sample; }
ss >> sample;
if (std::isnan(sample)) { buffer[count + m_samplesPerBuffer] = (count != 0) ? buffer[count + m_samplesPerBuffer - 1] : 0.0; }
else { buffer[count + m_samplesPerBuffer] = sample; }
}
m_nSentSample += count;
//A signal matrix is ready to be output
m_signalEncoder.encodeBuffer();
const uint64_t start = CTime(m_sampling, uint64_t(m_nSentSample - count)).time();
const uint64_t end = CTime(m_sampling, uint64_t(m_nSentSample)).time();
boxIO->markOutputAsReadyToSend(0, start, end);
//////
//Stimulations
std::vector<std::pair<uint64_t, uint64_t>> events;
bool changed = true;
while (changed)
{
changed = false;
const bool keepCurrentTrial =
((m_artifacts[m_currentTrial] && m_keepArtifactSamples) || !m_artifacts[m_currentTrial]) &&
((m_classLabels[m_currentTrial] == BCICompetitionIIIbReader_UndefinedClass && m_keepTestSamples) ||
(m_classLabels[m_currentTrial] != BCICompetitionIIIbReader_UndefinedClass && m_keepTrainingSamples));
if (m_triggerTimes[m_currentTrial] > (m_nSentSample - count) &&
m_triggerTimes[m_currentTrial] <= m_nSentSample
)
{
if (keepCurrentTrial)
{
//start of trial
events.push_back(std::pair<uint64_t, uint64_t>(0x300, m_triggerTimes[m_currentTrial]));
//display cross
events.push_back(std::pair<uint64_t, uint64_t>(0x312, m_triggerTimes[m_currentTrial]));
}
}
//send CUE stimulation
if (m_cueDisplayStarts[m_currentTrial] > (m_nSentSample - count) &&
m_cueDisplayStarts[m_currentTrial] <= m_nSentSample
)
{
if (keepCurrentTrial)
{
if (m_classLabels[m_currentTrial] != BCICompetitionIIIbReader_UndefinedClass)
{
//send class label
events.push_back(std::pair<uint64_t, uint64_t>(0x300 + m_classLabels[m_currentTrial], m_cueDisplayStarts[m_currentTrial]));
}
else
{
//send true label
events.push_back(std::pair<uint64_t, uint64_t>(0x300 + m_trueLabels[m_currentTrial], m_cueDisplayStarts[m_currentTrial]));
}
}
}
//send feedback start stimulation
if (m_feedbackStarts[m_currentTrial] > (m_nSentSample - count) && m_feedbackStarts[m_currentTrial] <= m_nSentSample)
{
if (keepCurrentTrial) { events.push_back(std::pair<uint64_t, uint64_t>(0x30D, m_feedbackStarts[m_currentTrial])); }
}
//send end of trial stimulation
if (m_endOfTrials[m_currentTrial] > (m_nSentSample - count) && m_endOfTrials[m_currentTrial] <= m_nSentSample
)
{
if (keepCurrentTrial) { events.push_back(std::pair<uint64_t, uint64_t>(0x320, m_endOfTrials[m_currentTrial])); }
m_currentTrial++;
changed = true;
}
}
if (!events.empty() || m_endOfFile)
{
IStimulationSet* stimSet = m_stimEncoder.getInputStimulationSet();
stimSet->setStimulationCount(events.size() + ((m_endOfFile) ? 1 : 0));
for (size_t j = 0; j < events.size(); ++j)
{
//compute date
const uint64_t date = CTime(m_sampling, events[j].second).time();
stimSet->insertStimulation(j, events[j].first, date, 0);
}
//add the ending stim
if (m_endOfFile)
{
//compute date
const uint64_t date = CTime(m_sampling, m_nSentSample).time();
stimSet->insertStimulation(events.size(), 0x3FF, date, 0);
}
m_stimEncoder.encodeBuffer();
boxIO->markOutputAsReadyToSend(1, start, end);
}
return true;
}
void CBCICompetitionIIIbReader::writeSignalInformation()
{
m_signalEncoder.getInputSamplingRate() = 125;
m_signalEncoder.getInputMatrix()->resize(2, m_samplesPerBuffer);
m_signalEncoder.getInputMatrix()->setDimensionLabel(0, 0, "+C3a-C3p");
m_signalEncoder.getInputMatrix()->setDimensionLabel(0, 1, "+C4a-C4p");
}
void CBCICompetitionIIIbReader::readTriggers()
{
const CString str = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 1);
std::ifstream file;
if (str) { file.open(str); }
std::string line;
std::istringstream ss;
uint64_t value;
while (getline(file, line))
{
ss.clear();
ss.str(line);
ss >> value;
m_triggerTimes.push_back(value);
m_cueDisplayStarts.push_back(value + uint64_t(floor(m_sampling * m_cueDisplayStart)));
m_feedbackStarts.push_back(value + uint64_t(floor(m_sampling * m_feedbackStart)));
m_endOfTrials.push_back(value + uint64_t(floor(m_sampling * m_trialLength)));
}
file.close();
}
void CBCICompetitionIIIbReader::readLabels()
{
const CString str = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 2);
std::ifstream file;
if (str) { file.open(str); }
std::string line;
std::istringstream ss;
uint64_t value;
while (getline(file, line))
{
if (line.compare(0, 3, "NaN", 0, 3) == 0) { m_classLabels.push_back(BCICompetitionIIIbReader_UndefinedClass); }
else
{
ss.clear();
ss.str(line);
ss >> value;
m_classLabels.push_back(value);
}
}
file.close();
}
void CBCICompetitionIIIbReader::readArtifacts()
{
const CString str = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 3);
std::ifstream file;
if (str) { file.open(str); }
std::string line;
std::istringstream ss;
uint64_t value;
while (getline(file, line))
{
ss.clear();
ss.str(line);
ss >> value;
m_artifacts.push_back(value == 1);
}
file.close();
}
void CBCICompetitionIIIbReader::readTrueLabels()
{
const CString str = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 4);
std::ifstream file;
if (str) { file.open(str); }
std::string line;
std::istringstream ss;
uint64_t value;
while (getline(file, line))
{
ss.clear();
ss.str(line);
ss >> value;
m_trueLabels.push_back(value);
}
file.close();
}
} // namespace FileIO
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,127 @@
#pragma once
#include "../ovp_defines.h"
#include <openvibe/ov_all.h>
#include <toolkit/ovtk_all.h>
#include <vector>
#include <fstream>
namespace OpenViBE {
namespace Plugins {
namespace FileIO {
class CBCICompetitionIIIbReader final : public Toolkit::TBoxAlgorithm<IBoxAlgorithm>
{
public:
CBCICompetitionIIIbReader() { } //TODO
void release() override { delete this; }
bool initialize() override;
bool uninitialize() override;
bool process() override;
bool processClock(Kernel::CMessageClock& msg) override;
uint64_t getClockFrequency() override { return m_clockFrequency; }
_IsDerivedFromClass_Final_(IBoxAlgorithm, OVP_ClassId_BCICompetitionIIIbReader)
protected:
void writeSignalInformation();
void readTriggers();
void readLabels();
void readArtifacts();
void readTrueLabels();
bool m_errorOccurred = false; //true if an error has occurred while reading the GDF file
//The filename and handle
std::ifstream m_file;
size_t m_fileSize = 0;
Toolkit::TSignalEncoder<CBCICompetitionIIIbReader> m_signalEncoder;
Toolkit::TStimulationEncoder<CBCICompetitionIIIbReader> m_stimEncoder;
uint64_t m_clockFrequency = 100LL << 32;
std::vector<uint64_t> m_triggerTimes;
std::vector<uint64_t> m_endOfTrials;
std::vector<uint64_t> m_cueDisplayStarts;
std::vector<uint64_t> m_feedbackStarts;
std::vector<size_t> m_classLabels;
std::vector<bool> m_artifacts;
std::vector<size_t> m_trueLabels;
size_t m_samplesPerBuffer = 0;
size_t m_sampling = 125;
size_t m_nSentSample = 0;
CMatrix* m_buffer = nullptr;
bool m_endOfFile = false;
size_t m_currentTrial = 0;
bool m_keepTrainingSamples = false;
bool m_keepTestSamples = false;
bool m_keepArtifactSamples = false;
double m_trialLength = 0;
double m_cueDisplayStart = 0;
double m_feedbackStart = 0;
};
class CBCICompetitionIIIbReaderDesc final : public IBoxAlgorithmDesc
{
public:
CString getName() const override { return CString("BCI competition IIIb reader"); }
CString getAuthorName() const override { return CString("Bruno Renier"); }
CString getAuthorCompanyName() const override { return CString("INRIA/IRISA"); }
CString getShortDescription() const override { return CString("Reads ASCII version of BCI competition IIIb datasets."); }
CString getDetailedDescription() const override
{
return CString("Reads signal samples, stimulations and class labels from the BCI competition IIIb ASCII datasets.");
}
CString getCategory() const override { return CString("File reading and writing/BCI Competition"); }
CString getVersion() const override { return CString("0.7"); }
CString getStockItemName() const override { return CString("gtk-open"); }
void release() override { }
CIdentifier getCreatedClass() const override { return OVP_ClassId_BCICompetitionIIIbReader; }
IPluginObject* create() override { return new CBCICompetitionIIIbReader(); }
bool getBoxPrototype(Kernel::IBoxProto& prototype) const override
{
// Adds box outputs
prototype.addOutput("Signal", OV_TypeId_Signal);
prototype.addOutput("Stimulations", OV_TypeId_Stimulations);
// Adds settings
prototype.addSetting("Signal file", OV_TypeId_Filename, "");
prototype.addSetting("Triggers file", OV_TypeId_Filename, "");
prototype.addSetting("Labels file", OV_TypeId_Filename, "");
prototype.addSetting("Artifact file", OV_TypeId_Filename, "");
prototype.addSetting("True labels file", OV_TypeId_Filename, "");
prototype.addSetting("Samples per buffer", OV_TypeId_Integer, "32");
prototype.addSetting("Offline", OV_TypeId_Boolean, "false");
prototype.addSetting("Train?", OV_TypeId_Boolean, "true");
prototype.addSetting("Test?", OV_TypeId_Boolean, "false");
prototype.addSetting("Keep artifacts?", OV_TypeId_Boolean, "false");
prototype.addSetting("Trial length", OV_TypeId_Float, "8.0");
prototype.addSetting("CUE display Start", OV_TypeId_Float, "3.0");
prototype.addSetting("Feedback start", OV_TypeId_Float, "4.0");
prototype.addFlag(OV_AttributeId_Box_FlagIsUnstable);
return true;
}
_IsDerivedFromClass_Final_(IBoxAlgorithmDesc, OVP_ClassId_BCICompetitionIIIbReaderDesc)
};
} // namespace FileIO
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,525 @@
#include "ovpCBoxAlgorithmSignalConcatenation.h"
namespace OpenViBE {
namespace Plugins {
namespace FileIO {
bool CBoxAlgorithmSignalConcatenation::initialize()
{
m_signalChunkBuffers.resize(this->getStaticBoxContext().getInputCount() >> 1);
m_stimulationChunkBuffers.resize(this->getStaticBoxContext().getInputCount() >> 1);
m_timeOut = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 0);
m_timeOut = m_timeOut << 32;
this->getLogManager() << Kernel::LogLevel_Info << "Timeout set to " << CTime(m_timeOut) << ".\n";
for (uint32_t i = 0; i < this->getStaticBoxContext().getInputCount(); i += 2)
{
m_eofStimulations.push_back(FSettingValueAutoCast(*this->getBoxAlgorithmContext(), (i >> 2) + 1));
m_eofReached.push_back(false);
m_fileEndTimes.push_back(0);
}
for (uint32_t i = 0; i < this->getStaticBoxContext().getInputCount(); i += 2)
{
auto* signalDecoder = new Toolkit::TSignalDecoder<CBoxAlgorithmSignalConcatenation>(*this, i);
auto* stimDecoder = new Toolkit::TStimulationDecoder<CBoxAlgorithmSignalConcatenation>(*this, i + 1);
m_signalDecoders.push_back(signalDecoder);
m_stimulationDecoders.push_back(stimDecoder);
auto* stimSet = new CStimulationSet();
m_stimulationSets.push_back(stimSet);
}
m_stimulationEncoder.initialize(*this, 1);
m_stimulationEncoder.getInputStimulationSet().setReferenceTarget(m_stimulationDecoders[0]->getOutputStimulationSet());
m_signalEncoder.initialize(*this, 0);
m_triggerEncoder.initialize(*this, 2);
m_triggerEncoder.getInputStimulationSet().setReferenceTarget(m_stimulationDecoders[0]->getOutputStimulationSet());
m_headerReceivedCount = 0;
m_endReceivedCount = 0;
m_headerSent = false;
m_endSent = false;
m_stimHeaderSent = false;
m_finished = false;
m_resynchroDone = false;
m_statsPrinted = false;
m_state.m_CurrentFileIdx = 0;
m_state.m_CurrentChunkIdx = 0;
m_state.m_CurrentStimulationIdx = 0;
m_triggerDate = 0;
m_lastChunkStartTime = 0;
m_lastChunkEndTime = 0;
return true;
}
/*******************************************************************************/
bool CBoxAlgorithmSignalConcatenation::uninitialize()
{
m_stimulationEncoder.uninitialize();
m_signalEncoder.uninitialize();
m_triggerEncoder.uninitialize();
for (uint32_t i = 0; i < m_signalDecoders.size(); ++i)
{
m_signalDecoders[i]->uninitialize();
m_stimulationDecoders[i]->uninitialize();
delete m_signalDecoders[i];
delete m_stimulationDecoders[i];
}
for (auto& signalChunkBuffer : m_signalChunkBuffers) { for (auto& signalChunk : signalChunkBuffer) { delete signalChunk.m_Buffer; } }
for (auto& stimulationChunkBuffer : m_stimulationChunkBuffers)
{
for (auto& stimulationChunk : stimulationChunkBuffer) { delete stimulationChunk.m_StimulationSet; }
}
for (auto& stimulationSet : m_stimulationSets) { delete stimulationSet; }
return true;
}
/*******************************************************************************/
bool CBoxAlgorithmSignalConcatenation::processInput(const size_t /*index*/)
{
getBoxAlgorithmContext()->markAlgorithmAsReadyToProcess();
return true;
}
/*******************************************************************************/
bool CBoxAlgorithmSignalConcatenation::processClock(Kernel::CMessageClock& /*msg*/)
{
if (!m_headerSent || m_finished) { return true; }
const uint64_t currentTime = this->getPlayerContext().getCurrentTime();
for (uint32_t i = 0; i < m_fileEndTimes.size(); ++i)
{
if (!m_eofReached[i] && currentTime > m_fileEndTimes[i] + m_timeOut)
{
m_eofReached[i] = true;
this->getLogManager() << Kernel::LogLevel_Info << "File #" << i + 1 << "/" << (this->getStaticBoxContext().getInputCount() / 2) <<
" has timed out (effective end time: " << CTime(m_fileEndTimes[i]) << ").\n";
}
}
getBoxAlgorithmContext()->markAlgorithmAsReadyToProcess();
return true;
}
/*******************************************************************************/
bool CBoxAlgorithmSignalConcatenation::process()
{
const Kernel::IBox& staticBoxContext = this->getStaticBoxContext();
Kernel::IBoxIO& boxCtx = this->getDynamicBoxContext();
//SIGNAL INPUTS
for (uint32_t i = 0; i < staticBoxContext.getInputCount(); i += 2)
{
const uint32_t idx = i >> 1;
for (uint32_t j = 0; j < boxCtx.getInputChunkCount(i); ++j)
{
m_signalDecoders[idx]->decode(j, true);
if (m_signalDecoders[idx]->isHeaderReceived())
{
// Not received all headers we expect? Decode and test ...
if (m_headerReceivedCount < staticBoxContext.getInputCount() / 2)
{
const uint64_t samplingFrequency = m_signalDecoders[idx]->getOutputSamplingRate();
const uint32_t nChannel = m_signalDecoders[idx]->getOutputMatrix()->getDimensionSize(0);
const uint32_t sampleCountPerBuffer = m_signalDecoders[idx]->getOutputMatrix()->getDimensionSize(1);
// Note that the stream may be decoded in any order, hence e.g. stream 2 header may be received before stream 1 ...
if (m_headerReceivedCount == 0)
{
this->getLogManager() << Kernel::LogLevel_Info << "Common sampling rate is " << samplingFrequency << ", channel count is " << nChannel
<< " and sample count per buffer is " << sampleCountPerBuffer << ".\n";
// Set the encoder to follow the parameters of this first received input
m_signalEncoder.getInputSamplingRate().setReferenceTarget(m_signalDecoders[idx]->getOutputSamplingRate());
m_signalEncoder.getInputMatrix().setReferenceTarget(m_signalDecoders[idx]->getOutputMatrix());
m_signalEncoder.encodeHeader();
boxCtx.markOutputAsReadyToSend(0, boxCtx.getInputChunkStartTime(i, j), boxCtx.getInputChunkEndTime(i, j));
m_headerSent = true;
}
else
{
if (m_signalEncoder.getInputSamplingRate() != samplingFrequency)
{
this->getLogManager() << Kernel::LogLevel_Error << "File #"
<< idx + 1 << "/" << (staticBoxContext.getInputCount() / 2)
<< " has a different sampling rate (" << samplingFrequency
<< "Hz) than other file(s) (" << m_signalEncoder.getInputSamplingRate() << "Hz).\n";
return false;
}
if (m_signalEncoder.getInputMatrix()->getDimensionSize(0) != nChannel)
{
this->getLogManager() << Kernel::LogLevel_Error << "File #"
<< idx + 1 << "/" << (staticBoxContext.getInputCount() / 2)
<< " has a different channel count (" << nChannel
<< ") than other file(s) (" << m_signalEncoder.getInputMatrix()->getDimensionSize(0) << ").\n";
return false;
}
if (m_signalEncoder.getInputMatrix()->getDimensionSize(1) != sampleCountPerBuffer)
{
this->getLogManager() << Kernel::LogLevel_Error << "File #"
<< idx + 1 << "/" << (staticBoxContext.getInputCount() / 2)
<< " has a different sample count per buffer (" << sampleCountPerBuffer
<< ") than other file(s) (" << m_signalEncoder.getInputMatrix()->getDimensionSize(1) << ").\n";
return false;
}
}
m_headerReceivedCount++;
}
}
if (m_signalDecoders[idx]->isBufferReceived() && !m_eofReached[idx])
{
IMemoryBuffer* buffer = new CMemoryBuffer();
buffer->setSize(boxCtx.getInputChunk(i, j)->getSize(), true);
memcpy(buffer->getDirectPointer(), boxCtx.getInputChunk(i, j)->getDirectPointer(), buffer->getSize());
SChunk val;
val.m_Buffer = buffer;
val.m_StartTime = boxCtx.getInputChunkStartTime(i, j);
val.m_EndTime = boxCtx.getInputChunkEndTime(i, j);
m_signalChunkBuffers[idx].push_back(val);
if (boxCtx.getInputChunkEndTime(i, j) < m_fileEndTimes[idx])
{
this->getLogManager() << Kernel::LogLevel_Warning << "Oops, added extra chunk " << CTime(boxCtx.getInputChunkStartTime(i, j))
<< " to " << CTime(boxCtx.getInputChunkEndTime(i, j)) << "\n";
}
m_fileEndTimes[idx] = boxCtx.getInputChunkEndTime(i, j);
}
if (m_signalDecoders[idx]->isEndReceived())
{
// we assume the signal chunks must be continuous, so the end time is the end of the last buffer, don't set here
//just discard it (automatic by decoder)
}
}
}
//STIMULATION INPUTS
for (uint32_t i = 1; i < staticBoxContext.getInputCount(); i += 2)
{
const uint32_t idx = i >> 1;
for (uint32_t j = 0; j < boxCtx.getInputChunkCount(i); ++j)
{
m_stimulationDecoders[idx]->decode(j, true);
if (m_stimulationDecoders[idx]->isHeaderReceived() && !m_stimHeaderSent)
{
m_stimulationEncoder.encodeHeader();
boxCtx.markOutputAsReadyToSend(1, boxCtx.getInputChunkStartTime(i, j), boxCtx.getInputChunkEndTime(i, j));
m_triggerEncoder.encodeHeader();
boxCtx.markOutputAsReadyToSend(2, boxCtx.getInputChunkStartTime(i, j), boxCtx.getInputChunkEndTime(i, j));
m_stimHeaderSent = true;
}
if (m_stimulationDecoders[idx]->isBufferReceived() && !m_eofReached[idx])
{
const IStimulationSet* stimSet = m_stimulationDecoders[idx]->getOutputStimulationSet();
SStimulationChunk val;
val.m_StartTime = boxCtx.getInputChunkStartTime(i, j);
val.m_EndTime = boxCtx.getInputChunkEndTime(i, j);
if (stimSet->getStimulationCount() > 0) { val.m_StimulationSet = new CStimulationSet(); }
else { val.m_StimulationSet = nullptr; }
m_stimulationChunkBuffers[idx].
push_back(val); // we store even if empty to be able to retain the chunking structure of the stimulation input stream
for (size_t stim = 0; stim < stimSet->getStimulationCount(); ++stim)
{
val.m_StimulationSet->appendStimulation(stimSet->getStimulationIdentifier(stim), stimSet->getStimulationDate(stim),
stimSet->getStimulationDuration(stim));
this->getLogManager() << Kernel::LogLevel_Trace << "Input " << i << ": Discovered stim " << stimSet->getStimulationIdentifier(stim)
<< " at date [" << CTime(stimSet->getStimulationDate(stim)) << "] in chunk [" << CTime(val.m_StartTime)
<< ", " << CTime(val.m_EndTime) << "]\n";
if (stimSet->getStimulationIdentifier(stim) == m_eofStimulations[idx])
{
m_eofReached[idx] = true;
m_fileEndTimes[idx] = val.m_EndTime;
this->getLogManager() << Kernel::LogLevel_Info << "File #" << idx + 1 << "/" << (staticBoxContext.getInputCount() / 2) <<
" is finished (end time: " << CTime(m_fileEndTimes[idx]) << "). Later signal chunks will be discarded.\n";
break;
}
}
}
if (m_stimulationDecoders[idx]->isEndReceived() && !m_endSent) { m_endReceivedCount++; }
if (m_endReceivedCount == staticBoxContext.getInputCount() / 2 - 1) { m_endSent = true; }
}
}
bool shouldConcatenate = true;
for (auto&& eof : m_eofReached) { shouldConcatenate &= eof; }
if (shouldConcatenate && !m_statsPrinted)
{
for (uint32_t i = 0; i < m_stimulationChunkBuffers.size(); ++i)
{
if (!m_signalChunkBuffers[i].empty())
{
this->getLogManager() << Kernel::LogLevel_Trace << "File " << i
<< " has 1st signal chunk at " << CTime(m_signalChunkBuffers[i][0].m_StartTime)
<< " last at [" << CTime(m_signalChunkBuffers[i].back().m_EndTime)
<< ", " << CTime(m_signalChunkBuffers[i].back().m_EndTime) << "].\n";
}
if (!m_stimulationChunkBuffers[i].empty())
{
this->getLogManager() << Kernel::LogLevel_Trace << "File " << i
<< " has 1st stim chunk at " << CTime(m_stimulationChunkBuffers[i][0].m_StartTime)
<< " last at [" << CTime(m_stimulationChunkBuffers[i].back().m_EndTime)
<< ", " << CTime(m_stimulationChunkBuffers[i].back().m_EndTime)
<< "].\n";
}
this->getLogManager() << Kernel::LogLevel_Trace << "File " << i << " EOF is at " << CTime(m_fileEndTimes[i]) << "\n";
}
m_statsPrinted = true;
}
if (shouldConcatenate && !m_finished)
{
if (!this->concate()) { return true; }
m_stimulationEncoder.encodeEnd();
boxCtx.markOutputAsReadyToSend(1, m_lastChunkEndTime, m_lastChunkEndTime);
m_triggerEncoder.encodeEnd();
boxCtx.markOutputAsReadyToSend(2, m_lastChunkEndTime, m_lastChunkEndTime);
m_signalEncoder.encodeEnd();
boxCtx.markOutputAsReadyToSend(0, m_lastChunkEndTime, m_lastChunkEndTime);
m_triggerEncoder.getInputStimulationSet()->appendStimulation(OVTK_StimulationId_EndOfFile, this->getPlayerContext().getCurrentTime(), 0);
m_triggerEncoder.encodeBuffer();
boxCtx.markOutputAsReadyToSend(2, this->getPlayerContext().getCurrentTime(), this->getPlayerContext().getCurrentTime());
m_finished = true;
}
return true;
}
bool CBoxAlgorithmSignalConcatenation::concate()
{
Kernel::IBoxIO& boxContext = this->getDynamicBoxContext();
if (!m_resynchroDone)
{
this->getLogManager() << Kernel::LogLevel_Info << "Concatenation in progress...\n";
this->getLogManager() << Kernel::LogLevel_Trace << "Resynchronizing Chunks ...\n";
// note: m_stimulationSets and m_signalChunkBuffers should have the same size (== number of files)
uint64_t offset = m_fileEndTimes[0];
for (uint32_t i = 1; i < m_stimulationChunkBuffers.size(); ++i)
{
for (auto& stimulationChunkBuffer : m_stimulationChunkBuffers[i])
{
IStimulationSet* stimSet = stimulationChunkBuffer.m_StimulationSet;
if (stimSet)
{
for (size_t k = 0; k < stimSet->getStimulationCount(); ++k)
{
const uint64_t synchronizedDate = stimSet->getStimulationDate(k) + offset;
stimSet->setStimulationDate(k, synchronizedDate);
//this->getLogManager() << Kernel::LogLevel_Info << "Resynchronizing stim ["<<m_stimulations[i][j].first<<"] from time ["<<m_stimulations[i][j].second<<"] to ["<<synchronizedDate<<"]\n";
}
}
stimulationChunkBuffer.m_StartTime += offset;
stimulationChunkBuffer.m_EndTime += offset;
}
for (auto& signalChunkBuffer : m_signalChunkBuffers[i])
{
signalChunkBuffer.m_StartTime += offset;
signalChunkBuffer.m_EndTime += offset;
}
offset = offset + m_fileEndTimes[i];
}
this->getLogManager() << Kernel::LogLevel_Trace << "Resynchronization finished.\n";
m_resynchroDone = true;
}
// When we get here, resynchro has been done
// note that the iterators are references on purpose...
for (uint32_t& i = m_state.m_CurrentFileIdx; i < m_signalChunkBuffers.size(); ++i)
{
const std::vector<SChunk>& chunkVector = m_signalChunkBuffers[i];
const std::vector<SStimulationChunk>& stimulusChunkVector = m_stimulationChunkBuffers[i];
// Send a signal chunk
uint32_t& chunk = m_state.m_CurrentChunkIdx;
if (chunk < chunkVector.size())
{
// we write the signal memory buffer
const IMemoryBuffer* iBuffer = chunkVector[chunk].m_Buffer;
IMemoryBuffer* oBuffer = boxContext.getOutputChunk(0);
oBuffer->setSize(iBuffer->getSize(), true);
memcpy(oBuffer->getDirectPointer(), iBuffer->getDirectPointer(), iBuffer->getSize());
boxContext.markOutputAsReadyToSend(0, chunkVector[chunk].m_StartTime, chunkVector[chunk].m_EndTime);
/*
if(CTime(chunkVector[chunk].m_StartTime).toSeconds()>236)
{
this->getLogManager() << Kernel::LogLevel_Info << "Adding signalchunk " << i << "," << chunk << " ["
<< CTime(chunkVector[chunk].m_StartTime) << ", " << CTime(chunkVector[chunk].m_EndTime) << "\n";
}
*/
const uint64_t signalChunkEnd = chunkVector[chunk].m_EndTime;
// Write stimulations up to this point
for (uint32_t& k = m_state.m_CurrentStimulationIdx; k < stimulusChunkVector.size() && stimulusChunkVector[k].m_EndTime <= signalChunkEnd; ++k)
{
const SStimulationChunk& stimChunk = stimulusChunkVector[k];
const IStimulationSet* bufferedStimSet = stimChunk.m_StimulationSet;
IStimulationSet* stimSet = m_stimulationEncoder.getInputStimulationSet();
stimSet->clear();
if (bufferedStimSet)
{
for (size_t s = 0; s < bufferedStimSet->getStimulationCount(); ++s)
{
stimSet->appendStimulation(bufferedStimSet->getStimulationIdentifier(s), bufferedStimSet->getStimulationDate(s),
bufferedStimSet->getStimulationDuration(s));
this->getLogManager() << Kernel::LogLevel_Trace << "Adding stimulation " << bufferedStimSet->getStimulationIdentifier(s)
<< " at date [" << CTime(stimSet->getStimulationDate(s))
<< "] to chunk [" << CTime(stimChunk.m_StartTime)
<< ", " << CTime(stimChunk.m_EndTime)
<< "]\n";
}
}
// encode the stim memory buffer even if it is empty
m_stimulationEncoder.encodeBuffer();
boxContext.markOutputAsReadyToSend(1, stimChunk.m_StartTime, stimChunk.m_EndTime);
/*
if(CTime(stimChunk.m_StartTime).toSeconds()>238 &&
CTime(stimChunk.m_StartTime).toSeconds()<242)
{
this->getLogManager() << Kernel::LogLevel_Info << "Adding stimchunk " << i << "," << k << " ["
<< CTime(stimChunk.m_StartTime)
<< ", " << CTime(stimChunk.m_EndTime)
<< "\n";
}
*/
}
// Let the kernel send blocks up to now, prevent freezing up sending everything at once
chunk++;
return false;
}
// For now we don't support stimuli that don't correspond to signal data, these ones are after the last signal chunk
for (uint32_t& k = m_state.m_CurrentStimulationIdx; k < stimulusChunkVector.size(); ++k)
{
const SStimulationChunk& stimChunk = stimulusChunkVector[k];
const IStimulationSet* bufferedStimSet = stimChunk.m_StimulationSet;
if (i == m_signalChunkBuffers.size() - 1)
{
// last file, let pass
IStimulationSet* stimSet = m_stimulationEncoder.getInputStimulationSet();
stimSet->clear();
if (bufferedStimSet)
{
for (size_t s = 0; s < bufferedStimSet->getStimulationCount(); ++s)
{
stimSet->appendStimulation(bufferedStimSet->getStimulationIdentifier(s), bufferedStimSet->getStimulationDate(s),
bufferedStimSet->getStimulationDuration(s));
this->getLogManager() << Kernel::LogLevel_Warning << "Stimulation " << bufferedStimSet->getStimulationIdentifier(s)
<< " at date [" << CTime(stimSet->getStimulationDate(s))
<< "] in chunk [" << CTime(stimChunk.m_StartTime)
<< ", " << CTime(stimChunk.m_EndTime)
<< "] is after signal ended, but last file, so adding.\n";
}
}
// encode the stim memory buffer even if it is empty
m_stimulationEncoder.encodeBuffer();
boxContext.markOutputAsReadyToSend(1, stimChunk.m_StartTime, stimChunk.m_EndTime);
}
else
{
if (bufferedStimSet)
{
for (size_t s = 0; s < bufferedStimSet->getStimulationCount(); ++s)
{
if (!chunkVector.empty())
{
this->getLogManager() << Kernel::LogLevel_Warning
<< "Stimulation " << bufferedStimSet->getStimulationIdentifier(s)
<< "'s chunk at [" << CTime(stimChunk.m_StartTime)
<< ", " << CTime(stimChunk.m_EndTime)
<< "] is after the last signal chunk end time " << CTime(chunkVector.back().m_EndTime)
<< ", discarded.\n";
}
}
}
}
}
// Finished with the file
// if(stimChunk.m_EndTime < chunkVector[m_CurrentChunkIdx].m_EndTime)
// {
// There is no corresponding signal anymore, skip the rest of the stimulations from this file
//this->getLogManager() << Kernel::LogLevel_Info << "Stimulus time " << CTime(stimulusChunkVector[j].m_EndTime)
// << " exceeds the last signal buffer end time " << CTime(chunkVector[chunkVector.size()-1].m_EndTime)
// << "\n";
//break;
//}
m_state.m_CurrentChunkIdx = 0;
m_state.m_CurrentStimulationIdx = 0;
this->getLogManager() << Kernel::LogLevel_Info << "File #" << i + 1 << " Finished.\n";
}
//We search for the last file with data.
for (uint32_t lastFile = m_signalChunkBuffers.size(); lastFile > 0; lastFile--)
{
const uint32_t lastChunkOfLastFile = m_signalChunkBuffers[lastFile - 1].size();
if (lastChunkOfLastFile != 0)
{
m_lastChunkEndTime = m_signalChunkBuffers[lastFile - 1][lastChunkOfLastFile - 1].m_EndTime;
break;
}
}
this->getLogManager() << Kernel::LogLevel_Info << "Concatenation finished !\n";
return true;
}
} // namespace FileIO
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,212 @@
#pragma once
#include <openvibe/ov_all.h>
#include <toolkit/ovtk_all.h>
#include <vector>
#include <list>
// The unique identifiers for the box and its descriptor.
// Identifier are randomly chosen by the skeleton-generator.
#define OVP_ClassId_BoxAlgorithm_SignalConcatenation OpenViBE::CIdentifier(0x372F3A9D, 0x49E20CD2)
#define OVP_ClassId_BoxAlgorithm_SignalConcatenationDesc OpenViBE::CIdentifier(0x372F3A9D, 0x49E20CD2)
namespace OpenViBE {
namespace Plugins {
namespace FileIO {
/**
* \class CBoxAlgorithmSignalConcatenation
* \author Laurent Bonnet (INRIA)
* \date Tue Jun 28 09:52:48 2011
* \brief The class CBoxAlgorithmSignalConcatenation describes the box Signal Concatenation.
*
*/
class CBoxAlgorithmSignalConcatenation final : virtual public Toolkit::TBoxAlgorithm<IBoxAlgorithm>
{
public:
void release() override { delete this; }
bool initialize() override;
bool uninitialize() override;
bool processClock(Kernel::CMessageClock& msg) override;
uint64_t getClockFrequency() override { return 8LL << 32; }
bool processInput(const size_t index) override;
bool process() override;
_IsDerivedFromClass_Final_(Toolkit::TBoxAlgorithm<IBoxAlgorithm>, OVP_ClassId_BoxAlgorithm_SignalConcatenation)
protected:
bool concate();
bool m_finished = false;
bool m_resynchroDone = false;
uint64_t m_timeOut = 0;
bool m_headerSent = false;
uint32_t m_headerReceivedCount = 0;
uint32_t m_endReceivedCount = 0;
bool m_stimHeaderSent = false;
bool m_endSent = false;
bool m_statsPrinted = false;
std::vector<uint64_t> m_eofStimulations;
std::vector<bool> m_eofReached;
struct SChunk
{
IMemoryBuffer* m_Buffer;
uint64_t m_StartTime;
uint64_t m_EndTime;
};
struct SStimulationChunk
{
IStimulationSet* m_StimulationSet;
uint64_t m_StartTime;
uint64_t m_EndTime;
};
uint64_t m_stimChunkLength = 0;
// File end times
std::vector<uint64_t> m_fileEndTimes;
// The signal buffers, one per file
std::vector<std::vector<SChunk>> m_signalChunkBuffers;
std::vector<std::vector<SStimulationChunk>> m_stimulationChunkBuffers;
// The stimulations are stored in one stimulation set per file. The chunk are reconstructed.
std::vector<IStimulationSet*> m_stimulationSets;
//The decoders, (1 signal/1 stim) per file
std::vector<Toolkit::TStimulationDecoder<CBoxAlgorithmSignalConcatenation>*> m_stimulationDecoders;
std::vector<Toolkit::TSignalDecoder<CBoxAlgorithmSignalConcatenation>*> m_signalDecoders;
// the encoders : signal, stim and trigger encoder.
Toolkit::TSignalEncoder<CBoxAlgorithmSignalConcatenation> m_signalEncoder;
Toolkit::TStimulationEncoder<CBoxAlgorithmSignalConcatenation> m_stimulationEncoder;
Toolkit::TStimulationEncoder<CBoxAlgorithmSignalConcatenation> m_triggerEncoder;
uint64_t m_triggerDate = 0;
uint64_t m_lastChunkStartTime = 0;
uint64_t m_lastChunkEndTime = 0;
struct SConcatenationState
{
SConcatenationState() : m_CurrentFileIdx(0), m_CurrentChunkIdx(0), m_CurrentStimulationIdx(0) { }
uint32_t m_CurrentFileIdx;
uint32_t m_CurrentChunkIdx;
uint32_t m_CurrentStimulationIdx;
};
SConcatenationState m_state;
};
// The box listener can be used to call specific callbacks whenever the box structure changes : input added, name changed, etc.
// Please uncomment below the callbacks you want to use.
class CBoxAlgorithmSignalConcatenationListener final : public Toolkit::TBoxListener<IBoxListener>
{
public:
bool check(Kernel::IBox& box) const
{
for (uint32_t i = 0; i < box.getInputCount() >> 1; ++i)
{
box.setInputName(i * 2, ("Input signal " + std::to_string(i + 1)).c_str());
box.setInputType(i * 2, OV_TypeId_Signal);
box.setInputName(i * 2 + 1, ("Input stimulations " + std::to_string(i + 1)).c_str());
box.setInputType(i * 2 + 1, OV_TypeId_Stimulations);
box.setSettingName(i + 1, ("End-of-file stimulation for input " + std::to_string(i + 1)).c_str());
}
return true;
}
bool onInputRemoved(Kernel::IBox& box, const size_t index) override
{
if (index & 1) { box.removeInput(index - 1); } // odd index
else { box.removeInput(index); } // even index
box.removeSetting(index >> 1);
return this->check(box);
}
bool onInputAdded(Kernel::IBox& box, const size_t /*index*/) override
{
box.addInput("", OV_TypeId_Stimulations);
box.addSetting("",OV_TypeId_Stimulation, "OVTK_StimulationId_ExperimentStop");
return this->check(box);
}
_IsDerivedFromClass_Final_(Toolkit::TBoxListener<IBoxListener>, CIdentifier::undefined())
};
/**
* \class CBoxAlgorithmSignalConcatenationDesc
* \author Laurent Bonnet (INRIA)
* \date Tue Jun 28 09:52:48 2011
* \brief Descriptor of the box Signal Concatenation.
*
*/
class CBoxAlgorithmSignalConcatenationDesc final : virtual public IBoxAlgorithmDesc
{
public:
void release() override { }
CString getName() const override { return CString("Signal Concatenation"); }
CString getAuthorName() const override { return CString("Laurent Bonnet"); }
CString getAuthorCompanyName() const override { return CString("INRIA"); }
CString getShortDescription() const override { return CString("Concatenates multiple signal streams"); }
CString getDetailedDescription() const override
{
return CString(
"The signal stream concatenation box reads multiple streams in parallel, and produces a single stream that is the concatenation of all inputs.");
}
CString getCategory() const override { return CString("File reading and writing"); }
CString getVersion() const override { return CString("2.0"); }
CString getStockItemName() const override { return CString("gtk-add"); }
CIdentifier getCreatedClass() const override { return OVP_ClassId_BoxAlgorithm_SignalConcatenation; }
IPluginObject* create() override { return new CBoxAlgorithmSignalConcatenation; }
IBoxListener* createBoxListener() const override { return new CBoxAlgorithmSignalConcatenationListener; }
void releaseBoxListener(IBoxListener* listener) const override { delete listener; }
bool getBoxPrototype(Kernel::IBoxProto& prototype) const override
{
prototype.addInput("Input signal 1",OV_TypeId_Signal);
prototype.addInput("Input stimulations 1",OV_TypeId_Stimulations);
prototype.addInput("Input signal 2",OV_TypeId_Signal);
prototype.addInput("Input stimulations 2",OV_TypeId_Stimulations);
prototype.addFlag(Kernel::BoxFlag_CanAddInput);
prototype.addOutput("Signal",OV_TypeId_Signal);
prototype.addOutput("Stimulations",OV_TypeId_Stimulations);
prototype.addOutput("Status",OV_TypeId_Stimulations);
prototype.addSetting("Time out before assuming end-of-file (in sec)",OV_TypeId_Integer, "5");
prototype.addSetting("End-of-file stimulation for input 1",OV_TypeId_Stimulation, "OVTK_StimulationId_ExperimentStop");
prototype.addSetting("End-of-file stimulation for input 2",OV_TypeId_Stimulation, "OVTK_StimulationId_ExperimentStop");
//prototype.addFlag(OV_AttributeId_Box_FlagIsUnstable);
return true;
}
_IsDerivedFromClass_Final_(IBoxAlgorithmDesc, OVP_ClassId_BoxAlgorithm_SignalConcatenationDesc)
};
} // namespace FileIO
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,824 @@
#include "ovpCGDFFileReader.h"
// @fixme memory leaks on errors
#include <iostream>
#include <cstdlib>
#include <cstring>
namespace OpenViBE {
namespace Plugins {
namespace FileIO {
// template<> double FileIO::CGDFFileReader::GDFTypeToFloat64<float>(float val, uint32_t channel) { std::cout << "specialized 1\n"; return val; }
// template<> double FileIO::CGDFFileReader::GDFTypeToFloat64<double>(double val, uint32_t channel) { std::cout << "specialized 2\n"; return val; }
// #define DEBUG_FILE_POSITIONS 1
//Plugin Methods
bool CGDFFileReader::initialize()
{
m_filename = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 0);
m_samplesPerBuffer = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 1);
m_translateByMinimum = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 2);
if (m_samplesPerBuffer == 0)
{
this->getLogManager() << Kernel::LogLevel_Error <<
"SamplesPerBuffer is 0, this will not work\n";
return false;
}
m_xpInfoEncoder = new Toolkit::TExperimentInfoEncoder<CGDFFileReader>;
m_xpInfoEncoder->initialize(*this, GDFReader_ExperimentInfoOutput);
m_signalEncoder = new Toolkit::TSignalEncoder<CGDFFileReader>;
m_signalEncoder->initialize(*this, GDFReader_SignalOutput);
m_stimulationEncoder = new Toolkit::TStimulationEncoder<CGDFFileReader>;
m_stimulationEncoder->initialize(*this, GDFReader_StimulationOutput);
//allocate the structure used to store the experiment information
m_xpInfoHeader = new CExperimentInfoHeader;
//opens the gdf file
if (m_filename != CString(""))
{
m_file.open(m_filename.toASCIIString(), std::ios::binary);
if (!m_file.good())
{
this->getLogManager() << Kernel::LogLevel_Error << "Could not open file [" << m_filename << "]\n";
return false;
}
}
m_file.seekg(0, std::ios::end);
m_fileSize = size_t(m_file.tellg());
m_file.seekg(0, std::ios::beg);
m_header3Length = 0;
// const char *filename = m_sFileName.toASCIIString();
// this->getLogManager() << Kernel::LogLevel_Trace << "Opening [" << filename << "]\n";
//reads the gdf headers and sends the corresponding buffers
const bool res = readFileHeader();
#ifdef DEBUG_FILE_POSITIONS
this->getLogManager() << Kernel::LogLevel_Info << "After all headers, file is at " << m_file.tellg() << "\n";
#endif
return res;
}
bool CGDFFileReader::uninitialize()
{
if (m_signalEncoder)
{
m_signalEncoder->uninitialize();
delete m_signalEncoder;
}
if (m_xpInfoEncoder)
{
m_xpInfoEncoder->uninitialize();
delete m_xpInfoEncoder;
}
if (m_stimulationEncoder)
{
m_stimulationEncoder->uninitialize();
delete m_stimulationEncoder;
}
//desallocate all of the remaining buffers
delete[] m_channelDataSize; //can be done before?
delete[] m_channelType; //can be done before?
delete[] m_channelScale;
delete[] m_channelTranslate;
delete[] m_dataRecordBuffer; //can be done before??
delete[] m_channelDataInDataRecord;
delete[] m_matrixBuffer; //can be done before?
delete[] m_eventsPositionBuffer;
delete[] m_eventsTypeBuffer;
//Close the GDF file
if (m_file) { m_file.close(); }
return true;
}
bool CGDFFileReader::processClock(Kernel::CMessageClock& msg)
{
if (m_signalDesc.m_Sampling == 0)
{
this->getLogManager() << Kernel::LogLevel_Error << "Sampling rate is 0 - not supported.\n";
return false;
}
const uint64_t time = CTime(m_signalDesc.m_Sampling, m_nSentSample + m_signalDesc.m_NSample).time();
if (msg.getTime() > time) { getBoxAlgorithmContext()->markAlgorithmAsReadyToProcess(); }
return true;
}
void CGDFFileReader::gdfBufferToDoubleBuffer(double* out, void* in, const uint64_t size, const uint32_t channel)
{
switch (m_channelType[channel])
{
case GDF::ChannelType_int8_t:
gdfTypeBufferToDoubleBuffer<int8_t>(out, reinterpret_cast<int8_t*>(in), size, channel);
break;
case GDF::ChannelType_uint8_t:
gdfTypeBufferToDoubleBuffer<uint8_t>(out, reinterpret_cast<uint8_t*>(in), size, channel);
break;
case GDF::ChannelType_int16_t:
gdfTypeBufferToDoubleBuffer<int16_t>(out, reinterpret_cast<int16_t*>(in), size, channel);
break;
case GDF::ChannelType_uint16_t:
gdfTypeBufferToDoubleBuffer<uint16_t>(out, reinterpret_cast<uint16_t*>(in), size, channel);
break;
case GDF::ChannelType_int32_t:
gdfTypeBufferToDoubleBuffer<int>(out, reinterpret_cast<int*>(in), size, channel);
break;
case GDF::ChannelType_uint32_t:
gdfTypeBufferToDoubleBuffer<uint32_t>(out, reinterpret_cast<uint32_t*>(in), size, channel);
break;
case GDF::ChannelType_int64_t:
gdfTypeBufferToDoubleBuffer<int64_t>(out, reinterpret_cast<int64_t*>(in), size, channel);
break;
case GDF::ChannelType_uint64_t:
gdfTypeBufferToDoubleBuffer<uint64_t>(out, reinterpret_cast<uint64_t*>(in), size, channel);
break;
case GDF::ChannelType_float:
gdfTypeBufferToDoubleBuffer<float>(out, reinterpret_cast<float*>(in), size, channel);
break;
case GDF::ChannelType_double:
gdfTypeBufferToDoubleBuffer<double>(out, reinterpret_cast<double*>(in), size, channel);
break;
case GDF::ChannelType_float128:
{
//Not handled
this->getLogManager() << Kernel::LogLevel_Warning << "This data type is currently not handled : float128.\n";
m_errorOccurred = true;
}
break;
case GDF::ChannelType_int24:
{
uint8_t* p = reinterpret_cast<uint8_t*>(in);
for (uint64_t i = 0; i < (size * 3); i += 3) { out[i] = gdfTypeToDouble(p[i] + (p[i + 1] << 8) + (p[i + 2] << 16), channel); }
}
break;
case GDF::ChannelType_uint24:
{
int8_t* p = reinterpret_cast<int8_t*>(in);
for (uint64_t i = 0; i < (size * 3); i += 3) { out[i] = gdfTypeToDouble(p[i] + (p[i + 1] << 8) + (p[i + 2] << 16), channel); }
}
break;
default:
//not handled
this->getLogManager() << Kernel::LogLevel_Warning << "Invalid GDF data type!\n";
m_errorOccurred = true;
break;
}
}
bool CGDFFileReader::readFileHeader()
{
Kernel::IBoxIO& boxIO = this->getDynamicBoxContext();
if (!m_xpInfoSent)
{
this->getLogManager() << Kernel::LogLevel_Trace << "Reading experiment information\n";
//First reads the file type
char type[3];
char version[6]; // field has size 5, we add +1 for terminating NULL
m_file.read(type, 3);
//if not a gdf file
if (strncmp(type, "GDF", 3) != 0)
{
//Handle error
this->getLogManager() << Kernel::LogLevel_Warning << "This is not a valid GDF File!\n";
m_errorOccurred = true;
return false;
}
m_file.read(version, 5);
version[5] = 0; // The version is not NULL-terminated in the file, we terminate with NULL manually for atof.
m_fileVersion = float(atof(&version[1]));
this->getLogManager() << Kernel::LogLevel_Debug << "File version parsed as " << m_fileVersion << "\n";
if (m_file.bad())
{
//Handle error
this->getLogManager() << Kernel::LogLevel_Warning << "Error while reading file.\n";
m_errorOccurred = true;
return false;
}
if (m_fileVersion < 3)
{
GDF::CFixedGDFHeader* header;
if (m_fileVersion > 2.12) { header = new GDF::CFixedGDF251Header; }
else if (m_fileVersion > 1.90) { header = new GDF::CFixedGDF2Header; }
else { header = new GDF::CFixedGDF1Header; }
if (!header->read(m_file))
{
this->getLogManager() << Kernel::LogLevel_Error <<
"Failure to parse fixed header\n";
m_errorOccurred = true;
delete header;
return false;
}
// kludge: header should be 256 bytes long, make sure the file position is there now.
if (m_fileVersion > 1.90) { m_file.seekg(256, std::ios_base::beg); }
m_xpInfoHeader->m_ExperimentID = header->getExperimentID();
m_xpInfoHeader->m_ExperimentDate = header->getExperimentDate();
m_xpInfoHeader->m_SubjectID = header->getSubjectID();
m_xpInfoHeader->m_SubjectName = header->getSubjectName();
m_xpInfoHeader->m_SubjectAge = header->getSubjectAge();
m_xpInfoHeader->m_SubjectSex = header->getSubjectSex();
m_xpInfoHeader->m_LaboratoryID = header->getLaboratoryID();
m_xpInfoHeader->m_LaboratoryName = header->getLaboratoryName();
m_xpInfoHeader->m_TechnicianID = header->getTechnicianID();
m_xpInfoHeader->m_TechnicianName = header->getTechnicianName();
//Experiment header ready to send now
m_xpInfoHeader->m_ReadyToSend = true;
m_durationDataRecord = header->getDataRecordDuration();
m_nDataRecords = header->getNDataRecords();
//this information is related to the signal
m_nChannels = uint16_t(header->getChannelCount());
m_signalDesc.m_NChannel = m_nChannels;
//Send the header
writeExperimentInfo();
boxIO.markOutputAsReadyToSend(GDFReader_ExperimentInfoOutput, 0, 0);
m_xpInfoSent = true;
//not needed anymore
delete header;
delete m_xpInfoHeader;
m_xpInfoHeader = nullptr;
}
else
{
//Not a known GDF File version
this->getLogManager() << Kernel::LogLevel_Error << "GDF file version " << m_fileVersion << " is not supported.\n";
//Error handling
m_errorOccurred = true;
return false;
}
}//END of ExperimentHeader
if (!m_signalDescSent)
{
this->getLogManager() << Kernel::LogLevel_Trace << "Reading signal description\n";
#ifdef DEBUG_FILE_POSITIONS
this->getLogManager() << Kernel::LogLevel_Info << "Before variable header, file is at " << m_file.tellg() << "\n";
#endif
//reads the whole variable header
char* headerBuffer = new char[m_nChannels * 256];
m_file.read(headerBuffer, m_nChannels * 256);
#ifdef DEBUG_FILE_POSITIONS
this->getLogManager() << Kernel::LogLevel_Info << "After variable header, file is at " << m_file.tellg() << "\n";
#endif
if (m_file.bad())
{
//Handle error
this->getLogManager() << Kernel::LogLevel_Error << "Read error.\n";
m_errorOccurred = true;
return false;
}
m_signalDesc.m_ChannelNames.resize(m_nChannels);
//channel's signal gain/translation
m_channelScale = new double[m_nChannels];
m_channelTranslate = new double[m_nChannels];
double* physicalMin = reinterpret_cast<double*>(headerBuffer + (104 * m_nChannels));
double* physicalMax = reinterpret_cast<double*>(headerBuffer + (112 * m_nChannels));
int64_t* digitalMin = reinterpret_cast<int64_t*>(headerBuffer + (120 * m_nChannels)); // v1?
int64_t* digitalMax = reinterpret_cast<int64_t*>(headerBuffer + (128 * m_nChannels));
double* digitalMinD = reinterpret_cast<double*>(headerBuffer + (120 * m_nChannels)); // v2+
double* digitalMaxD = reinterpret_cast<double*>(headerBuffer + (128 * m_nChannels));
for (uint16_t i = 0; i < m_nChannels; ++i)
{
if (m_fileVersion > 1.90) { m_channelScale[i] = (physicalMax[i] - physicalMin[i]) / (digitalMaxD[i] - digitalMinD[i]); }
else { m_channelScale[i] = (physicalMax[i] - physicalMin[i]) / (digitalMax[i] - digitalMin[i]); }
if (m_translateByMinimum) { m_channelTranslate[i] = physicalMin[i]; }
else { m_channelTranslate[i] = (physicalMax[i] + physicalMin[i]) / 2.0; }
this->getLogManager() << Kernel::LogLevel_Debug << "Channel " << i << " physMin " << physicalMin[i] << " physMax " << physicalMax[i]
<< " digMin " << digitalMin[i] << " digMax " << digitalMax[i]
<< " digMinF " << digitalMinD[i] << " digMaxF " << digitalMaxD[i]
<< " scale " << m_channelScale[i] << " trans " << m_channelTranslate[i] << "\n";
}
//Check if all the channels have the same sampling rate
uint32_t* nSamplesPerRecordArray = reinterpret_cast<uint32_t*>(headerBuffer + (216 * m_nChannels));
m_nSamplesPerRecord = nSamplesPerRecordArray[0];
for (uint16_t i = 1; i < m_nChannels; ++i)
{
//If all the channels don't have the same sampling rate
if (m_nSamplesPerRecord != nSamplesPerRecordArray[i])
{
if (m_fileVersion > 1.90)
{
this->getLogManager() << Kernel::LogLevel_Error <<
"Interpreted GDF file to have channels with varying sampling rates, which is not supported.\n";
this->getLogManager() << Kernel::LogLevel_Error << "This can be a misinterpretation of the newer GDF subformats. File claims to follow GDF "
<< m_fileVersion << ".\n";
}
else { this->getLogManager() << Kernel::LogLevel_Error << "Can't handle GDF files with channels having different sampling rates!\n"; }
m_errorOccurred = true;
return false;
}
}
//type of the channels' data
m_channelType = new uint32_t[m_nChannels];
memcpy(m_channelType, headerBuffer + (220 * m_nChannels), m_nChannels * 4);
m_channelDataSize = new uint16_t[m_nChannels];
this->getLogManager() << Kernel::LogLevel_Debug << "Found " << m_nChannels << " channels...\n";
for (uint16_t i = 0; i < m_nChannels; ++i)
{
//Find the data size for each channel
//TODO use enum to specify each type's name
m_channelDataSize[i] = GDF::GDFDataSize(m_channelType[i]);
//Here, we can compute the size of a data record, based on the type of each channel
m_dataRecordSize += m_nSamplesPerRecord * m_channelDataSize[i];
//reads the channels names
m_signalDesc.m_ChannelNames[i].assign(headerBuffer + (16 * i), 16);
this->getLogManager() << Kernel::LogLevel_Debug << " * Channel " << uint32_t(i + 1) << " : " << CString(
m_signalDesc.m_ChannelNames[i].c_str()) << "\n";
}
//This parameter is defined by the user of the plugin
m_signalDesc.m_NSample = uint32_t(m_samplesPerBuffer);
//needs to be computed based on the duration of a data record and the number of samples in one of those data records
m_signalDesc.m_Sampling = uint32_t(0.5 + (m_nSamplesPerRecord / m_durationDataRecord));
if (m_signalDesc.m_Sampling == 0)
{
this->getLogManager() << Kernel::LogLevel_Error << "Sampling rate is 0 - not supported\n";
m_errorOccurred = true;
return false;
}
this->getLogManager() << Kernel::LogLevel_Debug << "Samples in file : " << m_nDataRecords * m_nSamplesPerRecord << " samples\n";
this->getLogManager() << Kernel::LogLevel_Debug << "Sample count per buffer : " << m_samplesPerBuffer << "\n";
this->getLogManager() << Kernel::LogLevel_Debug << "Sampling rate : " << m_signalDesc.m_Sampling << "\n";
//computes clock frequency
if (m_samplesPerBuffer <= m_signalDesc.m_Sampling)
{
if (m_signalDesc.m_Sampling % m_samplesPerBuffer != 0)
{
this->getLogManager() << Kernel::LogLevel_Warning << "The sampling rate isn't a multiple of the buffer size\n";
this->getLogManager() << Kernel::LogLevel_Warning << "Please consider adjusting the GDFReader settings to correct this!\n";
this->getLogManager() << Kernel::LogLevel_Warning << "Sampling rate was " << m_signalDesc.m_Sampling << "\n";
this->getLogManager() << Kernel::LogLevel_Warning << "Buffer size was " << m_samplesPerBuffer << "\n";
}
// Intentional parameter swap to get the frequency
m_clockFrequency = CTime(m_samplesPerBuffer, m_signalDesc.m_Sampling).time();
}
// We may need to skip header3 with its tags and take its size into account
if (m_fileVersion >= 2.10)
{
#ifdef DEBUG_FILE_POSITIONS
this->getLogManager() << Kernel::LogLevel_Info << "Before header3, file is at " << m_file.tellg() << "\n";
#endif
m_header3Length = 0;
while (true)
{
char buffer[4];
m_file.read((char*)&buffer, 4); // Curious Cast
if (m_file.bad() || m_file.eof()) { break; }
m_header3Length += 4;
uint32_t tag = buffer[0];
uint32_t length = (uint32_t(buffer[1]) << 0) + (uint32_t(buffer[2]) << 8) + (uint32_t(buffer[3]) << 16); // src is uint24
this->getLogManager() << Kernel::LogLevel_Info << "Found tag " << tag << " at pos " << int64_t(m_file.tellg() - std::streamoff(4))
<< " [length " << length << "], skipping content.\n";
if (tag == 0) { break; }
m_file.seekg(std::streamoff(length), std::ios_base::cur);
m_header3Length += length;
}
// Skip possible padding
const uint64_t paddingRequired = (256 - m_header3Length) % 256;
m_header3Length += paddingRequired;
m_file.seekg(paddingRequired, std::ios::cur);
if (m_file.eof() || m_file.tellg() <= 0)
{
this->getLogManager() << Kernel::LogLevel_Error << "File ended by trying to skip the header.\n";
return false;
}
#ifdef DEBUG_FILE_POSITIONS
this->getLogManager() << Kernel::LogLevel_Info << "After header3, file is at " << m_file.tellg() << ". Header length: " << m_header3Length << ", padding was " << paddingRequired << "\n";
#endif
}
//Send the data to the output
writeSignalInformation();
boxIO.markOutputAsReadyToSend(GDFReader_SignalOutput, 0, 0);
delete[] headerBuffer;
m_signalDescSent = true;
}//END of SignalHeader
return true;
}
void CGDFFileReader::writeExperimentInfo() const
{
// Here we have to declare some variables in the same scope as the encoding call, because otherwise they might be freed before the
// encoder gets to process the data. The point of these is just to convert from std::string to CString as needed by the encoder.
CString date(m_xpInfoHeader->m_ExperimentDate.c_str());
CString name(m_xpInfoHeader->m_SubjectName.c_str());
CString labName(m_xpInfoHeader->m_LaboratoryName.c_str());
CString techName(m_xpInfoHeader->m_TechnicianName.c_str());
if (m_xpInfoHeader->m_ExperimentID != NO_VALUE_I) { m_xpInfoEncoder->getInputExperimentID() = m_xpInfoHeader->m_ExperimentID; }
if (m_xpInfoHeader->m_ExperimentDate != NO_VALUE_S) { m_xpInfoEncoder->getInputExperimentDate() = &date; }
if (m_xpInfoHeader->m_SubjectID != NO_VALUE_I) { m_xpInfoEncoder->getInputSubjectID() = m_xpInfoHeader->m_SubjectID; }
if (m_xpInfoHeader->m_SubjectName != NO_VALUE_S) { m_xpInfoEncoder->getInputSubjectName() = &name; }
if (m_xpInfoHeader->m_SubjectAge != NO_VALUE_I) { m_xpInfoEncoder->getInputSubjectAge() = m_xpInfoHeader->m_SubjectAge; }
if (m_xpInfoHeader->m_SubjectSex != NO_VALUE_I) { m_xpInfoEncoder->getInputSubjectGender() = m_xpInfoHeader->m_SubjectSex; }
if (m_xpInfoHeader->m_LaboratoryID != NO_VALUE_I) { m_xpInfoEncoder->getInputLaboratoryID() = m_xpInfoHeader->m_LaboratoryID; }
if (m_xpInfoHeader->m_LaboratoryName != NO_VALUE_S) { m_xpInfoEncoder->getInputLaboratoryName() = &labName; }
if (m_xpInfoHeader->m_TechnicianID != NO_VALUE_I) { m_xpInfoEncoder->getInputTechnicianID() = m_xpInfoHeader->m_TechnicianID; }
if (m_xpInfoHeader->m_TechnicianName != NO_VALUE_S) { m_xpInfoEncoder->getInputTechnicianName() = &techName; }
m_xpInfoEncoder->encodeHeader();
}
void CGDFFileReader::writeSignalInformation()
{
m_signalEncoder->getInputSamplingRate() = m_signalDesc.m_Sampling;
CMatrix* iMatrix = m_signalEncoder->getInputMatrix();
iMatrix->resize(m_signalDesc.m_NChannel, m_signalDesc.m_NSample);
for (uint32_t i = 0; i < m_signalDesc.m_NChannel; ++i) { iMatrix->setDimensionLabel(0, i, m_signalDesc.m_ChannelNames[i].c_str()); }
m_signalEncoder->encodeHeader();
}
void CGDFFileReader::writeEvents()
{
IStimulationSet* stimSet = m_stimulationEncoder->getInputStimulationSet();
stimSet->clear();
for (size_t i = 0; i < m_events.size(); ++i)
{
//compute date
const uint64_t date = CTime(m_signalDesc.m_Sampling, m_events[i].m_Position).time();
stimSet->appendStimulation(m_events[i].m_Type, date, 0);
}
m_stimulationEncoder->encodeBuffer();
}
bool CGDFFileReader::process()
{
//Don't do anything if an error as occurred while reading the input file
//for instance, if the file has channels with different sampling rates
if (m_errorOccurred)
{
this->getLogManager() << Kernel::LogLevel_Error << "Some error occurred, aborting.";
return false;
}
uint64_t start = 0;
uint64_t end = 0;
Kernel::IBoxIO& boxIO = this->getDynamicBoxContext();
// Process Matrices
if (m_signalDescSent && !m_matricesSent)
{
//If the matrix buffer is not allocated yet
//"first time"
if (!m_matrixBuffer)
{
//output matrix buffer
m_matrixBufferSize = m_signalDesc.m_NSample * m_signalDesc.m_NChannel;
m_matrixBuffer = new double[size_t(m_matrixBufferSize)];
//We also have to read the first data record
m_dataRecordBuffer = new uint8_t[size_t(m_dataRecordSize)];
m_file.read(reinterpret_cast<char*>(m_dataRecordBuffer), std::streamsize(m_dataRecordSize));
if (m_file.bad())
{
//Handle error
this->getLogManager() << Kernel::LogLevel_Error <<
"Read error\n";
m_errorOccurred = true;
return false;
}
//initialize subpointers
m_channelDataInDataRecord = new uint8_t*[m_nChannels];
m_channelDataInDataRecord[0] = m_dataRecordBuffer;
for (int i = 1; i < m_nChannels; ++i)
{
m_channelDataInDataRecord[i] = m_channelDataInDataRecord[i - 1] + m_channelDataSize[i - 1] * m_nSamplesPerRecord;
}
}
uint32_t nSample = 0;
bool readyToSend = false;
while (!readyToSend)
{
//there is the same number of samples
const uint64_t samplesRemainingInDataRecord = m_nSamplesPerRecord - m_currentSampleInDataRecord;
//If there is enough data in the current data record to read a matrix buffer
if ((m_samplesPerBuffer - nSample) <= samplesRemainingInDataRecord)
{
for (int i = 0; i < m_nChannels; ++i)
{
//reads m_samplesPerBuffer samples and converts/writes them in output buffer
gdfBufferToDoubleBuffer(m_matrixBuffer + (i * m_samplesPerBuffer) + nSample,
m_channelDataInDataRecord[i] + (m_currentSampleInDataRecord * m_channelDataSize[i]),
m_samplesPerBuffer - nSample, i);
}
m_currentSampleInDataRecord += uint32_t(m_samplesPerBuffer - nSample);
//Prepares for the next matrix
nSample = 0;
//We can send the matrix
readyToSend = true;
}
//Not enough data in the current data record. Read the remaining samples, then load a new data record and read the rest
else
{
//copy what is remaining in the current buffer
for (int i = 0; i < m_nChannels; ++i)
{
gdfBufferToDoubleBuffer(m_matrixBuffer + (i * m_samplesPerBuffer) + nSample,
m_channelDataInDataRecord[i] + (m_currentSampleInDataRecord * m_channelDataSize[i]),
samplesRemainingInDataRecord, i);
}
//Updates the index in the output matrix
nSample += uint32_t(samplesRemainingInDataRecord);
//reads the next data record if there is one
if (m_currentDataRecord < m_nDataRecords - 1)
{
//reads a data record
m_file.read(reinterpret_cast<char*>(m_dataRecordBuffer), std::streamsize(m_dataRecordSize));
if (m_file.bad())
{
//Handle error
this->getLogManager() << Kernel::LogLevel_Error <<
"Read error\n";
m_errorOccurred = true;
return false;
}
m_currentSampleInDataRecord = 0;
m_currentDataRecord++;
}
//if there are no more data records
else
{
//we can (for instance) pad the rest of the matrix with 0s
for (int i = 0; i < m_nChannels; ++i)
{
memset(m_matrixBuffer + (((i * m_samplesPerBuffer) + nSample)), 0, size_t(m_samplesPerBuffer - nSample) * sizeof(double));
}
//We can send the matrix
readyToSend = true;
//No more data after that
m_matricesSent = true;
}
}
//Check if we have finished the current data record
if (m_currentSampleInDataRecord == m_nSamplesPerRecord)
{
m_currentSampleInDataRecord = 0;
m_currentDataRecord++;
//if there are no more data records
if (m_currentDataRecord >= m_nDataRecords - 1)
{
//We don't have to read data records anymore
m_matricesSent = true;
}
else
{
//reads a data record
m_file.read(reinterpret_cast<char*>(m_dataRecordBuffer), std::streamsize(m_dataRecordSize));
if (m_file.bad())
{
//Handle error
this->getLogManager() << Kernel::LogLevel_Error <<
"Read error\n";
m_errorOccurred = true;
return false;
}
}
}
}
m_nSentSample += m_signalDesc.m_NSample;
// this->getLogManager() << Kernel::LogLevel_Trace << "Sent " << m_nSentSample << " samples\n";
//A signal matrix is ready to be output
start = CTime(m_signalDesc.m_Sampling, uint64_t(m_nSentSample - m_signalDesc.m_NSample)).time();
end = CTime(m_signalDesc.m_Sampling, uint64_t(m_nSentSample)).time();
CMatrix* iMatrix = m_signalEncoder->getInputMatrix();
double* buffer = iMatrix->getBuffer();
for (uint32_t i = 0; i < iMatrix->getBufferElementCount(); ++i) { buffer[i] = *(m_matrixBuffer + i); }
m_signalEncoder->encodeBuffer();
boxIO.markOutputAsReadyToSend(GDFReader_SignalOutput, start, end);
}
//Events
if (m_signalDescSent && !m_eventsSent)
{
//reads the events table header if it hasn't been done already
if (!m_eventsPositionBuffer)
{
const std::streamoff backupPos = m_file.tellg();
const std::streamoff eventDataPos = std::streamoff((256 * (m_nChannels + 1)) + m_header3Length + (m_nDataRecords * m_dataRecordSize));
//checks if there are event information
if (size_t(eventDataPos) + 1 < m_fileSize)
{
m_file.seekg(eventDataPos);
//reads the event table mode
m_file >> m_eventTableMode;
}
//no event information
else
{
m_file.seekg(backupPos);
m_eventsSent = true;
return true;
}
uint32_t eventTableHeaderMain[7];
uint8_t* eventTableHeader = reinterpret_cast<uint8_t*>(eventTableHeaderMain);
memset(eventTableHeaderMain, 0, sizeof(eventTableHeaderMain));
m_file.read(reinterpret_cast<char*>(eventTableHeader), 7);
if (m_fileVersion > 1.90) { m_nEvents = *(reinterpret_cast<uint32_t*>(eventTableHeader + 0)); }
else { m_nEvents = *(reinterpret_cast<uint32_t*>(eventTableHeader + 3)); }
this->getLogManager() << Kernel::LogLevel_Trace << "The file has " << m_nEvents << " events\n";
m_eventsPositionBuffer = new uint32_t[m_nEvents * 4];
m_eventsTypeBuffer = new uint16_t[m_nEvents * 2];
//we have to read all the events' position and type
m_file.read(reinterpret_cast<char*>(m_eventsPositionBuffer), m_nEvents * 4);
m_file.read(reinterpret_cast<char*>(m_eventsTypeBuffer), m_nEvents * 2);
m_file.seekg(backupPos);
// Sanity check the events & shift -1 sample
for (uint32_t i = 0; i < m_nEvents; ++i)
{
// GDF Spec v2.51, #33: sample indexing starts from 1, hence here we compensate with -1 as in OV the first sample is in index 0
if (m_eventsPositionBuffer[i] > 0) { m_eventsPositionBuffer[i]--; }
if (m_eventsPositionBuffer[i] >= (m_nDataRecords * m_nSamplesPerRecord))
{
this->getLogManager() << Kernel::LogLevel_Warning << "File has stimulation " << m_eventsTypeBuffer[i] << " at sample count "
<< m_eventsPositionBuffer[i] << " but the file has only " << (m_nDataRecords * m_nSamplesPerRecord)
<< " samples of signal. Stimulation will be dropped.\n";
// Note that with the current design of this box its not possible to keep producing stimulation chunks after the signal has ended, and
// it'd be an openvibe stream convention violation to append the stimulation at t to any chunk where t \notin [chunkStart,chunkEnd].
// Hence drop.
}
}
m_stimulationEncoder->encodeHeader();
boxIO.markOutputAsReadyToSend(GDFReader_StimulationOutput, 0, 0);
}
GDF::CGDFEvent event;
//todo check inclusive/exclusive conditions
while ((m_currentEvent != m_nEvents)
&& m_eventsPositionBuffer[m_currentEvent] >= m_nSentSample - m_signalDesc.m_NSample
&& m_eventsPositionBuffer[m_currentEvent] < m_nSentSample) // In current chunk range
{
//reads an event
event.m_Position = m_eventsPositionBuffer[m_currentEvent];
event.m_Type = m_eventsTypeBuffer[m_currentEvent];
//adds it to the list of events
m_events.push_back(event);
// If input already has an EOF marker, we don't add our own
if (event.m_Type == OVTK_StimulationId_EndOfFile) { m_appendEOF = false; }
m_currentEvent++;
}
//if we just read the last event
if (m_currentEvent == m_nEvents)
{
m_eventsSent = true;
delete[] m_eventsPositionBuffer;
m_eventsPositionBuffer = nullptr;
delete[] m_eventsTypeBuffer;
m_eventsTypeBuffer = nullptr;
}
}
// Send out stims. Logic:
// If we haven't yet sent out all matrices, or we have a pending EOF stim, send a stim chunk
// If we have sent out all matrices and we have a pending EOF, append it
if (!m_matricesSent || m_appendEOF)
{
if (m_matricesSent && m_appendEOF)
{
//creates an end of file event
GDF::CGDFEvent event;
event.m_Position = uint32_t(m_nDataRecords * m_nSamplesPerRecord);
event.m_Type = OVTK_StimulationId_EndOfFile;
//adds it to the list of events
m_events.push_back(event);
m_appendEOF = false;
}
// In OpenViBE, we should always send a stimulus chunk even if it was empty
writeEvents();
m_events.clear();
boxIO.markOutputAsReadyToSend(GDFReader_StimulationOutput, start, end);
}
return true;
}
} // namespace FileIO
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,214 @@
#pragma once
#include "../ovp_defines.h"
#include "../ovp_gdf_helpers.h"
#include <openvibe/ov_all.h>
#include <toolkit/ovtk_all.h>
#include <fstream>
#include <string>
#include <vector>
#define GDFReader_ExperimentInfoOutput 0
#define GDFReader_SignalOutput 1
#define GDFReader_StimulationOutput 2
namespace OpenViBE {
namespace Plugins {
namespace FileIO {
/// <summary> The GDF reader plugin main class. </summary>
/// <seealso cref="Toolkit::TBoxAlgorithm{IBoxAlgorithm}" />
class CGDFFileReader final : public Toolkit::TBoxAlgorithm<IBoxAlgorithm>
{
public:
//Helper structures
class CExperimentInfoHeader
{
public:
uint64_t m_ExperimentID = 0;
std::string m_ExperimentDate;
uint64_t m_SubjectID = 0;
std::string m_SubjectName;
uint64_t m_SubjectAge = 0;
uint64_t m_SubjectSex = 0;
uint64_t m_LaboratoryID = 0;
std::string m_LaboratoryName;
uint64_t m_TechnicianID = 0;
std::string m_TechnicianName;
bool m_ReadyToSend = false;
};
// Used to store information about the signal stream
class CSignalDescription
{
public:
CSignalDescription() = default;
~CSignalDescription() = default;
size_t m_Version = 1;
size_t m_Sampling = 0;
size_t m_NChannel = 0;
size_t m_NSample = 0;
std::vector<std::string> m_ChannelNames;
size_t m_CurrentChannel = 0;
bool m_ReadyToSend = false;
};
CGDFFileReader() { }
void release() override {}
bool initialize() override;
bool uninitialize() override;
uint64_t getClockFrequency() override { return m_clockFrequency; }
bool processClock(Kernel::CMessageClock& msg) override;
bool process() override;
_IsDerivedFromClass_Final_(IBoxAlgorithm, OVP_ClassId_GDFFileReader)
protected:
bool m_errorOccurred = false; // true if an error has occurred while reading the GDF file
//The GDF filename and handle
CString m_filename;
std::ifstream m_file;
size_t m_fileSize = 0;
uint64_t m_header3Length = 0;
float m_fileVersion = -1;
Toolkit::TSignalEncoder<CGDFFileReader>* m_signalEncoder = nullptr;
Toolkit::TExperimentInfoEncoder<CGDFFileReader>* m_xpInfoEncoder = nullptr;
Toolkit::TStimulationEncoder<CGDFFileReader>* m_stimulationEncoder = nullptr;
//Stream information
uint64_t m_samplesPerBuffer = 0; //user defined
//input
uint64_t m_nDataRecords = 0;
double m_durationDataRecord = 0;
uint16_t m_nChannels = 0;
uint32_t m_nSamplesPerRecord = 0; // We only handle the files where it is the same for all the channels
//info about channel's data type in data record
uint32_t* m_channelType = nullptr;
uint16_t* m_channelDataSize = nullptr;
double* m_channelScale = nullptr;
double* m_channelTranslate = nullptr;
//Size of a data record
uint64_t m_dataRecordSize = 0;
//The current data record's data
uint8_t* m_dataRecordBuffer = nullptr;
//pointers to each channel's information in the current data record
uint8_t** m_channelDataInDataRecord = nullptr;
//Output Stream matrix
double* m_matrixBuffer = nullptr;
uint64_t m_matrixBufferSize = 0;
bool m_matricesSent = false;
//Total number of samples sent up to now (used to compute start/end time)
uint32_t m_nSentSample = 0;
//indexes of current data record, channel, and sample
uint64_t m_currentDataRecord = 0;
uint32_t m_currentSampleInDataRecord = 0;
//Events variables
uint8_t m_eventTableMode = 0; // mode of the event table
uint32_t m_nEvents = 0; // number of events in the event table
uint32_t* m_eventsPositionBuffer = nullptr; // pointer on the array of event's positions
uint16_t* m_eventsTypeBuffer = nullptr; // pointer on the array of event's types
std::vector<GDF::CGDFEvent> m_events; //current stimulation block
uint32_t m_currentEvent = 0; // current event in event table
bool m_eventsSent = false; // true if all the events have been sent
bool m_appendEOF = true; // true if the file does contains a recognized EOF marker, then we add our own
uint64_t m_stimulationPerBuffer = 32; //user defined
//helper structures
CExperimentInfoHeader* m_xpInfoHeader = nullptr;
bool m_xpInfoSent = false;
CSignalDescription m_signalDesc;
bool m_signalDescSent = false;
uint64_t m_clockFrequency = 0;
bool m_translateByMinimum = false;
bool readFileHeader();
void writeExperimentInfo() const;
void writeSignalInformation();
void writeEvents();
template <class T>
double gdfTypeToDouble(T val, const uint32_t channel) { return m_channelScale[channel] * double(val) + m_channelTranslate[channel]; }
template <class T>
void gdfTypeBufferToDoubleBuffer(double* out, T* in, const uint64_t size, const uint32_t channel)
{
for (uint64_t i = 0; i < size; ++i) { out[i] = gdfTypeToDouble<T>(in[i], channel); }
}
void gdfBufferToDoubleBuffer(double* out, void* in, const uint64_t size, const uint32_t channel);
};
// template<> double CGDFFileReader::gdfTypeToDouble<float>(float val, uint32_t channel);
// template<> double CGDFFileReader::gdfTypeToDouble<double>(double val, uint32_t channel);
/**
* Description of the GDF Reader plugin
*/
class CGDFFileReaderDesc final : public IBoxAlgorithmDesc
{
public:
void release() override { }
CString getName() const override { return CString("GDF file reader"); }
CString getAuthorName() const override { return CString("Bruno Renier, Jussi T. Lindgren"); }
CString getAuthorCompanyName() const override { return CString("INRIA/IRISA"); }
CString getShortDescription() const override { return CString("GDF file reader"); }
CString getDetailedDescription() const override { return CString("Reads .GDF format files"); }
CString getCategory() const override { return CString("File reading and writing/GDF"); }
CString getVersion() const override { return CString("0.9.1"); }
CString getStockItemName() const override { return CString("gtk-open"); }
CIdentifier getCreatedClass() const override { return OVP_ClassId_GDFFileReader; }
IPluginObject* create() override { return new CGDFFileReader(); }
bool getBoxPrototype(Kernel::IBoxProto& prototype) const override
{
// Adds box outputs
prototype.addOutput("Experiment information", OV_TypeId_ExperimentInfo);
prototype.addOutput("EEG stream", OV_TypeId_Signal);
prototype.addOutput("Stimulations", OV_TypeId_Stimulations);
// Adds settings
prototype.addSetting("Filename", OV_TypeId_Filename, "");
prototype.addSetting("Samples per buffer", OV_TypeId_Integer, "32");
prototype.addSetting("Subtract physical minimum", OV_TypeId_Boolean, "false");
return true;
}
_IsDerivedFromClass_Final_(IBoxAlgorithmDesc, OVP_ClassId_GDFFileReaderDesc)
};
} // namespace FileIO
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,436 @@
#include "ovpCGDFFileWriter.h"
#include <system/ovCMemory.h>
#include <iostream>
#include <cmath>
#include <cfloat>
#include <cstring>
#include <algorithm> // std::min, etc on VS2013
namespace OpenViBE {
namespace Plugins {
namespace FileIO {
void CGDFFileWriter::setChannelCount(const size_t n)
{
m_fixedHeader.m_NSignals = n;
m_fixedHeader.m_NBytesInHeaderRecord = (n + 1) * 256;
m_variableHeader.setChannelCount(n);
m_samples.resize(n);
m_nSamples.resize(n);
}
void CGDFFileWriter::setChannelName(const size_t index, const char* name)
{
const uint32_t size = sizeof(m_variableHeader[index].m_Label);
// initialize label with spaces
memset(m_variableHeader[index].m_Label, ' ', size);
// copy at most size-1 characters, leaving room for 1 space. Note that the label is not NULL terminated.
const uint32_t len = std::min<uint32_t>(strlen(name), size - 1);
memcpy(m_variableHeader[index].m_Label, name, len);
m_variableHeader[index].m_ChannelType = 17; //double
m_variableHeader[index].m_NSamplesInEachRecord = 1;
m_variableHeader[index].m_PhysicalMin = +DBL_MAX; //starting value(to compare later)
m_variableHeader[index].m_PhysicalMax = -DBL_MAX; //starting value(to compare later)
m_variableHeader[index].m_DigitalMin = 0x8000000000000000LL;
m_variableHeader[index].m_DigitalMax = 0x7fffffffffffffffLL;
memcpy(m_variableHeader[index].m_PhysicalDimension, "uV", sizeof("uV"));
}
void CGDFFileWriter::setSampleCountPerBuffer(const size_t n)
{
m_samplesPerChannel = n;
//save the fixed header
if (m_fixedHeader.save(m_file))
{
//save the variable header
if (!m_variableHeader.save(m_file)) { m_error = true; }
}
else { m_error = true; }
if (m_error) { this->getLogManager() << Kernel::LogLevel_Warning << "Error while writing to the output file!\n"; }
}
void CGDFFileWriter::setSamplingRate(const size_t sampling)
{
m_sampling = sampling;
m_fixedHeader.m_DurationDataRecordNum = 1;
m_fixedHeader.m_DurationDataRecordDen = m_sampling;
}
void CGDFFileWriter::setSampleBuffer(const double* buffer)
{
//for each channel
for (size_t j = 0; j < m_fixedHeader.m_NSignals; ++j)
{
for (size_t i = 0; i < m_samplesPerChannel; ++i)
{
//gets a sample value
const double sample = buffer[j * m_samplesPerChannel + i];
//actualize channel's digital min/max
if (fabs(sample) > m_variableHeader[j].m_PhysicalMax)
{
m_variableHeader[j].m_PhysicalMax = fabs(sample + (1 / m_precision));
m_variableHeader[j].m_PhysicalMin = -m_variableHeader[j].m_PhysicalMax;
m_variableHeader[j].m_DigitalMax = int64_t(1 + (ceil(fabs(sample) * m_precision)));
m_variableHeader[j].m_DigitalMin = -m_variableHeader[j].m_DigitalMax;
}
//copy its current sample
m_samples[j].push_back(sample * m_precision);
}
//updates the sample count
m_nSamples[j] += m_samplesPerChannel;
}
// this->getLogManager() << Kernel::LogLevel_Info << "Received up to " << m_nSamples[0] << " samples\n";
//save in the file
saveMatrixData();
//updates the fixed header
m_fixedHeader.m_NDataRecords = m_nSamples[0];
//updates the variable header
if (m_fixedHeader.update(m_file)) { if (!m_variableHeader.update(m_file)) { m_error = true; } }
else { m_error = true; }
if (m_error) { this->getLogManager() << Kernel::LogLevel_Warning << "Error while writing to the output file!\n"; }
}
/*
* Experiment callback
*
*/
void CGDFFileWriter::setExperimentInfo()
{
uint64_t value = m_xpInfoDecoder->getOutputExperimentID();
sprintf(m_fixedHeader.m_RecordingID, "0x%08X", value);
m_fixedHeader.m_RecordingID[10] = ' ';
value = m_xpInfoDecoder->getOutputSubjectID();
sprintf(m_fixedHeader.m_PatientID, "0x%08X ", value);
m_fixedHeader.m_PatientID[11] = ' ';
m_xpInfoDecoder->getOutputSubjectAge();
// TODO using the experiment date, compute the birthdate?
value = m_xpInfoDecoder->getOutputSubjectGender();
switch (value)
{
case OVTK_Value_Sex_Female:
m_fixedHeader.m_PatientID[17] = 'F';
break;
case OVTK_Value_Sex_Male:
m_fixedHeader.m_PatientID[17] = 'M';
break;
case OVTK_Value_Sex_Unknown:
case OVTK_Value_Sex_NotSpecified:
default:
m_fixedHeader.m_PatientID[17] = 'X';
break;
}
m_fixedHeader.m_PatientID[18] = ' ';
value = m_xpInfoDecoder->getOutputLaboratoryID();
m_fixedHeader.m_LaboratoryID = value;
value = m_xpInfoDecoder->getOutputTechnicianID();
m_fixedHeader.m_TechnicianID = value;
CString* subjectName = m_xpInfoDecoder->getOutputSubjectName();
std::string formattedSubjectName((*subjectName).toASCIIString());
formattedSubjectName.replace(formattedSubjectName.begin(), formattedSubjectName.end(), ' ', '_');
sprintf(m_fixedHeader.m_PatientID + 31, "%s", formattedSubjectName.c_str());
if (!m_fixedHeader.save(m_file))
{
m_error = true;
this->getLogManager() << Kernel::LogLevel_Warning << "Error while writing to the output file!\n";
}
}
void CGDFFileWriter::setStimulation(const uint64_t identifier, const uint64_t date) { m_events.push_back(std::pair<uint64_t, uint64_t>(identifier, date)); }
bool CGDFFileWriter::initialize()
{
m_signalDecoder = new Toolkit::TSignalDecoder<CGDFFileWriter>;
m_xpInfoDecoder = new Toolkit::TExperimentInfoDecoder<CGDFFileWriter>;
m_stimulationDecoder = new Toolkit::TStimulationDecoder<CGDFFileWriter>;
m_signalDecoder->initialize(*this, 1);
m_xpInfoDecoder->initialize(*this, 0);
m_stimulationDecoder->initialize(*this, 2);
// Parses box settings to find filename
m_filename = FSettingValueAutoCast(*this->getBoxAlgorithmContext(), 0);
m_precision = 1000.0;
return true;
}
bool CGDFFileWriter::uninitialize()
{
// See that no event is outside the signal by padding the signal with zeroes if necessary
padByEvents();
//If the file is not open, that mean that the box is muted. If the file is not open because of a bug, it should have already been notify
if (m_file.is_open())
{
//update the fixed header
if (!m_nSamples.empty())
{
m_fixedHeader.m_NDataRecords = m_nSamples[0];
this->getLogManager() << Kernel::LogLevel_Trace << "Saving " << m_nSamples[0] << " data records\n";
}
if (m_fixedHeader.update(m_file))
{
//To save the Physical/Digital max/min values
if (!m_variableHeader.update(m_file)) { m_error = true; }
}
else { m_error = true; }
//write events
if (!m_events.empty())
{
this->getLogManager() << Kernel::LogLevel_Trace << "Saving " << m_events.size() << " events\n";
saveEvents();
}
if (m_error) { this->getLogManager() << Kernel::LogLevel_Warning << "Error while writing to the output file!\n"; }
m_file.close();
}
if (m_signalDecoder)
{
m_signalDecoder->uninitialize();
delete m_signalDecoder;
}
if (m_xpInfoDecoder)
{
m_xpInfoDecoder->uninitialize();
delete m_xpInfoDecoder;
}
if (m_stimulationDecoder)
{
m_stimulationDecoder->uninitialize();
delete m_stimulationDecoder;
}
return true;
}
bool CGDFFileWriter::processInput(const size_t /*index*/)
{
if (m_error) { return false; }
getBoxAlgorithmContext()->markAlgorithmAsReadyToProcess();
return true;
}
void CGDFFileWriter::saveMatrixData()
{
if (!m_file.is_open())
{
m_file.open(m_filename, std::ios::binary | std::ios::trunc);
if (!m_file.good())
{
m_error = true;
this->getLogManager() << Kernel::LogLevel_ImportantWarning << "Could not open file [" << m_filename << "]\n";
return;
}
}
size_t j;
m_file.seekp(0, std::ios::end);
//to "convert" (if needed) the double in little endian format
uint8_t littleEndianBuffer[8];
for (size_t i = 0; i < m_samples[0].size(); ++i)
{
for (j = 0; j < m_samples.size(); ++j)
{
const double v = m_samples[j][i];
System::Memory::hostToLittleEndian(v, littleEndianBuffer);
m_file.write(reinterpret_cast<char*>(littleEndianBuffer), sizeof(littleEndianBuffer));
}
}
for (j = 0; j < m_samples.size(); ++j) { m_samples[j].clear(); }
}
void CGDFFileWriter::padByEvents()
{
if (!m_file.is_open())
{
m_file.open(m_filename, std::ios::binary | std::ios::trunc);
if (!m_file.good())
{
m_error = true;
this->getLogManager() << Kernel::LogLevel_ImportantWarning << "Could not open file [" << m_filename << "]\n";
return;
}
}
uint32_t maxPos = 0;
for (const auto& e : m_events)
{
// GDF Spec v2.51, #33: sample indexing starts from 1, hence +1
const uint32_t pos = uint32_t(CTime(e.second).toSampleCount(m_sampling) + 1);
if (pos > m_nSamples[0] + 1)
{
this->getLogManager() << Kernel::LogLevel_Warning << "Stimulation " << uint16_t(e.first & 0xFFFF) << " will be written at " << pos
<< " (after last sample at " << m_nSamples[0] + 1 << "), padding the signal\n";
maxPos = std::max<uint32_t>(pos, maxPos);
}
}
if (maxPos > 0)
{
const CMatrix* oMatrix = m_signalDecoder->getOutputMatrix();
CMatrix zeros;
zeros.copyDescription(*oMatrix);
while (maxPos >= m_nSamples[0] + 1) { setSampleBuffer(zeros.getBuffer()); }
}
}
void CGDFFileWriter::saveEvents()
{
if (!m_file.is_open())
{
m_file.open(m_filename, std::ios::binary | std::ios::trunc);
if (!m_file.good())
{
m_error = true;
this->getLogManager() << Kernel::LogLevel_ImportantWarning << "Could not open file [" << m_filename << "]\n";
return;
}
}
m_file.seekp(0, std::ios::end);
//event mode
m_file.put(1);
//sample rate associated with event positions
m_file.put(0);
m_file.put(0);
m_file.put(0);
//number of events
uint8_t littleEndianBuffer[sizeof(size_t)]; // needs to be the size of the type returned by m_events.size()
System::Memory::hostToLittleEndian(m_events.size(), littleEndianBuffer);
m_file.write(reinterpret_cast<char*>(littleEndianBuffer), sizeof(size_t));
// write event positions
for (const auto& e : m_events)
{
// GDF Spec v2.51, #33: sample indexing starts from 1, hence +1
const uint32_t pos = uint32_t(CTime(e.second).toSampleCount(m_sampling) + 1);
System::Memory::hostToLittleEndian(pos, littleEndianBuffer);
m_file.write(reinterpret_cast<char*>(littleEndianBuffer), sizeof(uint32_t));
}
// write event types
for (const auto& e : m_events)
{
//Force to use only 16bits stimulations IDs
const uint16_t type = uint16_t(e.first & 0xFFFF);
System::Memory::hostToLittleEndian(type, littleEndianBuffer);
m_file.write(reinterpret_cast<char*>(littleEndianBuffer), sizeof(uint16_t));
}
}
bool CGDFFileWriter::process()
{
if (!m_file.is_open())
{
m_file.open(m_filename, std::ios::binary | std::ios::trunc);
if (!m_file.good())
{
m_error = true;
this->getLogManager() << Kernel::LogLevel_ImportantWarning << "Could not open file [" << m_filename << "]\n";
return false;
}
}
Kernel::IBoxIO& boxIO = this->getDynamicBoxContext();
//Experiment information
for (size_t i = 0; i < boxIO.getInputChunkCount(0); ++i)
{
m_xpInfoDecoder->decode(i);
if (m_xpInfoDecoder->isHeaderReceived()) { setExperimentInfo(); }
boxIO.markInputAsDeprecated(0, i);
}
//Signal
for (size_t i = 0; i < boxIO.getInputChunkCount(1); ++i)
{
m_signalDecoder->decode(i);
if (m_signalDecoder->isHeaderReceived())
{
CMatrix* oMatrix = m_signalDecoder->getOutputMatrix();
const size_t nChannel = oMatrix->getDimensionSize(0);
setChannelCount(nChannel);
for (size_t c = 0; c < nChannel; ++c) { setChannelName(c, oMatrix->getDimensionLabel(0, c)); }
setSamplingRate(size_t(m_signalDecoder->getOutputSamplingRate()));
setSampleCountPerBuffer(oMatrix->getDimensionSize(1));
}
if (m_signalDecoder->isBufferReceived())
{
CMatrix* oMatrix = m_signalDecoder->getOutputMatrix();
double* buffer = oMatrix->getBuffer();
setSampleBuffer(buffer);
}
boxIO.markInputAsDeprecated(1, i);
}
//Stimulations
for (uint32_t i = 0; i < boxIO.getInputChunkCount(2); ++i)
{
m_stimulationDecoder->decode(i);
if (m_stimulationDecoder->isBufferReceived())
{
IStimulationSet* stimulationSet = m_stimulationDecoder->getOutputStimulationSet();
for (size_t j = 0; j < stimulationSet->getStimulationCount(); ++j)
{
setStimulation(stimulationSet->getStimulationIdentifier(j), stimulationSet->getStimulationDate(j));
}
}
boxIO.markInputAsDeprecated(2, i);
}
return true;
}
} // namespace FileIO
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,113 @@
#pragma once
#include "../ovp_defines.h"
#include "../ovp_gdf_helpers.h"
#include <openvibe/ov_all.h>
#include <toolkit/ovtk_all.h>
#include <fstream>
#include <vector>
namespace OpenViBE {
namespace Plugins {
namespace FileIO {
/**
* The plugin's main class
*
*/
class CGDFFileWriter final : public Toolkit::TBoxAlgorithm<IBoxAlgorithm>
{
public:
CGDFFileWriter() { }
void release() override { delete this; }
bool initialize() override;
bool uninitialize() override;
bool processInput(const size_t index) override;
bool process() override;
_IsDerivedFromClass_Final_(IBoxAlgorithm, OVP_ClassId_GDFFileWriter)
protected:
void setChannelCount(size_t n);
void setChannelName(size_t index, const char* name);
void setSampleCountPerBuffer(size_t n);
void setSamplingRate(size_t sampling);
void setSampleBuffer(const double* buffer);
void setExperimentInfo();
void setStimulation(const uint64_t identifier, const uint64_t date);
void saveMatrixData();
void saveEvents();
void padByEvents();
std::ofstream m_file;
CString m_filename;
Toolkit::TSignalDecoder<CGDFFileWriter>* m_signalDecoder = nullptr;
Toolkit::TExperimentInfoDecoder<CGDFFileWriter>* m_xpInfoDecoder = nullptr;
Toolkit::TStimulationDecoder<CGDFFileWriter>* m_stimulationDecoder = nullptr;
//GDF structures
GDF::CFixedGDF1Header m_fixedHeader;
GDF::CVariableGDF1Header m_variableHeader;
std::vector<std::vector<double>> m_samples;
std::vector<int64_t> m_nSamples;
size_t m_samplesPerChannel = 0;
size_t m_sampling = 0;
std::vector<std::pair<uint64_t, uint64_t>> m_events;
bool m_error = false;
double m_precision = 0; // because of GDF writing problem (no scaling)
};
/**
* Plugin's description
*/
class CGDFFileWriterDesc final : public IBoxAlgorithmDesc
{
public:
CString getName() const override { return CString("GDF file writer"); }
CString getAuthorName() const override { return CString("Yann Renard"); }
CString getAuthorCompanyName() const override { return CString("INRIA/IRISA"); }
CString getShortDescription() const override { return CString("This algorithm records on disk what comes from a specific output"); }
CString getDetailedDescription() const override
{
return CString("This algorithm dumps on disk a stream from a specific output in the standard GDF file format");
}
CString getCategory() const override { return CString("File reading and writing/GDF"); }
CString getVersion() const override { return CString("0.6"); }
CString getStockItemName() const override { return CString("gtk-save"); }
void release() override { }
CIdentifier getCreatedClass() const override { return OVP_ClassId_GDFFileWriter; }
IPluginObject* create() override { return new CGDFFileWriter(); }
bool getBoxPrototype(Kernel::IBoxProto& prototype) const override
{
// Adds box inputs //swap order of the first two
prototype.addInput("Experiment information", OV_TypeId_ExperimentInfo);
prototype.addInput("Signal", OV_TypeId_Signal);
prototype.addInput("Stimulation", OV_TypeId_Stimulations);
// Adds box settings
prototype.addSetting("Filename", OV_TypeId_Filename, "record-[$core{date}-$core{time}].gdf");
return true;
}
_IsDerivedFromClass_Final_(IBoxAlgorithmDesc, OVP_ClassId_GDFFileWriterDesc)
};
} // namespace FileIO
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,95 @@
#pragma once
// Boxes
//---------------------------------------------------------------------------------------------------
#define OVP_ClassId_GDFFileReader OpenViBE::CIdentifier(0x3EEB1264, 0x4EDFBD9A)
#define OVP_ClassId_GDFFileReaderDesc OpenViBE::CIdentifier(0x2E2543C1, 0x47E3739E)
#define OVP_ClassId_GDFFileWriter OpenViBE::CIdentifier(0x1E7B2155, 0x107289CE)
#define OVP_ClassId_GDFFileWriterDesc OpenViBE::CIdentifier(0x375BF8EC, 0x023ACEEB)
#define OVP_ClassId_GenericStreamReader OpenViBE::CIdentifier(0x0B1D880D, 0x02A17229)
#define OVP_ClassId_GenericStreamReaderDesc OpenViBE::CIdentifier(0x1E8AAB1A, 0x085D72F6)
#define OVP_ClassId_GenericStreamWriter OpenViBE::CIdentifier(0x78EA86B0, 0x2933E255)
#define OVP_ClassId_GenericStreamWriterDesc OpenViBE::CIdentifier(0x02817C77, 0x77FE3D6A)
#define OVP_ClassId_BCICompetitionIIIbReader OpenViBE::CIdentifier(0x0085B814, 0x6E45DA7E)
#define OVP_ClassId_BCICompetitionIIIbReaderDesc OpenViBE::CIdentifier(0x00CC1B39, 0x206A17AE)
#define OVP_ClassId_BoxAlgorithm_CSVFileWriter OpenViBE::CIdentifier(0x2C9312F1, 0x2D6613E5)
#define OVP_ClassId_BoxAlgorithm_CSVFileWriterDesc OpenViBE::CIdentifier(0x65075FF7, 0x2B555E97)
#define OVP_ClassId_BoxAlgorithm_CSVFileReader OpenViBE::CIdentifier(0x641D0717, 0x02884107)
#define OVP_ClassId_BoxAlgorithm_CSVFileReaderDesc OpenViBE::CIdentifier(0x193F22E9, 0x26A67233)
// Brainamp File
//---------------------------------------------------------------------------------------------------
#define OVP_ClassId_Algorithm_BrainampFileReader OpenViBE::CIdentifier(0x2533718F, 0x07AB3AF5)
#define OVP_ClassId_Algorithm_BrainampFileReaderDesc OpenViBE::CIdentifier(0x2C5B2D6C, 0x242253CF)
#define OVP_ClassId_BoxAlgorithm_BrainampFileReader OpenViBE::CIdentifier(0x61894B28, 0x185E57D2)
#define OVP_ClassId_BoxAlgorithm_BrainampFileReaderDesc OpenViBE::CIdentifier(0x77C650B0, 0x41386F47)
#define OVP_Algorithm_BrainampFileReader_InputParameterId_Filename OpenViBE::CIdentifier(0x000C486C, 0x477C80B9)
#define OVP_Algorithm_BrainampFileReader_InputParameterId_EpochDuration OpenViBE::CIdentifier(0x002AEE72, 0x288D489E)
#define OVP_Algorithm_BrainampFileReader_InputParameterId_SeekTime OpenViBE::CIdentifier(0x00627156, 0x55790CAE)
#define OVP_Algorithm_BrainampFileReader_InputParameterId_ConvertStimuli OpenViBE::CIdentifier(0x537445DC, 0x6C792A96)
#define OVP_Algorithm_BrainampFileReader_InputParameterId_MarkerDictionaryFilename OpenViBE::CIdentifier(0x1F044ABB, 0x5AACf062)
#define OVP_Algorithm_BrainampFileReader_OutputParameterId_CurrentStartTime OpenViBE::CIdentifier(0x003CE019, 0x3DADE050)
#define OVP_Algorithm_BrainampFileReader_OutputParameterId_CurrentEndTime OpenViBE::CIdentifier(0x3B87DE16, 0xB8EFB2FB)
#define OVP_Algorithm_BrainampFileReader_OutputParameterId_Sampling OpenViBE::CIdentifier(0x00D3CABB, 0x339326C2)
#define OVP_Algorithm_BrainampFileReader_OutputParameterId_SignalMatrix OpenViBE::CIdentifier(0x0073A91A, 0x6D1D3D26)
#define OVP_Algorithm_BrainampFileReader_OutputParameterId_Stimulations OpenViBE::CIdentifier(0x008F7C49, 0x6ED710A9)
#define OVP_Algorithm_BrainampFileReader_InputTriggerId_Open OpenViBE::CIdentifier(0x00BCF286, 0x6F5FA2F4)
#define OVP_Algorithm_BrainampFileReader_InputTriggerId_Seek OpenViBE::CIdentifier(0x00C3ACAB, 0x4E3DE014)
#define OVP_Algorithm_BrainampFileReader_InputTriggerId_Next OpenViBE::CIdentifier(0x003CD062, 0x739F973E)
#define OVP_Algorithm_BrainampFileReader_InputTriggerId_Close OpenViBE::CIdentifier(0x003462EA, 0x031FB8FA)
#define OVP_Algorithm_BrainampFileReader_OutputTriggerId_Error OpenViBE::CIdentifier(0x00B1E3DF, 0x3ABC6C5A)
#define OVP_Algorithm_BrainampFileReader_OutputTriggerId_DataProduced OpenViBE::CIdentifier(0x00E7D5F9, 0x1471AFF2)
#define OVP_ClassId_BoxAlgorithm_BrainampFileWriter OpenViBE::CIdentifier(0xABACC29F, 0xD5A0E5A8)
#define OVP_ClassId_BoxAlgorithm_BrainampFileWriterDesc OpenViBE::CIdentifier(0x6347C864, 0x8483E2DF)
// OV Matrix File Reader
//---------------------------------------------------------------------------------------------------
#define OVP_ClassId_Algorithm_OVMatrixFileReader OpenViBE::CIdentifier(0x10661A33, 0x0B0F44A7)
#define OVP_ClassId_Algorithm_OVMatrixFileReaderDesc OpenViBE::CIdentifier(0x0E873B5E, 0x0A287FCB)
#define OVP_Algorithm_OVMatrixFileReader_InputParameterId_Filename OpenViBE::CIdentifier(0x28F87B29, 0x0B09737E)
#define OVP_Algorithm_OVMatrixFileReader_OutputParameterId_Matrix OpenViBE::CIdentifier(0x2F9521E0, 0x027D789F)
#define OVP_Algorithm_OVMatrixFileReader_InputTriggerId_Open OpenViBE::CIdentifier(0x2F996376, 0x2A942485)
#define OVP_Algorithm_OVMatrixFileReader_InputTriggerId_Load OpenViBE::CIdentifier(0x22841807, 0x102D681C)
#define OVP_Algorithm_OVMatrixFileReader_InputTriggerId_Close OpenViBE::CIdentifier(0x7FDE77DA, 0x384A0B3D)
#define OVP_Algorithm_OVMatrixFileReader_OutputTriggerId_Error OpenViBE::CIdentifier(0x6D4F2F4B, 0x05EC6CB9)
#define OVP_Algorithm_OVMatrixFileReader_OutputTriggerId_DataProduced OpenViBE::CIdentifier(0x76F46051, 0x003B6FE8)
#define OVP_ClassId_Algorithm_OVMatrixFileWriter OpenViBE::CIdentifier(0x739158FC, 0x1E8240CC)
#define OVP_ClassId_Algorithm_OVMatrixFileWriterDesc OpenViBE::CIdentifier(0x44CF6DD0, 0x329D47F9)
#define OVP_Algorithm_OVMatrixFileWriter_InputParameterId_Filename OpenViBE::CIdentifier(0x330D2D0B, 0x175271E6)
#define OVP_Algorithm_OVMatrixFileWriter_InputParameterId_Matrix OpenViBE::CIdentifier(0x6F6402EE, 0x493044F3)
// ElectrodeLocalisationFileReader
//---------------------------------------------------------------------------------------------------
#define OVP_ClassId_BoxAlgorithm_ElectrodeLocalisationFileReader OpenViBE::CIdentifier(0x40704155, 0x19C50E8F)
#define OVP_ClassId_BoxAlgorithm_ElectrodeLocalisationFileReaderDesc OpenViBE::CIdentifier(0x4796613F, 0x653A48D5)
// Global defines
//---------------------------------------------------------------------------------------------------
#ifdef TARGET_HAS_ThirdPartyOpenViBEPluginsGlobalDefines
#include "ovp_global_defines.h"
#endif // TARGET_HAS_ThirdPartyOpenViBEPluginsGlobalDefines
#define OV_AttributeId_Box_FlagIsUnstable OpenViBE::CIdentifier(0x666FFFFF, 0x666FFFFF)
#define OVP_NodeId_OpenViBEStream_Header EBML::CIdentifier(0xF59505AB, 0x3684C8D8)
#define OVP_NodeId_OpenViBEStream_Header_Compression EBML::CIdentifier(0x40358769, 0x166380D1)
#define OVP_NodeId_OpenViBEStream_Header_StreamType EBML::CIdentifier(0x732EC1D1, 0xFE904087)
#define OVP_NodeId_OpenViBEStream_Header_ChannelType OVP_NodeId_OpenViBEStream_Header_StreamType // deprecated old name
#define OVP_NodeId_OpenViBEStream_Buffer EBML::CIdentifier(0x2E60AD18, 0x87A29BDF)
#define OVP_NodeId_OpenViBEStream_Buffer_StreamIndex EBML::CIdentifier(0x30A56D8A, 0xB9C12238)
#define OVP_NodeId_OpenViBEStream_Buffer_ChannelIndex OVP_NodeId_OpenViBEStream_Buffer_StreamIndex // deprecated old name
#define OVP_NodeId_OpenViBEStream_Buffer_StartTime EBML::CIdentifier(0x093E6A0A, 0xC5A9467B)
#define OVP_NodeId_OpenViBEStream_Buffer_EndTime EBML::CIdentifier(0x8B5CCCD9, 0xC5024F29)
#define OVP_NodeId_OpenViBEStream_Buffer_Content EBML::CIdentifier(0x8D4B0BE8, 0x7051265C)
@@ -0,0 +1,489 @@
#include "ovp_gdf_helpers.h"
#include <system/ovCMemory.h>
#include <iostream>
#include <cstring>
namespace OpenViBE {
namespace Plugins {
namespace FileIO {
GDF::CFixedGDF1Header::CFixedGDF1Header()
{
memset(m_PatientID, ' ', sizeof(m_PatientID));
memset(m_RecordingID, ' ', sizeof(m_RecordingID));
memset(m_StartDateAndTimeOfRecording, ' ', sizeof(m_StartDateAndTimeOfRecording));
memset(m_ReservedSerialNumber, ' ', sizeof(m_ReservedSerialNumber));
//set version number
memcpy(m_VersionID, "GDF 1.25", 8);
//subjectId unknown by default
m_PatientID[0] = 'X';
//sex unknown by default
m_PatientID[17] = 'X';
}
bool GDF::CFixedGDF1Header::read(std::ifstream& file)
{
uint8_t buffer[72];
file.read(m_PatientID, sizeof(m_PatientID));
file.read(m_RecordingID, sizeof(m_RecordingID));
file.read(m_StartDateAndTimeOfRecording, sizeof(m_StartDateAndTimeOfRecording));
file.read(reinterpret_cast<char*>(buffer), 72);
System::Memory::littleEndianToHost(buffer, reinterpret_cast<uint64_t*>(&m_NBytesInHeaderRecord));
System::Memory::littleEndianToHost(buffer + 8, &m_EquipmentProviderID);
System::Memory::littleEndianToHost(buffer + 16, &m_LaboratoryID);
System::Memory::littleEndianToHost(buffer + 24, &m_TechnicianID);
System::Memory::littleEndianToHost(buffer + 52, reinterpret_cast<uint64_t*>(&m_NDataRecords));
System::Memory::littleEndianToHost(buffer + 60, &m_DurationDataRecordNum);
System::Memory::littleEndianToHost(buffer + 64, &m_DurationDataRecordDen);
System::Memory::littleEndianToHost(buffer + 68, &m_NSignals);
return !file.bad();
}
bool GDF::CFixedGDF1Header::save(std::ofstream& file)
{
if (file.is_open())
{
uint8_t buffer[sizeof(uint64_t)];
file.seekp(0, std::ios::beg);
file.write(m_VersionID, sizeof(m_VersionID));
file.write(m_PatientID, sizeof(m_PatientID));
file.write(m_RecordingID, sizeof(m_RecordingID));
file.write(m_StartDateAndTimeOfRecording, sizeof(m_StartDateAndTimeOfRecording));
System::Memory::hostToLittleEndian(uint64_t(m_NBytesInHeaderRecord), buffer);
file.write(reinterpret_cast<char*>(buffer), sizeof(int64_t));
System::Memory::hostToLittleEndian(uint64_t(m_EquipmentProviderID), buffer);
file.write(reinterpret_cast<char*>(buffer), sizeof(uint64_t));
System::Memory::hostToLittleEndian(uint64_t(m_LaboratoryID), buffer);
file.write(reinterpret_cast<char*>(buffer), sizeof(uint64_t));
System::Memory::hostToLittleEndian(uint64_t(m_TechnicianID), buffer);
file.write(reinterpret_cast<char*>(buffer), sizeof(uint64_t));
file.write(m_ReservedSerialNumber, sizeof(m_ReservedSerialNumber));
System::Memory::hostToLittleEndian(uint64_t(m_NDataRecords), buffer);
file.write(reinterpret_cast<char*>(buffer), sizeof(int64_t));
System::Memory::hostToLittleEndian(m_DurationDataRecordNum, buffer);
file.write(reinterpret_cast<char*>(buffer), sizeof(uint32_t));
System::Memory::hostToLittleEndian(m_DurationDataRecordDen, buffer);
file.write(reinterpret_cast<char*>(buffer), sizeof(uint32_t));
System::Memory::hostToLittleEndian(m_NSignals, buffer);
file.write(reinterpret_cast<char*>(buffer), sizeof(uint32_t));
if (file.bad())
{
//cout<<"Error while writing to the output file"<<endl;
return false;
}
return true;
}
return false;
}
bool GDF::CFixedGDF1Header::update(std::ofstream& file)
{
if (file.is_open())
{
uint8_t buffer[sizeof(uint64_t)];
const uint64_t backupPos = file.tellp();
file.seekp(236, std::ios::beg);
System::Memory::hostToLittleEndian(uint64_t(m_NDataRecords), buffer);
file.write(reinterpret_cast<char*>(buffer), sizeof(int64_t));
file.seekp(std::streamsize(backupPos), std::ios::beg);
if (file.bad())
{
//cout<<"Error while writing to the output file"<<endl;
return false;
}
return true;
}
return false;
}
std::string GDF::CFixedGDF1Header::getSubjectName()
{
// extracts the PID and Patient name from the m_sPatientId
char* token = strtok(m_PatientID, " ");
if (token)
{
//The PId is not a numerical value in GDF, it is useless for us
token = strtok(nullptr, " ");
}
if (token) { return token; }
return NO_VALUE_S;
}
uint64_t GDF::CFixedGDF1Header::getLaboratoryID()
{
uint8_t* temp = reinterpret_cast<uint8_t*>(&m_LaboratoryID);
bool blank = true;
for (int i = 0; i < 8 && blank; ++i) { if (temp[i] != 0x20) { blank = false; } }
if (blank) { return NO_VALUE_I; }
return m_LaboratoryID;
}
uint64_t GDF::CFixedGDF1Header::getTechnicianID()
{
uint8_t* temp = reinterpret_cast<uint8_t*>(&m_TechnicianID);
bool blank = true;
for (int i = 0; i < 8 && blank; ++i) { if (temp[i] != 0x20) { blank = false; } }
if (blank) { return NO_VALUE_I; }
return m_TechnicianID;
}
GDF::CFixedGDF2Header::CFixedGDF2Header()
{
memset(&m_VersionID, 0, sizeof(m_VersionID));
memset(&m_PatientID, 0, sizeof(m_PatientID));
memset(&m_Reserved, 0, sizeof(m_Reserved));
memset(&m_RecordingID, 0, sizeof(m_RecordingID));
memset(&m_RecordingLocation, 0, sizeof(m_RecordingLocation));
memset(&m_StartDateAndTimeOfRecording, 0, sizeof(m_StartDateAndTimeOfRecording));
memset(&m_Birthday, 0, sizeof(m_Birthday));
memset(&m_Reserved2, 0, sizeof(m_Reserved2));
memset(&m_IPAdress, 0, sizeof(m_IPAdress));
memset(&m_HeadSize, 0, sizeof(m_HeadSize));
memset(&m_PositionReferenceElectrode, 0, sizeof(m_PositionReferenceElectrode));
memset(&m_GroundElectrode, 0, sizeof(m_GroundElectrode));
}
bool GDF::CFixedGDF2Header::read(std::ifstream& oFile)
{
uint8_t buffer[104];
oFile.read(m_PatientID, 66);
oFile.read(reinterpret_cast<char*>(buffer), 14);
m_HealthInformation = buffer[10];
m_Weight = buffer[11];
m_Height = buffer[12];
m_SubjectInfo = buffer[13];
oFile.read(m_RecordingID, 64);
oFile.read(reinterpret_cast<char*>(buffer), 102);
for (int i = 0; i < 4; ++i) { System::Memory::littleEndianToHost(buffer + i * sizeof(uint32_t), &m_RecordingLocation[i]); }
System::Memory::littleEndianToHost(buffer + 16, &m_StartDateAndTimeOfRecording[0]);
System::Memory::littleEndianToHost(buffer + 20, &m_StartDateAndTimeOfRecording[1]);
System::Memory::littleEndianToHost(buffer + 24, &m_Birthday[0]);
System::Memory::littleEndianToHost(buffer + 28, &m_Birthday[1]);
System::Memory::littleEndianToHost(buffer + 32, &m_NBlocksInHeader);
// +34 patient classification 6 bytes
System::Memory::littleEndianToHost(buffer + 40, &m_EquipmentProviderID);
for (int i = 0; i < 6; ++i) { m_IPAdress[i] = buffer[48 + i]; }
for (int i = 0; i < 3; ++i)
{
System::Memory::littleEndianToHost(buffer + 54 + i * sizeof(uint16_t), &m_HeadSize[i]);
System::Memory::littleEndianToHost(buffer + 60 + i * sizeof(float), &m_PositionReferenceElectrode[i]);
System::Memory::littleEndianToHost(buffer + 72 + i * sizeof(float), &m_GroundElectrode[i]);
}
System::Memory::littleEndianToHost(buffer + 84, reinterpret_cast<uint64_t*>(&m_NDataRecords));
System::Memory::littleEndianToHost(buffer + 92, &m_DurationRecordNum);
System::Memory::littleEndianToHost(buffer + 96, &m_DurationDataRecordDen);
System::Memory::littleEndianToHost(buffer + 100, &m_NSignals);
return !oFile.bad();
}
bool GDF::CFixedGDF2Header::save(std::ofstream& /*file*/)
{
return false;
//TODO complete
/*
uint8_t buffer[sizeof(uint64_t)];
file.seekp(0,std::ios::beg);
file<<m_VersionId<<m_PatientID;
*/
}
std::string GDF::CFixedGDF2Header::getSubjectName()
{
// extracts the PID and Patient name from the m_sPatientId
char* token = strtok(m_PatientID, " ");
if (token)
{
//The PId is not a numerical value in GDF, it is useless for us
token = strtok(nullptr, " ");
}
if (token) { return token; }
return NO_VALUE_S;
}
std::string GDF::CFixedGDF2Header::getExperimentDate()
{
//computes the experiment date
//uint64_t tempDate = *(reinterpret_cast<uint64_t*>(m_startDateAndTimeOfRecording));
//time_t startDateAndTimeOfRecordingInSeconds = ((tempDate/2^32) - 719529) * (3600*24);
//tm * startDateAndTimeOfRecording = gmtime(&startDateAndTimeOfRecordingInSeconds);
//TODO check how date is coded in openvibe Date not good?
//(l_sStartDateAndTimeOfRecording->mon+1)<<8 + (l_sStartDateAndTimeOfRecording->day)
return NO_VALUE_S;
}
//VARIABLE HEADER
GDF::CVariableGDF1Header::CVariableGDF1HeaderPerChannel::CVariableGDF1HeaderPerChannel()
{
memset(m_Label, ' ', sizeof(m_Label));
memset(m_TranducerType, ' ', sizeof(m_TranducerType));
memset(m_PhysicalDimension, ' ', sizeof(m_PhysicalDimension));
memset(m_PreFiltering, ' ', sizeof(m_PreFiltering));
memset(m_Reserved, ' ', sizeof(m_Reserved));
}
bool GDF::CVariableGDF1Header::update(std::ofstream& file)
{
if (file.is_open())
{
uint8_t buffer[sizeof(uint64_t)];
const uint64_t backupPos = file.tellp();
const uint32_t nChannel = m_VariableHeaders.size();
file.seekp(0x100 + 104 * nChannel, std::ios::beg);
for (const auto& i : m_VariableHeaders)
{
System::Memory::hostToLittleEndian(i.m_PhysicalMin, buffer);
file.write(reinterpret_cast<char*>(buffer), sizeof(double));
}
for (const auto& i : m_VariableHeaders)
{
System::Memory::hostToLittleEndian(i.m_PhysicalMax, buffer);
file.write(reinterpret_cast<char*>(buffer), sizeof(double));
}
for (const auto& i : m_VariableHeaders)
{
System::Memory::hostToLittleEndian(uint64_t(i.m_DigitalMin), buffer);
file.write(reinterpret_cast<char*>(buffer), sizeof(int64_t));
}
for (const auto& i : m_VariableHeaders)
{
System::Memory::hostToLittleEndian(uint64_t(i.m_DigitalMax), buffer);
file.write(reinterpret_cast<char*>(buffer), sizeof(int64_t));
}
file.seekp(std::streamsize(backupPos), std::ios::beg);
if (file.bad())
{
//cout<<"Error while writing to the output file"<<endl;
return false;
}
return true;
}
return false;
}
bool GDF::CVariableGDF1Header::save(std::ofstream& file)
{
if (file.is_open())
{
uint8_t buffer[sizeof(uint64_t)];
file.seekp(0x100, std::ios::beg);
for (const auto& i : m_VariableHeaders) { file.write(i.m_Label, sizeof(i.m_Label)); }
for (const auto& i : m_VariableHeaders) { file.write(i.m_TranducerType, sizeof(i.m_TranducerType)); }
for (const auto& i : m_VariableHeaders) { file.write(i.m_PhysicalDimension, sizeof(i.m_PhysicalDimension)); }
for (const auto& i : m_VariableHeaders)
{
System::Memory::hostToLittleEndian(i.m_PhysicalMin, buffer);
file.write(reinterpret_cast<char*>(buffer), sizeof(double));
}
for (const auto& i : m_VariableHeaders)
{
System::Memory::hostToLittleEndian(i.m_PhysicalMax, buffer);
file.write(reinterpret_cast<char*>(buffer), sizeof(double));
}
for (const auto& i : m_VariableHeaders)
{
System::Memory::hostToLittleEndian(uint64_t(i.m_DigitalMin), buffer);
file.write(reinterpret_cast<char*>(buffer), sizeof(int64_t));
}
for (const auto& i : m_VariableHeaders)
{
System::Memory::hostToLittleEndian(uint64_t(i.m_DigitalMax), buffer);
file.write(reinterpret_cast<char*>(buffer), sizeof(int64_t));
}
for (const auto& i : m_VariableHeaders) { file.write(i.m_PreFiltering, sizeof(i.m_PreFiltering)); }
for (const auto& i : m_VariableHeaders)
{
System::Memory::hostToLittleEndian(i.m_NSamplesInEachRecord, buffer);
file.write(reinterpret_cast<char*>(buffer), sizeof(uint32_t));
}
for (const auto& i : m_VariableHeaders)
{
System::Memory::hostToLittleEndian(i.m_ChannelType, buffer);
file.write(reinterpret_cast<char*>(buffer), sizeof(uint32_t));
}
for (const auto& i : m_VariableHeaders) { file.write(i.m_Reserved, sizeof(i.m_Reserved)); }
if (file.bad())
{
//cout<<"Error while writing to the output file"<<endl;
return false;
}
return true;
}
return false;
}
//HELPER METHODS
uint16_t GDF::GDFDataSize(const uint32_t channelType)
{
switch (channelType)
{
case ChannelType_int8_t: return 1;
case ChannelType_uint8_t: return 1;
case ChannelType_int16_t: return 2;
case ChannelType_uint16_t: return 2;
case ChannelType_int32_t: return 4;
case ChannelType_uint32_t: return 4;
case ChannelType_int64_t: return 8;
case ChannelType_uint64_t: return 8;
case ChannelType_float: return 4;
case ChannelType_double: return 8;
case ChannelType_float128: return 16;
case ChannelType_int24: return 3;
case ChannelType_uint24: return 3;
default: return 0;
}
}
bool GDF::CFixedGDF251Header::read(std::ifstream& oFile)
{
// Due to issues with data alignment and platform-specific padding, we can't trivially read the struct to memory with one go.
oFile.seekg(0, std::ios_base::beg);
uint8_t buffer[256];
oFile.read(reinterpret_cast<char*>(buffer), 256);
strncpy(m_Header1.versionID, (char*)&buffer[0], 8);
strncpy(m_Header1.patientID, (char*)&buffer[8], 66);
m_Header1.healthInfo = buffer[84];
m_Header1.weight = buffer[85];
m_Header1.height = buffer[86];
m_Header1.subjectInfo = buffer[87];
strncpy(m_Header1.recordingID, (char*)&buffer[88], 64);
for (int i = 0; i < 4; ++i) { System::Memory::littleEndianToHost(buffer + 152 + i * sizeof(uint32_t), &m_Header1.recordingLocation[i]); }
System::Memory::littleEndianToHost(buffer + 168, &m_Header1.startDateAndTimeOfRecording[0]);
System::Memory::littleEndianToHost(buffer + 172, &m_Header1.startDateAndTimeOfRecording[1]);
System::Memory::littleEndianToHost(buffer + 176, &m_Header1.birthday[0]);
System::Memory::littleEndianToHost(buffer + 180, &m_Header1.birthday[1]);
System::Memory::littleEndianToHost(buffer + 184, &m_Header1.headerLength);
// +34 patient classification 6 bytes
System::Memory::littleEndianToHost(buffer + 192, &m_Header1.equipmentProviderID);
for (int i = 0; i < 6; ++i) { m_Header1.reserved1[i] = buffer[200 + i]; }
for (int i = 0; i < 3; ++i)
{
System::Memory::littleEndianToHost(buffer + 206 + i * sizeof(uint16_t), &m_Header1.headSize[i]);
System::Memory::littleEndianToHost(buffer + 212 + i * sizeof(float), &m_Header1.positionReferenceElectrode[i]);
System::Memory::littleEndianToHost(buffer + 224 + i * sizeof(float), &m_Header1.groundElectrode[i]);
}
System::Memory::littleEndianToHost(buffer + 236, reinterpret_cast<uint64_t*>(&m_Header1.nDataRecords));
System::Memory::littleEndianToHost(buffer + 244, &m_Header1.duration);
System::Memory::littleEndianToHost(buffer + 252, &m_Header1.nSignals);
System::Memory::littleEndianToHost(buffer + 254, &m_Header1.reserved2);
if (oFile.bad()) { return false; }
return true;
}
bool GDF::CFixedGDF251Header::save(std::ofstream& /*file*/)
{
return false;
//TODO complete
/*
uint8_t tempBuffer[sizeof(uint64_t)];
file.seekp(0,std::ios::beg);
file << m_VersionId << m_PatientId;
*/
}
std::string GDF::CFixedGDF251Header::getSubjectName()
{
// extracts the PID and Patient name from the m_sPatientId
char* token = strtok(m_Header1.patientID, " ");
if (token) { token = strtok(nullptr, " "); } //The PId is not a numerical value in GDF, it is useless for us
if (token) { return token; }
return NO_VALUE_S;
}
std::string GDF::CFixedGDF251Header::getExperimentDate()
{
//computes the experiment date
//uint64_t tempDate= *(reinterpret_cast<uint64_t*>(m_startDateAndTimeOfRecording));
//time_t startDateAndTimeOfRecordingInSeconds = ((tempDate/2^32) - 719529) * (3600*24);
//tm * startDateAndTimeOfRecording = gmtime(&startDateAndTimeOfRecordingInSeconds);
//TODO check how date is coded in openvibe Date not good?
//(startDateAndTimeOfRecording->mon+1)<<8 + (startDateAndTimeOfRecording->day)
return NO_VALUE_S;
}
} // namespace FileIO
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,346 @@
#pragma once
#include <openvibe/ov_all.h>
#include <cmath>
#include <fstream>
#include <vector>
namespace OpenViBE {
namespace Plugins {
namespace FileIO {
#define NO_VALUE_I 0xffffffff
#define NO_VALUE_S "_unspecified_"
/**
* Useful classes for GDF file format handling
*/
class GDF
{
public:
class CFixedGDFHeader
{
public:
virtual ~CFixedGDFHeader() {}
virtual bool read(std::ifstream& file) = 0;
virtual bool save(std::ofstream& file) = 0;
virtual bool update(std::ofstream& file) = 0;
virtual uint64_t getSubjectID() { return NO_VALUE_I; }
virtual std::string getSubjectName() { return NO_VALUE_S; }
virtual uint64_t getSubjectSex() { return NO_VALUE_I; }
virtual uint64_t getSubjectAge() { return NO_VALUE_I; }
virtual uint64_t getExperimentID() { return NO_VALUE_I; }
virtual std::string getExperimentDate() { return NO_VALUE_S; }
virtual uint64_t getLaboratoryID() { return NO_VALUE_I; }
virtual uint64_t getTechnicianID() { return NO_VALUE_I; }
virtual std::string getLaboratoryName() { return NO_VALUE_S; }
virtual std::string getTechnicianName() { return NO_VALUE_S; }
virtual double getDataRecordDuration() = 0;
virtual uint64_t getNDataRecords() = 0;
virtual size_t getChannelCount() = 0;
};
/**
* An helper class to manipulate GDF1 fixed-size headers
*/
class CFixedGDF1Header final : public CFixedGDFHeader
{
public:
CFixedGDF1Header();
~CFixedGDF1Header() override {}
/**
* Reads a GDF1 fixed Header from a file
* \param file The input file.
* \return true if the operation was successful
*/
bool read(std::ifstream& file) override;
/**
* Saves a GDF1 fixed Header in a file
* \param file The output file.
* \return true if the operation was successful
*/
bool save(std::ofstream& file) override;
/**
* Updates the number of data records field in
* a GDF1 fixed Header in a file
* \param file The output file.
* \return true if the operation was successful
*/
bool update(std::ofstream& file) override;
std::string getSubjectName() override;
uint64_t getLaboratoryID() override;
uint64_t getTechnicianID() override;
double getDataRecordDuration() override { return double(m_DurationDataRecordNum) / double(m_DurationDataRecordDen); }
uint64_t getNDataRecords() override { return m_NDataRecords; }
size_t getChannelCount() override { return m_NSignals; }
char m_VersionID[8];
char m_PatientID[80];
char m_RecordingID[80];
char m_StartDateAndTimeOfRecording[16];
int64_t m_NBytesInHeaderRecord = 0;
uint64_t m_EquipmentProviderID = 0;
uint64_t m_LaboratoryID = 0;
uint64_t m_TechnicianID = 0;
char m_ReservedSerialNumber[20];
int64_t m_NDataRecords = 0;
uint32_t m_DurationDataRecordNum = 0;
uint32_t m_DurationDataRecordDen = 0;
uint32_t m_NSignals = 0;
};
/**
* An helper class to manipulate GDF2 fixed-size headers
*/
class CFixedGDF2Header final : public CFixedGDFHeader
{
public:
CFixedGDF2Header();
~CFixedGDF2Header() override {}
/**
* Reads a GDF2 fixed Header from a file
* \param oFile The input file.
* \return true if the operation was successful
*/
bool read(std::ifstream& oFile) override;
/**
* Saves a GDF2 fixed Header in a file
* \param file The output file.
* \return true if the operation was successful
*/
bool save(std::ofstream& file) override;
/**
* Updates the number of data records field in
* a GDF2 fixed Header in a file
* \param file The output file.
* \return true if the operation was successful
*/
bool update(std::ofstream& file) override { return true; }
std::string getExperimentDate() override;
std::string getSubjectName() override;
uint64_t getSubjectSex() override { return m_SubjectInfo & 0x03; }
uint64_t getSubjectAge() override { return uint64_t(std::floor(double((m_StartDateAndTimeOfRecording[1] - m_Birthday[1]) / 365.242189813))); }
double getDataRecordDuration() override { return double(m_DurationRecordNum) / double(m_DurationDataRecordDen); }
uint64_t getNDataRecords() override { return m_NDataRecords; }
size_t getChannelCount() override { return m_NSignals; }
char m_VersionID[8];
char m_PatientID[66];
uint8_t m_Reserved[10];
uint8_t m_HealthInformation = 0; //smoking...
uint8_t m_Weight = 0;
uint8_t m_Height = 0;
uint8_t m_SubjectInfo = 0;//gender...
char m_RecordingID[64];
uint32_t m_RecordingLocation[4];
uint32_t m_StartDateAndTimeOfRecording[2];
uint32_t m_Birthday[2];
uint16_t m_NBlocksInHeader = 0;
uint8_t m_Reserved2[6];
uint64_t m_EquipmentProviderID = 0;
uint8_t m_IPAdress[6];
uint16_t m_HeadSize[3];
float m_PositionReferenceElectrode[3];
float m_GroundElectrode[3];
int64_t m_NDataRecords = 0;
uint32_t m_DurationRecordNum = 0;
uint32_t m_DurationDataRecordDen = 0;
uint16_t m_NSignals = 0;
uint16_t m_Reserved3 = 0;
};
/**
* An helper class to manipulate GDF2.51 fixed-size headers
*/
class CFixedGDF251Header final : public CFixedGDFHeader
{
public:
CFixedGDF251Header() { memset(&m_Header1, 0, sizeof(m_Header1)); }
~CFixedGDF251Header() override {}
/**
* Reads a GDF2 fixed Header from a file
* \param oFile The input file.
* \return true if the operation was successful
*/
bool read(std::ifstream& oFile) override;
/**
* Saves a GDF2 fixed Header in a file
* \param file The output file.
* \return true if the operation was successful
*/
bool save(std::ofstream& file) override;
/**
* Updates the number of data records field in
* a GDF2 fixed Header in a file
* \param file The output file.
* \return true if the operation was successful
*/
bool update(std::ofstream& file) override { return true; }
std::string getExperimentDate() override;
std::string getSubjectName() override;
uint64_t getSubjectSex() override { return m_Header1.subjectInfo & 0x03; }
uint64_t getSubjectAge() override
{
return uint64_t(floor(double((m_Header1.startDateAndTimeOfRecording[1] - m_Header1.birthday[1]) / 365.242189813)));
}
double getDataRecordDuration() override { return m_Header1.duration; }
uint64_t getNDataRecords() override { return m_Header1.nDataRecords; }
size_t getChannelCount() override { return m_Header1.nSignals; }
struct SGDFFixedHeader1
{
char versionID[8];
char patientID[66];
uint8_t reserved[10];
uint8_t healthInfo; //smoking...
uint8_t weight;
uint8_t height;
uint8_t subjectInfo;//gender...
char recordingID[64];
uint32_t recordingLocation[4];
uint32_t startDateAndTimeOfRecording[2];
uint32_t birthday[2];
uint16_t headerLength;
char patientClassification[6];
uint64_t equipmentProviderID;
uint8_t reserved1[6];
uint16_t headSize[3];
float positionReferenceElectrode[3];
float groundElectrode[3];
int64_t nDataRecords;
double duration; // Not double in the 2.51 spec document at the time of writing this, but seems to be so in practice?
uint16_t nSignals;
uint16_t reserved2;
};
SGDFFixedHeader1 m_Header1;
};
/**
* Base class for GDF file's variable headers
*/
class CVariableGDFHeader
{
public:
virtual ~CVariableGDFHeader() { }
virtual bool save(std::ofstream& file) = 0;
};
/**
* GDF1 variable header class
*/
class CVariableGDF1Header final : public CVariableGDFHeader
{
//! Stores information for one channel
class CVariableGDF1HeaderPerChannel
{
public:
CVariableGDF1HeaderPerChannel();
char m_Label[16];
char m_TranducerType[80];
char m_PhysicalDimension[8];
double m_PhysicalMin = 0;
double m_PhysicalMax = 0;
int64_t m_DigitalMin = 0;
int64_t m_DigitalMax = 0;
char m_PreFiltering[80];
uint32_t m_NSamplesInEachRecord = 0;
uint32_t m_ChannelType = 0;
char m_Reserved[32];
};
public:
~CVariableGDF1Header() override {}
/**
* Saves a GDF1 variable Header in a file
* \param file The output file.
* \return true if the operation was successful
*/
bool save(std::ofstream& file) override;
/**
* Updates the Physical/digital Min/max fields in
* a GDF1 variable Header in a file
* \param file The output file.
* \return true if the operation was successful
*/
bool update(std::ofstream& file);
/**
* Sets the number of channels in the file.
* \param nChannel Number of channels.
*/
void setChannelCount(const uint32_t nChannel) { m_VariableHeaders.resize(nChannel); }
CVariableGDF1HeaderPerChannel& operator[](const uint32_t channel) { return m_VariableHeaders[channel]; }
std::vector<CVariableGDF1HeaderPerChannel> m_VariableHeaders;
};
class CGDFEvent
{
public:
uint32_t m_Position = 0;
uint16_t m_Type = 0;
};
enum EChannelType
{
ChannelType_int8_t = 1,
ChannelType_uint8_t = 2,
ChannelType_int16_t = 3,
ChannelType_uint16_t = 4,
ChannelType_int32_t = 5,
ChannelType_uint32_t = 6,
ChannelType_int64_t = 7,
ChannelType_uint64_t = 8,
ChannelType_float = 16,
ChannelType_double = 17,
ChannelType_float128 = 18,
ChannelType_int24 = 279,
ChannelType_uint24 = 535
};
/**
* Gets the data size in bytes of the GDF data type
* \param channelType The GDF type
* \return The size in bytes of this GDF type's data
*/
static uint16_t GDFDataSize(uint32_t channelType);
};
} // namespace FileIO
} // namespace Plugins
} // namespace OpenViBE
@@ -0,0 +1,36 @@
#include "ovp_defines.h"
#include "box-algorithms/ovpCGDFFileReader.h"
#include "box-algorithms/ovpCGDFFileWriter.h"
#include "box-algorithms/ovpCBCICompetitionIIIbReader.h"
#include "algorithms/brainamp/ovpCAlgorithmBrainampFileReader.h"
#include "box-algorithms/brainamp/ovpCBoxAlgorithmBrainampFileReader.h"
#include "box-algorithms/brainamp/ovpCBoxAlgorithmBrainampFileWriter.h"
#include "box-algorithms/bci2000reader/ovpCBoxAlgorithmBCI2000Reader.h"
#include "box-algorithms/ovpCBoxAlgorithmSignalConcatenation.h"
namespace OpenViBE {
namespace Plugins {
namespace FileIO {
OVP_Declare_Begin()
context.getTypeManager().registerEnumerationEntry(OV_TypeId_BoxAlgorithmFlag, OV_AttributeId_Box_FlagIsUnstable.toString(),
OV_AttributeId_Box_FlagIsUnstable.id());
OVP_Declare_New(CGDFFileReaderDesc)
OVP_Declare_New(CGDFFileWriterDesc)
OVP_Declare_New(CBCICompetitionIIIbReaderDesc)
OVP_Declare_New(CAlgorithmBrainampFileReaderDesc)
OVP_Declare_New(CBoxAlgorithmBrainampFileReaderDesc)
OVP_Declare_New(CBoxAlgorithmBrainampFileWriterDesc)
OVP_Declare_New(CBoxAlgorithmBCI2000ReaderDesc)
OVP_Declare_New(CBoxAlgorithmSignalConcatenationDesc)
OVP_Declare_End()
} // namespace FileIO
} // namespace Plugins
} // namespace OpenViBE