class AiffAudio(AudioFile): """An AIFF audio file.""" SUFFIX = "aiff" NAME = SUFFIX AIFF_HEADER = Con.Struct("aiff_header", Con.Const(Con.Bytes("aiff_id", 4), "FORM"), Con.UBInt32("aiff_size"), Con.Const(Con.Bytes("aiff_type", 4), "AIFF")) CHUNK_HEADER = Con.Struct("chunk_header", Con.Bytes("chunk_id", 4), Con.UBInt32("chunk_length")) COMM_CHUNK = Con.Struct("comm", Con.UBInt16("channels"), Con.UBInt32("total_sample_frames"), Con.UBInt16("sample_size"), IEEE_Extended("sample_rate")) SSND_ALIGN = Con.Struct("ssnd", Con.UBInt32("offset"), Con.UBInt32("blocksize")) PRINTABLE_ASCII = set([chr(i) for i in xrange(0x20, 0x7E + 1)]) def __init__(self, filename): """filename is a plain string.""" self.filename = filename comm_found = False ssnd_found = False try: f = open(self.filename, 'rb') for (chunk_id, chunk_length, chunk_offset) in self.chunks(): if (chunk_id == 'COMM'): f.seek(chunk_offset, 0) comm = self.COMM_CHUNK.parse(f.read(chunk_length)) self.__channels__ = comm.channels self.__total_sample_frames__ = comm.total_sample_frames self.__sample_size__ = comm.sample_size self.__sample_rate__ = int(comm.sample_rate) comm_found = True elif (chunk_id == 'SSND'): f.seek(chunk_offset, 0) ssnd = self.SSND_ALIGN.parse_stream(f) ssnd_found = True elif (not set(chunk_id).issubset(self.PRINTABLE_ASCII)): raise InvalidAIFF(_("chunk header not ASCII")) if (not comm_found): raise InvalidAIFF(_("no COMM chunk found")) if (not ssnd_found): raise InvalidAIFF(_("no SSND chunk found")) f.close() except IOError, msg: raise InvalidAIFF(str(msg)) except Con.FieldError: raise InvalidAIFF(_("invalid COMM or SSND chunk"))
def b_tag_value(cls, file, tag): subtype = {1: Con.Byte("data"), 2: Con.CString("data"), 3: Con.UBInt16("data"), 4: Con.UBInt32("data"), 5: Con.Struct("data", Con.UBInt32("high"), Con.UBInt32("low"))}[tag.type] data = Con.StrictRepeater(tag.count, subtype) if ((tag.type != 2) and (data.sizeof() <= 4)): return tag.offset else: file.seek(tag.offset, 0) return data.parse_stream(file)
class AuAudio(AudioFile): """A Sun AU audio file.""" SUFFIX = "au" NAME = SUFFIX AU_HEADER = Con.Struct('header', Con.Const(Con.String('magic_number', 4), '.snd'), Con.UBInt32('data_offset'), Con.UBInt32('data_size'), Con.UBInt32('encoding_format'), Con.UBInt32('sample_rate'), Con.UBInt32('channels')) def __init__(self, filename): AudioFile.__init__(self, filename) try: f = file(filename, 'rb') except IOError, msg: raise InvalidAU(str(msg)) try: header = AuAudio.AU_HEADER.parse_stream(f) if (header.encoding_format not in (2, 3, 4)): raise InvalidFile(_(u"Unsupported Sun AU encoding format")) self.__bits_per_sample__ = { 2: 8, 3: 16, 4: 24 }[header.encoding_format] self.__channels__ = header.channels self.__sample_rate__ = header.sample_rate self.__total_frames__ = header.data_size / \ (self.__bits_per_sample__ / 8) / \ self.__channels__ self.__data_offset__ = header.data_offset self.__data_size__ = header.data_size except Con.ConstError: raise InvalidFile(_(u"Invalid Sun AU header")) except Con.FieldError: raise InvalidAU(_(u"Invalid Sun AU header"))
class MusepackAudio(ApeTaggedAudio, AudioFile): """A Musepack audio file.""" SUFFIX = "mpc" NAME = SUFFIX DEFAULT_COMPRESSION = "standard" COMPRESSION_MODES = ("thumb", "radio", "standard", "extreme", "insane") ###Musepack SV7### #BINARIES = ('mppdec','mppenc') ###Musepack SV8### BINARIES = ('mpcdec', 'mpcenc') MUSEPACK8_HEADER = Con.Struct('musepack8_header', Con.UBInt32('crc32'), Con.Byte('bitstream_version'), NutValue('sample_count'), NutValue('beginning_silence'), Con.Embed(Con.BitStruct( 'flags', Con.Bits('sample_frequency', 3), Con.Bits('max_used_bands', 5), Con.Bits('channel_count', 4), Con.Flag('mid_side_used'), Con.Bits('audio_block_frames', 3)))) #not sure about some of the flag locations #Musepack 7's header is very unusual MUSEPACK7_HEADER = Con.Struct('musepack7_header', Con.Const(Con.String('signature', 3), 'MP+'), Con.Byte('version'), Con.ULInt32('frame_count'), Con.ULInt16('max_level'), Con.Embed( Con.BitStruct('flags', Con.Bits('profile', 4), Con.Bits('link', 2), Con.Bits('sample_frequency', 2), Con.Flag('intensity_stereo'), Con.Flag('midside_stereo'), Con.Bits('maxband', 6))), Con.ULInt16('title_gain'), Con.ULInt16('title_peak'), Con.ULInt16('album_gain'), Con.ULInt16('album_peak'), Con.Embed( Con.BitStruct('more_flags', Con.Bits('unused1', 16), Con.Bits('last_frame_length_low', 4), Con.Flag('true_gapless'), Con.Bits('unused2', 3), Con.Flag('fast_seeking'), Con.Bits('last_frame_length_high', 7))), Con.Bytes('unknown', 3), Con.Byte('encoder_version')) def __init__(self, filename): """filename is a plain string.""" AudioFile.__init__(self, filename) f = file(filename, 'rb') try: if (f.read(4) == 'MPCK'): # a Musepack 8 stream for (key, packet) in Musepack8StreamReader(f).packets(): if (key == 'SH'): header = MusepackAudio.MUSEPACK8_HEADER.parse(packet) self.__sample_rate__ = (44100, 48000, 37800, 32000)[ header.sample_frequency] self.__total_frames__ = header.sample_count self.__channels__ = header.channel_count + 1 break elif (key == 'SE'): raise InvalidFile(_(u'No Musepack header found')) else: # a Musepack 7 stream f.seek(0, 0) try: header = MusepackAudio.MUSEPACK7_HEADER.parse_stream(f) except Con.ConstError: raise InvalidFile(_(u'Musepack signature incorrect')) header.last_frame_length = \ (header.last_frame_length_high << 4) | \ header.last_frame_length_low self.__sample_rate__ = (44100, 48000, 37800, 32000)[header.sample_frequency] self.__total_frames__ = (((header.frame_count - 1) * 1152) + header.last_frame_length) self.__channels__ = 2 finally: f.close() @classmethod def from_pcm(cls, filename, pcmreader, compression=None): """Encodes a new file from PCM data. Takes a filename string, PCMReader object and optional compression level string. Encodes a new audio file from pcmreader's data at the given filename with the specified compression level and returns a new MusepackAudio object.""" import tempfile import bisect if (str(compression) not in cls.COMPRESSION_MODES): compression = cls.DEFAULT_COMPRESSION if ((pcmreader.channels > 2) or (pcmreader.sample_rate not in (44100, 48000, 37800, 32000)) or (pcmreader.bits_per_sample != 16)): pcmreader = PCMConverter( pcmreader, sample_rate=[32000, 32000, 37800, 44100, 48000][bisect.bisect( [32000, 37800, 44100, 48000], pcmreader.sample_rate)], channels=min(pcmreader.channels, 2), bits_per_sample=16) f = tempfile.NamedTemporaryFile(suffix=".wav") w = WaveAudio.from_pcm(f.name, pcmreader) try: return cls.__from_wave__(filename, f.name, compression) finally: del(w) f.close() #While Musepack needs to pipe things through WAVE, #not all WAVEs are acceptable. #Use the *_pcm() methods first. def __to_wave__(self, wave_filename): devnull = file(os.devnull, "wb") try: sub = subprocess.Popen([BIN['mpcdec'], self.filename, wave_filename], stdout=devnull, stderr=devnull) #FIXME - small files (~5 seconds) result in an error by mpcdec, #even if they decode correctly. #Not much we can do except try to workaround its bugs. if (sub.wait() not in [0, 250]): raise DecodingError() finally: devnull.close() @classmethod def __from_wave__(cls, filename, wave_filename, compression=None): if (str(compression) not in cls.COMPRESSION_MODES): compression = cls.DEFAULT_COMPRESSION #mppenc requires files to end with .mpc for some reason if (not filename.endswith(".mpc")): import tempfile actual_filename = filename tempfile = tempfile.NamedTemporaryFile(suffix=".mpc") filename = tempfile.name else: actual_filename = tempfile = None ###Musepack SV7### #sub = subprocess.Popen([BIN['mppenc'], # "--silent", # "--overwrite", # "--%s" % (compression), # wave_filename, # filename], # preexec_fn=ignore_sigint) ###Musepack SV8### sub = subprocess.Popen([BIN['mpcenc'], "--silent", "--overwrite", "--%s" % (compression), wave_filename, filename]) if (sub.wait() == 0): if (tempfile is not None): filename = actual_filename f = file(filename, 'wb') tempfile.seek(0, 0) transfer_data(tempfile.read, f.write) f.close() tempfile.close() return MusepackAudio(filename) else: if (tempfile is not None): tempfile.close() raise EncodingError(u"error encoding file with mpcenc") @classmethod def is_type(cls, file): """Returns True if the given file object describes this format. Takes a seekable file pointer rewound to the start of the file.""" header = file.read(4) ###Musepack SV7### #return header == 'MP+\x07' ###Musepack SV8### return (header == 'MP+\x07') or (header == 'MPCK') def sample_rate(self): """Returns the rate of the track's audio as an integer number of Hz.""" return self.__sample_rate__ def total_frames(self): """Returns the total PCM frames of the track as an integer.""" return self.__total_frames__ def channels(self): """Returns an integer number of channels this track contains.""" return self.__channels__ def bits_per_sample(self): """Returns an integer number of bits-per-sample this track contains.""" return 16 def lossless(self): """Returns False.""" return False
def Atom(name): """A basic QuickTime atom struct.""" return AtomAdapter( Con.Struct(name, Con.UBInt32("size"), Con.String("type", 4), Con.String("data", lambda ctx: ctx["size"] - 8)))
def VersionLength(name): """A struct for 32 or 64 bit fields, depending on version field.""" return Con.IfThenElse(name, lambda ctx: ctx["version"] == 0, Con.UBInt32(None), Con.UBInt64(None))
def __init__(self, atom_name, sub_atom): Con.Struct.__init__(self, atom_name) self.atom_name = atom_name self.sub_atom = sub_atom self.header = Con.Struct(atom_name, Con.UBInt32("size"), Con.Const(Con.String("type", 4), atom_name))
header = self.header.parse_stream(stream) return self.sub_atom.parse_stream(stream) def _build(self, obj, stream, context): data = self.sub_atom.build(obj) stream.write( self.header.build( Con.Container(type=self.atom_name, size=len(data) + 8))) stream.write(data) def _sizeof(self, context): return self.sub_atom.sizeof(context) + 8 ATOM_FTYP = Con.Struct("ftyp", Con.String("major_brand", 4), Con.UBInt32("major_brand_version"), Con.GreedyRepeater(Con.String("compatible_brands", 4))) ATOM_MVHD = Con.Struct( "mvhd", Con.Byte("version"), Con.String("flags", 3), VersionLength("created_mac_UTC_date"), VersionLength("modified_mac_UTC_date"), Con.UBInt32("time_scale"), VersionLength("duration"), Con.UBInt32("playback_speed"), Con.UBInt16("user_volume"), Con.Padding(10), Con.Struct("windows", Con.UBInt32("geometry_matrix_a"), Con.UBInt32("geometry_matrix_b"), Con.UBInt32("geometry_matrix_u"), Con.UBInt32("geometry_matrix_c"), Con.UBInt32("geometry_matrix_d"), Con.UBInt32("geometry_matrix_v"), Con.UBInt32("geometry_matrix_x"),
class __TIFF__(ImageMetrics): HEADER = Con.Struct('header', Con.String('byte_order', 2), Con.Switch('order', lambda ctx: ctx['byte_order'], {"II": Con.Embed( Con.Struct('little_endian', Con.Const(Con.ULInt16('version'), 42), Con.ULInt32('offset'))), "MM": Con.Embed( Con.Struct('big_endian', Con.Const(Con.UBInt16('version'), 42), Con.UBInt32('offset')))})) L_IFD = Con.Struct('ifd', Con.PrefixedArray( length_field=Con.ULInt16('length'), subcon=Con.Struct('tags', Con.ULInt16('id'), Con.ULInt16('type'), Con.ULInt32('count'), Con.ULInt32('offset'))), Con.ULInt32('next')) B_IFD = Con.Struct('ifd', Con.PrefixedArray( length_field=Con.UBInt16('length'), subcon=Con.Struct('tags', Con.UBInt16('id'), Con.UBInt16('type'), Con.UBInt32('count'), Con.UBInt32('offset'))), Con.UBInt32('next')) def __init__(self, width, height, bits_per_pixel, color_count): ImageMetrics.__init__(self, width, height, bits_per_pixel, color_count, u'image/tiff') @classmethod def b_tag_value(cls, file, tag): subtype = {1: Con.Byte("data"), 2: Con.CString("data"), 3: Con.UBInt16("data"), 4: Con.UBInt32("data"), 5: Con.Struct("data", Con.UBInt32("high"), Con.UBInt32("low"))}[tag.type] data = Con.StrictRepeater(tag.count, subtype) if ((tag.type != 2) and (data.sizeof() <= 4)): return tag.offset else: file.seek(tag.offset, 0) return data.parse_stream(file) @classmethod def l_tag_value(cls, file, tag): subtype = {1: Con.Byte("data"), 2: Con.CString("data"), 3: Con.ULInt16("data"), 4: Con.ULInt32("data"), 5: Con.Struct("data", Con.ULInt32("high"), Con.ULInt32("low"))}[tag.type] data = Con.StrictRepeater(tag.count, subtype) if ((tag.type != 2) and (data.sizeof() <= 4)): return tag.offset else: file.seek(tag.offset, 0) return data.parse_stream(file) @classmethod def parse(cls, file): width = 0 height = 0 bits_per_sample = 0 color_count = 0 try: header = cls.HEADER.parse_stream(file) if (header.byte_order == 'II'): IFD = cls.L_IFD tag_value = cls.l_tag_value elif (header.byte_order == 'MM'): IFD = cls.B_IFD tag_value = cls.b_tag_value else: raise InvalidTIFF(_(u'Invalid byte order')) file.seek(header.offset, 0) ifd = IFD.parse_stream(file) while (True): for tag in ifd.tags: if (tag.id == 0x0100): width = tag_value(file, tag) elif (tag.id == 0x0101): height = tag_value(file, tag) elif (tag.id == 0x0102): try: bits_per_sample = sum(tag_value(file, tag)) except TypeError: bits_per_sample = tag_value(file, tag) elif (tag.id == 0x0140): color_count = tag.count / 3 else: pass if (ifd.next == 0x00): break else: file.seek(ifd.next, 0) ifd = IFD.parse_stream(file) return __TIFF__(width, height, bits_per_sample, color_count) except Con.ConstError: raise InvalidTIFF(_(u'Invalid TIFF'))
class __PNG__(ImageMetrics): HEADER = Con.Const(Con.String('header', 8), '89504e470d0a1a0a'.decode('hex')) CHUNK_HEADER = Con.Struct('chunk', Con.UBInt32('length'), Con.String('type', 4)) CHUNK_FOOTER = Con.Struct('crc32', Con.UBInt32('crc')) IHDR = Con.Struct('IHDR', Con.UBInt32('width'), Con.UBInt32('height'), Con.Byte('bit_depth'), Con.Byte('color_type'), Con.Byte('compression_method'), Con.Byte('filter_method'), Con.Byte('interlace_method')) def __init__(self, width, height, bits_per_pixel, color_count): ImageMetrics.__init__(self, width, height, bits_per_pixel, color_count, u'image/png') @classmethod def parse(cls, file): ihdr = None plte = None try: header = cls.HEADER.parse_stream(file) chunk_header = cls.CHUNK_HEADER.parse_stream(file) data = file.read(chunk_header.length) chunk_footer = cls.CHUNK_FOOTER.parse_stream(file) while (chunk_header.type != 'IEND'): if (chunk_header.type == 'IHDR'): ihdr = cls.IHDR.parse(data) elif (chunk_header.type == 'PLTE'): plte = data chunk_header = cls.CHUNK_HEADER.parse_stream(file) data = file.read(chunk_header.length) chunk_footer = cls.CHUNK_FOOTER.parse_stream(file) if (ihdr.color_type == 0): # grayscale bits_per_pixel = ihdr.bit_depth color_count = 0 elif (ihdr.color_type == 2): # RGB bits_per_pixel = ihdr.bit_depth * 3 color_count = 0 elif (ihdr.color_type == 3): # palette bits_per_pixel = 8 if ((len(plte) % 3) != 0): raise InvalidPNG(_(u'Invalid PLTE chunk length')) else: color_count = len(plte) / 3 elif (ihdr.color_type == 4): # grayscale + alpha bits_per_pixel = ihdr.bit_depth * 2 color_count = 0 elif (ihdr.color_type == 6): # RGB + alpha bits_per_pixel = ihdr.bit_depth * 4 color_count = 0 return __PNG__(ihdr.width, ihdr.height, bits_per_pixel, color_count) except Con.ConstError: raise InvalidPNG(_(u'Invalid PNG'))
class MP3Audio(AudioFile): """An MP3 audio file.""" SUFFIX = "mp3" NAME = SUFFIX DEFAULT_COMPRESSION = "2" #0 is better quality/lower compression #9 is worse quality/higher compression COMPRESSION_MODES = ("0", "1", "2", "3", "4", "5", "6", "7", "8", "9") BINARIES = ("lame",) REPLAYGAIN_BINARIES = ("mp3gain", ) #MPEG1, Layer 1 #MPEG1, Layer 2, #MPEG1, Layer 3, #MPEG2, Layer 1, #MPEG2, Layer 2, #MPEG2, Layer 3 MP3_BITRATE = ((None, None, None, None, None, None), (32, 32, 32, 32, 8, 8), (64, 48, 40, 48, 16, 16), (96, 56, 48, 56, 24, 24), (128, 64, 56, 64, 32, 32), (160, 80, 64, 80, 40, 40), (192, 96, 80, 96, 48, 48), (224, 112, 96, 112, 56, 56), (256, 128, 112, 128, 64, 64), (288, 160, 128, 144, 80, 80), (320, 192, 160, 160, 96, 96), (352, 224, 192, 176, 112, 112), (384, 256, 224, 192, 128, 128), (416, 320, 256, 224, 144, 144), (448, 384, 320, 256, 160, 160)) #MPEG1, MPEG2, MPEG2.5 MP3_SAMPLERATE = ((44100, 22050, 11025), (48000, 24000, 12000), (32000, 16000, 8000)) MP3_FRAME_HEADER = Con.BitStruct("mp3_header", Con.Const(Con.Bits("sync", 11), 0x7FF), Con.Bits("mpeg_version", 2), Con.Bits("layer", 2), Con.Flag("protection", 1), Con.Bits("bitrate", 4), Con.Bits("sampling_rate", 2), Con.Bits("padding", 1), Con.Bits("private", 1), Con.Bits("channel", 2), Con.Bits("mode_extension", 2), Con.Flag("copyright", 1), Con.Flag("original", 1), Con.Bits("emphasis", 2)) XING_HEADER = Con.Struct("xing_header", Con.Bytes("header_id", 4), Con.Bytes("flags", 4), Con.UBInt32("num_frames"), Con.UBInt32("bytes"), Con.StrictRepeater(100, Con.Byte("toc_entries")), Con.UBInt32("quality")) def __init__(self, filename): """filename is a plain string.""" AudioFile.__init__(self, filename) try: mp3file = file(filename, "rb") except IOError, msg: raise InvalidMP3(str(msg)) try: try: MP3Audio.__find_next_mp3_frame__(mp3file) except ValueError: raise InvalidMP3(_(u"MP3 frame not found")) fr = MP3Audio.MP3_FRAME_HEADER.parse(mp3file.read(4)) self.__samplerate__ = MP3Audio.__get_mp3_frame_sample_rate__(fr) self.__channels__ = MP3Audio.__get_mp3_frame_channels__(fr) self.__framelength__ = self.__length__() finally: mp3file.close()