Florian said:
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 \xc0\x00\x00\x00\x00\x01\xa4\x1c\x00\x00\x00\x00\x004\x83\x80\x00\x00@\x01\x02D\xc5`\x98l\x9f`\x8f`\x82\x13F\x8d\xba\x97{ 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&\xc0\xe9\xb1\x90\xe4\xc9\xd14\xf4\x00@\x00\x04\x80\x00D\xdd\xf3\xb8V\x19\x014?p\xef<\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