#!/usr/bin/python

#Audio Tools, a module and set of tools for manipulating audio data
#Copyright (C) 2007-2011  Brian Langenberger

#This program is free software; you can redistribute it and/or modify
#it under the terms of the GNU General Public License as published by
#the Free Software Foundation; either version 2 of the License, or
#(at your option) any later version.

#This program is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#GNU General Public License for more details.

#You should have received a copy of the GNU General Public License
#along with this program; if not, write to the Free Software
#Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA


from audiotools import (AudioFile, InvalidFile, PCMReader, PCMConverter,
                        Con, transfer_data, transfer_framelist_data,
                        subprocess, BIN, BIG_ENDIAN, ApeTag, ReplayGain,
                        ignore_sigint, open_files, EncodingError,
                        DecodingError, PCMReaderError, ChannelMask,
                        __default_quality__, config)
from __id3__ import *
import gettext

gettext.install("audiotools", unicode=True)


#######################
#MP3
#######################


class MPEG_Frame_Header(Con.Adapter):
    #mpeg_version->sample_rate bits->Hz
    SAMPLE_RATE = [[11025, 12000, 8000,  None],
                   [None,  None,  None,  None],
                   [22050, 24000, 16000, None],
                   [44100, 48000, 32000, None]]

    #(mpeg_version, layer)->bitrate bits->bits per second
    BIT_RATE = {(3, 3): [0,      32000,  64000,  96000,
                         128000, 160000, 192000, 224000,
                         256000, 288000, 320000, 352000,
                         384000, 416000, 448000, None],
                (3, 2): [0,      32000,  48000,  56000,
                         64000,  80000,  96000,  112000,
                         128000, 160000, 192000, 224000,
                         256000, 320000, 384000, None],
                (3, 1): [0,      32000,  40000,  48000,
                         56000,  64000,  80000,  96000,
                         112000, 128000, 160000, 192000,
                         224000, 256000, 320000, None],
                (2, 3): [0,      32000,  48000,  56000,
                         64000,  80000,  96000,  112000,
                         128000, 144000, 160000, 176000,
                         192000, 224000, 256000, None],
                (2, 2): [0,      8000,   16000,  24000,
                         32000,  40000,  48000,  56000,
                         64000,  80000,  96000,  112000,
                         128000, 144000, 160000, None]}

    #mpeg_version->Hz->sample_rate bits
    SAMPLE_RATE_REVERSE = {0: {11025: 0,
                               12000: 1,
                               8000: 2},
                           1: {None: 0},
                           2: {22050: 0,
                               24000: 1,
                               16000: 2,
                               None: 3},
                           3: {44100: 0,
                               48000: 1,
                               32000: 2,
                               None: 3}}

    BIT_RATE_REVERSE = dict([(key, dict([(rate, i) for (i, rate) in
                                         enumerate(values)]))
                             for (key, values) in BIT_RATE.items()])

    def __init__(self, name):
        Con.Adapter.__init__(
            self,
            Con.BitStruct("mp3_header",
                          Con.Const(Con.Bits("sync", 11), 0x7FF),
                          Con.Bits("mpeg_version", 2),
                          Con.Bits("layer", 2),
                          Con.Flag("no_crc16", 1),
                          Con.Bits("bitrate", 4),
                          Con.Bits("sample_rate", 2),
                          Con.Bits("pad", 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)))

    def _encode(self, obj, content):
        obj.sample_rate = self.SAMPLE_RATE_REVERSE[obj.mpeg_version][
            obj.sample_rate]
        obj.bitrate = self.BIT_RATE_REVERSE[(obj.mpeg_version, obj.layer)][
            obj.bitrate]
        return obj

    def _decode(self, obj, content):
        obj.sample_rate = self.SAMPLE_RATE[obj.mpeg_version][obj.sample_rate]
        obj.channel_count = [2, 2, 2, 1][obj.channel]
        obj.bitrate = self.BIT_RATE[(obj.mpeg_version, obj.layer)][obj.bitrate]

        if (obj.layer == 3):
            obj.byte_length = (((12 * obj.bitrate) / obj.sample_rate) +
                              obj.pad) * 4
        else:
            obj.byte_length = ((144 * obj.bitrate) / obj.sample_rate) + obj.pad

        return obj


