source: trunk/src/testing/bin/fileServer/misc/mmpython/audio/mp3info.py @ 4

Revision 4, 18.3 KB checked in by ajaworski, 13 years ago (diff)

Added modified SAGE sources

Line 
1#if 0
2# -----------------------------------------------------------------------
3# $Id: mp3info.py,v 1.12 2003/07/10 13:05:05 the_krow Exp $
4# -----------------------------------------------------------------------
5# $Log: mp3info.py,v $
6# Revision 1.12  2003/07/10 13:05:05  the_krow
7# o id3v2 tabled added to eyed3
8# o type changed to MUSIC
9#
10# Revision 1.11  2003/06/30 13:17:18  the_krow
11# o Refactored mediainfo into factory, synchronizedobject
12# o Parsers now register directly at mmpython not at mmpython.mediainfo
13# o use mmpython.Factory() instead of mmpython.mediainfo.get_singleton()
14# o Bugfix in PNG parser
15# o Renamed disc.AudioInfo into disc.AudioDiscInfo
16# o Renamed disc.DataInfo into disc.DataDiscInfo
17#
18# Revision 1.10  2003/06/20 19:17:22  dischi
19# remove filename again and use file.name
20#
21# Revision 1.9  2003/06/09 22:59:37  the_krow
22# hopefully mp3 files are not detected everytime an mp3 stream is inside a
23# container any longer.
24#
25# Revision 1.8  2003/06/09 14:31:56  the_krow
26# fixes on the mpeg parser
27# resolutions, fps and bitrate should be reported correctly now
28#
29# Revision 1.7  2003/06/09 12:50:08  the_krow
30# mp3 now fills tables
31#
32# Revision 1.6  2003/06/08 19:53:38  dischi
33# also give the filename to init for additional data tests
34#
35# Revision 1.5  2003/06/08 13:44:56  dischi
36# Changed all imports to use the complete mmpython path for mediainfo
37#
38# Revision 1.4  2003/06/08 13:11:25  dischi
39# removed print at the end and moved it into register
40#
41# Revision 1.3  2003/06/07 23:10:50  the_krow
42# Changed mp3 into new format.
43#
44#
45# -----------------------------------------------------------------------
46# MMPython - Media Metadata for Python
47# Copyright (C) 2003 Thomas Schueppel, et. al
48#
49# This program is free software; you can redistribute it and/or
50# modify it under the terms of the GNU General Public License as
51# published by the Free Software Foundation; either version 2 of the
52# License, or (at your option) any later version.
53#
54# This program is distributed in the hope that it will be useful,
55# but WITHOUT ANY WARRANTY; without even the implied warranty of
56# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
57# GNU General Public License for more details.
58#
59# You should have received a copy of the GNU General Public License
60# along with this program; if not, write to the Free Software
61# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
62# USA
63#
64# Most of the code of this module was taken from Vivake Guptas mp3info
65# code. Below is his copyright notice. All credit to him.
66#
67# Copyright (c) 2002 Vivake Gupta (vivakeATomniscia.org).  All rights reserved.
68# This software is maintained by Vivake (vivakeATomniscia.org) and is available at:
69#     http://www.omniscia.org/~vivake/python/MP3Info.py
70#
71#
72
73
74import struct
75import string
76from mmpython import mediainfo
77import mmpython
78import re
79
80def _from_synch_safe(synchsafe):
81    if isinstance(synchsafe, type(1)):
82        (b3, b2, b1, b0) = struct.unpack('!4b', struct.pack('!1i', synchsafe))
83    else:
84        while len(synchsafe) < 4:
85            synchsafe = (0,) + synchsafe
86        (b3, b2, b1, b0) = synchsafe
87
88    x = 256
89    return (((b3 * x + b2) * x + b1) * x + b0)
90
91def _strip_zero(s):
92    start = 0
93    while start < len(s) and (s[start] == '\0' or s[start] == ' '):
94        start = start + 1
95
96    end = len(s) - 1
97    while end >= 0 and (s[end] == '\0' or s[end] == ' '):
98        end = end - 1
99
100    return s[start:end+1]
101
102class ID3v2Frame:
103    def __init__(self, file, version):
104        self.name = ""
105        self.version = 0
106        self.padding = 0
107        self.size = 0
108        self.data = ""
109
110        self.flags = {}
111        self.f_tag_alter_preservation = 0
112        self.f_file_alter_preservation = 0
113        self.f_read_only = 0
114        self.f_compression = 0
115        self.f_encryption = 0
116        self.f_grouping_identity = 0
117        self.f_unsynchronization = 0
118        self.f_data_length_indicator = 0
119
120        if version == 2:
121            nameSize = 3
122        else:
123            nameSize = 4
124        self.name = file.read(nameSize)
125
126        self.version = version
127
128        if self.name == nameSize * '\0':
129            self.padding = 1
130            return
131
132        if self.name[0] < 'A' or self.name[0] > 'Z':
133            self.padding = 1
134            return
135
136        size = ()
137        if version == 2:
138            size = struct.unpack('!3b', file.read(3))
139        elif version == 3 or version == 4:
140            size = struct.unpack('!4b', file.read(4))
141
142        if version == 3:  # abc00000 def00000
143            (flags,) = struct.unpack('!1b', file.read(1))
144            self.f_tag_alter_preservation  = flags >> 7 & 1 #a
145            self.f_file_alter_preservation = flags >> 6 & 1 #b
146            self.f_read_only               = flags >> 5 & 1 #c
147            (flags,) = struct.unpack('!1b', file.read(1))
148            self.f_compression             = flags >> 7 & 1 #d
149            self.f_encryption              = flags >> 6 & 1 #e
150            self.f_grouping_identity       = flags >> 5 & 1 #f
151        elif version == 4: # 0abc0000 0h00kmnp
152            (flags,) = struct.unpack('!1b', file.read(1))
153            self.f_tag_alter_preservation  = flags >> 6 & 1 #a
154            self.f_file_alter_preservation = flags >> 5 & 1 #b
155            self.f_read_only               = flags >> 4 & 1 #c
156            (flags,) = struct.unpack('!1b', file.read(1))
157            self.f_grouping_identity       = flags >> 6 & 1 #h
158            self.f_compression             = flags >> 3 & 1 #k
159            self.f_encryption              = flags >> 2 & 1 #m
160            self.f_unsynchronization       = flags >> 1 & 1 #n
161            self.f_data_length_indicator   = flags >> 0 & 1 #p
162        self.size = _from_synch_safe(size)
163        # An ugly hack to obmit crashing for large data
164        if self.size < 65536:
165            self.data = _strip_zero(file.read(self.size))
166
167_genres = [
168    "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge",
169    "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B",
170    "Rap", "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska",
171    "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient", "Trip-Hop",
172    "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", "Instrumental",
173    "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", "AlternRock",
174    "Bass", "Soul", "Punk", "Space", "Meditative", "Instrumental Pop",
175    "Instrumental Rock", "Ethnic", "Gothic", "Darkwave", "Techno-industrial",
176    "Electronic", "Pop-Folk", "Eurodance", "Dream", "Southern Rock", "Comedy",
177    "Cult", "Gangsta", "Top 40", "Christian Rap", "Pop/Funk", "Jungle",
178    "Native American", "Cabaret", "New Wave", "Psychadelic", "Rave",
179    "Showtunes", "Trailer", "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz",
180    "Polka", "Retro", "Musical", "Rock & Roll", "Hard Rock", "Folk",
181    "Folk/Rock", "National Folk", "Swing", "Fast-Fusion", "Bebob", "Latin",
182    "Revival", "Celtic", "Bluegrass", "Avantegarde", "Gothic Rock",
183    "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", "Slow Rock",
184    "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour", "Speech",
185    "Chanson", "Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass",
186    "Primus", "Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba",
187    "Folklore", "Ballad", "Power Ballad", "Rythmic Soul", "Freestyle", "Duet",
188    "Punk Rock", "Drum Solo", "A capella", "Euro-House", "Dance Hall", "Goa",
189    "Drum & Bass", "Club House", "Hardcore", "Terror", "Indie", "BritPop",
190    "NegerPunk", "Polsk Punk", "Beat", "Christian Gangsta", "Heavy Metal",
191    "Black Metal", "Crossover", "Contemporary C", "Christian Rock", "Merengue",
192    "Salsa", "Thrash Metal", "Anime", "JPop", "SynthPop",
193]
194
195class ID3v1:
196    def __init__(self, file):
197        self.valid = 0
198
199        self.tags = { }
200
201        try:
202            file.seek(-128, 2)
203        except IOError:
204            pass
205
206        data = file.read(128)
207        if data[0:3] != 'TAG':
208            return
209        else:
210            self.valid = 1
211
212        self.tags['TT2'] = _strip_zero(data[ 3: 33])
213        self.tags['TP1'] = _strip_zero(data[33: 63])
214        self.tags['TAL'] = _strip_zero(data[63: 93])
215        self.tags['TYE'] = _strip_zero(data[93: 97])
216        self.tags['COM'] = _strip_zero(data[97:125])
217
218        if data[125] == '\0':
219            self.tags['TRK'] = ord(data[126])
220
221        try:
222            self.tags['TCO'] = _genres[ord(data[127])]
223        except IndexError:
224            self.tags['TCO'] = "(%i)" % ord(data[127])
225
226
227class ID3v2:
228    def __init__(self, file):
229        self.valid = 0
230
231        self.tags = { }
232
233        self.header_size = 0
234
235        self.major_version = 0
236        self.minor_version = 0
237
238        self.f_unsynchronization = 0
239        self.f_extended_header = 0
240        self.f_experimental = 0
241        self.f_footer = 0
242
243        self.f_extended_header_zie = 0
244        self.f_extended_num_flag_bytes = 0
245
246        self.ef_update = 0
247        self.ef_crc = 0
248        self.ef_restrictions = 0
249
250        self.crc = 0
251        self.restrictions = 0
252
253        self.frames = []
254        self.tags = {}
255
256        file.seek(0, 0)
257        if file.read(3) != "ID3":
258            return
259        else:
260            self.valid = 1
261
262        (self.major_version, self.minor_version) = struct.unpack('!2b', file.read(2))
263
264        # abcd 0000
265        (flags,) = struct.unpack('!1b', file.read(1))
266        self.f_unsynchronization = flags >> 7 & 1 # a
267        self.f_extended_header   = flags >> 6 & 1 # b
268        self.f_experimental      = flags >> 5 & 1 # c
269        self.f_footer            = flags >> 4 & 1 # d
270
271        self.header_size = _from_synch_safe(struct.unpack('!4b', file.read(4)))
272
273        while 1:
274            if file.tell() >= self.header_size:
275                break
276            frame = ID3v2Frame(file, self.major_version)
277            if frame.padding:
278                file.seek(self.header_size)
279                break
280
281            self.frames.append(frame)
282            self.tags[frame.name] = frame.data
283
284_bitrates = [
285    [ # MPEG-2 & 2.5
286        [0,32,48,56, 64, 80, 96,112,128,144,160,176,192,224,256,None], # Layer 1
287        [0, 8,16,24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160,None], # Layer 2
288        [0, 8,16,24, 32, 40, 48, 56, 64, 80, 96,112,128,144,160,None]  # Layer 3
289    ],
290
291    [ # MPEG-1
292        [0,32,64,96,128,160,192,224,256,288,320,352,384,416,448,None], # Layer 1
293        [0,32,48,56, 64, 80, 96,112,128,160,192,224,256,320,384,None], # Layer 2
294        [0,32,40,48, 56, 64, 80, 96,112,128,160,192,224,256,320,None]  # Layer 3
295    ]
296]
297
298_samplerates = [
299    [ 11025, 12000,  8000, None], # MPEG-2.5
300    [  None,  None,  None, None], # reserved
301    [ 22050, 24000, 16000, None], # MPEG-2
302    [ 44100, 48000, 32000, None], # MPEG-1
303]
304
305_modes = [ "stereo", "joint stereo", "dual channel", "mono" ]
306
307_mode_extensions = [
308    [ "4-31", "8-31", "12-31", "16-31" ],
309    [ "4-31", "8-31", "12-31", "16-31" ],
310    [     "",   "IS",    "MS", "IS+MS" ]
311]
312
313_emphases = [ "none", "50/15 ms", "reserved", "CCIT J.17" ]
314
315_MP3_HEADER_SEEK_LIMIT = 4096
316
317class MPEG:
318    def __init__(self, file):
319        self.valid = 0
320        s = file.read(4096)
321        if s[:3] == 'ID3':
322            self.valid = 1
323        if re.compile(r'0*\xFF\xFB\xB0\x04$').search(s):
324            self.valid = 1
325        if re.compile(r'0*\xFF\xFA\xB0\x04$').search(s):
326            self.valid = 1
327        file.seek(0, 2)
328        self.filesize = file.tell()       
329        file.seek(0, 0)
330
331        self.version = 0
332        self.layer = 0
333        self.protection = 0
334        self.bitrate = 0
335        self.samplerate = 0
336        self.padding = 0
337        self.private = 0
338        self.mode = ""
339        self.mode_extension = ""
340        self.copyright = 0
341        self.original = 0
342        self.emphasis = ""
343        self.length = 0
344
345        offset, header = self._find_header(file)
346        if offset == -1 or header is None:
347            return
348
349        self._parse_header(header)
350        ### offset + framelength will find another header. verify??
351        if not self.valid:
352            return
353
354        self._parse_xing(file)
355
356
357    def _find_header(self, file):
358        file.seek(0, 0)
359        amount_read = 0
360
361        # see if we get lucky with the first four bytes
362        amt = 4
363
364        while amount_read < _MP3_HEADER_SEEK_LIMIT:
365            header = file.read(amt)
366            if len(header) < amt:
367                # awfully short file. just give up.
368                return -1, None
369
370            amount_read = amount_read + len(header)
371
372            # on the next read, grab a lot more
373            amt = 500
374
375            # look for the sync byte
376            offset = string.find(header, chr(255))
377            if offset == -1:
378                continue
379            ### maybe verify more sync bits in next byte?
380
381            if offset + 4 > len(header):
382                more = file.read(4)
383                if len(more) < 4:
384                    # end of file. can't find a header
385                    return -1, None
386                amount_read = amount_read + 4
387                header = header + more
388            return amount_read - len(header) + offset, header[offset:offset+4]
389
390        # couldn't find the header
391        return -1, None
392
393    def _parse_header(self, header):
394        # AAAAAAAA AAABBCCD EEEEFFGH IIJJKLMM
395        (bytes,) = struct.unpack('>i', header)
396        mpeg_version =    (bytes >> 19) & 3  # BB   00 = MPEG2.5, 01 = res, 10 = MPEG2, 11 = MPEG1
397        layer =           (bytes >> 17) & 3  # CC   00 = res, 01 = Layer 3, 10 = Layer 2, 11 = Layer 1
398        protection_bit =  (bytes >> 16) & 1  # D    0 = protected, 1 = not protected
399        bitrate =         (bytes >> 12) & 15 # EEEE 0000 = free, 1111 = bad
400        samplerate =      (bytes >> 10) & 3  # F    11 = res
401        padding_bit =     (bytes >> 9)  & 1  # G    0 = not padded, 1 = padded
402        private_bit =     (bytes >> 8)  & 1  # H
403        mode =            (bytes >> 6)  & 3  # II   00 = stereo, 01 = joint stereo, 10 = dual channel, 11 = mono
404        mode_extension =  (bytes >> 4)  & 3  # JJ
405        copyright =       (bytes >> 3)  & 1  # K    00 = not copyrighted, 01 = copyrighted
406        original =        (bytes >> 2)  & 1  # L    00 = copy, 01 = original
407        emphasis =        (bytes >> 0)  & 3  # MM   00 = none, 01 = 50/15 ms, 10 = res, 11 = CCIT J.17
408
409        if mpeg_version == 0:
410            self.version = 2.5
411        elif mpeg_version == 2:
412            self.version = 2
413        elif mpeg_version == 3:
414            self.version = 1
415        else:
416            return
417
418        if layer > 0:
419            self.layer = 4 - layer
420        else:
421            return
422
423        self.protection = protection_bit
424
425        self.bitrate = _bitrates[mpeg_version & 1][self.layer - 1][bitrate]
426        self.samplerate = _samplerates[mpeg_version][samplerate]
427
428        if self.bitrate is None or self.samplerate is None:
429            return
430
431        self.padding = padding_bit
432        self.private = private_bit
433        self.mode = _modes[mode]
434        self.mode_extension = _mode_extensions[self.layer - 1][mode_extension]
435        self.copyright = copyright
436        self.original = original
437        self.emphasis = _emphases[emphasis]
438
439        if self.layer == 1:
440            self.framelength = ((  12 * (self.bitrate * 1000.0)/self.samplerate) + padding_bit) * 4
441            self.samplesperframe = 384.0
442        else:
443            self.framelength =  ( 144 * (self.bitrate * 1000.0)/self.samplerate) + padding_bit
444            self.samplesperframe = 1152.0
445        self.length = int(round((self.filesize / self.framelength) * (self.samplesperframe / self.samplerate)))
446
447        self.valid = 1
448
449    def _parse_xing(self, file):
450        """Parse the Xing-specific header.
451
452        For variable-bitrate (VBR) MPEG files, Xing includes a header which
453        can be used to approximate the (average) bitrate and the duration
454        of the file.
455        """
456        file.seek(0, 0)
457        header = file.read(128)
458
459        i = string.find(header, 'Xing')
460        if i > 0:
461            (flags,) = struct.unpack('>i', header[i+4:i+8])
462            if flags & 3:
463                # flags says "frames" and "bytes" are present. use them.
464                (frames,bytes) = struct.unpack('>ii', header[i+8:i+16])
465
466                if self.samplerate:
467                    self.length = int(round(frames * self.samplesperframe / self.samplerate))
468                    self.bitrate = ((bytes * 8.0 / self.length) / 1000)
469
470class MP3Info(mediainfo.MusicInfo):
471    def __init__(self, file):
472        mediainfo.MusicInfo.__init__(self)
473        self.valid = 0
474        mpeg = MPEG(file)
475        if not mpeg.valid:
476            self.valid = 0
477            return
478        self.info = {}
479        self.samplerate = mpeg.samplerate
480        self.bitrate = mpeg.bitrate * 1000
481        self.length = mpeg.length
482        self.channels = mpeg.mode
483        if self.samplerate and self.channels and self.bitrate:
484            pass
485        else:
486            self.valid = 0
487            return
488        self.type = 'MPEG %1.f' % mpeg.version
489        self.subtype = 'Layer %d' % mpeg.layer
490        self.valid = 1
491        id3 = ID3v1(file)
492        if id3.valid:
493            self.appendtable('id3v1', id3.tags)
494
495        id3v2 = ID3v2(file)
496        if id3.valid:
497            id3 = id3v2
498            self.appendtable('id3v2', id3.tags)
499
500        if id3 is None:
501            return
502
503        # Set the info fields
504        for tag in id3.tags.keys():
505            if tag == 'TT2' or tag == 'TIT2':
506                self.title = id3.tags[tag]
507            elif tag == 'TP1' or tag == 'TPE1':
508                self.artist = id3.tags[tag]
509            elif tag == 'TRK' or tag == 'TRCK':
510                self.trackno = id3.tags[tag]
511            elif tag == 'TYE' or tag == 'TYER':
512                self.date = id3.tags[tag]
513            elif tag == 'COM' or tag == 'COMM':
514                self.comment = id3.tags[tag]
515            elif tag == 'TCM':
516                self.composer = id3.tags[tag]
517            elif tag == 'TAL' or tag == 'TALB':
518                self.album = id3.tags[tag]
519            elif tag == 'TPA':
520                self.disc = id3.tags[tag]
521            elif tag == 'TCO' or tag == 'TCON':
522                genre = self.genre = id3.tags[tag]
523                if genre and genre[0] == '(' and genre[-1] == ')':
524                    try:
525                        self.info['genre'] = _genres[int(genre[1:-1])]
526                    except IndexError:
527                        self.info['genre'] = ""
528            elif tag == 'TEN' or tag == 'TENC':
529                self.encoder = id3.tags[tag]
530
531
532mmpython.registertype( 'audio/mp3', ('mp3',), mediainfo.TYPE_MUSIC, MP3Info )
Note: See TracBrowser for help on using the repository browser.