注册 登陆

Delphi解析MP3 tag

最近一直在用我的6300听评书-童林传,感觉还可以。

感觉还是有一些遗憾:
MP3的文件名大体上是这样的:单田芳_童林传300回版_001等等。
在手机上的曲目显示就是前面的几个字,到底是在播放第几回看不到,而且如果从电脑拷贝到手机上以后,并不是按文件名顺序来排列的。

我的看法,Nokia的MP3应该是读取MP3的Tag信息来排列MP3的顺序(后来证明是不对的)。当时,恶从心头起,就打算自己改变一下Tag信息,而且是要批量修改。

用Google查了一下MP3的Tag信息,发现MP3的tag格式,大体上有三种:id3v1、id3v2、apetag。

用UltraEdit打开一个MP3文件:

从上面的图形可以看出,这个MP3的tag里面包含id3v1和apetag2两种,如果想修改文件的tag信息,必须了解这两种tag的存储格式。

第一种,id3v1

id3v1标签,一般存在MP3文件的最后128个字节里面,内容大体如下:
tmp3tag = packed record // 128 字节
tagid: array[0..2] of char; // 3 字节: 必须是tag
title: array[0..29] of char; // 30 字节: 歌曲标题
artist: array[0..29] of char; // 30 字节: 歌曲的艺术家
album: array[0..29] of char; // 30 字节: 歌曲专辑
year: array[0..3] of char; // 4 字节: 出版年
comment: array[0..29] of char; // 30 字节: 评论
genre: byte; // 1 字节: 种类标识
end;

了解了以后就非常容易的读写了。

第二中:apetag2格式
在我的MP3文件里面,存放在id3v1标签的前面。
大体格式是这样(从MP3尾部倒数):
mp3 id2v1
apetag_footer //记录格式的大小、版本等
apetag_title    //具体的内容
apetag_header //一个头部

这个比较复杂,但是也能分析出来.

操作起的老狗-Delphi,开始编写。

id3v1单元:

