Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ jobs:
run: |
python -m pip install --upgrade pip poetry
pip install ".[dev]"
- name: Install libsndfile
if: startsWith(matrix.os, 'ubuntu')
run: |
sudo apt-get install -y libsndfile1
- name: Run tests
run: pytest
- name: Validate poetry file
Expand All @@ -48,6 +52,7 @@ jobs:
python3-pandas \
python3-requests \
python3-scipy \
python3-soundfile \
python3-pytest \
git

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ pip install wfdb
poetry add wfdb
```

On Linux systems, accessing *compressed* WFDB signal files requires installing `libsndfile`, by running `sudo apt-get install libsndfile1` or `sudo yum install libsndfile`. Support for Apple M1 systems is a work in progess (see <https://github.com/bastibe/python-soundfile/issues/310> and <https://github.com/bastibe/python-soundfile/issues/325>).

The development version is hosted at: <https://github.com/MIT-LCP/wfdb-python>. This repository also contains demo scripts and example data. To install the development version, clone or download the repository, navigate to the base directory, and run:

```sh
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ python = "^3.7"
numpy = "^1.10.1"
scipy = "^1.0.0"
pandas = "^1.0.0"
SoundFile = ">=0.10.0, <0.12.0"
matplotlib = "^3.2.2"
requests = "^2.8.1"
pytest = {version = "^7.1.1", optional = true}
Expand Down
Binary file added sample-data/flacformats.d0
Binary file not shown.
Binary file added sample-data/flacformats.d1
Binary file not shown.
Binary file added sample-data/flacformats.d2
Binary file not shown.
4 changes: 4 additions & 0 deletions sample-data/flacformats.hea
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
flacformats 3 200 499
flacformats.d0 508 200/mV 8 0 -127 -484 0 sig 0, fmt 508
flacformats.d1 516 200/mV 16 0 -32766 -750 0 sig 1, fmt 516
flacformats.d2 524 200/mV 24 0 -8388605 8721 0 sig 2, fmt 524
7 changes: 7 additions & 0 deletions sample-data/mixedsignals.hea
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
mixedsignals 6 62.4725/999.56 14400
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like you haven't added a test case for these signals?

mixedsignals_e.dat 516x4 200/mV 14 8192 0 24460 0 II
mixedsignals_e.dat 516x4 200/mV 14 8192 0 19772 0 III
mixedsignals_e.dat 516x4 200/mV 14 8192 0 22261 0 V
mixedsignals_p.dat 516x2 16(800)/mmHg 12 2048 0 49347 0 ABP
mixedsignals_p.dat 516x2 4096(0)/NU 12 2048 0 36026 0 Pleth
mixedsignals_r.dat 516 4093(2)/Ohm 12 2048 0 35395 0 Resp
Binary file added sample-data/mixedsignals_e.dat
Binary file not shown.
Binary file added sample-data/mixedsignals_p.dat
Binary file not shown.
Binary file added sample-data/mixedsignals_r.dat
Binary file not shown.
Binary file added tests/target-output/record-flac.gz
Binary file not shown.
31 changes: 31 additions & 0 deletions tests/test_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,37 @@ def test_1f(self):
"Mismatch in %s" % name,
)

def test_read_flac(self):
"""
All FLAC formats, multiple signal files in one record.

Target file created with:
rdsamp -r sample-data/flacformats | cut -f 2- |
gzip -9 -n > record-flac.gz
"""
record = wfdb.rdrecord("sample-data/flacformats", physical=False)
sig_target = np.genfromtxt("tests/target-output/record-flac.gz")

for n, name in enumerate(record.sig_name):
np.testing.assert_array_equal(
record.d_signal[:, n], sig_target[:, n], f"Mismatch in {name}"
)

for sampfrom in range(0, 3):
for sampto in range(record.sig_len - 3, record.sig_len):
record_2 = wfdb.rdrecord(
"sample-data/flacformats",
physical=False,
sampfrom=sampfrom,
sampto=sampto,
)
for n, name in enumerate(record.sig_name):
np.testing.assert_array_equal(
record_2.d_signal[:, n],
sig_target[sampfrom:sampto, n],
f"Mismatch in {name}",
)

# ------------------ 2. Special format records ------------------ #

def test_2a(self):
Expand Down
56 changes: 56 additions & 0 deletions tests/test_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,62 @@ def _test_binary(self, url, content, buffering):
self.assertEqual(bf.tell(), len(content))


class TestRemoteFLACFiles(unittest.TestCase):
"""
Test reading FLAC files over HTTP.
"""

def test_whole_file(self):
"""
Test reading a complete FLAC file using local and HTTP APIs.

This tests that we can read the file 'sample-data/flacformats.d2'
(a 24-bit FLAC stream) using the soundfile library, first by
reading the file from the local filesystem, and then using
wfdb.io._url.openurl() to access it through a simulated web server.

This is meant to verify that the soundfile library works using only
the standard Python file object API (as implemented by
wfdb.io._url.NetFile), and doesn't require the input file to be an
actual io.FileIO object.

Parameters
----------
N/A

Returns
-------
N/A

"""
import soundfile
import numpy as np

data_file_path = "sample-data/flacformats.d2"
expected_format = "FLAC"
expected_subtype = "PCM_24"

# Read the file using standard file I/O
sf1 = soundfile.SoundFile(data_file_path)
self.assertEqual(sf1.format, expected_format)
self.assertEqual(sf1.subtype, expected_subtype)
data1 = sf1.read()

# Read the file using HTTP
with open(data_file_path, "rb") as f:
file_content = {"/foo.dat": f.read()}
with DummyHTTPServer(file_content) as server:
url = server.url("/foo.dat")
file2 = wfdb.io._url.openurl(url, "rb")
sf2 = soundfile.SoundFile(file2)
self.assertEqual(sf2.format, expected_format)
self.assertEqual(sf2.subtype, expected_subtype)
data2 = sf2.read()

# Check that results are equal
np.testing.assert_array_equal(data1, data2)


class DummyHTTPServer(http.server.HTTPServer):
"""
HTTPServer used to simulate a web server for testing.
Expand Down
65 changes: 65 additions & 0 deletions wfdb/io/_coreio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from wfdb.io import _url


def _open_file(
pn_dir,
file_name,
mode="r",
*,
buffering=-1,
encoding=None,
errors=None,
newline=None,
check_access=False,
):
"""
Open a data file as a random-access file object.

See the documentation of `open` and `wfdb.io._url.openurl` for details
about the `mode`, `buffering`, `encoding`, `errors`, and `newline`
parameters.

Parameters
----------
pn_dir : str or None
The PhysioNet database directory where the file is stored, or None
if file_name is a local path.
file_name : str
The name of the file, either as a local filesystem path (if
`pn_dir` is None) or a URL path (if `pn_dir` is a string.)
mode : str, optional
The standard I/O mode for the file ("r" by default). If `pn_dir`
is not None, this must be "r", "rt", or "rb".
buffering : int, optional
Buffering policy.
encoding : str, optional
Name of character encoding used in text mode.
errors : str, optional
Error handling strategy used in text mode.
newline : str, optional
Newline translation mode used in text mode.
check_access : bool, optional
If true, raise an exception immediately if the file does not
exist or is not accessible.

"""
if pn_dir is None:
return open(
file_name,
mode,
buffering=buffering,
encoding=encoding,
errors=errors,
newline=newline,
)
else:
url = posixpath.join(config.db_index_url, pn_dir, file_name)
return _url.openurl(
url,
mode,
buffering=buffering,
encoding=encoding,
errors=errors,
newline=newline,
check_access=check_access,
)
Loading