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"))
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
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()
def ULInt24(name): return __24BitsLE__(Con.Bytes(name, 3))