#!/usr/bin/python2.6 # # Author: Olivier Gillet (ol.gillet@gmail.com) """Class and functions to read and write numpy array from and to audio files.""" import copy import logging import numpy import struct import sys sys.path.append('.') # Constant used when converting between unsigned char and float. A different # value is used in both directions to avoid clipping. _UNSIGNED_CHAR_TO_FLOAT_SCALE = 128.0 _FLOAT_TO_UNSIGNED_CHAR_SCALE = 127.0 _DATA_CHUNK_HEADER_SIZE = 8 _FMT_CHUNK_DATA_SIZE = 16 _FMT_CHUNK_HEADER_SIZE = 8 _RIFF_FORMAT_DESCRIPTOR_SIZE = 4 class AudioIoException(Exception): """An error indicating a failure in audio file reading/writing.""" def __init__(self, message): """Initializes an AudioIoException object.""" Exception.__init__(self, 'Audio IO error: %s' % message) def _ReadBytesOrFail(file_object, num_bytes, error_message): """Read a given number of bytes from the file or raise an error. Args: file_object: file object. num_bytes: int. number of bytes to read. error_message: string. text message of the exception thrown when the number of bytes could not be read (for example, identifying which section the caller attempted to read.) Returns: String with the bytes read from the file. Raises: AudioIoException: - The required number of bytes could not be read from the file. """ read = file_object.read(num_bytes) if len(read) < num_bytes: raise AudioIoException(error_message) return read def _GoToIffChunk(file_object, iff_chunk_id): """Jump to a named chunk in a (R)IFF file. Args: file_object: file object. iff_chunk_id: 4 chars ID of the chunk. Returns: length of the chunk in bytes. -1 if the chunk has not been found. If the chunk is found, file_object is positioned at the beginning of the chunk. Otherwise, it is positioned at the end of the file. """ while True: chunk_id = file_object.read(4) if len(chunk_id) < 4: return -1 chunk_size = file_object.read(4) if len(chunk_size) < 4: return -1 chunk_size = struct.unpack(' 96000: raise AudioIoException('Invalid sample rate') if bitdepth != 8 and bitdepth != 16: raise AudioIoException('Unsupported bit depth') sample_data_size = _GoToIffChunk(f, 'data') num_samples = sample_data_size / (bitdepth / 8) # Make sure we are reading a number of samples which is a multiple of the # number of channels. Some corrupted stereo .wav files may contain 5 samples! num_samples -= num_samples % num_channels if bitdepth == 8: samples = numpy.fromfile(f, dtype=numpy.ubyte, count=num_samples) if scale: samples = (samples / _UNSIGNED_CHAR_TO_FLOAT_SCALE) - 1.0 else: bytes = bitdepth / 8 samples = numpy.fromfile(f, dtype=' 0: scaled_signal = signal / norm else: scaled_signal = copy.copy(signal) if norm > 1.0: logging.warning('Some samples will be clipped.') # Clip samples above 1 and below -1. scaled_signal[scaled_signal < -1] = -1 scaled_signal[scaled_signal > 1] = 1 if bitdepth == 8: scaled_signal = (scaled_signal + 1.0) * _FLOAT_TO_UNSIGNED_CHAR_SCALE scaled_signal = numpy.array(scaled_signal, dtype=numpy.uint8) else: scale = (1 << (bitdepth - 1)) - 1 # pylint: disable-msg=C6407 scaled_signal = scaled_signal * scale scaled_signal = numpy.array(scaled_signal, dtype='i%d' % (bitdepth / 8)) return scaled_signal def WriteWavFile(signal, sample_rate, file_name, bitdepth=16, normalize=True): """Write a .wav file from a numpy array. Args: signal: 2-dimensional numpy array, of size (num_samples, num_channels). sample_rate: int. sample rate of the signal in Hz. file_name: string. name of the destination file. bitdepth: int. bitdepth in bits (default 16). normalize: boolean. if set to True, scale the data to the [-1, 1] range before writing. """ if signal.dtype == numpy.uint8 or signal.dtype == numpy.int16: bitdepth = signal.dtype.itemsize * 8 scaled_signal = signal else: scaled_signal = Quantize(signal, bitdepth, normalize=normalize) if scaled_signal.ndim == 1: num_channels = 1 else: num_channels = scaled_signal.shape[1] # Compute the total size of the output .wav file, minus the size of the # first two fields of the RIFF header. # RIFF Format. total_size = _RIFF_FORMAT_DESCRIPTOR_SIZE # 'fmt ' chunk. total_size += _FMT_CHUNK_HEADER_SIZE + _FMT_CHUNK_DATA_SIZE # 'data' chunk. total_size += _DATA_CHUNK_HEADER_SIZE + scaled_signal.nbytes f = file(file_name, 'w') try: f.write('RIFF') f.write(struct.pack('