v2.0.0
Loading...
Searching...
No Matches
fiff_events.cpp File Reference

Implementation of FiffEvents: FIFFB_MNE_EVENTS read / write and the stim-channel event-detection path. More...

#include "fiff_events.h"
#include "fiff_evoked_set.h"
#include "fiff_raw_data.h"
#include "fiff_stream.h"
#include "fiff_dir_node.h"
#include "fiff_tag.h"
#include "fiff_constants.h"
#include "fiff_file.h"
#include <QFile>
#include <QDebug>
#include <QRegularExpression>
#include <QTextStream>
#include <algorithm>
#include <cmath>
#include <numeric>
#include <vector>
Include dependency graph for fiff_events.cpp:

Go to the source code of this file.

Functions

MatrixXi selectRows (const MatrixXi &events, const std::vector< bool > &keep)

Detailed Description

Implementation of FiffEvents: FIFFB_MNE_EVENTS read / write and the stim-channel event-detection path.

SPDX-License-Identifier: BSD-3-Clause Copyright (c) 2026 MNE-CPP Authors

Author
Christoph Dinh chris.nosp@m.toph.nosp@m..dinh.nosp@m.@mne.nosp@m.-cpp..nosp@m.org
Since
2.0.0
Date
February 2026

Detection follows mne.find_events; the (sample, prev, new) triples are written under FIFFB_MNE_EVENTS so ``-eve.fif'' files round-trip through MNE-Python unchanged. */


INCLUDES


QT INCLUDES


USED NAMESPACES

using namespace FIFFLIB; using namespace Eigen;


DEFINE MEMBER METHODS

FiffEvents::FiffEvents() { }


FiffEvents::FiffEvents(QIODevice& p_IODevice) { Try FIFF first, then ASCII if (!read_from_fif(p_IODevice, *this)) { The FIFF attempt leaves the device open in binary mode. p_IODevice.close(); read_from_ascii(p_IODevice, *this); } }


bool FiffEvents::read(const QString& t_sEventName, const QString& t_fileRawName, FiffEvents& p_Events) { QString eventName = t_sEventName; QFile t_EventFile; qint32 p;

if (eventName.isEmpty()) { eventName = t_fileRawName; p = eventName.indexOf(".fif"); if (p > 0) { eventName.replace(p, 4, "-eve.fif"); } else { qWarning("Raw file name does not end properly\n"); return false; }

t_EventFile.setFileName(eventName); if (!read_from_fif(t_EventFile, p_Events)) { qWarning("Error while read events.\n"); return false; } qInfo("Events read from %s\n", eventName.toUtf8().constData()); } else { Binary file if (eventName.contains(".fif")) { t_EventFile.setFileName(eventName); if (!read_from_fif(t_EventFile, p_Events)) { qWarning("Error while read events.\n"); return false; } qInfo("Binary event file %s read\n", eventName.toUtf8().constData()); } else if (eventName.contains(".eve")) { } else { Text file qWarning("Text file %s is not supported jet.\n", eventName.toUtf8().constData()); } }

return true; }


