Tags are correctly recoded.
[amarok_sonynw.git] / walkgirl / id3.rb
blob25df4b00ced0fc6d37bde5dee19f7e992def60d7
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) , 'CZ*' ] ,
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_reader :audioMD5sum                  # MD5sum of the audio portion of the file
445       attr_reader :pwd,          :filename      # PWD and relative path/name how file was first referenced
446       attr_reader :dirname,      :basename      # absolute dirname and basename of the file (computed)
448       attr_accessor :tagID3v1, :tagID3v2
449       attr_reader   :hasID3tag                  # either false, or a string with all version numbers found
451       # ----------------------------------------------------------------------------
452       # initialize
453       #
454       #   AudioFile.new   does NOT open the file, but scans it and parses the info
456       #   e.g.:  ID3::AudioFile.new('mp3/a.mp3')
458       def initialize(filename)
459           @filename     = filename      # similar to path method from class File, which is a mis-nomer!
460           @pwd          = ENV["PWD"]
461           @dirname      = File.dirname( "#{@pwd}/#{@filename}" )   # just sugar
462           @basename     = File.basename( "#{@pwd}/#{@filename}" )  # just sugar
463           
464           @tagID3v1     = nil
465           @tagID3v2     = nil
466           
467           audioStartX   = 0
468           audioEndX     = File.size(filename)
470           if ID3.hasID3v1tag?(@filename)
471               @tagID3v1 = Tag1.new
472               @tagID3v1.read(@filename)
474               audioEndX -= ID3::ID3v1tagSize
475           end
476           if ID3.hasID3v2tag?(@filename) 
477               @tagID3v2 = Tag2.new
478               @tagID3v2.read(@filename)
480               audioStartX = @tagID3v2.raw.size
481           end
482           
483           # audioStartX audioEndX indices into the file need to be set
484           @audioStartX = audioStartX 
485           @audioEndX   = audioEndX
486           
487           # user may compute the MD5sum of the audio content later..
488           # but we're only doing this if the user requests it..
490           @audioMD5sum = nil
491       end
492       
493       # ----------------------------------------------------------------------------
494       # audioMD5sum
495       #     if the user tries to access @audioMD5sum, it will be computed for him, 
496       #     unless it was previously computed. We try to calculate that only once 
497       #     and on demand, because it's a bit expensive to compute..
498       
499       def audioMD5sum
500          if ! @audioMD5sum 
501             
502             File.open( File.join(@dirname,@basename) ) { |f|
503               f.seek(@audioStartX)
504               @audioMD5sum = MD5.new( f.read(@audioEndX - @audioStartX + 1) )
505             }
507          end
508          @audioMD5sum
509       end
510       # ----------------------------------------------------------------------------
511       # writeMD5sum
512       #     write the filename and MD5sum of the audio portion into an ascii file 
513       #     in the same location as the audio file, but with suffix .md5
514       #
515       #     computes the @audioMD5sum, if it wasn't previously computed..
517       def writeMD5sum
518       
519          self.audioMD5sum if ! @audioMD5sum  # compute MD5sum if it's not computed yet
520          
521          base = @basename.sub( /(.)\.[^.]+$/ , '\1')
522          base += '.md5'
523          File.open( File.join(@dirname,base) ,"w") { |f| 
524             f.printf("%s   %s\n",  File.join(@dirname,@basename), @audioMD5sum)
525          }
526          @audioMD5sum
527       end
528       # ----------------------------------------------------------------------------
529       # verifyMD5sum
530       #     compare the audioMD5sum against a previously stored md5sum file
531       #     and returns boolean value of comparison
532       #
533       #     If no md5sum file existed, we create one and return true.
534       #
535       #     computes the @audioMD5sum, if it wasn't previously computed..
537       def verifyMD5sum
539          oldMD5sum = ''
540          
541          self.audioMD5sum if ! @audioMD5sum  # compute MD5sum if it's not computed yet
543          base = @basename.sub( /(.)\.[^.]+$/ , '\1')   # remove suffix from audio-file
544          base += '.md5'                                # add new suffix .md5
545          md5name = File.join(@dirname,base)
546          
547          # if a MD5-file doesn't exist, we should create one and return TRUE ...
548          if File.exists?(md5name)
549             File.open( md5name ,"r") { |f| 
550                oldname,oldMD5sum = f.readline.split  # read old MD5-sum
551             }
552          else
553             oldMD5sum = self.writeMD5sum        # create MD5-file and return true..
554          end
555          @audioMD5sum == oldMD5sum
556          
557       end
558       # ----------------------------------------------------------------------------
559       def version
560          a = Array.new
561          a.push(@tagID3v1.version) if @tagID3v1
562          a.push(@tagID3v2.version) if @tagID3v2
563          return nil   if a == []
564          a.join(' ') 
565       end
566       alias versions version
567       # ----------------------------------------------------------------------------
569          
570       
571     end   # of class AudioFile
573     
574     # ==============================================================================
575     # Class RestrictedOrderedHash
576     
577     class RestrictedOrderedHash < Hash
579         attr_accessor :count , :order, :locked
581         def lock
582           @locked = true
583         end
584         
585         def initialize 
586           @locked = false
587           @count  = 0
588           @order  = []
589           super
590         end
592 #        alias old_store []=
594         def []= (key,val)
595           if self[key]
596             super
597           else
598              if @locked
599                 # we're not allowed to add new keys!
600                raise ArgumentError, "You can not add new keys! The ID3-frame #{@name} has fixed entries!\n" +
601                "               valid key are: " + self.keys.join(",") +"\n"
603              else 
604                 @count += 1
605                 @order += [key]
606                 super
607              end
608           end
609         end
610         
611         def values
612           array = []
613           @order.each { |key|
614              array.push self[key]
615           }
616           array
617         end
619         # returns the human-readable ordered hash in correct order .. ;-)
620         
621         def inspect
622            first = true
623            str = "{"
624            self.order.each{ |key|
625              str += ", " if !first
626              str += key.inspect
627              str += "=>"
628              str += (self[key]).inspect
629              first = false
630            }
631            str +="}"
632         end
633         
634         # users can not delete entries from a locked hash..
635         
636         alias old_delete delete
637         
638         def delete (key)
639            if !@locked
640               old_delete(key)
641               @order.delete(key)
642            end
643         end
644         
645     end
646     
647     
648     
649     # ==============================================================================
650     # Class GenericTag
651     #
652     # as per ID3-definition, the frames are in no fixed order! that's why Hash is OK
654     class GenericTag < Hash        ###### should this be RestrictedOrderedHash as well? 
655        attr_reader :version, :raw
657        # these definitions are to prevent users from inventing their own field names..
658        # but on the other hand, they should be able to create a new valid field, if
659        # it's not yet in the current tag, but it's valid for that ID3-version...
660        # ... so hiding this, is not enough!
661        
662 #       alias old_set []=
663 #       private :old_set
664   
665        # ----------------------------------------------------------------------
667        def []=(key,val)
668         if @version == ""
669           raise ArgumentError, "undefined version of ID3-tag! - set version before accessing components!\n" 
670         else
671           if ID3::SUPPORTED_SYMBOLS[@version].keys.include?(key)
672             if !val.is_a?(ID3::Frame) and respond_to? :set_frame
673               set_frame(key,val)
674             else
675               super
676             end
677           else 
678              # exception
679              raise ArgumentError, "Incorrect ID3-field \"#{key}\" for ID3 version #{@version}\n" +
680              "               valid fields are: " + SUPPORTED_SYMBOLS[@version].keys.join(",") +"\n"
681           end
682         end
683        end
684        # ----------------------------------------------------------------------
685        # convert the 4 bytes found in the id3v2 header and return the size
686        private
687        def unmungeSize(bytes)
688          size = 0
689          j = 0; i = 3 
690          while i >= 0
691             size += 128**i * (bytes[j] & 0x7f)
692             j += 1
693             i -= 1
694          end
695          return size
696        end
697        # ----------------------------------------------------------------------
698        # convert the size into 4 bytes to be written into an id3v2 header
699        public
700        def GenericTag.mungeSize(size)
701          bytes = Array.new(4,0)
702          j = 0;  i = 3
703          while i >= 0
704            bytes[j],size = size.divmod(128**i)
705            j += 1
706            i -= 1
707          end
709          return bytes
710        end
711        # ----------------------------------------------------------------------------
712         
713     end # of class GenericTag
714     
715     # ==============================================================================
716     # Class Tag1    ID3 Version 1.x Tag
717     #
718     #      parses ID3v1 tags from a binary array
719     #      dumps  ID3v1 tags into a binary array
720     #      allows to modify tag's contents
722     class Tag1 < GenericTag
724        # ----------------------------------------------------------------------
725        # read     reads a version 1.x ID3tag
726        #
727        #     30 title
728        #     30 artist
729        #     30 album
730        #      4 year
731        #     30 comment
732        #      1 genre
734        def read(filename)
735          f = File.open(filename, 'r')
736          f.seek(-ID3::ID3v1tagSize, IO::SEEK_END)
737          hastag = (f.read(3) == 'TAG')
738          if hastag
739            f.seek(-ID3::ID3v1tagSize, IO::SEEK_END)
740            @raw = f.read(ID3::ID3v1tagSize)
742 #           self.parse!(raw)    # we should use "parse!" instead of re-coding everything..
744            if (raw[ID3v1versionbyte] == 0) 
745               @version = "1.1"
746            else
747               @version = "1.0"
748            end
749          else
750            @raw = @version = nil
751          end
752          f.close
753          #
754          # now parse all the fields
756          ID3::SUPPORTED_SYMBOLS[@version].each{ |key,val|
757             if val.class == Range
758                self[key] = @raw[val].squeeze(" \000").chomp(" ").chomp("\000")
759             elsif val.class == Fixnum
760                self[key] = @raw[val].to_s
761             else 
762                # this can't happen the way we defined the hash..
763 #              printf "unknown key/val : #{key} / #{val}  ; val-type: %s\n", val.type
764             end       
765          }
766          hastag
767        end
768        # ----------------------------------------------------------------------
769        # write    writes a version 1.x ID3tag
770        #
771        # not implemented yet..
772        #
773        # need to loacte old tag, and remove it, then append new tag..
774        #
775        # always upgrade version 1.0 to 1.1 when writing
777        
778        # ----------------------------------------------------------------------
779        # this routine modifies self, e.g. the Tag1 object
780        #
781        # tag.parse!(raw)   returns boolean value, showing if parsing was successful
782        
783        def parse!(raw)
785          return false    if raw.size != ID3::ID3v1tagSize
787          if (raw[ID3v1versionbyte] == 0) 
788             @version = "1.1"
789          else
790             @version = "1.0"
791          end
793          self.clear    # remove all entries from Hash, we don't want left-overs..
795          ID3::SUPPORTED_SYMBOLS[@version].each{ |key,val|
796             if val.class == Range
797                self[key] = raw[val].squeeze(" \000").chomp(" ").chomp("\000")
798             elsif val.class == Fixnum
799                self[key] = raw[val].to_s
800             else 
801                # this can't happen the way we defined the hash..
802 #              printf "unknown key/val : #{key} / #{val}  ; val-type: %s\n", val.class
803             end       
804          }
805          @raw = raw
806          return true
807        end
808        # ----------------------------------------------------------------------
809        # dump version 1.1 ID3 Tag into a binary array
810        #
811        # although we provide this method, it's stongly discouraged to use it, 
812        # because ID3 version 1.x tags are inferior to version 2.x tags, as entries
813        # are often truncated and hence often useless..
814        
815        def dump
816          zeroes = "\0" * 32
817          raw = "\0" * ID3::ID3v1tagSize
818          raw[0..2] = 'TAG'
820          self.each{ |key,value|
822            range = ID3::Symbol2framename['1.1'][key]
824            if range.class == Range 
825               length = range.last - range.first + 1
826               paddedstring = value + zeroes
827               raw[range] = paddedstring[0..length-1]
828            elsif range.class == Fixnum
829               raw[range] = value.to_i
830            else
831               # this can't happen the way we defined the hash..
832               next
833            end
834          }
836          return raw
837        end
838        # ----------------------------------------------------------------------
839     end  # of class Tag1
840     
841     # ==============================================================================
842     # Class Tag2    ID3 Version 2.x.y Tag
843     #
844     #      parses ID3v2 tags from a binary array
845     #      dumps  ID3v2 tags into a binary array
846     #      allows to modify tag's contents
847     #
848     #      as per definition, the frames are in no fixed order
849     
850     class Tag2 < GenericTag
851       
852       attr_reader :rawflags, :flags
853       attr_writer :version
854       
855       def initialize
856          @rawflags = 0
857          @flags    = {}
858          @version = "2.4.0"
859          super
860       end
861       
862       def read(filename)
863           f = File.open(filename, 'r')
864           hastag = (f.read(3) == "ID3")
865           if hastag
866             major = f.getc
867             minor = f.getc
868             @version = "2." + major.to_s + '.' + minor.to_s
869             @rawflags = f.getc
870             size = ID3::ID3v2headerSize + unmungeSize(f.read(4))
871             f.seek(0)
872             @raw = f.read(size) 
873             
874             # parse the raw flags:
875             if (@rawflags & TAG_HEADER_FLAG_MASK[@version] != 0)
876                # in this case we need to skip parsing the frame... and skip to the next one...
877                wrong = @rawflags & TAG_HEADER_FLAG_MASK[@version]
878                error = printf "ID3 version %s header flags 0x%X contain invalid flags 0x%X !\n", @version, @rawflags, wrong
879                raise ArgumentError, error
880              end
882              @flags = Hash.new
884              TAG_HEADER_FLAGS[@version].each{ |key,val|
885                # only define the flags which are set..
886                @flags[key] = true   if  (@rawflags & val == 1)
887              }
888             
889             
890           else
891             @raw = nil
892             @version = nil
893             return false
894           end
895           f.close
896           #
897           # now parse all the frames
898           #
899           i = ID3::ID3v2headerSize; # we start parsing right after the ID3v2 header
901           while (i < @raw.size) && (@raw[i] != 0)
902              len,frame = parse_frame_header(i)   # this will create the correct frame
903              if len != 0
904                 i += len
905              else
906                 break
907              end
908           end
910           hastag
911       end
912     
913       # ----------------------------------------------------------------------
914       # write
915       #
916       # writes and replaces existing ID3-v2-tag if one is present
917       # Careful, this does NOT merge or append, it overwrites!
918       
919       def write(filename)
920          # check how long the old ID3-v2 tag is
921          
922          # dump ID3-v2-tag
923          
924          # append old audio to new tag
925          
926       end
927       # ----------------------------------------------------------------------
928       # parse_frame_header
929       #
930       # each frame consists of a header of fixed length; 
931       # depending on the ID3version, either 6 or 10 bytes.
932       # and of a data portion which is of variable length,
933       # and which contents might not be parsable by us
934       #
935       # INPUT:   index to where in the @raw data the frame starts
936       # RETURNS: if successful parse: 
937       #             total size in bytes, ID3frame struct
938       #          else:
939       #             0, nil
940       #
941       #
942       #          Struct of type ID3frame which contains:
943       #                the name, size (in bytes), headerX, 
944       #                dataStartX, dataEndX, flags
945       #          the data indices point into the @raw data, so we can cut out
946       #          and parse the data at a later point in time.
947       # 
948       #          total frame size = dataEndX - headerX
949       #          total header size= dataStartX - headerX
950       #          total data size  = dataEndX - dataStartX
951       #
952       private  
953       def parse_frame_header(x)
954          framename = ""; flags = nil
955          size = 0
956          
957          if @version =~ /^2\.2\./
958             frameHeaderSize = 6                     # 2.2.x Header Size is 6 bytes
959             header = @raw[x..x+frameHeaderSize-1]
961             framename = header[0..2]
962             size = (header[3]*256**2)+(header[4]*256)+header[5]
963             flags = nil
964 #            printf "frame: %s , size: %d\n", framename , size
966          elsif @version =~ /^2\.[34]\./
967             # for version 2.3.0 and 2.4.0 the header is 10 bytes long
968             frameHeaderSize = 10
969             header = @raw[x..x+frameHeaderSize-1]
971             framename = header[0..3]
972             size = (header[4]*256**3)+(header[5]*256**2)+(header[6]*256)+header[7]
973             flags= header[8..9]
974 #            printf "frame: %s , size: %d, flags: %s\n", framename , size, flags
976          else
977             # we can't parse higher versions
978             return 0, false
979          end
981          # if this is a valid frame of known type, we return it's total length and a struct
982          # 
983          if ID3::SUPPORTED_SYMBOLS[@version].has_value?(framename)
984              frame = ID3::Frame.new(self, framename, x, x+frameHeaderSize , x+frameHeaderSize + size - 1 , flags)
985              self[ Framename2symbol[@version][frame.name] ] = frame
986              return size+frameHeaderSize , frame
987          else
988              return 0, nil
989          end
990       end
991       # ----------------------------------------------------------------------
992       # dump a ID3-v2 tag into a binary array
993       
994       public      
995       def dump
996         data = ""
998         # dump all the frames
999         self.each { |framename,framedata|
1000            data << (framedata.dump || "")
1001         }
1002         # add some padding perhaps
1003         data << "\0" * 32
1004         
1005         # calculate the complete length of the data-section 
1006         size = GenericTag.mungeSize(data.size)
1007         
1008         major,minor = @version.sub(/^2\.([0-9])\.([0-9])/, '\1 \2').split
1009         
1010         # prepend a valid ID3-v2.x header to the data block
1011         header = "ID3" << major.to_i << minor.to_i << @rawflags << size[0] << size[1] << size[2] << size[3]
1013         header + data
1014       end
1015       # ----------------------------------------------------------------------
1017       def set_frame(tag, contents)
1018         self[tag] = Frame.new(tag, version, contents)
1019       end
1020     end  # of class Tag2
1021     
1022     # ==============================================================================
1023     # Class Frame   ID3 Version 2.x.y Frame
1024     #
1025     #      parses ID3v2 frames from a binary array
1026     #      dumps  ID3v2 frames into a binary array
1027     #      allows to modify frame's contents if the frame was decoded..
1028     #
1029     # NOTE:   right now the class Frame is derived from Hash, which is wrong..
1030     #         It should really be derived from something like RestrictedOrderedHash
1031     #         ... a new class, which preserves the order of keys, and which does 
1032     #         strict checking that all keys are present and reference correct values!
1033     #         e.g.   frames["COMMENT"]
1034     #         ==>  {"encoding"=>Byte, "language"=>Chars3, "text1"=>String, "text2"=>String}
1035     #
1036     #         e.g.  user should be able to create a new frame , like: 
1037     #              tag2.frames["COMMENT"] = "right side"
1038     #
1039     #         and the following checks should be done:
1040     #
1041     #            1) if "COMMENT" is a correct key for tag2
1042     #            2) if the "right side" contains the correct keys
1043     #            3) if the "right side" contains the correct value for each key
1044     #
1045     #         In the simplest case, the "right side" might be just a string, 
1046     #         but for most FrameTypes, it's a complex datastructure.. and we need
1047     #         to check it for correctness before doing the assignment..
1048     #
1049     # NOTE2:  the class Tag2 should have hash-like accessor functions to let the user
1050     #         easily access frames and their contents..
1051     #
1052     #         e.g.  tag2[framename] would really access tag2.frames[framename]
1053     #
1054     #         and if that works, we can make tag2.frames private and hidden!
1055     #
1056     #         This means, that when we generate the parse and dump routines dynamically, 
1057     #         we may want to create the corresponding accessor methods for Tag2 class 
1058     #         as well...? or are generic ones enough?
1059     #
1061     class Frame < RestrictedOrderedHash
1062         attr_reader :rawflags, :flags
1063         attr_reader :name, :version
1064         attr_reader :headerStartX, :dataStartX, :dataEndX, :rawdata, :rawheader  # debugging only
1065         ENCODINGS = ["latin1", "utf16", "utf16be", "utf8"]
1067         # ----------------------------------------------------------------------
1068         # return the complete raw frame
1069         
1070         def raw
1071           return @rawheader + @rawdata
1072         end    
1073         # ----------------------------------------------------------------------
1074         alias old_init initialize
1076         def initialize(*a)
1077           super()
1078           if a.length == 6
1079             read *a
1080           elsif a.length == 3
1081             create *a
1082           end
1083         end
1084         def create(tag, version, value)
1085           @version = version
1086           @name = SUPPORTED_SYMBOLS[version][tag]
1087           @rawflags = 0
1088           self["encoding"] = 0
1089           self["text"] = value
1090         end
1091         def recode(encoding)
1092           new_frame = self.clone
1093           new_frame["text"] = Iconv.conv(ENCODINGS[encoding], ENCODINGS[self["encoding"]], self["text"])
1094           new_frame["encoding"] = encoding
1095           new_frame
1096         end
1097         def read(tag, name, headerStartX, dataStartX, dataEndX, flags)
1098            @name = name
1099            @headerStartX = headerStartX
1100            @dataStartX   = dataStartX
1101            @dataEndX     = dataEndX
1103            @rawdata   = tag.raw[dataStartX..dataEndX]
1104            @rawheader = tag.raw[headerStartX..dataStartX-1]
1106            # initialize the super class..
1107            old_init
1108            
1109            # parse the darn flags, if there are any..
1111            @version = tag.version  # caching..
1112            case @version
1113              when /2\.2\.[0-9]/
1114                 # no flags, no extra attributes necessary
1116              when /2\.[34]\.0/
1117                 
1118                 
1119                 @rawflags = flags.to_i   # preserve the raw flags (for debugging only)
1121                 if (flags.to_i & FRAME_HEADER_FLAG_MASK[@version] != 0)
1122                    # in this case we need to skip parsing the frame... and skip to the next one...
1123                    wrong = flags.to_i & FRAME_HEADER_FLAG_MASK[@version]
1124                    error = printf "ID3 version %s frame header flags 0x%X contain invalid flags 0x%X !\n", @version, flags, wrong
1125                    raise ArgumentError, error
1126                 end
1128                 @flags = Hash.new
1129                 
1130                 FRAME_HEADER_FLAGS[@version].each{ |key,val|
1131                   # only define the flags which are set..
1132                   @flags[key] = true   if  (flags.to_i & val == 1)
1133                 }
1134                 
1135              else
1136                 raise ArgumentError, "ID3 version #{@version} not recognized when parsing frame header flags\n"
1137             end # parsing flags
1138            self.parse           # now we're using the just defined parsing routine
1140            self
1141           end
1142                  def parse
1143                     # here we GENERATE the code to parse, dump and verify  methods
1144                  
1145                     vars,packing = ID3::FRAME_PARSER[ ID3::FrameName2FrameType[ ID3::Framename2symbol[self.version][self.name]] ]
1147                     # debugging print-out:
1149                     if vars.class == Array
1150                        vars2 = vars.join(",") 
1151                     else
1152                        vars2 = vars
1153                     end
1155                     values = self.rawdata.unpack(packing)
1156                     vars.each { |key|
1157                        self[key] = values.shift
1158                     }
1159                     self.lock   # lock the OrderedHash
1160                  end
1162                  def dump
1163                    vars,packing = ID3::FRAME_PARSER[ ID3::FrameName2FrameType[ ID3::Framename2symbol[self.version][self.name]] ]
1164                    begin
1165                     data = self.values.pack(packing)     # we depend on an OrderedHash, so the values are in the correct order!!!
1166                     header  = self.name.dup         # we want the value! not the reference!!
1167                     len     = data.length
1168                     if self.version =~ /^2\.2\./
1169                        byte2,rest = len.divmod(256**2)
1170                        byte1,byte0 = rest.divmod(256)
1172                        header << byte2 << byte1 << byte0
1174                     elsif self.version =~ /^2\.[34]\./          # 10-byte header
1175                        byte3,rest = len.divmod(256**3)
1176                        byte2,rest = rest.divmod(256**2)
1177                        byte1,byte0 = rest.divmod(256)            
1179                        flags1,flags0 = self.rawflags.divmod(256)
1180                        
1181                        header << byte3 << byte2 << byte1 << byte0 << flags1 << flags0
1182                     end
1183                     header << data
1184                   rescue
1185                   end
1186                  end
1187         # ----------------------------------------------------------------------
1189        
1190     
1191     end  # of class Frame
1193     # ==============================================================================
1194     
1197 end   # of module ID3