WFDB-x

A fast, lightweight implementation of the WFDB library and command-line tools, with identical behavior across seven programming languages

View the Project on GitHub alphanumericslab/wfdb-x

WFDB-x for MATLAB and GNU Octave: implementation notes

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).

Requirements

Portability rules

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:

Layout

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.

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.

Data representation

Formatted output

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.

Library state

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.

Performance

Octave executes scalar code slowly (around a microsecond per simple statement), so per-sample loops are avoided on common paths:

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.

Internal API (foundation)

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 ://).

Signals (lib/signal.c)

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).

Annotations (lib/annot.c)

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().

Calibration (lib/calib.c)

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().

FLAC (formats 508, 516, 524)

+wfdbx/+internal/flac_*.m port python/src/wfdbx/_flac.py, without external libraries:

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

Writing a tool

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.

Standard input and the command line

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).

Testing

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.