A fast, lightweight implementation of the WFDB library and command-line tools, with identical behavior across seven programming languages
These notes describe how the MATLAB/Octave implementation is organized. They
complement docs/porting-guide.md (the C
semantics every implementation must reproduce). The Python implementation
(python/src/wfdbx/) is the model: the MATLAB code follows its structure,
module by module, and must behave identically (the conformance suite checks
this).
+wfdbx), classdef handle classes, persistent
variables, fopen/fread/fwrite. No string objects (use char;
the public functions convert MATLAB strings given as arguments with
isa(x, 'string') and char, or wfdbx.internal.strarg), no
arguments blocks, no Octave-only syntax (# comments, !, ++, +=,
double-quoted strings, end_if/endfunction, default arguments, …).MATLAB has not been available for testing, so these rules (from the documentation of both products) must be followed by reading the code, not by running MATLAB:
string/strings/"...", contains, startsWith,
endsWith, newline, isStringScalar, datetime/duration, rng,
isenv, unique(..., 'stable'), sum(..., 'all'), 'omitnan'
options, fopen with an encoding argument, arguments blocks. Usable:
isfolder/isfile (Octave 5, R2017b), repelem, cummax/cummin,
pow2, typecast, accumarray, the legacy cellfun('isempty', ...)
forms. Octave-only functions (argv, OCTAVE_HOME, fflush, hash,
isguirunning, rows, columns, index, printf, puts, fdisp,
unwind_protect, …) may be used only in a branch guarded by
wfdbx.internal.is_octave() with an equivalent MATLAB branch (the
current ones: cout/cerr/WFDBFile.sync (fflush), stdin_bytes
(reading fid 0), md5 (hash; Java in MATLAB, md5_pure otherwise),
tool_command, wfdb_fopen (MATLAB can open directories),
std_stream (stat, S_ISREG, S_ISCHR), and symlink in the tests).&& and || need scalar operands. Octave applies all() to an
array operand (and treats [] as false) where MATLAB throws an error,
so a non-scalar operand passes the tests and fails in MATLAB.size(x)(2), a(1)(2), f(x).field, [1 2 3](k), 'abc'(1)).bitand/bitor/bitxor/bitshift only on non-negative integer
values below 2^53 (or on integer classes): MATLAB throws an error for a
negative double, Octave silently returns a wrong value (bitand(-1,
4095) is 0). Use mod(x, 2^k) for x & (2^k - 1) when x may be
negative.regexp on data. Octave’s regexp works on UTF-8 (a two-byte
character is one .) and throws an error for invalid UTF-8 (any lone
byte above 127), MATLAB’s on characters. Apply regular expressions only
to ASCII text, or to a copy in which the bytes above 127 are replaced by
a character that no pattern tells apart from them (e.g. char(127)),
and take the strings from the original (see wrann, strtod).
Patterns may use (?:...), (?!...), possessive quantifiers, \d,
\t, \xNN; . matches newlines in both. $ also matches before a
final newline in Octave (PCRE), not in MATLAB: do not rely on it.'rb', 'wb', 'ab'):
'r'/'w' are text mode in Octave on Windows. Read text as bytes with
fread(..., 'uint8=>uint8') (MATLAB’s fgets/fgetl/fscanf
transcode characters). exist(name, 'file') and exist(name, 'dir')
search the MATLAB path for relative names: use isfile/isfolder for
files relative to the current directory (exist only with absolute
names, or 'builtin').system prints the output of the command on the standard output in
MATLAB and lets it through in Octave: capture it ([s, out] =
system(cmd)) and write it explicitly.getenv returns '' for unset and empty variables
alike (isenv is R2022b and not in Octave): cgetenv reports both as
unset everywhere.upper/lower also convert Latin-1 letters, Octave’s only ASCII: use
them only on ASCII text (option names, format conversions).*, /, ^ (matrix operations) on
non-scalar integer arrays (MATLAB throws errors). 64-bit integer
arithmetic saturates in both.| Path | Contents |
|---|---|
+wfdbx/*.m |
the WFDB library API: one function per C function (wfdbx.isigopen, wfdbx.getvec, wfdbx.strtim, …), annotation code constants (wfdbx.NORMAL, …), and the high-level API (wfdbx.read_record, wfdbx.read_annotations, wfdbx.write_record, wfdbx.write_annotations, wfdbx.check_record) |
+wfdbx/+internal/ |
implementation: library state (handle classes), C standard library emulation, file I/O, signal formats, FLAC codec |
+wfdbx/+apps/<tool>.m |
the command-line tools: status = <tool>(args) (args without the program name) |
bin/wfdbx_cli.m |
Octave command-line driver: octave-cli bin/wfdbx_cli.m TOOL ARGS... |
bin/wfdbx_matlab.sh |
MATLAB command-line driver (matlab -batch) |
tests/ |
unit tests (run_tests.m) |
Package functions call each other with their full names (wfdbx.internal.x),
as both MATLAB and Octave require.
Every .m file starts with its function (or classdef) line (the
package listing +wfdbx/Contents.m and the script bin/wfdbx_cli.m
start directly with their help), an H1 line (%NAME Short
description.) and help text in the MathWorks style (syntax lines with
upper-case argument names, inputs, outputs and return codes, an example
where useful, a “See also” line), and as the last paragraph of the help one
line on its origin: “Translation of the C function isigopen() (lib/signal.c,
WFDB 10.7.0): same arguments and results.” (with “Differences: …; see
docs/DIVERGENCES.md.” where WFDB-x behaves differently; for a tool, “the C
program rdsamp (app/rdsamp.c, …): same options and output”; for an
internal function, “Translation of the C function edfparse() (…).” or
“Part of the translation of lib/signal.c (WFDB 10.7.0).”), or “Part of
WFDB-x; not in the original WFDB package.” Then, after a blank line, as a
separate comment block that help does not show, comes the file header,
with the exact authors and years of docs/AUTHORS.md:
% This file is part of WFDB-x, an independent reimplementation of the WFDB
% Software Package in Python, C++, MATLAB/Octave, Rust, Julia, R and Kotlin.
%
% It is a MATLAB/Octave translation of lib/annot.c from the WFDB Software
% Package 10.7.0, by George B. Moody. The translation follows the logic and
% behavior of the original code, but it is new code, not the original C
% code.
%
% Changes from the original: none, apart from the language.
%
% Original C code: lib/annot.c, WFDB Software Package 10.7.0.
% Copyright (C) 1989-2013 George B. Moody; revised 2015-2020 by Benjamin
% Moody.
% MATLAB/Octave translation and changes: Copyright (C) 2026 Reza Sameni.
%
% This library is free software: you can redistribute it and/or modify it
% under the terms of the GNU General Public License as published by the Free
% Software Foundation, either version 3 of the License, or (at your option)
% any later version. See LICENSE.
+wfdbx/+apps/) say “original program” and “Original C program:
app/Only % comments; every
function ends with end.
Numeric constants are named (local variables at the top of the function,
with the C name where C has one: ACMAX = 49;, WFDB_DEFGAIN = 200;,
SKIP = 59 * CODE1;) or explained by a short comment; a local variable
costs nothing, while a call of wfdbx.ACMAX() costs about 10
microseconds. Shared tables are functions (wfdbx.internal.fmt_bits: the
sample width of each format).
Do not give a package function the name of an Octave built-in function
(e.g. isspace, which is why the C isspace is cisspace): Octave then
prints a “shadows a built-in function” warning on the standard error
output, which breaks the conformance tests. Check new names with
exist(name, 'builtin') in Octave.
double values (exact for all 32-bit sample
values and for times up to 2^53). Where C arithmetic wraps around
(int overflow, unsigned conversions, short/char casts), the
helpers of wfdbx.internal.c* (cint32, cuint32, cint16, cint8,
cuint8, cdiv, cmod, d2i) are applied explicitly. Never rely on
MATLAB integer classes for wrap-around: they saturate.char row vectors whose characters are the bytes of the
C strings (codes 0-255, i.e. ISO-8859-1). Files are read with
fread(..., 'uint8=>uint8') and converted with char(...); text is
written with fwrite(fid, uint8(s)). Never use fprintf/fgetl on file
contents (MATLAB would transcode non-ASCII bytes).'\n' is a backslash and an n. Write control
characters as char(10), sprintf('\r\n'), etc. when comparing or
building strings (and for Strtok delimiters). Only formats given to
csprintf/cprintf/ceprintf/wfdb_error/sprintf interpret \n,
\t, \\, as C string literals do; data must therefore always be
passed through %s, never inside a format.[v, e] = strtol(s, base, pos) returns the
index e just past the number (C’s end pointer + 1); “no conversion” is
e == pos.NULL strings are [] (an empty double); empty C strings are ''.
Test with ischar.WFDB_Siginfo, WFDB_Anninfo,
WFDB_Annotation, WFDB_Calinfo, WFDB_Seginfo) are MATLAB structs
with the C field names; constructors wfdbx.siginfo(), wfdbx.anninfo(),
wfdbx.annotation() return them with C’s zero values. An annotation’s
aux is [] or a uint8 row vector whose first element is the length
of the data that follows (as in C and in the Python port).[nsig, si] = wfdbx.isigopen(record, nsig),
[stat, v] = wfdbx.getvec(), [stat, annot] = wfdbx.getann(an).wfdbx.siginfo(n), wfdbx.annotation(n) and wfdbx.seginfo(n) return
1-by-n struct arrays (default n = 1); wfdbx.anninfo(name, stat) and
wfdbx.calinfo() return one structure. siginfo fields start as in the
Python port (spf = 1, strings []).wfdbx.WFDB_READ() (0),
WFDB_WRITE (1), WFDB_AHA_READ (2), WFDB_AHA_WRITE (3),
WFDB_APPEND (4). Internal code may use the literal values with a
comment (wfdb_open(s, rec, 0) % WFDB_READ), as a function call costs
about 10 microseconds in Octave.lib/ecgcodes.h) exist in two forms: one function
per code (wfdbx.NORMAL(), wfdbx.PVC(), …, wfdbx.ACMAX(), and the
position codes wfdbx.APUNDEF() … wfdbx.APALOW()), for users and
for code that runs once; and C = wfdbx.ecgcodes(), a structure with all
of them as fields (cached), for library code and loops (C.NORMAL; a
field access is about 10 times faster than a function call in Octave).
The mapping functions wfdbx.isann, isqrs, setisqrs, map1,
setmap1, map2, setmap2, ammap, mamap, annpos, setannpos
are ports of the macros of ecgmap.h; their modifiable tables are held
by wfdbx.internal.ecg_map() (an EcgMap handle with fields qrs,
mp1, mp2, annp, indexed by code + 1). The function versions of
the C library are also provided under their C names (wfdbx.wfdb_isann,
…, wfdbx.wfdb_setannpos, which call the functions above). The
mnemonic tables (annstr, …) belong to the annotation module.printf formatting must match glibc. The MATLAB/Octave sprintf is used
for each conversion, through wfdbx.internal.csprintf(fmt, args...), which
adds C semantics: nan/-nan/inf (not NaN/Inf; the sign of a NaN is
its sign bit, and 0/0 computed at run time is -nan as in C on x86),
%d/%ld/%lld of integer-valued doubles (non-integers truncated), %c
of a number or a char, %s of byte strings ([] prints (null)), %u,
%x, %o of wrapped values (modulo 2^32, or 2^64 with l), %hd/%hhd,
%F, and * widths and precisions. Parsed formats are cached; when all
arguments are ordinary (finite double scalars, integers for integer
conversions, non-empty char vectors), one sprintf call produces the
result (about 50 microseconds per call in Octave). %a, %n and %p
are not supported. tests/test_csprintf.m checks the results against
strings printed by glibc.
Plain sprintf is fine where its result is the same as C’s (e.g. %d of
an integer, %s of text) — notably for vectorized output. Octave’s
sprintf differs from C in: NaN/Inf/-Inf spellings, %d of a
non-integer (printed as with %f/%g), %lld/%hd (rejected: strip
length modifiers), %F and %a (rejected), and %d of uint64 values
above 2^53 (printed as a float; use %u).
Output to the standard output/error goes through wfdbx.internal.cout(s)
/ wfdbx.internal.cerr(s) (or cprintf(fmt, ...) / ceprintf(fmt, ...)),
which write bytes with fwrite; tools must not call fprintf/disp
directly. cout buffers its output (flushed when the buffer is large, by
cout(), before anything is written by cerr, when a tool run by
app_run returns, and before the drivers exit); cerr is unbuffered.
Write errors (WFDB-x: wfdbx_write_errors, and the exit status of the
tools) need care in Octave: a write to file identifier 1 never reports a
failure, and for other files fflush and fclose return 0 even when the
buffered data could not be written (only an fwrite of 4096 bytes or more
returns -1). fseek(fid, 0, 'cof') flushes the file and does report the
failure, so output files that are seekable are flushed that way
(WFDBFile.sync, OutputFile.close, cout). The Octave driver calls
std_stream('cli'), which makes the standard output /dev/fd/1 opened
for appending when it is a regular file or a device (pipes keep file
identifier 1, so a broken pipe behaves as before); after a failure, cout
drops the rest of the output and sets IOState.stdout_failed, which
app_run reports. In MATLAB, the driver copies the standard output from
its temporary file with cat and reports a failure of that copy itself.
The files that the tools write themselves are wfdbx.internal.OutputFile
objects.
Large outputs (e.g. rdsamp) are formatted in a vectorized way, with the
same results: wfdbx.internal.format_rows(V, pre, width, prec, dash)
prints the rows of a matrix as sprintf would with %Wd and %W.Pf
conversions, taking the digits of whole columns three at a time from
tables (several times as fast as sprintf); it returns [] when a value
cannot be printed that way (non-finite, a %d non-integer, 2^52 or more
after scaling, or too close to a rounding tie), and the caller then uses
one sprintf call over the matrix, after substituting the C spelling of
special values. tests/test_csprintf.m compares it with sprintf.
The C library’s global state (open signals, annotators, WFDB path, time
base, …) is kept in handle objects held by persistent variables of
accessor functions in +wfdbx/+internal/ (e.g. wfdbx.internal.sig_state()
returns the signal state object). wfdbx.wfdbquit() resets them as C does.
The I/O state is wfdbx.internal.io_state() (an IOState: wfdbpath,
path_prefix/path_type, error_print, error_message,
wfdb_filename, irec, the standard stream objects, …). Handle classes
are defined in +wfdbx/+internal/ (IOState.m, …) and created by their
accessor on first use. clear functions (or clear all) resets all the
library state.
Octave executes scalar code slowly (around a microsecond per simple statement), so per-sample loops are avoided on common paths:
fread into memory buffers;wfdbx.getframes(n) (as in the Python port) decodes many frames at once
with vector operations, with exactly the semantics of n calls of
getvec; it is used by rdsamp, read_record and other tools that read
whole records; wfdbx.putframes is its counterpart for writing;readheader’s tokens),
and decimal fields converted directly, strtol and strtod being called
only for the other forms;wfdbx.internal.getann_bulk, putann_bulk; see Annotations below).Orders of magnitude in Octave 8: a simple statement or builtin call takes
1-3 microseconds, a call of a package function about 10, a method call of a
handle object tens of microseconds, csprintf about 50, sprintf 5.
The per-sample functions (getvec, putvec, …) remain exact and are the
reference for the bulk paths.
The C-emulation and I/O layer in +wfdbx/+internal/ (each function has its
help text):
| Function | C equivalent / purpose |
|---|---|
cint32, cuint32, cint16, cuint16, cint8, cuint8 (vectorized) |
assignment to int, unsigned, short, unsigned short, char, unsigned char |
cdiv(a, b), cmod(a, b) |
integer / and % |
d2i(x) |
(int)x (INT_MIN for NaN/out of range) |
cisspace(c) |
isspace |
[v, e] = strtol(s, base, pos, lmin, lmax), [v, e] = strtod(s, pos), atoi(s), atof(s) |
the same; positions 1-based |
t = Strtok(s); tok = t.next(delims); t.rest() |
strtok(s, d) then strtok(NULL, d); [] is NULL |
[n, vals] = csscanf(s, fmt) |
sscanf: n is C’s return value (-1 = EOF), vals the n assigned values (cell); unassigned variables keep their old values |
lines = fgets_lines(data, bufsize) |
successive fgets(buf, bufsize, fp) results (cell of char) |
r = GlibcRand(seed); r.rand(); r.rand(n); r.srand(seed) |
glibc srand/rand (GlibcRand.RAND_MAX) |
s = csprintf(fmt, ...), cprintf(fmt, ...), ceprintf(fmt, ...) |
sprintf, printf, fprintf(stderr, ...) |
cout(s), cout(), cerr(s) |
fwrite to stdout (buffered; cout() flushes), to stderr |
[fid, check] = std_stream(k), std_stream('cli') |
the files of the standard output and error (see Write errors) |
f = OutputFile(name, pname) |
a file a tool writes itself (f.write(data, ...), f.close(): C’s wfdbx_fclose_output) |
b = stdin_bytes() |
the whole standard input (uint8 row; read once) |
v = cgetenv(name) |
getenv ([] if unset or empty: they cannot be told apart in Octave nor in MATLAB before R2022b) |
cexit(status) |
exit(status) inside a tool (throws wfdbx:exit) |
status = app_run(@main, argv) |
runs a tool’s main(argv) (argv{1} = program name); handles cexit; flushes |
status = cli_main(args) |
the command-line dispatcher used by both drivers |
cmd = tool_command(tool), shell_quote(s) |
shell command running a tool in a new process (for sortann from oannclose, in legacy mode) |
is_octave() |
true in Octave |
st = io_state() |
the I/O state (IOState) |
wfdb_error(fmt, ...) |
wfdb_error: saves the message; prints it unless wfdbquiet |
f = wfdb_open(s, record, mode) |
wfdb_open: the one location a name designates (resolve_name, open_at); in legacy mode, the WFDB path search; returns a WFDBFile or [] |
p = resolve_name(name, output), f = open_at(p, s), expand_home(n), is_url(n), is_absolute_path(n) |
WFDB-x name resolution: a relative name under the input (or output) directory, an absolute, ~ or URL name as given; the file at that location (both spellings) |
f = open_in(dir, s, name, record), wfdb_dirname(p) |
a file named in a header, in the header’s directory, with the containment check |
in_header_dir(name), expand_record(r), output_file(name) |
the containment rule; the trailing-‘/’ expansion of wfdb_open; the local file that an output name designates |
is_input_file(fname) |
(signal module) true if an output signal file is a signal file of the open input record, segments included (Octave: stat device and inode; MATLAB: java.nio.file.Files.isSameFile); openosig refuses such a file in normal mode |
init_input_dir(st), init_output_dir(st), put_input_dir(st, d), path_components(p) |
the settings (environment variables read once), and the path scanner shared by wfdb_parse_path and setwfdb |
f = fetch_open(url) |
a remote file (network access allowed), fetched once per session (the system program curl; if it is not found, webread in Octave, websave in MATLAB) and served from a temporary file deleted by close() |
f = wfdb_fopen(fname, mode) |
wfdb_fopen ('rb', 'wb', 'ab') |
wfdb_parse_path(p), wfdb_addtopath(s), getiwfdb(p) |
path handling (legacy mode) |
wfdb_checkname(p, what), wfdb_setirec(p), wfdb_getirec(), wfdb_striphea(p) |
the same |
m = ecg_map() |
annotation mapping tables (EcgMap) |
WFDBFile (the WFDB_FILE) methods: read(n), readall(), getc(),
write(b), putc(c), printf(fmt, ...), getline(), g16(), g32(),
p16(x), p32(x), seek(off, whence) (0/1/2 = SEEK_SET/CUR/END),
tell(), flush(), feof(), ferror(), clearerr(), close(),
sync(); properties fp, type, name, is_std, out (an output
file: closing it counts a write error if it could not be written). Bytes are uint8 rows; lines
are char. The standard input ('-' for reading) is served from
stdin_bytes() and is not seekable (as a pipe in C); writes to the
standard output ('-' for writing) go to cout. Method calls cost tens of
microseconds in Octave: read data in bulk (read, readall) and decode it
with vector operations.
Public I/O functions in +wfdbx/: getwfdb, setwfdb, resetwfdb,
wfdbquiet, wfdbverbose, wfdberror, wfdb_error (a wrapper of
internal.wfdb_error), wfdbfile, wfdbmemerr,
wfdbversion, wfdbldflags, wfdbcflags, wfdbdefwfdb,
wfdbdefwfdbcal, and the WFDB-x additions wfdbx_write_errors and
wfdbx_note_write_error.
Finding files (WFDB-x): nothing is searched in normal mode. The
settings (input directory, output directory, network access) are
properties of IOState (input_dir, output_dir, network, …), each
read from its environment variable when first needed
(init_input_dir, init_output_dir, wfdbx_get_network); wfdbquit
does not reset them. The signal module notes the directory of the header
read last (SigState.hdr_dir) and of the master header of a
multi-segment record (msrec_dir, msrec_name), so that signal files and
segments are opened next to their header (open_in). Remote files are
read only in normal mode with network access on; legacy mode keeps the
original code (wfdb_fopen refuses names containing .. or ://).
The module follows python/src/wfdbx/_signal.py function by function.
State. S = wfdbx.internal.sig_state() (a SigState) holds the
statics of signal.c under their C names (ffreq, sfreq, cfreq,
btime, bdate, nsamples, segarray, segp, istime, ostime,
gvmode, skewmax, dsbuf, tspf, …). Per-signal data are struct
arrays: hsd (header data: info, start, skew; see
wfdbx.internal.hsdata), isd and vsd (input signals: info, samp,
skew, gvindex, gvcount; wfdbx.internal.isdata), osd (output:
info, samp, skew), smi (sigmap). The info fields are
wfdbx.siginfo structures; info.group is 0-based as in C. Signal
groups (one per signal file) are handle objects in cell rows: igd{g+1}
(IGData: fp, buf/bp/be, count, data, datb, stat,
start, bsize, seek, initial_skip, flac, and ahead, the
blocks read ahead by the bulk reader, which a refill takes first) and
ogd{g+1} (OGData). Flags replace C pointer tests that cannot be written in
Octave (== is not defined for handle objects): has_dsbuf,
has_pinfo, outinfo_hdr (outinfo is oheader). plan caches
per-signal constants for getskewedframe (reset by isigopen and
isigclose).
Reading. wfdbx.internal.readheader (and edfparse for EDF files)
fill hsd; isigopen opens the groups. A frame is read by
wfdbx.internal.getskewedframe(S, v), which reads the samples of each
group with wfdbx.internal.ig_read(ig, fmt, n, reset): n successive
samples with exactly the semantics of C’s r8/r16/r212/… (buffer
refills of bsize or ibsize bytes, the stale bytes returned after the
end of the file, ig.stat after each sample); ig_skip is the loop of
r8() calls of isgsetframe. Bit-packed state (data, datb) is kept
as unsigned 16-bit values (only their low 16 bits matter in C).
rgetvec (multi-frequency modes), getframe_s (deskewing) and getvec
(resampling with setifreq) are layered as in C; getvec calls
getskewedframe directly when none of them is needed. Internal functions
take the state object S as an argument (a sig_state() call costs
about 10 microseconds).
Bulk reading. [v, stat] = wfdbx.getframes(n) (the array first, as
in Python) is exactly n calls of getvec (stopping at the first
negative status). It calls wfdbx.internal.bulk_frames, which decodes
whole frames from the bytes of each group with vector operations: records
of several sampling frequencies (in WFDB_LOWRES mode, averaged as
meansamp, and in WFDB_HIGHRES mode, ispfmax vectors per frame, from
a frame boundary), FLAC groups (through flac_getsamps), null signals
(format 0, of known length), WFDB_GVPAD, and the segments of
multi-segment records, of fixed or variable layout (the frames are then
mapped to the layout signals as sigmap does). Each group is read in
whole blocks of its file (the buffer is left as the refills would leave
it: the block holding the last byte used, the later blocks in ahead)
and in whole packed units (an alignment period of frames, the least common
multiple over the groups of ns / gcd(gfl, ns), with gfl the samples of
a frame of the group and ns those of a packed unit). The frames
of a group are decoded into an n-by-gfl matrix, in blocks of about
SigState.bulk_block samples (65536; the temporary arrays then stay in
the processor’s cache), with tables of the bytes for the bit-packed
formats (formats 16 and 61: at once, by typecast); the sums of its
columns (modulo 2^32, for the checksums) and whether they hold the
invalid value are computed as they are decoded, so that a signal without
invalid samples needs no other work, and a single group holding every
signal is the result itself (no copy). The frame at which the checksums
are verified (the last frame of each segment) is included when they are
correct (they are known once decoded; otherwise the frames are trimmed
back to it, and getvec reads it and reports the error), except with
FLAC or format 8 groups. It stops before any other frame that needs
special handling: the end of a file, a partial bit-packed group, the
middle of a frame in WFDB_HIGHRES mode. getframes then calls getvec
for one vector and tries the bulk path again (it reads at most 2^22
frames at a time, so that n may be Inf). The samples, statuses and
library state (istime, each signal’s samp, cksum, nsamp, gvc,
tvector, …) are those of getvec. Skewed records (dsbuf),
setifreq resampling and unskipped prologs use getvec throughout.
read_record reads chunks of a multiple of 6 frames (6 * ispfmax
vectors in high resolution), so that formats 212, 310 and 311 end each
chunk on a whole packed unit, joins them at the end (a record read in one
chunk is not copied), and converts to physical units in place, in blocks
of rows. The SigState properties bulk_read (set it to false to turn
the bulk path off: the tests compare both paths), bulk_vectors (the
number of vectors read in blocks) and bulk_block (the tests make it
small) are not reset by wfdbquit.
Writing. putvec encodes a frame into the bytes of each group and
appends them with wfdbx.internal.og_put(og, bytes) (which writes the
buffer in chunks of obuflen bytes, as C’s w8 does byte by byte).
wfdbx.putframes(M) writes the rows of M with exactly the effect of
calling putvec for each (vector encoding of all formats, continuing any
partial bit-packed group; format 8 differences are clipped sample by
sample only where they exceed [-128, 127], as clipping is sequential).
The frames of a group whose signals share a format other than 8 are taken
as a matrix (its rows are the samples in file order); in formats 16, 61,
80, 160, 24 and 32 they are converted to bytes with typecast when they
are all integers of the format’s range, by the arithmetic of the general
path otherwise (out of range values wrap as in C).
wfdb_osflush (from wfdbflush and osigclose) writes partial
bit-packed groups and rewinds as C does. setheader, newheader,
setmsheader, setinfo, putinfo write headers (\r\n line ends);
[stat, si] = setheader(...) also returns the units and desc strings
as C’s strtok leaves them.
API conventions (besides those above): [nsig, si] =
wfdbx.isigopen(record, nsig) (nsig <= 0: only the header is read, si
describes -nsig signals); [nsig, si] = wfdbx.osigopen(record, nsig);
n = wfdbx.osigfopen(si, nsig); [stat, v] = wfdbx.getvec(v0) and
[stat, v] = wfdbx.getframe(v0) (v0 optional: the vector C would
leave unchanged); wfdbx.getinfo(record) / wfdbx.getinfo([]) return
[] at the end; wfdbx.setmsheader(record, segnames) takes a cell
array; wfdbx.getseginfo() returns a wfdbx.seginfo struct array.
Constants: wfdbx.WFDB_INVALID_SAMPLE(), WFDB_LOWRES, WFDB_HIGHRES,
WFDB_GVPAD, WFDB_DEFFREQ, WFDB_DEFGAIN, WFDB_DEFRES,
WFDB_MAXRNL, WFDB_MAXUSL, WFDB_MAXDSL.
WFDB_DEFFREQ, WFDB_DEFGAIN and WFDB_DEFRES are used only in legacy
mode (below): WFDB-x never assumes a sampling frequency, a gain, an ADC resolution or units
(docs/DIVERGENCES.md, defined by
tests/reference/divergences.patch); code that needs one and does not
have it reports an error, with the comment “WFDB-x: no default sampling
frequency (C assumed 250 Hz)” or the like. An unspecified ADC resolution
is 0, unspecified units are [] (shown as nd where a tool must print
them). The converters’ -F, -G, -R, -U options are parsed by
wfdbx.internal.opt_sigparam and checked by wfdbx.internal.sigparams;
the files affected by this design say so in the “Changes from the
original” paragraph of their file header.
Legacy mode (--legacy, docs/DIVERGENCES.md): the flag is the
SigState property legacy_mode (wfdbx.wfdbx_set_legacy,
wfdbx_get_legacy; not reset by wfdbquit). app_run calls
wfdbx.wfdbx_legacy_args before a tool’s main and restores the previous
setting when the tool returns. Every divergence branches on it, with the
original code in the legacy branch (comment “WFDB-x: legacy mode”); in the
tools, options added by WFDB-x are made unrecognized by clearing the
option character before the switch, and help prints the original text
in legacy mode. The high-level functions take a 'legacy' option
(wfdbx.internal.hl_legacy turns the library’s legacy mode on for the
call and restores it with an onCleanup object).
Used by other modules: the time functions (wfdbx.sampfreq,
setsampfreq, strtim, timstr, mstimstr, getcfreq,
getbasecount; wfdbx.internal.fstrtim, fmstimstr, ftimstr take the
frequency as an argument), and wfdbquit/wfdbflush, which call the
annotation module’s wfdbx.internal.wfdb_anclose and wfdb_oaflush.
The signal module calls wfdbx.getafreq/setafreq (EDF headers,
setifreq) and the FLAC functions. Calls into other modules are guarded
with wfdbx.internal.pkg_has(name) (cached), because exist does not
find package functions in Octave.
Files. C’s fseek succeeds beyond the end of a file (MATLAB’s and
Octave’s do not), and isigsettime relies on it: WFDBFile.seek emulates
it (reads there find the end of the file; a write extends the file with
zeros).
Speed (Octave 8): see docs/BENCHMARKS.md
for read_record, write_record, the annotation functions and rdsamp.
getvec and putvec cost about 0.4 and 0.25 ms per call (a handle
property access costs 3-5 microseconds).
State: wfdbx.internal.ann_state() (an AnnState): iad and oad, cell
rows of structures for the open input and output annotators (annotator
number n, 0-based as in C, is iad{n+1}), oafreq, annclose_error,
and the mnemonic tables (cstring, astring, tstring, modified),
which wfdbquit does not reset. wfdbx.wfdbquit calls
wfdbx.internal.wfdb_anclose(), and wfdbx.wfdbflush calls
wfdbx.internal.wfdb_oaflush().
annopen reads the whole file and decodes it at once with
vector operations (wfdbx.internal.ann_parse): the result P lists the
successive values that the C variable ia->ann takes during one pass
over the file (times before scaling, codes, subtypes, chan/num carried
over, the AUX words, and whether the file ends inside the last
annotation, which makes getann report unexpected EOF). The input
annotator structure keeps k, the index in P of the current
annotation, plus the other fields of C’s iadata (pann, ateof,
tmul, ann_tt, prev_tt, …). Bytes read past the end of the file
are emulated as in WFDBFile (255), so truncated files behave as in C.annot.aux points into a 771-byte buffer used
as a ring (auxstr), and the aux string of an annotation is taken after
the next annotation has been read; stale bytes can therefore be
visible. This is emulated exactly (buf, index, auxpos;
ann_auxput stores an AUX word, ann_auxget returns what C finds at
aux, up to a null). The buffer carries over from one pass to the next
(iannsettime rewinds by starting a new pass, k = 0), so it is never
precomputed.[stat, annot] = wfdbx.getann(an); wfdbx.getann(an, annot0) returns
annot0 unchanged when no annotation is read (C leaves its argument
unchanged; without it, a zero annotation is returned). A fast path
handles annotations without aux strings; a call takes about 0.1 ms.[stat, A] = wfdbx.internal.getann_bulk(an, m, noaux,
nofail) has exactly the effect of up to m calls of getann (m may
be Inf) and returns a structure of rows (time, anntyp, subtyp,
chan, num, and the cell row aux); with noaux the aux strings are
not built (for skipping); with nofail, the call that would fail is not
made (stat is then 1): tools that may stop early use this, then make
the failing call themselves only if C would. (Reading an AHA-format
annotation can print a message, so code that may stop early reads AHA
files one annotation at a time: C reads only one annotation ahead.)
400,000 annotations are read in about 0.2 s. iannsettime uses it to
skip annotations.putann encodes into a byte buffer of the output annotator
(obuf, written by wfdbx.internal.ann_oflush when large, by
wfdb_oaflush, and by oannclose; output to the standard output is
written at once, so that it interleaves with other output as in C).
wfdbx.internal.putann_bulk(an, A) writes the annotations of a
structure of rows A (as returned by getann_bulk) with exactly the
effect of calling putann for each (vectorized for MIT format).
When annotations were written out of order, oannclose rearranges
the file (oa.fname, the name with which it was created) in the same
process with wfdbx.internal.ann_sortfile, byte for byte as the
sortann tool: the file is decoded into a private input annotator
structure (ann_format, ann_parse, ann_iadata, ann_readtable
with a private definition table, ann_defs, ann_getbulk), ordered
by ann_sortlist (insert_ann of sortann.c, which the sortann tool
uses too), and written with a private output annotator structure
(ann_oadata, ann_puttable, ann_putbulk). These functions take and
return the annotator structures (the library functions getann_bulk,
putann, putann_bulk, ann_gettable call them for the annotators of
ann_state()), so the open annotators, the library’s tables and
setafreq are not touched. ann_sortlist never compares the list head
as an annotation (the original did, losing an out-of-order annotation at
time 0); its legacy argument restores that, for the sortann tool in
legacy mode. In legacy mode, oannclose runs sortann --legacy in a
separate process (wfdbx.internal.tool_command('sortann')), as C does,
in the output directory if one is set (cd 'DIR' && ...).rdann, ann2rr, wrann, rr2ann, sortann read and write
through the bulk functions and format their output with vector
operations where the output fields have their usual widths (falling
back to the per-annotation code otherwise): rdann prints 400,000
annotations in about 2.5 s, ann2rr in 0.6 s; wrann parses lines with
regular expressions (the lines that sscanf converts completely),
others with csscanf.wfdbx.read_annotations(record, annotator, name, value,
...) and wfdbx.write_annotations(record, annotator, samples, name,
value, ...) are built on these.calopen, getcal ([stat, cal] = wfdbx.getcal(desc, units)),
putcal, newcal, flushcal; the list is the property list (a struct
array of wfdbx.calinfo() structures) of wfdbx.internal.cal_state().
+wfdbx/+internal/flac_*.m port python/src/wfdbx/_flac.py, without
external libraries:
flac_decoder is libFLAC’s stream decoder (1.4.3), step by step, as
in the Python port: its states, frame-sync search, CRC checks, the
return to just after the last sync code after a damaged frame (with its
8192-byte input buffer modelled), the binary search of
seek_absolute, the channel count of each frame, ID3v2 tags. A
structure D is passed in and out ('new', 'process', 'seek',
'flush', 'finish'); a read that finds no input sets D.ni, which
the function where libFLAC returns false clears. Subframes are decoded
with vector operations (Rice codes by pointer doubling over the
positions of the one bits; LPC subframes need a loop over the samples),
and the buffer refills that their reads cause are replayed afterwards.
The test records decode in 0.05-0.3 s.[chans, bps, md5] = flac_decode(bytes) decodes a whole stream; the
first error is thrown with the identifiers
wfdbx:flac:meta|sync|header|crc|unparseable.frame = flac_encode_frame(block, bps, frame_no) encodes one frame
exactly as the Python encoder does (the files written are identical to
Python’s, vendor string wfdb-x).md5(bytes) (Octave’s hash, Java in MATLAB, or the MATLAB code of
md5_pure), flac_crc(bytes, 8|16).Glue contract with the signal module (same functions and arguments as
the Python _flac.isopen, getsamp, …). The input group ig
(IGData) and output group og (OGData) are handle objects; the FLAC
functions use these properties: ig.fp (the open WFDBFile, read with
readall() on the first read), ig.start (offset added to seeks, in
samples), ig.stat, ig.count, ig.flac; og.fp (tell, write,
seek), og.bsize, og.buf, og.flac. ig.flac/og.flac hold the
codec state (a structure; [] when the group is not FLAC).
| Function | Called by | Effect |
|---|---|---|
stat = flac_isopen(ig, hs, ns) |
isigopen, after opening ig.fp (hs: the group’s 1-by-ns header data, fields info.fmt, info.spf) |
checks (at most 8 signals, equal spf), sets ig.flac, ig.count = 0; 0 or -1 |
v = flac_getsamp(ig) |
getvec (per sample) |
next sample in WFDB frame order (per frame, spf samples of each signal in turn); decodes the next FLAC frame when no sample is left (flac_refill); at the end returns 0 with ig.stat = 0 (and the “samples left over” warning once); after a decoding error, 0 with ig.stat = -2; ig.stat unchanged otherwise |
[v, st] = flac_getsamps(ig, n) |
bulk readers | exactly n calls of flac_getsamp (st(k) = ig.stat after each, as ig_read returns); n = Inf returns the samples up to the end of the data or an error |
ok = flac_refill(ig, ahead) |
flac_getsamp; bulk_frames (ahead true) |
decodes frames until samples are added; decoding ahead defers a failure (ig.flac.pending) until the samples before it are read |
stat = flac_isseek(ig, t) |
isgsettime |
positions at frame t (+ ig.start) with libFLAC’s seek; sets ig.stat = 1, ig.count = 0; returns 1, or -1 (the seek failed) |
stat = flac_isclose(ig) |
isigclose |
MD5 check if WFDB_FLAC_CHECK_MD5 is set |
stat = flac_osinit(og, si, ns) |
osigfopen, before opening og.fp (si: 1-by-ns siginfo) |
checks, sets og.flac and og.bsize = ns * spf * 4; 0 or -1 |
stat = flac_osopen(og) |
after opening og.fp |
writes the stream header |
stat = flac_putsamp(v, fmt, og) |
putvec (per sample, frame order) |
buffers the sample; encodes and writes each 4096-sample block |
stat = flac_putsamps(v, fmt, og) |
bulk writers | the same for a vector of samples |
stat = flac_osclose(og) |
osigclose, before closing og.fp |
encodes the rest, rewrites STREAMINFO (seekable files), sets og.flac = [] and empties og.buf |
function status = rdann(args)
%RDANN Print an annotation file in ASCII form.
% STATUS = wfdbx.apps.rdann(ARGS) runs the rdann tool ...
% ... usage as for the C program, example, See also ...
%
% Translation of the C program rdann (app/rdann.c, WFDB 10.7.0): same
% options and output. Differences: ...; see docs/DIVERGENCES.md.
% This file is part of WFDB-x, an independent reimplementation of the WFDB
% Software Package in Python, C++, MATLAB/Octave, Rust, Julia, R and Kotlin.
%
% It is a MATLAB/Octave translation of app/rdann.c from the WFDB Software
% Package 10.7.0, by T. Baker and George B. Moody. ...
%
% Changes from the original: ...
%
% Original C program: app/rdann.c, WFDB Software Package 10.7.0.
% Copyright (C) 1981-2010 George B. Moody; revised 2017-2018 by Benjamin
% Moody.
% MATLAB/Octave translation and changes: Copyright (C) 2026 Reza Sameni.
%
% This program is free software: ... See LICENSE.
if nargin < 1
args = {};
end
status = wfdbx.internal.app_run(@main, [{'rdann'}, args]);
end
function status = main(argv)
% line-by-line port of main(); argc = numel(argv); C argv[i] is argv{i+1}
...
status = 0;
end
exit(n) in C becomes wfdbx.internal.cexit(n) (or status = n; return
in main). Messages go through cprintf/ceprintf (C formats, so
'%s\n'); a tool must never print with fprintf or disp. See
+wfdbx/+apps/wfdbwhich.m and wfdbcat.m.
Tools read the standard input through wfdbx.internal.stdin_bytes(), which
reads it completely on first use (in MATLAB, which cannot read the standard
input, bin/wfdbx_matlab.sh saves it to a file named by the WFDBX_STDIN
environment variable). The record name - refers to the standard input or
output, as in C.
bin/wfdbx_cli.m (an Octave script) adds matlab/ to the path and calls
wfdbx.internal.cli_main(argv()), which calls status =
wfdbx.apps.<tool>(args); the script then exits with status. An unknown
tool, or a MATLAB error in a tool, gives a message on the standard error
output and status 1. bin/wfdbx_matlab.sh saves the standard input to a
temporary file (WFDBX_STDIN), passes the arguments in hexadecimal (decoded
by wfdbx.internal.hexargs, so that any bytes arrive unchanged), and runs
matlab -batch ($WFDBX_MATLAB overrides the MATLAB command); it waits for
the end of its standard input before starting MATLAB. It has not been run
with MATLAB; its MATLAB command has been checked in Octave. Messages that
include the program name use the tool’s name.
Keep start-up work minimal (no work at load time; state is created on first
use); docs/BENCHMARKS.md gives the start-up time
of the tools. In legacy mode, oannclose sorts out-of-order annotation
files by running the sortann tool in a separate process (the same
interpreter, bin/wfdbx_cli.m).
python3 tests/conformance/harness.py check --impl octave # conformance
octave-cli --no-gui --norc -q --eval "cd matlab/tests; run_tests" # unit tests
run_tests runs every matlab/tests/test_*.m (a function without
arguments that throws on failure; assert_eq(actual, expected, what) helps)
and exits with status 1 on failure when run non-interactively.
run_tests('wfdbio') runs the tests whose names match a pattern.