def __find_next_mp3_frame__(cls, mp3file): from audiotools.id3 import skip_id3v2_comment # if we're starting at an ID3v2 header, skip it to save a bunch of time bytes_skipped = skip_id3v2_comment(mp3file) # then find the next mp3 frame from audiotools.bitstream import BitstreamReader reader = BitstreamReader(mp3file, False) pos = reader.getpos() try: (sync, mpeg_id, layer_description) = reader.parse("11u 2u 2u 1p") except IOError as err: raise err while (not ((sync == 0x7FF) and (mpeg_id in (0, 2, 3)) and (layer_description in (1, 2, 3)))): reader.setpos(pos) reader.skip(8) bytes_skipped += 1 pos = reader.getpos() try: (sync, mpeg_id, layer_description) = reader.parse("11u 2u 2u 1p") except IOError as err: raise err else: reader.setpos(pos) return bytes_skipped
def __init__(self, filename): from audiotools.id3 import skip_id3v2_comment AudioFile.__init__(self, filename) try: with open(filename, "rb") as f: skip_id3v2_comment(f) from audiotools.bitstream import BitstreamReader from audiotools.text import (ERR_TTA_INVALID_SIGNATURE, ERR_TTA_INVALID_FORMAT) reader = BitstreamReader(f, True) (signature, format_, self.__channels__, self.__bits_per_sample__, self.__sample_rate__, self.__total_pcm_frames__ ) = reader.parse("4b 16u 16u 16u 32u 32u 32p") if signature != b"TTA1": raise InvalidTTA(ERR_TTA_INVALID_SIGNATURE) elif format_ != 1: raise InvalidTTA(ERR_TTA_INVALID_FORMAT) self.__total_tta_frames__ = div_ceil( self.__total_pcm_frames__ * 245, self.__sample_rate__ * 256) self.__frame_lengths__ = list( reader.parse( "{:d}* 32u".format(self.__total_tta_frames__) + "32p")) except IOError as msg: raise InvalidTTA(str(msg))
def __init__(self, filename): from audiotools.id3 import skip_id3v2_comment AudioFile.__init__(self, filename) try: with open(filename, "rb") as f: skip_id3v2_comment(f) from audiotools.bitstream import BitstreamReader from audiotools.text import (ERR_TTA_INVALID_SIGNATURE, ERR_TTA_INVALID_FORMAT) reader = BitstreamReader(f, True) (signature, format_, self.__channels__, self.__bits_per_sample__, self.__sample_rate__, self.__total_pcm_frames__) = reader.parse( "4b 16u 16u 16u 32u 32u 32p") if (signature != b"TTA1"): raise InvalidTTA(ERR_TTA_INVALID_SIGNATURE) elif (format_ != 1): raise InvalidTTA(ERR_TTA_INVALID_FORMAT) self.__total_tta_frames__ = div_ceil( self.__total_pcm_frames__ * 245, self.__sample_rate__ * 256) self.__frame_lengths__ = list(reader.parse( "%d* 32u" % (self.__total_tta_frames__) + "32p")) except IOError as msg: raise InvalidTTA(str(msg))
def perform_lookup(disc_id, accuraterip_server="www.accuraterip.com", accuraterip_port=80): """performs web-based lookup using the given DiscID object and returns a dict of {track_number:[(confidence, crc, crc2), ...], ...} where track_number starts from 1 may return a dict of empty lists if no AccurateRip entry is found may raise urllib2.HTTPError if an error occurs querying the server """ from audiotools.bitstream import BitstreamReader try: from urllib.request import urlopen, URLError except ImportError: from urllib2 import urlopen, URLError matches = {n: [] for n in disc_id.track_numbers()} url = "http://%s:%s/accuraterip/%s/%s/%s/%s" % (accuraterip_server, accuraterip_port, str(disc_id)[16], str(disc_id)[15], str(disc_id)[14], disc_id) try: response = BitstreamReader(urlopen(url), True) except URLError: # no CD found matching given parameters return matches try: while (True): (track_count, id1, id2, freedb_disc_id) = response.parse("8u 32u 32u 32u") if (((id1 == disc_id.id1()) and (id2 == disc_id.id2()) and (freedb_disc_id == disc_id.freedb_disc_id()))): for track_number in range(1, track_count + 1): if (track_number in matches): matches[track_number].append( tuple(response.parse("8u 32u 32u"))) except IOError: # keep trying to parse values until the data runs out response.close() return matches
def perform_lookup(disc_id, accuraterip_server="www.accuraterip.com", accuraterip_port=80): """performs web-based lookup using the given DiscID object and returns a dict of {track_number:[(confidence, crc, crc2), ...], ...} where track_number starts from 1 may return a dict of empty lists if no AccurateRip entry is found may raise urllib2.HTTPError if an error occurs querying the server """ from audiotools.bitstream import BitstreamReader try: from urllib.request import urlopen, URLError except ImportError: from urllib2 import urlopen, URLError matches = {n: [] for n in disc_id.track_numbers()} url = "http://%s:%s/accuraterip/%s/%s/%s/%s" % (accuraterip_server, accuraterip_port, str(disc_id)[16], str(disc_id)[15], str(disc_id)[14], disc_id) try: response = BitstreamReader(urlopen(url), True) except URLError: # no CD found matching given parameters return matches try: while True: (track_count, id1, id2, freedb_disc_id) = response.parse("8u 32u 32u 32u") if (((id1 == disc_id.id1()) and (id2 == disc_id.id2()) and (freedb_disc_id == disc_id.freedb_disc_id()))): for track_number in range(1, track_count + 1): if track_number in matches: matches[track_number].append( tuple(response.parse("8u 32u 32u"))) except IOError: # keep trying to parse values until the data runs out response.close() return matches
def __read_identification__(self): from .bitstream import BitstreamReader f = open(self.filename, "rb") try: ogg_reader = BitstreamReader(f, 1) (magic_number, version, header_type, granule_position, self.__serial_number__, page_sequence_number, checksum, segment_count) = ogg_reader.parse("4b 8u 8u 64S 32u 32u 32u 8u") if (magic_number != 'OggS'): from .text import ERR_OGG_INVALID_MAGIC_NUMBER raise InvalidFLAC(ERR_OGG_INVALID_MAGIC_NUMBER) if (version != 0): from .text import ERR_OGG_INVALID_VERSION raise InvalidFLAC(ERR_OGG_INVALID_VERSION) segment_length = ogg_reader.read(8) (vorbis_type, header, version, self.__channels__, self.__sample_rate__, maximum_bitrate, nominal_bitrate, minimum_bitrate, blocksize0, blocksize1, framing) = ogg_reader.parse( "8u 6b 32u 8u 32u 32u 32u 32u 4u 4u 1u") if (vorbis_type != 1): from .text import ERR_VORBIS_INVALID_TYPE raise InvalidVorbis(ERR_VORBIS_INVALID_TYPE) if (header != 'vorbis'): from .text import ERR_VORBIS_INVALID_HEADER raise InvalidVorbis(ERR_VORBIS_INVALID_HEADER) if (version != 0): from .text import ERR_VORBIS_INVALID_VERSION raise InvalidVorbis(ERR_VORBIS_INVALID_VERSION) if (framing != 1): from .text import ERR_VORBIS_INVALID_FRAMING_BIT raise InvalidVorbis(ERR_VORBIS_INVALID_FRAMING_BIT) finally: f.close()
def read(cls, apefile): """returns an ApeTag object from an APEv2 tagged file object may return None if the file object has no tag""" from audiotools.bitstream import BitstreamReader apefile.seek(-32, 2) reader = BitstreamReader(apefile, 1) (preamble, version, tag_size, item_count, read_only, item_encoding, is_header, no_footer, has_header) = reader.parse(cls.HEADER_FORMAT) if ((preamble != "APETAGEX") or (version != 2000)): return None apefile.seek(-tag_size, 2) return cls([ApeTagItem.parse(reader) for i in range(item_count)], contains_header=has_header, contains_footer=True)
def get_metadata(self): """returns a MetaData object, or None raises IOError if unable to read the file""" from cStringIO import StringIO from audiotools.bitstream import BitstreamReader from audiotools.ogg import PacketReader, PageReader from audiotools.vorbiscomment import VorbisComment reader = PacketReader(PageReader(open(self.filename, "rb"))) identification = reader.read_packet() comment = BitstreamReader(StringIO(reader.read_packet()), True) (packet_type, packet_header) = comment.parse("8u 6b") if ((packet_type == 3) and (packet_header == 'vorbis')): vendor_string = \ comment.read_bytes(comment.read(32)).decode('utf-8') comment_strings = [ comment.read_bytes(comment.read(32)).decode('utf-8') for i in xrange(comment.read(32))] if (comment.read(1) == 1): # framing bit return VorbisComment(comment_strings, vendor_string) else: return None else: return None
def parse(cls, mp3_file): """given an MP3 file, returns an ID3v1Comment raises ValueError if the comment is invalid""" from audiotools.bitstream import BitstreamReader mp3_file.seek(-128, 2) reader = BitstreamReader(mp3_file, 0) (tag, track_name, artist_name, album_name, year, comment, track_number, genre) = reader.parse("3b 30b 30b 30b 4b 28b 8p 1b 1b") if (tag != 'TAG'): raise ValueError(u"invalid ID3v1 tag") return ID3v1Comment(track_name=track_name, artist_name=artist_name, album_name=album_name, year=year, comment=comment, track_number=track_number, genre=genre)
def validate_footer(footer, ssnd_bytes_written): """given a footer string as returned by aiff_header_footer() and PCM stream parameters, returns True if the footer is valid raises ValueError is the footer is invalid""" from io import BytesIO from audiotools.bitstream import BitstreamReader total_size = len(footer) aiff_file = BitstreamReader(BytesIO(footer), False) try: # ensure footer is padded properly if necessary # based on size of data bytes written if ssnd_bytes_written % 2: aiff_file.skip_bytes(1) total_size -= 1 while total_size > 0: (chunk_id, chunk_size) = aiff_file.parse("4b 32u") if frozenset(chunk_id).issubset(AiffAudio.PRINTABLE_ASCII): total_size -= 8 else: from audiotools.text import ERR_AIFF_INVALID_CHUNK raise ValueError(ERR_AIFF_INVALID_CHUNK) if chunk_id == b"COMM": # ensure no COMM chunks are found from audiotools.text import ERR_AIFF_MULTIPLE_COMM_CHUNKS raise ValueError(ERR_AIFF_MULTIPLE_COMM_CHUNKS) elif chunk_id == b"SSND": # ensure no SSND chunks are found from audiotools.text import ERR_AIFF_MULTIPLE_SSND_CHUNKS raise ValueError(ERR_AIFF_MULTIPLE_SSND_CHUNKS) else: # skip the full contents of non-audio chunks if chunk_size % 2: aiff_file.skip_bytes(chunk_size + 1) total_size -= (chunk_size + 1) else: aiff_file.skip_bytes(chunk_size) total_size -= chunk_size else: return True except IOError: from audiotools.text import ERR_AIFF_FOOTER_IOERROR raise ValueError(ERR_AIFF_FOOTER_IOERROR)
def validate_footer(footer, data_bytes_written): """given a footer string as returned by wave_header_footer() and PCM stream parameters, returns True if the footer is valid raises ValueError if the footer is invalid""" from io import BytesIO from audiotools.bitstream import BitstreamReader total_size = len(footer) wave_file = BitstreamReader(BytesIO(footer), True) try: # ensure footer is padded properly if necessary # based on size of data bytes written if data_bytes_written % 2: wave_file.skip_bytes(1) total_size -= 1 while total_size > 0: (chunk_id, chunk_size) = wave_file.parse("4b 32u") if not frozenset(chunk_id).issubset(WaveAudio.PRINTABLE_ASCII): from audiotools.text import ERR_WAV_INVALID_CHUNK raise ValueError(ERR_WAV_INVALID_CHUNK) else: total_size -= 8 if chunk_id == b"fmt ": # ensure no fmt chunks are found from audiotools.text import ERR_WAV_MULTIPLE_FMT raise ValueError(ERR_WAV_MULTIPLE_FMT) elif chunk_id == b"data": # ensure no data chunks are found from audiotools.text import ERR_WAV_MULTIPLE_DATA raise ValueError(ERR_WAV_MULTIPLE_DATA) else: # skip the full contents of non-audio chunks if chunk_size % 2: wave_file.skip_bytes(chunk_size + 1) total_size -= (chunk_size + 1) else: wave_file.skip_bytes(chunk_size) total_size -= chunk_size else: return True except IOError: from audiotools.text import ERR_WAV_FOOTER_IOERROR raise ValueError(ERR_WAV_FOOTER_IOERROR)
def __find_mp3_start__(cls, mp3file): """places mp3file at the position of the MP3 file's start""" from audiotools.id3 import skip_id3v2_comment # if we're starting at an ID3v2 header, skip it to save a bunch of time skip_id3v2_comment(mp3file) from audiotools.bitstream import BitstreamReader reader = BitstreamReader(mp3file, False) # skip over any bytes that aren't a valid MPEG header pos = reader.getpos() (frame_sync, mpeg_id, layer) = reader.parse("11u 2u 2u 1p") while (not ((frame_sync == 0x7FF) and (mpeg_id in (0, 2, 3)) and (layer in (1, 2, 3)))): reader.setpos(pos) reader.skip(8) pos = reader.getpos() reader.setpos(pos)
def __init__(self, filename): """filename is a plain string""" AudioFile.__init__(self, filename) self.__channels__ = 0 self.__channel_mask__ = 0 # get channel count and channel mask from first packet from audiotools.bitstream import BitstreamReader try: f = open(filename, "rb") try: ogg_reader = BitstreamReader(f, 1) (magic_number, version, header_type, granule_position, self.__serial_number__, page_sequence_number, checksum, segment_count ) = ogg_reader.parse("4b 8u 8u 64S 32u 32u 32u 8u") if (magic_number != 'OggS'): from audiotools.text import ERR_OGG_INVALID_MAGIC_NUMBER raise InvalidOpus(ERR_OGG_INVALID_MAGIC_NUMBER) if (version != 0): from audiotools.text import ERR_OGG_INVALID_VERSION raise InvalidOpus(ERR_OGG_INVALID_VERSION) segment_length = ogg_reader.read(8) (opushead, version, self.__channels__, pre_skip, input_sample_rate, output_gain, mapping_family) = ogg_reader.parse("8b 8u 8u 16u 32u 16s 8u") if (opushead != "OpusHead"): from audiotools.text import ERR_OPUS_INVALID_TYPE raise InvalidOpus(ERR_OPUS_INVALID_TYPE) if (version != 1): from audiotools.text import ERR_OPUS_INVALID_VERSION raise InvalidOpus(ERR_OPUS_INVALID_VERSION) if (self.__channels__ == 0): from audiotools.text import ERR_OPUS_INVALID_CHANNELS raise InvalidOpus(ERR_OPUS_INVALID_CHANNELS) # FIXME - assign channel mask from mapping family if (mapping_family == 0): if (self.__channels__ == 1): self.__channel_mask__ = VorbisChannelMask(0x4) elif (self.__channels__ == 2): self.__channel_mask__ = VorbisChannelMask(0x3) else: self.__channel_mask__ = VorbisChannelMask(0) else: (stream_count, coupled_stream_count) = ogg_reader.parse("8u 8u") if (self.__channels__ != ((coupled_stream_count * 2) + (stream_count - coupled_stream_count))): from audiotools.text import ERR_OPUS_INVALID_CHANNELS raise InvalidOpus(ERR_OPUS_INVALID_CHANNELS) channel_mapping = [ ogg_reader.read(8) for i in range(self.__channels__) ] finally: f.close() except IOError as msg: raise InvalidOpus(str(msg))
class FlacDecoder: CHANNEL_COUNT = [1, 2, 3, 4, 5, 6, 7, 8, 2, 2, 2, None, None, None, None, None] (SUBFRAME_CONSTANT, SUBFRAME_VERBATIM, SUBFRAME_FIXED, SUBFRAME_LPC) = range(4) def __init__(self, filename, channel_mask): self.reader = BitstreamReader(open(filename, "rb"), 0) if (self.reader.read_bytes(4) != 'fLaC'): raise ValueError("invalid FLAC file") self.current_md5sum = md5() #locate the STREAMINFO, #which is sometimes needed to handle non-subset streams for (block_id, block_size, block_reader) in self.metadata_blocks(self.reader): if (block_id == 0): #read STREAMINFO self.minimum_block_size = block_reader.read(16) self.maximum_block_size = block_reader.read(16) self.minimum_frame_size = block_reader.read(24) self.maximum_frame_size = block_reader.read(24) self.sample_rate = block_reader.read(20) self.channels = block_reader.read(3) + 1 self.channel_mask = channel_mask self.bits_per_sample = block_reader.read(5) + 1 self.total_frames = block_reader.read64(36) self.md5sum = block_reader.read_bytes(16) #these are frame header lookup tables #which vary slightly depending on STREAMINFO's values self.BLOCK_SIZE = [self.maximum_block_size, 192, 576, 1152, 2304, 4608, None, None, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768] self.SAMPLE_RATE = [self.sample_rate, 88200, 176400, 192000, 8000, 16000, 22050, 24000, 32000, 44100, 48000, 96000, None, None, None, None] self.BITS_PER_SAMPLE = [self.bits_per_sample, 8, 12, None, 16, 20, 24, None] def metadata_blocks(self, reader): """yields a (block_id, block_size, block_reader) tuple per metadata block where block_reader is a BitstreamReader substream""" (last_block, block_id, block_size) = self.reader.parse("1u 7u 24u") while (last_block == 0): yield (block_id, block_size, self.reader.substream(block_size)) (last_block, block_id, block_size) = self.reader.parse("1u 7u 24u") else: yield (block_id, block_size, self.reader.substream(block_size)) def read(self, pcm_frames): #if the stream is exhausted, #verify its MD5 sum and return an empty pcm.FrameList object if (self.total_frames < 1): if (self.md5sum == self.current_md5sum.digest()): return from_list([], self.channels, self.bits_per_sample, True) else: raise ValueError("MD5 checksum mismatch") crc16 = CRC16() self.reader.add_callback(crc16.update) #fetch the decoding parameters from the frame header (block_size, channel_assignment, bits_per_sample) = self.read_frame_header() channel_count = self.CHANNEL_COUNT[channel_assignment] if (channel_count is None): raise ValueError("invalid channel assignment") #channel data will be a list of signed sample lists, one per channel #such as [[1, 2, 3, ...], [4, 5, 6, ...]] for a 2 channel stream channel_data = [] for channel_number in xrange(channel_count): if ((channel_assignment == 0x8) and (channel_number == 1)): #for left-difference assignment #the difference channel has 1 additional bit channel_data.append(self.read_subframe(block_size, bits_per_sample + 1)) elif ((channel_assignment == 0x9) and (channel_number == 0)): #for difference-right assignment #the difference channel has 1 additional bit channel_data.append(self.read_subframe(block_size, bits_per_sample + 1)) elif ((channel_assignment == 0xA) and (channel_number == 1)): #for mid-side assignment #the side channel has 1 additional bit channel_data.append(self.read_subframe(block_size, bits_per_sample + 1)) else: #otherwise, use the frame's bits-per-sample value channel_data.append(self.read_subframe(block_size, bits_per_sample)) #one all the subframes have been decoded, #reconstruct them depending on the channel assignment if (channel_assignment == 0x8): #left-difference samples = [] for (left, difference) in zip(*channel_data): samples.append(left) samples.append(left - difference) elif (channel_assignment == 0x9): #difference-right samples = [] for (difference, right) in zip(*channel_data): samples.append(difference + right) samples.append(right) elif (channel_assignment == 0xA): #mid-side samples = [] for (mid, side) in zip(*channel_data): samples.append((((mid * 2) + (side % 2)) + side) / 2) samples.append((((mid * 2) + (side % 2)) - side) / 2) else: #independent samples = [0] * block_size * channel_count for (i, channel) in enumerate(channel_data): samples[i::channel_count] = channel self.reader.byte_align() #read and verify the frame's trailing CRC-16 footer self.reader.read(16) self.reader.pop_callback() if (int(crc16) != 0): raise ValueError("CRC16 mismatch in frame footer") #deduct the amount of PCM frames from the remaining amount self.total_frames -= block_size #build a pcm.FrameList object from the combined samples framelist = from_list(samples, channel_count, bits_per_sample, True) #update the running MD5 sum calculation with the frame's data self.current_md5sum.update(framelist.to_bytes(0, 1)) #and finally return the frame data return framelist def read_frame_header(self): crc8 = CRC8() self.reader.add_callback(crc8.update) #read the 32-bit FLAC frame header sync_code = self.reader.read(14) if (sync_code != 0x3FFE): raise ValueError("invalid sync code") self.reader.skip(1) blocking_strategy = self.reader.read(1) block_size_bits = self.reader.read(4) sample_rate_bits = self.reader.read(4) channel_assignment = self.reader.read(4) bits_per_sample_bits = self.reader.read(3) self.reader.skip(1) #the frame number is a UTF-8 encoded value #which takes a variable number of whole bytes frame_number = self.read_utf8() #unpack the 4 bit block size field #which is the total PCM frames in the FLAC frame #and may require up to 16 more bits if the frame is usually-sized #(which typically happens at the end of the stream) if (block_size_bits == 0x6): block_size = self.reader.read(8) + 1 elif (block_size_bits == 0x7): block_size = self.reader.read(16) + 1 else: block_size = self.BLOCK_SIZE[block_size_bits] #unpack the 4 bit sample rate field #which is used for playback, but not needed for decoding #and may require up to 16 more bits #if the stream has a particularly unusual sample rate if (sample_rate_bits == 0xC): sample_rate = self.reader.read(8) * 1000 elif (sample_rate_bits == 0xD): sample_rate = self.reader.read(16) elif (sample_rate_bits == 0xE): sample_rate = self.reader.read(16) * 10 elif (sample_rate_bits == 0xF): raise ValueError("invalid sample rate") else: sample_rate = self.SAMPLE_RATE[sample_rate_bits] #unpack the 3 bit bits-per-sample field #this never requires additional bits if ((bits_per_sample_bits == 0x3) or (bits_per_sample_bits == 0x7)): raise ValueError("invalid bits per sample") else: bits_per_sample = self.BITS_PER_SAMPLE[bits_per_sample_bits] #read and verify frame's CRC-8 value self.reader.read(8) self.reader.pop_callback() if (int(crc8) != 0): raise ValueError("CRC8 mismatch in frame header") return (block_size, channel_assignment, bits_per_sample) def read_subframe_header(self): """returns a tuple of (subframe_type, subframe_order, wasted_bps)""" self.reader.skip(1) subframe_type = self.reader.read(6) if (self.reader.read(1) == 1): wasted_bps = self.reader.unary(1) + 1 else: wasted_bps = 0 #extract "order" value from 6 bit subframe type, if necessary if (subframe_type == 0): return (self.SUBFRAME_CONSTANT, None, wasted_bps) elif (subframe_type == 1): return (self.SUBFRAME_VERBATIM, None, wasted_bps) elif ((subframe_type & 0x38) == 0x08): return (self.SUBFRAME_FIXED, subframe_type & 0x07, wasted_bps) elif ((subframe_type & 0x20) == 0x20): return (self.SUBFRAME_LPC, (subframe_type & 0x1F) + 1, wasted_bps) else: raise ValueError("invalid subframe type") def read_subframe(self, block_size, bits_per_sample): (subframe_type, subframe_order, wasted_bps) = self.read_subframe_header() #read a list of signed sample values #depending on the subframe type, block size, #adjusted bits per sample and optional subframe order if (subframe_type == self.SUBFRAME_CONSTANT): subframe_samples = self.read_constant_subframe( block_size, bits_per_sample - wasted_bps) elif (subframe_type == self.SUBFRAME_VERBATIM): subframe_samples = self.read_verbatim_subframe( block_size, bits_per_sample - wasted_bps) elif (subframe_type == self.SUBFRAME_FIXED): subframe_samples = self.read_fixed_subframe( block_size, bits_per_sample - wasted_bps, subframe_order) else: subframe_samples = self.read_lpc_subframe( block_size, bits_per_sample - wasted_bps, subframe_order) #account for wasted bits-per-sample, if necessary if (wasted_bps): return [sample << wasted_bps for sample in subframe_samples] else: return subframe_samples def read_constant_subframe(self, block_size, bits_per_sample): sample = self.reader.read_signed(bits_per_sample) return [sample] * block_size def read_verbatim_subframe(self, block_size, bits_per_sample): return [self.reader.read_signed(bits_per_sample) for x in xrange(block_size)] def read_fixed_subframe(self, block_size, bits_per_sample, order): #"order" number of warm-up samples samples = [self.reader.read_signed(bits_per_sample) for i in xrange(order)] #"block_size" - "order" number of residual values residuals = self.read_residual(block_size, order) #which are applied to the warm-up samples #depending on the FIXED subframe order #and results in "block_size" number of total samples if (order == 0): return residuals elif (order == 1): for residual in residuals: samples.append( samples[-1] + residual) return samples elif (order == 2): for residual in residuals: samples.append( (2 * samples[-1]) - samples[-2] + residual) return samples elif (order == 3): for residual in residuals: samples.append( (3 * samples[-1]) - (3 * samples[-2]) + samples[-3] + residual) return samples elif (order == 4): for residual in residuals: samples.append( (4 * samples[-1]) - (6 * samples[-2]) + (4 * samples[-3]) - samples[-4] + residual) return samples else: raise ValueError("unsupported FIXED subframe order") def read_lpc_subframe(self, block_size, bits_per_sample, order): #"order" number of warm-up samples samples = [self.reader.read_signed(bits_per_sample) for i in xrange(order)] #the size of each QLP coefficient, in bits qlp_precision = self.reader.read(4) #the amount of right shift to apply #during LPC calculation #(though this is a signed value, negative shifts are noops # in the reference FLAC decoder) qlp_shift_needed = max(self.reader.read_signed(5), 0) #"order" number of signed QLP coefficients qlp_coeffs = [self.reader.read_signed(qlp_precision + 1) for i in xrange(order)] #QLP coefficients are applied in reverse order qlp_coeffs.reverse() #"block_size" - "order" number of residual values residuals = self.read_residual(block_size, order) #which are applied to the running LPC calculation for residual in residuals: samples.append((sum([coeff * sample for (coeff, sample) in zip(qlp_coeffs, samples[-order:])]) >> qlp_shift_needed) + residual) return samples def read_residual(self, block_size, order): residuals = [] coding_method = self.reader.read(2) partition_order = self.reader.read(4) #each parititon contains block_size / 2 ** partition_order #number of residuals for partition_number in xrange(2 ** partition_order): if (partition_number == 0): #except for the first partition #which contains "order" less than the rest residuals.extend( self.read_residual_partition( coding_method, (block_size / 2 ** partition_order) - order)) else: residuals.extend( self.read_residual_partition( coding_method, block_size / 2 ** partition_order)) return residuals def read_residual_partition(self, coding_method, residual_count): if (coding_method == 0): #the Rice parameters determines the number of #least-significant bits to read for each residual rice_parameter = self.reader.read(4) if (rice_parameter == 0xF): escape_code = self.reader.read(5) return [self.reader.read_signed(escape_code) for i in xrange(residual_count)] elif (coding_method == 1): #24 bps streams may use a 5-bit Rice parameter #for better compression rice_parameter = self.reader.read(5) if (rice_parameter == 0x1F): escape_code = self.reader.read(5) return [self.reader.read_signed(escape_code) for i in xrange(residual_count)] else: raise ValueError("invalid Rice coding parameter") #a list of signed residual values partition_residuals = [] for i in xrange(residual_count): msb = self.reader.unary(1) # most-significant bits lsb = self.reader.read(rice_parameter) # least-significant bits value = (msb << rice_parameter) | lsb # combined into a value if (value & 1): # whose least-significant bit is the sign value partition_residuals.append(-(value >> 1) - 1) else: partition_residuals.append(value >> 1) return partition_residuals def read_utf8(self): total_bytes = self.reader.unary(0) value = self.reader.read(7 - total_bytes) while (total_bytes > 1): value = ((value << 6) | self.reader.parse("2p 6u")[0]) total_bytes -= 1 return value def close(self): self.reader.close()
def read_metadata(self): command = self.unsigned(2) if (command == 9): # got verbatim, so read data verbatim_bytes = "".join([chr(self.unsigned(8) & 0xFF) for i in range(self.unsigned(5))]) try: wave = BitstreamReader(cStringIO.StringIO(verbatim_bytes), 1) header = wave.read_bytes(12) if (header.startswith("RIFF") and header.endswith("WAVE")): # got RIFF/WAVE header, so parse wave blocks as needed total_size = len(verbatim_bytes) - 12 while (total_size > 0): (chunk_id, chunk_size) = wave.parse("4b 32u") total_size -= 8 if (chunk_id == 'fmt '): (channels, self.sample_rate, bits_per_sample, channel_mask) = parse_fmt( wave.substream(chunk_size)) self.channel_mask = int(channel_mask) return else: if (chunk_size % 2): wave.read_bytes(chunk_size + 1) total_size -= (chunk_size + 1) else: wave.read_bytes(chunk_size) total_size -= chunk_size else: # no fmt chunk, so use default metadata pass except (IOError, ValueError): pass try: aiff = BitstreamReader(cStringIO.StringIO(verbatim_bytes), 0) header = aiff.read_bytes(12) if (header.startswith("FORM") and header.endswith("AIFF")): # got FORM/AIFF header, so parse aiff blocks as needed total_size = len(verbatim_bytes) - 12 while (total_size > 0): (chunk_id, chunk_size) = aiff.parse("4b 32u") total_size -= 8 if (chunk_id == 'COMM'): (channels, total_sample_frames, bits_per_sample, self.sample_rate, channel_mask) = parse_comm( aiff.substream(chunk_size)) self.channel_mask = int(channel_mask) return else: if (chunk_size % 2): aiff.read_bytes(chunk_size + 1) total_size -= (chunk_size + 1) else: aiff.read_bytes(chunk_size) total_size -= chunk_size else: # no COMM chunk, so use default metadata pass except IOError: pass # got something else, so invent some PCM parameters self.sample_rate = 44100 if (self.channels == 1): self.channel_mask = 0x4 elif (self.channels == 2): self.channel_mask = 0x3 else: self.channel_mask = 0
def __init__(self, filename): """filename is a plain string""" AudioFile.__init__(self, filename) from audiotools.bitstream import parse try: mp3file = open(filename, "rb") except IOError as msg: raise InvalidMP3(str(msg)) try: try: header_bytes = MP3Audio.__find_next_mp3_frame__(mp3file) except IOError: from audiotools.text import ERR_MP3_FRAME_NOT_FOUND raise InvalidMP3(ERR_MP3_FRAME_NOT_FOUND) (frame_sync, mpeg_id, layer, bit_rate, sample_rate, pad, channels) = parse("11u 2u 2u 1p 4u 2u 1u 1p 2u 6p", False, mp3file.read(4)) self.__samplerate__ = self.SAMPLE_RATE[mpeg_id][sample_rate] if self.__samplerate__ is None: from audiotools.text import ERR_MP3_INVALID_SAMPLE_RATE raise InvalidMP3(ERR_MP3_INVALID_SAMPLE_RATE) if channels in (0, 1, 2): self.__channels__ = 2 else: self.__channels__ = 1 first_frame = mp3file.read(self.frame_length(mpeg_id, layer, bit_rate, sample_rate, pad) - 4) if ((b"Xing" in first_frame) and (len(first_frame[first_frame.index(b"Xing"): first_frame.index(b"Xing") + 160]) == 160)): # pull length from Xing header, if present self.__pcm_frames__ = ( parse("32p 32p 32u 32p 832p", 0, first_frame[first_frame.index(b"Xing"): first_frame.index(b"Xing") + 160])[0] * self.PCM_FRAMES_PER_MPEG_FRAME[layer]) else: # otherwise, bounce through file frames from audiotools.bitstream import BitstreamReader reader = BitstreamReader(mp3file, False) self.__pcm_frames__ = 0 try: (frame_sync, mpeg_id, layer, bit_rate, sample_rate, pad) = reader.parse("11u 2u 2u 1p 4u 2u 1u 9p") while frame_sync == 0x7FF: self.__pcm_frames__ += \ self.PCM_FRAMES_PER_MPEG_FRAME[layer] reader.skip_bytes(self.frame_length(mpeg_id, layer, bit_rate, sample_rate, pad) - 4) (frame_sync, mpeg_id, layer, bit_rate, sample_rate, pad) = reader.parse("11u 2u 2u 1p 4u 2u 1u 9p") except IOError: pass except ValueError as err: raise InvalidMP3(err) finally: mp3file.close()
def __titles__(self, titleset): """returns a list of DVDATitle objects for the given titleset""" # this requires bouncing all over the ATS_XX_0.IFO file import os from audiotools.bitstream import BitstreamReader try: f = open(self.files['ATS_%2.2d_0.IFO' % (titleset)], 'rb') except (KeyError, IOError): from audiotools.text import ERR_DVDA_IOERROR_ATS raise InvalidDVDA(ERR_DVDA_IOERROR_ATS % (titleset)) try: # ensure the file's identifier is correct # which is all we care about from the first sector if (f.read(12) != 'DVDAUDIO-ATS'): from audiotools.text import ERR_DVDA_INVALID_ATS raise InvalidDVDA(ERR_DVDA_INVALID_ATS % (titleset)) # seek to the second sector and read the title count # and list of title table offset values f.seek(DVDAudio.SECTOR_SIZE, os.SEEK_SET) ats_reader = BitstreamReader(f, 0) (title_count, last_byte_address) = ats_reader.parse("16u 16p 32u") title_offsets = [ ats_reader.parse("8u 24p 32u")[1] for title in range(title_count) ] titles = [] for (title_number, title_offset) in enumerate(title_offsets): # for each title, seek to its title table # and read the title's values and its track timestamps f.seek(DVDAudio.SECTOR_SIZE + title_offset, os.SEEK_SET) ats_reader = BitstreamReader(f, 0) (tracks, indexes, track_length, sector_pointers_table ) = ats_reader.parse("16p 8u 8u 32u 4P 16u 2P") timestamps = [ ats_reader.parse("32p 8u 8p 32u 32u 48p") for track in range(tracks) ] # seek to the title's sector pointers table # and read the first and last sector values for title's tracks f.seek( DVDAudio.SECTOR_SIZE + title_offset + sector_pointers_table, os.SEEK_SET) ats_reader = BitstreamReader(f, 0) sector_pointers = [ ats_reader.parse("32u 32u 32u") for i in range(indexes) ] if ((len(sector_pointers) > 1) and ({p[0] for p in sector_pointers[1:]} != {0x01000000})): from audiotools.text import ERR_DVDA_INVALID_SECTOR_POINTER raise InvalidDVDA(ERR_DVDA_INVALID_SECTOR_POINTER) else: sector_pointers = [None] + sector_pointers # build a preliminary DVDATitle object # which we'll populate with track data dvda_title = DVDATitle(dvdaudio=self, titleset=titleset, title=title_number + 1, pts_length=track_length, tracks=[]) # for each track, determine its first and last sector # based on the sector pointers between the track's # initial index and the next track's initial index for (track_number, (timestamp, next_timestamp)) in enumerate( zip(timestamps, timestamps[1:])): (index_number, first_pts, pts_length) = timestamp next_timestamp_index = next_timestamp[0] dvda_title.tracks.append( DVDATrack( dvdaudio=self, titleset=titleset, title=dvda_title, track=track_number + 1, first_pts=first_pts, pts_length=pts_length, first_sector=sector_pointers[index_number][1], last_sector=sector_pointers[next_timestamp_index - 1][2])) # for the last track, its sector pointers # simply consume what remains on the list (index_number, first_pts, pts_length) = timestamps[-1] dvda_title.tracks.append( DVDATrack(dvdaudio=self, titleset=titleset, title=dvda_title, track=len(timestamps), first_pts=first_pts, pts_length=pts_length, first_sector=sector_pointers[index_number][1], last_sector=sector_pointers[-1][2])) # fill in the title's info such as sample_rate, channels, etc. dvda_title.__parse_info__() titles.append(dvda_title) return titles finally: f.close()
def __init__(self, filename): """filename is a plain string""" from audiotools.bitstream import BitstreamReader from audiotools import ChannelMask from io import BytesIO WaveContainer.__init__(self, filename) try: f = open(filename, 'rb') except IOError as msg: raise InvalidShorten(str(msg)) reader = BitstreamReader(f, 0) try: if (reader.parse("4b 8u") != ["ajkg", 2]): raise InvalidShorten("invalid Shorten header") except IOError: raise InvalidShorten("invalid Shorten header") def read_unsigned(r, c): MSB = r.unary(1) LSB = r.read(c) return MSB * 2 ** c + LSB def read_long(r): return read_unsigned(r, read_unsigned(r, 2)) # populate channels and bits_per_sample from Shorten header (file_type, self.__channels__, block_length, max_LPC, number_of_means, bytes_to_skip) = [read_long(reader) for i in range(6)] if ((1 <= file_type) and (file_type <= 2)): self.__bits_per_sample__ = 8 elif ((3 <= file_type) and (file_type <= 6)): self.__bits_per_sample__ = 16 else: # FIXME raise InvalidShorten("unsupported Shorten file type") # setup some default dummy metadata self.__sample_rate__ = 44100 if (self.__channels__ == 1): self.__channel_mask__ = ChannelMask(0x4) elif (self.__channels__ == 2): self.__channel_mask__ = ChannelMask(0x3) else: self.__channel_mask__ = ChannelMask(0) self.__total_frames__ = 0 # populate sample_rate and total_frames from first VERBATIM command command = read_unsigned(reader, 2) if (command == 9): verbatim_bytes = "".join([chr(read_unsigned(reader, 8) & 0xFF) for i in range(read_unsigned(reader, 5))]) try: wave = BitstreamReader(BytesIO(verbatim_bytes), 1) header = wave.read_bytes(12) if (header.startswith("RIFF") and header.endswith("WAVE")): # got RIFF/WAVE header, so parse wave blocks as needed total_size = len(verbatim_bytes) - 12 while (total_size >= 8): (chunk_id, chunk_size) = wave.parse("4b 32u") total_size -= 8 if (chunk_id == 'fmt '): from audiotools.wav import parse_fmt (channels, self.__sample_rate__, bits_per_sample, self.__channel_mask__) = parse_fmt( wave.substream(chunk_size)) elif (chunk_id == 'data'): self.__total_frames__ = \ (chunk_size // (self.__channels__ * (self.__bits_per_sample__ // 8))) else: if (chunk_size % 2): wave.read_bytes(chunk_size + 1) total_size -= (chunk_size + 1) else: wave.read_bytes(chunk_size) total_size -= chunk_size except (IOError, ValueError): pass try: aiff = BitstreamReader(BytesIO(verbatim_bytes), 0) header = aiff.read_bytes(12) if (header.startswith("FORM") and header.endswith("AIFF")): # got FORM/AIFF header, so parse aiff blocks as needed total_size = len(verbatim_bytes) - 12 while (total_size >= 8): (chunk_id, chunk_size) = aiff.parse("4b 32u") total_size -= 8 if (chunk_id == 'COMM'): from audiotools.aiff import parse_comm (channels, total_sample_frames, bits_per_sample, self.__sample_rate__, self.__channel_mask__) = parse_comm( aiff.substream(chunk_size)) elif (chunk_id == 'SSND'): # subtract 8 bytes for "offset" and "block size" self.__total_frames__ = \ ((chunk_size - 8) // (self.__channels__ * (self.__bits_per_sample__ // 8))) else: if (chunk_size % 2): aiff.read_bytes(chunk_size + 1) total_size -= (chunk_size + 1) else: aiff.read_bytes(chunk_size) total_size -= chunk_size except IOError: pass
class AiffReader(object): """a PCMReader object for reading AIFF file contents""" def __init__(self, aiff_filename): """aiff_filename is a string""" from audiotools.bitstream import BitstreamReader self.stream = BitstreamReader(open(aiff_filename, "rb"), False) # ensure FORM<size>AIFF header is ok try: (form, total_size, aiff) = self.stream.parse("4b 32u 4b") except struct.error: from audiotools.text import ERR_AIFF_INVALID_AIFF raise InvalidAIFF(ERR_AIFF_INVALID_AIFF) if form != b'FORM': from audiotools.text import ERR_AIFF_NOT_AIFF raise ValueError(ERR_AIFF_NOT_AIFF) elif aiff != b'AIFF': from audiotools.text import ERR_AIFF_INVALID_AIFF raise ValueError(ERR_AIFF_INVALID_AIFF) else: total_size -= 4 comm_chunk_read = False # walk through chunks until "SSND" chunk encountered while total_size > 0: try: (chunk_id, chunk_size) = self.stream.parse("4b 32u") except struct.error: from audiotools.text import ERR_AIFF_INVALID_AIFF raise ValueError(ERR_AIFF_INVALID_AIFF) if not frozenset(chunk_id).issubset(AiffAudio.PRINTABLE_ASCII): from audiotools.text import ERR_AIFF_INVALID_CHUNK_ID raise ValueError(ERR_AIFF_INVALID_CHUNK_ID) else: total_size -= 8 if chunk_id == b"COMM": # when "COMM" chunk encountered, # use it to populate PCMReader attributes (self.channels, self.total_pcm_frames, self.bits_per_sample, self.sample_rate, channel_mask) = parse_comm(self.stream) self.channel_mask = int(channel_mask) self.bytes_per_pcm_frame = ((self.bits_per_sample // 8) * self.channels) self.remaining_pcm_frames = self.total_pcm_frames comm_chunk_read = True elif chunk_id == b"SSND": # when "SSND" chunk encountered, # strip off the "offset" and "block_size" attributes # and ready PCMReader for reading if not comm_chunk_read: from audiotools.text import ERR_AIFF_PREMATURE_SSND_CHUNK raise ValueError(ERR_AIFF_PREMATURE_SSND_CHUNK) else: self.stream.skip_bytes(8) self.ssnd_start = self.stream.getpos() return else: # all other chunks are ignored self.stream.skip_bytes(chunk_size) if chunk_size % 2: if len(self.stream.read_bytes(1)) < 1: from audiotools.text import ERR_AIFF_INVALID_CHUNK raise ValueError(ERR_AIFF_INVALID_CHUNK) total_size -= (chunk_size + 1) else: total_size -= chunk_size else: # raise an error if no "SSND" chunk is encountered from audiotools.text import ERR_AIFF_NO_SSND_CHUNK raise ValueError(ERR_AIFF_NO_SSND_CHUNK) def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.close() def read(self, pcm_frames): """try to read a pcm.FrameList with the given number of PCM frames""" # try to read requested PCM frames or remaining frames requested_pcm_frames = min(max(pcm_frames, 1), self.remaining_pcm_frames) requested_bytes = (self.bytes_per_pcm_frame * requested_pcm_frames) pcm_data = self.stream.read_bytes(requested_bytes) # raise exception if "SSND" chunk exhausted early if len(pcm_data) < requested_bytes: from audiotools.text import ERR_AIFF_TRUNCATED_SSND_CHUNK raise IOError(ERR_AIFF_TRUNCATED_SSND_CHUNK) else: self.remaining_pcm_frames -= requested_pcm_frames # return parsed chunk return FrameList(pcm_data, self.channels, self.bits_per_sample, True, True) def read_closed(self, pcm_frames): raise ValueError("cannot read closed stream") def seek(self, pcm_frame_offset): """tries to seek to the given PCM frame offset returns the total amount of frames actually seeked over""" if pcm_frame_offset < 0: from audiotools.text import ERR_NEGATIVE_SEEK raise ValueError(ERR_NEGATIVE_SEEK) # ensure one doesn't walk off the end of the file pcm_frame_offset = min(pcm_frame_offset, self.total_pcm_frames) # position file in "SSND" chunk self.stream.setpos(self.ssnd_start) self.stream.seek((pcm_frame_offset * self.bytes_per_pcm_frame), 1) self.remaining_pcm_frames = (self.total_pcm_frames - pcm_frame_offset) return pcm_frame_offset def seek_closed(self, pcm_frame_offset): raise ValueError("cannot seek closed stream") def close(self): """closes the stream for reading""" self.stream.close() self.read = self.read_closed self.seek = self.seek_closed
def read_metadata(self): from io import BytesIO command = self.unsigned(2) if command == 9: # got verbatim, so read data verbatim_bytes = ints_to_bytes([self.unsigned(8) & 0xFF for i in range(self.unsigned(5))]) try: wave = BitstreamReader(BytesIO(verbatim_bytes), True) header = wave.read_bytes(12) if header.startswith(b"RIFF") and header.endswith(b"WAVE"): # got RIFF/WAVE header, so parse wave blocks as needed total_size = len(verbatim_bytes) - 12 while total_size > 0: (chunk_id, chunk_size) = wave.parse("4b 32u") total_size -= 8 if chunk_id == b'fmt ': (channels, self.sample_rate, bits_per_sample, channel_mask) = parse_fmt( wave.substream(chunk_size)) self.channel_mask = int(channel_mask) return else: if chunk_size % 2: wave.read_bytes(chunk_size + 1) total_size -= (chunk_size + 1) else: wave.read_bytes(chunk_size) total_size -= chunk_size else: # no fmt chunk, so use default metadata pass except (IOError, ValueError): pass try: aiff = BitstreamReader(BytesIO(verbatim_bytes), False) header = aiff.read_bytes(12) if header.startswith(b"FORM") and header.endswith(b"AIFF"): # got FORM/AIFF header, so parse aiff blocks as needed total_size = len(verbatim_bytes) - 12 while total_size > 0: (chunk_id, chunk_size) = aiff.parse("4b 32u") total_size -= 8 if chunk_id == b'COMM': (channels, total_sample_frames, bits_per_sample, self.sample_rate, channel_mask) = parse_comm( aiff.substream(chunk_size)) self.channel_mask = int(channel_mask) return else: if chunk_size % 2: aiff.read_bytes(chunk_size + 1) total_size -= (chunk_size + 1) else: aiff.read_bytes(chunk_size) total_size -= chunk_size else: # no COMM chunk, so use default metadata pass except IOError: pass # got something else, so invent some PCM parameters self.sample_rate = 44100 if self.channels == 1: self.channel_mask = 0x4 elif self.channels == 2: self.channel_mask = 0x3 else: self.channel_mask = 0
def aiff_header_footer(self): """returns (header, footer) tuple of strings containing all data before and after the PCM stream if self.has_foreign_aiff_chunks() is False, may raise ValueError if the file has no header and footer for any reason""" from audiotools.bitstream import BitstreamReader from audiotools.bitstream import BitstreamRecorder from audiotools.text import (ERR_AIFF_NOT_AIFF, ERR_AIFF_INVALID_AIFF, ERR_AIFF_INVALID_CHUNK_ID) head = BitstreamRecorder(0) tail = BitstreamRecorder(0) current_block = head aiff_file = BitstreamReader(open(self.filename, 'rb'), 0) try: # transfer the 12-byte "RIFFsizeWAVE" header to head (form, size, aiff) = aiff_file.parse("4b 32u 4b") if (form != 'FORM'): raise InvalidAIFF(ERR_AIFF_NOT_AIFF) elif (aiff != 'AIFF'): raise InvalidAIFF(ERR_AIFF_INVALID_AIFF) else: current_block.build("4b 32u 4b", (form, size, aiff)) total_size = size - 4 while (total_size > 0): # transfer each chunk header (chunk_id, chunk_size) = aiff_file.parse("4b 32u") if (not frozenset(chunk_id).issubset(self.PRINTABLE_ASCII)): raise InvalidAIFF(ERR_AIFF_INVALID_CHUNK_ID) else: current_block.build("4b 32u", (chunk_id, chunk_size)) total_size -= 8 # and transfer the full content of non-audio chunks if (chunk_id != "SSND"): if (chunk_size % 2): current_block.write_bytes( aiff_file.read_bytes(chunk_size + 1)) total_size -= (chunk_size + 1) else: current_block.write_bytes( aiff_file.read_bytes(chunk_size)) total_size -= chunk_size else: # transfer alignment as part of SSND's chunk header align = aiff_file.parse("32u 32u") current_block.build("32u 32u", align) aiff_file.skip_bytes(chunk_size - 8) current_block = tail if (chunk_size % 2): current_block.write_bytes(aiff_file.read_bytes(1)) total_size -= (chunk_size + 1) else: total_size -= chunk_size return (head.data(), tail.data()) finally: aiff_file.close()
def __init__(self, filename): """filename is a plain string""" from audiotools.bitstream import BitstreamReader from audiotools import ChannelMask from io import BytesIO WaveContainer.__init__(self, filename) try: f = open(filename, 'rb') except IOError as msg: raise InvalidShorten(str(msg)) reader = BitstreamReader(f, 0) try: if (reader.parse("4b 8u") != ["ajkg", 2]): raise InvalidShorten("invalid Shorten header") except IOError: raise InvalidShorten("invalid Shorten header") def read_unsigned(r, c): MSB = r.unary(1) LSB = r.read(c) return MSB * 2**c + LSB def read_long(r): return read_unsigned(r, read_unsigned(r, 2)) # populate channels and bits_per_sample from Shorten header (file_type, self.__channels__, block_length, max_LPC, number_of_means, bytes_to_skip) = [read_long(reader) for i in range(6)] if ((1 <= file_type) and (file_type <= 2)): self.__bits_per_sample__ = 8 elif ((3 <= file_type) and (file_type <= 6)): self.__bits_per_sample__ = 16 else: # FIXME raise InvalidShorten("unsupported Shorten file type") # setup some default dummy metadata self.__sample_rate__ = 44100 if (self.__channels__ == 1): self.__channel_mask__ = ChannelMask(0x4) elif (self.__channels__ == 2): self.__channel_mask__ = ChannelMask(0x3) else: self.__channel_mask__ = ChannelMask(0) self.__total_frames__ = 0 # populate sample_rate and total_frames from first VERBATIM command command = read_unsigned(reader, 2) if (command == 9): verbatim_bytes = "".join([ chr(read_unsigned(reader, 8) & 0xFF) for i in range(read_unsigned(reader, 5)) ]) try: wave = BitstreamReader(BytesIO(verbatim_bytes), 1) header = wave.read_bytes(12) if (header.startswith("RIFF") and header.endswith("WAVE")): # got RIFF/WAVE header, so parse wave blocks as needed total_size = len(verbatim_bytes) - 12 while (total_size >= 8): (chunk_id, chunk_size) = wave.parse("4b 32u") total_size -= 8 if (chunk_id == 'fmt '): from audiotools.wav import parse_fmt (channels, self.__sample_rate__, bits_per_sample, self.__channel_mask__) = parse_fmt( wave.substream(chunk_size)) elif (chunk_id == 'data'): self.__total_frames__ = \ (chunk_size // (self.__channels__ * (self.__bits_per_sample__ // 8))) else: if (chunk_size % 2): wave.read_bytes(chunk_size + 1) total_size -= (chunk_size + 1) else: wave.read_bytes(chunk_size) total_size -= chunk_size except (IOError, ValueError): pass try: aiff = BitstreamReader(BytesIO(verbatim_bytes), 0) header = aiff.read_bytes(12) if (header.startswith("FORM") and header.endswith("AIFF")): # got FORM/AIFF header, so parse aiff blocks as needed total_size = len(verbatim_bytes) - 12 while (total_size >= 8): (chunk_id, chunk_size) = aiff.parse("4b 32u") total_size -= 8 if (chunk_id == 'COMM'): from audiotools.aiff import parse_comm (channels, total_sample_frames, bits_per_sample, self.__sample_rate__, self.__channel_mask__) = parse_comm( aiff.substream(chunk_size)) elif (chunk_id == 'SSND'): # subtract 8 bytes for "offset" and "block size" self.__total_frames__ = \ ((chunk_size - 8) // (self.__channels__ * (self.__bits_per_sample__ // 8))) else: if (chunk_size % 2): aiff.read_bytes(chunk_size + 1) total_size -= (chunk_size + 1) else: aiff.read_bytes(chunk_size) total_size -= chunk_size except IOError: pass
class TTADecoder: def __init__(self, filename): self.reader = BitstreamReader(open(filename, "rb"), True) crc = CRC32() self.reader.add_callback(crc.update) #read the header (signature, format_, self.channels, self.bits_per_sample, self.sample_rate, self.total_pcm_frames) = self.reader.parse( "4b 16u 16u 16u 32u 32u") self.reader.pop_callback() header_crc = self.reader.read(32) if (int(crc) != header_crc): raise ValueError( "CRC32 mismatch in header (0x%8.8X != 0x%8.8X)" % (header_crc, int(crc))) self.channel_mask = {1:0x4, 2:0x3}.get(self.channels, 0) total_tta_frames = div_ceil(self.total_pcm_frames * 245, self.sample_rate * 256) self.pcm_frames_per_tta_frame = (self.sample_rate * 256) / 245 #read the seektable crc = CRC32() self.reader.add_callback(crc.update) self.frame_sizes = [self.reader.read(32) for i in xrange(total_tta_frames)] self.reader.pop_callback() seektable_crc = self.reader.read(32) if (int(crc) != seektable_crc): raise ValueError( "CRC32 mismatch in seektable (0x%8.8X != 0x%8.8X)" % (header_crc, int(crc))) self.current_tta_frame = 0 def read(self, pcm_frames): if (self.total_pcm_frames == 0): return FrameList("", self.channels, self.bits_per_sample, True, True) pcm_frames = min(self.pcm_frames_per_tta_frame, self.total_pcm_frames) frame_reader = self.reader.substream( self.frame_sizes[self.current_tta_frame]) crc = CRC32() frame_reader.add_callback(crc.update) self.total_pcm_frames -= pcm_frames self.current_tta_frame += 1 #setup Rice parameters for each channel k0 = [10] * self.channels k1 = [10] * self.channels sum0 = [2 ** 14] * self.channels sum1 = [2 ** 14] * self.channels #list of unfiltered output for each channel unfiltered = [[] for i in xrange(self.channels)] for f in xrange(pcm_frames): correlated = [] for (c, ch_output) in enumerate(unfiltered): #read most-significant bits MSB = frame_reader.unary(0) if (MSB == 0): #read least-significant bits unsigned = frame_reader.read(k0[c]) else: #read least-significant bits LSB = frame_reader.read(k1[c]) unshifted = ((MSB - 1) << k1[c]) + LSB unsigned = unshifted + (1 << k0[c]) #adjust sum1 and k1 sum1[c] += (unshifted - (sum1[c] >> 4)) if (sum1[c] < (2 ** (k1[c] + 4))): k1[c] = max(k1[c] - 1, 0) elif (sum1[c] > (2 ** (k1[c] + 5))): k1[c] += 1 #adjust sum0 and k0 sum0[c] += (unsigned - (sum0[c] >> 4)) if (sum0[c] < (2 ** (k0[c] + 4))): k0[c] = max(k0[c] - 1, 0) elif (sum0[c] > (2 ** (k0[c] + 5))): k0[c] += 1 #apply sign bit if ((unsigned % 2) == 1): #positive ch_output.append((unsigned + 1) / 2) else: #negative ch_output.append(-(unsigned / 2)) #check frame's trailing CRC32 now that reading is finished frame_reader.byte_align() frame_reader.pop_callback() frame_crc = frame_reader.read(32) if (int(crc) != frame_crc): raise ValueError("CRC32 mismatch in frame (0x%8.8X != 0x%8.8X)" % (frame_crc, int(crc))) #run hybrid filter on each channel filtered = [] for unfiltered_ch in unfiltered: filtered.append( tta_filter(self.bits_per_sample, unfiltered_ch)) #run fixed order prediction on each channel predicted = [] for filtered_ch in filtered: predicted.append( fixed_predictor(self.bits_per_sample, filtered_ch)) if (self.channels == 1): #send channel as-is return from_list(predicted[0], 1, self.bits_per_sample, True) else: #decorrelate channels decorrelated = decorrelate(predicted) #return all channels as single FrameList return from_channels([from_list(decorrelated_ch, 1, self.bits_per_sample, True) for decorrelated_ch in decorrelated]) def close(self): #FIXME pass
def wave_header_footer(self): """returns a pair of data strings before and after PCM data the first contains all data before the PCM content of the data chunk the second containing all data after the data chunk for example: >>> w = audiotools.open("input.wav") >>> (head, tail) = w.wave_header_footer() >>> f = open("output.wav", "wb") >>> f.write(head) >>> audiotools.transfer_framelist_data(w.to_pcm(), f.write) >>> f.write(tail) >>> f.close() should result in "output.wav" being identical to "input.wav" """ from audiotools.bitstream import BitstreamReader from audiotools.bitstream import BitstreamRecorder head = BitstreamRecorder(1) tail = BitstreamRecorder(1) current_block = head fmt_found = False wave_file = BitstreamReader(open(self.filename, 'rb'), 1) try: # transfer the 12-byte "RIFFsizeWAVE" header to head (riff, size, wave) = wave_file.parse("4b 32u 4b") if (riff != 'RIFF'): from audiotools.text import ERR_WAV_NOT_WAVE raise ValueError(ERR_WAV_NOT_WAVE) elif (wave != 'WAVE'): from audiotools.text import ERR_WAV_INVALID_WAVE raise ValueError(ERR_WAV_INVALID_WAVE) else: current_block.build("4b 32u 4b", (riff, size, wave)) total_size = size - 4 while (total_size > 0): # transfer each chunk header (chunk_id, chunk_size) = wave_file.parse("4b 32u") if (not frozenset(chunk_id).issubset(self.PRINTABLE_ASCII)): from audiotools.text import ERR_WAV_INVALID_CHUNK raise ValueError(ERR_WAV_INVALID_CHUNK) else: current_block.build("4b 32u", (chunk_id, chunk_size)) total_size -= 8 # and transfer the full content of non-audio chunks if (chunk_id != "data"): if (chunk_id == "fmt "): if (not fmt_found): fmt_found = True else: from audiotools.text import ERR_WAV_MULTIPLE_FMT raise ValueError(ERR_WAV_MULTIPLE_FMT) if (chunk_size % 2): current_block.write_bytes( wave_file.read_bytes(chunk_size + 1)) total_size -= (chunk_size + 1) else: current_block.write_bytes( wave_file.read_bytes(chunk_size)) total_size -= chunk_size else: wave_file.skip_bytes(chunk_size) current_block = tail if (chunk_size % 2): current_block.write_bytes(wave_file.read_bytes(1)) total_size -= (chunk_size + 1) else: total_size -= chunk_size if (fmt_found): return (head.data(), tail.data()) else: from audiotools.text import ERR_WAV_NO_FMT_CHUNK return ValueError(ERR_WAV_NO_FMT_CHUNK) finally: wave_file.close()
class FlacDecoder(object): CHANNEL_COUNT = [ 1, 2, 3, 4, 5, 6, 7, 8, 2, 2, 2, None, None, None, None, None ] (SUBFRAME_CONSTANT, SUBFRAME_VERBATIM, SUBFRAME_FIXED, SUBFRAME_LPC) = range(4) def __init__(self, filename, channel_mask): self.reader = BitstreamReader(open(filename, "rb"), False) if (self.reader.read_bytes(4) != b'fLaC'): raise ValueError("invalid FLAC file") self.current_md5sum = md5() # locate the STREAMINFO, # which is sometimes needed to handle non-subset streams for (block_id, block_size, block_reader) in self.metadata_blocks(self.reader): if (block_id == 0): # read STREAMINFO self.minimum_block_size = block_reader.read(16) self.maximum_block_size = block_reader.read(16) self.minimum_frame_size = block_reader.read(24) self.maximum_frame_size = block_reader.read(24) self.sample_rate = block_reader.read(20) self.channels = block_reader.read(3) + 1 self.channel_mask = channel_mask self.bits_per_sample = block_reader.read(5) + 1 self.total_frames = block_reader.read(36) self.md5sum = block_reader.read_bytes(16) # these are frame header lookup tables # which vary slightly depending on STREAMINFO's values self.BLOCK_SIZE = [ self.maximum_block_size, 192, 576, 1152, 2304, 4608, None, None, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768 ] self.SAMPLE_RATE = [ self.sample_rate, 88200, 176400, 192000, 8000, 16000, 22050, 24000, 32000, 44100, 48000, 96000, None, None, None, None ] self.BITS_PER_SAMPLE = [ self.bits_per_sample, 8, 12, None, 16, 20, 24, None ] def metadata_blocks(self, reader): """yields a (block_id, block_size, block_reader) tuple per metadata block where block_reader is a BitstreamReader substream""" (last_block, block_id, block_size) = self.reader.parse("1u 7u 24u") while (last_block == 0): yield (block_id, block_size, self.reader.substream(block_size)) (last_block, block_id, block_size) = self.reader.parse("1u 7u 24u") else: yield (block_id, block_size, self.reader.substream(block_size)) def read(self, pcm_frames): # if the stream is exhausted, # verify its MD5 sum and return an empty pcm.FrameList object if (self.total_frames < 1): if (self.md5sum == self.current_md5sum.digest()): return empty_framelist(self.channels, self.bits_per_sample) else: raise ValueError("MD5 checksum mismatch") crc16 = CRC16() self.reader.add_callback(crc16.update) # fetch the decoding parameters from the frame header (block_size, channel_assignment, bits_per_sample) = self.read_frame_header() channel_count = self.CHANNEL_COUNT[channel_assignment] if (channel_count is None): raise ValueError("invalid channel assignment") # channel data will be a list of signed sample lists, one per channel # such as [[1, 2, 3, ...], [4, 5, 6, ...]] for a 2 channel stream channel_data = [] for channel_number in range(channel_count): if ((channel_assignment == 0x8) and (channel_number == 1)): # for left-difference assignment # the difference channel has 1 additional bit channel_data.append( self.read_subframe(block_size, bits_per_sample + 1)) elif ((channel_assignment == 0x9) and (channel_number == 0)): # for difference-right assignment # the difference channel has 1 additional bit channel_data.append( self.read_subframe(block_size, bits_per_sample + 1)) elif ((channel_assignment == 0xA) and (channel_number == 1)): # for average-difference assignment # the difference channel has 1 additional bit channel_data.append( self.read_subframe(block_size, bits_per_sample + 1)) else: # otherwise, use the frame's bits-per-sample value channel_data.append( self.read_subframe(block_size, bits_per_sample)) # one all the subframes have been decoded, # reconstruct them depending on the channel assignment if (channel_assignment == 0x8): # left-difference samples = [] for (left, difference) in zip(*channel_data): samples.append(left) samples.append(left - difference) elif (channel_assignment == 0x9): # difference-right samples = [] for (difference, right) in zip(*channel_data): samples.append(difference + right) samples.append(right) elif (channel_assignment == 0xA): # mid-side samples = [] for (mid, side) in zip(*channel_data): samples.append((((mid * 2) + (side % 2)) + side) // 2) samples.append((((mid * 2) + (side % 2)) - side) // 2) else: # independent samples = [0] * block_size * channel_count for (i, channel) in enumerate(channel_data): samples[i::channel_count] = channel self.reader.byte_align() # read and verify the frame's trailing CRC-16 footer self.reader.read(16) self.reader.pop_callback() if (int(crc16) != 0): raise ValueError("CRC16 mismatch in frame footer") # deduct the amount of PCM frames from the remaining amount self.total_frames -= block_size # build a pcm.FrameList object from the combined samples framelist = from_list(samples, channel_count, bits_per_sample, True) # update the running MD5 sum calculation with the frame's data self.current_md5sum.update(framelist.to_bytes(0, 1)) # and finally return the frame data return framelist def read_frame_header(self): crc8 = CRC8() self.reader.add_callback(crc8.update) # read the 32-bit FLAC frame header sync_code = self.reader.read(14) if (sync_code != 0x3FFE): raise ValueError("invalid sync code") self.reader.skip(1) blocking_strategy = self.reader.read(1) block_size_bits = self.reader.read(4) sample_rate_bits = self.reader.read(4) channel_assignment = self.reader.read(4) bits_per_sample_bits = self.reader.read(3) self.reader.skip(1) # the frame number is a UTF-8 encoded value # which takes a variable number of whole bytes frame_number = self.read_utf8() # unpack the 4 bit block size field # which is the total PCM frames in the FLAC frame # and may require up to 16 more bits if the frame is usually-sized # (which typically happens at the end of the stream) if (block_size_bits == 0x6): block_size = self.reader.read(8) + 1 elif (block_size_bits == 0x7): block_size = self.reader.read(16) + 1 else: block_size = self.BLOCK_SIZE[block_size_bits] # unpack the 4 bit sample rate field # which is used for playback, but not needed for decoding # and may require up to 16 more bits # if the stream has a particularly unusual sample rate if (sample_rate_bits == 0xC): sample_rate = self.reader.read(8) * 1000 elif (sample_rate_bits == 0xD): sample_rate = self.reader.read(16) elif (sample_rate_bits == 0xE): sample_rate = self.reader.read(16) * 10 elif (sample_rate_bits == 0xF): raise ValueError("invalid sample rate") else: sample_rate = self.SAMPLE_RATE[sample_rate_bits] # unpack the 3 bit bits-per-sample field # this never requires additional bits if ((bits_per_sample_bits == 0x3) or (bits_per_sample_bits == 0x7)): raise ValueError("invalid bits per sample") else: bits_per_sample = self.BITS_PER_SAMPLE[bits_per_sample_bits] # read and verify frame's CRC-8 value self.reader.read(8) self.reader.pop_callback() if (int(crc8) != 0): raise ValueError("CRC8 mismatch in frame header") return (block_size, channel_assignment, bits_per_sample) def read_subframe_header(self): """returns a tuple of (subframe_type, subframe_order, wasted_bps)""" self.reader.skip(1) subframe_type = self.reader.read(6) if (self.reader.read(1) == 1): wasted_bps = self.reader.unary(1) + 1 else: wasted_bps = 0 # extract "order" value from 6 bit subframe type, if necessary if (subframe_type == 0): return (self.SUBFRAME_CONSTANT, None, wasted_bps) elif (subframe_type == 1): return (self.SUBFRAME_VERBATIM, None, wasted_bps) elif ((subframe_type & 0x38) == 0x08): return (self.SUBFRAME_FIXED, subframe_type & 0x07, wasted_bps) elif ((subframe_type & 0x20) == 0x20): return (self.SUBFRAME_LPC, (subframe_type & 0x1F) + 1, wasted_bps) else: raise ValueError("invalid subframe type") def read_subframe(self, block_size, bits_per_sample): (subframe_type, subframe_order, wasted_bps) = self.read_subframe_header() # read a list of signed sample values # depending on the subframe type, block size, # adjusted bits per sample and optional subframe order if (subframe_type == self.SUBFRAME_CONSTANT): subframe_samples = self.read_constant_subframe( block_size, bits_per_sample - wasted_bps) elif (subframe_type == self.SUBFRAME_VERBATIM): subframe_samples = self.read_verbatim_subframe( block_size, bits_per_sample - wasted_bps) elif (subframe_type == self.SUBFRAME_FIXED): subframe_samples = self.read_fixed_subframe( block_size, bits_per_sample - wasted_bps, subframe_order) else: subframe_samples = self.read_lpc_subframe( block_size, bits_per_sample - wasted_bps, subframe_order) # account for wasted bits-per-sample, if necessary if (wasted_bps): return [sample << wasted_bps for sample in subframe_samples] else: return subframe_samples def read_constant_subframe(self, block_size, bits_per_sample): sample = self.reader.read_signed(bits_per_sample) return [sample] * block_size def read_verbatim_subframe(self, block_size, bits_per_sample): return [ self.reader.read_signed(bits_per_sample) for x in range(block_size) ] def read_fixed_subframe(self, block_size, bits_per_sample, order): # "order" number of warm-up samples samples = [ self.reader.read_signed(bits_per_sample) for i in range(order) ] # "block_size" - "order" number of residual values residuals = self.read_residual(block_size, order) # which are applied to the warm-up samples # depending on the FIXED subframe order # and results in "block_size" number of total samples if (order == 0): return residuals elif (order == 1): for residual in residuals: samples.append(samples[-1] + residual) return samples elif (order == 2): for residual in residuals: samples.append((2 * samples[-1]) - samples[-2] + residual) return samples elif (order == 3): for residual in residuals: samples.append((3 * samples[-1]) - (3 * samples[-2]) + samples[-3] + residual) return samples elif (order == 4): for residual in residuals: samples.append((4 * samples[-1]) - (6 * samples[-2]) + (4 * samples[-3]) - samples[-4] + residual) return samples else: raise ValueError("unsupported FIXED subframe order") def read_lpc_subframe(self, block_size, bits_per_sample, order): # "order" number of warm-up samples samples = [ self.reader.read_signed(bits_per_sample) for i in range(order) ] # the size of each QLP coefficient, in bits qlp_precision = self.reader.read(4) # the amount of right shift to apply # during LPC calculation # (though this is a signed value, negative shifts are noops # in the reference FLAC decoder) qlp_shift_needed = max(self.reader.read_signed(5), 0) # "order" number of signed QLP coefficients qlp_coeffs = [ self.reader.read_signed(qlp_precision + 1) for i in range(order) ] # QLP coefficients are applied in reverse order qlp_coeffs.reverse() # "block_size" - "order" number of residual values residuals = self.read_residual(block_size, order) # which are applied to the running LPC calculation for residual in residuals: samples.append((sum([ coeff * sample for (coeff, sample) in zip(qlp_coeffs, samples[-order:]) ]) >> qlp_shift_needed) + residual) return samples def read_residual(self, block_size, order): residuals = [] coding_method = self.reader.read(2) partition_order = self.reader.read(4) # each parititon contains block_size / 2 ** partition_order # number of residuals for partition_number in range(2**partition_order): if (partition_number == 0): # except for the first partition # which contains "order" less than the rest residuals.extend( self.read_residual_partition( coding_method, (block_size // 2**partition_order) - order)) else: residuals.extend( self.read_residual_partition( coding_method, block_size // 2**partition_order)) return residuals def read_residual_partition(self, coding_method, residual_count): if (coding_method == 0): # the Rice parameters determines the number of # least-significant bits to read for each residual rice_parameter = self.reader.read(4) if (rice_parameter == 0xF): escape_code = self.reader.read(5) return [ self.reader.read_signed(escape_code) for i in range(residual_count) ] elif (coding_method == 1): # 24 bps streams may use a 5-bit Rice parameter # for better compression rice_parameter = self.reader.read(5) if (rice_parameter == 0x1F): escape_code = self.reader.read(5) return [ self.reader.read_signed(escape_code) for i in range(residual_count) ] else: raise ValueError("invalid Rice coding parameter") # a list of signed residual values partition_residuals = [] for i in range(residual_count): msb = self.reader.unary(1) # most-significant bits lsb = self.reader.read(rice_parameter) # least-significant bits value = (msb << rice_parameter) | lsb # combined into a value if (value & 1): # whose least-significant bit is the sign value partition_residuals.append(-(value >> 1) - 1) else: partition_residuals.append(value >> 1) return partition_residuals def read_utf8(self): total_bytes = self.reader.unary(0) value = self.reader.read(7 - total_bytes) while (total_bytes > 1): value = ((value << 6) | self.reader.parse("2p 6u")[0]) total_bytes -= 1 return value def close(self): self.reader.close() def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.close()
def read_metadata(self): from io import BytesIO command = self.unsigned(2) if (command == 9): # got verbatim, so read data verbatim_bytes = ints_to_bytes( [self.unsigned(8) & 0xFF for i in range(self.unsigned(5))]) try: wave = BitstreamReader(BytesIO(verbatim_bytes), True) header = wave.read_bytes(12) if (header.startswith(b"RIFF") and header.endswith(b"WAVE")): # got RIFF/WAVE header, so parse wave blocks as needed total_size = len(verbatim_bytes) - 12 while (total_size > 0): (chunk_id, chunk_size) = wave.parse("4b 32u") total_size -= 8 if (chunk_id == b'fmt '): (channels, self.sample_rate, bits_per_sample, channel_mask) = parse_fmt( wave.substream(chunk_size)) self.channel_mask = int(channel_mask) return else: if (chunk_size % 2): wave.read_bytes(chunk_size + 1) total_size -= (chunk_size + 1) else: wave.read_bytes(chunk_size) total_size -= chunk_size else: # no fmt chunk, so use default metadata pass except (IOError, ValueError): pass try: aiff = BitstreamReader(BytesIO(verbatim_bytes), False) header = aiff.read_bytes(12) if (header.startswith(b"FORM") and header.endswith(b"AIFF")): # got FORM/AIFF header, so parse aiff blocks as needed total_size = len(verbatim_bytes) - 12 while (total_size > 0): (chunk_id, chunk_size) = aiff.parse("4b 32u") total_size -= 8 if (chunk_id == b'COMM'): (channels, total_sample_frames, bits_per_sample, self.sample_rate, channel_mask) = parse_comm( aiff.substream(chunk_size)) self.channel_mask = int(channel_mask) return else: if (chunk_size % 2): aiff.read_bytes(chunk_size + 1) total_size -= (chunk_size + 1) else: aiff.read_bytes(chunk_size) total_size -= chunk_size else: # no COMM chunk, so use default metadata pass except IOError: pass # got something else, so invent some PCM parameters self.sample_rate = 44100 if (self.channels == 1): self.channel_mask = 0x4 elif (self.channels == 2): self.channel_mask = 0x3 else: self.channel_mask = 0
def validate_header(header): """given header string as returned by aiff_header_footer() returns (total size, ssnd size) where total size is the size of the file in bytes and ssnd size is the size of the SSND chunk in bytes (including the 8 prefix bytes in the chunk but *not* including any padding byte at the end) the size of the SSND chunk and of the total file should be validated after the file has been completely written such that len(header) + len(SSND chunk) + len(footer) = total size raises ValueError if the header is invalid""" from io import BytesIO from audiotools.bitstream import BitstreamReader header_size = len(header) aiff_file = BitstreamReader(BytesIO(header), False) try: # ensure header starts with FORM<size>AIFF chunk (form, remaining_size, aiff) = aiff_file.parse("4b 32u 4b") if form != b"FORM": from audiotools.text import ERR_AIFF_NOT_AIFF raise ValueError(ERR_AIFF_NOT_AIFF) elif aiff != b"AIFF": from audiotools.text import ERR_AIFF_INVALID_AIFF raise ValueError(ERR_AIFF_INVALID_AIFF) else: total_size = remaining_size + 8 header_size -= 12 comm_found = False while header_size > 0: # ensure each chunk header is valid (chunk_id, chunk_size) = aiff_file.parse("4b 32u") if frozenset(chunk_id).issubset(AiffAudio.PRINTABLE_ASCII): header_size -= 8 else: from audiotools.text import ERR_AIFF_INVALID_CHUNK raise ValueError(ERR_AIFF_INVALID_CHUNK) if chunk_id == b"COMM": if not comm_found: # skip COMM chunk when found comm_found = True if chunk_size % 2: aiff_file.skip_bytes(chunk_size + 1) header_size -= (chunk_size + 1) else: aiff_file.skip_bytes(chunk_size) header_size -= chunk_size else: # ensure only one COMM chunk is found from audiotools.text import ERR_AIFF_MULTIPLE_COMM_CHUNKS raise ValueError(ERR_AIFF_MULTIPLE_COMM_CHUNKS) elif chunk_id == b"SSND": if not comm_found: # ensure at least one COMM chunk is found from audiotools.text import ERR_AIFF_PREMATURE_SSND_CHUNK raise ValueError(ERR_AIFF_PREMATURE_SSND_CHUNK) elif header_size > 8: # ensure exactly 8 bytes remain after SSND chunk header from audiotools.text import ERR_AIFF_HEADER_EXTRA_SSND raise ValueError(ERR_AIFF_HEADER_EXTRA_SSND) elif header_size < 8: from audiotools.text import ERR_AIFF_HEADER_MISSING_SSND raise ValueError(ERR_AIFF_HEADER_MISSING_SSND) else: return (total_size, chunk_size - 8) else: # skip the full contents of non-audio chunks if chunk_size % 2: aiff_file.skip_bytes(chunk_size + 1) header_size -= (chunk_size + 1) else: aiff_file.skip_bytes(chunk_size) header_size -= chunk_size else: # header parsed with no SSND chunks found from audiotools.text import ERR_AIFF_NO_SSND_CHUNK raise ValueError(ERR_AIFF_NO_SSND_CHUNK) except IOError: from audiotools.text import ERR_AIFF_HEADER_IOERROR raise ValueError(ERR_AIFF_HEADER_IOERROR)
class TTADecoder(object): def __init__(self, filename): self.reader = BitstreamReader(open(filename, "rb"), True) crc = CRC32() self.reader.add_callback(crc.update) # read the header (signature, format_, self.channels, self.bits_per_sample, self.sample_rate, self.total_pcm_frames) = self.reader.parse("4b 16u 16u 16u 32u 32u") self.reader.pop_callback() header_crc = self.reader.read(32) if (int(crc) != header_crc): raise ValueError("CRC32 mismatch in header (0x%8.8X != 0x%8.8X)" % (header_crc, int(crc))) self.channel_mask = {1: 0x4, 2: 0x3}.get(self.channels, 0) total_tta_frames = div_ceil(self.total_pcm_frames * 245, self.sample_rate * 256) self.pcm_frames_per_tta_frame = (self.sample_rate * 256) // 245 # read the seektable crc = CRC32() self.reader.add_callback(crc.update) self.frame_sizes = [ self.reader.read(32) for i in range(total_tta_frames) ] self.reader.pop_callback() seektable_crc = self.reader.read(32) if (int(crc) != seektable_crc): raise ValueError( "CRC32 mismatch in seektable (0x%8.8X != 0x%8.8X)" % (header_crc, int(crc))) self.current_tta_frame = 0 def read(self, pcm_frames): if (self.total_pcm_frames == 0): return FrameList("", self.channels, self.bits_per_sample, True, True) pcm_frames = min(self.pcm_frames_per_tta_frame, self.total_pcm_frames) frame_reader = self.reader.substream( self.frame_sizes[self.current_tta_frame]) crc = CRC32() frame_reader.add_callback(crc.update) self.total_pcm_frames -= pcm_frames self.current_tta_frame += 1 # setup Rice parameters for each channel k0 = [10] * self.channels k1 = [10] * self.channels sum0 = [2**14] * self.channels sum1 = [2**14] * self.channels # list of unfiltered output for each channel unfiltered = [[] for i in range(self.channels)] for f in range(pcm_frames): correlated = [] for (c, ch_output) in enumerate(unfiltered): # read most-significant bits MSB = frame_reader.unary(0) if (MSB == 0): # read least-significant bits unsigned = frame_reader.read(k0[c]) else: # read least-significant bits LSB = frame_reader.read(k1[c]) unshifted = ((MSB - 1) << k1[c]) + LSB unsigned = unshifted + (1 << k0[c]) # adjust sum1 and k1 sum1[c] += (unshifted - (sum1[c] >> 4)) if (sum1[c] < (2**(k1[c] + 4))): k1[c] = max(k1[c] - 1, 0) elif (sum1[c] > (2**(k1[c] + 5))): k1[c] += 1 # adjust sum0 and k0 sum0[c] += (unsigned - (sum0[c] >> 4)) if (sum0[c] < (2**(k0[c] + 4))): k0[c] = max(k0[c] - 1, 0) elif (sum0[c] > (2**(k0[c] + 5))): k0[c] += 1 # apply sign bit if ((unsigned % 2) == 1): # positive ch_output.append((unsigned + 1) // 2) else: # negative ch_output.append(-(unsigned // 2)) # check frame's trailing CRC32 now that reading is finished frame_reader.byte_align() frame_reader.pop_callback() frame_crc = frame_reader.read(32) if (int(crc) != frame_crc): raise ValueError("CRC32 mismatch in frame (0x%8.8X != 0x%8.8X)" % (frame_crc, int(crc))) # run hybrid filter on each channel filtered = [] for unfiltered_ch in unfiltered: filtered.append(tta_filter(self.bits_per_sample, unfiltered_ch)) # run fixed order prediction on each channel predicted = [] for filtered_ch in filtered: predicted.append(fixed_predictor(self.bits_per_sample, filtered_ch)) if (self.channels == 1): # send channel as-is return from_list(predicted[0], 1, self.bits_per_sample, True) else: # decorrelate channels decorrelated = decorrelate(predicted) # return all channels as single FrameList return from_channels([ from_list(decorrelated_ch, 1, self.bits_per_sample, True) for decorrelated_ch in decorrelated ]) def close(self): # FIXME pass
class AuReader(object): def __init__(self, au_filename): from audiotools.bitstream import BitstreamReader from audiotools.text import (ERR_AU_INVALID_HEADER, ERR_AU_UNSUPPORTED_FORMAT) self.stream = BitstreamReader(open(au_filename, "rb"), False) (magic_number, self.data_offset, data_size, encoding_format, self.sample_rate, self.channels) = self.stream.parse("4b 5* 32u") if (magic_number != b'.snd'): self.stream.close() raise ValueError(ERR_AU_INVALID_HEADER) try: self.bits_per_sample = {2: 8, 3: 16, 4: 24}[encoding_format] except KeyError: self.stream.close() raise ValueError(ERR_AU_UNSUPPORTED_FORMAT) self.channel_mask = {1: 0x4, 2: 0x3}.get(self.channels, 0) self.bytes_per_pcm_frame = ((self.bits_per_sample // 8) * self.channels) self.total_pcm_frames = (data_size // self.bytes_per_pcm_frame) self.remaining_pcm_frames = self.total_pcm_frames def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.close() def read(self, pcm_frames): # try to read requested PCM frames or remaining frames requested_pcm_frames = min(max(pcm_frames, 1), self.remaining_pcm_frames) requested_bytes = (self.bytes_per_pcm_frame * requested_pcm_frames) pcm_data = self.stream.read_bytes(requested_bytes) # raise exception if data block exhausted early if (len(pcm_data) < requested_bytes): from audiotools.text import ERR_AU_TRUNCATED_DATA raise IOError(ERR_AU_TRUNCATED_DATA) else: self.remaining_pcm_frames -= requested_pcm_frames # return parsed chunk return FrameList(pcm_data, self.channels, self.bits_per_sample, True, True) def read_closed(self, pcm_frames): raise ValueError("cannot read closed stream") def seek(self, pcm_frame_offset): if (pcm_frame_offset < 0): from audiotools.text import ERR_NEGATIVE_SEEK raise ValueError(ERR_NEGATIVE_SEEK) # ensure one doesn't walk off the end of the file pcm_frame_offset = min(pcm_frame_offset, self.total_pcm_frames) # position file in data block self.stream.seek( self.data_offset + (pcm_frame_offset * self.bytes_per_pcm_frame), 0) self.remaining_pcm_frames = (self.total_pcm_frames - pcm_frame_offset) return pcm_frame_offset def seek_closed(self, pcm_frame_offset): raise ValueError("cannot seek closed stream") def close(self): self.stream.close() self.read = self.read_closed self.seek = self.seek_closed
def __parse_info__(self): """generates a cache of sample_rate, bits-per-sample, etc""" import re import os.path from audiotools.bitstream import BitstreamReader if (len(self.tracks) == 0): return # Why is this post-init processing necessary? # DVDATrack references DVDATitle # so a DVDATitle must exist when DVDATrack is initialized. # But because reading this info requires knowing the sector # of the first track, we wind up with a circular dependency. # Doing a "post-process" pass fixes that problem. # find the AOB file of the title's first track track_sector = self[0].first_sector titleset = re.compile("ATS_%2.2d_\\d\\.AOB" % (self.titleset)) for aob_path in sorted([ self.dvdaudio.files[key] for key in self.dvdaudio.files.keys() if (titleset.match(key)) ]): aob_sectors = os.path.getsize(aob_path) // DVDAudio.SECTOR_SIZE if (track_sector > aob_sectors): track_sector -= aob_sectors else: break else: from audiotools.text import ERR_DVDA_NO_TRACK_SECTOR raise ValueError(ERR_DVDA_NO_TRACK_SECTOR) # open that AOB file and seek to that track's first sector aob_file = open(aob_path, 'rb') try: aob_file.seek(track_sector * DVDAudio.SECTOR_SIZE) aob_reader = BitstreamReader(aob_file, 0) # read and validate the pack header # (there's one pack header per sector, at the sector's start) (sync_bytes, marker1, current_pts_high, marker2, current_pts_mid, marker3, current_pts_low, marker4, scr_extension, marker5, bit_rate, marker6, stuffing_length) = aob_reader.parse( "32u 2u 3u 1u 15u 1u 15u 1u 9u 1u 22u 2u 5p 3u") aob_reader.skip_bytes(stuffing_length) if (sync_bytes != 0x1BA): from audiotools.text import ERR_DVDA_INVALID_AOB_SYNC raise InvalidDVDA(ERR_DVDA_INVALID_AOB_SYNC) if (((marker1 != 1) or (marker2 != 1) or (marker3 != 1) or (marker4 != 1) or (marker5 != 1) or (marker6 != 3))): from audiotools.text import ERR_DVDA_INVALID_AOB_MARKER raise InvalidDVDA(ERR_DVDA_INVALID_AOB_MARKER) packet_pts = ((current_pts_high << 30) | (current_pts_mid << 15) | current_pts_low) # skip packets until one with a stream ID of 0xBD is found (start_code, stream_id, packet_length) = aob_reader.parse("24u 8u 16u") if (start_code != 1): from audiotools.text import ERR_DVDA_INVALID_AOB_START raise InvalidDVDA(ERR_DVDA_INVALID_AOB_START) while (stream_id != 0xBD): aob_reader.skip_bytes(packet_length) (start_code, stream_id, packet_length) = aob_reader.parse("24u 8u 16u") if (start_code != 1): from audiotools.text import ERR_DVDA_INVALID_AOB_START raise InvalidDVDA(ERR_DVDA_INVALID_AOB_START) # parse the PCM/MLP header in the packet data (pad1_size, ) = aob_reader.parse("16p 8u") aob_reader.skip_bytes(pad1_size) (stream_id, crc) = aob_reader.parse("8u 8u 8p") if (stream_id == 0xA0): # PCM # read a PCM reader (pad2_size, first_audio_frame, padding2, group1_bps, group2_bps, group1_sample_rate, group2_sample_rate, padding3, channel_assignment ) = aob_reader.parse("8u 16u 8u 4u 4u 4u 4u 8u 8u") else: # MLP aob_reader.skip_bytes(aob_reader.read(8)) # skip pad2 # read a total frame size + MLP major sync header (total_frame_size, sync_words, stream_type, group1_bps, group2_bps, group1_sample_rate, group2_sample_rate, unknown1, channel_assignment, unknown2) = aob_reader.parse( "4p 12u 16p 24u 8u 4u 4u 4u 4u 11u 5u 48u") # return the values indicated by the header self.sample_rate = DVDATrack.SAMPLE_RATE[group1_sample_rate] self.channels = DVDATrack.CHANNELS[channel_assignment] self.channel_mask = DVDATrack.CHANNEL_MASK[channel_assignment] self.bits_per_sample = DVDATrack.BITS_PER_SAMPLE[group1_bps] self.stream_id = stream_id finally: aob_file.close()
def validate_header(header): """given header string as returned by wave_header_footer(), returns (total size, data size) where total size is the size of the file in bytes and data size is the size of the data chunk in bytes (not including any padding byte) the size of the data chunk and of the total file should be validated after the file has been completely written such that len(header) + len(data chunk) + len(footer) = total size raises ValueError if the header is invalid """ from io import BytesIO from audiotools.bitstream import BitstreamReader header_size = len(header) wave_file = BitstreamReader(BytesIO(header), True) try: # ensure header starts with RIFF<size>WAVE chunk (riff, remaining_size, wave) = wave_file.parse("4b 32u 4b") if riff != b"RIFF": from audiotools.text import ERR_WAV_NOT_WAVE raise ValueError(ERR_WAV_NOT_WAVE) elif wave != b"WAVE": from audiotools.text import ERR_WAV_INVALID_WAVE raise ValueError(ERR_WAV_INVALID_WAVE) else: total_size = remaining_size + 8 header_size -= 12 fmt_found = False while header_size > 0: # ensure each chunk header is valid (chunk_id, chunk_size) = wave_file.parse("4b 32u") if not frozenset(chunk_id).issubset(WaveAudio.PRINTABLE_ASCII): from audiotools.text import ERR_WAV_INVALID_CHUNK raise ValueError(ERR_WAV_INVALID_CHUNK) else: header_size -= 8 if chunk_id == b"fmt ": if not fmt_found: # skip fmt chunk data when found fmt_found = True if chunk_size % 2: wave_file.skip_bytes(chunk_size + 1) header_size -= (chunk_size + 1) else: wave_file.skip_bytes(chunk_size) header_size -= chunk_size else: # ensure only one fmt chunk is found from audiotools.text import ERR_WAV_MULTIPLE_FMT raise ValueError(ERR_WAV_MULTIPLE_FMT) elif chunk_id == b"data": if not fmt_found: # ensure at least one fmt chunk is found from audiotools.text import ERR_WAV_PREMATURE_DATA raise ValueError(ERR_WAV_PREMATURE_DATA) elif header_size > 0: # ensure no data remains after data chunk header from audiotools.text import ERR_WAV_HEADER_EXTRA_DATA raise ValueError( ERR_WAV_HEADER_EXTRA_DATA.format(header_size)) else: return (total_size, chunk_size) else: # skip the full contents of non-audio chunks if chunk_size % 2: wave_file.skip_bytes(chunk_size + 1) header_size -= (chunk_size + 1) else: wave_file.skip_bytes(chunk_size) header_size -= chunk_size else: # header parsed with no data chunks found from audiotools.text import ERR_WAV_NO_DATA_CHUNK raise ValueError(ERR_WAV_NO_DATA_CHUNK) except IOError: from audiotools.text import ERR_WAV_HEADER_IOERROR raise ValueError(ERR_WAV_HEADER_IOERROR)
class WaveReader(object): """a PCMReader object for reading wave file contents""" def __init__(self, wave_filename): """wave_filename is a string""" from audiotools.bitstream import BitstreamReader self.stream = BitstreamReader(open(wave_filename, "rb"), True) # ensure RIFF<size>WAVE header is ok try: (riff, total_size, wave) = self.stream.parse("4b 32u 4b") except struct.error: from audiotools.text import ERR_WAV_INVALID_WAVE self.stream.close() raise ValueError(ERR_WAV_INVALID_WAVE) if riff != b'RIFF': from audiotools.text import ERR_WAV_NOT_WAVE self.stream.close() raise ValueError(ERR_WAV_NOT_WAVE) elif wave != b'WAVE': from audiotools.text import ERR_WAV_INVALID_WAVE self.stream.close() raise ValueError(ERR_WAV_INVALID_WAVE) else: total_size -= 4 fmt_chunk_read = False # walk through chunks until "data" chunk encountered while total_size > 0: try: (chunk_id, chunk_size) = self.stream.parse("4b 32u") except struct.error: from audiotools.text import ERR_WAV_INVALID_WAVE self.stream.close() raise ValueError(ERR_WAV_INVALID_WAVE) if not frozenset(chunk_id).issubset(WaveAudio.PRINTABLE_ASCII): from audiotools.text import ERR_WAV_INVALID_CHUNK self.stream.close() raise ValueError(ERR_WAV_INVALID_CHUNK) else: total_size -= 8 if chunk_id == b"fmt ": # when "fmt " chunk encountered, # use it to populate PCMReader attributes (self.channels, self.sample_rate, self.bits_per_sample, channel_mask) = parse_fmt(self.stream) self.channel_mask = int(channel_mask) self.bytes_per_pcm_frame = ((self.bits_per_sample // 8) * self.channels) fmt_chunk_read = True elif chunk_id == b"data": # when "data" chunk encountered, # use its size to determine total PCM frames # and ready PCMReader for reading if not fmt_chunk_read: from audiotools.text import ERR_WAV_PREMATURE_DATA self.stream.close() raise ValueError(ERR_WAV_PREMATURE_DATA) else: self.total_pcm_frames = (chunk_size // self.bytes_per_pcm_frame) self.remaining_pcm_frames = self.total_pcm_frames self.data_start = self.stream.getpos() return else: # all other chunks are ignored self.stream.skip_bytes(chunk_size) if chunk_size % 2: if len(self.stream.read_bytes(1)) < 1: from audiotools.text import ERR_WAV_INVALID_CHUNK self.stream.close() raise ValueError(ERR_WAV_INVALID_CHUNK) total_size -= (chunk_size + 1) else: total_size -= chunk_size else: # raise an error if no "data" chunk is encountered from audiotools.text import ERR_WAV_NO_DATA_CHUNK self.stream.close() raise ValueError(ERR_WAV_NO_DATA_CHUNK) def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.close() def read(self, pcm_frames): """try to read a pcm.FrameList with the given number of PCM frames""" # try to read requested PCM frames or remaining frames requested_pcm_frames = min(max(pcm_frames, 1), self.remaining_pcm_frames) requested_bytes = (self.bytes_per_pcm_frame * requested_pcm_frames) pcm_data = self.stream.read_bytes(requested_bytes) # raise exception if "data" chunk exhausted early if len(pcm_data) < requested_bytes: from audiotools.text import ERR_WAV_TRUNCATED_DATA_CHUNK raise IOError(ERR_WAV_TRUNCATED_DATA_CHUNK) else: self.remaining_pcm_frames -= requested_pcm_frames # return parsed chunk return FrameList(pcm_data, self.channels, self.bits_per_sample, False, self.bits_per_sample != 8) def read_closed(self, pcm_frames): raise ValueError("cannot read closed stream") def seek(self, pcm_frame_offset): """tries to seek to the given PCM frame offset returns the total amount of frames actually seeked over""" if pcm_frame_offset < 0: from audiotools.text import ERR_NEGATIVE_SEEK raise ValueError(ERR_NEGATIVE_SEEK) # ensure one doesn't walk off the end of the file pcm_frame_offset = min(pcm_frame_offset, self.total_pcm_frames) # position file in "data" chunk self.stream.setpos(self.data_start) self.stream.seek(pcm_frame_offset * self.bytes_per_pcm_frame, 1) self.remaining_pcm_frames = (self.total_pcm_frames - pcm_frame_offset) return pcm_frame_offset def seek_closed(self, pcm_frame_offset): raise ValueError("cannot seek closed stream") def close(self): """closes the stream for reading""" self.stream.close() self.read = self.read_closed self.seek = self.seek_closed
def __init__(self, filename): """filename is a plain string""" AudioFile.__init__(self, filename) from audiotools.bitstream import parse try: mp3file = open(filename, "rb") except IOError as msg: raise InvalidMP3(str(msg)) try: try: header_bytes = MP3Audio.__find_next_mp3_frame__(mp3file) except IOError: from audiotools.text import ERR_MP3_FRAME_NOT_FOUND raise InvalidMP3(ERR_MP3_FRAME_NOT_FOUND) (frame_sync, mpeg_id, layer, bit_rate, sample_rate, pad, channels) = parse("11u 2u 2u 1p 4u 2u 1u 1p 2u 6p", False, mp3file.read(4)) self.__samplerate__ = self.SAMPLE_RATE[mpeg_id][sample_rate] if self.__samplerate__ is None: from audiotools.text import ERR_MP3_INVALID_SAMPLE_RATE raise InvalidMP3(ERR_MP3_INVALID_SAMPLE_RATE) if channels in (0, 1, 2): self.__channels__ = 2 else: self.__channels__ = 1 first_frame = mp3file.read( self.frame_length(mpeg_id, layer, bit_rate, sample_rate, pad) - 4) if ((b"Xing" in first_frame) and (len(first_frame[first_frame. index(b"Xing"):first_frame.index(b"Xing") + 160]) == 160)): # pull length from Xing header, if present self.__pcm_frames__ = (parse( "32p 32p 32u 32p 832p", 0, first_frame[first_frame.index( b"Xing"):first_frame.index(b"Xing") + 160])[0] * self.PCM_FRAMES_PER_MPEG_FRAME[layer]) else: # otherwise, bounce through file frames from audiotools.bitstream import BitstreamReader reader = BitstreamReader(mp3file, False) self.__pcm_frames__ = 0 try: (frame_sync, mpeg_id, layer, bit_rate, sample_rate, pad) = reader.parse("11u 2u 2u 1p 4u 2u 1u 9p") while frame_sync == 0x7FF: self.__pcm_frames__ += \ self.PCM_FRAMES_PER_MPEG_FRAME[layer] reader.skip_bytes( self.frame_length(mpeg_id, layer, bit_rate, sample_rate, pad) - 4) (frame_sync, mpeg_id, layer, bit_rate, sample_rate, pad) = reader.parse("11u 2u 2u 1p 4u 2u 1u 9p") except IOError: pass except ValueError as err: raise InvalidMP3(err) finally: mp3file.close()
class ALACDecoder(object): def __init__(self, filename): self.reader = BitstreamReader(open(filename, "rb"), False) self.reader.mark() try: # locate the "alac" atom # which is full of required decoding parameters try: stsd = self.find_sub_atom(b"moov", b"trak", b"mdia", b"minf", b"stbl", b"stsd") except KeyError: raise ValueError("required stsd atom not found") (stsd_version, descriptions) = stsd.parse("8u 24p 32u") (alac1, alac2, self.samples_per_frame, self.bits_per_sample, self.history_multiplier, self.initial_history, self.maximum_k, self.channels, self.sample_rate) = stsd.parse( # ignore much of the stuff in the "high" ALAC atom "32p 4b 6P 16p 16p 16p 4P 16p 16p 16p 16p 4P" + # and use the attributes in the "low" ALAC atom instead "32p 4b 4P 32u 8p 8u 8u 8u 8u 8u 16p 32p 32p 32u") self.channel_mask = { 1: 0x0004, 2: 0x0003, 3: 0x0007, 4: 0x0107, 5: 0x0037, 6: 0x003F, 7: 0x013F, 8: 0x00FF }.get(self.channels, 0) if ((alac1 != b'alac') or (alac2 != b'alac')): raise ValueError("Invalid alac atom") # also locate the "mdhd" atom # which contains the stream's length in PCM frames self.reader.rewind() mdhd = self.find_sub_atom(b"moov", b"trak", b"mdia", b"mdhd") (version, ) = mdhd.parse("8u 24p") if (version == 0): (self.total_pcm_frames, ) = mdhd.parse("32p 32p 32p 32u 2P 16p") elif (version == 1): (self.total_pcm_frames, ) = mdhd.parse("64p 64p 32p 64U 2P 16p") else: raise ValueError("invalid mdhd version") # finally, set our stream to the "mdat" atom self.reader.rewind() (atom_size, atom_name) = self.reader.parse("32u 4b") while (atom_name != b"mdat"): self.reader.skip_bytes(atom_size - 8) (atom_size, atom_name) = self.reader.parse("32u 4b") finally: self.reader.unmark() def find_sub_atom(self, *atom_names): reader = self.reader for (last, next_atom) in iter_last(iter(atom_names)): try: (length, stream_atom) = reader.parse("32u 4b") while (stream_atom != next_atom): reader.skip_bytes(length - 8) (length, stream_atom) = reader.parse("32u 4b") if (last): return reader.substream(length - 8) else: reader = reader.substream(length - 8) except IOError: raise KeyError(next_atom) def read(self, pcm_frames): # if the stream is exhausted, return an empty pcm.FrameList object if (self.total_pcm_frames == 0): return empty_framelist(self.channels, self.bits_per_sample) # otherwise, read one ALAC frameset's worth of frame data frameset_data = [] frame_channels = self.reader.read(3) + 1 while (frame_channels != 0x8): frameset_data.extend(self.read_frame(frame_channels)) frame_channels = self.reader.read(3) + 1 self.reader.byte_align() # reorder the frameset to Wave order, depending on channel count if ((self.channels == 1) or (self.channels == 2)): pass elif (self.channels == 3): frameset_data = [ frameset_data[1], frameset_data[2], frameset_data[0] ] elif (self.channels == 4): frameset_data = [ frameset_data[1], frameset_data[2], frameset_data[0], frameset_data[3] ] elif (self.channels == 5): frameset_data = [ frameset_data[1], frameset_data[2], frameset_data[0], frameset_data[3], frameset_data[4] ] elif (self.channels == 6): frameset_data = [ frameset_data[1], frameset_data[2], frameset_data[0], frameset_data[5], frameset_data[3], frameset_data[4] ] elif (self.channels == 7): frameset_data = [ frameset_data[1], frameset_data[2], frameset_data[0], frameset_data[6], frameset_data[3], frameset_data[4], frameset_data[5] ] elif (self.channels == 8): frameset_data = [ frameset_data[3], frameset_data[4], frameset_data[0], frameset_data[7], frameset_data[5], frameset_data[6], frameset_data[1], frameset_data[2] ] else: raise ValueError("unsupported channel count") framelist = from_channels([ from_list(channel, 1, self.bits_per_sample, True) for channel in frameset_data ]) # deduct PCM frames from remainder self.total_pcm_frames -= framelist.frames # return samples as a pcm.FrameList object return framelist def read_frame(self, channel_count): """returns a list of PCM sample lists, one per channel""" # read the ALAC frame header self.reader.skip(16) has_sample_count = self.reader.read(1) uncompressed_lsb_size = self.reader.read(2) uncompressed = self.reader.read(1) if (has_sample_count): sample_count = self.reader.read(32) else: sample_count = self.samples_per_frame if (uncompressed == 1): # if the frame is uncompressed, # read the raw, interlaced samples samples = [ self.reader.read_signed(self.bits_per_sample) for i in range(sample_count * channel_count) ] return [samples[i::channel_count] for i in range(channel_count)] else: # if the frame is compressed, # read the interlacing parameters interlacing_shift = self.reader.read(8) interlacing_leftweight = self.reader.read(8) # subframe headers subframe_headers = [ self.read_subframe_header() for i in range(channel_count) ] # optional uncompressed LSB values if (uncompressed_lsb_size > 0): uncompressed_lsbs = [ self.reader.read(uncompressed_lsb_size * 8) for i in range(sample_count * channel_count) ] else: uncompressed_lsbs = [] sample_size = (self.bits_per_sample - (uncompressed_lsb_size * 8) + channel_count - 1) # and residual blocks residual_blocks = [ self.read_residuals(sample_size, sample_count) for i in range(channel_count) ] # calculate subframe samples based on # subframe header's QLP coefficients and QLP shift-needed decoded_subframes = [ self.decode_subframe(header[0], header[1], sample_size, residuals) for (header, residuals) in zip(subframe_headers, residual_blocks) ] # decorrelate channels according interlacing shift and leftweight decorrelated_channels = self.decorrelate_channels( decoded_subframes, interlacing_shift, interlacing_leftweight) # if uncompressed LSB values are present, # prepend them to each sample of each channel if (uncompressed_lsb_size > 0): channels = [] for (i, channel) in enumerate(decorrelated_channels): assert (len(channel) == len( uncompressed_lsbs[i::channel_count])) channels.append([ s << (uncompressed_lsb_size * 8) | l for (s, l) in zip( channel, uncompressed_lsbs[i::channel_count]) ]) return channels else: return decorrelated_channels def read_subframe_header(self): prediction_type = self.reader.read(4) qlp_shift_needed = self.reader.read(4) rice_modifier = self.reader.read(3) qlp_coefficients = [ self.reader.read_signed(16) for i in range(self.reader.read(5)) ] return (qlp_shift_needed, qlp_coefficients) def read_residuals(self, sample_size, sample_count): residuals = [] history = self.initial_history sign_modifier = 0 i = 0 while (i < sample_count): # get an unsigned residual based on "history" # and on "sample_size" as a lst resort k = min(log2(history // (2**9) + 3), self.maximum_k) unsigned = self.read_residual(k, sample_size) + sign_modifier # clear out old sign modifier, if any sign_modifier = 0 # change unsigned residual to signed residual if (unsigned & 1): residuals.append(-((unsigned + 1) // 2)) else: residuals.append(unsigned // 2) # update history based on unsigned residual if (unsigned <= 0xFFFF): history += ((unsigned * self.history_multiplier) - ((history * self.history_multiplier) >> 9)) else: history = 0xFFFF # if history gets too small, we may have a block of 0 samples # which can be compressed more efficiently if ((history < 128) and ((i + 1) < sample_count)): zeroes_k = min(7 - log2(history) + ((history + 16) // 64), self.maximum_k) zero_residuals = self.read_residual(zeroes_k, 16) if (zero_residuals > 0): residuals.extend([0] * zero_residuals) i += zero_residuals history = 0 if (zero_residuals <= 0xFFFF): sign_modifier = 1 i += 1 return residuals def read_residual(self, k, sample_size): msb = self.reader.read_huffman_code(RESIDUAL) if (msb == -1): return self.reader.read(sample_size) elif (k == 0): return msb else: lsb = self.reader.read(k) if (lsb > 1): return msb * ((1 << k) - 1) + (lsb - 1) elif (lsb == 1): self.reader.unread(1) return msb * ((1 << k) - 1) else: self.reader.unread(0) return msb * ((1 << k) - 1) def decode_subframe(self, qlp_shift_needed, qlp_coefficients, sample_size, residuals): # first sample is always copied verbatim samples = [residuals.pop(0)] if (len(qlp_coefficients) < 31): # the next "coefficient count" samples # are applied as differences to the previous for i in range(len(qlp_coefficients)): samples.append( truncate_bits(samples[-1] + residuals.pop(0), sample_size)) # remaining samples are processed much like LPC for residual in residuals: base_sample = samples[-len(qlp_coefficients) - 1] lpc_sum = sum([(s - base_sample) * c for (s, c) in zip(samples[-len(qlp_coefficients):], reversed(qlp_coefficients))]) outval = (1 << (qlp_shift_needed - 1)) + lpc_sum outval >>= qlp_shift_needed samples.append( truncate_bits(outval + residual + base_sample, sample_size)) buf = samples[-len(qlp_coefficients) - 2:-1] # error value then adjusts the coefficients table if (residual > 0): predictor_num = len(qlp_coefficients) - 1 while ((predictor_num >= 0) and residual > 0): val = (buf[0] - buf[len(qlp_coefficients) - predictor_num]) sign = sign_only(val) qlp_coefficients[predictor_num] -= sign val *= sign residual -= ((val >> qlp_shift_needed) * (len(qlp_coefficients) - predictor_num)) predictor_num -= 1 elif (residual < 0): # the same as above, but we break if residual goes positive predictor_num = len(qlp_coefficients) - 1 while ((predictor_num >= 0) and residual < 0): val = (buf[0] - buf[len(qlp_coefficients) - predictor_num]) sign = -sign_only(val) qlp_coefficients[predictor_num] -= sign val *= sign residual -= ((val >> qlp_shift_needed) * (len(qlp_coefficients) - predictor_num)) predictor_num -= 1 else: # residuals are encoded as simple difference values for residual in residuals: samples.append( truncate_bits(samples[-1] + residual, sample_size)) return samples def decorrelate_channels(self, channel_data, interlacing_shift, interlacing_leftweight): if (len(channel_data) != 2): return channel_data elif (interlacing_leftweight == 0): return channel_data else: left = [] right = [] for (ch1, ch2) in zip(*channel_data): right.append(ch1 - ((ch2 * interlacing_leftweight) // (2**interlacing_shift))) left.append(ch2 + right[-1]) return [left, right] def close(self): self.reader.close() def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.close()
def __read_info__(self): from audiotools.bitstream import BitstreamReader from audiotools import ChannelMask reader = BitstreamReader(open(self.filename, "rb"), 1) reader.mark() try: (block_id, total_samples, bits_per_sample, mono_output, initial_block, final_block, sample_rate) = reader.parse( "4b 64p 32u 64p 2u 1u 8p 1u 1u 5p 5p 4u 37p") if (block_id != 'wvpk'): from audiotools.text import ERR_WAVPACK_INVALID_HEADER raise InvalidWavPack(ERR_WAVPACK_INVALID_HEADER) if (sample_rate != 0xF): self.__samplerate__ = WavPackAudio.SAMPLING_RATE[sample_rate] else: # if unknown, pull from SAMPLE_RATE sub-block for (block_id, nondecoder, data_size, data) in self.sub_blocks(reader): if ((block_id == 0x7) and nondecoder): self.__samplerate__ = data.read(data_size * 8) break else: # no SAMPLE RATE sub-block found # so pull info from FMT chunk reader.rewind() (self.__samplerate__,) = self.fmt_chunk(reader).parse( "32p 32u") self.__bitspersample__ = [8, 16, 24, 32][bits_per_sample] self.__total_frames__ = total_samples if (initial_block and final_block): if (mono_output): self.__channels__ = 1 self.__channel_mask__ = ChannelMask(0x4) else: self.__channels__ = 2 self.__channel_mask__ = ChannelMask(0x3) else: # if not mono or stereo, pull from CHANNEL INFO sub-block reader.rewind() for (block_id, nondecoder, data_size, data) in self.sub_blocks(reader): if ((block_id == 0xD) and not nondecoder): self.__channels__ = data.read(8) self.__channel_mask__ = ChannelMask( data.read((data_size - 1) * 8)) break else: # no CHANNEL INFO sub-block found # so pull info from FMT chunk reader.rewind() fmt = self.fmt_chunk(reader) compression_code = fmt.read(16) self.__channels__ = fmt.read(16) if (compression_code == 1): # this is theoretically possible # with very old .wav files, # but shouldn't happen in practice self.__channel_mask__ = \ {1: ChannelMask.from_fields(front_center=True), 2: ChannelMask.from_fields(front_left=True, front_right=True), 3: ChannelMask.from_fields(front_left=True, front_right=True, front_center=True), 4: ChannelMask.from_fields(front_left=True, front_right=True, back_left=True, back_right=True), 5: ChannelMask.from_fields(front_left=True, front_right=True, back_left=True, back_right=True, front_center=True), 6: ChannelMask.from_fields(front_left=True, front_right=True, back_left=True, back_right=True, front_center=True, low_frequency=True) }.get(self.__channels__, ChannelMask(0)) elif (compression_code == 0xFFFE): fmt.skip(128) mask = fmt.read(32) self.__channel_mask__ = ChannelMask(mask) else: from audiotools.text import ERR_WAVPACK_UNSUPPORTED_FMT raise InvalidWavPack(ERR_WAVPACK_UNSUPPORTED_FMT) finally: reader.unmark() reader.close()
class AuReader(object): def __init__(self, au_filename): from audiotools.bitstream import BitstreamReader from audiotools.text import (ERR_AU_INVALID_HEADER, ERR_AU_UNSUPPORTED_FORMAT) self.stream = BitstreamReader(open(au_filename, "rb"), False) (magic_number, self.data_offset, data_size, encoding_format, self.sample_rate, self.channels) = self.stream.parse("4b 5* 32u") if magic_number != b'.snd': self.stream.close() raise ValueError(ERR_AU_INVALID_HEADER) try: self.bits_per_sample = {2: 8, 3: 16, 4: 24}[encoding_format] except KeyError: self.stream.close() raise ValueError(ERR_AU_UNSUPPORTED_FORMAT) self.channel_mask = {1: 0x4, 2: 0x3}.get(self.channels, 0) self.bytes_per_pcm_frame = ((self.bits_per_sample // 8) * self.channels) self.total_pcm_frames = (data_size // self.bytes_per_pcm_frame) self.remaining_pcm_frames = self.total_pcm_frames def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): self.close() def read(self, pcm_frames): # try to read requested PCM frames or remaining frames requested_pcm_frames = min(max(pcm_frames, 1), self.remaining_pcm_frames) requested_bytes = (self.bytes_per_pcm_frame * requested_pcm_frames) pcm_data = self.stream.read_bytes(requested_bytes) # raise exception if data block exhausted early if len(pcm_data) < requested_bytes: from audiotools.text import ERR_AU_TRUNCATED_DATA raise IOError(ERR_AU_TRUNCATED_DATA) else: self.remaining_pcm_frames -= requested_pcm_frames # return parsed chunk return FrameList(pcm_data, self.channels, self.bits_per_sample, True, True) def read_closed(self, pcm_frames): raise ValueError("cannot read closed stream") def seek(self, pcm_frame_offset): if pcm_frame_offset < 0: from audiotools.text import ERR_NEGATIVE_SEEK raise ValueError(ERR_NEGATIVE_SEEK) # ensure one doesn't walk off the end of the file pcm_frame_offset = min(pcm_frame_offset, self.total_pcm_frames) # position file in data block self.stream.seek(self.data_offset + (pcm_frame_offset * self.bytes_per_pcm_frame), 0) self.remaining_pcm_frames = (self.total_pcm_frames - pcm_frame_offset) return pcm_frame_offset def seek_closed(self, pcm_frame_offset): raise ValueError("cannot seek closed stream") def close(self): self.stream.close() self.read = self.read_closed self.seek = self.seek_closed
class ALACDecoder: def __init__(self, filename): self.reader = BitstreamReader(open(filename, "rb"), 0) self.reader.mark() try: #locate the "alac" atom #which is full of required decoding parameters try: stsd = self.find_sub_atom("moov", "trak", "mdia", "minf", "stbl", "stsd") except KeyError: raise ValueError("required stsd atom not found") (stsd_version, descriptions) = stsd.parse("8u 24p 32u") (alac1, alac2, self.samples_per_frame, self.bits_per_sample, self.history_multiplier, self.initial_history, self.maximum_k, self.channels, self.sample_rate) = stsd.parse( #ignore much of the stuff in the "high" ALAC atom "32p 4b 6P 16p 16p 16p 4P 16p 16p 16p 16p 4P" + #and use the attributes in the "low" ALAC atom instead "32p 4b 4P 32u 8p 8u 8u 8u 8u 8u 16p 32p 32p 32u") self.channel_mask = {1: 0x0004, 2: 0x0003, 3: 0x0007, 4: 0x0107, 5: 0x0037, 6: 0x003F, 7: 0x013F, 8: 0x00FF}.get(self.channels, 0) if ((alac1 != 'alac') or (alac2 != 'alac')): raise ValueError("Invalid alac atom") #also locate the "mdhd" atom #which contains the stream's length in PCM frames self.reader.rewind() mdhd = self.find_sub_atom("moov", "trak", "mdia", "mdhd") (version, ) = mdhd.parse("8u 24p") if (version == 0): (self.total_pcm_frames,) = mdhd.parse( "32p 32p 32p 32u 2P 16p") elif (version == 1): (self.total_pcm_frames,) = mdhd.parse( "64p 64p 32p 64U 2P 16p") else: raise ValueError("invalid mdhd version") #finally, set our stream to the "mdat" atom self.reader.rewind() (atom_size, atom_name) = self.reader.parse("32u 4b") while (atom_name != "mdat"): self.reader.skip_bytes(atom_size - 8) (atom_size, atom_name) = self.reader.parse("32u 4b") finally: self.reader.unmark() def find_sub_atom(self, *atom_names): reader = self.reader for (last, next_atom) in iter_last(iter(atom_names)): try: (length, stream_atom) = reader.parse("32u 4b") while (stream_atom != next_atom): reader.skip_bytes(length - 8) (length, stream_atom) = reader.parse("32u 4b") if (last): return reader.substream(length - 8) else: reader = reader.substream(length - 8) except IOError: raise KeyError(next_atom) def read(self, pcm_frames): #if the stream is exhausted, return an empty pcm.FrameList object if (self.total_pcm_frames == 0): return from_list([], self.channels, self.bits_per_sample, True) #otherwise, read one ALAC frameset's worth of frame data frameset_data = [] frame_channels = self.reader.read(3) + 1 while (frame_channels != 0x8): frameset_data.extend(self.read_frame(frame_channels)) frame_channels = self.reader.read(3) + 1 self.reader.byte_align() #reorder the frameset to Wave order, depending on channel count if ((self.channels == 1) or (self.channels == 2)): pass elif (self.channels == 3): frameset_data = [frameset_data[1], frameset_data[2], frameset_data[0]] elif (self.channels == 4): frameset_data = [frameset_data[1], frameset_data[2], frameset_data[0], frameset_data[3]] elif (self.channels == 5): frameset_data = [frameset_data[1], frameset_data[2], frameset_data[0], frameset_data[3], frameset_data[4]] elif (self.channels == 6): frameset_data = [frameset_data[1], frameset_data[2], frameset_data[0], frameset_data[5], frameset_data[3], frameset_data[4]] elif (self.channels == 7): frameset_data = [frameset_data[1], frameset_data[2], frameset_data[0], frameset_data[6], frameset_data[3], frameset_data[4], frameset_data[5]] elif (self.channels == 8): frameset_data = [frameset_data[3], frameset_data[4], frameset_data[0], frameset_data[7], frameset_data[5], frameset_data[6], frameset_data[1], frameset_data[2]] else: raise ValueError("unsupported channel count") framelist = from_channels([from_list(channel, 1, self.bits_per_sample, True) for channel in frameset_data]) #deduct PCM frames from remainder self.total_pcm_frames -= framelist.frames #return samples as a pcm.FrameList object return framelist def read_frame(self, channel_count): """returns a list of PCM sample lists, one per channel""" #read the ALAC frame header self.reader.skip(16) has_sample_count = self.reader.read(1) uncompressed_lsb_size = self.reader.read(2) uncompressed = self.reader.read(1) if (has_sample_count): sample_count = self.reader.read(32) else: sample_count = self.samples_per_frame if (uncompressed == 1): #if the frame is uncompressed, #read the raw, interlaced samples samples = [self.reader.read_signed(self.bits_per_sample) for i in xrange(sample_count * channel_count)] return [samples[i::channel_count] for i in xrange(channel_count)] else: #if the frame is compressed, #read the interlacing parameters interlacing_shift = self.reader.read(8) interlacing_leftweight = self.reader.read(8) #subframe headers subframe_headers = [self.read_subframe_header() for i in xrange(channel_count)] #optional uncompressed LSB values if (uncompressed_lsb_size > 0): uncompressed_lsbs = [ self.reader.read(uncompressed_lsb_size * 8) for i in xrange(sample_count * channel_count)] else: uncompressed_lsbs = [] sample_size = (self.bits_per_sample - (uncompressed_lsb_size * 8) + channel_count - 1) #and residual blocks residual_blocks = [self.read_residuals(sample_size, sample_count) for i in xrange(channel_count)] #calculate subframe samples based on #subframe header's QLP coefficients and QLP shift-needed decoded_subframes = [self.decode_subframe(header[0], header[1], sample_size, residuals) for (header, residuals) in zip(subframe_headers, residual_blocks)] #decorrelate channels according interlacing shift and leftweight decorrelated_channels = self.decorrelate_channels( decoded_subframes, interlacing_shift, interlacing_leftweight) #if uncompressed LSB values are present, #prepend them to each sample of each channel if (uncompressed_lsb_size > 0): channels = [] for (i, channel) in enumerate(decorrelated_channels): assert(len(channel) == len(uncompressed_lsbs[i::channel_count])) channels.append([s << (uncompressed_lsb_size * 8) | l for (s, l) in zip(channel, uncompressed_lsbs[i::channel_count])]) return channels else: return decorrelated_channels def read_subframe_header(self): prediction_type = self.reader.read(4) qlp_shift_needed = self.reader.read(4) rice_modifier = self.reader.read(3) qlp_coefficients = [self.reader.read_signed(16) for i in xrange(self.reader.read(5))] return (qlp_shift_needed, qlp_coefficients) def read_residuals(self, sample_size, sample_count): residuals = [] history = self.initial_history sign_modifier = 0 i = 0 while (i < sample_count): #get an unsigned residual based on "history" #and on "sample_size" as a lst resort k = min(log2(history / (2 ** 9) + 3), self.maximum_k) unsigned = self.read_residual(k, sample_size) + sign_modifier #clear out old sign modifier, if any sign_modifier = 0 #change unsigned residual to signed residual if (unsigned & 1): residuals.append(-((unsigned + 1) / 2)) else: residuals.append(unsigned / 2) #update history based on unsigned residual if (unsigned <= 0xFFFF): history += ((unsigned * self.history_multiplier) - ((history * self.history_multiplier) >> 9)) else: history = 0xFFFF #if history gets too small, we may have a block of 0 samples #which can be compressed more efficiently if ((history < 128) and ((i + 1) < sample_count)): zeroes_k = min(7 - log2(history) + ((history + 16) / 64), self.maximum_k) zero_residuals = self.read_residual(zeroes_k, 16) if (zero_residuals > 0): residuals.extend([0] * zero_residuals) i += zero_residuals history = 0 if (zero_residuals <= 0xFFFF): sign_modifier = 1 i += 1 return residuals def read_residual(self, k, sample_size): msb = self.reader.limited_unary(0, 9) if (msb is None): return self.reader.read(sample_size) elif (k == 0): return msb else: lsb = self.reader.read(k) if (lsb > 1): return msb * ((1 << k) - 1) + (lsb - 1) elif (lsb == 1): self.reader.unread(1) return msb * ((1 << k) - 1) else: self.reader.unread(0) return msb * ((1 << k) - 1) def decode_subframe(self, qlp_shift_needed, qlp_coefficients, sample_size, residuals): #first sample is always copied verbatim samples = [residuals.pop(0)] if (len(qlp_coefficients) < 31): #the next "coefficient count" samples #are applied as differences to the previous for i in xrange(len(qlp_coefficients)): samples.append(truncate_bits(samples[-1] + residuals.pop(0), sample_size)) #remaining samples are processed much like LPC for residual in residuals: base_sample = samples[-len(qlp_coefficients) - 1] lpc_sum = sum([(s - base_sample) * c for (s, c) in zip(samples[-len(qlp_coefficients):], reversed(qlp_coefficients))]) outval = (1 << (qlp_shift_needed - 1)) + lpc_sum outval >>= qlp_shift_needed samples.append(truncate_bits(outval + residual + base_sample, sample_size)) buf = samples[-len(qlp_coefficients) - 2:-1] #error value then adjusts the coefficients table if (residual > 0): predictor_num = len(qlp_coefficients) - 1 while ((predictor_num >= 0) and residual > 0): val = (buf[0] - buf[len(qlp_coefficients) - predictor_num]) sign = sign_only(val) qlp_coefficients[predictor_num] -= sign val *= sign residual -= ((val >> qlp_shift_needed) * (len(qlp_coefficients) - predictor_num)) predictor_num -= 1 elif (residual < 0): #the same as above, but we break if residual goes positive predictor_num = len(qlp_coefficients) - 1 while ((predictor_num >= 0) and residual < 0): val = (buf[0] - buf[len(qlp_coefficients) - predictor_num]) sign = -sign_only(val) qlp_coefficients[predictor_num] -= sign val *= sign residual -= ((val >> qlp_shift_needed) * (len(qlp_coefficients) - predictor_num)) predictor_num -= 1 else: #residuals are encoded as simple difference values for residual in residuals: samples.append(truncate_bits(samples[-1] + residual, sample_size)) return samples def decorrelate_channels(self, channel_data, interlacing_shift, interlacing_leftweight): if (len(channel_data) != 2): return channel_data elif (interlacing_leftweight == 0): return channel_data else: left = [] right = [] for (ch1, ch2) in zip(*channel_data): right.append(ch1 - ((ch2 * interlacing_leftweight) / (2 ** interlacing_shift))) left.append(ch2 + right[-1]) return [left, right] def close(self): pass
def __titles__(self, titleset): """returns a list of DVDATitle objects for the given titleset""" # this requires bouncing all over the ATS_XX_0.IFO file import os from audiotools.bitstream import BitstreamReader try: f = open(self.files['ATS_%2.2d_0.IFO' % (titleset)], 'rb') except (KeyError, IOError): from audiotools.text import ERR_DVDA_IOERROR_ATS raise InvalidDVDA(ERR_DVDA_IOERROR_ATS % (titleset)) try: # ensure the file's identifier is correct # which is all we care about from the first sector if (f.read(12) != 'DVDAUDIO-ATS'): from audiotools.text import ERR_DVDA_INVALID_ATS raise InvalidDVDA(ERR_DVDA_INVALID_ATS % (titleset)) # seek to the second sector and read the title count # and list of title table offset values f.seek(DVDAudio.SECTOR_SIZE, os.SEEK_SET) ats_reader = BitstreamReader(f, 0) (title_count, last_byte_address) = ats_reader.parse("16u 16p 32u") title_offsets = [ats_reader.parse("8u 24p 32u")[1] for title in range(title_count)] titles = [] for (title_number, title_offset) in enumerate(title_offsets): # for each title, seek to its title table # and read the title's values and its track timestamps f.seek(DVDAudio.SECTOR_SIZE + title_offset, os.SEEK_SET) ats_reader = BitstreamReader(f, 0) (tracks, indexes, track_length, sector_pointers_table) = ats_reader.parse( "16p 8u 8u 32u 4P 16u 2P") timestamps = [ats_reader.parse("32p 8u 8p 32u 32u 48p") for track in range(tracks)] # seek to the title's sector pointers table # and read the first and last sector values for title's tracks f.seek(DVDAudio.SECTOR_SIZE + title_offset + sector_pointers_table, os.SEEK_SET) ats_reader = BitstreamReader(f, 0) sector_pointers = [ats_reader.parse("32u 32u 32u") for i in range(indexes)] if ((len(sector_pointers) > 1) and ({p[0] for p in sector_pointers[1:]} != {0x01000000})): from audiotools.text import ERR_DVDA_INVALID_SECTOR_POINTER raise InvalidDVDA(ERR_DVDA_INVALID_SECTOR_POINTER) else: sector_pointers = [None] + sector_pointers # build a preliminary DVDATitle object # which we'll populate with track data dvda_title = DVDATitle(dvdaudio=self, titleset=titleset, title=title_number + 1, pts_length=track_length, tracks=[]) # for each track, determine its first and last sector # based on the sector pointers between the track's # initial index and the next track's initial index for (track_number, (timestamp, next_timestamp)) in enumerate(zip(timestamps, timestamps[1:])): (index_number, first_pts, pts_length) = timestamp next_timestamp_index = next_timestamp[0] dvda_title.tracks.append( DVDATrack( dvdaudio=self, titleset=titleset, title=dvda_title, track=track_number + 1, first_pts=first_pts, pts_length=pts_length, first_sector=sector_pointers[index_number][1], last_sector=sector_pointers[ next_timestamp_index - 1][2])) # for the last track, its sector pointers # simply consume what remains on the list (index_number, first_pts, pts_length) = timestamps[-1] dvda_title.tracks.append( DVDATrack( dvdaudio=self, titleset=titleset, title=dvda_title, track=len(timestamps), first_pts=first_pts, pts_length=pts_length, first_sector=sector_pointers[index_number][1], last_sector=sector_pointers[-1][2])) # fill in the title's info such as sample_rate, channels, etc. dvda_title.__parse_info__() titles.append(dvda_title) return titles finally: f.close()
def __read_info__(self): from audiotools.bitstream import BitstreamReader from audiotools import ChannelMask reader = BitstreamReader(open(self.filename, "rb"), 1) reader.mark() try: (block_id, total_samples, bits_per_sample, mono_output, initial_block, final_block, sample_rate) = reader.parse( "4b 64p 32u 64p 2u 1u 8p 1u 1u 5p 5p 4u 37p") if (block_id != b"wvpk"): from audiotools.text import ERR_WAVPACK_INVALID_HEADER raise InvalidWavPack(ERR_WAVPACK_INVALID_HEADER) if (sample_rate != 0xF): self.__samplerate__ = WavPackAudio.SAMPLING_RATE[sample_rate] else: # if unknown, pull from SAMPLE_RATE sub-block for (block_id, nondecoder, data_size, data) in self.sub_blocks(reader): if ((block_id == 0x7) and nondecoder): self.__samplerate__ = data.read(data_size * 8) break else: # no SAMPLE RATE sub-block found # so pull info from FMT chunk reader.rewind() (self.__samplerate__,) = self.fmt_chunk(reader).parse( "32p 32u") self.__bitspersample__ = [8, 16, 24, 32][bits_per_sample] self.__total_frames__ = total_samples if (initial_block and final_block): if (mono_output): self.__channels__ = 1 self.__channel_mask__ = ChannelMask(0x4) else: self.__channels__ = 2 self.__channel_mask__ = ChannelMask(0x3) else: # if not mono or stereo, pull from CHANNEL INFO sub-block reader.rewind() for (block_id, nondecoder, data_size, data) in self.sub_blocks(reader): if ((block_id == 0xD) and not nondecoder): self.__channels__ = data.read(8) self.__channel_mask__ = ChannelMask( data.read((data_size - 1) * 8)) break else: # no CHANNEL INFO sub-block found # so pull info from FMT chunk reader.rewind() fmt = self.fmt_chunk(reader) compression_code = fmt.read(16) self.__channels__ = fmt.read(16) if (compression_code == 1): # this is theoretically possible # with very old .wav files, # but shouldn't happen in practice self.__channel_mask__ = \ {1: ChannelMask.from_fields(front_center=True), 2: ChannelMask.from_fields(front_left=True, front_right=True), 3: ChannelMask.from_fields(front_left=True, front_right=True, front_center=True), 4: ChannelMask.from_fields(front_left=True, front_right=True, back_left=True, back_right=True), 5: ChannelMask.from_fields(front_left=True, front_right=True, back_left=True, back_right=True, front_center=True), 6: ChannelMask.from_fields(front_left=True, front_right=True, back_left=True, back_right=True, front_center=True, low_frequency=True) }.get(self.__channels__, ChannelMask(0)) elif (compression_code == 0xFFFE): fmt.skip(128) mask = fmt.read(32) self.__channel_mask__ = ChannelMask(mask) else: from audiotools.text import ERR_WAVPACK_UNSUPPORTED_FMT raise InvalidWavPack(ERR_WAVPACK_UNSUPPORTED_FMT) finally: reader.unmark() reader.close()
def __parse_info__(self): """generates a cache of sample_rate, bits-per-sample, etc""" import re import os.path from audiotools.bitstream import BitstreamReader if (len(self.tracks) == 0): return # Why is this post-init processing necessary? # DVDATrack references DVDATitle # so a DVDATitle must exist when DVDATrack is initialized. # But because reading this info requires knowing the sector # of the first track, we wind up with a circular dependency. # Doing a "post-process" pass fixes that problem. # find the AOB file of the title's first track track_sector = self[0].first_sector titleset = re.compile("ATS_%2.2d_\\d\\.AOB" % (self.titleset)) for aob_path in sorted([self.dvdaudio.files[key] for key in self.dvdaudio.files.keys() if (titleset.match(key))]): aob_sectors = os.path.getsize(aob_path) // DVDAudio.SECTOR_SIZE if (track_sector > aob_sectors): track_sector -= aob_sectors else: break else: from audiotools.text import ERR_DVDA_NO_TRACK_SECTOR raise ValueError(ERR_DVDA_NO_TRACK_SECTOR) # open that AOB file and seek to that track's first sector aob_file = open(aob_path, 'rb') try: aob_file.seek(track_sector * DVDAudio.SECTOR_SIZE) aob_reader = BitstreamReader(aob_file, 0) # read and validate the pack header # (there's one pack header per sector, at the sector's start) (sync_bytes, marker1, current_pts_high, marker2, current_pts_mid, marker3, current_pts_low, marker4, scr_extension, marker5, bit_rate, marker6, stuffing_length) = aob_reader.parse( "32u 2u 3u 1u 15u 1u 15u 1u 9u 1u 22u 2u 5p 3u") aob_reader.skip_bytes(stuffing_length) if (sync_bytes != 0x1BA): from audiotools.text import ERR_DVDA_INVALID_AOB_SYNC raise InvalidDVDA(ERR_DVDA_INVALID_AOB_SYNC) if (((marker1 != 1) or (marker2 != 1) or (marker3 != 1) or (marker4 != 1) or (marker5 != 1) or (marker6 != 3))): from audiotools.text import ERR_DVDA_INVALID_AOB_MARKER raise InvalidDVDA(ERR_DVDA_INVALID_AOB_MARKER) packet_pts = ((current_pts_high << 30) | (current_pts_mid << 15) | current_pts_low) # skip packets until one with a stream ID of 0xBD is found (start_code, stream_id, packet_length) = aob_reader.parse("24u 8u 16u") if (start_code != 1): from audiotools.text import ERR_DVDA_INVALID_AOB_START raise InvalidDVDA(ERR_DVDA_INVALID_AOB_START) while (stream_id != 0xBD): aob_reader.skip_bytes(packet_length) (start_code, stream_id, packet_length) = aob_reader.parse("24u 8u 16u") if (start_code != 1): from audiotools.text import ERR_DVDA_INVALID_AOB_START raise InvalidDVDA(ERR_DVDA_INVALID_AOB_START) # parse the PCM/MLP header in the packet data (pad1_size,) = aob_reader.parse("16p 8u") aob_reader.skip_bytes(pad1_size) (stream_id, crc) = aob_reader.parse("8u 8u 8p") if (stream_id == 0xA0): # PCM # read a PCM reader (pad2_size, first_audio_frame, padding2, group1_bps, group2_bps, group1_sample_rate, group2_sample_rate, padding3, channel_assignment) = aob_reader.parse( "8u 16u 8u 4u 4u 4u 4u 8u 8u") else: # MLP aob_reader.skip_bytes(aob_reader.read(8)) # skip pad2 # read a total frame size + MLP major sync header (total_frame_size, sync_words, stream_type, group1_bps, group2_bps, group1_sample_rate, group2_sample_rate, unknown1, channel_assignment, unknown2) = aob_reader.parse( "4p 12u 16p 24u 8u 4u 4u 4u 4u 11u 5u 48u") # return the values indicated by the header self.sample_rate = DVDATrack.SAMPLE_RATE[group1_sample_rate] self.channels = DVDATrack.CHANNELS[channel_assignment] self.channel_mask = DVDATrack.CHANNEL_MASK[channel_assignment] self.bits_per_sample = DVDATrack.BITS_PER_SAMPLE[group1_bps] self.stream_id = stream_id finally: aob_file.close()
def __init__(self, filename): """filename is a plain string""" AudioFile.__init__(self, filename) self.__channels__ = 0 self.__channel_mask__ = 0 #get channel count and channel mask from first packet from .bitstream import BitstreamReader try: f = open(filename, "rb") try: ogg_reader = BitstreamReader(f, 1) (magic_number, version, header_type, granule_position, self.__serial_number__, page_sequence_number, checksum, segment_count) = ogg_reader.parse( "4b 8u 8u 64S 32u 32u 32u 8u") if (magic_number != 'OggS'): from .text import ERR_OGG_INVALID_MAGIC_NUMBER raise InvalidFLAC(ERR_OGG_INVALID_MAGIC_NUMBER) if (version != 0): from .text import ERR_OGG_INVALID_VERSION raise InvalidFLAC(ERR_OGG_INVALID_VERSION) segment_length = ogg_reader.read(8) (opushead, version, self.__channels__, pre_skip, input_sample_rate, output_gain, mapping_family) = ogg_reader.parse( "8b 8u 8u 16u 32u 16s 8u") if (opushead != "OpusHead"): from .text import ERR_OPUS_INVALID_TYPE raise InvalidOpus(ERR_OPUS_INVALID_TYPE) if (version != 1): from .text import ERR_OPUS_INVALID_VERSION raise InvalidOpus(ERR_OPUS_INVALID_VERSION) if (self.__channels__ == 0): from .text import ERR_OPUS_INVALID_CHANNELS raise InvalidOpus(ERR_OPUS_INVALID_CHANNELS) #FIXME - assign channel mask from mapping family if (mapping_family == 0): if (self.__channels__ == 1): self.__channel_mask__ = VorbisChannelMask(0x4) elif (self.__channels__ == 2): self.__channel_mask__ = VorbisChannelMask(0x3) else: self.__channel_mask__ = VorbisChannelMask(0) else: (stream_count, coupled_stream_count) = ogg_reader.parse("8u 8u") if (self.__channels__ != ((coupled_stream_count * 2) + (stream_count - coupled_stream_count))): from .text import ERR_OPUS_INVALID_CHANNELS raise InvalidOpus(ERR_OPUS_INVALID_CHANNELS) channel_mapping = [ogg_reader.read(8) for i in xrange(self.__channels__)] finally: f.close() except IOError, msg: raise InvalidOpus(str(msg))