|
10 | 10 | import mne
|
11 | 11 | import math
|
12 | 12 | import functools
|
| 13 | +import struct |
13 | 14 | import pdb
|
14 | 15 |
|
15 | 16 | from wfdb.io import _header
|
@@ -1497,6 +1498,214 @@ def edf2mit(record_name, pn_dir=None, delete_file=True, record_only=False):
|
1497 | 1498 | pass
|
1498 | 1499 |
|
1499 | 1500 |
|
| 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 | + |
1500 | 1709 | #------------------------- Reading Records --------------------------- #
|
1501 | 1710 |
|
1502 | 1711 |
|
@@ -1626,6 +1835,8 @@ def rdrecord(record_name, sampfrom=0, sampto=None, channels=None,
|
1626 | 1835 | parameter is set, this parameter should contain just the base
|
1627 | 1836 | record name, and the files fill be searched for remotely.
|
1628 | 1837 | 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`. |
1629 | 1840 | sampfrom : int, optional
|
1630 | 1841 | The starting sample number to read for all channels.
|
1631 | 1842 | sampto : int, 'end', optional
|
@@ -1713,6 +1924,8 @@ def rdrecord(record_name, sampfrom=0, sampto=None, channels=None,
|
1713 | 1924 |
|
1714 | 1925 | if record_name.endswith('.edf'):
|
1715 | 1926 | 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) |
1716 | 1929 | else:
|
1717 | 1930 | record = rdheader(record_name, pn_dir=pn_dir, rd_segments=False)
|
1718 | 1931 |
|
@@ -1785,7 +1998,7 @@ def rdrecord(record_name, sampfrom=0, sampto=None, channels=None,
|
1785 | 1998 | if smooth_frames or max([record.samps_per_frame[c] for c in channels]) == 1:
|
1786 | 1999 | # Read signals from the associated dat files that contain
|
1787 | 2000 | # wanted channels
|
1788 |
| - if record_name.endswith('.edf'): |
| 2001 | + if record_name.endswith('.edf') or record_name.endswith('.wav'): |
1789 | 2002 | record.d_signal = _signal._rd_segment(record.file_name,
|
1790 | 2003 | dir_name, pn_dir,
|
1791 | 2004 | record.fmt,
|
@@ -1825,7 +2038,7 @@ def rdrecord(record_name, sampfrom=0, sampto=None, channels=None,
|
1825 | 2038 |
|
1826 | 2039 | # Return each sample of the signals with multiple samples per frame
|
1827 | 2040 | else:
|
1828 |
| - if record_name.endswith('.edf'): |
| 2041 | + if record_name.endswith('.edf') or record_name.endswith('.wav'): |
1829 | 2042 | record.e_d_signal = _signal._rd_segment(record.file_name,
|
1830 | 2043 | dir_name, pn_dir,
|
1831 | 2044 | record.fmt,
|
|
0 commit comments