def MPEG_crc16(data, total_bits):
    def crc16_val(value, crc, total_bits):
        value <<= 8
        for i in xrange(total_bits):
            value <<= 1
            crc <<= 1

            if (((crc ^ value) & 0x10000)):
                crc ^= 0x8005

        return crc & 0xFFFF

    checksum = 0xFFFF
    data = map(ord, data)
    while (total_bits >= 8):
        checksum = crc16_val(data.pop(0), checksum, 8)
        total_bits -= 8

    if (total_bits > 0):
        return crc16_val(data.pop(0), checksum, total_bits)
    else:
        return checksum


class InvalidMP3(InvalidFile):
    """Raised by invalid files during MP3 initialization."""

    pass


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",
                         "medium", "standard", "extreme", "insane")
    COMPRESSION_DESCRIPTIONS = {"0": _(u"high quality, larger files, " +
                                       u"corresponds to lame's -V0"),
                                "6": _(u"lower quality, smaller files, " +
                                       u"corresponds to lame's -V6"),
                                "medium": _(u"corresponds to lame's " +
                                            u"--preset medium"),
                                "standard": _(u"corresponds to lame's " +
                                              u"--preset standard"),
                                "extreme": _(u"corresponds to lame's " +
                                             u"--preset extreme"),
                                "insane": _(u"corresponds to lame's " +
                                            u"--preset insane")}
    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()

    @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."""

        ID3v2Comment.skip(file)

        try:
            frame = cls.MP3_FRAME_HEADER.parse_stream(file)
            if ((frame.sync == 0x07FF) and
                (frame.mpeg_version in (0x03, 0x02, 0x00)) and
                (frame.layer in (0x01, 0x03))):
                return True
            else:
                #oddly, MP3s sometimes turn up in RIFF containers
                #this isn't a good idea, but can be supported nonetheless
                file.seek(-cls.MP3_FRAME_HEADER.sizeof(), 1)
                header = file.read(12)
                if ((header[0:4] == 'RIFF') and
                    (header[8:12] == 'RMP3')):
                    return True
                else:
                    return False
        except:
            return False

    def lossless(self):
        """Returns False."""

        return False

    def to_pcm(self):
        """Returns a PCMReader object containing the track's PCM data."""

        #if mpg123 is available, use that for decoding
        if (BIN.can_execute(BIN["mpg123"])):
            sub = subprocess.Popen([BIN["mpg123"], "-qs", self.filename],
                                   stdout=subprocess.PIPE,
                                   stderr=file(os.devnull, "a"))
            return PCMReader(sub.stdout,
                             sample_rate=self.sample_rate(),
                             channels=self.channels(),
                             bits_per_sample=16,
                             channel_mask=int(ChannelMask.from_channels(
                        self.channels())),
                             process=sub,
                             big_endian=BIG_ENDIAN)
        else:
            #if not, use LAME for decoding
            if (self.filename.endswith("." + self.SUFFIX)):
                if (BIG_ENDIAN):
                    endian = ['-x']
                else:
                    endian = []

                sub = subprocess.Popen([BIN['lame']] + endian + \
                                           ["--decode", "-t", "--quiet",
                                            self.filename, "-"],
                                       stdout=subprocess.PIPE)
                return PCMReader(
                    sub.stdout,
                    sample_rate=self.sample_rate(),
                    channels=self.channels(),
                    bits_per_sample=16,
                    channel_mask=int(self.channel_mask()),
                    process=sub)
            else:
                import tempfile
                from audiotools import TempWaveReader
                #copy our file to one that ends with .mp3
                tempmp3 = tempfile.NamedTemporaryFile(suffix='.' + self.SUFFIX)
                f = open(self.filename, 'rb')
                transfer_data(f.read, tempmp3.write)
                f.close()
                tempmp3.flush()

                #decode the mp3 file to a WAVE file
                wave = tempfile.NamedTemporaryFile(suffix='.wav')
                returnval = subprocess.call([BIN['lame'], "--decode",
                                             "--quiet",
                                             tempmp3.name, wave.name])
                tempmp3.close()

                if (returnval == 0):
                    #return WAVE file as a stream
                    wave.seek(0, 0)
                    return TempWaveReader(wave)
                else:
                    return PCMReaderError(
                        error_message=u"lame exited with error",
                        sample_rate=self.sample_rate(),
                        channels=self.channels(),
                        channel_mask=int(self.channel_mask()),
                        bits_per_sample=16)

    @classmethod
    def __help_output__(cls):
        import cStringIO
        help_data = cStringIO.StringIO()
        sub = subprocess.Popen([BIN['lame'], '--help'],
                               stdout=subprocess.PIPE)
        transfer_data(sub.stdout.read, help_data.write)
        sub.wait()
        return help_data.getvalue()

    @classmethod
    def __lame_version__(cls):
        try:
            version = re.findall(r'version \d+\.\d+',
                                 cls.__help_output__())[0]
            return tuple(map(int, version[len('version '):].split(".")))
        except IndexError:
            return (0, 0)

    @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 MP3Audio object."""

        import decimal
        import bisect

        if ((compression is None) or
            (compression not in cls.COMPRESSION_MODES)):
            compression = __default_quality__(cls.NAME)

        if ((pcmreader.channels > 2) or
            (pcmreader.sample_rate not in (32000, 48000, 44100))):
            pcmreader = PCMConverter(
                pcmreader,
                sample_rate=[32000, 32000, 44100, 48000][bisect.bisect(
                        [32000, 44100, 48000], pcmreader.sample_rate)],
                channels=min(pcmreader.channels, 2),
                channel_mask=ChannelMask.from_channels(
                    min(pcmreader.channels, 2)),
                bits_per_sample=16)

        if (pcmreader.channels > 1):
            mode = "j"
        else:
            mode = "m"

        #FIXME - not sure if all LAME versions support "--little-endian"
        # #LAME 3.98 (and up, presumably) handle the byteswap correctly
        # #LAME 3.97 always uses -x
        # if (BIG_ENDIAN or (cls.__lame_version__() < (3,98))):
        #     endian = ['-x']
        # else:
        #     endian = []

        devnull = file(os.devnull, 'ab')

        if (str(compression) in map(str, range(0, 10))):
            compression = ["-V" + str(compression)]
        else:
            compression = ["--preset", str(compression)]

        sub = subprocess.Popen([
                BIN['lame'], "--quiet",
                "-r",
                "-s", str(decimal.Decimal(pcmreader.sample_rate) / 1000),
                "--bitwidth", str(pcmreader.bits_per_sample),
                "--signed", "--little-endian",
                "-m", mode] + compression + ["-", filename],
                               stdin=subprocess.PIPE,
                               stdout=devnull,
                               stderr=devnull,
                               preexec_fn=ignore_sigint)

        try:
            transfer_framelist_data(pcmreader, sub.stdin.write)
        except (IOError, ValueError), err:
            sub.stdin.close()
            sub.wait()
            cls.__unlink__(filename)
            raise EncodingError(str(err))
        except Exception, err:
            sub.stdin.close()
            sub.wait()
            cls.__unlink__(filename)
            raise err

        try:
            pcmreader.close()
        except DecodingError, err:
            cls.__unlink__(filename)
            raise EncodingError(err.error_message)
        sub.stdin.close()

        devnull.close()

        if (sub.wait() == 0):
            return MP3Audio(filename)
        else:
            cls.__unlink__(filename)
            raise EncodingError(u"error encoding file with lame")

    def bits_per_sample(self):
        """Returns an integer number of bits-per-sample this track contains."""

        return 16

    def channels(self):
        """Returns an integer number of channels this track contains."""

        return self.__channels__

    def sample_rate(self):
        """Returns the rate of the track's audio as an integer number of Hz."""

        return self.__samplerate__

    def get_metadata(self):
        """Returns a MetaData object, or None.

        Raises IOError if unable to read the file."""

        f = file(self.filename, "rb")
        try:
            if (f.read(3) != "ID3"):      # no ID3v2 tag, try ID3v1
                id3v1 = ID3v1Comment.read_id3v1_comment(self.filename)
                if (id3v1[-1] == -1):     # no ID3v1 either
                    return None
                else:
                    return ID3v1Comment(id3v1)
            else:
                id3v2 = ID3v2Comment.read_id3v2_comment(self.filename)

                id3v1 = ID3v1Comment.read_id3v1_comment(self.filename)
                if (id3v1[-1] == -1):      # only ID3v2, no ID3v1
                    return id3v2
                else:                      # both ID3v2 and ID3v1
                    return ID3CommentPair(
                        id3v2,
                        ID3v1Comment(id3v1))
        finally:
            f.close()

    def set_metadata(self, metadata):
        """Takes a MetaData object and sets this track's metadata.

        This metadata includes track name, album name, and so on.
        Raises IOError if unable to write the file."""

        if (metadata is None):
            return

        if ((not isinstance(metadata, ID3v2Comment)) and
            (not isinstance(metadata, ID3v1Comment))):
            DEFAULT_ID3V2 = "id3v2.3"
            DEFAULT_ID3V1 = "id3v1.1"

            id3v2_class = {"id3v2.2": ID3v22Comment,
                           "id3v2.3": ID3v23Comment,
                           "id3v2.4": ID3v24Comment,
                           "none": None}.get(config.get_default("ID3",
                                                                "id3v2",
                                                                DEFAULT_ID3V2),
                                             DEFAULT_ID3V2)
            id3v1_class = {"id3v1.1": ID3v1Comment,
                           "none": None}.get(config.get_default("ID3",
                                                                "id3v1",
                                                                DEFAULT_ID3V1))
            if ((id3v2_class is not None) and (id3v1_class is not None)):
                metadata = ID3CommentPair.converted(metadata,
                                                    id3v2_class=id3v2_class,
                                                    id3v1_class=id3v1_class)
            elif (id3v2_class is not None):
                metadata = id3v2_class.converted(metadata)
            elif (id3v1_class is not None):
                metadata = id3v1_class.converted(metadata)
            else:
                return

        #get the original MP3 data
        f = file(self.filename, "rb")
        MP3Audio.__find_mp3_start__(f)
        data_start = f.tell()
        MP3Audio.__find_last_mp3_frame__(f)
        data_end = f.tell()
        f.seek(data_start, 0)
        mp3_data = f.read(data_end - data_start)
        f.close()

        if (isinstance(metadata, ID3CommentPair)):
            id3v2 = metadata.id3v2.build()
            id3v1 = metadata.id3v1.build_tag()
        elif (isinstance(metadata, ID3v2Comment)):
            id3v2 = metadata.build()
            id3v1 = ""
        elif (isinstance(metadata, ID3v1Comment)):
            id3v2 = ""
            id3v1 = metadata.build_tag()

        #write id3v2 + data + id3v1 to file
        f = file(self.filename, "wb")
        f.write(id3v2)
        f.write(mp3_data)
        f.write(id3v1)
        f.close()

    def delete_metadata(self):
        """Deletes the track's MetaData.

        This removes or unsets tags as necessary in order to remove all data.
        Raises IOError if unable to write the file."""

        #get the original MP3 data
        f = file(self.filename, "rb")
        MP3Audio.__find_mp3_start__(f)
        data_start = f.tell()
        MP3Audio.__find_last_mp3_frame__(f)
        data_end = f.tell()
        f.seek(data_start, 0)
        mp3_data = f.read(data_end - data_start)
        f.close()

        #write data to file
        f = file(self.filename, "wb")
        f.write(mp3_data)
        f.close()

    #places mp3file at the position of the next MP3 frame's start
    @classmethod
    def __find_next_mp3_frame__(cls, mp3file):
        #if we're starting at an ID3v2 header, skip it to save a bunch of time
        ID3v2Comment.skip(mp3file)

        #then find the next mp3 frame
        (b1, b2) = mp3file.read(2)
        while ((b1 != chr(0xFF)) or ((ord(b2) & 0xE0) != 0xE0)):
            mp3file.seek(-1, 1)
            (b1, b2) = mp3file.read(2)
        mp3file.seek(-2, 1)

    #places mp3file at the position of the MP3 file's start
    #either at the next frame (most commonly)
    #or at the "RIFF????RMP3" header
    @classmethod
    def __find_mp3_start__(cls, mp3file):
        #if we're starting at an ID3v2 header, skip it to save a bunch of time
        ID3v2Comment.skip(mp3file)

        while (True):
            byte = mp3file.read(1)
            while ((byte != chr(0xFF)) and (byte != 'R') and (len(byte) > 0)):
                byte = mp3file.read(1)

            if (byte == chr(0xFF)):  # possibly a frame sync
                mp3file.seek(-1, 1)
                try:
                    header = cls.MP3_FRAME_HEADER.parse_stream(mp3file)
                    if ((header.sync == 0x07FF) and
                        (header.mpeg_version in (0x03, 0x02, 0x00)) and
                        (header.layer in (0x01, 0x02, 0x03))):
                        mp3file.seek(-4, 1)
                        return
                    else:
                        mp3file.seek(-3, 1)
                except:
                    continue
            elif (byte == 'R'):     # possibly a 'RIFF????RMP3' header
                header = mp3file.read(11)
                if ((header[0:3] == 'IFF') and
                    (header[7:11] == 'RMP3')):
                    mp3file.seek(-12, 1)
                    return
                else:
                    mp3file.seek(-11, 1)
            elif (len(byte) == 0):  # we've run out of MP3 file
                return

    #places mp3file at the position of the last MP3 frame's end
    #(either the last byte in the file or just before the ID3v1 tag)
    #this may not be strictly accurate if ReplayGain data is present,
    #since APEv2 tags came before the ID3v1 tag,
    #but we're not planning to change that tag anyway
    @classmethod
    def __find_last_mp3_frame__(cls, mp3file):
        mp3file.seek(-128, 2)
        if (mp3file.read(3) == 'TAG'):
            mp3file.seek(-128, 2)
            return
        else:
            mp3file.seek(0, 2)
        return

    #header is a Construct parsed from 4 bytes sent to MP3_FRAME_HEADER
    #returns the total length of the frame, including the header
    #(subtract 4 when doing a seek or read to the next one)
    @classmethod
    def __mp3_frame_length__(cls, header):
        layer = 4 - header.layer  # layer 1, 2 or 3

        bit_rate = MP3Audio.__get_mp3_frame_bitrate__(header)
        if (bit_rate is None):
            raise InvalidMP3(_(u"Invalid bit rate"))

        sample_rate = MP3Audio.__get_mp3_frame_sample_rate__(header)

        if (layer == 1):
            return (12 * (bit_rate * 1000) / sample_rate + header.padding) * 4
        else:
            return 144 * (bit_rate * 1000) / sample_rate + header.padding

    #takes a parsed MP3_FRAME_HEADER
    #returns the mp3's sample rate based on that information
    #(typically 44100)
    @classmethod
    def __get_mp3_frame_sample_rate__(cls, frame):
        try:
            if (frame.mpeg_version == 0x00):    # MPEG 2.5
                return MP3Audio.MP3_SAMPLERATE[frame.sampling_rate][2]
            elif (frame.mpeg_version == 0x02):  # MPEG 2
                return MP3Audio.MP3_SAMPLERATE[frame.sampling_rate][1]
            else:                               # MPEG 1
                return MP3Audio.MP3_SAMPLERATE[frame.sampling_rate][0]
        except IndexError:
            raise InvalidMP3(_(u"Invalid sampling rate"))

    @classmethod
    def __get_mp3_frame_channels__(cls, frame):
        if (frame.channel == 0x03):
            return 1
        else:
            return 2

    @classmethod
    def __get_mp3_frame_bitrate__(cls, frame):
        layer = 4 - frame.layer  # layer 1, 2 or 3

        try:
            if (frame.mpeg_version == 0x00):    # MPEG 2.5
                return MP3Audio.MP3_BITRATE[frame.bitrate][layer + 2]
            elif (frame.mpeg_version == 0x02):  # MPEG 2
                return MP3Audio.MP3_BITRATE[frame.bitrate][layer + 2]
            elif (frame.mpeg_version == 0x03):  # MPEG 1
                return MP3Audio.MP3_BITRATE[frame.bitrate][layer - 1]
            else:
                return 0
        except IndexError:
            raise InvalidMP3(_(u"Invalid bit rate"))

    def cd_frames(self):
        """Returns the total length of the track in CD frames.

        Each CD frame is 1/75th of a second."""

        #calculate length at create-time so that we can
        #throw InvalidMP3 as soon as possible
        return self.__framelength__

    #returns the length of this file in CD frame
    #raises InvalidMP3 if any portion of the frame is invalid
    def __length__(self):
        mp3file = file(self.filename, "rb")

        try:
            MP3Audio.__find_next_mp3_frame__(mp3file)

            start_position = mp3file.tell()

            fr = MP3Audio.MP3_FRAME_HEADER.parse(mp3file.read(4))

            first_frame = mp3file.read(MP3Audio.__mp3_frame_length__(fr) - 4)

            sample_rate = MP3Audio.__get_mp3_frame_sample_rate__(fr)

            if (fr.mpeg_version == 0x00):    # MPEG 2.5
                version = 3
            elif (fr.mpeg_version == 0x02):  # MPEG 2
                version = 3
            else:                            # MPEG 1
                version = 0

            try:
                if (fr.layer == 0x03):    # layer 1
                    frames_per_sample = 384
                    bit_rate = MP3Audio.MP3_BITRATE[fr.bitrate][version]
                elif (fr.layer == 0x02):  # layer 2
                    frames_per_sample = 1152
                    bit_rate = MP3Audio.MP3_BITRATE[fr.bitrate][version + 1]
                elif (fr.layer == 0x01):  # layer 3
                    frames_per_sample = 1152
                    bit_rate = MP3Audio.MP3_BITRATE[fr.bitrate][version + 2]
                else:
                    raise InvalidMP3(_(u"Unsupported MPEG layer"))
            except IndexError:
                raise InvalidMP3(_(u"Invalid bit rate"))

            if ('Xing' in first_frame):
                #the first frame has a Xing header,
                #use that to calculate the mp3's length
                xing_header = MP3Audio.XING_HEADER.parse(
                    first_frame[first_frame.index('Xing'):])

                return (xing_header.num_frames * frames_per_sample * 75 /
                        sample_rate)
            else:
                #no Xing header,
                #assume a constant bitrate file
                mp3file.seek(-128, 2)
                if (mp3file.read(3) == "TAG"):
                    end_position = mp3file.tell() - 3
                else:
                    mp3file.seek(0, 2)
                    end_position = mp3file.tell()

                return ((end_position - start_position) * 75 * 8 /
                        (bit_rate * 1000))
        finally:
            mp3file.close()

    def total_frames(self):
        """Returns the total PCM frames of the track as an integer."""

        return self.cd_frames() * self.sample_rate() / 75

    @classmethod
    def can_add_replay_gain(cls):
        """Returns True if we have the necessary binaries to add ReplayGain."""

        return BIN.can_execute(BIN['mp3gain'])

    @classmethod
    def lossless_replay_gain(cls):
        """Returns False."""

        return False

    @classmethod
    def add_replay_gain(cls, filenames, progress=None):
        """Adds ReplayGain values to a list of filename strings.

        All the filenames must be of this AudioFile type.
        Raises ValueError if some problem occurs during ReplayGain application.
        """

        track_names = [track.filename for track in
                       open_files(filenames) if
                       isinstance(track, cls)]

        if (progress is not None):
            progress(0, 1)

        if ((len(track_names) > 0) and (BIN.can_execute(BIN['mp3gain']))):
            devnull = file(os.devnull, 'ab')
            sub = subprocess.Popen([BIN['mp3gain'], '-f', '-k', '-q', '-r'] + \
                                       track_names,
                                   stdout=devnull,
                                   stderr=devnull)
            sub.wait()

            devnull.close()

        if (progress is not None):
            progress(1, 1)

    def mpeg_frames(self):
        """Yields (header, data) tuples of the file's contents.

        header is an MPEG_Frame_Header Construct.
        data is a string of MP3 data."""

        header_struct = MPEG_Frame_Header("header")
        f = open(self.filename, 'rb')
        try:
            #FIXME - this won't handle RIFF RMP3 well
            #perhaps I should use tracklint to clean those up
            MP3Audio.__find_last_mp3_frame__(f)
            stop_position = f.tell()
            f.seek(0, 0)
            MP3Audio.__find_mp3_start__(f)
            while (f.tell() < stop_position):
                header = header_struct.parse_stream(f)
                data = f.read(header.byte_length - 4)
                yield (header, data)
        finally:
            f.close()

    def verify(self, progress=None):
        from . import verify
        try:
            f = open(self.filename, 'rb')
        except IOError, err:
            raise InvalidMP3(str(err))

        #MP3 verification is likely to be so fast
        #that individual calls to progress() are
        #a waste of time.
        if (progress is not None):
            progress(0, 1)

        try:
            try:
                #skip ID3v2/ID3v1 tags during verification
                self.__find_mp3_start__(f)
                start = f.tell()
                self.__find_last_mp3_frame__(f)
                end = f.tell()
                f.seek(start, 0)

                verify.mpeg(f, start, end)
                if (progress is not None):
                    progress(1, 1)

                return True
            except (IOError, ValueError), err:
                raise InvalidMP3(str(err))
        finally:
            f.close()