bool FiffEvents::read_from_fif(QIODevice& p_IODevice, FiffEvents& p_Events) {

Open file

FiffStream::SPtr t_pStream(new FiffStream(&p_IODevice));

if (!t_pStream->open()) { return false; }

Find the desired block

QList<FiffDirNode::SPtr> eventsBlocks = t_pStream->dirtree()->dir_tree_find(FIFFB_MNE_EVENTS);

if (eventsBlocks.size() == 0) { qWarning("Could not find event data\n"); return false; }

nelem is only assigned when a matching tag is found; the guard below returns before it is read in that case, but it must not start out indeterminate. qint32 k, nelem = 0; fiff_int_t kind, pos; FiffTag::UPtr t_pTag; quint32* serial_eventlist_uint = nullptr; qint32* serial_eventlist_int = nullptr;

for (k = 0; k < eventsBlocks[0]->nent(); ++k) { kind = eventsBlocks[0]->dir[k]->kind; pos = eventsBlocks[0]->dir[k]->pos; if (kind == FIFF_MNE_EVENT_LIST) { t_pStream->read_tag(t_pTag, pos); if (t_pTag->type == FIFFT_UINT) { serial_eventlist_uint = t_pTag->toUnsignedInt(); nelem = t_pTag->size() / 4; }

if (t_pTag->type == FIFFT_INT) { serial_eventlist_int = t_pTag->toInt(); nelem = t_pTag->size() / 4; }

break; } }

if (serial_eventlist_uint == nullptr && serial_eventlist_int == nullptr) { qWarning("Could not find any events\n"); return false; }

p_Events.events.resize(nelem / 3, 3); if (serial_eventlist_uint != nullptr) { for (k = 0; k < nelem / 3; ++k) { p_Events.events(k, 0) = serial_eventlist_uint[k * 3]; p_Events.events(k, 1) = serial_eventlist_uint[k * 3 + 1]; p_Events.events(k, 2) = serial_eventlist_uint[k * 3 + 2]; } }

if (serial_eventlist_int != nullptr) { for (k = 0; k < nelem / 3; ++k) { p_Events.events(k, 0) = serial_eventlist_int[k * 3]; p_Events.events(k, 1) = serial_eventlist_int[k * 3 + 1]; p_Events.events(k, 2) = serial_eventlist_int[k * 3 + 2]; } }

return true; }


bool FiffEvents::read_from_ascii(QIODevice& p_IODevice, FiffEvents& p_Events) { if (!p_IODevice.open(QIODevice::ReadOnly | QIODevice::Text)) { return false; } QTextStream textStream(&p_IODevice);

QList<int> sampleList; QList<int> beforeList; QList<int> afterList;

while (!textStream.atEnd()) { QString line = textStream.readLine().trimmed(); if (line.isEmpty()) continue;

A standard .eve line has four fields: sample, onset in seconds, value before and value after the transition. The onset is redundant with the sample and is skipped, which is what write_to_ascii emits. Reading it as an integer used to shift every later field along by one, so the trigger code was lost and the event looked like it had a code of zero. Files carrying only the three integer fields are still accepted. const QStringList fields = line.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); if (fields.isEmpty()) continue;

const int iOffset = (fields.size() >= 4) ? 1 : 0;

int iSample = fields.at(0).toInt(); int iBefore = (fields.size() > 1 + iOffset) ? fields.at(1 + iOffset).toInt() : 0; int iAfter = (fields.size() > 2 + iOffset) ? fields.at(2 + iOffset).toInt() : 0;

sampleList.append(iSample); beforeList.append(iBefore); afterList.append(iAfter); }

p_Events.events.resize(sampleList.size(), 3);

for (int i = 0; i < sampleList.size(); i++) { p_Events.events(i, 0) = sampleList[i]; p_Events.events(i, 1) = beforeList[i]; p_Events.events(i, 2) = afterList[i]; } return true; }


bool FiffEvents::write_to_fif(QIODevice& p_IODevice) const { if (events.rows() == 0 || events.cols() < 3) return false;

FiffStream::SPtr pStream = FiffStream::start_file(p_IODevice); if (!pStream) return false;

FIFF stores the list row by row (sample, before, after). const Eigen::Matrix<int, Eigen::Dynamic, 3, Eigen::RowMajor> rows = events.leftCols(3); pStream->start_block(FIFFB_MNE_EVENTS); pStream->write_int(FIFF_MNE_EVENT_LIST, rows.data(), rows.rows() * 3); pStream->end_block(FIFFB_MNE_EVENTS); pStream->end_file();

return true; }


