init
This commit is contained in:
+440
@@ -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
|
||||
+111
@@ -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
|
||||
+256
@@ -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
|
||||
+70
@@ -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
|
||||
+41
@@ -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
|
||||
+43
@@ -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
|
||||
+155
@@ -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
|
||||
+103
@@ -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
|
||||
+163
@@ -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
|
||||
+71
@@ -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
|
||||
+310
@@ -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
|
||||
+92
@@ -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
|
||||
+336
@@ -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
|
||||
+127
@@ -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
|
||||
+525
@@ -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
|
||||
+212
@@ -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
|
||||
+824
@@ -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
|
||||
+214
@@ -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
|
||||
+436
@@ -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
|
||||
+113
@@ -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)
|
||||
+489
@@ -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
|
||||
Reference in New Issue
Block a user