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.stream.mark() 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 __del__(self): if (self.stream.has_mark()): self.stream.unmark() 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.rewind() 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
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.stream.mark() 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 __del__(self): if (self.stream.has_mark()): self.stream.unmark() 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.rewind() 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