2 // Mp3Reader.cs : Reads an Id3v1.0,1.1,2.2,2.3 tag from the given stream
5 // Raphaël Slinckx <raf.raf@wol.be>
7 // Copyright 2004 (C) Raphaël Slinckx (ported from http://entagged.sourceforge.net)
11 // Permission is hereby granted, free of charge, to any person obtaining a
12 // copy of this software and associated documentation files (the "Software"),
13 // to deal in the Software without restriction, including without limitation
14 // the rights to use, copy, modify, merge, publish, distribute, sublicense,
15 // and/or sell copies of the Software, and to permit persons to whom the
16 // Software is furnished to do so, subject to the following conditions:
18 // The above copyright notice and this permission notice shall be included in
19 // all copies or substantial portions of the Software.
21 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26 // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
27 // DEALINGS IN THE SOFTWARE.
33 using System
.Collections
;
35 namespace Beagle
.Util
.AudioUtil
{
39 public class Id3v2TagSynchronizer
{
41 public int synchronize (byte[] b
)
47 while (oldPtr
< b
.Length
&& newPtr
< b
.Length
) {
50 if (((cur
&0xFF) == 0xFF) && (oldPtr
< b
.Length
)) { //First part of synchronization
52 if (cur
!= 0x00) { //If different, we have not a synchronization, so we take this value
58 //We have finished, retun the length of the new data.
63 public class Id3v23TagReader
{
65 public static int ID3V22
= 0;
66 public static int ID3V23
= 1;
68 private Hashtable conversion
;
69 private ASCIIEncoding ascii
= new ASCIIEncoding ();
70 private UTF8Encoding utf
= new UTF8Encoding ();
72 public Id3v23TagReader ()
74 this.conversion
= initConversionTable ();
77 public Tag
Read (byte[] data
, int tagSize
, byte flags
, int version
)
83 bool extended
= ((flags
&64) == 64) ? true : false;
84 //--------------------------------------------------------------
85 if (version
== ID3V23
&& extended
)
86 elapsedBytes
= processExtendedHeader (data
);
87 //--------------------------------------------------------------
88 Hashtable ht
= new Hashtable (10);
90 int specSize
= (version
== ID3V22
) ? 3 : 4;
91 for (int a
= 0; a
< tagSize
; a
+=elapsedBytes
) {
92 string field
= ascii
.GetString (data
, elapsedBytes
, specSize
);
94 if (data
[elapsedBytes
] == 0)
96 elapsedBytes
+= specSize
;
98 //-------------------------------------
99 int frameSize
= ReadInteger (data
, elapsedBytes
, version
);
100 elapsedBytes
+= specSize
;
102 if (version
== ID3V23
) {
103 //Frame flags, skipping
107 if ((frameSize
+ elapsedBytes
) > tagSize
|| frameSize
<= 0){
108 throw new Exception ("Frame size error, skipping file");
111 if (version
== ID3V22
) {
112 //We try to translte the tag from a 3 letters
113 //v22 format to a 4 letter v23-4 format
114 field
= convertFromId3v22 (field
);
115 //if the conversion do not succeed,
116 //then the frame is lost
120 byte[] content
= new byte[frameSize
];
122 for (int i
= 0; i
<content
.Length
; i
++)
123 content
[i
] = data
[i
+elapsedBytes
];
125 ht
[field
] = toSimplestring (field
, content
);
127 elapsedBytes
+= frameSize
;
130 //--------------------------------------------------------------
132 foreach (DictionaryEntry en
in ht
) {
133 string field
= (string) en
.Key
;
134 string content
= (string) en
.Value
;
138 else if (field
== "TPE1")
139 tag
.Artist
= content
;
140 else if (field
== "TALB")
142 else if (field
== "TYER")
144 else if (field
== "COMM") {
145 tag
.Comment
= content
.Substring (4);
146 } else if (field
== "TRCK")
148 else if (field
== "TCON")
155 private Hashtable
initConversionTable ()
157 Hashtable ht
= new Hashtable (64, 1.0f
);
159 "BUF", "CNT", "COM", "CRA",
160 "CRM", "ETC", "EQU", "GEO",
161 "IPL", "LNK", "MCI", "MLL",
162 "PIC", "POP", "REV", "RVA",
163 "SLT", "STC", "TAL", "TBP",
164 "TCM", "TCO", "TCR", "TDA",
165 "TDY", "TEN","TFT", "TIM",
166 "TKE", "TLA", "TLE", "TMT",
167 "TOA", "TOF", "TOL", "TOR",
168 "TOT", "TP1", "TP2", "TP3",
169 "TP4", "TPA", "TPB", "TRC",
170 "TRD", "TRK", "TSI", "TSS",
171 "TT1", "TT2", "TT3", "TXT",
172 "TXX", "TYE", "UFI", "ULT",
173 "WAF", "WAR", "WAS", "WCM",
177 "RBUF", "PCNT", "COMM", "AENC",
178 "", "ETCO", "EQUA", "GEOB",
179 "IPLS", "LINK", "MCDI", "MLLT",
180 "APIC", "POPM", "RVRB", "RVAD",
181 "SYLT", "SYTC", "TALB", "TBPM",
182 "TCOM", "TCON", "TCOP", "TDAT",
183 "TDLY", "TENC", "TFLT", "TIME",
184 "TKEY", "TLAN", "TLEN", "TMED",
185 "TOPE", "TOFN", "TOLY", "TORY",
186 "TOAL", "TPE1", "TPE2", "TPE3",
187 "TPE4", "TPOS", "TPUB", "TSRC",
188 "TRDA", "TRCK", "TSIZ", "TSSE",
189 "TIT1", "TIT2", "TIT3", "TEXT",
190 "TXXX", "TYER", "UFID", "USLT",
191 "WOAF", "WOAR", "WOAS", "WCOM",
192 "WCOP", "WPUB", "WXXX"
195 for (int i
= 0; i
<v22
.Length
; i
++)
201 private string convertFromId3v22 (string field
)
203 string s
= (string) this.conversion
[field
];
211 //returns the number of bytes to skip, to skip the extended data
212 private int processExtendedHeader (byte[] data
)
214 int extsize
= ReadInteger (data
,0, ID3V23
);
215 // The extended header size includes those first four bytes.
219 private string toSimplestring (string field
, byte[] content
)
221 //-----------HERE FRAME SPECIFICITIES WILL BE HANDLED !! -----
223 if (field
.StartsWith ("T") && !field
.StartsWith ("TX")) {
225 //Encoding|string| (Termination)
227 //Are we null terminated ? x00 or x0000 if ISO or UTF
228 int idx
= indexOfFirstNull (content
, 1);
233 length
= content
.Length
-1;
235 if (content
[0] == 1) {
237 return utf
.GetString (content
, 1, length
);
240 //if content[0] == 0 , we use ISO-8859-1, if not, this is a default.
241 return ascii
.GetString (content
, 1, length
);
243 else if (field
.StartsWith ("COMM") || field
.StartsWith ("WX")
244 || field
.StartsWith ("IPLS") || field
.StartsWith ("USLT")
245 || field
.StartsWith ("TX") || field
.StartsWith ("USER")) {
250 //We get here the custom TXXX and WXXX frames, same constitution
251 int length
= content
.Length
- 1;
253 if (content
[0] == 1) {
255 return utf
.GetString (content
, 1, length
);
258 //if content[0] == 0 , we use ISO-8859-1, if not, this is a default.
259 return ascii
.GetString (content
, 1, length
);
261 else if (field
.StartsWith ("UFID") || field
.StartsWith ("MCDI")
262 || field
.StartsWith ("ETCO") || field
.StartsWith ("MLLT")
263 || field
.StartsWith ("SYTC") || field
.StartsWith ("RVAD")
264 || field
.StartsWith ("EQUA") || field
.StartsWith ("RVRB")
265 || field
.StartsWith ("APIC") || field
.StartsWith ("GEOB")
266 || field
.StartsWith ("SYLT") || field
.StartsWith ("PCNT")
267 || field
.StartsWith ("POPM") || field
.StartsWith ("RBUF")
268 || field
.StartsWith ("AENC") || field
.StartsWith ("LINK")
269 || field
.StartsWith ("POSS") || field
.StartsWith ("OWNE")
270 || field
.StartsWith ("COMR") || field
.StartsWith ("ENCR")
271 || field
.StartsWith ("GRID") || field
.StartsWith ("PRIV")) {
273 //|string| aka Binary data
275 //WARNING APIC, GEOB, OWNE, SYLT, COMR use a mix of ISO and UTF encoding
276 //They are saved as binary data and, one to further process the contained data
277 //has to trasform the string o bytes (using iso) then parse appropriately !
279 return ascii
.GetString (content
);
281 else if (field
.StartsWith ("W") && !field
.StartsWith ("WX")) {
283 //|string| (Termination)
285 int idx
= indexOfFirstNull (content
, 1);
290 length
= content
.Length
-1;
292 return ascii
.GetString (content
, 1, length
);
295 //We get here for all the frame that are not supported by the ID3 document
296 //We simply save the content as binary data !
297 Console
.Error
.Write ("Unknown Tag frame: " + field
);
298 return ascii
.GetString (content
);
302 private int indexOfFirstNull (byte[] b
, int offset
)
304 for (int i
= offset
; i
<b
.Length
; i
++)
310 private int ReadInteger (byte[] bb
, int offset
, int version
)
314 if (version
== ID3V23
)
315 value += (bb
[offset
]& 0xFF) << 24;
316 value += (bb
[offset
+1]& 0xFF) << 16;
317 value += (bb
[offset
+2]& 0xFF) << 8;
318 value += (bb
[offset
+3]& 0xFF);
325 public class Id3v1TagReader
{
327 public static string[] ID3V1_GENRES
= new string[] {"Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", "Hip-Hop", "Jazz",
328 "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", "Rap", "Reggae", "Rock", "Techno", "Industrial", "Alternative",
329 "Ska", "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient", "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion",
330 "Trance", "Classical", "Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel", "Noise", "AlternRock",
331 "Bass", "Soul", "Punk", "Space", "Meditative", "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic",
332 "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance", "Dream", "Southern Rock", "Comedy",
333 "Cult", "Gangsta", "Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American", "Cabaret", "New Wave",
334 "Psychadelic", "Rave", "Showtunes", "Trailer", "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro",
335 "Musical", "Rock & Roll", "Hard Rock", "Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion", "Bebob", "Latin", "Revival",
336 "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock", "Slow Rock",
337 "Big Band", "Chorus", "Easy Listening", "Acoustic", "Humour", "Speech", "Chanson", "Opera", "Chamber Music", "Sonata",
338 "Symphony", "Booty Bass", "Primus", "Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba", "Folklore", "Ballad",
339 "Power Ballad", "Rhythmic Soul", "Freestyle", "Duet", "Punk Rock", "Drum Solo", "A capella", "Euro-House", "Dance Hall"};
341 public Tag
Read (FileStream fs
)
343 byte[] buf
= new byte[30];
344 ASCIIEncoding ascii
= new ASCIIEncoding ();
345 Tag tag
= new Tag ();
347 //Check wether the file contains an Id3v1 tag--------------------------------
348 fs
.Seek (-128, SeekOrigin
.End
);
351 fs
.Seek (0, SeekOrigin
.Begin
);
353 string tagS
= ascii
.GetString (buf
, 0, 3);
355 throw new Exception ("There is no Id3v1 Tag in this file");
357 //Parse the tag -)------------------------------------------------
358 fs
.Seek (-128 + 3, SeekOrigin
.End
);
359 fs
.Read (buf
, 0, 30);
360 string songName
= ascii
.GetString (buf
, 0, 30);
361 songName
= songName
.Trim ();
362 //------------------------------------------------
363 fs
.Read (buf
, 0, 30);
364 string artist
= ascii
.GetString (buf
, 0, 30);
365 artist
= artist
.Trim ();
366 //------------------------------------------------
367 fs
.Read (buf
, 0, 30);
368 string album
= ascii
.GetString (buf
, 0, 30);
369 album
= album
.Trim ();
370 //------------------------------------------------
372 string year
= ascii
.GetString (buf
, 0, 4);
374 //------------------------------------------------
375 fs
.Read (buf
, 0, 30);
377 string trackNumber
= "";
381 trackNumber
= buf
[29].ToString ();
383 comment
= ascii
.GetString (buf
, 0, 28);
384 comment
= comment
.Trim ();
387 comment
= ascii
.GetString (buf
, 0, 30);
388 comment
= comment
.Trim ();
390 //------------------------------------------------
392 byte genreByte
= buf
[0];
394 tag
.Title
= songName
;
398 tag
.Comment
= comment
;
399 tag
.Track
= trackNumber
;
400 tag
.Genre
= TranslateGenre (genreByte
);
402 fs
.Seek (0, SeekOrigin
.Begin
);
407 private string TranslateGenre (byte b
)
411 if (i
== 255 || i
> ID3V1_GENRES
.Length
- 1)
414 return ID3V1_GENRES
[i
];
418 public class Id3v2TagReader
{
420 private Mp3
.Id3v2TagSynchronizer synchronizer
= new Mp3
.Id3v2TagSynchronizer ();
421 private Mp3
.Id3v23TagReader v23
= new Mp3
.Id3v23TagReader ();
422 private ASCIIEncoding ascii
= new ASCIIEncoding ();
424 public Tag
Read (Stream fs
)
428 fs
.Seek (0, SeekOrigin
.Begin
);
429 byte[] buf
= new byte[3];
432 string ID3
= ascii
.GetString (buf
);
434 throw new Exception ("There is no Id3v2 Tag in this file");
436 //Begins tag parsing ---------------------------------------------
437 fs
.Seek (3, SeekOrigin
.Begin
);
438 //----------------------------------------------------------------------------
439 string versionHigh
= fs
.ReadByte () + "";
441 // Since we never use versionID3, we just read the byte off of
442 // the stream instead of constructing the version string.
443 //string versionID3 = versionHigh + "." + fs.ReadByte ();
446 //------------------------------------------------------------------------- ---
447 byte flags
= (byte) fs
.ReadByte (); //ID3 Flags, skipping
448 //----------------------------------------------------------------------------
449 int tagSize
= ReadSyncsafeInt (fs
);
450 //-----------------------------------------------------------------
451 //Fill a byte buffer, then process according to correct version
452 buf
= new byte[tagSize
+2];
453 fs
.Read (buf
, 0, buf
.Length
);
455 bool unsynch
= ((flags
&128) == 128) ? true : false;
457 //We have unsynchronization, first re-synchronize
458 tagSize
= synchronizer
.synchronize (buf
);
461 if (versionHigh
== "2") {
462 tag
= v23
.Read (buf
, tagSize
, flags
, Mp3
.Id3v23TagReader
.ID3V22
);
464 else if (versionHigh
== "3") {
465 tag
= v23
.Read (buf
, tagSize
, flags
, Mp3
.Id3v23TagReader
.ID3V23
);
467 else if (versionHigh
== "4") {
468 //tag = v24.Read (raf, ID3Flags);
469 throw new Exception ("Cannot read ID3v2.4 tags right now !");
472 throw new Exception ("Cannot read unknown version: ID3v2."+versionHigh
);
478 private int ReadSyncsafeInt (Stream fs
)
482 val
+= (fs
.ReadByte ()& 0xFF) << 21;
483 val
+= (fs
.ReadByte ()& 0xFF) << 14;
484 val
+= (fs
.ReadByte ()& 0xFF) << 7;
485 val
+= fs
.ReadByte ()& 0xFF;