65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 ID3 进行任何操作

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (74投票s)

2007 年 3 月 6 日

11分钟阅读

viewsIcon

473315

downloadIcon

23964

用于读写 ID3 的类

引言

ID3 是附加到 mp3 文件上的额外信息。这些信息可以是:文本、文件、图片、歌词等等。

Screenshot - TextInfo.png
ID3 编辑器 – 文本信息对话框

ID3Info 是一个允许您管理文件 ID3 的类。我的文章就是关于这个类的。

要详细了解 ID3 的工作原理,请参阅 The ID3 Homepage。这是帮助您理解 ID3 详细信息的最佳场所。在本文中,我不会解释 ID3 的细节。我将只解释 ID3 的基本知识以及 ID3Info 类的工作原理。

此类别可以读/写 ID3 的两个版本:ID3v1 和 ID3v2。对于 ID3v2 的次要版本,此类别可以读取 ID3v2.2、ID3v2.3、ID3v2.4,并可以写入 ID3v2.3 和 ID3v2.4。

ID3 最流行的版本是 2.3.0 和 1 版本。

在本文中,我的重点更多地放在 ID3v2 上,因为有太多其他关于 ID3v1 的优秀参考资料。

要理解这个类的工作原理,您需要了解一些基本知识。类、继承以及一些有用的 .NET 类(如 ArrayListFileStreamMemoryStream 等)。

我写了一个带有 ID3Info 的应用程序。这个应用程序没有使用 ID3Info 60% 以上的功能。我只是写了这个应用程序来查找 bug。在我看来,它还不完整。我将尝试用我自己的时间和用户的想法来改进它。

我在这里只解释一些基本知识。

ID3v1

Screenshot - General.png
ID3 编辑器 – 通用对话框

ID3v1 的使用非常简单。文件最后的 128 字节可能包含 ID3v1 信息。它以“TAG”开头,这是 ID3v1 标识符。“TAG”关键字后面跟着标题(30 个字符)、艺术家(30 个字符)、专辑(30 个字符)、年份(4 个字符)、评论(28 个字符)、曲目号(1 字节)和流派(1 字节)。

流派在 ID3 定义中有一个表格。所有软件都必须使用该表格。ID3Info 返回的是流派编号而不是流派名称。

正如您所见,ID3v1 并没有什么复杂之处。

ID3v1 类,在 ID3.ID3v1Frames 命名空间中处理 ID3v1 作业。

private string _FilePath;
private string _Title;
private string _Artist;
private string _Album;
private string _Year;
private string _Comment;
private byte _TrackNumber;
private byte _Genre;

这个类中的变量存储 ID3v1 数据。每个变量都有一些属性,它们检查 ID3v1 的限制。例如,_Comment 不允许评论超过 28 个字符。

还有一个变量允许 ID3Info 类知道当前文件是否必须包含 ID3v1。这个变量名为 _HaveTag

在构造函数中,将 _FilePath 变量设置为文件的路径。如果程序员想在构造函数中加载 ID3v1 数据(使用 LoadData 参数),否则加载会将信息设置为“” 。

加载 ID3v1

要加载 ID3v1,您必须使用“Load()”方法。此方法首先为一个文件定义一个 FileStream,并尝试确定文件是否包含 ID3 数据。HaveID3 方法确定文件是否包含 ID3v1。如果文件包含 ID3v1,HaveID3 会尝试加载它。

为了读取文本,我使用了“Frame.ReadText”方法。此方法根据 TextEncoding 和可用最大长度,仅从指定的 FileStream 中读取文本。

public void Load()
{ 
    FileStream FS = new FileStream(_FilePath, FileMode.Open);
    if (!HaveID3(FS))
    {
        FS.Close(); 
        _HaveTag = false;
        return;
    }
    _Title = Frame.ReadText(FS, 30, TextEncodings.Ascii);
    FS.Seek(-95, SeekOrigin.End);
    //…
    FS.Seek(-2, SeekOrigin.End);
    _TrackNumber = Convert.ToByte(FS.ReadByte());
    _Genre = Convert.ToByte(FS.ReadByte());
    FS.Close();
    _HaveTag = true;
}   

