Skip to content

Commit f65cf2e

Browse files
authored
Merge pull request MIT-LCP#239 from MIT-LCP/wav2mit
Produces record file from WAV format
2 parents f902cb5 + 6429c2f commit f65cf2e

File tree

4 files changed

+217
-4
lines changed

4 files changed

+217
-4
lines changed

sample-data/SC4001E0-PSG.wav

1.06 MB
Binary file not shown.

wfdb/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from wfdb.io.record import (Record, MultiRecord, rdheader, rdrecord, rdsamp,
2-
wrsamp, dl_database, edf2mit, sampfreq, signame)
2+
wrsamp, dl_database, edf2mit, wav2mit, sampfreq, signame)
33
from wfdb.io.annotation import (Annotation, rdann, wrann, show_ann_labels,
44
show_ann_classes, ann2rr)
55
from wfdb.io.download import get_dbs, get_record_list, dl_files, set_db_index_url

wfdb/io/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from wfdb.io.record import (Record, MultiRecord, rdheader, rdrecord, rdsamp, wrsamp,
2-
dl_database, edf2mit, sampfreq, signame, SIGNAL_CLASSES)
2+
dl_database, edf2mit, wav2mit, sampfreq, signame, SIGNAL_CLASSES)
33
from wfdb.io._signal import est_res, wr_dat_file
44
from wfdb.io.annotation import (Annotation, rdann, wrann, show_ann_labels,
55
show_ann_classes, ann2rr)

wfdb/io/record.py

Lines changed: 215 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import mne
1111
import math
1212
import functools
13+
import struct
1314
import pdb
1415

1516
from wfdb.io import _header
@@ -1497,6 +1498,214 @@ def edf2mit(record_name, pn_dir=None, delete_file=True, record_only=False):
14971498
pass
14981499

14991500

