Correctly writes out the db.
[amarok_sonynw.git] / walkgirl / id3.rb
blobeca032ce7219eae575c470a9ac6e5c4e82313098
1 ################################################################################
2 # id3.rb  Ruby Module for handling the following ID3-tag versions:
3 #         ID3v1.0 , ID3v1.1,  ID3v2.2.0, ID3v2.3.0, ID3v2.4.0
4
5 # Copyright (C) 2002,2003,2004 by Tilo Sloboda <tilo@unixgods.org>
6 # Copyright (C) 2007-2008 by RafaƂ Rzepecki <divided.mind@gmail.com>
8 # created:      12 Oct 2002
9 # updated:      Time-stamp: <Mon 27-Dec-2004 22:23:49 Tilo Sloboda>
11 # Docs:   http://www.id3.org/id3v2-00.txt
12 #         http://www.id3.org/id3v2.3.0.txt
13 #         http://www.id3.org/id3v2.4.0-changes.txt
14 #         http://www.id3.org/id3v2.4.0-structure.txt
15 #         http://www.id3.org/id3v2.4.0-frames.txt
16 #  
17 #         different versions of ID3 tags, support different fields.
18 #         See: http://www.unixgods.org/~tilo/ID3v2_frames_comparison.txt
19 #         See: http://www.unixgods.org/~tilo/ID3/docs/ID3_comparison.html
21 # License:     
22 #         Freely available under the terms of the OpenSource "Artistic License"
23 #         in combination with the Addendum A (below)
24
25 #         In case you did not get a copy of the license along with the software, 
26 #         it is also available at:   http://www.unixgods.org/~tilo/artistic-license.html
28 # Addendum A: 
29 #         THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU!
30 #         SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
31 #         REPAIR OR CORRECTION. 
33 #         IN NO EVENT WILL THE COPYRIGHT HOLDERS  BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, 
34 #         SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY 
35 #         TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED 
36 #         INACCURATE OR USELESS OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM 
37 #         TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF THE COPYRIGHT HOLDERS OR OTHER PARTY HAS BEEN
38 #         ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
40 # Author's Rant:
41 #         The author of this ID3-library for Ruby is not responsible in any way for 
42 #         the definition of the ID3-standards..
44 #         You're lucky though that you can use this little library, rather than having 
45 #         to parse ID3v2 tags yourself!  Trust me!  At the first glance it doesn't seem
46 #         to be so complicated, but the ID3v2 definitions are so convoluted and 
47 #         unnecessarily complicated, with so many useless frame-types, it's a pain to 
48 #         read the documents describing the ID3 V2.x standards.. and even worse 
49 #         to implement them..  
51 #         I don't know what these people were thinking... can we make it any more 
52 #         complicated than that??  ID3 version 2.4.0 tops everything!   If this flag
53 #         is set and it's a full moon, and an even weekday number, then do this.. 
54 #         Outch!!!  I assume that's why I don't find any 2.4.0 tags in any of my 
55 #         MP3-files... seems like noone is writing 2.4.0 tags... iTunes writes 2.3.0
57 #         If you have some files with valid 2.4.0 tags, please send them my way! 
58 #         Thank you!
60 #-------------------------------------------------------------------------------
61 #  Module ID3
62 #    
63 #    Module Functions:
64 #       hasID3v1tag?(filename)
65 #       hasID3v2tag?(filename)
66 #       removeID3v1tag(filename)
68 #    Classes:
69 #       File
70 #       Tag1
71 #       Tag2
72 #       Frame
74 ################################################################################
76 # ==============================================================================
77 # Lading other stuff..
78 # ==============================================================================
80 require "md5"
82 require 'hexdump'                  # load hexdump method to extend class String
83 require 'invert_hash'              # new invert method for old Hash
84 require 'iconv'
86 class Hash                         # overwrite Hash.invert method
87     alias old_invert invert
89     def invert
90        self.inverse
91     end
92 end
95 module ID3
97     # ----------------------------------------------------------------------------
98     #    CONSTANTS
99     # ----------------------------------------------------------------------------
100     @@RCSid = '$Id: id3.rb,v 1.2 2004/11/29 05:18:44 tilo Exp tilo $'
102     ID3v1tagSize     = 128     # ID3v1 and ID3v1.1 have fixed size tags
103     ID3v1versionbyte = 125
104     ID3v2headerSize  = 10
107     SUPPORTED_SYMBOLS = {
108     "1.0"   => {"ARTIST"=>33..62 , "ALBUM"=>63..92 ,"TITLE"=>3..32,
109                 "YEAR"=>93..96 , "COMMENT"=>97..126,"GENREID"=>127,
110 #               "VERSION"=>"1.0"
111                }  ,
112     "1.1"   => {"ARTIST"=>33..62 , "ALBUM"=>63..92 ,"TITLE"=>3..32,
113                 "YEAR"=>93..96 , "COMMENT"=>97..124,
114                 "TRACKNUM"=>126, "GENREID"=>127,
115 #                "VERSION"=>"1.1"
116                }  ,
118     "2.2.0" => {"CONTENTGROUP"=>"TT1", "TITLE"=>"TT2", "SUBTITLE"=>"TT3",
119                 "ARTIST"=>"TP1", "BAND"=>"TP2", "CONDUCTOR"=>"TP3", "MIXARTIST"=>"TP4",
120                 "COMPOSER"=>"TCM", "LYRICIST"=>"TXT", "LANGUAGE"=>"TLA", "CONTENTTYPE"=>"TCO",
121                 "ALBUM"=>"TAL", "TRACKNUM"=>"TRK", "PARTINSET"=>"TPA", "ISRC"=>"TRC", 
122                 "DATE"=>"TDA", "YEAR"=>"TYE", "TIME"=>"TIM", "RECORDINGDATES"=>"TRD",
123                 "ORIGYEAR"=>"TOR", "BPM"=>"TBP", "MEDIATYPE"=>"TMT", "FILETYPE"=>"TFT", 
124                 "COPYRIGHT"=>"TCR", "PUBLISHER"=>"TPB", "ENCODEDBY"=>"TEN", 
125                 "ENCODERSETTINGS"=>"TSS", "SONGLEN"=>"TLE", "SIZE"=>"TSI",
126                 "PLAYLISTDELAY"=>"TDY", "INITIALKEY"=>"TKE", "ORIGALBUM"=>"TOT",
127                 "ORIGFILENAME"=>"TOF", "ORIGARTIST"=>"TOA", "ORIGLYRICIST"=>"TOL",
128                 "USERTEXT"=>"TXX", 
129                 "WWWAUDIOFILE"=>"WAF", "WWWARTIST"=>"WAR", "WWWAUDIOSOURCE"=>"WAS",
130                 "WWWCOMMERCIALINFO"=>"WCM", "WWWCOPYRIGHT"=>"WCP", "WWWPUBLISHER"=>"WPB",
131                 "WWWUSER"=>"WXX", "UNIQUEFILEID"=>"UFI",
132                 "INVOLVEDPEOPLE"=>"IPL", "UNSYNCEDLYRICS"=>"ULT", "COMMENT"=>"COM",
133                 "CDID"=>"MCI", "EVENTTIMING"=>"ETC", "MPEGLOOKUP"=>"MLL",
134                 "SYNCEDTEMPO"=>"STC", "SYNCEDLYRICS"=>"SLT", "VOLUMEADJ"=>"RVA",
135                 "EQUALIZATION"=>"EQU", "REVERB"=>"REV", "PICTURE"=>"PIC",
136                 "GENERALOBJECT"=>"GEO", "PLAYCOUNTER"=>"CNT", "POPULARIMETER"=>"POP",
137                 "BUFFERSIZE"=>"BUF", "CRYPTEDMETA"=>"CRM", "AUDIOCRYPTO"=>"CRA",
138                 "LINKED"=>"LNK"
139                } ,
141     "2.3.0" => {"CONTENTGROUP"=>"TIT1", "TITLE"=>"TIT2", "SUBTITLE"=>"TIT3",
142                 "ARTIST"=>"TPE1", "BAND"=>"TPE2", "CONDUCTOR"=>"TPE3", "MIXARTIST"=>"TPE4",
143                 "COMPOSER"=>"TCOM", "LYRICIST"=>"TEXT", "LANGUAGE"=>"TLAN", "CONTENTTYPE"=>"TCON",
144                 "ALBUM"=>"TALB", "TRACKNUM"=>"TRCK", "PARTINSET"=>"TPOS", "ISRC"=>"TSRC",
145                 "DATE"=>"TDAT", "YEAR"=>"TYER", "TIME"=>"TIME", "RECORDINGDATES"=>"TRDA",
146                 "ORIGYEAR"=>"TORY", "SIZE"=>"TSIZ", 
147                 "BPM"=>"TBPM", "MEDIATYPE"=>"TMED", "FILETYPE"=>"TFLT", "COPYRIGHT"=>"TCOP",
148                 "PUBLISHER"=>"TPUB", "ENCODEDBY"=>"TENC", "ENCODERSETTINGS"=>"TSSE",
149                 "SONGLEN"=>"TLEN", "PLAYLISTDELAY"=>"TDLY", "INITIALKEY"=>"TKEY",
150                 "ORIGALBUM"=>"TOAL", "ORIGFILENAME"=>"TOFN", "ORIGARTIST"=>"TOPE",
151                 "ORIGLYRICIST"=>"TOLY", "FILEOWNER"=>"TOWN", "NETRADIOSTATION"=>"TRSN",
152                 "NETRADIOOWNER"=>"TRSO", "USERTEXT"=>"TXXX",
153                 "WWWAUDIOFILE"=>"WOAF", "WWWARTIST"=>"WOAR", "WWWAUDIOSOURCE"=>"WOAS",
154                 "WWWCOMMERCIALINFO"=>"WCOM", "WWWCOPYRIGHT"=>"WCOP", "WWWPUBLISHER"=>"WPUB",
155                 "WWWRADIOPAGE"=>"WORS", "WWWPAYMENT"=>"WPAY", "WWWUSER"=>"WXXX", "UNIQUEFILEID"=>"UFID",
156                 "INVOLVEDPEOPLE"=>"IPLS", 
157                 "UNSYNCEDLYRICS"=>"USLT", "COMMENT"=>"COMM", "TERMSOFUSE"=>"USER",
158                 "CDID"=>"MCDI", "EVENTTIMING"=>"ETCO", "MPEGLOOKUP"=>"MLLT",
159                 "SYNCEDTEMPO"=>"SYTC", "SYNCEDLYRICS"=>"SYLT", 
160                 "VOLUMEADJ"=>"RVAD", "EQUALIZATION"=>"EQUA", 
161                 "REVERB"=>"RVRB", "PICTURE"=>"APIC", "GENERALOBJECT"=>"GEOB",
162                 "PLAYCOUNTER"=>"PCNT", "POPULARIMETER"=>"POPM", "BUFFERSIZE"=>"RBUF",
163                 "AUDIOCRYPTO"=>"AENC", "LINKEDINFO"=>"LINK", "POSITIONSYNC"=>"POSS",
164                 "COMMERCIAL"=>"COMR", "CRYPTOREG"=>"ENCR", "GROUPINGREG"=>"GRID", 
165                 "PRIVATE"=>"PRIV"
166                } ,
168     "2.4.0" => {"CONTENTGROUP"=>"TIT1", "TITLE"=>"TIT2", "SUBTITLE"=>"TIT3",
169                 "ARTIST"=>"TPE1", "BAND"=>"TPE2", "CONDUCTOR"=>"TPE3", "MIXARTIST"=>"TPE4",
170                 "COMPOSER"=>"TCOM", "LYRICIST"=>"TEXT", "LANGUAGE"=>"TLAN", "CONTENTTYPE"=>"TCON",
171                 "ALBUM"=>"TALB", "TRACKNUM"=>"TRCK", "PARTINSET"=>"TPOS", "ISRC"=>"TSRC",
172                 "RECORDINGTIME"=>"TDRC", "ORIGRELEASETIME"=>"TDOR",
173                 "BPM"=>"TBPM", "MEDIATYPE"=>"TMED", "FILETYPE"=>"TFLT", "COPYRIGHT"=>"TCOP",
174                 "PUBLISHER"=>"TPUB", "ENCODEDBY"=>"TENC", "ENCODERSETTINGS"=>"TSSE",
175                 "SONGLEN"=>"TLEN", "PLAYLISTDELAY"=>"TDLY", "INITIALKEY"=>"TKEY",
176                 "ORIGALBUM"=>"TOAL", "ORIGFILENAME"=>"TOFN", "ORIGARTIST"=>"TOPE",
177                 "ORIGLYRICIST"=>"TOLY", "FILEOWNER"=>"TOWN", "NETRADIOSTATION"=>"TRSN",
178                 "NETRADIOOWNER"=>"TRSO", "USERTEXT"=>"TXXX",
179                 "SETSUBTITLE"=>"TSST", "MOOD"=>"TMOO", "PRODUCEDNOTICE"=>"TPRO",
180                 "ENCODINGTIME"=>"TDEN", "RELEASETIME"=>"TDRL", "TAGGINGTIME"=>"TDTG",
181                 "ALBUMSORTORDER"=>"TSOA", "PERFORMERSORTORDER"=>"TSOP", "TITLESORTORDER"=>"TSOT",
182                 "WWWAUDIOFILE"=>"WOAF", "WWWARTIST"=>"WOAR", "WWWAUDIOSOURCE"=>"WOAS",
183                 "WWWCOMMERCIALINFO"=>"WCOM", "WWWCOPYRIGHT"=>"WCOP", "WWWPUBLISHER"=>"WPUB",
184                 "WWWRADIOPAGE"=>"WORS", "WWWPAYMENT"=>"WPAY", "WWWUSER"=>"WXXX", "UNIQUEFILEID"=>"UFID",
185                 "MUSICIANCREDITLIST"=>"TMCL", "INVOLVEDPEOPLE2"=>"TIPL",
186                 "UNSYNCEDLYRICS"=>"USLT", "COMMENT"=>"COMM", "TERMSOFUSE"=>"USER",
187                 "CDID"=>"MCDI", "EVENTTIMING"=>"ETCO", "MPEGLOOKUP"=>"MLLT",
188                 "SYNCEDTEMPO"=>"SYTC", "SYNCEDLYRICS"=>"SYLT", 
189                 "VOLUMEADJ2"=>"RVA2", "EQUALIZATION2"=>"EQU2",
190                 "REVERB"=>"RVRB", "PICTURE"=>"APIC", "GENERALOBJECT"=>"GEOB",
191                 "PLAYCOUNTER"=>"PCNT", "POPULARIMETER"=>"POPM", "BUFFERSIZE"=>"RBUF",
192                 "AUDIOCRYPTO"=>"AENC", "LINKEDINFO"=>"LINK", "POSITIONSYNC"=>"POSS",
193                 "COMMERCIAL"=>"COMR", "CRYPTOREG"=>"ENCR", "GROUPINGREG"=>"GRID", 
194                 "PRIVATE"=>"PRIV",
195                 "OWNERSHIP"=>"OWNE", "SIGNATURE"=>"SIGN", "SEEKFRAME"=>"SEEK",
196                 "AUDIOSEEKPOINT"=>"ASPI"
197                }
198     }
200     # ----------------------------------------------------------------------------
201     # Flags in the ID3-Tag Header:
202     
203     TAG_HEADER_FLAG_MASK = {  # the mask is inverse, for error detection
204                               # those flags are supposed to be zero!
205       "2.2.0" =>  0x3F,   # 0xC0 , 
206       "2.3.0" =>  0x1F,   # 0xE0 , 
207       "2.4.0" =>  0x0F    # 0xF0 
208     }
209     
210     TAG_HEADER_FLAGS = {
211       "2.2.0" => { "Unsynchronisation"      => 0x80 ,
212                    "Compression"            => 0x40 ,
213                  } ,
214       "2.3.0" => { "Unsynchronisation"      => 0x80 ,
215                    "ExtendedHeader"         => 0x40 ,
216                    "Experimental"           => 0x20 ,
217                  } ,
218       "2.4.0" => { "Unsynchronisation"      => 0x80 ,
219                    "ExtendedHeader"         => 0x40 ,
220                    "Experimental"           => 0x20 ,
221                    "Footer"                 => 0x10 , 
222                  }
223     }
225     # ----------------------------------------------------------------------------
226     # Flags in the ID3-Frame Header:
227     
228     FRAME_HEADER_FLAG_MASK = { # the mask is inverse, for error detection
229                                # those flags are supposed to be zero!
230       "2.3.0" =>  0x1F1F,   # 0xD0D0 ,
231       "2.4.0" =>  0x8FB0    # 0x704F ,
232     }
233     
234     FRAME_HEADER_FLAGS = {
235       "2.3.0" => { "TagAlterPreservation"   => 0x8000 ,
236                    "FileAlterPreservation"  => 0x4000 ,
237                    "ReadOnly"               => 0x2000 ,
239                    "Compression"            => 0x0080 ,
240                    "Encryption"             => 0x0040 ,
241                    "GroupIdentity"          => 0x0020 ,
242                  } ,
243       "2.4.0" => { "TagAlterPreservation"   => 0x4000 , 
244                    "FileAlterPreservation"  => 0x2000 ,
245                    "ReadOnly"               => 0x1000 ,
247                    "GroupIdentity"          => 0x0040 ,
248                    "Compression"            => 0x0008 ,
249                    "Encryption"             => 0x0004 ,
250                    "Unsynchronisation"      => 0x0002 ,
251                    "DataLengthIndicator"    => 0x0001 ,
252                  }
253     }
255     # the FrameTypes are not visible to the user - they are just a mechanism 
256     # to define only one parser for multiple FraneNames.. 
257     #
259     FRAMETYPE2FRAMENAME = {
260        "TEXT" => %w(TENTGROUP TITLE SUBTITLE ARTIST BAND CONDUCTOR MIXARTIST COMPOSER LYRICIST LANGUAGE CONTENTTYPE ALBUM TRACKNUM PARTINSET ISRC DATE YEAR TIME RECORDINGDATES ORIGYEAR BPM MEDIATYPE FILETYPE COPYRIGHT PUBLISHER ENCODEDBY ENCODERSETTINGS SONGLEN SIZE PLAYLISTDELAY INITIALKEY ORIGALBUM ORIGFILENAME ORIGARTIST ORIGLYRICIST FILEOWNER NETRADIOSTATION NETRADIOOWNER SETSUBTITLE MOOD PRODUCEDNOTICE ALBUMSORTORDER PERFORMERSORTORDER TITLESORTORDER INVOLVEDPEOPLE), 
261        "USERTEXT" => "USERTEXT",
262        
263        "WEB"      => %w(WWWAUDIOFILE WWWARTIST WWWAUDIOSOURCE WWWCOMMERCIALINFO WWWCOPYRIGHT WWWPUBLISHER WWWRADIOPAGE WWWPAYMENT) , 
264        "WWWUSER"  => "WWWUSER",
265        "LTEXT"    => "TERMSOFUSE" ,
266        "PICTURE"  => "PICTURE" , 
267        "UNSYNCEDLYRICS"  => "UNSYNCEDLYRICS" , 
268        "COMMENT"  => "COMMENT" , 
269        "BINARY"   => %w(PLAYCOUNTER CDID) ,
271        # For the following Frames there are no parser stings defined .. the user has access to the raw data
272        # The following frames are good examples for completely useless junk which was put into the ID3-definitions.. what were they smoking?
273        #
274        "UNPARSED"  => %w(UNIQUEFILEID OWNERSHIP SYNCEDTEMPO MPEGLOOKUP REVERB SYNCEDLYRICS CONTENTGROUP POPULARIMETER GENERALOBJECT VOLUMEADJ AUDIOCRYPTO CRYPTEDMETA BUFFERSIZE EVENTTIMING EQUALIZATION LINKED PRIVATE LINKEDINFO POSITIONSYNC GROUPINGREG CRYPTOREG COMMERCIAL SEEKFRAME AUDIOSEEKPOINT SIGNATURE EQUALIZATION2 VOLUMEADJ2 MUSICIANCREDITLIST INVOLVEDPEOPLE2 RECORDINGTIME ORIGRELEASETIME ENCODINGTIME RELEASETIME TAGGINGTIME)
275     }
277     VARS    = 0
278     PACKING = 1
280                                 #  not sure if it's   Z* or  A*
281                                 #  A*  does not append a \0 when writing!
282                                 
283     # STILL NEED TO CAREFULLY VERIFY THESE AGAINST THE STANDARDS AND GET TEST-CASES!
284     # seems like i have no version 2.4.x ID3-tags!! If you have some, send them my way!
286     FRAME_PARSER = {
287       "TEXT"      => [ %w(encoding text) , 'Ca*' ] ,
288       "USERTEXT"  => [ %w(encoding description value) , 'Ca*a*' ] ,
290       "PICTURE"   => [ %w(encoding mimeType pictType description picture) , 'CZ*Ca*a*' ] ,
292       "WEB"       => [ "url" , 'a*' ] ,
293       "WWWUSER"   => [ %w(encoding description url) , 'CZ*Z*' ] ,
295       "LTEXT"     => [ %w(encoding language text) , 'CZ*Z*' ] ,
296       "UNSYNCEDLYRICS"    => [ %w(encoding language content text) , 'Ca3Z*Z*' ] ,
297       "COMMENT"   => [ %w(encoding language short long) , 'Ca3Z*Z*' ] ,
298       "BINARY"    => [ "binary" , 'a*' ] ,
299       "UNPARSED"  => [ "raw" , 'a*' ]       # how would we do value checking for this?
300     }
301     
302     # ----------------------------------------------------------------------------
303     # MODULE VARIABLES
304     # ----------------------------------------------------------------------------
305     Symbol2framename = ID3::SUPPORTED_SYMBOLS
306     Framename2symbol = Hash.new
307     Framename2symbol["1.0"]   = ID3::SUPPORTED_SYMBOLS["1.0"].invert
308     Framename2symbol["1.1"]   = ID3::SUPPORTED_SYMBOLS["1.1"].invert
309     Framename2symbol["2.2.0"] = ID3::SUPPORTED_SYMBOLS["2.2.0"].invert
310     Framename2symbol["2.3.0"] = ID3::SUPPORTED_SYMBOLS["2.3.0"].invert
311     Framename2symbol["2.4.0"] = ID3::SUPPORTED_SYMBOLS["2.4.0"].invert
313     FrameType2FrameName = ID3::FRAMETYPE2FRAMENAME
315     FrameName2FrameType = FrameType2FrameName.invert
316     
317     # ----------------------------------------------------------------------------
318     # the following piece of code is just for debugging, to sanity-check that all
319     # the FrameSymbols map back to a FrameType -- otherwise the library code will
320     # break if we encounter a Frame which can't be mapped to a FrameType..
321     # ----------------------------------------------------------------------------
322     #
323     # ensure we have a FrameType defined for each FrameName, otherwise
324     # code might break later..
325     #
327 #    print "\nMISSING SYMBOLS:\n"
328     
329     (ID3::Framename2symbol["2.2.0"].values +
330      ID3::Framename2symbol["2.3.0"].values +
331      ID3::Framename2symbol["2.4.0"].values).uniq.each { |symbol|
332 #       print "#{symbol} " if ! ID3::FrameName2FrameType[symbol]
333       print "SYMBOL: #{symbol} not defined!\n" if ! ID3::FrameName2FrameType[symbol]
334     }
335 #    print "\n\n"
336     
337     # ----------------------------------------------------------------------------
338     # MODULE FUNCTIONS:
339     # ----------------------------------------------------------------------------
340     # The ID3 module functions are to query or modify files directly.
341     # They give direct acess to files, and don't parse the tags, despite their headers
342     #
343     #
344     
345     # ----------------------------------------------------------------------------
346     # hasID3v1tag? 
347     #              returns string with version 1.0 or 1.1 if tag was found 
348     #              returns false  otherwise
350     def ID3.hasID3v1tag?(filename)
351       hasID3v1tag     = false
353       # be careful with empty or corrupt files..
354       return false if File.size(filename) < ID3v1tagSize
356       f = File.open(filename, 'r')
357       f.seek(-ID3v1tagSize, IO::SEEK_END)
358       if (f.read(3) == "TAG")
359         f.seek(-ID3v1tagSize + ID3v1versionbyte, IO::SEEK_END)
360         c = f.getc;                         # this is character 125 of the tag
361         if (c == 0) 
362            hasID3v1tag = "1.1"
363         else
364            hasID3v1tag = "1.0"
365         end
366       end
367       f.close
368       return hasID3v1tag
369     end
371     # ----------------------------------------------------------------------------
372     # hasID3v2tag? 
373     #              returns string with version 2.2.0, 2.3.0 or 2.4.0 if tag found
374     #              returns false  otherwise
376     def ID3.hasID3v2tag?(filename)
377       hasID3v2tag     = false
379       f = File.open(filename, 'r')
380       if (f.read(3) == "ID3")
381          major = f.getc
382          minor = f.getc
383          version   = "2." + major.to_s + '.' + minor.to_s
384          hasID3v2tag = version
385       end
386       f.close
387       return hasID3v2tag
388     end
390     # ----------------------------------------------------------------------------
391     # hasID3tag? 
392     #              returns string with all versions found, space separated
393     #              returns false  otherwise
394     
395     def ID3.hasID3tag?(filename)
396       v1 = ID3.hasID3v1tag?(filename)
397       v2 = ID3.hasID3v2tag?(filename)
399       return false if !v1 && !v2 
400       return v1    if !v2
401       return v2    if !v1
402       return "#{v1} #{v2}"
403     end
405     # ----------------------------------------------------------------------------
406     # removeID3v1tag
407     #            returns  nil  if no v1 tag was found, or it couldn't be removed
408     #            returns  true if v1 tag found and it was removed..
409     #
410     # in the future:
411     #            returns  ID3.Tag1  object if a v1 tag was found and removed
413     def ID3.removeID3v1tag(filename)
414       stat = File.stat(filename)
415       if stat.file? && stat.writable? && ID3.hasID3v1tag?(filename)
416          
417          # CAREFUL: this does not check if there really is a valid tag:
418          
419          newsize = stat.size - ID3v1tagSize
420          File.open(filename, "r+") { |f| f.truncate(newsize) }
422          return true
423       else
424          return nil
425       end
426     end
427     # ----------------------------------------------------------------------------
428     
429         
430     # ==============================================================================
431     # Class AudioFile    may call this ID3File
432     #
433     #    reads and parses audio files for tags
434     #    writes audio files and attaches dumped tags to it..
435     #    revert feature would be nice to have..
436     # 
437     #    If we query and AudioFile object, we query what's currently associated with it
438     #    e.g. we're not querying the file itself, but the perhaps modified tags
439     #    To query the file itself, use the module functions
441     class AudioFile
443       attr_reader :audioStartX , :audioEndX     # begin and end indices of audio data in file
444       attr_writer :audioStartX , :audioEndX     # begin and end indices of audio data in file
445       attr_reader :audioMD5sum                  # MD5sum of the audio portion of the file
447       attr_reader :pwd,          :filename      # PWD and relative path/name how file was first referenced
448       attr_reader :dirname,      :basename      # absolute dirname and basename of the file (computed)
450       attr_accessor :tagID3v1, :tagID3v2
451       attr_reader   :hasID3tag                  # either false, or a string with all version numbers found
453       # ----------------------------------------------------------------------------
454       # initialize
455       #
456       #   AudioFile.new   does NOT open the file, but scans it and parses the info
458       #   e.g.:  ID3::AudioFile.new('mp3/a.mp3')
460       def initialize(filename)
461           @filename     = filename      # similar to path method from class File, which is a mis-nomer!
462           @pwd          = ENV["PWD"]
463           @dirname      = File.dirname( "#{@pwd}/#{@filename}" )   # just sugar
464           @basename     = File.basename( "#{@pwd}/#{@filename}" )  # just sugar
465           
466           @tagID3v1     = nil
467           @tagID3v2     = nil
468           
469           audioStartX   = 0
470           audioEndX     = File.size(filename)
472           if ID3.hasID3v1tag?(@filename)
473               @tagID3v1 = Tag1.new
474               @tagID3v1.read(@filename)
476               audioEndX -= ID3::ID3v1tagSize
477           end
478           if ID3.hasID3v2tag?(@filename) 
479               @tagID3v2 = Tag2.new
480               @tagID3v2.read(@filename)
482               audioStartX = @tagID3v2.raw.size
483           end
484           
485           # audioStartX audioEndX indices into the file need to be set
486           @audioStartX = audioStartX 
487           @audioEndX   = audioEndX
488           
489           # user may compute the MD5sum of the audio content later..
490           # but we're only doing this if the user requests it..
492           @audioMD5sum = nil
493       end
494       
495       # ----------------------------------------------------------------------------
496       # audioMD5sum
497       #     if the user tries to access @audioMD5sum, it will be computed for him, 
498       #     unless it was previously computed. We try to calculate that only once 
499       #     and on demand, because it's a bit expensive to compute..
500       
501       def audioMD5sum
502          if ! @audioMD5sum 
503             
504             File.open( File.join(@dirname,@basename) ) { |f|
505               f.seek(@audioStartX)
506               @audioMD5sum = MD5.new( f.read(@audioEndX - @audioStartX + 1) )
507             }
509          end
510          @audioMD5sum
511       end
512       # ----------------------------------------------------------------------------
513       # writeMD5sum
514       #     write the filename and MD5sum of the audio portion into an ascii file 
515       #     in the same location as the audio file, but with suffix .md5
516       #
517       #     computes the @audioMD5sum, if it wasn't previously computed..
519       def writeMD5sum
520       
521          self.audioMD5sum if ! @audioMD5sum  # compute MD5sum if it's not computed yet
522          
523          base = @basename.sub( /(.)\.[^.]+$/ , '\1')
524          base += '.md5'
525          File.open( File.join(@dirname,base) ,"w") { |f| 
526             f.printf("%s   %s\n",  File.join(@dirname,@basename), @audioMD5sum)
527          }
528          @audioMD5sum
529       end
530       # ----------------------------------------------------------------------------
531       # verifyMD5sum
532       #     compare the audioMD5sum against a previously stored md5sum file
533       #     and returns boolean value of comparison
534       #
535       #     If no md5sum file existed, we create one and return true.
536       #
537       #     computes the @audioMD5sum, if it wasn't previously computed..
539       def verifyMD5sum
541          oldMD5sum = ''
542          
543          self.audioMD5sum if ! @audioMD5sum  # compute MD5sum if it's not computed yet
545          base = @basename.sub( /(.)\.[^.]+$/ , '\1')   # remove suffix from audio-file
546          base += '.md5'                                # add new suffix .md5
547          md5name = File.join(@dirname,base)
548          
549          # if a MD5-file doesn't exist, we should create one and return TRUE ...
550          if File.exists?(md5name)
551             File.open( md5name ,"r") { |f| 
552                oldname,oldMD5sum = f.readline.split  # read old MD5-sum
553             }
554          else
555             oldMD5sum = self.writeMD5sum        # create MD5-file and return true..
556          end
557          @audioMD5sum == oldMD5sum
558          
559       end
560       # ----------------------------------------------------------------------------
561       def version
562          a = Array.new
563          a.push(@tagID3v1.version) if @tagID3v1
564          a.push(@tagID3v2.version) if @tagID3v2
565          return nil   if a == []
566          a.join(' ') 
567       end
568       alias versions version
569       # ----------------------------------------------------------------------------
571          
572       
573     end   # of class AudioFile
575     
576     # ==============================================================================
577     # Class RestrictedOrderedHash
578     
579     class RestrictedOrderedHash < Hash
581         attr_accessor :count , :order, :locked
583         def lock
584           @locked = true
585         end
586         
587         def initialize 
588           @locked = false
589           @count  = 0
590           @order  = []
591           super
592         end
594 #        alias old_store []=
596         def []= (key,val)
597           if self[key]
598             super
599           else
600              if @locked
601                 # we're not allowed to add new keys!
602                raise ArgumentError, "You can not add new keys! The ID3-frame #{@name} has fixed entries!\n" +
603                "               valid key are: " + self.keys.join(",") +"\n"
605              else 
606                 @count += 1
607                 @order += [key]
608                 super
609              end
610           end
611         end
612         
613         def values
614           array = []
615           @order.each { |key|
616              array.push self[key]
617           }
618           array
619         end
621         # returns the human-readable ordered hash in correct order .. ;-)
622         
623         def inspect
624            first = true
625            str = "{"
626            self.order.each{ |key|
627              str += ", " if !first
628              str += key.inspect
629              str += "=>"
630              str += (self[key]).inspect
631              first = false
632            }
633            str +="}"
634         end
635         
636         # users can not delete entries from a locked hash..
637         
638         alias old_delete delete
639         
640         def delete (key)
641            if !@locked
642               old_delete(key)
643               @order.delete(key)
644            end
645         end
646         
647     end
648     
649     
650     
651     # ==============================================================================
652     # Class GenericTag
653     #
654     # as per ID3-definition, the frames are in no fixed order! that's why Hash is OK
656     class GenericTag < Hash        ###### should this be RestrictedOrderedHash as well? 
657        attr_reader :version, :raw
659        # these definitions are to prevent users from inventing their own field names..
660        # but on the other hand, they should be able to create a new valid field, if
661        # it's not yet in the current tag, but it's valid for that ID3-version...
662        # ... so hiding this, is not enough!
663        
664 #       alias old_set []=
665 #       private :old_set
666   
667        # ----------------------------------------------------------------------
669        def []=(key,val)
670         if @version == ""
671           raise ArgumentError, "undefined version of ID3-tag! - set version before accessing components!\n" 
672         else
673           if ID3::SUPPORTED_SYMBOLS[@version].keys.include?(key)
674             if !val.is_a?(ID3::Frame) and respond_to? :set_frame
675               set_frame(key,val)
676             else
677               super
678             end
679           else 
680              # exception
681              raise ArgumentError, "Incorrect ID3-field \"#{key}\" for ID3 version #{@version}\n" +
682              "               valid fields are: " + SUPPORTED_SYMBOLS[@version].keys.join(",") +"\n"
683           end
684         end
685        end
686        # ----------------------------------------------------------------------
687        # convert the 4 bytes found in the id3v2 header and return the size
688        private
689        def unmungeSize(bytes)
690          size = 0
691          j = 0; i = 3 
692          while i >= 0
693             size += 128**i * (bytes[j] & 0x7f)
694             j += 1
695             i -= 1
696          end
697          return size
698        end
699        # ----------------------------------------------------------------------
700        # convert the size into 4 bytes to be written into an id3v2 header
701        public
702        def GenericTag.mungeSize(size)
703          bytes = Array.new(4,0)
704          j = 0;  i = 3
705          while i >= 0
706            bytes[j],size = size.divmod(128**i)
707            j += 1
708            i -= 1
709          end
711          return bytes
712        end
713        # ----------------------------------------------------------------------------
714         
715     end # of class GenericTag
716     
717     # ==============================================================================
718     # Class Tag1    ID3 Version 1.x Tag
719     #
720     #      parses ID3v1 tags from a binary array
721     #      dumps  ID3v1 tags into a binary array
722     #      allows to modify tag's contents
724     class Tag1 < GenericTag
726        # ----------------------------------------------------------------------
727        # read     reads a version 1.x ID3tag
728        #
729        #     30 title
730        #     30 artist
731        #     30 album
732        #      4 year
733        #     30 comment
734        #      1 genre
736        def read(filename)
737          f = File.open(filename, 'r')
738          f.seek(-ID3::ID3v1tagSize, IO::SEEK_END)
739          hastag = (f.read(3) == 'TAG')
740          if hastag
741            f.seek(-ID3::ID3v1tagSize, IO::SEEK_END)
742            @raw = f.read(ID3::ID3v1tagSize)
744 #           self.parse!(raw)    # we should use "parse!" instead of re-coding everything..
746            if (raw[ID3v1versionbyte] == 0) 
747               @version = "1.1"
748            else
749               @version = "1.0"
750            end
751          else
752            @raw = @version = nil
753          end
754          f.close
755          #
756          # now parse all the fields
758          ID3::SUPPORTED_SYMBOLS[@version].each{ |key,val|
759             if val.class == Range
760                self[key] = @raw[val].squeeze(" \000").chomp(" ").chomp("\000")
761             elsif val.class == Fixnum
762                self[key] = @raw[val].to_s
763             else 
764                # this can't happen the way we defined the hash..
765 #              printf "unknown key/val : #{key} / #{val}  ; val-type: %s\n", val.type
766             end       
767          }
768          hastag
769        end
770        # ----------------------------------------------------------------------
771        # write    writes a version 1.x ID3tag
772        #
773        # not implemented yet..
774        #
775        # need to loacte old tag, and remove it, then append new tag..
776        #
777        # always upgrade version 1.0 to 1.1 when writing
779        
780        # ----------------------------------------------------------------------
781        # this routine modifies self, e.g. the Tag1 object
782        #
783        # tag.parse!(raw)   returns boolean value, showing if parsing was successful
784        
785        def parse!(raw)
787          return false    if raw.size != ID3::ID3v1tagSize
789          if (raw[ID3v1versionbyte] == 0) 
790             @version = "1.1"
791          else
792             @version = "1.0"
793          end
795          self.clear    # remove all entries from Hash, we don't want left-overs..
797          ID3::SUPPORTED_SYMBOLS[@version].each{ |key,val|
798             if val.class == Range
799                self[key] = raw[val].squeeze(" \000").chomp(" ").chomp("\000")
800             elsif val.class == Fixnum
801                self[key] = raw[val].to_s
802             else 
803                # this can't happen the way we defined the hash..
804 #              printf "unknown key/val : #{key} / #{val}  ; val-type: %s\n", val.class
805             end       
806          }
807          @raw = raw
808          return true
809        end
810        # ----------------------------------------------------------------------
811        # dump version 1.1 ID3 Tag into a binary array
812        #
813        # although we provide this method, it's stongly discouraged to use it, 
814        # because ID3 version 1.x tags are inferior to version 2.x tags, as entries
815        # are often truncated and hence often useless..
816        
817        def dump
818          zeroes = "\0" * 32
819          raw = "\0" * ID3::ID3v1tagSize
820          raw[0..2] = 'TAG'
822          self.each{ |key,value|
824            range = ID3::Symbol2framename['1.1'][key]
826            if range.class == Range 
827               length = range.last - range.first + 1
828               paddedstring = value + zeroes
829               raw[range] = paddedstring[0..length-1]
830            elsif range.class == Fixnum
831               raw[range] = value.to_i
832            else
833               # this can't happen the way we defined the hash..
834               next
835            end
836          }
838          return raw
839        end
840        # ----------------------------------------------------------------------
841     end  # of class Tag1
842     
843     # ==============================================================================
844     # Class Tag2    ID3 Version 2.x.y Tag
845     #
846     #      parses ID3v2 tags from a binary array
847     #      dumps  ID3v2 tags into a binary array
848     #      allows to modify tag's contents
849     #
850     #      as per definition, the frames are in no fixed order
851     
852     class Tag2 < GenericTag
853       
854       attr_reader :rawflags, :flags
855       attr_writer :version, :raw
856       
857       def initialize
858          @rawflags = 0
859          @flags    = {}
860          @version = "2.4.0"
861          super
862       end
864       def read(filename)
865           f = File.open(filename, 'r')
866           hastag = (f.read(3) == "ID3")
867           if hastag
868             f.seek(6)
869             size = ID3::ID3v2headerSize + unmungeSize(f.read(4))
870             f.seek(0)
871             @raw = f.read(size)
872             parse!
873           else
874             @raw = nil
875             @version = nil
876             return false
877           end
878           f.close
879         end
880         def parse!
881           hastag = (@raw[0...3] == "ID3")
882           if hastag
883             major = @raw[3]
884             minor = @raw[4]
885             @version = "2." + major.to_s + '.' + minor.to_s
886             @rawflags = @raw[5]
887             size = ID3::ID3v2headerSize + unmungeSize(@raw[6...10])
889             # parse the raw flags:
890             if (@rawflags & TAG_HEADER_FLAG_MASK[@version] != 0)
891                # in this case we need to skip parsing the frame... and skip to the next one...
892                wrong = @rawflags & TAG_HEADER_FLAG_MASK[@version]
893                error = printf "ID3 version %s header flags 0x%X contain invalid flags 0x%X !\n", @version, @rawflags, wrong
894                raise ArgumentError, error
895              end
897              @flags = Hash.new
899              TAG_HEADER_FLAGS[@version].each{ |key,val|
900                # only define the flags which are set..
901                @flags[key] = true   if  (@rawflags & val == 1)
902              }
903             
904             
905           else
906             @raw = nil
907             @version = nil
908             return false
909           end
910 #          f.close
911           #
912           # now parse all the frames
913           #
914           i = ID3::ID3v2headerSize; # we start parsing right after the ID3v2 header
916           while (i < @raw.size) && (@raw[i] != 0)
917              len,frame = parse_frame_header(i)   # this will create the correct frame
918              if len != 0
919                 i += len
920              else
921                 break
922              end
923           end
925           hastag
926       end
927     
928       # ----------------------------------------------------------------------
929       # write
930       #
931       # writes and replaces existing ID3-v2-tag if one is present
932       # Careful, this does NOT merge or append, it overwrites!
933       
934       def write(filename)
935          # check how long the old ID3-v2 tag is
936          
937          # dump ID3-v2-tag
938          
939          # append old audio to new tag
940          
941       end
942       # ----------------------------------------------------------------------
943       # parse_frame_header
944       #
945       # each frame consists of a header of fixed length; 
946       # depending on the ID3version, either 6 or 10 bytes.
947       # and of a data portion which is of variable length,
948       # and which contents might not be parsable by us
949       #
950       # INPUT:   index to where in the @raw data the frame starts
951       # RETURNS: if successful parse: 
952       #             total size in bytes, ID3frame struct
953       #          else:
954       #             0, nil
955       #
956       #
957       #          Struct of type ID3frame which contains:
958       #                the name, size (in bytes), headerX, 
959       #                dataStartX, dataEndX, flags
960       #          the data indices point into the @raw data, so we can cut out
961       #          and parse the data at a later point in time.
962       # 
963       #          total frame size = dataEndX - headerX
964       #          total header size= dataStartX - headerX
965       #          total data size  = dataEndX - dataStartX
966       #
967       private  
968       def parse_frame_header(x)
969          framename = ""; flags = nil
970          size = 0
971          
972          if @version =~ /^2\.2\./
973             frameHeaderSize = 6                     # 2.2.x Header Size is 6 bytes
974             header = @raw[x..x+frameHeaderSize-1]
976             framename = header[0..2]
977             size = (header[3]*256**2)+(header[4]*256)+header[5]
978             flags = nil
979 #            printf "frame: %s , size: %d\n", framename , size
981          elsif @version =~ /^2\.[34]\./
982             # for version 2.3.0 and 2.4.0 the header is 10 bytes long
983             frameHeaderSize = 10
984             header = @raw[x..x+frameHeaderSize-1]
986             framename = header[0..3]
987             size = (header[4]*256**3)+(header[5]*256**2)+(header[6]*256)+header[7]
988             flags= header[8..9]
989 #            printf "frame: %s , size: %d, flags: %s\n", framename , size, flags
991          else
992             # we can't parse higher versions
993             return 0, false
994          end
996          # if this is a valid frame of known type, we return it's total length and a struct
997          # 
998          if ID3::SUPPORTED_SYMBOLS[@version].has_value?(framename)
999              frame = ID3::Frame.new(self, framename, x, x+frameHeaderSize , x+frameHeaderSize + size - 1 , flags)
1000              self[ Framename2symbol[@version][frame.name] ] = frame
1001              return size+frameHeaderSize , frame
1002          else
1003              return 0, nil
1004          end
1005       end
1006       # ----------------------------------------------------------------------
1007       # dump a ID3-v2 tag into a binary array
1008       
1009       public      
1010       def dump
1011         data = ""
1013         # dump all the frames
1014         self.each { |framename,framedata|
1015            data << (framedata.dump || "")
1016         }
1017         # add some padding perhaps
1018         data << "\0" * 32
1019         
1020         # calculate the complete length of the data-section 
1021         size = GenericTag.mungeSize(data.size)
1022         
1023         major,minor = @version.sub(/^2\.([0-9])\.([0-9])/, '\1 \2').split
1024         
1025         # prepend a valid ID3-v2.x header to the data block
1026         header = "ID3" << major.to_i << minor.to_i << @rawflags << size[0] << size[1] << size[2] << size[3]
1028         header + data
1029       end
1030       # ----------------------------------------------------------------------
1032       def set_frame(tag, contents)
1033         self[tag] = Frame.new(tag, version, contents)
1034       end
1035     end  # of class Tag2
1036     
1037     # ==============================================================================
1038     # Class Frame   ID3 Version 2.x.y Frame
1039     #
1040     #      parses ID3v2 frames from a binary array
1041     #      dumps  ID3v2 frames into a binary array
1042     #      allows to modify frame's contents if the frame was decoded..
1043     #
1044     # NOTE:   right now the class Frame is derived from Hash, which is wrong..
1045     #         It should really be derived from something like RestrictedOrderedHash
1046     #         ... a new class, which preserves the order of keys, and which does 
1047     #         strict checking that all keys are present and reference correct values!
1048     #         e.g.   frames["COMMENT"]
1049     #         ==>  {"encoding"=>Byte, "language"=>Chars3, "text1"=>String, "text2"=>String}
1050     #
1051     #         e.g.  user should be able to create a new frame , like: 
1052     #              tag2.frames["COMMENT"] = "right side"
1053     #
1054     #         and the following checks should be done:
1055     #
1056     #            1) if "COMMENT" is a correct key for tag2
1057     #            2) if the "right side" contains the correct keys
1058     #            3) if the "right side" contains the correct value for each key
1059     #
1060     #         In the simplest case, the "right side" might be just a string, 
1061     #         but for most FrameTypes, it's a complex datastructure.. and we need
1062     #         to check it for correctness before doing the assignment..
1063     #
1064     # NOTE2:  the class Tag2 should have hash-like accessor functions to let the user
1065     #         easily access frames and their contents..
1066     #
1067     #         e.g.  tag2[framename] would really access tag2.frames[framename]
1068     #
1069     #         and if that works, we can make tag2.frames private and hidden!
1070     #
1071     #         This means, that when we generate the parse and dump routines dynamically, 
1072     #         we may want to create the corresponding accessor methods for Tag2 class 
1073     #         as well...? or are generic ones enough?
1074     #
1076     class Frame < RestrictedOrderedHash
1077         attr_reader :rawflags, :flags
1078         attr_reader :name, :version
1079         attr_reader :headerStartX, :dataStartX, :dataEndX, :rawdata, :rawheader  # debugging only
1080         ENCODINGS = ["latin1", "utf16", "utf16be", "utf8"]
1082         # ----------------------------------------------------------------------
1083         # return the complete raw frame
1084         
1085         def raw
1086           return @rawheader + @rawdata
1087         end    
1088         # ----------------------------------------------------------------------
1089         alias old_init initialize
1091         def initialize(*a)
1092           super()
1093           if a.length == 6
1094             read *a
1095           elsif a.length == 3 or a.length == 4
1096             create *a
1097           end
1098         end
1099         def create(tag, version, value, encoding = 0)
1100           @version = version
1101           @name = SUPPORTED_SYMBOLS[version][tag]
1102           @rawflags = 0
1103           @rawdata = value
1104           self.parse
1105         end
1106         def recode(encoding)
1107           self["text"] = Iconv.conv(ENCODINGS[encoding], ENCODINGS[self["encoding"]], self["text"])
1108           self["encoding"] = encoding
1109           self
1110         end
1111         def read(tag, name, headerStartX, dataStartX, dataEndX, flags)
1112            @name = name
1113            @headerStartX = headerStartX
1114            @dataStartX   = dataStartX
1115            @dataEndX     = dataEndX
1117            @rawdata   = tag.raw[dataStartX..dataEndX]
1118            @rawheader = tag.raw[headerStartX..dataStartX-1]
1120            # initialize the super class..
1121            old_init
1122            
1123            # parse the darn flags, if there are any..
1125            @version = tag.version  # caching..
1126            case @version
1127              when /2\.2\.[0-9]/
1128                 # no flags, no extra attributes necessary
1130              when /2\.[34]\.0/
1131                 
1132                 
1133                 @rawflags = flags.to_i   # preserve the raw flags (for debugging only)
1135                 if (flags.to_i & FRAME_HEADER_FLAG_MASK[@version] != 0)
1136                    # in this case we need to skip parsing the frame... and skip to the next one...
1137                    wrong = flags.to_i & FRAME_HEADER_FLAG_MASK[@version]
1138                    error = printf "ID3 version %s frame header flags 0x%X contain invalid flags 0x%X !\n", @version, flags, wrong
1139                    raise ArgumentError, error
1140                 end
1142                 @flags = Hash.new
1143                 
1144                 FRAME_HEADER_FLAGS[@version].each{ |key,val|
1145                   # only define the flags which are set..
1146                   @flags[key] = true   if  (flags.to_i & val == 1)
1147                 }
1148                 
1149              else
1150                 raise ArgumentError, "ID3 version #{@version} not recognized when parsing frame header flags\n"
1151             end # parsing flags
1152            self.parse           # now we're using the just defined parsing routine
1154            self
1155           end
1156                  def parse
1157                     # here we GENERATE the code to parse, dump and verify  methods
1158                  
1159                     vars,packing = ID3::FRAME_PARSER[ ID3::FrameName2FrameType[ ID3::Framename2symbol[self.version][self.name]] ]
1161                     # debugging print-out:
1163                     if vars.class == Array
1164                        vars2 = vars.join(",") 
1165                     else
1166                        vars2 = vars
1167                     end
1169                     values = self.rawdata.unpack(packing)
1170                     vars.each { |key|
1171                        self[key] = values.shift
1172                     }
1173                     self.lock   # lock the OrderedHash
1174                  end
1176                  def data
1177                    vars,packing = ID3::FRAME_PARSER[ ID3::FrameName2FrameType[ ID3::Framename2symbol[self.version][self.name]] ]
1178                     self.values.pack(packing)     # we depend on an OrderedHash, so the values are in the correct order!!!
1179                  end
1180                  def dump
1181                    begin
1182                     header  = self.name.dup         # we want the value! not the reference!!
1183                     len     = data.length
1184                     if self.version =~ /^2\.2\./
1185                        byte2,rest = len.divmod(256**2)
1186                        byte1,byte0 = rest.divmod(256)
1188                        header << byte2 << byte1 << byte0
1190                     elsif self.version =~ /^2\.[34]\./          # 10-byte header
1191                        byte3,rest = len.divmod(256**3)
1192                        byte2,rest = rest.divmod(256**2)
1193                        byte1,byte0 = rest.divmod(256)            
1195                        flags1,flags0 = self.rawflags.divmod(256)
1196                        
1197                        header << byte3 << byte2 << byte1 << byte0 << flags1 << flags0
1198                     end
1199                     header << data
1200                   rescue
1201                   end
1202                  end
1203         # ----------------------------------------------------------------------
1205        
1206     
1207     end  # of class Frame
1209     # ==============================================================================
1210     
1213 end   # of module ID3