Flutter ID3解码实现-v2.4

之前用Flutter开发了一个桌面版的网易云音乐,传送门:源码文章。其中有个下载功能,当时是用的mp3+JSON的方式实现下载并保存歌曲信息的。这样有个问题,信息的分离和容易解析出错。其实更好的方式是将歌曲信息写入到MP3文件内部,这里就要用到ID3的知识了。这也是这篇文章的由来。目前,我已经实现并开源了一个id3_codec库来帮助解析读取mp3文件中的ID3信息。接下来我将详细讲解下ID3的原理和代码的实现过程。

Flutter ID3解码实现- v1、v1.1、v2.2、v2.3

概述

ID3是一种metadata容器,多应用于MP3格式的音频文件中。它可以将相关的曲名、演唱者、专辑、音轨数等信息存储在MP3文件中,又称作“ID3Tags”。

ID3一般位于一个mp3文件的开头或末尾的若干字节内,附加了关于该mp3的歌手,标题,专辑名称,年代,风格等信息,该信息就被称为ID3信息。ID3主要分为两个大版本,v1和v2版。这是两个完全不一样的版本,两者间的格式相差巨大。ID3v1两个小版本,分别是ID3v1和ID3v1.1。ID3v2则有三个小版本,ID3v2.2、v2.3、v2.4。接下来我将详细讲解下关于v2.4的结构和解码过程。

ID3v2.4是ID3v2.3的升级版,两者结构相似,但是v2.4更易扩展和修改。

我们先来整体看下ID3v2.4的结构:

Header (10 bytes)
Extended Header
(variable length, OPTIONAL)
Frames (variable length)
Padding
(variable length, OPTIONAL)
Footer
(10 bytes, OPTIONAL)

从表格中很容易看出,这结构和ID3v2.3很像,只是在尾部多处一个可选的Footer。至于这个Footer什么时候出现,留在后面介绍。接下来我们一点点剖析它的结构。

Header

header和ID3v2.3及其相似,只有flags字段略有不同。

字段 大小/字节 描述
file id 3 固定为“ID3”
version 2 $04 00
flags 1 %abcd0000
size 4 4 * %0xxxxxxx

开头以3个字节存标识符“ID3”。表示ID3v2的开始,紧接着就是版本信息,占2个字节。之后是1哥子节的flags字段,在ID3v2.4中共四个flag值。