1501+
def wav2mit(record_name, pn_dir=None, delete_file=True, record_only=False):
1502+
"""
1503+
Convert .wav (format 16, multiplexed signals, with embedded header
1504+
information) formatted files to MIT format. See here for more details about
1505+
the formatting of a .wav file: http://soundfile.sapp.org/doc/WaveFormat/.
1506+
1507+
This process may not work with some .wav files that are encoded using
1508+
variants of the original .wav format that are not WFDB-compatible. In
1509+
principle, this program should be able to recognize such files by their
1510+
format codes, and it will produce an error message in such cases. If
1511+
the format code is incorrect, however, `wav2mit` may not recognize that
1512+
an error has occurred.
1513+
1514+
Parameters
1515+
----------
1516+
record_name : str
1517+
The name of the input .wav record to be read.
1518+
pn_dir : str, optional
1519+
Option used to stream data from Physionet. The Physionet
1520+
database directory from which to find the required record files.
1521+
eg. For record '100' in 'http://physionet.org/content/mitdb'
1522+
pn_dir='mitdb'.
1523+
delete_file : bool, optional
1524+
Whether to delete the saved .wav file (False) or not (True)
1525+
after being imported.
1526+
record_only : bool, optional
1527+
Whether to only return the record information (True) or not (False).
1528+
If false, this function will generate both a .dat and .hea file.
1529+
1530+
Returns
1531+
-------
1532+
record : dict, optional
1533+
All of the record information needed to generate MIT formatted files.
1534+
Only returns if 'record_only' is set to True, else generates the
1535+
corresponding .dat and .hea files. This record file will not match the
1536+
`rdrecord` output since it will only give us the digital signal for now.
1537+
1538+
Notes
1539+
-----
1540+
Files that can be processed successfully using `wav2mit` always have exactly
1541+
three chunks (a header chunk, a format chunk, and a data chunk). In .wav
1542+
files, binary data are always written in little-endian format (least
1543+
significant byte first). The format of `wav2mit`'s input files is as follows:
1544+
1545+
[Header chunk]
1546+
Bytes 0 - 3: "RIFF" [4 ASCII characters]
1547+
Bytes 4 - 7: L-8 (number of bytes to follow in the file, excluding bytes 0-7)
1548+
Bytes 8 - 11: "WAVE" [4 ASCII characters]
1549+
1550+
[Format chunk]
1551+
Bytes 12 - 15: "fmt " [4 ASCII characters, note trailing space]
1552+
Bytes 16 - 19: 16 (format chunk length in bytes, excluding bytes 12-19)
1553+
Bytes 20 - 35: format specification, consisting of:
1554+
Bytes 20 - 21: 1 (format tag, indicating no compression is used)
1555+
Bytes 22 - 23: number of signals (1 - 65535)
1556+
Bytes 24 - 27: sampling frequency in Hz (per signal)
1557+
Note that the sampling frequency in a .wav file must be an
1558+
integer multiple of 1 Hz, a restriction that is not imposed
1559+
by MIT (WFDB) format.
1560+
Bytes 28 - 31: bytes per second (sampling frequency * frame size in bytes)
1561+
Bytes 32 - 33: frame size in bytes
1562+
Bytes 34 - 35: bits per sample (ADC resolution in bits)
1563+
Note that the actual ADC resolution (e.g., 12) is written in
1564+
this field, although each output sample is right-padded to fill
1565+
a full (16-bit) word. (.wav format allows for 8, 16, 24, and
1566+
32 bits per sample)
1567+
1568+
[Data chunk]
1569+
Bytes 36 - 39: "data" [4 ASCII characters]
1570+
Bytes 40 - 43: L-44 (number of bytes to follow in the data chunk)
1571+
Bytes 44 - L-1: sample data, consisting of:
1572+
Bytes 44 - 45: sample 0, channel 0
1573+
Bytes 46 - 47: sample 0, channel 1
1574+
... etc. (same order as in a multiplexed WFDB signal file)
1575+
1576+
Examples
1577+
--------
1578+
>>> wav_record = wfdb.wav2mit('sample-data/SC4001E0-PSG.wav', record_only=True)
1579+
1580+
"""
1581+
if not record_name.endswith('.wav'):
1582+
raise Exception('Name of the input file must end in .wav')
1583+
1584+
if pn_dir is not None:
1585+
1586+
if '.' not in pn_dir:
1587+
dir_list = pn_dir.split(os.sep)
1588+
pn_dir = posixpath.join(dir_list[0], get_version(dir_list[0]), *dir_list[1:])
1589+
1590+
file_url = posixpath.join(download.PN_INDEX_URL, pn_dir, record_name)
1591+
# Currently must download file to read it though can give the
1592+
# user the option to delete it immediately afterwards
1593+
r = requests.get(file_url, allow_redirects=False)
1594+
open(record_name, 'wb').write(r.content)
1595+
1596+
wave_file = open(record_name, mode='rb')
1597+
record_name_out = record_name.split(os.sep)[-1].replace('-','_').replace('.wav','')
1598+
1599+
chunk_ID = ''.join([s.decode() for s in struct.unpack('>4s', wave_file.read(4))])
1600+
if chunk_ID != 'RIFF':
1601+
raise Exception('{} is not a .wav-format file'.format(record_name))
1602+
1603+
correct_chunk_size = os.path.getsize(record_name) - 8
1604+
chunk_size = struct.unpack('<I', wave_file.read(4))[0]
1605+
if chunk_size != correct_chunk_size:
1606+
raise Exception('Header chunk has incorrect length (is {} should be {})'.format(chunk_size,correct_chunk_size))
1607+
1608+
fmt = struct.unpack('>4s', wave_file.read(4))[0].decode()
1609+
if fmt != 'WAVE':
1610+
raise Exception('{} is not a .wav-format file'.format(record_name))
1611+
1612+
subchunk1_ID = struct.unpack('>4s', wave_file.read(4))[0].decode()
1613+
if subchunk1_ID != 'fmt ':
1614+
raise Exception('Format chunk missing or corrupt')
1615+
1616+
subchunk1_size = struct.unpack('<I', wave_file.read(4))[0]
1617+
audio_format = struct.unpack('<H', wave_file.read(2))[0]
1618+
if audio_format > 1:
1619+
print('PCM has compression of {}'.format(audio_format))
1620+
1621+
if (subchunk1_size != 16) or (audio_format != 1):
1622+
raise Exception('Unsupported format {}'.format(audio_format))
1623+
1624+
num_channels = struct.unpack('<H', wave_file.read(2))[0]
1625+
if num_channels == 1:
1626+
print('Reading Mono formatted .wav file...')
1627+
elif num_channels == 2:
1628+
print('Reading Stereo formatted .wav file...')
1629+
else:
1630+
print('Reading {}-channel formatted .wav file...'.format(num_channels))
1631+
1632+
sample_rate = struct.unpack('<I', wave_file.read(4))[0]
1633+
print('Sample rate: {}'.format(sample_rate))
1634+
byte_rate = struct.unpack('<I', wave_file.read(4))[0]
1635+
print('Byte rate: {}'.format(byte_rate))
1636+
block_align = struct.unpack('<H', wave_file.read(2))[0]
1637+
print('Block align: {}'.format(block_align))
1638+
bits_per_sample = struct.unpack('<H', wave_file.read(2))[0]
1639+
print('Bits per sample: {}'.format(bits_per_sample))
1640+
# I wish this were more precise but unfortunately some information
1641+
# is lost in .wav files which is needed for these calculations
1642+
if bits_per_sample <= 8:
1643+
adc_res = 8
1644+
adc_gain = 12.5
1645+
elif bits_per_sample <= 16:
1646+
adc_res = 16
1647+
adc_gain = 6400
1648+
else:
1649+
raise Exception('Unsupported resolution ({} bits/sample)'.format(bits_per_sample))
1650+
1651+
if block_align != (num_channels * int(adc_res / 8)):
1652+
raise Exception('Format chunk of {} has incorrect frame length'.format(block_align))
1653+
1654+
subchunk2_ID = struct.unpack('>4s', wave_file.read(4))[0].decode()
1655+
if subchunk2_ID != 'data':
1656+
raise Exception('Format chunk missing or corrupt')
1657+
1658+
correct_subchunk2_size = os.path.getsize(record_name) - 44
1659+
subchunk2_size = struct.unpack('<I', wave_file.read(4))[0]
1660+
if subchunk2_size != correct_subchunk2_size:
1661+
raise Exception('Data chunk has incorrect length.. (is {} should be {})'.format(subchunk2_size, correct_subchunk2_size))
1662+
sig_len = int(subchunk2_size / block_align)
1663+
1664+
sig_data = (np.fromfile(wave_file, dtype=np.int16).reshape((-1,num_channels)) / (2*adc_res)).astype(np.int16)
1665+
1666+
init_value = [int(s[0]) for s in np.transpose(sig_data)]
1667+
checksum = [int(np.sum(v) % 65536) for v in np.transpose(sig_data)] # not all values correct?
1668+
1669+
if pn_dir is not None and delete_file:
1670+
os.remove(record_name)
1671+
1672+
record = Record(
1673+
record_name = record_name_out,
1674+
n_sig = num_channels,
1675+
fs = num_channels * [sample_rate],
1676+
samps_per_frame = num_channels * [1],
1677+
counter_freq = None,
1678+
base_counter = None,
1679+
sig_len = sig_len,
1680+
base_time = None,
1681+
base_date = None,
1682+
comments = [],
1683+
sig_name = num_channels * [None],
1684+
p_signal = None,
1685+
d_signal = sig_data,
1686+
e_p_signal = None,
1687+
e_d_signal = None,
1688+
file_name = num_channels * [record_name_out + '.dat'],
1689+
fmt = num_channels * ['16' if (adc_res == 16) else '80'],
1690+
skew = num_channels * [None],
1691+
byte_offset = num_channels * [None],
1692+
adc_gain = num_channels * [adc_gain],
1693+
baseline = num_channels * [0 if (adc_res == 16) else 128],
1694+
units = num_channels * [None],
1695+
adc_res = num_channels * [adc_res],
1696+
adc_zero = num_channels * [0 if (adc_res == 16) else 128],
1697+
init_value = init_value,
1698+
checksum = checksum,
1699+
block_size = num_channels * [0]
1700+
)
1701+
1702+
if record_only:
1703+
return record
1704+
else:
1705+
# TODO: Generate the .dat and .hea files
1706+
pass
1707+
1708+
15001709
#------------------------- Reading Records --------------------------- #
15011710

