""" 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 = ' 1: data = data.reshape(-1, channels) return data def _skip_unknown_chunk(fid, is_big_endian): if is_big_endian: fmt = '>I' else: fmt = '>> 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(' 0xFFFFFFFF: raise ValueError("Data exceeds wave file size limit") fid.write(header_data) # data chunk fid.write(b'data') fid.write(struct.pack('' 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('= 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())