a – 不同步(Unsynchronisation

表示所有数据帧是否不同步。

b – 扩展Header(Extended header

表示是否存在扩展头Extended header

c – 实验指标(Experimental indicator

表示是否处于实验阶段,如果当前标签处于实验测试阶段,这个值将被设置为1。

d – 是否存在Footer(Footer present

表示是否在尾部存在Footer。

接下来说说Size字段及它的计算方法。其实有关内容在文章《Flutter ID3解码实现- v1、v1.1、v2.2、v2.3》中有详细描述,这边遇到了就再讲解一遍。Size共占用4个字节,每个字节的最高位不存数据恒定为0,因此实际计算时只用到了28bit。最大可以存储256M的标签内容。

我们假定有4个字节的数据List<int> sizeBytes,那么size的计算方式如下(注意运算符优先级,该加的括号别漏了):

int size = (sizeBytes[3] & 0x7F) +
        ((sizeBytes[2] & 0x7F) << 7) +
        ((sizeBytes[1] & 0x7F) << 14) +
        ((sizeBytes[0] & 0x7F) << 21);

到这里Header就解析完了,都是按固定套路完成的,比较简单。

int _parseHeader(int start) {
    // Parse Version Tag
    _major = readValue(header[1], start).toString();
    start += header[1].length;
    _revision = readValue(header[2], start).toString();
    start += header[2].length;

    // Parse Flags Tag
    final flags = readValue(header[3], start);
    start += header[3].length;
    _hasExtendedHeader = false;
    _hasFooter = false;

    // ID3v2.4  flags  %abcd0000
      bool unsynchronisation = (flags & 0x80) != 0;
      _hasExtendedHeader = (flags & 0x40) != 0;
      bool experimentalIndicator = (flags & 0x20) != 0;
      /*
        d - Footer present

     Bit 4 indicates that a footer (section 3.4) is present at the very
     end of the tag. A set bit indicates the presence of a footer.
      */
      _hasFooter = (flags & 0x10) != 0;

    // Parse Size Tag
    List<int> sizeBytes = readValue(header[4], start);
    start += header[4].length;
    /*
      The ID3v2 tag size is encoded with four bytes where the most
    significant bit (bit 7) is set to zero in every byte, making a total
    of 28 bits. The zeroed bits are ignored, so a 257 bytes long tag is
    represented as $00 00 02 01.

    [ID3v2.4]The ID3v2 tag size is the sum of the byte length of the extended
   header, the padding and the frames after unsynchronisation. If a
   footer is present this equals to ('total size' - 20) bytes, otherwise
   ('total size' - 10) bytes.
    */
    int size = (sizeBytes[3] & 0x7F) +
        ((sizeBytes[2] & 0x7F) << 7) +
        ((sizeBytes[1] & 0x7F) << 14) +
        ((sizeBytes[0] & 0x7F) << 21);
    _size = size;
    return start;
  }

Extended Header

如果在第一步的Headerflags中解出存在Extended Header,也就是flagsb为1。那么我们第二步就是解析扩展头的数据。相较于ID3v2.3而言,ID3v2.4的Extended Header改变了不少内容。

字段 大小/字节 描述
Extended header size 4 整个扩展头的大小
Number of flag bytes 1 $01
Extended Flags 1 %0bcd0000
Flag data 由具体flag而定 flag具体内容

我们一起来分析下这个Extended Header,首先它由一个Extended header size字段存储整个扩展头的大小,其大小将不下于6个字节。接下来是Number of flag bytes字段,在ID3v2.4版本中固定为1,表示有1组Extended flags。换句话说,在接下来的版本,Extended Flags有可能有多组,数量由Number of flag bytes来控制。在后面就是Flag data。Flag data紧跟在Extended Header后面,顺序由设置的flag先后而定。

在ID3v2.4种,Extended Flags是一个字节大小,有三个flag标志位。

b – 标签是一个更新(Tag is an update

简单说就是 在读取frame的时候,如果有重复的,那么新的frame将覆盖旧的frame内容。这个flag没有对应的Flag data

c – 存在CRC数据(CRC data present

CRC-32由所有的frame和Padding共同计算而来,共占用5个字节,每个字节的最高位总时为0。这个数据将被存在Flag data中。结构如下:

Flag data length       $05
Total frame CRC    5 * %0xxxxxxx

d – 标签限制(Tag restrictions

对写入标签进行限制,所以写入标签时必须要检查这个旗标以及其Flag Data字段。这个旗标打开时产生的Flag Data总共占2个子节,格式如下:

Flag data length       $01
Restrictions           %ppqrrstt

pp – 标签大小限制

  • 00:128个以下的frame数量,1MB以下的总标签大小
  • 01:64个以下的frame数量,128KB以下的总标签大小
  • 10:32个以下的frame数量,40KB以下的总标签大小
  • 11:32个以下的frame数量,4KB以下的总标签大小

q – 文本编码限制

  • 0:没有限制
  • 1:只能用ISO-8859-1或UTF-8编码

rr – 文本字段大小限制

  • 00:没有限制
  • 01:字符串不得长于1024个字符
  • 10:字符串不得长于128个字符
  • 11:字符串不得长于30个字符

s – 图片编码限制

  • 0:没有限制
  • 1:图片只能编码为PNG或JPEG格式

tt – 图片大小限制

  • 00:没有限制
  • 01:图片大小不能大于256×256像素
  • 10:图片大小不能大于64×64像素
  • 11:图片大小只能是64×64像素
int _parseV2_4ExtendedHeader(int start) {
    // Extended header size   4 * %0xxxxxxx
    // Where the 'Extended header size' is the size of the whole extended
    // header, stored as a 32 bit synchsafe integer.
    final extendedSizeBytes = readValue(extendedV2_4Header[0], start);
    start += extendedV2_4Header[0].length;
    _extendedSize = (extendedSizeBytes[3] & 0x7F) +
        ((extendedSizeBytes[2] & 0x7F) << 7) +
        ((extendedSizeBytes[1] & 0x7F) << 14) +
        ((extendedSizeBytes[0] & 0x7F) << 21);

    // Number of flag bytes       $01
    final numberOfFlagBytes = readValue(extendedV2_4Header[1], start);
    start += extendedV2_4Header[1].length;

    // Extended Flags             $xx = %0bcd0000
    // There is only one set of extended flags in v2.4
    // Each flag that is set in the extended header has data attached
    // Attach structure:
    // -----------------------------
    // | Flag data length | 1byte  |
    // -----------------------------
    // | Flag content     | xbytes |
    // -----------------------------
    final extendedFlag = readValue(extendedV2_4Header[2], start);
    start += extendedV2_4Header[2].length;

    /*
      b - Tag is an update

      If this flag is set, the present tag is an update of a tag found
     earlier in the present file or stream. If frames defined as unique
     are found in the present tag, they are to override any
     corresponding ones found in the earlier tag. This flag has no
     corresponding data.

         Flag data length      $00
    */
    final b = (extendedFlag & 0x40) != 0;
    if (b) {
      // If this flag is set, Flag data length is one byte, no content.
      start += 1;
    }

    /*
      c - CRC data present

     If this flag is set, a CRC-32 [ISO-3309] data is included in the
     extended header. The CRC is calculated on all the data between the
     header and footer as indicated by the header's tag length field,
     minus the extended header. Note that this includes the padding (if
     there is any), but excludes the footer. The CRC-32 is stored as an
     35 bit synchsafe integer, leaving the upper four bits always
     zeroed.

        Flag data length       $05
        Total frame CRC    5 * %0xxxxxxx
    */
    final c = (extendedFlag & 0x20) != 0;
    if (c) {
      start += 6;
    }
    /*
      d - Tag restrictions

      For some applications it might be desired to restrict a tag in more
     ways than imposed by the ID3v2 specification. Note that the
     presence of these restrictions does not affect how the tag is
     decoded, merely how it was restricted before encoding. If this flag
     is set the tag is restricted as follows:

        Flag data length       $01
        Restrictions           %ppqrrstt
    */
    final d = (extendedFlag & 0x10) != 0;
    if (d) {
      start += 1;

      final flagContent = bytes.sublist(start, 1).first;
      start += 1;
      /*
       p - Tag size restrictions

      00   No more than 128 frames and 1 MB total tag size.
       01   No more than 64 frames and 128 KB total tag size.
       10   No more than 32 frames and 40 KB total tag size.
       11   No more than 32 frames and 4 KB total tag size.
      */
      final p = (flagContent & 0xC0) >> 6

      /*
      q - Text encoding restrictions

       0    No restrictions
       1    Strings are only encoded with ISO-8859-1 [ISO-8859-1] or
            UTF-8 [UTF-8].
      */
      final q = (flagContent & 0x20) >> 5;

      /*
        r - Text fields size restrictions

       00   No restrictions
       01   No string is longer than 1024 characters.
       10   No string is longer than 128 characters.
       11   No string is longer than 30 characters.
      */
      final r = (flagContent & 0x18) >> 3;

      /*
      s - Image encoding restrictions

       0   No restrictions
       1   Images are encoded only with PNG [PNG] or JPEG [JFIF].
      */
      final s = (flagContent & 0x4) >> 2

      /*
      t - Image size restrictions

       00  No restrictions
       01  All images are 256x256 pixels or smaller.
       10  All images are 64x64 pixels or smaller.
       11  All images are exactly 64x64 pixels, unless required
           otherwise.
      */
      final t = flagContent & 0x3;
    }
    return start;
  }

Frames

数据帧架的解析和ID3v2.3基本一致,我在程序中也是复用封装好的ContentDecoder做为解析的具体实现。拿一个常见的数据帧做下介绍,比如我要解析“TXXX”。

class ContentDecoder {
  ContentDecoder({
    required this.frameID,
    required this.bytes,
  }) {
    if (frameID == 'TXXX' || frameID == 'TXX') {
      _decoder = _TXXXDecoder(frameID);
} 
// other frame
}

TXXX解码具体实现。

/*
  <Header for 'User defined text information frame', ID: "TXXX">
     Text encoding     $xx
     Description       <text string according to encoding> $00 (00)
     Value             <text string according to encoding>
*/
class _TXXXDecoder extends _ContentDecoder {
  _TXXXDecoder(super.frameID);

  @override
  FrameContent decode(List<int> bytes) {
    final content = FrameContent();
    int start = 0;

    final encoding = bytes.sublist(0, 1).first;
    final codec = ByteCodec(textEncodingByte: encoding);
    start += 1;

    // Description
    final descBytes = codec.readBytesUtilTerminator(bytes.sublist(start));
    content.set('Description', codec.decode(descBytes.bytes));
    start += descBytes.length;

    // Value
    final value = codec.decode(bytes.sublist(start));
    content.set('Value', value);
    return content;
  }
}

其实到这里,ID3v2.4的主要内容已经讲完了。剩下的Padding和Footer都是为了使得解析和扩展更方便而优化的结构。

Padding

当ID3标签处于文件头部的时候,为了易于修改,而不重写音频数据,在ID3和音频之间填充一些值为0的Padding将使修改变得容易。不过当ID3v2.4位于文件尾部的时候,Padding是不需要的,那个时候用到的是Footer。

Footer

当ID3标签处于稳健尾部的时候,就需要用到Footer了,注意了,此时是不能有Padding的。Footer的结构和Header一样,只不过identifier反了一下,如下:

字段 大小/字节 描述
identifier 3 “3DI”
version 2 $04 00
flags 1 %abcd0000
size 4 4 * %0xxxxxxx

我们从文件末尾(倒数第10个字节)开始读取,如果读到了3DI的标识字符串,那么表示在文件末尾存在ID3v2.4标签。这里要注意的是size的大小,它表示的是扩展头+Padding+frames的总大小。因此,当存在Footer时,标签的总大小=20+size,否则标签总大小=10+size。

int _searchFooterReturnFixStart(int start) {
    // ID
    final idBytes = bytes.sublist(start, start + 3);
    final id = latin1.decode(idBytes);
    if (id != '3DI') {
      return 0;
    }
    start += 3;

    // add version and flags byte sizes
    start += 3;

    // size
    final sizeBytes = bytes.sublist(start, start + 4);
    start += 4;
    int size = (sizeBytes[3] & 0x7F) +
        ((sizeBytes[2] & 0x7F) << 7) +
        ((sizeBytes[1] & 0x7F) << 14) +
        ((sizeBytes[0] & 0x7F) << 21);

    // fix start
    start -= (10 /*footer size*/
        +
        size /*the sum of the byte length of the extended header, the padding and the frames after unsynchronisation*/
        +
        10 /*header size*/);
    return start;
  }

总结

本文详细讲解了ID3v2.4版本的标签结构及解析过程,ID3v2.4标签可以位于文件头部,也可以位于文件尾部,当在尾部的时候将在标签最后拼接一个10字节的Footer,以便于识别。我已将所有内容都放在代码仓库id3_codec中,你也可以直接在Flutter项目中使用pub的方式引入:id3_codec: ^0.0.1。感谢阅读支持!

参考资料

MP3标签格式(ID3,APE)超详细解释

别人开发的id3库

id3v2.4.0-frames官方文档

id3v2.4.0-structure官方文档

ID3维基百科

© 版权声明
THE END
喜欢就支持一下吧
点赞10 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容