bool FiffEvents::write_to_ascii(QIODevice& p_IODevice, float sfreq) const { if (!p_IODevice.open(QIODevice::WriteOnly | QIODevice::Text)) return false;

for (int k = 0; k < events.rows(); ++k) { int sample = events(k, 0); int before = (events.cols() > 1) ? events(k, 1) : 0; int after = (events.cols() > 2) ? events(k, 2) : 0; float time = (sfreq > 0.0f) ? static_cast<float>(sample) / sfreq : 0.0f; QTextStream out(&p_IODevice); out << QString("%1 %2 %3 %4\n") .arg(sample, 6) .arg(time, -10, 'f', 3) .arg(before, 3) .arg(after, 3); }

p_IODevice.close(); return true; }


bool FiffEvents::detect_from_raw(const FiffRawData& raw, FiffEvents& p_Events, const QString& triggerCh, unsigned int triggerMask, bool leadingEdge) { QString stimCh = triggerCh.isEmpty() ? QString("STI 014") : triggerCh;

Channel names are read without spaces ("STI014"), so compare without them. const QString wanted = QString(stimCh).remove(' '); int triggerChIdx = -1; for (int k = 0; k < raw.info.ch_names.size(); ++k) { if (QString(raw.info.ch_names[k]).remove(' ') == wanted) { triggerChIdx = k; break; } } if (triggerChIdx < 0) { qWarning() << "[FiffEvents::detect_from_raw] Trigger channel" << stimCh << "not found."; return false; }

Read trigger channel data MatrixXd data; MatrixXd times; if (!raw.read_raw_segment(data, times, raw.first_samp, raw.last_samp, RowVectorXi::LinSpaced(1, triggerChIdx, triggerChIdx))) { qWarning() << "[FiffEvents::detect_from_raw] Could not read trigger channel data."; return false; }

RowVectorXd trigData = data.row(0); int nSamples = static_cast<int>(trigData.cols());

Detect flanks QList<int> eventSamples; QList<int> eventBefore; QList<int> eventAfter;

int prevVal = static_cast<int>(trigData(0)) & triggerMask; for (int s = 1; s < nSamples; ++s) { int curVal = static_cast<int>(trigData(s)) & triggerMask; if (curVal != prevVal) { if (!leadingEdge || (leadingEdge && prevVal == 0 && curVal != 0)) { eventSamples.append(static_cast<int>(raw.first_samp) + s); eventBefore.append(prevVal); eventAfter.append(curVal); } } prevVal = curVal; }

int nEvents = eventSamples.size(); p_Events.events.resize(nEvents, 3); for (int k = 0; k < nEvents; ++k) { p_Events.events(k, 0) = eventSamples[k]; p_Events.events(k, 1) = eventBefore[k]; p_Events.events(k, 2) = eventAfter[k]; }

return nEvents > 0; }


bool FiffEvents::matchEvent(const AverageCategory& cat, const MatrixXi& events, int eventIdx) { if (eventIdx < 0 || eventIdx >= events.rows()) return false;

int evFrom = events(eventIdx, 1); int evTo = events(eventIdx, 2);

Check if any of the category's event codes match bool match = false; for (int k = 0; k < cat.events.size(); ++k) { if ((evFrom & ~cat.ignore) == 0 && (evTo & ~cat.ignore) == cat.events[k]) { match = true; break; } } if (!match) return false;

Check previous event constraint if (cat.prevEvent != 0) { bool found = false; for (int j = eventIdx - 1; j >= 0; –j) { if ((events(j, 1) & ~cat.prevIgnore) == 0) { found = true; match = match && ((events(j, 2) & ~cat.prevIgnore) == cat.prevEvent); break; } } if (!found) match = false; }

Check next event constraint if (cat.nextEvent != 0) { bool found = false; for (int j = eventIdx + 1; j < events.rows(); ++j) { if ((events(j, 1) & ~cat.nextIgnore) == 0) { found = true; match = match && ((events(j, 2) & ~cat.nextIgnore) == cat.nextEvent); break; } } if (!found) match = false; }

return match; }


namespace {

/** The rows of events whose flag is set, in order.

Definition in file fiff_events.cpp.

Function Documentation

◆ selectRows()

MatrixXi selectRows ( const MatrixXi & events,
const std::vector< bool > & keep )

Definition at line 417 of file fiff_events.cpp.