/* EDF Parser Implementation * * Copyright (c) 2019 The OSCAR Team * Copyright (c) 2011-2018 Mark Watkins * * This file is subject to the terms and conditions of the GNU General Public * License. See the file COPYING in the main directory of the source code * for more details. */ #include #include #include #include #include #ifdef _MSC_VER #include #else #include #endif #include "edfparser.h" //EDFSignal::~EDFSignal() //{ // delete [] dataArray; //} EDFInfo::EDFInfo() { filename = QString(); edfsignals.clear(); filesize = 0; datasize = 0; signalPtr = nullptr; hdrPtr = nullptr; // fileData = nullptr; } int EDFInfo::TZ_offset = QTimeZone::systemTimeZone().offsetFromUtc(QDateTime::currentDateTime()); QTimeZone EDFInfo::localNoDST = QTimeZone(TZ_offset); EDFInfo::~EDFInfo() { // if ( fileData ) { if (fileData.size() > 0) { qDebug() << "EDFInfo destructor clearing fileData"; fileData.clear(); } // } for (auto & s : edfsignals) { if (s.dataArray) delete [] s.dataArray; } // for (auto & a : annotations) // delete a; } bool EDFInfo::Open(const QString & name) { if (hdrPtr != nullptr) { qWarning() << "EDFInfo::Open() called with file already open " << name; return false; } QFile fi(name); if (!fi.open(QFile::ReadOnly)) { qDebug() << "EDFInfo::Open() Couldn't open file " << name; return false; } // fileData = new QByteArray(); #ifndef DUMPSTR if (name.endsWith(STR_ext_gz)) { fileData = gUncompress(fi.readAll()); // Open and decompress file } else { fileData = fi.readAll(); // Open and read uncompressed file } #else fileData = fi.readAll(); // Open and read uncompressed file #endif fi.close(); if (fileData.size() <= EDFHeaderSize) { fileData.clear();; qDebug() << "EDFInfo::Open() File too short " << name; return false; } filename = name; // return fileData; return true; } QDateTime EDFInfo::getStartDT( QString dateTimeStr ) { // edfHdr.startdate_orig = QDateTime::fromString(QString::fromLatin1(hdrPtr->datetime, 16), "dd.MM.yyHH.mm.ss"); // QString dateTimeStr; // , dateStr, timeStr; QDate qDate; QTime qTime; // dateTimeStr = QString::fromLatin1(hdrPtr->datetime, 16); // dateStr = dateTimeStr.left(8); // timeStr = dateTimeStr.right(8); qDate = QDate::fromString(dateTimeStr.left(8), "dd.MM.yy"); qTime = QTime::fromString(dateTimeStr.right(8), "HH.mm.ss"); return QDateTime(qDate, qTime, localNoDST); } bool EDFInfo::parseHeader( EDFHeaderRaw *hdrPtr ) { bool ok; edfHdr.version = QString::fromLatin1(hdrPtr->version, 8).toLong(&ok); if (!ok) { qWarning() << "EDFInfo::Parse() Bad Version " << filename; // sleep(1); fileData.clear(); return false; } edfHdr.patientident=QString::fromLatin1(hdrPtr->patientident,80).trimmed(); edfHdr.recordingident = QString::fromLatin1(hdrPtr->recordingident, 80).trimmed(); // Serial number is in here.. edfHdr.startdate_orig = getStartDT(QString::fromLatin1(hdrPtr->datetime, 16)); // This conversion will fail in 2084 after when the spec calls for the year to be 'yy' instead of digits // The solution is left for the afflicted - it won't be me! QDate d2 = edfHdr.startdate_orig.date(); if (d2.year() < 2000) { d2.setDate(d2.year() + 100, d2.month(), d2.day()); edfHdr.startdate_orig.setDate(d2); } edfHdr.num_header_bytes = QString::fromLatin1(hdrPtr->num_header_bytes, 8).toLong(&ok); if (!ok) { qWarning() << "EDFInfo::Parse() Bad header byte count " << filename; // sleep(1); fileData.clear(); return false; } edfHdr.reserved44=QString::fromLatin1(hdrPtr->reserved, 44).trimmed(); edfHdr.num_data_records = QString::fromLatin1(hdrPtr->num_data_records, 8).toLong(&ok); if (!ok) { qWarning() << "EDFInfo::Parse() Bad data record count " << filename; // sleep(1); fileData.clear(); return false; } edfHdr.duration_Seconds = QString::fromLatin1(hdrPtr->dur_data_records, 8).toDouble(&ok); if (!ok) { qWarning() << "EDFInfo::Parse() Bad duration " << filename; // sleep(1); fileData.clear(); return false; } edfHdr.num_signals = QString::fromLatin1(hdrPtr->num_signals, 4).toLong(&ok); if (!ok) { qWarning() << "EDFInfo::Parse() Bad number of signals " << filename; // sleep(1); fileData.clear(); return false; } return true; } bool EDFInfo::Parse() { bool ok; if (fileData.size() == 0) { qWarning() << "EDFInfo::Parse() called without valid EDF data " << filename; // sleep(1); return false; } hdrPtr = (EDFHeaderRaw *)fileData.constData(); signalPtr = (char *)fileData.constData() + EDFHeaderSize; filesize = fileData.size(); datasize = filesize - EDFHeaderSize; pos = 0; eof = false; if ( ! parseHeader( hdrPtr ) ) return false; // Initialize fixed-size signal list. edfsignals.resize(edfHdr.num_signals); // Now copy all the Signal descriptives into edfsignals for (auto & sig : edfsignals) { sig.dataArray = nullptr; sig.label = ReadBytes(16); signal_labels.push_back(sig.label); signalList[sig.label].push_back(&sig); if (eof) { qWarning() << "EDFInfo::Parse() Early end of file " << filename; sleep(1); fileData.clear(); return false; } } for (auto & sig : edfsignals) { sig.transducer_type = ReadBytes(80); } for (auto & sig : edfsignals) { sig.physical_dimension = ReadBytes(8); } for (auto & sig : edfsignals) { sig.physical_minimum = ReadBytes(8).toDouble(&ok); } for (auto & sig : edfsignals) { sig.physical_maximum = ReadBytes(8).toDouble(&ok); } for (auto & sig : edfsignals) { sig.digital_minimum = ReadBytes(8).toDouble(&ok); } for (auto & sig : edfsignals) { sig.digital_maximum = ReadBytes(8).toDouble(&ok); sig.gain = (sig.physical_maximum - sig.physical_minimum) / (sig.digital_maximum - sig.digital_minimum); sig.offset = 0; } for (auto & sig : edfsignals) { sig.prefiltering = ReadBytes(80); } for (auto & sig : edfsignals) { sig.sampleCnt = ReadBytes(8).toLong(&ok); } for (auto & sig : edfsignals) { sig.reserved = ReadBytes(32); } // could do it earlier, but it won't crash from > EOF Reads if (eof) { qWarning() << "EDFInfo::Parse() Early end of file " << filename; // sleep(1); fileData.clear(); return false; } // Now check the file isn't truncated before allocating space for the values long allocsize = 0; for (auto & sig : edfsignals) { if (edfHdr.num_data_records > 0) { allocsize += sig.sampleCnt * edfHdr.num_data_records * 2; } } if (allocsize > (datasize - pos)) { // Space required more than the remainder left to read, // so abort and let the user clean up the corrupted file themselves qWarning() << "EDFInfo::Parse(): " << filename << " is too short!"; // sleep(1); fileData.clear(); return false; } // allocate the arrays for the signal values for (auto & sig : edfsignals) { long samples = sig.sampleCnt * edfHdr.num_data_records; if (edfHdr.num_data_records <= 0) { sig.dataArray = nullptr; continue; } sig.dataArray = new qint16 [samples]; // sig.pos = 0; } for (int recNo = 0; recNo < edfHdr.num_data_records; recNo++) { for (auto & sig : edfsignals) { if ( sig.label.contains("Annotations") ) { annotations.push_back(ReadAnnotations( (char *)&signalPtr[pos], sig.sampleCnt*2)); pos += sig.sampleCnt * 2; } else { // it's got genuine 16-bit values for (int j=0;j EDFInfo::ReadAnnotations(const char * data, int charLen) { QVector annoVec = QVector(); // Process event annotation record long pos = 0; double offset; double duration; while (pos < charLen) { QString text; bool sign, ok; char c = data[pos]; if ((c != '+') && (c != '-')) // Annotaion must start with a +/- sign break; sign = (data[pos++] == '+'); text = ""; c = data[pos]; do { // collect the offset text += c; pos++; c = data[pos]; } while ((c != AnnoSep) && (c != AnnoDurMark)); // a duration is optional offset = text.toDouble(&ok); if (!ok) { qDebug() << "Faulty offset in annotation record "; // sleep(1); break; } if (!sign) offset = -offset; duration = -1.0; // This indicates no duration supplied // First entry if (data[pos] == AnnoDurMark) { // get duration.(preceded by decimal 21 byte) pos++; text = ""; do { // collect the duration text += data[pos]; pos++; } while ((data[pos] != AnnoSep) && (pos < charLen)); // separator code duration = text.toDouble(&ok); if (!ok) { qDebug() << "Faulty duration in annotation record "; // sleep(1); break; } } while ((data[pos] == AnnoSep) && (pos < charLen)) { int textLen = 0; pos++; const char * textStart = &data[pos]; if (data[pos] == AnnoEnd) break; if (data[pos] == AnnoSep) { pos++; break; } do { // collect the annotation text pos++; // officially UTF-8 is allowed here, so don't mangle it textLen++; } while ((data[pos] != AnnoSep) && (pos < charLen)); // separator code text = QString::fromUtf8(textStart, textLen); annoVec.push_back( Annotation( offset, duration, text) ); if (pos >= charLen) { qDebug() << "Short EDF Annotations record"; // sleep(1); break; } } while ((pos < charLen) && (data[pos] == AnnoEnd)) pos++; if (pos >= charLen) break; } return annoVec; } // Read a 16 bits integer qint16 EDFInfo::Read16() { if ((pos + 2) > datasize) { eof = true; return 0; } #ifdef Q_LITTLE_ENDIAN // Intel, etc... qint16 res = *(qint16 *)&signalPtr[pos]; #else // ARM, PPC, etc.. qint16 res = quint8(signalPtr[pos]) | (qint8(signalPtr[pos+1]) << 8); #endif pos += 2; return res; } QString EDFInfo::ReadBytes(unsigned n) { if ((pos + long(n)) > datasize) { eof = true; return QString(); } QByteArray buf(&signalPtr[pos], n); pos+=n; return buf.trimmed(); } EDFSignal *EDFInfo::lookupLabel(const QString & name, int index) { auto it = signalList.find(name); if (it == signalList.end()) return nullptr; if (index >= it.value().size()) return nullptr; return it.value()[index]; }