Florian Schulze wrote:
On Sat, 11 Dec 2004 20:32:30 +0100, Ivo Woltring <[EMAIL PROTECTED]> wrote:

mmpython will help but not always.
Lets put it this way. I will ALWAYS read through the whole file. In that
order I don't mind evaluating each frame. The thing I don't seem to be able
to find is the timelength-constants for frames for the different mp3
versions, bitrates and layers. Can anybody help me?


From http://www.oreilly.com/catalog/mp3/chapter/ch02.html#71109
"In addition, the number of samples stored in an MP3 frame is constant, at 1,152 samples per frame."



So you only need the samplerate for each frame to calculate the duration of that frame.


1152 samples / 44100 samples per second ~ 0.026 seconds

I don't exactly know whether you need to include mono/stereo into the calculation, you would have to test that out.

Regards,
Florian Schulze


This thread prompted me to dig up some old code I wrote in April 2003 parsing MPEG audio headers. Don't remember much about it except I had trouble finding reference material.


This is what I used for frame duration:

self.size = (144*self.bitrate) / self.samplerate + self.padding

if self.bitrate:
    self.duration = self.size*8.0/self.bitrate
else:
    self.duration = 0.0

That is, using bitrate instead of samplerate. More complicated, if you don't need the frame size. However, remember there might be metaframes, so the naive samplerate method might be off. I think most encoders set bitrate to 0 for metaframes, but you should check the Xing/Info tag to be sure...

Ofcourse, the right way to do it is to parse and use the VBR tag...

I'm attaching my old as-is MPEG code. Most of that project was lost in a disk crash and abandoned, so I don't know what state it's in, but...


Erik
#!/usr/bin/env python
#
# dAMP
# - Music Server
#
# $Id: mpeg.py,v 1.5 2003/04/28 23:35:11 eh Exp $
#
# history
# 2003-04-xx eh  created
# 2003-04-28 eh  integrated mp3 streaming
#
# Copyright (c) 2003 by Erik Heneryd.
#

import song

class NoFrame(Exception):
    pass

# MPEG tables
MPEG_VERSIONS = (2.5, -1, 2.0, 1.0)
MPEG_LAYERS = (-1, 3, 2, 1)
MPEG_BITRATES = {
    (1, 1): (0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 
448),
    (1, 2): (0, 32, 48, 56,  64,  80,  96, 112, 128, 160, 192, 224, 256, 320, 
384),
    (1, 3): (0, 32, 40, 48,  56,  64,  80,  96, 112, 128, 160, 192, 224, 256, 
320),
    (2, 1): (0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 
448),
    (2, 2): (0, 32, 48, 56,  64,  80,  96, 112, 128, 160, 192, 224, 256, 320, 
384),
    (2, 3): (0,  8, 16, 24,  32,  64,  80,  56,  64, 128, 160, 112, 128, 256, 
320)}
MPEG_SAMPLERATES = {
    1.0: (44100, 48000, 32000),
    2.0: (22050, 24000, 16000),
    2.5: (11025, 12000,  8000)}
ID3_GENRES = [
    'Blues', 'Classic Rock', 'Country', 'Dance', 'Disco', 'Funk', 'Grunge', 
'Hip-Hop', 'Jazz', 'Metal', 'New Age', 'Oldies', 'Other', 'Pop', 'R&B', 'Rap', 
'Reggae', 'Rock', 'Techno', 'Industrial', 'Alternative', 'Ska', 'Death Metal', 
'Pranks', 'Soundtrack', 'Euro-Techno', 'Ambient', 'Trip-Hop', 'Vocal', 
'Jazz+Funk', 'Fusion', 'Trance', 'Classical', 'Instrumental', 'Acid', 'House', 
'Game', 'Sound Clip', 'Gospel', 'Noise', 'AlternRock', 'Bass', 'Soul', 'Punk', 
'Space', 'Meditative', 'Instrumental Pop', 'Instrumental Rock', 'Ethnic', 
'Gothic', 'Darkwave', 'Techno-Industrial', 'Electronic', 'Pop-Folk', 
'Eurodance', 'Dream', 'Southern Rock', 'Comedy', 'Cult', 'Gangsta', 'Top 40', 
'Christian Rap', 'Pop/Funk', 'Jungle', 'Native American', 'Cabaret', 'New 
Wave', 'Psychadelic', 'Rave', 'Showtunes', 'Trailer', 'Lo-Fi', 'Tribal', 'Acid 
Punk', 'Acid Jazz', 'Polka', 'Retro', 'Musical', 'Rock & Roll', 'Hard Rock',
    # added by winamp
    'Folk', 'Folk-Rock', 'National Folk', 'Swing', 'Fast Fusion', 'Bebob', 
'Latin', 'Revival', 'Celtic', 'Bluegrass', 'Avantgarde', 'Gothic Rock', 
'Progressive Rock', 'Psychedelic Rock', 'Symphonic Rock', 'Slow Rock', 'Big 
Band', 'Chorus', 'Easy Listening', 'Acoustic', 'Humour', 'Speech', 'Chanson', 
'Opera', 'Chamber Music', 'Sonata', 'Symphony', 'Booty Bass', 'Primus', 'Porn 
Groove', 'Satire', 'Slow Jam', 'Club', 'Tango', 'Samba', 'Folklore', 'Ballad', 
'Power Ballad', 'Rhythmic Soul', 'Freestyle', 'Duet', 'Punk Rock', 'Drum Solo', 
'Acapella', 'Euro-House', 'Dance Hall', 'Goa', 'Drum & Bass', 'Club-House', 
'Hardcore', 'Terror', 'Indie', 'BritPop', 'Negerpunk', 'Polsk Punk', 'Beat', 
'Christian Gangsta Rap', 'Heavy Metal', 'Black Metal', 'Crossover', 
'Contemporary Christian', 'Christian Rock', 'Merengue', 'Salsa']
STEREO, JOINT_STEREO, DUAL_CHANNEL, SINGLE_CHANNEL = range(4)