#######################
#MP2 AUDIO
#######################

class MP2Audio(MP3Audio):
    """An MP2 audio file."""

    SUFFIX = "mp2"
    NAME = SUFFIX
    DEFAULT_COMPRESSION = str(192)
    COMPRESSION_MODES = tuple(map(str, (64,  96,  112, 128, 160, 192,
                                        224, 256, 320, 384)))
    COMPRESSION_DESCRIPTIONS = {"64": _(u"total bitrate of 64kbps"),
                                "384": _(u"total bitrate of 384kbps")}
    BINARIES = ("lame", "twolame")

    @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."""

        ID3v2Comment.skip(file)

        try:
            frame = cls.MP3_FRAME_HEADER.parse_stream(file)

            return ((frame.sync == 0x07FF) and
                    (frame.mpeg_version in (0x03, 0x02, 0x00)) and
                    (frame.layer == 0x02))
        except:
            return False

    @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 MP2Audio object."""

        import decimal
        import bisect

        if ((compression is None) or
            (compression not in cls.COMPRESSION_MODES)):
            compression = __default_quality__(cls.NAME)

        if ((pcmreader.channels > 2) or
            (pcmreader.sample_rate not in (32000, 48000, 44100)) or
            (pcmreader.bits_per_sample != 16)):
            pcmreader = PCMConverter(
                pcmreader,
                sample_rate=[32000, 32000, 44100, 48000][bisect.bisect(
                        [32000, 44100, 48000], pcmreader.sample_rate)],
                channels=min(pcmreader.channels, 2),
                channel_mask=pcmreader.channel_mask,
                bits_per_sample=16)

        devnull = file(os.devnull, 'ab')

        sub = subprocess.Popen([BIN['twolame'], "--quiet",
                                "-r",
                                "-s", str(pcmreader.sample_rate),
                                "--samplesize", str(pcmreader.bits_per_sample),
                                "-N", str(pcmreader.channels),
                                "-m", "a",
                                "-b", compression,
                                "-",
                                filename],
                               stdin=subprocess.PIPE,
                               stdout=devnull,
                               stderr=devnull,
                               preexec_fn=ignore_sigint)

        try:
            transfer_framelist_data(pcmreader, sub.stdin.write)
        except (ValueError, IOError), err:
            sub.stdin.close()
            sub.wait()
            cls.__unlink__(filename)
            raise EncodingError(str(err))
        except Exception, err:
            sub.stdin.close()
            sub.wait()
            cls.__unlink__(filename)
            raise err

        try:
            pcmreader.close()
        except DecodingError, err:
            cls.__unlink__(filename)
            raise EncodingError(err.error_message)

        sub.stdin.close()
        devnull.close()

        if (sub.wait() == 0):
            return MP2Audio(filename)
        else:
            cls.__unlink__(filename)
            raise EncodingError(u"twolame exited with error")
