from attr import attrs, attrib, Factory from attr.validators import instance_of, optional import struct from collections import namedtuple from enum import IntEnum from six import string_types ChunkPosition = namedtuple('ChunkPosition', ['chunkId', 'size', 'data', 'end']) class ChunkIndex(object): def __init__(self, size, position): self._size = size self._position = ChunkPosition(position, position + 4, position + 8, position + 8 + size) @property def size(self): return self._size @property def position(self): return self._position class Format(IntEnum): PCM = 1 WAVE_FORMAT_IEEE_FLOAT = 3 WAVE_FORMAT_EXTENSIBLE = 65534 class FormatInfoChunk(object): """ Class representation of the FormatChunk This class can be either used to create a new format chunk or simplify reading and validation of a format chunk. Once created the object cannot be changed. You can only read the saved data. The order of the constructor arguments might seem a bit strange at first sight. The order corresponds to the order within a BW64 file. This makes it easier to create an object from the data read from a file. Cumbersome values like bytesPerSecond or blockAlignment can be omitted (thus set to `None`). But: if they are set, they have to be correct. Otherwise a ValueError is raised. To simplify the writing of files the FormatChunk (like every Chunk class in this module) has a asByteArray method. This method returns the correct byte array representation of the FormatChunk, which can be directly written to a file. """ def __init__(self, formatTag=1, channelCount=1, sampleRate=48000, bytesPerSecond=None, blockAlignment=None, bitsPerSample=16, cbSize=None, extraData=None): self._formatTag = int(formatTag) self._channelCount = int(channelCount) self._sampleRate = int(sampleRate) self._bitsPerSample = int(bitsPerSample) self._cbSize = cbSize if(extraData): self._extraData = ExtraData(*extraData) else: self._extraData = None if(self.formatTag not in list(Format)): raise ValueError('format not supported: ' + str(self.formatTag)) if self.cbSize: if cbSize != self.cbSize: raise ValueError( 'sanity check failed. \'cbSize\' is ' + str(cbSize) + ' but should be ' + str(self.cbSize) ) if(formatTag == Format.WAVE_FORMAT_EXTENSIBLE): if not self.extraData: raise RuntimeError( 'missing extra data for WAVE_FORMAT_EXTENSIBLE') if(self.channelCount < 1): raise ValueError('channelCount < 1') if(self.sampleRate < 1): raise ValueError('sampleRate < 1') if(self.bitsPerSample not in [16, 24, 32]): raise ValueError('bit depth not supported: ' + str(self._bitsPerSample)) if(bytesPerSecond): if(int(bytesPerSecond) != self.bytesPerSecond): raise ValueError( 'sanity check failed. \'bytesPerSecond\' is ' + str(bytesPerSecond) + ' but should be ' + str(self.bytesPerSecond) ) if(blockAlignment): if(int(blockAlignment) != self.blockAlignment): raise ValueError( 'sanity check failed. \'blockAlignment\' is ' + str(blockAlignment) + ' but should be ' + str(self.blockAlignment) ) if(cbSize): if(int(cbSize) != self.cbSize): raise ValueError( 'sanity check failed. \'cbSize\' is ' + str(cbSize) + ' but should be ' + str(self.cbSize) ) @property def formatTag(self): if ( self.extraData is not None and self._formatTag == Format.WAVE_FORMAT_EXTENSIBLE ): return self.extraData.subFormat else: return self._formatTag @property def channelCount(self): return self._channelCount @property def sampleRate(self): return self._sampleRate @property def bytesPerSecond(self): return self.sampleRate * self.blockAlignment @property def blockAlignment(self): return int(self.channelCount * self.bitsPerSample / 8) @property def bitsPerSample(self): return self._bitsPerSample @property def cbSize(self): if(self.extraData): return len(self.extraData.asByteArray()) else: return 0 @property def extraData(self): return self._extraData def asByteArray(self): byteArrayData = struct.pack('