def int32(s):
    return (ord(s[0]) << 24) + (ord(s[1]) << 16) + (ord(s[2]) << 8) + ord(s[3])


class MPEGFrame:
    def __init__(self, data, start=0):
        self.start = start
        while 1:
            self.start = ix = data.find("\xff", self.start)
            if not -1 < ix < len(data)-4:
                raise NoFrame # no valid MPEG frame (header) found in data

            head = int32(data[ix:ix+4])
            try:
                if (head >> 21) & 0x7ff != 0x7ff: # sync bits
                    raise IndexError

                self.version = MPEG_VERSIONS[(head >> 19) & 0x3]
                self.layer = MPEG_LAYERS[(head >> 17) & 0x3]
                self.protection = (head >> 16) & 0x1
                self.bitrate = MPEG_BITRATES[int(self.version), 
self.layer][(head >> 12) & 0xf] * 1000
                self.samplerate = MPEG_SAMPLERATES[self.version][(head >> 10) & 
0x3]
                self.padding = (head >> 9) & 0x1
                self.private = (head >> 8) & 0x1
                self.chanmode = (head >> 6) & 0x3
                self.modeext = (head >> 4) & 0x3
                self.copyright = (head >> 3) & 0x1
                self.original = (head >> 2) & 0x1
                self.emphasis = head & 0x3

                self.size = (144*self.bitrate) / self.samplerate + self.padding

                if self.bitrate:
                    self.duration = self.size*8.0/self.bitrate
                else:
                    self.duration = 0.0

                if self.version == 1.0:
                    if self.chanmode == SINGLE_CHANNEL:
                        ix += 21
                    else:
                        ix += 36
                else:
                    if self.chanmode == SINGLE_CHANNEL:
                        ix += 13
                    else:
                        ix += 21

                # XING VBR
                self.totalframes = None
                self.totalsize = None

                if data[ix:ix+4] in ["Xing", "Info"]:
                    self.vbr = True
                    ix += 4
                    flags = int32(data[ix:ix+4])
                    if flags & 0x1: # FRAMES_FLAG
                        ix += 4
                        self.totalframes = int32(data[ix:ix+4])
                    if flags & 0x2: # BYTES_FLAG
                        ix += 4
                        self.totalsize = int32(data[ix:ix+4])
                else:
                    self.vbr = False

                break # header ok

            except (IndexError, KeyError):
                # found bad header, continue searching
                self.start += 1


class MPEGSong(song.Song):
    """MP3 song."""

    type = "MPEG"
    playcmd = ("mpg123",)
    usecmd = False

    # stream seek
    seek = 0
    data = None

    def getinfo(self):
        """Parse info from mp3 frame headers + get ID3 tag info."""
        if self.data is None:
            f = file(self.filename)
            data = f.read()
            f.close()
        else:
            data = self.data

        try:
            frame = MPEGFrame(data, 0)
        except NoFrame:
            return

        if frame.vbr:
            self.vbr = True
            if frame.totalframes and frame.totalsize:
                br = float(frame.totalsize/frame.totalframes)*frame.samplerate
                if frame.version == 1.0:
                    self.bitrate = br/144000
                else:
                    self.bitrate = br/72000
            else:
                # FIXME: continue and search for a proper header?
                raise NotImplemented
        else:
            self.vbr = False
            self.bitrate = frame.bitrate

        self.version = frame.version
        self.layer = frame.layer
        self.chanmode = frame.chanmode
        self.samplerate = frame.samplerate

        if self.bitrate:
            self.duration = len(data)*0.008/self.bitrate
        else:
            self.duration = 0.0

        # id3 tags

        f = lambda s: s.strip("\0").strip()

        id3 = data[-128:]
        if len(id3) == 128 and id3[:3] == "TAG":
            self.title = f(id3[3:33])
            self.artist = f(id3[33:63])
            self.album = f(id3[63:93])
            if id3[93:97].isdigit():
                self.year = int(id3[93:97])
            self.comment = f(id3[97:127])
            if ord(id3[126]):
                self.track = ord(id3[126])
            self.genre = ord(id3[127])

    def stop(self):
        self.data = None
        self.seek = 0
        Song.stop(self)

    def getdata(self):
        if self.status() == PLAYING:
            if self.data is None:
                f = file(self.filename)
                self.data = f.read()
                f.close()

            try:
                frame = MPEGFrame(self.data, self.seek)
                self.seek = frame.start + max(1, frame.size)
                return self.data[frame.start:frame.start+frame.size], 