Delphi代码
  1. unit mp3_id3v1;   
  2.   
  3. interface  
  4. uses  
  5. Windows,Classes,SysUtils;   
  6.   
  7. type  
  8. TID3v1Rec = packed record  
  9.     Tag     : array[0..2of Char;   
  10.     Title   : array[0..29of Char;   
  11.     Artist : array[0..29of Char;   
  12.     Album   : array[0..29of Char;   
  13.     Year    : array[0..3of Char;   
  14.     Comment : array[0..29of Char;   
  15.     Genre   : Byte;   
  16. end;   
  17.   
  18. TMP3Info=class  
  19. private  
  20.     FhasTag: Boolean;   
  21.     FGenre: string;   
  22.     Ftitle: string;   
  23.     FArtist: string;   
  24.     FComment: string;   
  25.     FAlbum: string;   
  26.     FYear: string;   
  27.   
  28.     fFileName:string;   
  29.     procedure SetAlbum(const Value: string);   
  30.     procedure SetArtist(const Value: string);   
  31.     procedure SetComment(const Value: string);   
  32.     procedure SetGenre(const Value: string);   
  33.     procedure SethasTag(const Value: Boolean);   
  34.     procedure Settitle(const Value: string);   
  35.     procedure SetYear(const Value: string);   
  36. public  
  37.     constructor Create(fileName:string);   
  38.     procedure GetMp3Info;   
  39.     procedure WriteMp3Info;   
  40.   
  41. published  
  42.     property hasTag:Boolean read FhasTag write SethasTag;   
  43.     property title:string read Ftitle write Settitle;   
  44.     property Artist:string read FArtist write SetArtist;   
  45.     property Comment:string read FComment write SetComment;   
  46.     property Album:string read FAlbum write SetAlbum;   
  47.     property Year:string read FYear write SetYear;   
  48.     property Genre:string read FGenre write SetGenre;   
  49. end;   
  50.   
  51. const  
  52. MaxID3Genre=147;   
  53. ID3Genre: array[0..MaxID3Genre] of string = (   
  54.     'Blues''Classic Rock''Country''Dance''Disco''Funk''Grunge',   
  55.     'Hip-Hop''Jazz''Metal''New Age''Oldies''Other''Pop''R&B',   
  56.     'Rap''Reggae''Rock''Techno''Industrial''Alternative''Ska',   
  57.     'Death Metal''Pranks''Soundtrack''Euro-Techno''Ambient',   
  58.     'Trip-Hop''Vocal''Jazz+Funk''Fusion''Trance''Classical',   
  59.     'Instrumental''Acid''House''Game''Sound Clip''Gospel',   
  60.     'Noise''AlternRock''Bass''Soul''Punk''Space''Meditative',   
  61.     'Instrumental Pop''Instrumental Rock''Ethnic''Gothic',   
  62.     'Darkwave''Techno-Industrial''Electronic''Pop-Folk',   
  63.     'Eurodance''Dream''Southern Rock''Comedy''Cult''Gangsta',   
  64.     'Top 40''Christian Rap''Pop/Funk''Jungle''Native American',   
  65.     'Cabaret''New Wave''Psychadelic''Rave''Showtunes''Trailer',   
  66.     'Lo-Fi''Tribal''Acid Punk''Acid Jazz''Polka''Retro',   
  67.     'Musical''Rock & Roll''Hard Rock''Folk''Folk-Rock',   
  68.     'National Folk''Swing''Fast Fusion''Bebob''Latin''Revival',   
  69.     'Celtic''Bluegrass''Avantgarde''Gothic Rock''Progressive Rock',   
  70.     'Psychedelic Rock''Symphonic Rock''Slow Rock''Big Band',   
  71.     'Chorus''Easy Listening''Acoustic''Humour''Speech''Chanson',   
  72.     'Opera''Chamber Music''Sonata''Symphony''Booty Bass''Primus',   
  73.     'Porn Groove''Satire''Slow Jam''Club''Tango''Samba',   
  74.     'Folklore''Ballad''Power Ballad''Rhythmic Soul''Freestyle',   
  75.     'Duet''Punk Rock''Drum Solo''Acapella''Euro-House''Dance Hall',   
  76.     'Goa''Drum & Bass''Club-House''Hardcore''Terror''Indie',   
  77.     'BritPop''Negerpunk''Polsk Punk''Beat''Christian Gangsta Rap',   
  78.     'Heavy Metal''Black Metal''Crossover''Contemporary Christian',   
  79.     'Christian Rock''Merengue''Salsa''Trash Metal''Anime''Jpop',   
  80.     'Synthpop' {and probably more to come}  
  81. );   
  82.   
  83. implementation  
  84.   
  85.   
  86. { TMP3Info }  
  87.   
  88. constructor TMP3Info.Create(fileName: string);   
  89. begin  
  90. fFileName := fileName;   
  91. end;   
  92.   
  93. procedure TMP3Info.GetMp3Info;   
  94. var  
  95. id3rec:TID3v1Rec;   
  96. fsMp3:TFileStream;   
  97. begin  
  98. fsMp3:= TFileStream.Create(fFileName,fmOpenRead);   
  99. fsMp3.Seek(-128,soFromEnd);   
  100. fsMp3.Read(id3rec,SizeOf(id3rec));   
  101.   
  102. fsMp3.Free;   
  103.   
  104. if id3rec.Tag='TAG' then  
  105. begin  
  106.     Ftitle := id3rec.Title;   
  107.     FArtist := id3rec.Artist;   
  108.     FComment := id3rec.Comment;   
  109.     FAlbum := id3rec.Album;   
  110.     FYear := id3rec.Year;   
  111.     FGenre := ID3Genre[id3rec.Genre];   
  112.     FhasTag := True;   
  113. end  
  114. else  
  115. begin  
  116.     FhasTag := False;   
  117. end;   
  118. end;   
  119.   
  120. procedure TMP3Info.SetAlbum(const Value: string);   
  121. begin  
  122. FAlbum := Value;   
  123. end;   
  124.   
  125. procedure TMP3Info.SetArtist(const Value: string);   
  126. begin  
  127. FArtist := Value;   
  128. end;   
  129.   
  130. procedure TMP3Info.SetComment(const Value: string);   
  131. begin  
  132. FComment := Value;   
  133. end;   
  134.   
  135. procedure TMP3Info.SetGenre(const Value: string);   
  136. begin  
  137. FGenre := Value;   
  138. end;   
  139.   
  140. procedure TMP3Info.SethasTag(const Value: Boolean);   
  141. begin  
  142. FhasTag := Value;   
  143. end;   
  144.   
  145. procedure TMP3Info.Settitle(const Value: string);   
  146. begin  
  147. Ftitle := Value;   
  148. end;   
  149.   
  150. procedure TMP3Info.SetYear(const Value: string);   
  151. begin  
  152. FYear := Value;   
  153. end;   
  154.   
  155. procedure TMP3Info.WriteMp3Info;   
  156. var  
  157. id3Tag : TID3v1Rec;   
  158. fMp3   : TFileStream;   
  159.   
  160. function SearchGenre(sGenre:string):Byte;   
  161. var  
  162.     i:Byte;   
  163. begin  
  164.     result:=0;   
  165.     for i := 0 to MaxID3Genre do  
  166.     begin  
  167.        if ID3Genre[i] = sGenre then  
  168.           result:=i;   
  169.     end;   
  170. end;   
  171. begin  
  172.   
  173. if not hasTag then  
  174.     exit;   
  175.   
  176. StrPCopy(id3Tag.Tag,'TAG');   
  177.   
  178. if Length(Ftitle) > 30 then  
  179.     SetLength(Ftitle,30);   
  180. StrPCopy(id3Tag.Title,Ftitle);   
  181.   
  182. if Length(FArtist) > 30 then  
  183.     SetLength(FArtist,30);   
  184. StrPCopy(id3Tag.Artist,FArtist);   
  185.   
  186. if Length(FAlbum) > 30 then  
  187.     SetLength(FAlbum,30);   
  188. StrPCopy(id3Tag.Album,FAlbum);   
  189.   
  190. if Length(FYear)>4 then  
  191.     SetLength(FYear,4);   
  192. StrPCopy(id3Tag.Year,FYear);   
  193.   
  194. if Length(FComment) > 30 then  
  195.     SetLength(FComment,30);   
  196. StrPCopy(id3Tag.Comment,FComment);   
  197.   
  198. id3Tag.Genre := SearchGenre(FGenre);   
  199.   
  200. fMp3 := TFileStream.Create(fFileName,fmOpenWrite);   
  201. fMp3.Seek(-128,soFromEnd);   
  202. fMp3.Write(id3Tag,SizeOf(id3Tag));   
  203.   
  204. fMp3.Free;   
  205. end;   
  206.   
  207. end.   
  208.   
  209. apetag读写单元:   
  210. unit apetag;   
  211.   
  212. interface  
  213.   
  214. uses  
  215. Classes,SysUtils;   
  216.   
  217. const  
  218. { Tag ID }  
  219. ID3V1_ID = 'TAG';                                                   { ID3v1 }  
  220. APE_ID = 'APETAGEX';                                                  { APE }  
  221.   
  222. { Size constants }  
  223. ID3V1_TAG_SIZE = 128;                                           { ID3v1 tag }  
  224. APE_TAG_FOOTER_SIZE = 32;                                  { APE tag footer }  
  225. APE_TAG_HEADER_SIZE = 32;                                  { APE tag header }  
  226.   
  227. { Version of APE tag }  
  228. APE_VERSION_1_0 = 1000;   
  229. APE_VERSION_2_0 = 1000;   
  230.   
  231. type  
  232.    RTagHeader = record  
  233.     { Real structure of APE footer }  
  234.     ID: array [0..7of Char;                             { Always "APETAGEX" }  
  235.     Version: Integer;                                           { Tag version }  
  236.     Size: Integer;                                { Tag size including footer }  
  237.     Fields: Integer;                                       { Number of fields }  
  238.     Flags: Integer;                                               { Tag flags }  
  239.     Reserved: array [0..7of Char;                  { Reserved for later use }  
  240.     { Extended data }  
  241.     DataShift: Byte;                                { Used if ID3v1 tag found }  
  242.     FileSize: Integer;                                    { File size (bytes) }  
  243. end;   
  244.   
  245. RField = record  
  246.     Name: string;   
  247.     Value: UTF8string;   
  248. end;   
  249. AField = array of RField;   
  250.   
  251. TAPETag = class  
  252.     private  
  253.       pField: Afield;   
  254.       pExists: Boolean;   
  255.       pVersion: Integer;   
  256.       pSize: Integer;   
  257.       function ReadFooter(sFile: stringvar footer: RTagHeader): boolean;   
  258.       procedure ReadFields(sFile: string; footer: RTagHeader);   
  259.     public  
  260.       property Exists: Boolean read pExists;              { True if tag found }  
  261.       property Version: Integer read pVersion;                  { Tag version }  
  262.       property Fields: AField read pField;   
  263.       property Size: Integer read pSize;   
  264.       constructor Create();   
  265.   
  266.       function ReadFromFile(sFile:string):Boolean;   
  267.       function WriteToFile(sFile:string):Boolean;   
  268.       procedure ResetData;   
  269.     end;   
  270.   
  271. implementation  
  272.   
  273. { TAPETag }  
  274.   
  275. constructor TAPETag.Create;   
  276. begin  
  277. inherited;   
  278.   
  279. ResetData;   
  280. end;   
  281.   
  282. procedure TAPETag.ReadFields(sFile: string; footer: RTagHeader);   
  283. var  
  284. fileMP3:TFileStream;   
  285. FieldName: String;   
  286. FieldValue: array [1..250of Char;   
  287. NextChar: Char;   
  288. Iterator, ValueSize, ValuePosition, FieldFlags: Integer;   
  289. begin  
  290. fileMP3 := TFileStream.Create(sFile,fmOpenRead);   
  291. try  
  292.     fileMP3.Seek(footer.FileSize - footer.DataShift - footer.Size, soFromBeginning);   
  293.     SetLength(pField,footer.Fields);   
  294.   
  295.     for Iterator := 0 to footer.Fields-1 do  
  296.     begin  
  297.         FillChar(FieldValue,SizeOf(FieldValue),0);   
  298.         fileMP3.Read(ValueSize, SizeOf(ValueSize));   
  299.         fileMP3.Read(FieldFlags, SizeOf(FieldFlags));   
  300.         FieldName := '';   
  301.         repeat  
  302.           fileMP3.Read(NextChar, SizeOf(NextChar));   
  303.           FieldName := FieldName + NextChar;   
  304.         until Ord(NextChar) = 0;   
  305.         ValuePosition := fileMP3.Position;   
  306.         fileMP3.Read(FieldValue, ValueSize mod SizeOf(FieldValue));   
  307.         pField[Iterator].Name := Trim(FieldName);   
  308.         pField[Iterator].Value := Trim(FieldValue);   
  309.         fileMP3.Seek(ValuePosition + ValueSize, soFromBeginning);   
  310.     end;   
  311. finally  
  312.     fileMP3.Free;   
  313. end;   
  314. end;   
  315.   
  316. function TAPETag.ReadFooter(sFile: string;   
  317. var footer: RTagHeader): boolean;   
  318. var  
  319. fileMP3:TFileStream;   
  320. TagID:array[0..2of char;   
  321. transffered:Integer;   
  322. begin  
  323. Result := True;   
  324. fileMP3 := TFileStream.Create(sFile,fmOpenRead);   
  325. try  
  326.     footer.FileSize := fileMP3.Size;   
  327.     fileMP3.Seek(0-ID3V1_TAG_SIZE,soFromEnd);   
  328.     fileMP3.Read(TagID,3);   
  329.   
  330.     if TagID=ID3V1_ID then  
  331.       footer.DataShift := ID3V1_TAG_SIZE   
  332.     else  
  333.       footer.DataShift := 0;   
  334.   
  335.     transffered := 0;   
  336.     fileMP3.Seek(0-ID3V1_TAG_SIZE-APE_TAG_FOOTER_SIZE,soFromEnd);   
  337.     transffered := fileMP3.Read(footer,APE_TAG_FOOTER_SIZE);   
  338.   
  339.     if transffered < APE_TAG_FOOTER_SIZE then  
  340.       Result := false;   
  341. finally  
  342.     fileMP3.Free;   
  343. end// try       
  344. end;   
  345.   
  346.   
  347.   
  348. function TAPETag.ReadFromFile(sFile: string): Boolean;   
  349. var  
  350. footer:RTagHeader;   
  351. begin  
  352. ResetData;   
  353. Result := ReadFooter(sFile, Footer);   
  354. { Process data if loaded and footer valid }  
  355. if (Result) and (Footer.ID = APE_ID) then  
  356. begin  
  357.     pExists := True;   
  358.     pVersion := Footer.Version;   
  359.     pSize := Footer.Size;   
  360.     ReadFields(sFile, Footer);   
  361. end;   
  362. end;   
  363.   
  364. procedure TAPETag.ResetData;   
  365. begin  
  366. SetLength(pField,0);   
  367. pExists := False;   
  368. pVersion := 0;   
  369. pSize := 0;   
  370. end;   
  371.   
  372. function TAPETag.WriteToFile(sFile: string): Boolean;   
  373. const  
  374. APEPreample: array [0..7of char = ('A','P','E','T','A','G','E','X');   
  375. var  
  376. SourceFile: TFileStream;   
  377. Header, Footer, RefFooter: RTagHeader;   
  378. ID3: PChar;   
  379. i, len, TagSize, Flags: integer;   
  380. TagData: TStringStream;   
  381. begin  
  382.     ID3 := nil;   
  383.     // method : first, save any eventual ID3v1 tag lying around   
  384.     //          then we truncate the file after the audio data   
  385.     //          then write the APE tag (and possibly the ID3)   
  386.     Result := ReadFooter(sFile, RefFooter);   
  387.     { Process data if loaded and footer valid }  
  388.     if (Result) and (RefFooter.ID = APE_ID) then  
  389.     begin  
  390.       SourceFile := TFileStream.Create(sFile, fmOpenReadWrite or fmShareDenyWrite);   
  391.       { If there is an ID3v1 tag roaming around behind the APE tag, we have to buffer it }  
  392.       if RefFooter.DataShift = ID3V1_TAG_SIZE then  
  393.       begin  
  394.         GetMem(ID3,ID3V1_TAG_SIZE);   
  395.         SourceFile.Seek(Reffooter.FileSize - Reffooter.DataShift, soFromBeginning);   
  396.         SourceFile.Read(ID3^, ID3V1_TAG_SIZE);   
  397.       end;   
  398.       { If this is an APEv2, header size must be added }  
  399.       //if (RefFooter.Flags shr 31) > 0 then   
  400.         Inc(RefFooter.Size, APE_TAG_HEADER_SIZE);   
  401.       SourceFile.Seek(RefFooter.FileSize - RefFooter.Size-RefFooter.DataShift, soFromBeginning);   
  402.       //truncate   
  403.       SourceFile.Size := SourceFile.Position;   
  404.       SourceFile.Free;   
  405.     end;   
  406.     TagData := TStringStream.Create('');   
  407.     TagSize := APE_TAG_FOOTER_SIZE;   
  408.     for i:=0 to high(pField) do  
  409.     begin  
  410.       TagSize := TagSize + 9 + Length(pField[i].Name) + Length(pField[i].Value);   
  411.     end;   
  412.     Header.ID[0] := 'A';   
  413.     Header.ID[1] := 'P';   
  414.     Header.ID[2] := 'E';   
  415.     Header.ID[3] := 'T';   
  416.     Header.ID[4] := 'A';   
  417.     Header.ID[5] := 'G';   
  418.     Header.ID[6] := 'E';   
  419.     Header.ID[7] := 'X';   
  420.     Header.Version := 2000;   
  421.     Header.Size := TagSize;   
  422.     Header.Fields := Length(pField);   
  423.     Header.Flags := 0 or (1 shl 29or (1 shl 31);       // tag contains a header and this is the header   
  424.     //ShowMessage(IntToSTr(Header.Flags));   
  425.     TagData.Write(Header,APE_TAG_HEADER_SIZE);   
  426.     for i:=0 to high(pField) do  
  427.     begin  
  428.       len := Length(pField[i].Value);   
  429.       Flags := 0;   
  430.       TagData.Write(len, SizeOf(len));   
  431.       TagData.Write(Flags, SizeOf(Flags));   
  432.       TagData.WriteString(pField[i].Name + #0);   
  433.       TagData.WriteString(pField[i].Value);   
  434.     end;   
  435.     Footer.ID[0] := 'A';   
  436.     Footer.ID[1] := 'P';   
  437.     Footer.ID[2] := 'E';   
  438.     Footer.ID[3] := 'T';   
  439.     Footer.ID[4] := 'A';   
  440.     Footer.ID[5] := 'G';   
  441.     Footer.ID[6] := 'E';   
  442.     Footer.ID[7] := 'X';   
  443.     Footer.Version := 2000;   
  444.     Footer.Size := TagSize;   
  445.     Footer.Fields := Length(pField);   
  446.     Footer.Flags := 0 or (1 shl 31);                     // tag contains a header and this is the footer   
  447.     TagData.Write(Footer,APE_TAG_FOOTER_SIZE) ;   
  448.     if (RefFooter.DataShift = ID3V1_TAG_SIZE) and Assigned(ID3)then  
  449.     begin  
  450.       TagData.Write(ID3^,ID3V1_TAG_SIZE);   
  451.       FreeMem(ID3);   
  452.     end;   
  453.     SourceFile := TFileStream.Create(sFile, fmOpenReadWrite or fmShareDenyWrite);   
  454.     SourceFile.Seek(0, soFromEnd);   
  455.     TagData.Seek(0, soFromBeginning);   
  456.     SourceFile.CopyFrom(TagData, TagData.Size);   
  457.     SourceFile.Free;   
  458.     TagData.Free;   
  459. end;   
  460.   
  461. end.   

« 上一篇 | 下一篇 »