#!/usr/bin/python # vim: set expandtab tabstop=4 shiftwidth=4: # Note - currently this util is hard-coded to only accept # MPEG Version 1 Layer 3 files, with no CRC prodection. It # won't recognize a file with any other kind of audio frames. # # It should handle id3v1 and id3v2 frames as well, though. # # http://www.mp3-tech.org/programmer/frame_header.html # http://en.wikipedia.org/wiki/MP3 # http://upload.wikimedia.org/wikipedia/commons/0/01/Mp3filestructure.svg # http://www.id3.org/id3v2.4.0-structure # http://en.wikipedia.org/wiki/ID3 # http://www.mp3-converter.com/mp3codec/frames.htm <- not entirely accurate import sys, termios, fcntl from struct import pack, unpack from math import log, exp # Global vars hist = {} framecount = 0 # Lookup tables brlookup = { 0x0: 0, 0x1: 32, 0x2: 40, 0x3: 48, 0x4: 56, 0x5: 64, 0x6: 80, 0x7: 96, 0x8: 112, 0x9: 128, 0xA: 160, 0xB: 192, 0xC: 224, 0xD: 256, 0xE: 320, 0xF: 999 } samplookup = { 0x0: 44100, 0x1: 48000, 0x2: 32000, 0x3: 99999 } def handle_mp3(df, data): global hist, framecount padded = 0 if (data & 0x0200 == 0x0200): padded = 1 bitrate = brlookup[(data & 0xFF00) >> 12] samprate = samplookup[(data & 0x0F00) >> 10] # Ordinarily we should check for CRC here, but we're # just assuming that it doesn't have it right now # (ie: the pattern 0xFFFB that we test for in the main loop) datasize = int((144000*bitrate)/samprate) + padded - 4 data = df.read(datasize) if (len(data) != datasize): print 'WARNING: Couldn\'t read mp3 data' return False hist[bitrate] = hist[bitrate]+1 framecount = framecount+1 return True def unsyncsafe(var, bytes): retvar = 0 for i in range(bytes): tempval = var >> (i*8) tempval = tempval & 0x7F tempval = tempval << (i*7) retvar = retvar + tempval return retvar def handle_id3v2(df, data): (flags, size) = unpack('>x B I', df.read(6)) size = unsyncsafe(size, 4) data = df.read(size) if (flags & 0x10 == 0x10): size = size + 10 if (len(data) != size): print 'WARNING: Couldn\'t read id3v2 frame' return False return True def handle_id3v1(df, data): data = df.read(124) if (len(data) != 124): print 'WARNING: Couldn\'t read id3v1 tag' return False return True def processfile(filename): global hist, framecount hist = {} tagcount_v1 = 0 tagcount_v2 = 0 framecount = 0 for rate in brlookup.values(): hist[rate] = 0 valid = True # Initial setup try: df = open(filename, 'rb') except IOError: print '"' + filename + '" could not be opened.' return False # Start processing frames data = df.read(4) while (len(data) == 4): data = unpack('>I', data)[0] if (data & 0xFFFF0000 == 0xFFFB0000): rv = handle_mp3(df, data) pass elif (data & 0xFFFFFF00 == 0x49443300): rv = handle_id3v2(df, data) tagcount_v2 += 1 pass elif (data & 0xFFFFFF00 == 0x54414700): rv = handle_id3v1(df, data) tagcount_v1 += 1 pass else: valid = False rv = None print 'Unknown data in mp3: %0X' % data break if (not rv): print 'Errors processing file, skipping' valid = False break data = df.read(4) # Clean up df.close() # Calculate some stats max = 0 amean = 0 gmean = 0 secs = 0 mins = 0 for rate in hist.keys(): if (hist[rate] > max): max = hist[rate] amean = amean + float(rate*hist[rate])/framecount if (rate != 0 and hist[rate] != 0): gmean = gmean + hist[rate] * log(rate) gmean = exp(gmean/framecount) # .026 secs per frame is *wrong* - it's actually 1152 samples # per frame, which you want to divide by our 44100Hz. Which # simplified leaves us with 32/1225, btw (common factor = 36). # (or, if you want to be pedantic, .02612244897959183673....) #secs = int(framecount*26/1000) secs = int(framecount*32/1225) milli = (framecount*32.0/1225.0)-secs mins = int(secs/60) secs = secs - (mins*60) # Window geometry numdigits = len(str(max)) temp = pack('HHHH', 0, 0, 0, 0) try: cols = unpack('xxHxxxx', fcntl.ioctl(sys.stdout.fileno(), termios.TIOCGWINSZ, temp))[0] except IOError: cols = 80 if (cols < 10): cols = 80 cols = cols - (9 + numdigits) formatstr = '%3d [%' + str(numdigits) + 'd] ' # report if (valid): print '%s VBR layout:' % (filename) print ' * Total track time: %d:%02d.%04d' % (mins, secs, round(milli*10000)) print ' * Arithmetic mean: %dkbps' % (amean) print ' * Geometric mean: %dkbps' % (gmean) #print '(Total tag counts - v1: %d | v2: %d)' % (tagcount_v1, tagcount_v2) print 'Total frames: %d' % (framecount) histkeys = hist.keys() histkeys.sort() for rate in histkeys: if (hist[rate] != 0): # we need to cast to float, otherwise the expression returns 0 for all but the # longest row print (formatstr + '*'*int((float(hist[rate])/max)*cols)) % (rate, hist[rate]) print # ... and return return True def main(argv=None): if argv is None: argv = sys.argv for file in sys.argv[1:]: processfile(file) return 0 if __name__ == '__main__': sys.exit(main())