frame.duration
            except NoFrame:
                self.stop()
                return "", 0.0
        else:
            # return silent MPEG frame
            return 
'\xff\xfb\xa0\x00\x00\x00\x00\x00\x00i\x06\x00\x00\x00\x00\x00\r [EMAIL 
PROTECTED] 
80\xb0\x14\x0f(\xb1s\xf4\xaeE\xcf`\xe0\x1a\x07\x83A\xa0x\xa5\x8b\xda"\x7f\xf2\xf7\xef|\x10(d\x90eK\xbb\xdc\x0b\x9fp\x88\x95\xa2%Ib\xef\x02\xe2\xf7\t[\xdf\xff\xcf\xff\xbb\xbd\xfc\xbd\xc1\x02\x86H4\x0f)\xc5\xdf\xfe]\xe0\x81C$PQ7w\xd0\\\xfd\x12]\xf7}\x13\xf8{\x84\x92\xc5\xc5\xd8\\]\xe1\x05\x05\x11\xc3\x81Jw{\xfe]\xf2\x05\x05*A\x90\x00\x03\x04\x89\x85\xc0\x18\x03\x03d\xea\x02d\xe9\n\t1qZ8\xcfw\x7f\xbcc\xde\xb4G\x88\xcfw\xfb^\xb4s\xc9\xa6`\r6!\x87\x83\x85\x94\x03\x0bb\x0892z}\xf8\xf7w\xbf\xff\xff\xf7\x11\xee\xfbe\x90\x87\xbbc\x10r\x08=\xdbD9\x04\x1e\xef\xc4<C\xde\xc1\x04\x1c\x82\x0fw\xe2\x1e1\xefL
 A\xc8 \xf7lb\x19\x11\xefZ"/\xb4c\xde\xc14\xda!\xcfM\x89\xa6\xc6C\x93&[EMAIL 
PROTECTED]<\xa7\xd8r\x97\x82\x03\x03\t\xb7mc\xac"\x87\t\x07\xef\xabmO\xa8\xb0\x94\xbe\x93\x8f\x93\x92\xfb$}\x14\xf7\xefV\x13Q!\xd9s1i\xc5S\x8bN\x00\x96~\x1f\xe8\x9a\xe7\xbbq\'\x14X\x88q\x17Z\x80\x98K\xfc\xfea\xea\xec\xbf\x14\xaf\xfc\xb9\x9cB\xcc)e\xd7\x90\x1c\n??\xbf\xf8r\xea\x96C->\x10\xef\xdf\x97\xaex\xfa\xb5\xc5K\xd0^,\xf3\xfc?\x9c\xcf\xdf\xf8"W:\xe0A\xcem;\x8e\xd0a\xe2\xe3\xb1a\x02\x1c\xb4\xe6\x83\x8bq\x87\xf7\x0c9\xce\xfe\x18b\xd9Y}\xfaJ\xab2!
 
p\x1fUjA\x1a\x01\'\x1a\n\x1cF\xb2\x8c\x80\xf0\xb6U\xf9\xcc\xfb\xfb\xee\x1f\x9fs\xef\xd8\xe2\xebf\x8e[\x8e\xbe\xdb\xea\xad2\x86<\xe2L\xb9k\xf1\xa5\xd19mU\x18',
 0.0261


if __name__ == "__main__":
    import sys
    s = MPEGSong(sys.argv[1])
    print "title:", s.title
    print "artist:", s.artist
    print "album:", s.album
    print "track:", s.track
    print "year:", s.year
    print "---"
    print "version:", s.version
    print "layer:", s.layer
    print "chanmode:", s.chanmode
    print "samplerate:", s.samplerate
    print "bitrate:", s.bitrate
    print "duration:", s.duration
-- 
http://mail.python.org/mailman/listinfo/python-list

Reply via email to