15021711

@@ -1626,6 +1835,8 @@ def rdrecord(record_name, sampfrom=0, sampto=None, channels=None,
16261835
parameter is set, this parameter should contain just the base
16271836
record name, and the files fill be searched for remotely.
16281837
Otherwise, the data files will be searched for in the local path.
1838+
Can also handle .edf and .wav files as long as the extension is
1839+
provided in the `record_name`.
16291840
sampfrom : int, optional
16301841
The starting sample number to read for all channels.
16311842
sampto : int, 'end', optional
@@ -1713,6 +1924,8 @@ def rdrecord(record_name, sampfrom=0, sampto=None, channels=None,
17131924

17141925
if record_name.endswith('.edf'):
17151926
record = edf2mit(record_name, pn_dir=pn_dir, record_only=True)
1927+
elif record_name.endswith('.wav'):
1928+
record = wav2mit(record_name, pn_dir=pn_dir, record_only=True)
17161929
else:
17171930
record = rdheader(record_name, pn_dir=pn_dir, rd_segments=False)
17181931

@@ -1785,7 +1998,7 @@ def rdrecord(record_name, sampfrom=0, sampto=None, channels=None,
17851998
if smooth_frames or max([record.samps_per_frame[c] for c in channels]) == 1:
17861999
# Read signals from the associated dat files that contain
17872000
# wanted channels
1788-
if record_name.endswith('.edf'):
2001+
if record_name.endswith('.edf') or record_name.endswith('.wav'):
17892002
record.d_signal = _signal._rd_segment(record.file_name,
17902003
dir_name, pn_dir,
17912004
record.fmt,
@@ -1825,7 +2038,7 @@ def rdrecord(record_name, sampfrom=0, sampto=None, channels=None,
18252038

18262039
# Return each sample of the signals with multiple samples per frame
18272040
else:
1828-
if record_name.endswith('.edf'):
2041+
if record_name.endswith('.edf') or record_name.endswith('.wav'):
18292042
record.e_d_signal = _signal._rd_segment(record.file_name,
18302043
dir_name, pn_dir,
18312044
record.fmt,

0 commit comments

Comments
 (0)