A fast, lightweight implementation of the WFDB library and command-line tools, with identical behavior across seven programming languages
WFDB-x reproduces the WFDB Software Package 10.7.0 (C) exactly, including behaviors that may look surprising. This document lists the notable ones, so that users (and implementers of new language ports) know they are intentional, and says for each one whether the official documentation describes it.
Sources checked: the WFDB Applications Guide (third_party/wag.pdf, built
from the man pages in doc/wag-src/ of the C sources), the WFDB
Programmer’s Guide (doc/wpg-src/wpg0.tex, cited as WPG), and comments in
the C sources. Where the documentation and the C code disagree, WFDB-x
follows the code.
The deliberate exceptions (no built-in signal parameters, files found only
where they are named, in-process annotation sorting that loses nothing, no
output signal file over an input signal file, and exit status 1 after a write error) are
described in DIVERGENCES.md.
A few C behaviors are undefined (crashes, infinite loops, reads of
uninitialized memory). WFDB-x cannot reproduce those; for each one, the
behavior chosen by WFDB-x is stated below. The conformance tests check a
few of these choices, with reference results recorded from WFDB-x instead
of C (the cases with golden_impl in tests/conformance/cases.py); they
never run the C tools on such input.
The reference platform is x86-64 Linux with glibc: char is signed,
long is 64 bits, and the library is built with -DWFDB_LARGETIME
(so WFDB_Time is 64 bits).
The stored reference results were produced on that platform. These C behaviors can differ elsewhere, and WFDB-x reproduces the reference platform’s:
a*b + c as one
fused multiply-add (Clang does by default, e.g. Apple Clang on ARM),
which rounds once instead of twice and can change results, such as the
baselines edf2mit computes. WFDB-x’s C++ build, and the build of the C
reference, turn contraction off (-ffp-contract=off); the other
implementations never contract.0/0. x86-64 produces a NaN with its sign bit set (which
printf shows as -nan), ARM one without. WFDB-x’s tests construct the
NaNs they format from explicit bit patterns.xform -i vlay -o fmt16, where
record vlay reads fmt16.dat, which -o fmt16 rewrites), what is read
depends on how much of the file the C library had read ahead before it
was truncated: glibc reads 4096-byte blocks. WFDB-x refuses such a
command (see DIVERGENCES.md); in legacy mode it
reproduces glibc’s result, but an implementation whose own runtime reads
ahead further (R’s file connections on macOS, for example) reads more of
the old file. That is why the legacy conformance case
xform-i_vlay_o_fmt16_n_xf_f_s1320 fails for R on macOS (180 frames
instead of 45); every other case passes there.These look surprising but are described in the documentation.
r8() returns a signed char, so format 8
first differences are signed bytes. signal(5): “Each sample is
represented as an 8-bit first difference” and differences that do not fit
“are represented instead by the largest difference of the appropriate sign
(-128 or +127)”.wfdbquit() will correctly write out the remaining samples, adding padding
if necessary”; signal.c (f310): “the file will appear to have an extra
(zero) sample at the end”.getvec prints getvec: checksum
error in signal N and returns -4 for that frame. WPG (getvec): “-4
Failure: checksum error (detected only at end of file)”. Applications that
loop while getvec(...) >= 0, such as rdsamp, therefore stop before
printing the last frame (this consequence is not documented). WFDB-x’s
C-style functions and tools do the same. Its high-level readers (an
addition) return every frame, the last one included, since all of them
were read, and give a warning, checksum error in record REC: the signal
data do not match the checksums in its header, in each language’s usual
way (Python warnings, R warning(), Julia @warn, MATLAB/Octave
warning(); the standard error in C++, Rust and Kotlin, also silenced by
wfdbquiet()). The checksum_warning option turns the warning off, and
the record’s checksum_ok field is false either way; this behaves
identically in all seven implementations.annopen consumes the NOTE annotations at time
0 (subtype 0) at the start of an annotation file, so getann does not
return them. WPG (glossary, “Modification label”): “An ‘invisible’
annotation at the beginning of an annotation file … When annopen …
opens an annotation file that contains modification labels, it
automatically calls setannstr and setanndesc”, and “When annopen …
creates an annotation file, it automatically generates modification labels,
for each annotation code that has been (re)defined”. So reading a file
whose table redefines mnemonics makes every annotation file written later
in the same process repeat the table. After iannsettime(0) the labels
are returned like other annotations; rdann(1): “if modification labels are
present, they are not printed unless -f 0 is given explicitly”.
(Undocumented details: see the next section.)setafreq() with a
positive value (e.g. wrann, which calls setafreq(sampfreq(record)))
writes a ## time resolution: NOTE at the start of its output files. WPG
(changes in 10.6.0): “the time resolution and annotation type definitions
will not be written until the first time putann() is called”.sortann as a separate process, unless
WFDBANNSORT=0; Rearranging annotations for output annotator ...
done! is printed on the standard error output. WPG (“Annotation
Order”, wfdbquit, oannclose) documents the mechanism and WFDBANNSORT;
the message text is not documented. WFDB-x sorts in-process instead,
writing the file sortann writes; the separate process remains in legacy
mode, and the high-level write_annotations has a per-call sort option
(see DIVERGENCES.md).sigmap(), halfway cases rounded up) and the
sigmap_init: loss of precision ... warning, printed when a segment’s
gain exceeds that of the layout, are not documented.setcfreq is overridden by each header read: WPG: “The effect of
setcfreq is nullified by later invoking any of the functions that read
header files” (readheader sets the counter frequency to the sampling
frequency when the header gives none).sortann keeps only the last of several annotations with the same
time, num and chan, and drops the location entirely if that last one
is NOTQRS. sortann(1): “If the input contains two or more annotations
with the same time, num, and chan fields, only the last one is
copied”, and “If the input annotations are already in the correct order,
no output is written unless you have used the -o option” (so an
in-order file keeps its NOTQRS annotations unless -o is given). A
duplicate always counts as out of order, so such a file is rewritten.wrann turns an unrecognized mnemonic into a NOTE whose aux string
starts with the mnemonic. rdedfann(1): “Recent versions of wrann copy
unrecognized mnemonics into the aux field, setting the annotation type to
NOTE”.mit2edf uses the current date and time as the start of a record that
has none, and writes Startdate not recorded. mit2edf.c: “start
date/time not recorded – use current date”; WPG (setbasetime): “If
string is empty or NULL, the current date and time are read from the
system clock”.Neither the man pages nor the Programmer’s Guide describe these. Items marked bug are almost certainly unintended by the C authors.
annopen consumes every time-0,
subtype-0 NOTE at the start of the file, even one that is not a valid
table entry (aux text that is not ## ... or CODE MNEMONIC [DESCRIPTION],
or no aux string at all). It stops at the first other annotation. The
library ends each table it writes with a time-0 NOTQRS marker, so NOTEs
written by an application after a table are not affected.putann treats an annotation at
time 0 that follows another at time 0 as in order. Annotations written
twice at the same time-0 location therefore do not trigger the automatic
sortann, and both copies remain (contrary to WPG, see “Documentation and
code disagree”).setheader modifies the caller’s units and desc strings (it
calls strtok on them, cutting units at the first blank and desc at
the first line break), although WPG declares the argument as
const WFDB_Siginfo *. Bug.wfdbquit. The counter frequency and
base counter value (see “Documentation and code disagree”), the output
annotation time resolution (setafreq, which setifreq also calls), the
buffer sizes (setibsize, setobsize), the mnemonic and description
tables, and the flags of codes whose mnemonics were modified (which make
every later output annotation file start with a table) are not reset.
resetwfdb (called by wfdbquit) restores the WFDB path to “the value
that was returned by the first call to getwfdb” (comment in wfdbio.c),
so only if getwfdb was called before any setwfdb; it restores the
string only and does not re-parse the search path.setisqrs and the other ecgmap.h setters store the new value in a
char, so wfdb_setisqrs(2, 300) returns 44. (WPG only says that x “is
the value to be returned when the corresponding mapping macro is
invoked”.)osigclose: write error ... or oannclose: write error ...), when the
standard output cannot be written (nothing is printed), and when a file
that snip, mit2edf or parsescp writes itself fails (nothing is
printed). WFDB-x reproduces this in legacy mode; otherwise the status is
1 (DIVERGENCES.md).ann2rr -i s|m|h prints intervals rounded to the requested number
of digits and then adjusts the end time of each interval to match the
printed (rounded) value, so rounding errors do not accumulate. The
adjusted time is also the start of the next interval and the time
printed by -v.ann2rr -h pauses after 23 lines of help (--More--), reading a
line from the standard input (as nguess, pschart and psfd do).ann2rr names the wrong option in its error messages for -p and
-P (must follow -P for -p, and the reverse).wrann keeps the values of the subtype, chan and num fields from
the previous line when a line omits them.sortann loses annotations: when an annotation at time 0 with num
0 and chan 0 arrives after later ones, it is dropped, and so is every
annotation inserted after it in the list (input sample times 5, 0, 3 give
a file with only 5; 5, 1, 3 are sorted correctly). The search for the
insertion point also compares the annotation with the list’s empty head
entry, whose time, num and chan are 0, takes it for a duplicate of
that entry, and links it in the head’s place, where the output loop
(which starts from the head) never reaches it; annotations later
inserted between it and the rest of the list are lost with it.
sortann(1) says only that the last of several annotations at
the same location is kept. The library’s automatic sorting, which runs
sortann, loses them in the same way. Bug. Fixed in WFDB-x, except in
legacy mode (see DIVERGENCES.md).rr2ann reads each line with sscanf("%lf"). For an empty or
all-blank line, sscanf returns EOF rather than 0, so the previous
value is used again and an extra annotation is written. rr2ann(1) says
empty lines are ignored. Bug.revise rejects an old-style header that describes exactly 32
signals (init: too many signals), because its loop over the signals of
a group stops at WFDB_MAXSIG before counting the last one; 31 signals
are accepted. WFDB_MAXSIG (32) is documented as the “maximum number of
input or output signals”. Bug.edf2mit reads header fields into one reusable 81-byte buffer and,
for signal labels, starts removing trailing blanks at byte 15 instead of
byte 16. A 16-character label is therefore followed by a space and by
whatever bytes remain in the buffer from earlier (longer) fields, up to
the next NUL. Bug.rdedfann never clears the duration of the previous TAL, so an
annotation whose TAL has no duration repeats the last duration printed.
(In EDF+, a duration belongs to its own TAL.) Bug.ahaecg2mit, for .ecg input, writes the annotation of sample k at
time k-1 (its time counter starts at -1 and is incremented after the
annotation is written). Every annotation is one sample early, and one on
the first sample is written at time -1, which makes the library sort the
output file when it is closed. For .txt input the counter is
incremented first and the times are correct. Probably a bug.ahaecg2mit names argv[1] instead of the failing file in its
can't open message.parsescp reads the encoding type of sections 5 and 6 from byte 4
of the section data (the high byte of the sample interval) instead of
byte 5, so a 2000 µs interval gives “Encoding type: 7”, treated as second
differences, and it reads the section 6 compression type from byte 5
(which holds the encoding type in the SCP-ECG layout), so second-difference
data give “Compression type: 2”, treated as bimodal. Bug (harmless for
the SpaceLabs/Burdick files the program was written for).readid prints the whole first word of the 24-byte ID block, even if
it is longer than 8 characters. (sscanf("%s") writes it into a 9-byte
buffer; strictly this is a buffer overflow, but its result is
deterministic and is reproduced.)In each case WFDB-x follows the C code.
| Topic | Documentation | C code (and WFDB-x) |
|---|---|---|
wfdbquit |
WPG: resets “the factors used for converting between samples, seconds, and counter values … and the base counter value (reset to 0)” | the counter frequency (setcfreq/getcfreq) and the base counter value are not reset |
| Annotations at one location | WPG (“Annotation Order”): “only the last annotation written to any given location is retained” | true only when the file is sorted; repeated annotations at time 0 do not trigger sorting, so all copies are kept |
setheader |
WPG prototype takes const WFDB_Siginfo * |
modifies units and desc in place (strtok) |
setafreq |
WPG (setafreq): no effect on output annotation files “already open” |
takes effect for open files until their first putann (as WPG’s 10.6.0 notes say) |
sortann |
sortann(1): automatic sorting is disabled by setting WFDBNOSORT |
the variable is WFDBANNSORT=0 (WPG: “WFDBNOSORT was replaced by WFDBANNSORT”); WFDBNOSORT is ignored |
rr2ann |
rr2ann(1): empty lines and blanks at the start of a line are ignored | an empty or all-blank line repeats the previous interval |
wrsamp -c |
checks that each line has the same number of fields | accepted but has no effect |
wrsamp -l n |
reads up to n characters per line (default 1024), truncating longer lines with a warning | prints -l is obsolete, ignored; lines of any length are read whole |
wrsamp separators |
default field separators are spaces and tabs; -s c makes c the only separator |
default separators are space, tab, CR, LF and comma; -s takes a set of characters |
edf2mit default record name |
“use the patient ID field from the input file” | the input file name up to its first ., with characters other than letters and digits replaced by _, truncated to 50 characters |
wav2mit default record name |
“the base name of the input file” | the input file name without .wav, including any directory part |
parsescp byte order |
.ecg and .dat samples are big-endian |
little-endian on the reference platform (.ecg: written with native byte order, since SWAP is not defined; .dat: written by the library in format 16) |
parsescp missing samples |
“each missing sample is assigned a special value (WFDB_INVALID_SAMPLE, or -32768)” | a missing lead is written as zeros (parsescp.c: “Allocate an array of zero samples to use for the missing lead(s)”); -32768 appears only with -z |
revise |
WFDB_MAXSIG = 32 is the maximum number of signals |
32 signals are rejected |
Where the documentation states what should happen, the last column says whether the WFDB-x choice matches it.
| Program / function | C behavior | WFDB-x behavior |
|---|---|---|
time strings whose value is out of the range of WFDB_Time (e.g. 1e300 seconds), or frame numbers (fN) when no record is open (frame frequency 0) |
strtim converts an out-of-range double (infinite or NaN for fN) to WFDB_Time |
LONG_MIN, as the x86-64 conversion gives ([...] times: the subtraction of the base time wraps around, as WFDB_Time arithmetic does on x86-64; the MATLAB/Octave and R ports hold times in doubles, exact up to 2^53) |
isgsettime(g, t) with a group number g that is not that of a signal group, when the input pointer is already at t (this also happens inside isigsettime when opening a segment of a multi-segment record leaves fewer signal groups, e.g. a zero-length first segment followed by segments with fewer signal files) |
isgsetframe reads igd[g] past the end of its array before checking g (crash, or a return value of 0) |
the group number is reported as incorrect (isgsettime: incorrect signal group number, -2), as for other times. A group that isigopen allocated but could not open (a segment whose header is valid but none of whose signal files can be read) is not past the end: C reads its zeroed slot, needs no seek and prints nothing, and so does WFDB-x |
a multi-segment record whose segment lengths add up to more than the range of WFDB_Time (e.g. a segment length of 99999999999999999999, read by strtol as LONG_MAX) |
the sum overflows (signed overflow) | the sum wraps around as a two’s-complement 64-bit integer, as on x86-64 (the warning about the stated record length shows the wrapped value) |
reading a multi-segment record after a segment that could not be opened because its frame is too large (sigmap_init: frame size too large) |
the signal map of the earlier segment is kept, and sigmap reads past the end of the segment’s input vector (usually 0 is read) |
a sample beyond the input vector reads as 0 |
snip when the last frame to copy cannot be read and no frame was read before it (e.g. a segment that cannot be opened) |
putvec writes the sample vector anyway, which is uninitialized memory (malloc) |
the vector is initially 0, so 0 is written |
mit2edf with a record whose sampling frequency is nan |
the number of frames per data record is computed by an integer division by zero (SIGFPE) | the EDF header is written with no data records, and the exit status is 0 |
edf2mit with an input file shorter than the 256-byte fixed part of the EDF header |
the fields that were not read are taken from uninitialized memory: the number of signals and the output differ from run to run | edf2mit: incomplete EDF header in FILE, no output files, exit status 2 |
xform changing the sampling frequency or gain of samples close to the limits of an int (format 32, or large gains) |
int overflow in the difference of consecutive samples, and conversions of out-of-range doubles to int |
as on x86-64: the difference wraps around, and out-of-range conversions give INT_MIN |
edf2mit with a signal that has 0 samples per data record (sampling frequency 0) |
the number of samples per frame is the conversion of 0.0/0.0 (NaN) to int; with another signal of nonzero frequency, gcd() loops forever |
INT_MIN (as on x86-64); the loop is not avoided |
reading an EDF file (rdsamp, with or without -f, or any program that reads its samples) whose signals have 0 samples per data record, or a blank field |
division by zero (SIGFPE) | a signal group whose frames hold no samples is at its end at once: no samples are read |
a header whose number of signals is negative or larger than the number of signal lines that follow (e.g. n -1 250 10) |
the count is converted to an unsigned size and allocated (can't allocate (4294967295*8) bytes for hsd, exit status 1, or, where the allocation succeeds, unexpected EOF in header file) |
init: unexpected EOF in header file for record n, at once, without allocating for the declared count |
| a header block size of -2147483648 | negating INT_MIN overflows, and the result is converted to an unsigned size (can't allocate) |
the block size is 2147483648, as the negation of a negative block size intends; the read buffer is never larger than the signal file |
reading a FLAC signal file (formats 508, 516 and 524) of 3 or more samples per frame whose FLAC block size is not a multiple of them (e.g. format 508x3 with blocks of 1001 samples: the test record flacspf) |
iflac_samples makes room for one block plus one frame, but the block also completes the frame left unfinished by the previous block: it writes past the end of its buffer (heap corruption; an abort such as free(): invalid next size, or no visible effect) |
the samples are read correctly |
xform -M with an output record whose signals have more samples per frame than the input signals |
the array of output samples is sized for the input frame: the output frame is written past its end, and the input frame is read past its end (free(): invalid pointer, abort, or a segmentation fault) |
both arrays have room for every sample; the output samples are those C computes, with 0 for the input samples beyond the input frame |
wfdb2mat -r R with a one-character record name |
reads the byte before the name to build the output name: writes R.mat and R.hea, overwriting the input header |
the output record is Rm (Rm.mat, Rm.hea), as for longer names |
mit2wav with a signal of more than 16 bits of ADC resolution (formats 24 and 32) |
shifts by a negative amount: on x86-64 the gain multiplier becomes 65536 times too large and the samples are written as 0 | the samples are shifted right to 16 bits, and the gain multiplier is 0 |
edf2mit with a start date or time field that does not hold three numbers (e.g. a NUL byte in the field) |
the day, month, year, hour, minute or second not read by sscanf is an uninitialized variable |
0 is used |
rdsamp -c/-X with a signal description or units string containing ' or \ |
escapify() loops forever or crashes |
the characters are escaped with a backslash (the evident intent of escapify(); wrsamp, which rdsamp(1) says parses -X output, treats \ as an escape) |
wrann input line with no space after column 9 (including fragments of lines longer than 399 bytes) |
scans past the end of the line buffer (crash or garbage) | the remainder of the line is treated as empty |
wrann input with a mnemonic longer than 9 characters |
buffer overflow | the full mnemonic is used (matches rdedfann(1): unrecognized mnemonics are copied into the aux field) |
wrann description longer than its 256-byte aux buffer |
buffer overflow | the aux string is truncated as for shorter overflows (the intent of the truncation code) |
headers whose format field ends with x, : or + |
reads past the end of the token | the empty field is ignored (as if absent; header(5) makes these fields optional) |
wrsamp input line ending with a field separator (e.g. trailing space, or CRLF line endings) |
heap corruption (malloc(): corrupted top size, abort) |
the trailing separator is ignored (matches wrsamp(1): “no special treatment is required for files containing both carriage returns and linefeeds”) |
wrsamp line with fewer fields than selected columns |
uses a dangling pointer | the missing sample is written as invalid (matches wrsamp(1), which records missing and non-numeric entries as invalid samples) |
signame -s N where N equals the number of signals |
reads past the end of an array (segfault) | prints [INVALID] (matches signame(1): “If the specified signal does not exist in record, signame outputs [INVALID]”) |
sortann replacing a duplicate annotation whose earlier copy is at the head of its list (e.g. two time-0 annotations with a negative num; or an earlier copy that follows a time-0, num 0, chan 0 annotation, which sortann itself drops) |
reads freed memory (the C program then drops the earlier annotations) | the duplicate is replaced and the earlier annotations are kept (matches sortann(1) and WPG: only the last annotation at a location is dropped) |
xform -o OREC without -M, where OREC has signals with more than one sample per frame |
putvec reads one sample per output sample of a frame from an output array that holds only one per signal (reads past its end) |
the output signals are (re)opened with one sample per frame, as xform does for output signals specified interactively (without -M, xform(1) processes one sample of each signal per frame; with -n or -N, the new header says so) |
md2a with a multi-frequency record (including EDF files, in which each signal has as many samples per frame as per data record) |
putvec reads one sample per output sample of a frame from a local array of WFDB_MAXSIG elements of which getvec sets only the first nsig: the others are uninitialized, and frames of more than WFDB_MAXSIG samples read past the array (crash) |
the array has one element per output sample of a frame (at least WFDB_MAXSIG), initially zero: each frame holds the getvec samples followed by zeros |
snip with -a given more than once |
the count of annotators is not reset: the names are the arguments that follow the last -a, as many as all the -a options name together, continuing into the following arguments (e.g. -a atr -a qrs -f 10 opens annotators qrs and -f, which fails); past the end of the argument list it reads argv[argc] (a null pointer: crash) and beyond |
as in C, except that names beyond the last argument are ignored (-t 5 -a atr -a qrs copies qrs only) |
edf2mit without -r, with an input file name that has no . and is shorter than 50 characters |
the terminating NUL becomes _ and the loop reads past the end of the name |
the record name ends with the file name |
wav2mit without -r |
the record name contains one uninitialized byte | the input name without .wav is used (wav2mit(1): “the base name of the input file”) |
ahaecg2mit with .txt input |
each line is read into a 16-byte buffer that is not NUL-terminated and keeps the bytes of earlier lines; sscanf reads past its end (output depends on stack contents) |
the buffer is emulated, with bytes beyond it treated as NUL (the buffer, the samples and the annotation start from zero for each input file) |
ahaecg2mit with an input file name of fewer than 4 characters before the extension |
the record name is taken from memory before the string | the available characters are used |
ahaecg2mit -s with a record name of fewer than 2 characters |
the terminating NUL of the name is incremented, so the name runs into the following memory | the name is left unchanged (the documented renaming, n0nn to n2nn and n1nn to n3nn, applies to 4-digit AHA names) |
a2m, ad2m without -r, with an input file name ending in / |
the extension search starts past the end of the (empty) record name | the record name is empty |
ad2m with a record name of more than 7 characters |
the signal file name overflows its 12-byte buffer | the full name (RECORD.dat) is used |
| A header that gives no description for a signal, with a record name (or path) longer than about 85 characters | readheader writes the default description “record NAME, signal N” into a 101-byte buffer (lib/signal.c), overflowing the heap; the C tools crash (malloc(): invalid next size) |
the full description is used |
a2m, ad2m with a directory as input file (-i DIR) |
reads forever | can't read input file, exit status 2 |
mit2edf, makeid |
uninitialized variables in rare cases (e.g. a single annotation) | 0 is used |
parsescp -v/-x with a section 7 (global measurements) |
the dump of the remaining bytes of section 7 reads one byte past its end (past the end of the input if section 7 is the last section) | bytes beyond the end of the input are read as 0 |
parsescp with a lead shorter than lead 0 (or than 5000 samples), or whose sample count cannot be allocated |
reads past the end of the lead’s array, or dereferences a null pointer | the missing samples are read as 0, as the C code does for missing leads (parsescp(1) documents -32768 for samples missing from a recording shorter than 10 seconds) |
parsescp with lead indices outside the signal array (a section 3 longer than its lead table, whose entries past the declared number of leads set the indices of leads such as I or V2; or indices left over from an earlier section 3 with more leads) |
reads and writes outside the array of signals (heap corruption, abort) | such a lead reads as an array of zero samples, like a missing lead, and its sample count as 0 |
parsescp with a section 3 after section 5 that gives more leads |
reads reference beats past the end of their array | the missing reference beats are absent (nothing is added to those leads) |
parsescp with QRS subtraction zones missing from section 4, or zones (shifted by -s) outside the signal or the template |
uses uninitialized memory, or writes outside the arrays | missing zones are ignored, and samples outside the arrays are skipped |
parsescp with no date or time of acquisition, or an invalid month, in section 1; patient ID, referring physician or comments longer than their 30-, 100- or 200-byte buffers |
null pointer dereference, read outside the month table, or an unterminated string | the date or time is left empty, the month is printed as ???, and the strings are truncated |
wfdbversion, wfdbldflags, wfdbcflags and wfdbdefwfdb return strings
fixed when the C library is configured and built (installation prefix,
compiler flags), and wfdberror (before any error) includes the build date.
All ports of WFDB-x return the values of a release build of 10.7.0
installed in /usr/local: "10.7.0", "", "", ". /usr/local/database
http://physionet.org/physiobank/database", and "WFDB library version
10.7.0 (Jun 10 2022).\n". wfdbdefwfdbcal is "wfdbcal", as in C.