Files
2026-06-04 17:01:14 +03:00

478 lines
17 KiB
Python

"""
The wavio module defines the functions:
read(file)
Read a WAV file and return a `wavio.Wav` object, with attributes
`data`, `rate` and `sampwidth`.
write(filename, data, rate, scale=None, sampwidth=None)
Write a numpy array to a WAV file.
-----
Author: Warren Weckesser
License: BSD 2-Clause:
Copyright (c) 2015-2022, Warren Weckesser
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
"""
import warnings as _warnings
import wave as _wave
import numpy as _np
__version__ = "0.0.9"
class ClippedDataWarning(UserWarning):
def __init__(self, message=None):
if message is None:
message = ('Some data values were clipped when converted to the '
'output format.')
self.args = (message,)
class ClippedDataError(RuntimeError):
def __init__(self, message=None):
if message is None:
message = ('Some data values were clipped when converted to the '
'output format.')
self.args = (message,)
def _wav2array(nchannels, sampwidth, data):
"""data must be the string containing the bytes from the wav file."""
num_samples, remainder = divmod(len(data), sampwidth * nchannels)
if remainder > 0:
raise ValueError('The length of data is not a multiple of '
'sampwidth * num_channels.')
if sampwidth > 4:
raise ValueError("sampwidth must not be greater than 4.")
if sampwidth == 3:
a = _np.empty((num_samples, nchannels, 4), dtype=_np.uint8)
raw_bytes = _np.frombuffer(data, dtype=_np.uint8)
a[:, :, :sampwidth] = raw_bytes.reshape(-1, nchannels, sampwidth)
a[:, :, sampwidth:] = (a[:, :, sampwidth - 1:sampwidth] >> 7) * 255
result = a.view('<i4').reshape(a.shape[:-1])
else:
# 8 bit samples are stored as unsigned ints; others as signed ints.
dt_char = 'u' if sampwidth == 1 else 'i'
a = _np.frombuffer(data, dtype='<%s%d' % (dt_char, sampwidth))
result = a.reshape(-1, nchannels)
return result
def _array2wav(a, sampwidth):
"""
Convert the input array `a` to a string of WAV data.
a.dtype must be one of uint8, int16 or int32. Allowed sampwidth
values are:
dtype sampwidth
uint8 1
int16 2
int32 3 or 4
When sampwidth is 3, the *low* bytes of `a` are assumed to contain
the values to include in the string.
"""
if sampwidth == 3:
# `a` must have dtype int32
if a.ndim == 1:
# Convert to a 2D array with a single column.
a = a.reshape(-1, 1)
# By shifting first 0 bits, then 8, then 16, the resulting output
# is 24 bit little-endian.
a8 = (a.reshape(a.shape + (1,)) >> _np.array([0, 8, 16])) & 255
wavdata = a8.astype(_np.uint8).tobytes()
else:
# Make sure the array is little-endian, and then convert using
# tobytes().
a = a.astype('<' + a.dtype.str[1:], copy=False)
wavdata = a.tobytes()
return wavdata
class Wav(object):
"""
Object returned by `wavio.read`. Attributes are:
data : numpy array
The array of data read from the WAV file. The shape of the array
is (num_samples, num_channels). num_channels is the number of audio
channels (1 for mono, 2 for stereo). The data type of the array
(i.e. data.dtype) is determined by `sampwidth`::
sampwidth dtype
1 numpy.uint8
2 numpy.int16
3 numpy.int32
4 numpy.int32
rate : int
The sampling frequency (i.e. frame rate or sample rate) of the
WAV file.
sampwidth : int
The sample width (i.e. number of bytes per sample) of the WAV file.
For example, `sampwidth == 3` is a 24 bit WAV file.
"""
def __init__(self, data, rate, sampwidth):
self.data = data
self.rate = rate
self.sampwidth = sampwidth
def __repr__(self):
s = ("Wav(data.shape=%s, data.dtype=%s, rate=%r, sampwidth=%r)" %
(self.data.shape, self.data.dtype, self.rate, self.sampwidth))
return s
def read(file):
"""
Read a WAV file.
Parameters
----------
file : string or file object
Either the name of a file or an open file pointer.
Returns
-------
wav : wavio.Wav() instance
The return value is an instance of the class `wavio.Wav`,
with the following attributes:
data : numpy array
The array of data read from the WAV file. The shape of the
array is (num_samples, num_channels). num_channels is the
number of audio channels (1 for mono, 2 for stereo). The
data type of the array (i.e. data.dtype) is determined by
`sampwidth`::
sampwidth dtype
1 numpy.uint8
2 numpy.int16
3 numpy.int32
4 numpy.int32
rate : int
The sampling frequency (i.e. frame rate or sample rate) of the
WAV file.
sampwidth : int
The sample width (i.e. number of bytes per sample) of the
WAV file. For example, `sampwidth == 3` is a 24 bit WAV file.
Notes
-----
This function uses the `wave` module of the Python standard libary
to read the WAV file, so it has the same limitations as that library.
In particular, the function does not read compressed WAV files, and
it does not read files with floating point data.
The array returned by `wavio.read` is always two-dimensional. If the
WAV data is mono, the array will have shape (num_samples, 1).
`wavio.read()` does not scale or normalize the data. The data in the
array `wav.data` is the data that was in the file. When the file
contains 24 bit samples, the resulting numpy array is 32 bit integers,
with values that have been sign-extended.
"""
wav = _wave.open(file)
rate = wav.getframerate()
nchannels = wav.getnchannels()
sampwidth = wav.getsampwidth()
nframes = wav.getnframes()
data = wav.readframes(nframes)
wav.close()
array = _wav2array(nchannels, sampwidth, data)
w = Wav(data=array, rate=rate, sampwidth=sampwidth)
return w
_sampwidth_dtypes = {1: _np.uint8,
2: _np.int16,
3: _np.int32,
4: _np.int32}
_sampwidth_minmax = {1: (0, 255),
2: (-2**15, 2**15 - 1),
3: (-2**23, 2**23 - 1),
4: (-2**31, 2**31 - 1)}
def _float_to_integer(x, sampwidth, scale=None, clip="warn"):
# For a given sampwidth and scale, the actual allowed
# interval for float input is [-(1 + 1/c)*scale, scale],
# where c = 2**(8*sampwidth - 1) - 0.5. Values outside
# that interval will result in clipping.
nbits = 8*sampwidth
c = 2**(nbits - 1) - 0.5
if scale == "auto":
scale = max(_np.max(_np.r_[x[x > 0], 0]),
_np.max(_np.r_[-x[x < 0], 0])/(1 + 1/c))
elif scale is None:
scale = 1.0
if sampwidth == 1:
int_min = 0
midpoint = 128
int_max = 255
else:
int_min = -2**(nbits - 1)
midpoint = 0
int_max = 2**(nbits - 1) - 1
scaled_x = x / scale
if _np.any(scaled_x > 1) or _np.any(scaled_x < -1 - 1/c):
msg = (f'Some data values have been clipped. With scale={scale}, the '
'interval of input values that will not be clipped '
f'is [{-(1 + 1/c)*scale}, {scale}]')
if clip == "warn":
_warnings.warn(ClippedDataWarning(msg))
elif clip == "raise":
raise ClippedDataError(msg)
y = midpoint + _round_with_half_towards_zero(x/scale*c)
y = _np.clip(y, int_min, int_max).astype(_sampwidth_dtypes[sampwidth])
return y
def _round_with_half_towards_zero(x):
s = _np.sign(x)
return s * _np.ceil(_np.abs(x) - 0.5)
def write(file, data, rate, scale=None, sampwidth=None, clip="warn"):
"""
Write the numpy array `data` to a WAV file.
The Python standard library "wave" is used to write the data to the
file, so this function has the same limitations as that module. In
particular, the Python library does not support floating point data,
so this function must convert floating point input to integers before
writing the data to the file. See below for the conversion rules.
*Important notes*
* If `data` has an *integer* data type, signed or unsigned and any bit
depth, the values are never scaled or shifted. The only possible
changes to the values that can occur is if the data must be clipped
to fit the desired output sample width. It is an error to give a
value for the `scale` parameter if `data` has an integer data type.
* If `data` is a floating point type, `sampwidth` must be given. The
default behavior is to scale input values in the range [-1, 1] to
the output range [min_int+1, max_int], where min_int and max_int are
the minimum and maximum values of the output data type determined by
`sampwidth`. See the description of `scale` below for more options.
Parameters
----------
file : string, or file object open for writing in binary mode
Either the name of a file or an open file pointer.
data : numpy array, 1- or 2-dimensional, integer or floating point
If it is 2-d, the rows are the frames (i.e. samples) and the
columns are the channels.
rate : int
The sampling frequency (i.e. frame rate) of the data.
sampwidth : int, optional
The sample width, in bytes, of the output file.
If `sampwidth` is not given, it is inferred (if possible) from
the data type of `data`, as follows::
data.dtype sampwidth
---------- ---------
uint8, int8 1
uint16, int16 2
uint32, int32 4
For any other data types, or to write a 24 bit file, `sampwidth`
must be given.
scale : float, optional
This controls the output range when the input is floating point.
`scale` must not be given when the input data has integer data
type.
if `scale` is a numeric value, then input values in the range
`[-scale, scale]` are mapped to the integer output range centered
at the midpoint of the output range. For 8 bit unsigned integer
output (i.e. `sampwdith=1`), the midpoint is 128. For `sampwidth`
2, 3 or 4 (corresponding to signed integer output), the midpoint
is 0.
If `scale` is the string `"auto"`, the data written to the file is
scaled up or down to occupy the full range of the output data type.
For example, with `sampwidth=2` the input
`data=[-0.5, -1.0, 0.0, 0.25, 1.0]` would result in
the 16 signed integers [-16384, -32767, 0, 8192, 32767] being
written to the WAV file. By setting `scale=2`, the output values
would be [-8192, -16384, 0, 4096, 16384].
When the input is floating point and `scale` is not given, the
default value `scale=1.0` is used.
clip : str, optional
If "warn" (the default), the function will generate a warning if
any of the data values must be clipped when written to the format
of the output array. If "raise", the function will raise an
exception if clipping occurs. If "ignore", no warning or
exception is generated if clipping occurs.
Examples
--------
Create a 3 second 440 Hz sine wave, and save it in a 24-bit WAV file.
>>> import numpy as np
>>> import wavio
>>> rate = 22050 # samples per second
>>> T = 3 # sample duration (seconds)
>>> n = int(rate*T) # number of samples
>>> t = np.arange(n)/rate # grid of time values
>>> f = 440.0 # sound frequency (Hz)
>>> x = np.sin(2*np.pi * f * t)
`x` is a single sine wave with amplitude 1, so we can use the default
`scale`.
>>> wavio.write("sine24.wav", x, rate, sampwidth=3)
Create a file that contains the 16 bit integer values -10000 and 10000
repeated 100 times. Use a sample rate of 8000.
>>> x = np.empty(200, dtype=np.int16)
>>> x[::2] = -10000
>>> x[1::2] = 10000
>>> wavio.write("foo.wav", x, 8000)
Check that the file contains what we expect. The values are checked
for exact equality. The input was an integer array, so the values are
not scaled.
>>> w = wavio.read("foo.wav")
>>> np.all(w.data[:, 0] == x)
True
Write floating point data to a 16 bit WAV file. The floating point
values are assumed to be within the range [-2, 2], and we want the
values 2 and -2 to correspond to the full output range, even if the
actual values in the data do not fill this range. We do that by
specifying `scale=2`.
`T`, `rate` and `t` are from above. The data is the sum of two
sinusoids, with frequencies 440 and 880 Hz, modulated by a parabolic
curve that is zero at the start and end of the data.
>>> envelope = (4/T**2)*(t * (T - t))
>>> omega1 = 2*np.pi*440
>>> omega2 = 2*np.pi*880
>>> y = envelope*(np.sin(omega1*t) + 0.3*np.sin(omega2*t + 0.2))
>>> y.min(), y.max()
(-1.1745469775555515, 1.093833464065767)
Write the WAV file, with `scale=2`.
>>> wavio.write('harmonic.wav', y, rate, sampwidth=2, scale=2)
Check the minimum and maximum integers that were actually written
to the file:
>>> w = wavio.read("harmonic.wav")
>>> w.data.min(), w.data.max()
(-19243, 17921)
If we want the WAV file to use as much of the range of the output
integer type as possible (while still mapping 0.0 in the input to 0 in
the output), we set `scale="auto"`.
>>> wavio.write('harmonic_full.wav', y, rate, sampwidth=2, scale="auto")
>>> w = wavio.read('harmonic_full.wav')
>>> w.data.min(), w.data.max()
(-32768, 30517)
"""
if clip not in ["ignore", "warn", "raise"]:
raise ValueError('clip must be one of "ignore", "warn" or "raise".')
data = _np.asarray(data)
if sampwidth is None:
if not _np.issubdtype(data.dtype, _np.integer) or data.itemsize > 4:
raise ValueError('when data.dtype is not an 8-, 16-, or 32-bit '
'integer type, sampwidth must be specified.')
sampwidth = data.itemsize
else:
if sampwidth not in [1, 2, 3, 4]:
raise ValueError('sampwidth must be 1, 2, 3 or 4.')
outdtype = _sampwidth_dtypes[sampwidth]
outmin, outmax = _sampwidth_minmax[sampwidth]
if _np.issubdtype(data.dtype, _np.integer):
if scale is not None:
raise ValueError('The scale parameter must not be set when the '
'input is an integer array. No shifting or '
'scaling is done to integer input values.')
if (data.min() < outmin or data.max() > outmax):
if clip == "warn":
_warnings.warn(ClippedDataWarning())
elif clip == "raise":
raise ClippedDataError()
clip_min = max(outmin, _np.iinfo(data.dtype).min)
clip_max = min(outmax, _np.iinfo(data.dtype).max)
data = data.clip(clip_min, clip_max).astype(outdtype)
elif _np.issubdtype(data.dtype, _np.floating):
data = _float_to_integer(data, sampwidth, scale=scale, clip=clip)
else:
raise TypeError(f'unsupported input array data type: {data.dtype}')
# At this point, `data` has been converted to have one of the following:
# sampwidth dtype
# --------- -----
# 1 uint8
# 2 int16
# 3 int32
# 4 int32
# The values in `data` are in the form in which they will be saved;
# no more scaling will take place.
if data.ndim == 1:
data = data.reshape(-1, 1)
wavdata = _array2wav(data, sampwidth)
w = _wave.open(file, 'wb')
w.setnchannels(data.shape[1])
w.setsampwidth(sampwidth)
w.setframerate(rate)
w.writeframes(wavdata)
w.close()