A fast, lightweight implementation of the WFDB library and command-line tools, with identical behavior across seven programming languages
WFDB-x is a reimplementation of the WFDB Software Package (the library and its command-line tools) in seven languages: Python, C++, MATLAB/Octave, Rust, Julia, R and Kotlin, tested to give byte-for-byte identical results to the original C package and to each other. This is the C++ implementation. Project page and the other languages · WFDB-x at a glance (overview figure) · Benchmarks
libwfdbx reads and writes physiological signal records in the WFDB format
used by PhysioNet. It is a complete C++17
implementation of the WFDB Software Package: the library and all of its
command-line tools. It reads and writes the same files as the original C
library, and its tools print the same output.
Everything is in namespace wfdbx. The library needs only the C++17
standard library. It builds with CMake 3.14 or later and a C++17 compiler whose
standard library has std::filesystem (GCC 9, Clang 9 or MSVC 2019 and
later; macOS 10.15 or later), on Linux, macOS and Windows.
All three are written in C++; none of them calls the C library.
read_record,
read_header, read_annotations, write_record, write_annotations
and check_record, in <wfdbx/record.hpp>, read and write whole
records and annotation files as std::vectors, in one call, with
options given as fields of a struct (ReadOptions, WriteOptions), and
throw wfdbx::Error on errors. They read signal files in large blocks,
so they are the fastest way to read a record. They are an addition of
WFDB-x: the C package has no equivalent. Use them for new code and
analysis.<wfdbx/wfdb.hpp>, under their C
names (isigopen, getvec, getann, putann, …), with the C
behavior: sample by sample and annotation by annotation, with errors
reported by return values. Use them to port a program written against
the C library, or for the exact C semantics.rdsamp, rdann, wrsamp, xform, and
29 others), with the options, output and exit status of the C tools,
built as separate programs and run by their names (rdsamp -r 100s).
Use them in shell scripts and pipelines, or to reproduce work done with
the original tools.The high-level functions return exactly the samples and annotations that the library functions (and the C library) give.
From the root of a copy of the repository, build the library, the tools and the unit tests, then install them:
cmake -S cpp -B cpp/build # a Release build
cmake --build cpp/build -j8
cmake --install cpp/build --prefix /usr/local # optional
The library is cpp/build/libwfdbx.a and the tools are in cpp/build/bin/.
The install step copies the library, the headers (include/wfdbx/), the
tools and a CMake package file. These options change what is built:
| Option | Default | Effect |
|---|---|---|
WFDBX_BUILD_TOOLS |
ON |
build the command-line tools |
WFDBX_BUILD_TESTS |
ON |
build the unit tests |
BUILD_SHARED_LIBS |
OFF |
build a shared library |
To use the installed library from a CMake project, add these lines to its
CMakeLists.txt:
find_package(wfdbx REQUIRED)
target_link_libraries(myprogram PRIVATE wfdbx::wfdbx)
To compile a program directly, link it with -lwfdbx:
c++ -std=c++17 myprogram.cpp -lwfdbx
The repository includes a sample record, 100s, in tests/data: the first
minute of record 100 of the
MIT-BIH Arrhythmia Database, with two
ECG signals at 360 Hz and its beat annotations. This program reads it and
writes a copy of its first 10 seconds:
#include <cstdio>
#include <wfdbx/record.hpp>
int main() {
// Read the sample record from the tests/data directory of the repository.
wfdbx::wfdbx_set_input_dir("tests/data");
wfdbx::ReadOptions opt;
opt.to = "0:10"; // the first 10 seconds
wfdbx::Record rec = wfdbx::read_record("100s", opt);
std::printf("%g %d %zu\n", rec.fs, rec.nsig, rec.nsamples()); // 360 2 3600
std::printf("%d %g\n", rec.at(0, 0), rec.physical(0, 0)); // 995 -0.145
wfdbx::Header hdr = wfdbx::read_header("100s"); // the header only, no samples
std::printf("%d %lld\n", hdr.nsig, hdr.nsamples); // 2 21600
wfdbx::Annotations ann = wfdbx::read_annotations("100s", "atr");
std::printf("%zu %s %lld\n", ann.size(), ann.symbols[1].c_str(),
ann.samples[1]); // 75 N 77
// Write the 10 seconds as a new record in the current directory.
wfdbx::WriteOptions w;
w.gains = {200};
w.baselines = {1024};
w.adcres = {11};
w.units = {"mV"};
w.names = rec.names();
wfdbx::write_record("copy", rec.samples, rec.nsig, rec.fs, w); // copy.hea, copy.dat
}
Compile it and run it from the root of the repository:
c++ -std=c++17 -Icpp/include quick.cpp cpp/build/libwfdbx.a -o quick
./quick
The functions of <wfdbx/record.hpp> return their results by value, and
throw wfdbx::Error when they fail.
read_recordread_record(record, opt) reads the signals of a record. The options are
fields of a ReadOptions. The examples of this section read the sample
record 100s (the first minute of record 100 of the MIT-BIH Arrhythmia
Database), which is included with WFDB-x in the tests/data directory of a
copy of the repository; run them from the root of the repository:
wfdbx::ReadOptions opt;
opt.input_dir = "tests/data"; // where the sample record 100s is
opt.channels = {"MLII"};
opt.from = "0:10";
opt.to = "0:20";
wfdbx::Record rec = wfdbx::read_record("100s", opt);
std::vector<double> mlii = rec.physical_signal(0); // 3600 values
Field of ReadOptions |
Meaning | Default |
|---|---|---|
channels |
signals to read, by number ("0") or name ("V5", as in names()) |
all signals |
from, to |
first sample, and the sample after the last one: a sample number or a time string (see below) | the whole record |
highres |
read a multi-frequency record at its highest sampling frequency | false |
pad |
replace invalid samples by the previous valid one | false |
checksum_warning |
warn when the data do not match the header’s checksums | true |
input_dir, network |
input directory and network access for this call only | the current settings |
legacy |
behave as the original package (see Legacy mode) | false |
Times can be sample numbers or WFDB time strings: "0:10" (10 seconds),
"1:02:03.5", "s3600" (sample 3600) or "e" (the end of the record).
opt.from = 3600; and opt.from = "0:10"; both work.
The result is a Record:
| Member | Contents |
|---|---|
samples |
the samples in ADC units, frame by frame: samples[i * nsig + s] is sample i of signal s |
at(i, s), physical(i, s) |
sample i of signal s, in ADC units or in physical units |
physical_signal(s) |
all the samples of signal s in physical units, as a std::vector<double> |
nsamples(), nsig |
the number of samples per signal, and the number of signals |
fs |
sampling frequency in Hz (0 if the header gives none) |
names() |
the description of each signal, without the white space that a header may put at either end of it |
siginfo |
the signal specifications (WFDB_Siginfo): desc, units, gain (ADC units per physical unit; 0 means uncalibrated), baseline, adcres, fmt, … |
signals |
the number, in the record, of each signal returned |
sampfrom |
the number of the first sample returned |
base_time |
start time and date as text, to the second, such as "[14:00:03 21/06/2024]" |
base_time_ms |
start time in milliseconds after midnight, such as 50403500 for 14:00:03.500 |
base_date |
start date as {year, month, day} |
comments |
the info strings of the header |
checksum_ok |
false if the data do not match the header’s checksums |
base_time, base_time_ms and base_date are std::nullopt when the
header does not give them, and so are the units of a signal. Invalid
samples are WFDB_INVALID_SAMPLE, or NaN in physical units.
read_headerread_header(record) reads the header of a record without opening its
signal files. It returns a Header with these fields: name, fs,
counter_freq, base_counter, nsamples (0 if the header does not give
the length), nsig, siginfo, base_time, base_time_ms, base_date and
comments. They have the same meaning as in Record.
wfdbx::wfdbx_set_input_dir("tests/data"); // the sample record 100s
wfdbx::Header hdr = wfdbx::read_header("100s");
for (const wfdbx::WFDB_Siginfo &s : hdr.siginfo)
std::printf("%s %d %g %d\n", s.desc->c_str(), s.fmt, s.gain, s.adcres);
The full signature is read_header(record, legacy, input). input is an
InputOptions with the fields input_dir and network, as in
ReadOptions.
read_annotationsThis reads the annotations of the sample record 100s from 10 to 30
seconds:
wfdbx::wfdbx_set_input_dir("tests/data"); // the sample record 100s
wfdbx::Annotations ann = wfdbx::read_annotations("100s", "atr", "0:10", "0:30");
The full signature is
read_annotations(record, annotator, from, to, fs, legacy, input).
annotator is the file suffix ("atr" reads 100s.atr). The optional fs
gives the sampling frequency when the record has no header, or its header
gives none. input is an InputOptions, as for read_header. The result
is an Annotations object, in file order:
| Member | Contents |
|---|---|
samples |
annotation times, in samples |
codes, symbols |
annotation codes (wfdbx::NORMAL, wfdbx::PVC, …) and their mnemonics ("N", "V", …) |
subtypes, chans, nums |
subtype, channel and annotator number of each annotation |
aux |
auxiliary text of each annotation, or std::nullopt |
fs |
the frequency in which samples are counted (0 if unknown) |
size() |
the number of annotations |
write_recordwrite_record(record, samples, nsig, fs, opt) writes samples in ADC units.
write_record_physical takes the same arguments, with samples in physical
units. This writes a new record, rand, of two signals of random values:
std::mt19937 gen(1);
std::normal_distribution<double> randn;
std::vector<double> x(3600 * 2); // 3600 frames of 2 signals
for (double &v : x) v = randn(gen);
wfdbx::WriteOptions w;
w.gains = {1000};
w.adcres = {16};
w.units = {"mV"};
w.names = {"A", "B"};
wfdbx::write_record_physical("rand", x, 2, 360, w);
This writes the header rand.hea and the signal file rand.dat. The
samples are given frame by frame, as in Record::samples. The sampling
frequency, the gain and ADC resolution of each signal, and the units of each
calibrated signal are required; there are no defaults. A gain of 0 marks a
signal as uncalibrated, which is only allowed for samples given in ADC
units; negative gains are allowed. The sampling frequency and the gains must
be finite. Per-signal fields take one value for all signals, or one value
per signal; any other length throws wfdbx::Error.
Field of WriteOptions |
Meaning |
|---|---|
fmt |
storage format, such as 16, 212, 24 or 516 (FLAC); default 16 |
gains, adcres, units |
gain, ADC resolution in bits, and physical units |
baselines, adczeros, names |
optional signal specifications |
base_time, comments |
start time ("hh:mm:ss dd/mm/yyyy") and info strings |
samps_per_frame |
samples per frame of each signal, for a multi-frequency record |
dir |
directory in which to create the files |
legacy |
behave as the original package (see Legacy mode) |
write_annotationsThis writes the annotation file rand.qrs for the record rand written
above:
wfdbx::Annotations qrs;
qrs.samples = {100, 460, 820};
qrs.symbols = {"N", "N", "V"};
wfdbx::write_annotations("rand", "qrs", qrs);
The full signature is write_annotations(record, annotator, ann, fs, dir,
sort). Give the annotation types as symbols or as codes. Both may be
given, as read_annotations returns them, if they agree: each non-empty
symbol must be the mnemonic of the code at the same position (otherwise
“symbols and codes disagree at annotation K”, K counted from 0). subtypes,
chans, nums and aux are optional. Each of these fields is either empty
or has one entry per annotation; any other length throws wfdbx::Error
before anything is written. subtypes, chans and nums are stored in 8
bits, as in C (300 is stored as 44). fs is 0 (none) or a finite
frequency greater than 0, which is recorded in the file. dir is the
directory in which to create the file. The arguments are checked before the
file is created.
If the annotations are not in time order, the file is sorted when it is
written. sort = false keeps the given order (without the library’s
advice to run sortann) and sort = true always sorts, for that call
only. Without sort, the WFDBANNSORT environment variable decides
(sorting is on unless it is 0).
wfdbx::Error, such as “can’t read record nosuchrecord”. e.what()
returns the message.wfdbx::Error:
“can’t read signal data of record REC (getvec status -3)”.wfdbx::Error: “can’t read annotation file ANN of record REC (getann
status S)”. A read that stops (at to) before the damaged part returns
its annotations.wfdbx::Error. For example, a time string such as "0:10"
needs the sampling frequency (“sampling frequency of record REC is not
specified”), and physical() needs the gain of the signal (“signal N of
record REC is uncalibrated”).read_record
returns all the data and prints a warning on the standard error: “WFDB-x:
checksum error in record REC: the signal data do not match the checksums
in its header”. checksum_warning = false turns the warning off;
rec.checksum_ok is false either way. Checksums are only checked when a
read reaches the end of the record.write_record throws wfdbx::Error if a samps_per_frame value is less
than 1 (“write_record: samps_per_frame must hold integers >= 1”, checked
before anything else), if the record name is empty, if there are no
signals (“write_record: at least one signal is required”), if the number
of samples is not a multiple of the frame size, or if base_time is not
a valid time (“write_record: incorrect base time ‘T’”).
write_record_physical also throws if a sample is infinite or out of the
range of a 32-bit ADC value (NaN is stored as an invalid sample). Nothing
is written then.read_record throws wfdbx::Error if from is negative. write_record and write_annotations throw
wfdbx::Error when a file cannot be written completely, for example on a
full disk: “write error on record REC”, “write error on annotator ANN for
record REC” (also in legacy mode).write_record reads nothing, so it overwrites any existing files of the
same name.The messages of the library are printed on the standard error.
wfdbx::wfdbquiet() turns them off.
This catches an error:
try {
wfdbx::Record rec = wfdbx::read_record("nosuchrecord");
} catch (const wfdbx::Error &e) {
std::printf("%s\n", e.what()); // can't read record nosuchrecord
}
The high-level functions call wfdbquit() before and after their work,
also when they throw, which closes anything opened with the WFDB library
functions. The library’s settings (legacy mode, getvec mode, input and
output directories) are the same after a call as before it.
check_recordThis checks the sample record 100s (in tests/data) and its reference
annotations:
wfdbx::wfdbx_set_input_dir("tests/data"); // the sample record 100s
wfdbx::CheckReport report = wfdbx::check_record("100s", {"atr"});
std::printf("%s\n", report.to_string().c_str());
check_record(record, annotators, input) checks the header of a record,
its signal specifications, every sample of its signal files, and the
annotation files named. It reports each problem it finds. It never
throws for a problem in the record (it throws wfdbx::Error only for an
empty record name). input is an InputOptions (input_dir, network),
as for read_header. It checks for problems similar to those found by
Benjamin Moody’s wfdbcheck.
The result is a wfdbx::CheckReport. report.ok is true if there is no
error. report.issues lists the problems. Each wfdbx::Issue has a
severity, a code (such as "checksum"), a where ("" for the record,
"signal 1", "segment s2", "annotator atr") and a message.
| Severity | Meaning |
|---|---|
error |
the record is damaged or does not follow the WFDB format: programs will read wrong data or fail |
warning |
the record can be read, but something is missing or unusual, and may give wrong results |
info |
a fact worth knowing, such as invalid (missing) samples |
report.to_string() gives one line per problem, or the line
no problems found:
warning: signal 0: the units are not specified
error: signal 1: the checksum does not match: the header gives 12, the samples sum to 13
error: annotator atr: 3 annotations are out of order; the first is at sample 50, after sample 100
report.lines() returns these lines as a std::vector<std::string>, and
issue.to_string() the line of one issue. The lines are the same in the seven
languages of WFDB-x. The checks run in normal mode, even if legacy mode is
on, and the library prints no messages during the call.
What is checked:
The sample record 100s gives two warnings: its header gives no units.
WFDB-x reads and writes files exactly where they are named, and nowhere else:
| The name is | It is read from | It is written to |
|---|---|---|
a full path: /data/mitdb/100, C:\data\100 |
that path | that path |
a home path: ~/data/100 |
the user’s home directory | the user’s home directory |
a URL: https://.../100 |
that URL, if network access is on | (URLs cannot be written) |
anything else: 100, mitdb/100, ../data/100 |
the input directory | the output directory |
In this table, /data/mitdb stands for a local copy of the MIT-BIH
Arrhythmia Database, and 100 for its record 100.
The input and output directories are both the current directory unless they are set:
| Setting | Function | Option of the high-level functions | Environment variable |
|---|---|---|---|
input directory: a local directory, ~/... or a URL |
wfdbx_set_input_dir(d) |
input_dir of ReadOptions and InputOptions |
WFDBX_INPUT_DIR |
| output directory | wfdbx_set_output_dir(d) |
WriteOptions::dir, dir of write_annotations |
WFDBX_OUTPUT_DIR |
| network access (off by default) | wfdbx_set_network(1) |
network of ReadOptions and InputOptions |
WFDBX_NETWORK=1 |
A function call overrides the environment variable, and an option applies to
that one call. Each set function returns the previous setting.
wfdbx_get_input_dir(), wfdbx_get_output_dir() and wfdbx_get_network()
return the current settings. Passing nullptr to wfdbx_set_input_dir or
wfdbx_set_output_dir, or -1 to wfdbx_set_network, restores the
default.
This reads record 100 from /data/mitdb, which stands for a local copy of
the MIT-BIH Arrhythmia Database, and writes an annotation file in out:
wfdbx::wfdbx_set_input_dir("/data/mitdb");
wfdbx::Record rec = wfdbx::read_record("100"); // reads /data/mitdb/100.hea and 100.dat
wfdbx::Annotations ann = wfdbx::read_annotations("100", "atr");
wfdbx::write_annotations("100", "qrs", ann, 0, "out"); // writes out/100.qrs
The signal files that a header names are read from the header’s directory.
A record name is made of letters, digits, _, - and ~. / separates
directories, and the names of directories may contain any characters, such
as dots: my.data/rec is a valid record name, rec.1 is not. A name whose
last part contains a dot is the name of a file: 100s.hea is read as the
header of record 100s, and 100s.edf as an EDF file.
Records can be written under any such name: write_record("my.data/rec",
...) creates my.data/rec.hea and my.data/rec.dat. The header names the
record and its signal file without the directory (rec, rec.dat), so the
record can be read under any path that leads to it. Annotator names may
contain letters, digits, _, - and ~.
Set the input directory to the URL of the database and turn network access
on. The URL of a database is https://physionet.org/files/<database>/<version>/.
This reads record 100 of the MIT-BIH Arrhythmia Database:
wfdbx::ReadOptions opt;
opt.input_dir = "https://physionet.org/files/mitdb/1.0.0/";
opt.network = true;
wfdbx::Record rec = wfdbx::read_record("100", opt);
Network access uses the curl program, which must be installed. Remote
files are read once and kept until the program ends. A download gives up
when it cannot connect, or receives nothing, for 30 seconds.
WFDB-x never assumes a sampling frequency, gain, ADC resolution or units
that a record does not give. Where one is needed, it must be given: in
write_record, as the fs argument of read_annotations, or as an option
of the tools (see below). A record whose header gives no frequency has fs =
0. See
DIVERGENCES.md
for the reasons.
Each tool is a separate program in cpp/build/bin/, or in the bin
directory of the installation. With that directory in the PATH, run a
tool by its name. These commands read the sample record 100s (the first
minute of record 100 of the MIT-BIH Arrhythmia Database), included with
WFDB-x in the tests/data directory of a copy of the repository:
export WFDBX_INPUT_DIR=tests/data # run from the root of the repository
rdsamp -r 100s -t 10 -p # print the first 10 seconds, in physical units
rdann -r 100s -a atr # print the annotations
wfdbdesc 100s # describe the record
The tools have the options and output of the original WFDB tools, which are
described in the WFDB Applications Guide
(third_party/wag.pdf):
rdsamp, wrsamp, wfdbdesc, sigamp, snip, xform,
sampfreq, signame, signum, wfdbcat, wfdbwhichwfdbtime, time2secrdann, wrann, sortann, ann2rr, rr2annedf2mit, mit2edf, rdedfann, wav2mit, mit2wav,
wfdb2mat, a2m, m2a, ad2m, md2a, ahaecg2mit, makeid, readid,
revise, parsescpThe tools use the environment variables of the table above. To read record 100 of the MIT-BIH Arrhythmia Database from PhysioNet:
export WFDBX_INPUT_DIR=https://physionet.org/files/mitdb/1.0.0/
export WFDBX_NETWORK=1
rdsamp -r 100 -t 0:05
Tools that write signal parameters require them as options:
| Tool | Required options |
|---|---|
wrsamp (with -o) |
-F frequency, -G gain, -R ADC resolution, and -U units for calibrated signals |
wav2mit |
-G gain, and -U units unless the gain is 0 |
ad2m, ahaecg2mit |
-F, -G, -R, and -U unless the gain is 0 |
a2m, rdann, ann2rr, sortann, m2a |
-F frequency, when the record’s header does not give one |
For example, wrsamp -F 250 -G 200 -R 16 -U mV -i samples.txt -o rec
writes a new record, rec, from the columns of a text file samples.txt.
<wfdbx/wfdb.hpp> declares every function of the WFDB library under its C
name, with the same arguments and return values, so programs and
documentation written for the C library carry over directly. The WFDB
Programmer’s Guide applies, and the header summarizes each function. This
program reads the samples and the annotations of the sample record 100s
(the first minute of record 100 of the MIT-BIH Arrhythmia Database), from
the tests/data directory of a copy of the repository:
#include <cstdio>
#include <wfdbx/wfdb.hpp>
using namespace wfdbx;
int main() {
wfdbx_set_input_dir("tests/data");
WFDB_Siginfo si[2];
if (isigopen("100s", si, 2) == 2) { // open 2 signals; si receives their specifications
WFDB_Sample v[2];
while (getvec(v) > 0) { // v receives one sample per signal
// ...
}
}
WFDB_Anninfo ai{"atr", WFDB_READ};
WFDB_Annotation a;
if (annopen("100s", &ai, 1) == 0)
while (getann(0, &a) == 0)
std::printf("%s %s\n", mstimstr(a.time).c_str(), annstr(a.anntyp)->c_str());
wfdbquit();
}
Differences from C that come from the language:
wfdbx. String arguments are const char *.std::string, or
std::optional<std::string> where C may return NULL. They stay valid
after later calls.WFDB_Siginfo and WFDB_Calinfo are
std::optional<std::string>, since C distinguishes NULL from "".aux is std::nullopt, or a std::string whose first
byte is the length of the text that follows, as in C.WFDB_Time is 64 bits.getvecs(buffer, n) reads up to n frames at once, with the same result
as n calls of getvec.wfdberror() returns the last message, and
wfdbquiet() stops the messages from being printed.wfdbquit() closes them; the directory and
network settings are kept.wfdbx_write_errors() returns the number of output files that the library
could not write, flush or close (found when it closes them), and
wfdbx_note_write_error() counts one more, for a file that a program
writes itself. The tools exit with status 1 after a write error, or when
the standard output cannot be written (“PROGRAM: write error on standard
output”); with --legacy, they keep the original exit status.The annotation codes (NORMAL, PVC, …) are in <wfdbx/ecgcodes.hpp>,
and the macros of <wfdb/ecgmap.h> (isqrs, map1, map2, …) are
functions in <wfdbx/ecgmap.hpp>.
Legacy mode reproduces the original WFDB package exactly, including its assumed signal parameters and its search for files. Use it only to reproduce old results:
--legacy as the first argument, as in rdsamp --legacy -r 100s,
run in the directory of the sample record 100s (tests/data in a copy
of the repository). Legacy mode finds files through the WFDB path, which begins with the
current directory, and does not use the input directory;legacy field of ReadOptions and
WriteOptions, and the legacy argument of read_header and
read_annotations;wfdbx_set_legacy(1).DIVERGENCES.md describes what changes in legacy mode.
read_record reads signal files in large blocks, and is the
fastest way to read a record. Code that processes a record as a stream
should read it with getvecs(buffer, n), which reads many frames at once,
rather than with one getvec call per frame.
BENCHMARKS.md
compares the seven languages and wfdb-python.From the root of the repository, after building:
ctest --test-dir cpp/build # unit tests
python3 tests/conformance/harness.py check --impl cpp -j8 # conformance suite
The unit tests cover the WFDB library functions, the high-level functions
and the FLAC codec. The tests of check_record write the corpus of
python/tests/check_corpus.py with python3 (or the program named by
WFDBX_PYTHON) and the Python package of python/src, and compare the
reports with its expected.txt; without Python they are skipped.
cpp/build/wfdbx_check_corpus DIR prints the reports for a corpus in DIR.
Developer notes are in DEVELOPING.md. The conformance suite
(docs/conformance.md)
runs every tool and compares its results, byte for byte, with those of the
original C programs. Add --legacy to run it in legacy mode.
WFDB-x implements version 10.7.0 of the WFDB Software Package.
The WFDB library and tools were written by George B. Moody, with revisions by
Benjamin Moody and contributions from many others; parsescp was written by
George B. Moody and Edna S. Moody. The header of each source file names the
authors of the C code it implements, and
AUTHORS.md
gives the details. The C++ implementation was developed by Reza Sameni
(2026), through AI-assisted development, cross-language
verification and unit testing.
WFDB-x is an independent reimplementation. Its C++ code is a translation of the C code of the WFDB Software Package into C++, written for WFDB-x; it is not code by the original WFDB developers. It follows the original’s logic and behavior, with the changes described in DIVERGENCES.md. The authors of the original C code are credited in each file and in AUTHORS.md.
WFDB-x is distributed under the GNU General Public License, version 3 or (at your option) any later version. See LICENSE.