Skip to content

MATLAB guide

The MATLAB binding wraps the same C++ core as the Python package — identical semantics, identical performance (see the benchmark), and the same version number. It ships as a +mef3io package (two handle classes) over a single compiled MEX.

Setup

Either download the mef3io-matlab-vX.Y.Z.zip bundle from a GitHub release — classes and the CI-tested MEX for all platforms in one ready-to-addpath folder (see Install for the caveats) — or compile once from the repo (~30 s, needs a C++20 compiler via mex -setup C++):

run('<repo>/matlab/build_mex.m')
addpath('<repo>/matlab')

Verify: mef3io_mex('version') prints the library version. Test suite: run('<repo>/matlab/test_mef3io.m').

Times everywhere are uUTC (microseconds since the Unix epoch), int64 or double. Errors from the core surface as MATLAB errors (id mef3io:error) with the C++ message.

Writing

w = mef3io.Writer('session.mefd', Overwrite=true, Units='uV');
w.write('ch1', data, startUutc, 256);                 % NaN = gap, precision inferred
w.write('ch1', more, t2, 256);                        % appends in-segment
w.write('ch1', x, t3, 256, NewSegment=true);          % force a new segment
w.writeInt32('ch2', counts, 0.01, startUutc, 256, Valid=mask);
w.writeAnnotations(struct('time', t, 'text', 'note'), 'ch1');
delete(w);                                            % or let it go out of scope

mef3io.Writer(path, ...) name-value options: Overwrite (default false — an existing session is reopened for appending), Password1/Password2 (both required for encryption), Units, BlockLength, Threads.

  • write(channel, data, startUutc, fs, Precision=-1, NewSegment=false) — float path: NaN runs become gaps; Precision sets the conversion factor to 10^-precision, -1 infers it (or reuses the segment's on append). Returns a summary struct (samples_written, blocks, gaps_skipped, segment).
  • writeInt32(channel, data, ufact, startUutc, fs, Valid=[], NewSegment=false) — verbatim int32 counts + conversion factor (bit-exact round trip). data must be integer-typed: floating-point input errors (use write for float data) and values beyond the int32 range error instead of saturating. Valid is any numeric/logical mask, nonzero = valid.
  • writeAnnotations(records, channel) — struct array (or table) with fields time (required), type (default 'Note'), text, duration. Omit channel for session-level records. Replaces records at that level.
  • setMetadata(md) / the Metadata= constructor option — session-wide subject/acquisition metadata. Use the object API (mirrors Python):
md = mef3io.Metadata( ...
    subject=mef3io.Subject(id="MRN-123", name_1="Jane", recording_location="EMU-4"), ...
    acquisition=mef3io.Acquisition(session_description="pre-surgical", line_frequency=60));
w = mef3io.Writer('s.mefd', Overwrite=true, Metadata=md);

Every field defaults, so set only what you have. setMetadata also accepts a plain struct with the flat field names (subject_id, session_description, …). Read it back with r.metadata, a mef3io.Metadata object with .subject and .acquisition (subject fields empty without a level-2 password).

Reading

r = mef3io.Reader('session.mefd');            % password as 2nd argument if encrypted
r.channels                                    % cellstr
info = r.info('ch1');                         % fs, ufact, times, subject metadata
x = r.read('ch1');                            % whole channel: double column, NaN gaps
x = r.read('ch1', t0, t1);                    % [t0, t1) window
raw = r.readRaw('ch2');                       % int32 counts + logical valid mask
segs = r.segments('ch1');                     % what data is where, per segment
blocks = r.toc('ch1');                        % block-level index (struct of vectors)
anns = r.records('ch1');                      % annotations; r.records() = session level
delete(r);

info returns sampling_frequency, units_conversion_factor, units_description, start_time/end_time (int64 uUTC), number_of_samples (stored samples; gaps not counted), n_segments, section3_available and the subject metadata fields (empty under a level-1 password — the signal still reads correctly, unlike the legacy stack).

Tar archives (single-file sessions)

Pack a session into one uncompressed tar file and read it in place — handy for sharing and safe archiving:

tarPath = mef3io.archiveSession('session.mefd');   % -> 'session.mefd.tar'
r = mef3io.Reader(tarPath);                        % no extraction needed

Windowed reads stay cheap (the archive is uncompressed, so only the needed byte ranges are fetched), and any tar tool restores the directory (tar -xf session.mefd.tar). Tar sessions are read-only — mef3io.Writer refuses .tar paths; unpack first to modify:

sessionDir = mef3io.extractSession(tarPath);       % -> 'session.mefd' back
w = mef3io.Writer(sessionDir);                     % writable again

archiveSession(dir, tarPath, Overwrite=true) / extractSession(tarPath, destDir, Overwrite=true) pick targets and replace existing ones; they mirror Python's mef3io.archive_session / extract_session.

Session naming is enforced at every entry point: directories must end .mefd, archives .mefd.tar — anything else is refused, so a stray directory or file can never be misread (or overwritten) as a session.

How it is put together

matlab/mef3io_mex.cpp is a single command-dispatch MEX over the flat C ABI (core/include/mef3io/c_api.h), so C++ exceptions never cross the MEX boundary. The +mef3io classes manage the native handles: delete closes them, and the MEX keeps itself locked while any handle is alive, so clear mex cannot dangle a session.