-
Notifications
You must be signed in to change notification settings - Fork 302
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Read FLAC signal files #372
Conversation
26e94ce
to
f71f2a5
Compare
I see that this is still WIP. Can we all add type annotations to affected code from now on? |
It sounds like a good idea in principle, though it adds quite a bit of visual noise and we don't have an established style for doing so. Could we take this discussion to a separate issue? |
See issue #378. |
pyproject.toml
Outdated
@@ -10,6 +10,7 @@ python = "^3.7" | |||
numpy = "^1.10.1" | |||
scipy = "^1.0.0" | |||
pandas = "^1.0.0" | |||
soundfile = "^0.10.0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have trouble installing this with poetry install
, which results in: SolverProblemError Because wfdb depends on soundfile (^0.10.0) which doesn't match any versions, version solving failed.
. Running poetry add soundfile@^0.10.0
results in SoundFile = "^0.10.0"
being added
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks clean. Please add a note in the README about installing WFDB and libsndfile.
wfdb/io/_signal.py
Outdated
import soundfile | ||
|
||
for spf in samps_per_frame[1:]: | ||
assert spf == samps_per_frame[0], "spf mismatch" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a better error to use here?
else: | ||
raise ValueError(f"unknown subtype in {fp.name} ({sf.subtype})") | ||
|
||
max_bits = int(fmt) - 500 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the WFDB dat formats not map 1:1 with the flac subtype?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you explain this logic?
wfdb/io/download.py
Outdated
check_access=False, | ||
): | ||
""" | ||
Open a database file as a random-access file object. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A better name for this function would be nice. "Database file" doesn't convey much meaning.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, perhaps this function should be in its own module, as download.py
doesn't quite make sense. eg. Module coreio.py
and function open_file
.
I have trouble installing this with `poetry install`, which results
in: `SolverProblemError Because wfdb depends on soundfile (^0.10.0)
which doesn't match any versions, version solving failed.`. Running
`poetry add soundfile@^0.10.0` results in `SoundFile = "^0.10.0"`
being added
Yeesh. Can I just call that a bug in poetry? Also, it looks like
soundfile 0.11.0b5 changed the name in setup.py from SoundFile to
soundfile. I don't know if the name on PyPI follows the name in
setup.py or what.
|
Do you have any multiplexed signal files for testing?
That's an excellent point, I'll dig out a small real-world example and
add it here.
|
Oh, on M1 Mac when trying to `import soundfile`: `OSError: sndfile
library not found`. Looks like a new release should support it:
bastibe/python-soundfile#310
Yeah, this is an annoyance for sure. Did you try the (hot off the
presses) python-soundfile 0.11.0b5?
|
Bug: these formats need to be handled in _digi_nan. |
I am completely lost when it comes to poetry. If I check out 63c8039, run
If I do
(it adds a bunch of junk to poetry.lock but it doesn't actually install any of the dependencies.) The same happens if I do If I rebase In conclusion, I have no idea how to make poetry work, and I have no idea whether poetry will throw a fit if If you have some idea how to make poetry work, please let me know the exact commands I ought to be running. |
Ooh, that command finally finished running after 14 minutes and it still didn't install anything. |
I always remove Anyway, I did manage to successfully install this config (removed big packages to save time) by running
Running I think if they change their name back to |
I did not. Figured it wouldn't make a difference to this PR even if it works. I only tested your changes with the specified soundfile on amd64 Linux. |
Thanks - it would be nice to check it works but not a top priority if that's difficult. I haven't forgotten about the other issues you raised above and I'll try to address them tomorrow, but for now I've uploaded a short example of some real-world data you can play around with if you like. |
This defines the function _open_file, which opens an input data file as a random-access file object. This module is intended to eventually replace the "streaming I/O" functions in the wfdb.io.download module (_remote_file_size, _stream_header, _stream_dat, and _stream_annotation.) Some notes on the implementation: - Contrary to many existing functions, I've deliberately made pn_dir the first argument rather than the second, since putting the prefix first seems more natural. - There's no dir_name argument; instead, callers should include the local directory name as part of file_name. I consider it a bug that some functions have a separate dir_name argument that is ignored when pn_dir is set. - This function does not do automatic version number resolution for PhysioNet projects. That's something the caller (still) needs to do and should be handled elsewhere.
The soundfile library provides a python wrapper for libsndfile, which in turn provides an interface to libFLAC for reading and writing FLAC files.
"build": pip install will install the Python soundfile package. On Windows/MacOS, this will install a binary wheel package that includes a private copy of libsndfile; on Ubuntu, it will install a generic package that requires us to install libsndfile separately. "test-deb10-i386": python3-soundfile installs the Python soundfile package and also depends on libsndfile1.
The _digi_bounds function previously did not handle formats 310, 311, 61, 160, or 8 (which are not supported by wrsamp). Add these formats to SAMPLE_VALUE_RANGE for consistency even though they're currently not used. This will also raise an exception for unknown formats, rather than silently returning None.
This will also raise an exception for unknown formats, rather than silently returning None.
These format codes are used to designate FLAC signal files. Format 508 indicates 8-bit, format 516 indicates 16-bit, and format 524 indicates 24-bit resolution. Other format codes between 500 and 532 (inclusive) are reserved. A FLAC signal file contains between one and eight channels, which are sampled at the same frequency. Therefore, all signals stored in the same signal file must have the same number of samples per frame. (If a record contains more than eight signals, or if they have differing sampling frequencies, they will require more than one signal file.) Note that the number of samples per frame has no effect on the encoding or decoding of the FLAC signal file; there is no interleaving like there is in a binary signal file. There is also no requiremement that WFDB frame boundaries match up with FLAC compression-block boundaries. (Perhaps confusingly, the FLAC specification uses the word "frame" to refer to the encoded form of a block of samples; this has nothing to do with WFDB frames, and unless otherwise noted, the word "frame" refers to a WFDB frame.) To simplify this initial implementation, the function _rd_compressed_file is nearly a drop-in replacement for _rd_dat_file, which means that this function must rearrange the input data into the format that _rd_dat_signals expects.
The fmt argument is the format of the singular dat file. It is not a list and that wouldn't make any sense here.
The record "flacformats" contains one signal in each of the three WFDB FLAC signal formats. As with the "binformats" record, sample j of signal i is equal to: (i + 16843019 * j) % ((1 << adcres) - 1) + 1 - (1 << (adcres - 1))) Use this record to test that it is possible to read all of the formats correctly, including when we skip one or two samples from the start and/or end of the record.
We want to be sure that the soundfile library is compatible with wfdb.io._url.NetFile, since wfdb.io._signal._rd_compressed_file relies on this. This isn't a complete test of WFDB/FLAC functionality, in part because the sample files are very small so we can't really exercise the seeking functionality. However, this should be sufficient to verify that soundfile doesn't make silly assumptions (like assuming the input file corresponds to an OS file descriptor.)
Updated with the following changes:
Correctly handling invalid samples makes this example look better:
It still looks a little odd because the channels aren't synchronized (see pull #390). |
By the way, I was poking around the other day and I saw that both |
README.md
Outdated
@@ -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://https://github.com/bastibe/python-soundfile/issues/310 and https://https://github.com/bastibe/python-soundfile/issues/325). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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://https://github.com/bastibe/python-soundfile/issues/310 and https://https://github.com/bastibe/python-soundfile/issues/325). | |
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>). |
@@ -0,0 +1,7 @@ | |||
mixedsignals 6 62.4725/999.56 14400 |
There was a problem hiding this comment.
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?
else: | ||
raise ValueError(f"unknown subtype in {fp.name} ({sf.subtype})") | ||
|
||
max_bits = int(fmt) - 500 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you explain this logic?
Looks like you haven't added a test case for these signals?
No, I didn't think of that. Somebody want to add that? :P
Could you explain this logic?
Well, one answer is just "trying to replicate the behavior of libFLAC
by unwrapping the layers that are added on top of it by libsndfile and
SoundFile." But why?
libFLAC, like WFDB, deals with samples as integer values: an "8-bit"
FLAC file will be decoded as a series of integers between -128 and
127. The "full range" is regarded as kind of an implementation detail
(fun fact, libFLAC doesn't enforce range limits on the input samples,
which can give weird results.) Also, FLAC compression itself is
deeply tied to the quantization step.
SoundFile, on the other hand, conceptually regards samples as
fractions of the full range, and the quantization step is thought of
as an implementation detail. If you ask for 32-bit integer data,
SoundFile assumes what you want is a value scaled to the range -2^31
to 2^31, regardless of the original storage format. This is what you
generally want when working with audio.
So what we are doing is trying to persuade SoundFile to give us the
most efficient format we can get, while undoing any automatic
transformation it performs (which we can infer from the subtype
attribute).
As for the "max_bits" thing: the idea there is simply that you may
have a collection of FLAC files *not* produced by WFDB, and possibly
with unknown/varying bit depths, and you want to read them using WFDB.
So the format code is an upper bound and not a strict requirement.
|
@cx1111 are we good to merge this? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't sure if something got lost in communication about changes needing to get pushed. I did leave the note about the test cases for the multiplexed record case, and updating the README URL typo.
The main logic looks good, from what I understand. @bemoody should be good to merge when ready. Thanks!
Thanks, Chen! I fixed the typo in the README. It would definitely be a good idea to add a test case for multiplexed/multi-frequency signals in the future but for right now I just want to merge the code as-is. |
WFDB 10.7 defines new signal formats 508, 516, and 524. Signal files in these formats are compressed using the FLAC algorithm.
Here, we add support for reading these formats using the soundfile package, which is a wrapper around libsndfile. As such, you'll need to have libsndfile (and libFLAC.) The soundfile wheel packages for Windows and MacOS include a copy of libsndfile that's statically linked with libFLAC. On Linux or any other platform you will need to install libsndfile yourself, though there's a good chance you already have it.
Other implementation strategies are possible but this seems simplest.
The implementation in _signal.py is messier than I would like, because the _rd_dat_signals function combines low-level details (byte offsets, format-specific transformations) with high-level semantics (deskewing, length checking.) Preferably the _skew_sig/_check_sig_dims stuff would be moved into _rd_segment instead.
Writing FLAC files is not implemented yet, and I think I'd like to try to address some of the other issues with wrsamp first (#333, #336). Of course if somebody else wants to take a stab at it, feel free ;)