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