保存 ID3v1

要保存 ID3v1,我们首先需要了解文件当前是否具有 ID3。为此,请使用“HaveID3(FilesStream)”函数。如果文件当前包含 ID3 并且用户想要删除 ID3,我们只需将其删除。如果文件当前不包含 ID3 并且用户想要保存信息,我们只需在文件末尾写入 ID3。如果您想更改 ID3 信息,则必须覆盖它。

GetTagBytes 是一个将 ID3 信息转换为字节数组的属性。

public void Save()
{
    FileStream fs = new FileStream(_FilePath, FileMode.Open);
    bool HTag = HaveID3(fs);
    if (HTag && !_HaveTag) // just delete ID3
        fs.SetLength(fs.Length - 128);
    else if (!HTag && _HaveTag)
    {
        fs.Seek(0, SeekOrigin.End);
        fs.Write(GetTagBytes, 0, 128);
    }
    else if (HTag && _HaveTag)
    {
        fs.Seek(-128, SeekOrigin.End);
        fs.Write(GetTagBytes, 0, 128);
    }
    fs.Close();
} 

带有 ID3 的 Mp3 文件看起来像这样

Screenshot - Mp3File.png

ID3v2

ID3v2 保存在文件开头。它保存为帧。每个帧包含一些信息,例如,一个帧包含艺术家,另一个包含歌曲标题。帧还包含:发布日期、作曲家、与曲目相关的图片、曲目号以及更多信息。

每个帧包含两个主要部分

  1. 帧头:包含一个 FrameID,指示此帧包含什么。例如,如果 FrameID 是“TIT2”,这意味着此帧包含“歌曲标题”。帧头还包含一些标志以及以字节为单位的帧长度。有关 ID3 帧的更多信息,请参阅 The ID3 Homepage
  2. 帧数据:包含该帧的数据。每个帧都有自己的结构来存储数据。

我将 ID3 帧分为 25 个类。所有这些都继承了“Frame”类。

ID3v2 次要版本

ID3v2 有一些次要版本,它们之间存在细微差别。主要区别在于 2.2 版本。在此版本中,FrameID 是三个字符,但在后续版本中增加到四个字符。并且在 v2.2 中,帧头不包含帧标志。许多帧已添加到 v2.3,有些已添加到 v2.4。最流行的版本是 v2.3。

Frame 类

在“Main.cs”文件中是“Frame”类。这是用于 ID3 帧的主类。正如您所见,这是一个抽象类(在 Visual Basic 中必须继承),因为对于任何帧都有另一个类继承该类。这个类完成了一些最重要的工作。

在这个类中,我们有一些抽象属性和方法。

  • IsAvailable:指示当前信息对此帧是否有效。
  • FrameStream:获取一个 MemoryStream,其中包含要保存的帧数据。
  • Length:指示此帧需要多少字节才能保存。

这两个属性和一个方法必须由所有继承的类覆盖。此类还包含一些用于从 FileStream 读取/写入数据的静态方法。

当我们要保存一个帧时,我们首先需要保存帧头。FrameHeader 方法创建一个 MemoryStream 并将帧头写入其中。此方法将在继承的帧中用于在帧头之后写入其自己的数据。

ID3v2 类

这个类读取和写入 ID3v2。在构造函数中,您需要指定文件路径,并确定是否加载其数据。

要加载数据,您可以使用筛选器选项。

Filter:您可以通过打开一些特殊帧来指定类。想象一下您有一个播放器应用程序,并且您只想查看文件的 ID3 “歌曲标题”。在您的应用程序中,您只显示“歌曲标题”。那么为什么您需要加载所有帧呢?这会使应用程序变慢,并消耗更多内存来存储无用信息。

