460 lines
14 KiB
Python
460 lines
14 KiB
Python
|
"""
|
||
|
Module to read / write wav files using numpy arrays
|
||
|
|
||
|
Functions
|
||
|
---------
|
||
|
`read`: Return the sample rate (in samples/sec) and data from a WAV file.
|
||
|
|
||
|
`write`: Write a numpy array as a WAV file.
|
||
|
|
||
|
"""
|
||
|
from __future__ import division, print_function, absolute_import
|
||
|
|
||
|
import sys
|
||
|
import numpy
|
||
|
import struct
|
||
|
import warnings
|
||
|
|
||
|
|
||
|
__all__ = [
|
||
|
'WavFileWarning',
|
||
|
'read',
|
||
|
'write'
|
||
|
]
|
||
|
|
||
|
|
||
|
class WavFileWarning(UserWarning):
|
||
|
pass
|
||
|
|
||
|
|
||
|
WAVE_FORMAT_PCM = 0x0001
|
||
|
WAVE_FORMAT_IEEE_FLOAT = 0x0003
|
||
|
WAVE_FORMAT_EXTENSIBLE = 0xfffe
|
||
|
KNOWN_WAVE_FORMATS = (WAVE_FORMAT_PCM, WAVE_FORMAT_IEEE_FLOAT)
|
||
|
|
||
|
# assumes file pointer is immediately
|
||
|
# after the 'fmt ' id
|
||
|
|
||
|
|
||
|
def _read_fmt_chunk(fid, is_big_endian):
|
||
|
"""
|
||
|
Returns
|
||
|
-------
|
||
|
size : int
|
||
|
size of format subchunk in bytes (minus 8 for "fmt " and itself)
|
||
|
format_tag : int
|
||
|
PCM, float, or compressed format
|
||
|
channels : int
|
||
|
number of channels
|
||
|
fs : int
|
||
|
sampling frequency in samples per second
|
||
|
bytes_per_second : int
|
||
|
overall byte rate for the file
|
||
|
block_align : int
|
||
|
bytes per sample, including all channels
|
||
|
bit_depth : int
|
||
|
bits per sample
|
||
|
"""
|
||
|
if is_big_endian:
|
||
|
fmt = '>'
|
||
|
else:
|
||
|
fmt = '<'
|
||
|
|
||
|
size = res = struct.unpack(fmt+'I', fid.read(4))[0]
|
||
|
bytes_read = 0
|
||
|
|
||
|
if size < 16:
|
||
|
raise ValueError("Binary structure of wave file is not compliant")
|
||
|
|
||
|
res = struct.unpack(fmt+'HHIIHH', fid.read(16))
|
||
|
bytes_read += 16
|
||
|
|
||
|
format_tag, channels, fs, bytes_per_second, block_align, bit_depth = res
|
||
|
|
||
|
if format_tag == WAVE_FORMAT_EXTENSIBLE and size >= (16+2):
|
||
|
ext_chunk_size = struct.unpack(fmt+'H', fid.read(2))[0]
|
||
|
bytes_read += 2
|
||
|
if ext_chunk_size >= 22:
|
||
|
extensible_chunk_data = fid.read(22)
|
||
|
bytes_read += 22
|
||
|
raw_guid = extensible_chunk_data[2+4:2+4+16]
|
||
|
# GUID template {XXXXXXXX-0000-0010-8000-00AA00389B71} (RFC-2361)
|
||
|
# MS GUID byte order: first three groups are native byte order,
|
||
|
# rest is Big Endian
|
||
|
if is_big_endian:
|
||
|
tail = b'\x00\x00\x00\x10\x80\x00\x00\xAA\x00\x38\x9B\x71'
|
||
|
else:
|
||
|
tail = b'\x00\x00\x10\x00\x80\x00\x00\xAA\x00\x38\x9B\x71'
|
||
|
if raw_guid.endswith(tail):
|
||
|
format_tag = struct.unpack(fmt+'I', raw_guid[:4])[0]
|
||
|
else:
|
||
|
raise ValueError("Binary structure of wave file is not compliant")
|
||
|
|
||
|
if format_tag not in KNOWN_WAVE_FORMATS:
|
||
|
raise ValueError("Unknown wave file format")
|
||
|
|
||
|
# move file pointer to next chunk
|
||
|
if size > (bytes_read):
|
||
|
fid.read(size - bytes_read)
|
||
|
|
||
|
return (size, format_tag, channels, fs, bytes_per_second, block_align,
|
||
|
bit_depth)
|
||
|
|
||
|
|
||
|
# assumes file pointer is immediately after the 'data' id
|
||
|
def _read_data_chunk(fid, format_tag, channels, bit_depth, is_big_endian,
|
||
|
mmap=False):
|
||
|
if is_big_endian:
|
||
|
fmt = '>I'
|
||
|
else:
|
||
|
fmt = '<I'
|
||
|
|
||
|
# Size of the data subchunk in bytes
|
||
|
size = struct.unpack(fmt, fid.read(4))[0]
|
||
|
|
||
|
# Number of bytes per sample
|
||
|
bytes_per_sample = bit_depth//8
|
||
|
if bit_depth == 8:
|
||
|
dtype = 'u1'
|
||
|
else:
|
||
|
if is_big_endian:
|
||
|
dtype = '>'
|
||
|
else:
|
||
|
dtype = '<'
|
||
|
if format_tag == WAVE_FORMAT_PCM:
|
||
|
dtype += 'i%d' % bytes_per_sample
|
||
|
else:
|
||
|
dtype += 'f%d' % bytes_per_sample
|
||
|
if not mmap:
|
||
|
data = numpy.frombuffer(fid.read(size), dtype=dtype)
|
||
|
else:
|
||
|
start = fid.tell()
|
||
|
data = numpy.memmap(fid, dtype=dtype, mode='c', offset=start,
|
||
|
shape=(size//bytes_per_sample,))
|
||
|
fid.seek(start + size)
|
||
|
|
||
|
if channels > 1:
|
||
|
data = data.reshape(-1, channels)
|
||
|
return data
|
||
|
|
||
|
|
||
|
def _skip_unknown_chunk(fid, is_big_endian):
|
||
|
if is_big_endian:
|
||
|
fmt = '>I'
|
||
|
else:
|
||
|
fmt = '<I'
|
||
|
|
||
|
data = fid.read(4)
|
||
|
# call unpack() and seek() only if we have really read data from file
|
||
|
# otherwise empty read at the end of the file would trigger
|
||
|
# unnecessary exception at unpack() call
|
||
|
# in case data equals somehow to 0, there is no need for seek() anyway
|
||
|
if data:
|
||
|
size = struct.unpack(fmt, data)[0]
|
||
|
fid.seek(size, 1)
|
||
|
|
||
|
|
||
|
def _read_riff_chunk(fid):
|
||
|
str1 = fid.read(4) # File signature
|
||
|
if str1 == b'RIFF':
|
||
|
is_big_endian = False
|
||
|
fmt = '<I'
|
||
|
elif str1 == b'RIFX':
|
||
|
is_big_endian = True
|
||
|
fmt = '>I'
|
||
|
else:
|
||
|
# There are also .wav files with "FFIR" or "XFIR" signatures?
|
||
|
raise ValueError("File format {}... not "
|
||
|
"understood.".format(repr(str1)))
|
||
|
|
||
|
# Size of entire file
|
||
|
file_size = struct.unpack(fmt, fid.read(4))[0] + 8
|
||
|
|
||
|
str2 = fid.read(4)
|
||
|
if str2 != b'WAVE':
|
||
|
raise ValueError("Not a WAV file.")
|
||
|
|
||
|
return file_size, is_big_endian
|
||
|
|
||
|
|
||
|
def read(filename, mmap=False):
|
||
|
"""
|
||
|
Open a WAV file
|
||
|
|
||
|
Return the sample rate (in samples/sec) and data from a WAV file.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
filename : string or open file handle
|
||
|
Input wav file.
|
||
|
mmap : bool, optional
|
||
|
Whether to read data as memory-mapped.
|
||
|
Only to be used on real files (Default: False).
|
||
|
|
||
|
.. versionadded:: 0.12.0
|
||
|
|
||
|
Returns
|
||
|
-------
|
||
|
rate : int
|
||
|
Sample rate of wav file.
|
||
|
data : numpy array
|
||
|
Data read from wav file. Data-type is determined from the file;
|
||
|
see Notes.
|
||
|
|
||
|
Notes
|
||
|
-----
|
||
|
This function cannot read wav files with 24-bit data.
|
||
|
|
||
|
Common data types: [1]_
|
||
|
|
||
|
===================== =========== =========== =============
|
||
|
WAV format Min Max NumPy dtype
|
||
|
===================== =========== =========== =============
|
||
|
32-bit floating-point -1.0 +1.0 float32
|
||
|
32-bit PCM -2147483648 +2147483647 int32
|
||
|
16-bit PCM -32768 +32767 int16
|
||
|
8-bit PCM 0 255 uint8
|
||
|
===================== =========== =========== =============
|
||
|
|
||
|
Note that 8-bit PCM is unsigned.
|
||
|
|
||
|
References
|
||
|
----------
|
||
|
.. [1] IBM Corporation and Microsoft Corporation, "Multimedia Programming
|
||
|
Interface and Data Specifications 1.0", section "Data Format of the
|
||
|
Samples", August 1991
|
||
|
http://www.tactilemedia.com/info/MCI_Control_Info.html
|
||
|
|
||
|
Examples
|
||
|
--------
|
||
|
>>> from os.path import dirname, join as pjoin
|
||
|
>>> import scipy.io as sio
|
||
|
|
||
|
Get the filename for an example .wav file from the tests/data directory.
|
||
|
|
||
|
>>> data_dir = pjoin(dirname(sio.__file__), 'tests', 'data')
|
||
|
>>> wav_fname = pjoin(data_dir, 'test-44100Hz-2ch-32bit-float-be.wav')
|
||
|
|
||
|
Load the .wav file contents.
|
||
|
|
||
|
>>> samplerate, data = sio.wavfile.read(wav_fname)
|
||
|
>>> print(f"number of channels = {data.shape[1]}")
|
||
|
number of channels = 2
|
||
|
>>> length = data.shape[0] / samplerate
|
||
|
>>> print(f"length = {length}s")
|
||
|
length = 0.01s
|
||
|
|
||
|
Plot the waveform.
|
||
|
|
||
|
>>> import matplotlib.pyplot as plt
|
||
|
>>> import numpy as np
|
||
|
>>> time = np.linspace(0., length, data.shape[0])
|
||
|
>>> plt.plot(time, data[:, 0], label="Left channel")
|
||
|
>>> plt.plot(time, data[:, 1], label="Right channel")
|
||
|
>>> plt.legend()
|
||
|
>>> plt.xlabel("Time [s]")
|
||
|
>>> plt.ylabel("Amplitude")
|
||
|
>>> plt.show()
|
||
|
|
||
|
"""
|
||
|
if hasattr(filename, 'read'):
|
||
|
fid = filename
|
||
|
mmap = False
|
||
|
else:
|
||
|
fid = open(filename, 'rb')
|
||
|
|
||
|
try:
|
||
|
file_size, is_big_endian = _read_riff_chunk(fid)
|
||
|
fmt_chunk_received = False
|
||
|
data_chunk_received = False
|
||
|
channels = 1
|
||
|
bit_depth = 8
|
||
|
format_tag = WAVE_FORMAT_PCM
|
||
|
while fid.tell() < file_size:
|
||
|
# read the next chunk
|
||
|
chunk_id = fid.read(4)
|
||
|
|
||
|
if not chunk_id:
|
||
|
if data_chunk_received:
|
||
|
# End of file but data successfully read
|
||
|
warnings.warn(
|
||
|
"Reached EOF prematurely; finished at {:d} bytes, "
|
||
|
"expected {:d} bytes from header."
|
||
|
.format(fid.tell(), file_size),
|
||
|
WavFileWarning, stacklevel=2)
|
||
|
break
|
||
|
else:
|
||
|
raise ValueError("Unexpected end of file.")
|
||
|
elif len(chunk_id) < 4:
|
||
|
raise ValueError("Incomplete wav chunk.")
|
||
|
|
||
|
if chunk_id == b'fmt ':
|
||
|
fmt_chunk_received = True
|
||
|
fmt_chunk = _read_fmt_chunk(fid, is_big_endian)
|
||
|
format_tag, channels, fs = fmt_chunk[1:4]
|
||
|
bit_depth = fmt_chunk[6]
|
||
|
if bit_depth not in (8, 16, 32, 64, 96, 128):
|
||
|
raise ValueError("Unsupported bit depth: the wav file "
|
||
|
"has {}-bit data.".format(bit_depth))
|
||
|
elif chunk_id == b'fact':
|
||
|
_skip_unknown_chunk(fid, is_big_endian)
|
||
|
elif chunk_id == b'data':
|
||
|
data_chunk_received = True
|
||
|
if not fmt_chunk_received:
|
||
|
raise ValueError("No fmt chunk before data")
|
||
|
data = _read_data_chunk(fid, format_tag, channels, bit_depth,
|
||
|
is_big_endian, mmap)
|
||
|
elif chunk_id == b'LIST':
|
||
|
# Someday this could be handled properly but for now skip it
|
||
|
_skip_unknown_chunk(fid, is_big_endian)
|
||
|
elif chunk_id in (b'JUNK', b'Fake'):
|
||
|
# Skip alignment chunks without warning
|
||
|
_skip_unknown_chunk(fid, is_big_endian)
|
||
|
else:
|
||
|
warnings.warn("Chunk (non-data) not understood, skipping it.",
|
||
|
WavFileWarning, stacklevel=2)
|
||
|
_skip_unknown_chunk(fid, is_big_endian)
|
||
|
finally:
|
||
|
if not hasattr(filename, 'read'):
|
||
|
fid.close()
|
||
|
else:
|
||
|
fid.seek(0)
|
||
|
|
||
|
return fs, data
|
||
|
|
||
|
|
||
|
def write(filename, rate, data):
|
||
|
"""
|
||
|
Write a numpy array as a WAV file.
|
||
|
|
||
|
Parameters
|
||
|
----------
|
||
|
filename : string or open file handle
|
||
|
Output wav file.
|
||
|
rate : int
|
||
|
The sample rate (in samples/sec).
|
||
|
data : ndarray
|
||
|
A 1-D or 2-D numpy array of either integer or float data-type.
|
||
|
|
||
|
Notes
|
||
|
-----
|
||
|
* Writes a simple uncompressed WAV file.
|
||
|
* To write multiple-channels, use a 2-D array of shape
|
||
|
(Nsamples, Nchannels).
|
||
|
* The bits-per-sample and PCM/float will be determined by the data-type.
|
||
|
|
||
|
Common data types: [1]_
|
||
|
|
||
|
===================== =========== =========== =============
|
||
|
WAV format Min Max NumPy dtype
|
||
|
===================== =========== =========== =============
|
||
|
32-bit floating-point -1.0 +1.0 float32
|
||
|
32-bit PCM -2147483648 +2147483647 int32
|
||
|
16-bit PCM -32768 +32767 int16
|
||
|
8-bit PCM 0 255 uint8
|
||
|
===================== =========== =========== =============
|
||
|
|
||
|
Note that 8-bit PCM is unsigned.
|
||
|
|
||
|
References
|
||
|
----------
|
||
|
.. [1] IBM Corporation and Microsoft Corporation, "Multimedia Programming
|
||
|
Interface and Data Specifications 1.0", section "Data Format of the
|
||
|
Samples", August 1991
|
||
|
http://www.tactilemedia.com/info/MCI_Control_Info.html
|
||
|
|
||
|
Examples
|
||
|
--------
|
||
|
Create a 100Hz sine wave, sampled at 44100Hz.
|
||
|
Write to 16-bit PCM, Mono.
|
||
|
|
||
|
>>> from scipy.io.wavfile import write
|
||
|
>>> samplerate = 44100; fs = 100
|
||
|
>>> t = np.linspace(0., 1., samplerate)
|
||
|
>>> amplitude = np.iinfo(np.int16).max
|
||
|
>>> data = amplitude * np.sin(2. * np.pi * fs * t)
|
||
|
>>> write("example.wav", samplerate, data)
|
||
|
|
||
|
"""
|
||
|
if hasattr(filename, 'write'):
|
||
|
fid = filename
|
||
|
else:
|
||
|
fid = open(filename, 'wb')
|
||
|
|
||
|
fs = rate
|
||
|
|
||
|
try:
|
||
|
dkind = data.dtype.kind
|
||
|
if not (dkind == 'i' or dkind == 'f' or (dkind == 'u' and
|
||
|
data.dtype.itemsize == 1)):
|
||
|
raise ValueError("Unsupported data type '%s'" % data.dtype)
|
||
|
|
||
|
header_data = b''
|
||
|
|
||
|
header_data += b'RIFF'
|
||
|
header_data += b'\x00\x00\x00\x00'
|
||
|
header_data += b'WAVE'
|
||
|
|
||
|
# fmt chunk
|
||
|
header_data += b'fmt '
|
||
|
if dkind == 'f':
|
||
|
format_tag = WAVE_FORMAT_IEEE_FLOAT
|
||
|
else:
|
||
|
format_tag = WAVE_FORMAT_PCM
|
||
|
if data.ndim == 1:
|
||
|
channels = 1
|
||
|
else:
|
||
|
channels = data.shape[1]
|
||
|
bit_depth = data.dtype.itemsize * 8
|
||
|
bytes_per_second = fs*(bit_depth // 8)*channels
|
||
|
block_align = channels * (bit_depth // 8)
|
||
|
|
||
|
fmt_chunk_data = struct.pack('<HHIIHH', format_tag, channels, fs,
|
||
|
bytes_per_second, block_align, bit_depth)
|
||
|
if not (dkind == 'i' or dkind == 'u'):
|
||
|
# add cbSize field for non-PCM files
|
||
|
fmt_chunk_data += b'\x00\x00'
|
||
|
|
||
|
header_data += struct.pack('<I', len(fmt_chunk_data))
|
||
|
header_data += fmt_chunk_data
|
||
|
|
||
|
# fact chunk (non-PCM files)
|
||
|
if not (dkind == 'i' or dkind == 'u'):
|
||
|
header_data += b'fact'
|
||
|
header_data += struct.pack('<II', 4, data.shape[0])
|
||
|
|
||
|
# check data size (needs to be immediately before the data chunk)
|
||
|
if ((len(header_data)-4-4) + (4+4+data.nbytes)) > 0xFFFFFFFF:
|
||
|
raise ValueError("Data exceeds wave file size limit")
|
||
|
|
||
|
fid.write(header_data)
|
||
|
|
||
|
# data chunk
|
||
|
fid.write(b'data')
|
||
|
fid.write(struct.pack('<I', data.nbytes))
|
||
|
if data.dtype.byteorder == '>' or (data.dtype.byteorder == '=' and
|
||
|
sys.byteorder == 'big'):
|
||
|
data = data.byteswap()
|
||
|
_array_tofile(fid, data)
|
||
|
|
||
|
# Determine file size and place it in correct
|
||
|
# position at start of the file.
|
||
|
size = fid.tell()
|
||
|
fid.seek(4)
|
||
|
fid.write(struct.pack('<I', size-8))
|
||
|
|
||
|
finally:
|
||
|
if not hasattr(filename, 'write'):
|
||
|
fid.close()
|
||
|
else:
|
||
|
fid.seek(0)
|
||
|
|
||
|
|
||
|
if sys.version_info[0] >= 3:
|
||
|
def _array_tofile(fid, data):
|
||
|
# ravel gives a c-contiguous buffer
|
||
|
fid.write(data.ravel().view('b').data)
|
||
|
else:
|
||
|
def _array_tofile(fid, data):
|
||
|
fid.write(data.tostring())
|