阅读 59

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 id3固定为“ID3”
version2$04 00
flags1%abcd0000
size44 * %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 size4整个扩展头的大小
Number of flag bytes1$01
Extended Flags1%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:图片大小不能大于256x256像素

  • 10:图片大小不能大于64x64像素

  • 11:图片大小只能是64x64像素

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反了一下,如下:

字段大小/字节描述
identifier3“3DI”
version2$04 00
flags1%abcd0000
size44 * %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。感谢阅读支持!


作者:ijinfeng
链接:https://juejin.cn/post/7168678355020021796


文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