您可以将“TIT2”添加到 Filter。并将 FilterType 设置为 LoadFiltersOnly 以加载曲目标题(如果存在)。您必须始终记住:如果您使用过滤,请不要保存文件信息,因为您可能会丢失一些数据。

Load 方法检查文件是否有 ID3(HaveID3 方法)。如果文件有 ID3,则 Load 开始读取帧。但首先,必须使用 ReadVersion(FileStream) 方法读取版本信息。此方法将版本信息存储在 _Ver 变量中,该变量可以通过“VersionInfo”属性访问。然后通过“ReadFrames”读取所有帧。

public void Load()
{
    FileStream ID3File = new FileStream(_FilePath, FileMode.Open);
    if (!HaveID3(ID3File)) // If file doesn't contain ID3v2 exit function
    {
        _HaveTag = false;
        ID3File.Close();
        return;
    }
    ReadVersion(ID3File); // Read ID3v2 version
    _Flags = (ID3v2Flags)ID3File.ReadByte();
    // Extended Header Must Read Here
    ReadFrames(ID3File, ReadSize(ID3File));
    ID3File.Close();
    _HaveTag = true;
}  

读取帧方法

注意:ID3v2.2 中的 FrameID 是三个字符,但在后续版本中是四个字符。

private void ReadFrames(FileStream Data, int Length)
{
    string FrameID;
    int FrameLength;
    FrameFlags Flags = new FrameFlags();
    byte Buf;
    // If ID3v2 is ID3v2.2 FrameID, FrameLength of Frames is 3 byte
    // otherwise it's 4 character
    int FrameIDLen = VersionInfo.Minor == 2 ? 3 : 4;
    // Minimum frame size is 10 because frame header is 10 byte
    while (Length > 10)
    {
        // check for padding( 00 bytes )
        Buf = Frame.ReadByte(Data);
        if (Buf == 0)
        {
            Length--;
            continue;
        }
        // if read byte is not zero. it must read as FrameID
        Data.Seek(-1, SeekOrigin.Current);
        // ---------- Read Frame Header -----------------------
        FrameID = Frame.ReadText(Data, FrameIDLen, TextEncodings.Ascii);
        if (FrameIDLen == 3)
        FrameID = FramesList.Get4CharID(FrameID);
        FrameLength = Convert.ToInt32(Frame.Read(Data, FrameIDLen));
        if (FrameIDLen == 4)
            Flags = (FrameFlags)Frame.Read(Data, 2);
        else
            Flags = 0; // must set to default flag
        long Position = Data.Position;
        if (Length > 0x10000000)
            throw (new FileLoadException("This file contain frame that 
                have more than 256MB data"));
        bool Added = false;
        if (IsAddable(FrameID)) // Check if frame is not filter
        Added = AddFrame(Data, FrameID, FrameLength, Flags);
        if (!Added)
        {
            // if don't read this frame
            // we must go forward to read next frame
            Data.Position = Position + FrameLength;
        }
        Length -= FrameLength + 10;
    }
}

帧如何存储在此类中?

正如您所见,“ReadFrames”读取帧头,而“AddFrame”方法尝试加载帧的内容。“AddFrame”方法首先检查帧是否为文本帧。

文本帧是仅包含文本的帧。例如:标题、艺术家、专辑等。所有文本帧的 FrameID 都以“T”开头;您可以在 ID3 文档中找到文本帧列表。如果不是文本帧,则使用 switch 语句来尝试确定它是什么类型的帧。

Screenshot - Files.png
ID3 编辑器 – 文件对话框

正如我之前提到的,大约有 25 个类用于存储帧。您可以在“Frame classes”目录中找到这些类。Switch 语句使用 FrameID 来选择“正确”的类。例如,如果 FrameID 是“APIC”。

AttachedPictureFrame TempAttachedPictureFrame = new AttachedPictureFrame(
    FrameID, Flags, Data, Length);
if (TempAttachedPictureFrame.IsReadableFrame)
{
    _AttachedPictureFrames.Add(TempAttachedPictureFrame);
    return true;
}
else
    AddError(new ID3Error(TempAttachedPictureFrame.ErrorMessage, FrameID));
break;  

正如您所见,我创建了“AttachedPictureFrame”。这个类继承了 Frame 类,并从帧中读取附加图片的信息。在创建附加图片后,通过“IsReadable”属性检查帧数据是否可读。如果数据可读,“AttachedPictureFrame”会尝试将此附加图片添加到附加图片数组中。

注意:您可以在某些帧中存储多个信息。例如,在某些帧中,您可以保存任意数量的图片。但对于其他帧则不可能。例如,在相对音量帧中,ID3v2 类只包含该帧的一个变量。(要更好地理解可以包含额外信息的帧,请参阅参考资料)。

错误

如果您更仔细地查看 AttachedPicture 的代码,您会发现 else 语句包含一个“AddError”方法。如果出于任何原因,Frame 类(AttachedPictureFrame 继承自该类)无法读取帧的数据,IsReadable 属性将返回 false。这意味着在读取时发生了错误,因此程序会将错误消息添加到错误数组中。

为什么是错误集合而不是异常?

将错误添加到集合中允许程序加载没有错误的其它帧。如果在发生错误时程序抛出异常,则后续帧将无法读取。

请记住,您可以清除错误列表,并且在需要时必须这样做。

帧的相等性

正如我多次说过的,对于每种类型的帧,我们都有一个特殊的类。想象一下,对于一个 mp3 文件,我们开始读取帧。我们找到一个带有“TIT2”FrameID 的帧。然后找到另一个带有“TIT2”FrameID 的帧。我们能有两个带有“TIT2”FrameID 的帧吗?答案是不能。我们必须用第二个帧覆盖第一个帧。这意味着如果我们找到两个带有 FrameID 的帧,它们不能重复(尽管有些 FrameID 可以重复)——我们必须存储最后一个。

对于每个帧类,都有一个 Equal 方法。它是从 object 类继承的方法。为了解释 Equal 方法,我将使用“TextFrame”类作为示例。正如您所见,“TextFrame”类的相等性覆盖如下(“TextFrames.cs”)。

public override bool Equals(object obj)
{
    if (obj.GetType() != this.GetType())
        return false;
    // if FrameID of two text frames were equal they are equal
    // ( the text value is not important )
    if (this._FrameID == ((TextFrame)obj)._FrameID)
        return true;
    else
        return false;
}  

在此方法中,我检查了当前帧类的 FrameID 是否与另一个相等。它们是相等的。帧的文本不重要。这是因为对于文本帧,我们不能有多个具有相同 FrameID 的帧。

此覆盖的用法在“ArrayList”类中。当我们想将新帧添加到列表中时,我们可以简单地使用“ArrayList.Remove”删除前一个(因为此方法使用“Object.Equal”方法)。

所以,请始终记住,帧的相等性意味着没有两个帧具有完全相同的数据。

Screenshot - Lyric.png
ID3 编辑器 – 歌词编辑器对话框

使用 ID3Info

在本部分,我将解释如何使用 ID3Info 类。我已在“ID3 editor”应用程序中使用了 ID3Info 类。要查看如何使用 ID3Info 的示例,您可以查看“ID3 Editor”的源代码。

如何加载 ID3Info

要加载数据,您可以使用构造函数。

ID3Info File = new ID3Info("Path", true);

或者

ID3Info File = new ID3Info("Path", false);
// You can use filter here
// ID3File.ID3v2Info.Filter.Add("TIT2");
// ID3File.ID3v2Info.FilterType = FilterTypes.LoadFiltersOnly;
File.Load(); 

读取文本信息

在定义了 ID3Info 类并加载数据后,您需要从类中获取数据。要获取文本信息,您可以使用 GetTextFrame 方法,如下所示。

ID3Info File = new ID3Info("Path", true);
String Title = File.ID3v2Info.GetTextFrame("TIT2");
And to set text information you can use 'SetInformation' method, And below.
ID3Info File = new ID3Info("Path", true);
File.ID3v2Info.SetTextFrame("TIT2","My Title", TextEncodings.Ascii, 
    ID3File.ID3v2Info.VersionInfo.Minor); 

注意:对于任何文本,您都必须设置文本编码和 ID3v2 的次要版本。这是因为这两个参数对文本信息很重要。在下一个版本中,它们都将是自动的。

您可以从 File.ID3v2Info.TextFrames 获取与文件相关的 W所有文本帧的列表。要获取 FrameID 列表,请查看 FrameID 表。

未知帧

如果应用程序找不到 FrameID 的含义,它不会删除该帧。该帧将被添加到未知帧数组(File.ID3v2Info.UnKnownFrames)中。还有一个布尔属性指示您是否要保存未知帧(File.ID3v2Info.DropUnknown)。

加载链接帧

链接帧是从另一个文件附加的信息。例如,您可以将一张图片附加到一个文件,然后对于同一专辑中的另一个文件,使用链接帧从该文件中添加相同的图片。这样,您就可以节省内存,消耗更少的内存,节省数据。

链接帧指示必须从哪个文件读取哪个 FrameID。如果将 ID3File.ID3v2Info.LoadLinkedFrames 的布尔属性设置为 true,则应用程序将在加载数据时自动加载链接帧。

保存

要保存数据,您可以调用“ID3Info.Save()”方法。这将正常保存 ID3v1,并以次要版本 3 保存 ID3v2。或者使用 ID3Info.Save(MinorVersion of ID3v2)。最后,您可以使用 ID3Info.Save(ID3v2 minor version, Rename string)。Rename string 是一个字符串,可让您在保存时重命名文件。在重命名字符串中,您可以使用 [Title]、[Track]、[00Track]、[000Track]、[Album]。这些是区分大小写的。

示例

加载此字符串“[Title] – [Album]”

ID3Info File = new ID3Info("Path", true);
String MyText = File.ID3v2Info.GetTextFrame("TIT2") + " - " + 
    File.ID3v2Info.GetTextFrame("TALB");
// Now my text contain Title – Album

加载所有附加图片

using ID3;
using ID3.ID3v2Frames.BinaryFrames

// Code:
ID3Info File = new ID3Info("Path", true);
Image[] ImageArray = new Image[File.ID3v2Info.AttachedPictureFrames.Count];
for(int I = 0; I < File.ID3v2Info.AttachedPictureFrames.Count; i++)
    ImageArray[I] = Image.FromStream(
        ID3File.ID3v2Info.AttachedPictureFrames.Items[i])
// now ImageArray contain array of images attached to file

检索英文未同步歌词

此方法返回英文歌词(如果存在)。

using ID3;
using ID3.TextFrames;
ID3Info File = new ID3Info("Path", true);
foreach(TextWithLanguageFrame TWL in File.ID3v2Info.TextWithLanguageFrames)
    if(TWL.FrameID == "USLT" && TWL.Language == "ENG")
        return TWL.Text;
return null; 

有关更多示例,您可以查看“ID3 Editor”的源代码。如果有问题,请联系我。

注意:ID3 编辑器有一个帮助功能,您可以使用它来了解 ID3 编辑器的工作原理。源代码也可用于查看如何处理每个帧。

未来

我知道 ID3Class 应用程序的某些部分需要一些更改。它可以更强大。我将尝试改进 ID3Info 类和 ID3 Editor。如果您有任何关于如何使此应用程序更出色的想法,请与我联系。在此处写下您的评论。

© . All rights reserved.