严黎斌
unit ID3Kernel;
interface
type
Tid3v1= record
Tag: array[0..2] of char; //00..02 , ='TAG'
Title:array[0..$1d] of char; //03..20
Artist:array[0..$1d] of char; //21..3e
Album:array[0..$1d] of char; //3f..5c
Year:array[0..3] of char; //5d..60
Comment:array[0..$1c] of char; //61..7d
Track:byte; //7e
Genre:byte; //7f
end;
function ReadID3v1(strFile:string;var pid3v1:Tid3v1):integer;
function WriteID3v1(strFile:string;var pid3v1:Tid3v1):integer;
function DeleteID3v1(strFile:string):integer;
implementation
function ReadID3v1(strFile:string;var pid3v1:Tid3v1):integer;
var
f1:file of byte;
bytAll: array [0..$7f] of byte;
i: integer;
begin
result:=1;
if strFile='' then exit;
AssignFile(f1,strFile);
FileMode:=0;
Reset(f1);
if FileSize(f1)<=$80 then exit;
Seek(f1, FileSize(f1)-$80);
for i:=0 to $7f do Read(f1,bytAll[i]);
if (bytAll[0]<>ord('T')) and (bytAll[1]<>ord('A'))
and (bytAll[2]<>ord('G')) then exit; // no 'TAG' found
Move(bytAll,pid3v1,$80);
CloseFile(f1);
result:=0;
end;
function WriteID3v1(strFile:string;var pid3v1:Tid3v1):integer;
var
f1:file of byte;
bytAll: array [0..$7f] of byte;
i: integer;
begin
result:=1;
AssignFile(f1,strFile);
FileMode:=2;
Reset(f1);
if FileSize(f1)<=$80 then exit;
Seek(f1, FileSize(f1)-$80);
for i:=0 to $2 do Read(f1,bytAll[i]); // test if 'TAG' exists
if (bytAll[0]=ord('T')) and (bytAll[1]=ord('A'))
and (bytAll[2]=ord('G'))
then Seek(f1,FileSize(f1)-$80)
else Seek(f1,FileSize(f1));
Move(pid3v1,bytAll,$80);
for i:=0 to $7f do Write(f1,bytAll[i]);
CloseFile(f1);
result:=0;
end;
function DeleteID3v1(strFile:string):integer;
var
f1:file of byte;
bytAll: array [0..$7f] of byte;
i: integer;
begin
Result:=1;
AssignFile(f1,strFile);
FileMode:=2;
Reset(f1);
if FileSize(f1)<=$80 then exit;
Seek(f1, FileSize(f1)-$80);
for i:=0 to $2 do Read(f1,bytAll[i]); // test if 'TAG' exists
if (bytAll[0]=ord('T')) and (bytAll[1]=ord('A'))
and (bytAll[2]=ord('G'))
then begin
Seek(f1,FileSize(f1)-$80);
Truncate(f1)
end;
CloseFile(f1);
Result:=0;
end;
end.
***********************************************
{
Byte 1-3 = ID 'TAG'
Byte 4-33 = Titel / Title
Byte 34-63 = Artist
Byte 64-93 = Album
Byte 94-97 = Jahr / Year
Byte 98-127 = Kommentar / Comment
Byte 128 = Genre
}
type
TID3Tag = record
ID: string[3];
Titel: string[30];
Artist: string[30];
Album: string[30];
Year: string[4];
Comment: string[30];
Genre: Byte;
end;
const
Genres : array[0..146] 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',
'Alternative Rock','Bass','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 US','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','Rhytmic 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','Heavy Metal','Black Metal','Crossover','Contemporary C',
'Christian Rock','Merengue','Salsa','Thrash Metal','Anime','JPop','SynthPop');
var
Form1: TForm1;
implementation
{$R *.dfm}
function readID3Tag(FileName: string): TID3Tag;
var
FS: TFileStream;
Buffer: array [1..128] of Char;
begin
FS := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
FS.Seek(-128, soFromEnd);
FS.Read(Buffer, 128);
with Result do
begin
ID := Copy(Buffer, 1, 3);
Titel := Copy(Buffer, 4, 30);
Artist := Copy(Buffer, 34, 30);
Album := Copy(Buffer, 64, 30);
Year := Copy(Buffer, 94, 4);
Comment := Copy(Buffer, 98, 30);
Genre := Ord(Buffer[128]);
end;
finally
FS.Free;
end;
end;
procedure TfrmMain.Button1Click(Sender: TObject);
begin
if OpenDialog1.Execute then
begin
with readID3Tag(OpenDialog1.FileName) do
begin
LlbID.Caption := 'ID: ' + ID;
LlbTitel.Caption := 'Titel: ' + Titel;
LlbArtist.Caption := 'Artist: ' + Artist;
LlbAlbum.Caption := 'Album: ' + Album;
LlbYear.Caption := 'Year: ' + Year;
LlbComment.Caption := 'Comment: ' + Comment;
if (Genre >= 0) and (Genre <=146) then
LlbGenre.Caption := 'Genre: ' + Genres[Genre]
else
LlbGenre.Caption := 'N/A';
end;
end;
end;
---------------------------------------
ID3v2.pas 单元代码:
unit ID3v2;
interface
uses
Classes, SysUtils;
const
TAG_VERSION_2_3 = 3; { Code for ID3v2.3.x
tag }
type
{ Class TID3v2 }
TID3v2 = class(TObject)
private
{ Private declarations }
FExists: Boolean;
FVersionID: Byte;
FSize: Integer;
FTitle: string;
FArtist: string;
FAlbum: string;
FTrack: Byte;
FYear: string;
FGenre: string;
FComment: string;
procedure FSetTitle(const NewTitle: string);
procedure FSetArtist(const NewArtist: string);
procedure FSetAlbum(const NewAlbum: string);
procedure FSetTrack(const NewTrack: Byte);
procedure FSetYear(const NewYear: string);
procedure FSetGenre(const NewGenre: string);
procedure FSetComment(const NewComment: string);
public
{ Public declarations }
constructor Create; { Create
object }
procedure ResetData; { Reset all
data }
function ReadFromFile(const FileName: string): Boolean; { Load
tag }
function RemoveFromFile(const FileName: string): Boolean; { Delete
tag }
function SaveToFile(const FileName: string): Boolean; { Save
tag }
property Exists: Boolean read FExists; { True if tag
found }
property VersionID: Byte read FVersionID; { Version
code }
property Size: Integer read FSize; { Total tag
size }
property Title: string read FTitle write FSetTitle; { Song
title }
property Artist: string read FArtist write FSetArtist; { Artist
name }
property Album: string read FAlbum write FSetAlbum; { Album
title }
property Track: Byte read FTrack write FSetTrack; { Track
number }
property Year: string read FYear write FSetYear; { Release
year }
property Genre: string read FGenre write FSetGenre; { Genre
name }
property Comment: string read FComment write FSetComment; {
Comment }
end;
implementation
const
{ ID3v2 tag ID }
ID3V2_ID = 'ID3';
{ Max. number of supported tag frames }
ID3V2_FRAME_COUNT = 7;
{ Names of supported tag frames }
ID3V2_FRAME: array [1..ID3V2_FRAME_COUNT] of string =
('TIT2', 'TPE1', 'TALB', 'TRCK', 'TYER', 'TCON', 'COMM');
type
{ ID3v2 frame header }
FrameHeader = record
ID: array [1..4] of Char; { Frame
ID }
Size: Integer; { Size excluding
header }
Flags: Word; {
Flags }
end;
{ ID3v2 header data - for internal use }
TagInfo = record
{ Real structure of ID3v2 header }
ID: array [1..3] of Char; { Always
"ID3" }
Version: Byte; { Version
number }
Revision: Byte; { Revision
number }
Flags: Byte; { Flags of
tag }
Size: array [1..4] of Byte; { Tag size excluding
header }
{ Extended data }
FileSize: Integer; { File size
(bytes) }
Frame: array [1..ID3V2_FRAME_COUNT] of string; { Information from
frames }
end;
{ ********************* Auxiliary functions & procedures
******************** }
function ReadHeader(const FileName: string; var Tag: TagInfo): Boolean;
var
SourceFile: file;
Transferred: Integer;
begin
try
Result := true;
{ Set read-access and open file }
AssignFile(SourceFile, FileName);
FileMode := 0;
Reset(SourceFile, 1);
{ Read header and get file size }
BlockRead(SourceFile, Tag, 10, Transferred);
Tag.FileSize := FileSize(SourceFile);
CloseFile(SourceFile);
{ if transfer is not complete }
if Transferred < 10 then Result := false;
except
{ Error }
Result := false;
end;
end;
{ ---------------------------------------------------------------------------
}
function GetVersionID(const Tag: TagInfo): Byte;
begin
{ Get tag version from header }
Result := Tag.Version;
end;
{ ---------------------------------------------------------------------------
}
function GetTagSize(const Tag: TagInfo): Integer;
begin
{ Get total tag size }
Result :=
Tag.Size[1] * $200000 +
Tag.Size[2] * $4000 +
Tag.Size[3] * $80 +
Tag.Size[4] + 10;
if Result > Tag.FileSize then Result := 0;
end;
{ ---------------------------------------------------------------------------
}
procedure SetTagItem(const ID, Data: string; var Tag: TagInfo);
var
Iterator: Byte;
begin
{ Set tag item if supported frame found }
for Iterator := 1 to ID3V2_FRAME_COUNT do
if ID3V2_FRAME[Iterator] = ID then Tag.Frame[Iterator] := Data;
end;
{ ---------------------------------------------------------------------------
}
function Swap32(const Figure: Integer): Integer;
var
ByteArray: array [1..4] of Byte absolute Figure;
begin
{ Swap 4 bytes }
Result :=
ByteArray[1] * $1000000 +
ByteArray[2] * $10000 +
ByteArray[3] * $100 +
ByteArray[4];
end;
{ ---------------------------------------------------------------------------
}
procedure ReadFrames(const FileName: string; var Tag: TagInfo);
var
SourceFile: file;
Frame: FrameHeader;
Data: array [1..250] of Char;
DataPosition: Integer;
begin
try
{ Set read-access, open file }
AssignFile(SourceFile, FileName);
FileMode := 0;
Reset(SourceFile, 1);
Seek(SourceFile, 10);
while (FilePos(SourceFile) < GetTagSize(Tag)) and (not
EOF(SourceFile)) do
begin
FillChar(Data, SizeOf(Data), 0);
{ Read frame header and check frame ID }
BlockRead(SourceFile, Frame, 10);
if not (Frame.ID[1] in ['A'..'Z']) then break;
DataPosition := FilePos(SourceFile);
{ Read frame data and set tag item if frame supported }
BlockRead(SourceFile, Data, Swap32(Frame.Size) mod SizeOf(Data));
SetTagItem(Frame.ID, Data, Tag);
Seek(SourceFile, DataPosition + Swap32(Frame.Size));
end;
CloseFile(SourceFile);
except
end;
end;
{ ---------------------------------------------------------------------------
}
function ExtractTrack(const TrackString: string): Byte;
var
Index, Value, Code: Integer;
begin
{ Extract track from string }
Index := Pos('/', Trim(TrackString));
if Index = 0 then Val(Trim(TrackString), Value, Code)
else Val(Copy(Trim(TrackString), 1, Index - 1), Value, Code);
if Code = 0 then Result := Value
else Result := 0;
end;
{ ---------------------------------------------------------------------------
}
function ExtractGenre(const GenreString: string): string;
begin
{ Extract genre from string }
Result := Trim(GenreString);
if Pos(')', Result) > 0 then Delete(Result, 1, LastDelimiter(')',
Result));
end;
{ ---------------------------------------------------------------------------
}
function RebuildFile(const FileName: string; NewTagData: TStream):
Boolean;
var
Tag: TagInfo;
Source, Destination: TFileStream;
BufferName: string;
begin
{ Rebuild file with old file data and new tag (optional) }
Result := false;
if (not FileExists(FileName)) or (FileSetAttr(FileName, 0) <> 0) then
exit;
if not ReadHeader(FileName, Tag) then exit;
if (NewTagData = nil) and (Tag.ID <> ID3V2_ID) then exit;
try
{ Create file streams }
BufferName := FileName + '~';
Source := TFileStream.Create(FileName, fmOpenRead or
fmShareExclusive);
Destination := TFileStream.Create(BufferName, fmCreate);
{ Copy data blocks }
if Tag.ID = ID3V2_ID then Source.Seek(GetTagSize(Tag),
soFromBeginning);
if NewTagData <> nil then Destination.CopyFrom(NewTagData, 0);
Destination.CopyFrom(Source, Source.Size - Source.Position);
{ Free resources }
Source.Free;
Destination.Free;
{ Replace old file and delete temporary file }
if (DeleteFile(FileName)) and (RenameFile(BufferName, FileName)) then
Result := true
else
raise Exception.Create('');
except
{ Access error }
if FileExists(BufferName) then DeleteFile(BufferName);
end;
end;
{ ---------------------------------------------------------------------------
}
procedure BuildHeader(var Tag: TagInfo);
var
Iterator, TagSize: Integer;
begin
{ Build tag header }
Tag.ID := ID3V2_ID;
Tag.Version := TAG_VERSION_2_3;
Tag.Revision := 0;
Tag.Flags := 0;
TagSize := 0;
for Iterator := 1 to ID3V2_FRAME_COUNT do
if Tag.Frame[Iterator] <> '' then
Inc(TagSize, Length(Tag.Frame[Iterator]) + 11);
{ Convert tag size }
Tag.Size[1] := TagSize div $200000;
Tag.Size[2] := TagSize div $4000;
Tag.Size[3] := TagSize div $80;
Tag.Size[4] := TagSize mod $80;
end;
{ ********************** Private functions & procedures
********************* }
procedure TID3v2.FSetTitle(const NewTitle: string);
begin
{ Set song title }
FTitle := TrimRight(NewTitle);
end;
{ ---------------------------------------------------------------------------
}
procedure TID3v2.FSetArtist(const NewArtist: string);
begin
{ Set artist name }
FArtist := TrimRight(NewArtist);
end;
{ ---------------------------------------------------------------------------
}
procedure TID3v2.FSetAlbum(const NewAlbum: string);
begin
{ Set album title }
FAlbum := TrimRight(NewAlbum);
end;
{ ---------------------------------------------------------------------------
}
procedure TID3v2.FSetTrack(const NewTrack: Byte);
begin
{ Set track number }
FTrack := NewTrack;
end;
{ ---------------------------------------------------------------------------
}
procedure TID3v2.FSetYear(const NewYear: string);
begin
{ Set release year }
FYear := TrimRight(NewYear);
end;
{ ---------------------------------------------------------------------------
}
procedure TID3v2.FSetGenre(const NewGenre: string);
begin
{ Set genre name }
FGenre := TrimRight(NewGenre);
end;
{ ---------------------------------------------------------------------------
}
procedure TID3v2.FSetComment(const NewComment: string);
begin
{ Set comment }
FComment := TrimRight(NewComment);
end;
{ ********************** Public functions & procedures
********************** }
constructor TID3v2.Create;
begin
{ Create object }
inherited;
ResetData;
end;
{ ---------------------------------------------------------------------------
}
procedure TID3v2.ResetData;
begin
{ Reset all variables }
FExists := false;
FVersionID := 0;
FSize := 0;
FTitle := '';
FArtist := '';
FAlbum := '';
FTrack := 0;
FYear := '';
FGenre := '';
FComment := '';
end;
{ ---------------------------------------------------------------------------
}
function TID3v2.ReadFromFile(const FileName: string): Boolean;
var
Tag: TagInfo;
begin
{ Reset data and load header from file to variable }
ResetData;
Result := ReadHeader(FileName, Tag);
{ Process data if loaded and header valid }
if (Result) and (Tag.ID = ID3V2_ID) then
begin
FExists := true;
{ Fill properties with header data }
FVersionID := GetVersionID(Tag);
FSize := GetTagSize(Tag);
{ Get information from frames if version supported }
if (FVersionID = TAG_VERSION_2_3) and (FSize > 0) then
begin
ReadFrames(FileName, Tag);
{ Fill properties with data from frames }
FTitle := Trim(Tag.Frame[1]);
FArtist := Trim(Tag.Frame[2]);
FAlbum := Trim(Tag.Frame[3]);
FTrack := ExtractTrack(Tag.Frame[4]);
FYear := Trim(Tag.Frame[5]);
FGenre := ExtractGenre(Tag.Frame[6]);
FComment := Trim(Copy(Tag.Frame[7], 5, Length(Tag.Frame[7]) - 4));
end;
end;
end;
{ ---------------------------------------------------------------------------
}
function TID3v2.RemoveFromFile(const FileName: string): Boolean;
begin
{ Remove tag from file }
Result := RebuildFile(FileName, nil);
end;
{ ---------------------------------------------------------------------------
}
function TID3v2.SaveToFile(const FileName: string): Boolean;
var
NewTagData: TStringStream;
Tag: TagInfo;
Iterator, FrameSize: Integer;
begin
{ Build tag and save to file }
NewTagData := TStringStream.Create('');
{ Prepare tag data }
FillChar(Tag, SizeOf(Tag), 0);
Tag.Frame[1] := FTitle;
Tag.Frame[2] := FArtist;
Tag.Frame[3] := FAlbum;
if FTrack > 0 then Tag.Frame[4] := IntToStr(FTrack);
Tag.Frame[5] := FYear;
Tag.Frame[6] := FGenre;
if FComment <> '' then Tag.Frame[7] := 'eng' + #0 + FComment;
{ Build and write tag header }
BuildHeader(Tag);
NewTagData.Write(Tag, 10);
{ Build and write tag frames }
for Iterator := 1 to ID3V2_FRAME_COUNT do
if Tag.Frame[Iterator] <> '' then
begin
NewTagData.WriteString(ID3V2_FRAME[Iterator]);
FrameSize := Swap32(Length(Tag.Frame[Iterator]) + 1);
NewTagData.Write(FrameSize, SizeOf(FrameSize));
NewTagData.WriteString(#0#0#0 + Tag.Frame[Iterator]);
end;
{ Rebuild file with new tag }
Result := RebuildFile(FileName, NewTagData);
NewTagData.Free;
end;
end.
=============================================================================
例子代码:
unit Main;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms,
Dialogs,
StdCtrls, FileCtrl, ExtCtrls, ID3v2;
type
TMainForm = class(TForm)
DriveList: TDriveComboBox;
FolderList: TDirectoryListBox;
FileList: TFileListBox;
CloseButton: TButton;
InfoBevel: TBevel;
IconImage: TImage;
TagExistsLabel: TLabel;
TagExistsValue: TEdit;
VersionLabel: TLabel;
VersionValue: TEdit;
SizeLabel: TLabel;
SizeValue: TEdit;
TitleLabel: TLabel;
TitleEdit: TEdit;
ArtistLabel: TLabel;
ArtistEdit: TEdit;
AlbumLabel: TLabel;
AlbumEdit: TEdit;
TrackLabel: TLabel;
TrackEdit: TEdit;
YearLabel: TLabel;
YearEdit: TEdit;
GenreLabel: TLabel;
GenreEdit: TEdit;
CommentLabel: TLabel;
CommentEdit: TEdit;
RemoveButton: TButton;
SaveButton: TButton;
procedure CloseButtonClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure FileListChange(Sender: TObject);
procedure FormClose(Sender: TObject; var Action: TCloseAction);
procedure SaveButtonClick(Sender: TObject);
procedure RemoveButtonClick(Sender: TObject);
private
{ Private declarations }
FileTag: TID3v2;
procedure ClearAll;
end;
var
MainForm: TMainForm;
implementation
{$R *.dfm}
procedure TMainForm.ClearAll;
begin
{ Clear all captions }
TagExistsValue.Text := '';
VersionValue.Text := '';
SizeValue.Text := '';
TitleEdit.Text := '';
ArtistEdit.Text := '';
AlbumEdit.Text := '';
TrackEdit.Text := '';
YearEdit.Text := '';
GenreEdit.Text := '';
CommentEdit.Text := '';
end;
procedure TMainForm.CloseButtonClick(Sender: TObject);
begin
{ Exit }
Close;
end;
procedure TMainForm.FormCreate(Sender: TObject);
begin
{ Create object and clear captions }
FileTag := TID3v2.Create;
ClearAll;
end;
procedure TMainForm.FileListChange(Sender: TObject);
begin
{ Clear captions }
ClearAll;
if FileList.FileName = '' then exit;
if FileExists(FileList.FileName) then
{ Load tag data }
if FileTag.ReadFromFile(FileList.FileName) then
if FileTag.Exists then
begin
{ Fill captions }
TagExistsValue.Text := 'Yes';
VersionValue.Text := '2.' + IntToStr(FileTag.VersionID);
SizeValue.Text := IntToStr(FileTag.Size) + ' bytes';
TitleEdit.Text := FileTag.Title;
ArtistEdit.Text := FileTag.Artist;
AlbumEdit.Text := FileTag.Album;
if FileTag.Track > 0 then TrackEdit.Text :=
IntToStr(FileTag.Track);
YearEdit.Text := FileTag.Year;
GenreEdit.Text := FileTag.Genre;
CommentEdit.Text := FileTag.Comment;
end
else
{ Tag not found }
TagExistsValue.Text := 'No'
else
{ Read error }
ShowMessage('Can not read tag from the file: ' + FileList.FileName)
else
{ File does not exist }
ShowMessage('The file does not exist: ' + FileList.FileName);
end;
procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
{ Free memory }
FileTag.Free;
end;
procedure TMainForm.SaveButtonClick(Sender: TObject);
var
Value, Code: Integer;
begin
{ Prepare tag data }
FileTag.Title := TitleEdit.Text;
FileTag.Artist := ArtistEdit.Text;
FileTag.Album := AlbumEdit.Text;
Val(TrackEdit.Text, Value, Code);
if (Code = 0) and (Value > 0) then FileTag.Track := Value
else FileTag.Track := 0;
FileTag.Year := YearEdit.Text;
FileTag.Genre := GenreEdit.Text;
FileTag.Comment := CommentEdit.Text;
{ Save tag data }
if (not FileExists(FileList.FileName)) or
(not FileTag.SaveToFile(FileList.FileName)) then
ShowMessage('Can not save tag to the file: ' + FileList.FileName);
FileListChange(Self);
end;
procedure TMainForm.RemoveButtonClick(Sender: TObject);
begin
{ Delete tag data }
if (FileExists(FileList.FileName)) and
(FileTag.RemoveFromFile(FileList.FileName)) then ClearAll
else ShowMessage('Can not remove tag from the file: ' +
FileList.FileName);
end;
end.