Skip to content

Read and write records in .wfdb archives - #35

Closed
tompollard wants to merge 3 commits into
bemoody:mainfrom
tompollard:wfdb-archives
Closed

tompollard wants to merge 3 commits into
bemoody:mainfrom
tompollard:wfdb-archives

Conversation

@tompollard

Copy link
Copy Markdown

This pull request adds support for reading and writing records stored in .wfdb archives: a ZIP file that holds the header, signal and annotation files of a single record. It is the C counterpart of MIT-LCP/wfdb-python#541 (see also wfdb/wfdb-spec#21), and uses the same format, so archives written by either package can be read by the other.

Usage

Records in archives are opened by giving the record name with the .wfdb extension. All applications built with the library can then read and write archives:

# Read a record and its annotations from an archive
rdsamp -r 100.wfdb -f 10 -t 20
rdann -r 100.wfdb -a atr
wfdbdesc 100.wfdb

# Write a new record into an archive
xform -i 100 -n 100.wfdb
wrsamp -i data.txt -o rec.wfdb -F 250 0 1

# Add annotations to an existing archive
sqrs -r 100.wfdb

The archive is found by searching the WFDB path, like any other file.

Implementation

The archive code is in lib/wfdbio.c, behind the existing WFDB_FILE functions, with no new dependencies. There are three commits:

  1. Library.
    • wfdb_open builds file names such as 100.wfdb.hea. wfdb_fopen recognizes these as the file 100.hea within the archive 100.wfdb.
    • Reading: the header is the file named after the archive (100.hea in 100.wfdb), or the only header in the archive. Signal files are read from the same archive, by the names in the header, and a missing signal file is an error. Headers larger than 1 MB are rejected, as in the Python package. ZIP64 archives are supported.
    • Writing: output files named RECORD.wfdb.EXT are stored as RECORD.EXT. This is the name applications such as xform and wrsamp already give signal files of an output record named RECORD.wfdb. setheader requires the signal files of a record in an archive to be in the same archive, and writes their names within the archive to the header.
    • Files written to an archive are kept in temporary files until wfdbquit() is called (or the program exits). The new archive is then written to a temporary file next to the old one and moved into place, so an existing archive is either fully replaced or left unchanged. A replaced archive keeps its permissions.
    • Other files already in the archive are kept. This lets sqrs -r 100.wfdb add 100.qrs to an archive, and lets setinfo/putinfo add info strings. This differs from wrsamp(wfdb_archive=...) in Python, which always writes a new archive.
    • Files are stored without compression, as in the Python package, so that any range of samples can be read without reading the whole signal file.
  2. Tests.
    • lcheck now copies record 100y into the archive 100x.wfdb (including info strings), then copies 100x.wfdb to 100z. 100z.dat and 100z.hea must still match the expected files.
    • appcheck copies 100s and its annotations into an archive with xform, adds annotations made by sqrs, and checks the signals and both annotation files read back from the archive.
  3. Docs. A paragraph on archives in the Programmer's Guide, and a NEWS entry.

Not supported yet

These give a clear error:

  • multi-segment records in archives (as in the Python package)
  • archives on remote servers
  • compressed files within archives (for example, archives made with zip without -0). Reading these would need zlib, which could be added as an optional dependency later, like libFLAC.

Testing

  • make check passes with and without NETFILES (libcheck 8/8 and appcheck 57/57 with NETFILES, using WFDB_NO_NET_CHECK).
  • Cross-checked against wfdb-python at the head of #541. Python reads archives written by xform, and the C library reads archives written by Python's wrsamp and create_archive. This covered several signal files, formats 16/212/80, annotations and sample ranges.
  • Error cases checked: missing archive, a file that is not a ZIP, compressed files, a missing signal file, multi-segment records, and a signal file outside the archive. A write that can't create its temporary file leaves the existing archive unchanged.
  • The ZIP reader was run under AddressSanitizer and UndefinedBehaviorSanitizer on 400 randomly corrupted and truncated archives, with no errors.
  • FLAC signals in archives were not tested, because libFLAC wasn't available on the test machine.
  • Built and tested on macOS only.

A .wfdb archive is a ZIP file that holds the files of a single record:
its header file, the signal files listed in the header, and optionally
annotation and other files.  This is the same format that the WFDB
Python package reads and writes.

A record in an archive is opened by giving its name with the '.wfdb'
extension (for example, 'rdsamp -r 100.wfdb').  wfdb_open then builds
a file name such as '100.wfdb.hea', which wfdb_fopen recognizes as the
file '100.hea' within the archive '100.wfdb'.  The archive is found by
searching the WFDB path, like any other file.

Reading:

- The header is the file named after the archive ('100.hea' in
  '100.wfdb'), or the only header in the archive.  Headers larger than
  1 MB are rejected.
- Signal files are read from the same archive as the header, by the
  names given in the header.  A missing signal file is an error.
- Files must be stored without compression, so that a range of samples
  can be read without reading the whole signal file, and so that no
  compression library is needed.  ZIP64 archives are supported.

Writing:

- Output files named 'RECORD.wfdb.EXT' (as produced by applications
  such as xform and wrsamp for an output record named 'RECORD.wfdb')
  are stored in the archive as 'RECORD.EXT'.  setheader requires the
  signal files of a record in an archive to be in the same archive, and
  writes their names within the archive to the header.
- Files are kept in temporary files until wfdbquit is called (or the
  program exits).  The new archive is then written to a temporary file
  next to the old one and moved into place, so an existing archive is
  either fully replaced or left unchanged.  Other files already in the
  archive are kept, so that (for example) 'sqrs -r 100.wfdb' adds
  100.qrs to the archive.  A replaced archive keeps its permissions.

Archives on remote servers and multi-segment records in archives are
not supported, and give an error.
lcheck now copies record 100y into the archive 100x.wfdb (including its
info strings, written with setinfo and putinfo), then copies 100x.wfdb
to 100z.  100z.dat and 100z.hea must still match the expected files.

appcheck copies record 100s and its annotations into an archive with
xform, adds annotations made by sqrs to the archive, and checks the
signals and both annotation files read from the archive.
@bemoody

bemoody commented Nov 2, 2026 via email

Copy link
Copy Markdown
Owner

@tompollard

Copy link
Copy Markdown
Author

Yes this is an AI port of the python implementation

@bemoody

bemoody commented Nov 4, 2026

Copy link
Copy Markdown
Owner

Thanks, but I am not accepting any AI-generated contributions.

@bemoody bemoody closed this Nov 4, 2026
@tompollard
tompollard deleted the wfdb-archives branch November 4, 2026 21:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants