Delphi解析MP3 tag

2018-10-30

unit mp3_id3v1;

interface
uses
Windows,Classes,SysUtils;

type
TID3v1Rec = packed record
Tag : array[0..2] of Char;
Title : array[0..29] of Char;
Artist : array[0..29] of Char;
Album : array[0..29] of Char;
Year : array[0..3] of Char;
Comment : array[0..29] of Char;
Genre : Byte;
end;

TMP3Info=class
private
FhasTag: Boolean;
FGenre: string;
Ftitle: string;
FArtist: string;
FComment: string;
FAlbum: string;
FYear: string;

fFileName:string;
procedure SetAlbum(const Value: string);
procedure SetArtist(const Value: string);
procedure SetComment(const Value: string);
procedure SetGenre(const Value: string);
procedure SethasTag(const Value: Boolean);
procedure Settitle(const Value: string);
procedure SetYear(const Value: string);
public
constructor Create(fileName:string);
procedure GetMp3Info;
procedure WriteMp3Info;

published
property hasTag:Boolean read FhasTag write SethasTag;
property title:string read Ftitle write Settitle;
property Artist:string read FArtist write SetArtist;
property Comment:string read FComment write SetComment;
property Album:string read FAlbum write SetAlbum;
property Year:string read FYear write SetYear;
property Genre:string read FGenre write SetGenre;
end;

const
MaxID3Genre=147;
ID3Genre: array[0..MaxID3Genre] of string = (
'Blues', 'Classic Rock', 'Country', 'Dance', 'Disco', 'Funk', 'Grunge',
'Hip-Hop', 'Jazz', 'Metal', 'New Age', 'Oldies', 'Other', 'Pop', 'R&B',
'Rap', 'Reggae', 'Rock', 'Techno', 'Industrial', 'Alternative', 'Ska',
'Death Metal', 'Pranks', 'Soundtrack', 'Euro-Techno', 'Ambient',
'Trip-Hop', 'Vocal', 'Jazz+Funk', 'Fusion', 'Trance', 'Classical',
'Instrumental', 'Acid', 'House', 'Game', 'Sound Clip', 'Gospel',
'Noise', 'AlternRock', 'Bass', 'Soul', 'Punk', 'Space', 'Meditative',
'Instrumental Pop', 'Instrumental Rock', 'Ethnic', 'Gothic',
'Darkwave', 'Techno-Industrial', 'Electronic', 'Pop-Folk',
'Eurodance', 'Dream', 'Southern Rock', 'Comedy', 'Cult', 'Gangsta',
'Top 40', 'Christian Rap', 'Pop/Funk', 'Jungle', 'Native American',
'Cabaret', 'New Wave', 'Psychadelic', 'Rave', 'Showtunes', 'Trailer',
'Lo-Fi', 'Tribal', 'Acid Punk', 'Acid Jazz', 'Polka', 'Retro',
'Musical', 'Rock & Roll', 'Hard Rock', 'Folk', 'Folk-Rock',
'National Folk', 'Swing', 'Fast Fusion', 'Bebob', 'Latin', 'Revival',
'Celtic', 'Bluegrass', 'Avantgarde', 'Gothic Rock', 'Progressive Rock',
'Psychedelic Rock', 'Symphonic Rock', 'Slow Rock', 'Big Band',
'Chorus', 'Easy Listening', 'Acoustic', 'Humour', 'Speech', 'Chanson',
'Opera', 'Chamber Music', 'Sonata', 'Symphony', 'Booty Bass', 'Primus',
'Porn Groove', 'Satire', 'Slow Jam', 'Club', 'Tango', 'Samba',
'Folklore', 'Ballad', 'Power Ballad', 'Rhythmic Soul', 'Freestyle',
'Duet', 'Punk Rock', 'Drum Solo', 'Acapella', 'Euro-House', 'Dance Hall',
'Goa', 'Drum & Bass', 'Club-House', 'Hardcore', 'Terror', 'Indie',
'BritPop', 'Negerpunk', 'Polsk Punk', 'Beat', 'Christian Gangsta Rap',
'Heavy Metal', 'Black Metal', 'Crossover', 'Contemporary Christian',
'Christian Rock', 'Merengue', 'Salsa', 'Trash Metal', 'Anime', 'Jpop',
'Synthpop' {and probably more to come}
);

implementation


{ TMP3Info }

constructor TMP3Info.Create(fileName: string);
begin
fFileName := fileName;
end;

procedure TMP3Info.GetMp3Info;
var
id3rec:TID3v1Rec;
fsMp3:TFileStream;
begin
fsMp3:= TFileStream.Create(fFileName,fmOpenRead);
fsMp3.Seek(-128,soFromEnd);
fsMp3.Read(id3rec,SizeOf(id3rec));

fsMp3.Free;

if id3rec.Tag='TAG' then
begin
Ftitle := id3rec.Title;
FArtist := id3rec.Artist;
FComment := id3rec.Comment;
FAlbum := id3rec.Album;
FYear := id3rec.Year;
FGenre := ID3Genre[id3rec.Genre];
FhasTag := True;
end
else
begin
FhasTag := False;
end;
end;

procedure TMP3Info.SetAlbum(const Value: string);
begin
FAlbum := Value;
end;

procedure TMP3Info.SetArtist(const Value: string);
begin
FArtist := Value;
end;

procedure TMP3Info.SetComment(const Value: string);
begin
FComment := Value;
end;

procedure TMP3Info.SetGenre(const Value: string);
begin
FGenre := Value;
end;

procedure TMP3Info.SethasTag(const Value: Boolean);
begin
FhasTag := Value;
end;

procedure TMP3Info.Settitle(const Value: string);
begin
Ftitle := Value;
end;

procedure TMP3Info.SetYear(const Value: string);
begin
FYear := Value;
end;

procedure TMP3Info.WriteMp3Info;
var
id3Tag : TID3v1Rec;
fMp3 : TFileStream;

function SearchGenre(sGenre:string):Byte;
var
i:Byte;
begin
result:=0;
for i := 0 to MaxID3Genre do
begin
if ID3Genre[i] = sGenre then
result:=i;
end;
end;
begin

if not hasTag then
exit;

StrPCopy(id3Tag.Tag,'TAG');

if Length(Ftitle) > 30 then
SetLength(Ftitle,30);
StrPCopy(id3Tag.Title,Ftitle);

if Length(FArtist) > 30 then
SetLength(FArtist,30);
StrPCopy(id3Tag.Artist,FArtist);

if Length(FAlbum) > 30 then
SetLength(FAlbum,30);
StrPCopy(id3Tag.Album,FAlbum);

if Length(FYear)>4 then
SetLength(FYear,4);
StrPCopy(id3Tag.Year,FYear);

if Length(FComment) > 30 then
SetLength(FComment,30);
StrPCopy(id3Tag.Comment,FComment);

id3Tag.Genre := SearchGenre(FGenre);

fMp3 := TFileStream.Create(fFileName,fmOpenWrite);
fMp3.Seek(-128,soFromEnd);
fMp3.Write(id3Tag,SizeOf(id3Tag));

fMp3.Free;
end;

end.

apetag读写单元:
unit apetag;

interface

uses
Classes,SysUtils;

const
{ Tag ID }
ID3V1_ID = 'TAG'; { ID3v1 }
APE_ID = 'APETAGEX'; { APE }

{ Size constants }
ID3V1_TAG_SIZE = 128; { ID3v1 tag }
APE_TAG_FOOTER_SIZE = 32; { APE tag footer }
APE_TAG_HEADER_SIZE = 32; { APE tag header }

{ Version of APE tag }
APE_VERSION_1_0 = 1000;
APE_VERSION_2_0 = 1000;

type
RTagHeader = record
{ Real structure of APE footer }
ID: array [0..7] of Char; { Always "APETAGEX" }
Version: Integer; { Tag version }
Size: Integer; { Tag size including footer }
Fields: Integer; { Number of fields }
Flags: Integer; { Tag flags }
Reserved: array [0..7] of Char; { Reserved for later use }
{ Extended data }
DataShift: Byte; { Used if ID3v1 tag found }
FileSize: Integer; { File size (bytes) }
end;

RField = record
Name: string;
Value: UTF8string;
end;
AField = array of RField;

TAPETag = class
private
pField: Afield;
pExists: Boolean;
pVersion: Integer;
pSize: Integer;
function ReadFooter(sFile: string; var footer: RTagHeader): boolean;
procedure ReadFields(sFile: string; footer: RTagHeader);
public
property Exists: Boolean read pExists; { True if tag found }
property Version: Integer read pVersion; { Tag version }
property Fields: AField read pField;
property Size: Integer read pSize;
constructor Create();

function ReadFromFile(sFile:string):Boolean;
function WriteToFile(sFile:string):Boolean;
procedure ResetData;
end;

implementation

{ TAPETag }

constructor TAPETag.Create;
begin
inherited;

ResetData;
end;

procedure TAPETag.ReadFields(sFile: string; footer: RTagHeader);
var
fileMP3:TFileStream;
FieldName: String;
FieldValue: array [1..250] of Char;
NextChar: Char;
Iterator, ValueSize, ValuePosition, FieldFlags: Integer;
begin
fileMP3 := TFileStream.Create(sFile,fmOpenRead);
try
fileMP3.Seek(footer.FileSize - footer.DataShift - footer.Size, soFromBeginning);
SetLength(pField,footer.Fields);

for Iterator := 0 to footer.Fields-1 do
begin
FillChar(FieldValue,SizeOf(FieldValue),0);
fileMP3.Read(ValueSize, SizeOf(ValueSize));
fileMP3.Read(FieldFlags, SizeOf(FieldFlags));
FieldName := '';
repeat
fileMP3.Read(NextChar, SizeOf(NextChar));
FieldName := FieldName + NextChar;
until Ord(NextChar) = 0;
ValuePosition := fileMP3.Position;
fileMP3.Read(FieldValue, ValueSize mod SizeOf(FieldValue));
pField[Iterator].Name := Trim(FieldName);
pField[Iterator].Value := Trim(FieldValue);
fileMP3.Seek(ValuePosition + ValueSize, soFromBeginning);
end;
finally
fileMP3.Free;
end;
end;

function TAPETag.ReadFooter(sFile: string;
var footer: RTagHeader): boolean;
var
fileMP3:TFileStream;
TagID:array[0..2] of char;
transffered:Integer;
begin
Result := True;
fileMP3 := TFileStream.Create(sFile,fmOpenRead);
try
footer.FileSize := fileMP3.Size;
fileMP3.Seek(0-ID3V1_TAG_SIZE,soFromEnd);
fileMP3.Read(TagID,3);

if TagID=ID3V1_ID then
footer.DataShift := ID3V1_TAG_SIZE
else
footer.DataShift := 0;

transffered := 0;
fileMP3.Seek(0-ID3V1_TAG_SIZE-APE_TAG_FOOTER_SIZE,soFromEnd);
transffered := fileMP3.Read(footer,APE_TAG_FOOTER_SIZE);

if transffered < APE_TAG_FOOTER_SIZE then
Result := false;
finally
fileMP3.Free;
end; // try
end;



function TAPETag.ReadFromFile(sFile: string): Boolean;
var
footer:RTagHeader;
begin
ResetData;
Result := ReadFooter(sFile, Footer);
{ Process data if loaded and footer valid }
if (Result) and (Footer.ID = APE_ID) then
begin
pExists := True;
pVersion := Footer.Version;
pSize := Footer.Size;
ReadFields(sFile, Footer);
end;
end;

procedure TAPETag.ResetData;
begin
SetLength(pField,0);
pExists := False;
pVersion := 0;
pSize := 0;
end;

function TAPETag.WriteToFile(sFile: string): Boolean;
const
APEPreample: array [0..7] of char = ('A','P','E','T','A','G','E','X');
var
SourceFile: TFileStream;
Header, Footer, RefFooter: RTagHeader;
ID3: PChar;
i, len, TagSize, Flags: integer;
TagData: TStringStream;
begin
ID3 := nil;
// method : first, save any eventual ID3v1 tag lying around
// then we truncate the file after the audio data
// then write the APE tag (and possibly the ID3)
Result := ReadFooter(sFile, RefFooter);
{ Process data if loaded and footer valid }
if (Result) and (RefFooter.ID = APE_ID) then
begin
SourceFile := TFileStream.Create(sFile, fmOpenReadWrite or fmShareDenyWrite);
{ If there is an ID3v1 tag roaming around behind the APE tag, we have to buffer it }
if RefFooter.DataShift = ID3V1_TAG_SIZE then
begin
GetMem(ID3,ID3V1_TAG_SIZE);
SourceFile.Seek(Reffooter.FileSize - Reffooter.DataShift, soFromBeginning);
SourceFile.Read(ID3^, ID3V1_TAG_SIZE);
end;
{ If this is an APEv2, header size must be added }
//if (RefFooter.Flags shr 31) > 0 then
Inc(RefFooter.Size, APE_TAG_HEADER_SIZE);
SourceFile.Seek(RefFooter.FileSize - RefFooter.Size-RefFooter.DataShift, soFromBeginning);
//truncate
SourceFile.Size := SourceFile.Position;
SourceFile.Free;
end;
TagData := TStringStream.Create('');
TagSize := APE_TAG_FOOTER_SIZE;
for i:=0 to high(pField) do
begin
TagSize := TagSize + 9 + Length(pField[i].Name) + Length(pField[i].Value);
end;
Header.ID[0] := 'A';
Header.ID[1] := 'P';
Header.ID[2] := 'E';
Header.ID[3] := 'T';
Header.ID[4] := 'A';
Header.ID[5] := 'G';
Header.ID[6] := 'E';
Header.ID[7] := 'X';
Header.Version := 2000;
Header.Size := TagSize;
Header.Fields := Length(pField);
Header.Flags := 0 or (1 shl 29) or (1 shl 31); // tag contains a header and this is the header
//ShowMessage(IntToSTr(Header.Flags));
TagData.Write(Header,APE_TAG_HEADER_SIZE);
for i:=0 to high(pField) do
begin
len := Length(pField[i].Value);
Flags := 0;
TagData.Write(len, SizeOf(len));
TagData.Write(Flags, SizeOf(Flags));
TagData.WriteString(pField[i].Name + #0);
TagData.WriteString(pField[i].Value);
end;
Footer.ID[0] := 'A';
Footer.ID[1] := 'P';
Footer.ID[2] := 'E';
Footer.ID[3] := 'T';
Footer.ID[4] := 'A';
Footer.ID[5] := 'G';
Footer.ID[6] := 'E';
Footer.ID[7] := 'X';
Footer.Version := 2000;
Footer.Size := TagSize;
Footer.Fields := Length(pField);
Footer.Flags := 0 or (1 shl 31); // tag contains a header and this is the footer
TagData.Write(Footer,APE_TAG_FOOTER_SIZE) ;
if (RefFooter.DataShift = ID3V1_TAG_SIZE) and Assigned(ID3)then
begin
TagData.Write(ID3^,ID3V1_TAG_SIZE);
FreeMem(ID3);
end;
SourceFile := TFileStream.Create(sFile, fmOpenReadWrite or fmShareDenyWrite);
SourceFile.Seek(0, soFromEnd);
TagData.Seek(0, soFromBeginning);
SourceFile.CopyFrom(TagData, TagData.Size);
SourceFile.Free;
TagData.Free;
end;

end.

阅读40