之前用Flutter开发了一个桌面版的网易云音乐,传送门:源码,文章。其中有个下载功能,当时是用的mp3+JSON的方式实现下载并保存歌曲信息的。这样有个问题,信息的分离和容易解析出错。其实更好的方式是将歌曲信息写入到MP3文件内部,这里就要用到ID3的知识了。这也是这系列篇文章的由来。目前,我已经实现并开源了一个id3_codec库来帮助编解码mp3文件中的ID3信息。
前文引导👇
Flutter ID3解码实现- v1、v1.1、v2.2、v2.3
前面两篇文章都是讲述的如何去解码MP3中的ID3信息,现在,终于到了实现编码的时候了。一旦实现了编码部分,我在开篇中讲述的下载MP3后信息存储的问题也将由刃而解。而已经实现的解码部分则可以辅助我们验证编码的成功与否,当然你也可以使用社区相关解析库进行验证。
话不多说,我们先从最简单的ID3v1开始。
编码ID3v1
有关ID3v1数据结构的问题,可以阅读我前面写过的相关文章,这里不再做详细讲述。
要实现编码,那么首先我们需要读取MP3数据,并判断当前MP3文件中是否已经存在ID3v1标签了。如果存在,那么就是编辑标签,否则就需要在文件末尾添加128个字节作为接下来的占位符使用。
我对外定义了一个统一的类,通过初始化类ID3Encoder
来调用decode:
接口实现不同版本ID3的编码过程。
int start = bytes.length - 128;
final codec = ByteCodec();
List<int> output = List.from(bytes);
// exist ID3v1 tags
if (start >= 0 && codec.decode(output.sublist(start, start + 3)) == 'TAG') {
start += 3;
} else {
start = bytes.length;
final append = List.filled(128, 0x00);
output.addAll(append);
final idBytes = codec.encode('TAG', limitByteLength: 3);
output.replaceRange(start, start + 3, idBytes);
start += 3;
}
那么,接下来就是根据模型MetadataV1Body
来填充占位符的过程了。MetadataV1Body结构如下:
class MetadataV1Body extends EncodeMetadata {
const MetadataV1Body(
{this.title,
this.artist,
this.album,
this.year,
this.comment,
this.track,
this.genre});
// 歌曲标题
final String? title;
// 艺术家
final String? artist;
// 专辑名字
final String? album;
// 年份,4个字符,'2022‘
final String? year;
// 评论
final String? comment;
// 艺术类型
final int? genre;
// 歌曲通道
final int? track;
}
这里需要注意的是,由于ID3v1只支持ISO_8859_1
解码,因此你不能输入中文(或其他ISO_8859_1
不支持的字符集),否则在解码的时候会报错。
对于已经存在的字段,比如Title
已经存在了,如果你在MetadataV1Body
中又给title
传入了非空的值,那么将覆盖原文件中的Title
。其他字段同理,不再赘述。
还有一点,ID3Encoder
只是对字节组做了编码操作,并没有对源文件进行任何修改,因此,如果你需要修改源文件,只需要再将修改后的字节覆盖掉源文件中的字节就可以了。就像下面这样。
final file = File('assets/some_song.mp3');
file.writeAsBytes(updateBytes);
我从网上下载了一首歌,使用ID3Decoder
解码后可以看到只有ID3v2.3的一些简单信息。
flutter: [ ID3MetaInfo ]
flutter: - Range: {0, 45}
flutter: == Header ==
flutter: - FileID: ID3
flutter: - Version: V2.3.0
flutter: - Size: 35
flutter: == Frames ==
flutter: - Frame ID: TSSE[Software/Hardware and settings used for encoding]
flutter: - Frame Size: 15
flutter: - Frame Flags: a: 0, b: 0, c: 0, i: 0, j: 0, k: 0[%abc00000 %ijk00000]
flutter: - Content: {Information: Lavf57.71.100
flutter: == Padding ==
flutter: - Padding Size: 10
flutter: =========> All data received <=======
然后,我随意填一些数据进去,看我操作哒哒哒:
final encoder = ID3Encoder(bytes);
// ignore: prefer_const_constructors
bytes = encoder.encode(MetadataV1Body(
title: 'Ting wo shuo,xiexie ni',
artist: 'Wu ming',
album: 'Gan en you ni',
year: '2021',
comment: 'I am very happy!',
track: 1,
genre: 2
));
然后再次读取下编辑后的bytes试试,可以看到ID3MetaInfo
更新了,存在了两个ID3标签信息。并且,由于我穿入了TRACK信息,因此这个标签还被解析为了ID3v1.1版本。这就表示,我们的ID3v1编码部分已经大功告成。
flutter: [ ID3MetaInfo ]
flutter: - Range: {10885791, 10885919}
flutter: == Version ==
flutter: - Version: V1.1
flutter: == Title ==
flutter: - Title: Ting wo shuo,xiexie ni
flutter: == Artist ==
flutter: - Artist: Wu ming
flutter: == Album ==
flutter: - Album: Gan en you ni
flutter: == Year ==
flutter: - Year: 2021
flutter: == Comment ==
flutter: - Comment: I am very happy!
flutter: == Reserve ==
flutter: - Reserve: 1
flutter: == Track ==
flutter: - Track: 1
flutter: == Genre ==
flutter: - Genre: Country
flutter: =========> All data received <=======
flutter: [ ID3MetaInfo ]
flutter: - Range: {0, 45}
flutter: == Header ==
flutter: - FileID: ID3
flutter: - Version: V2.3.0
flutter: - Size: 35
flutter: == Frames ==
flutter: - Frame ID: TSSE[Software/Hardware and settings used for encoding]
flutter: - Frame Size: 15
flutter: - Frame Flags: a: 0, b: 0, c: 0, i: 0, j: 0, k: 0[%abc00000 %ijk00000]
flutter: - Content: {Information: Lavf57.71.100
flutter: == Padding ==
flutter: - Padding Size: 10
flutter: =========> All data received <=======
接下来,难度升级,我们要开始编码ID3v2部分了。
编码ID3v2
老实说,当时写ID3v1编码部分只花了一天时间。但是到写ID3v2.3花了我一周时间,顺便还改了不少BUG😊。
因为ID3v2.2目前支持较少,所以我也不准备支持其编码,有需求再说嘛。因此我们直接从v2.3开始实现。
ID3v2.3
ID3v2.3是比较流行的一个版本。它存在于文件的头部,以3个字节的“ID3”作为起始标识。如果没有找到ID3标识,那么这个文件只会有三种可能,ID3v1编码,ID3v2.4编码且位于文件尾部,没有ID3信息。
针对没有找到ID3标识的情况,我们可以认为其不存在ID3v2.3标签。因此,我们需要根据给定信息创建一个新的ID3v2.3标签出来。
我们先定一个MetadataV2_3Body
类,用于封装我们需要修改或添加的数据。title
表示歌曲标题,artist
表示歌曲的艺术家,album
表示歌曲的专辑信息,imageBytes
表示歌曲的封面图,encoding
表示歌曲的编码方式,userDefines
表示用户自定义的数据,以键值对的形式传入。
class MetadataV2_3Body extends MetadataV2Body {
const MetadataV2_3Body({
this.title,
this.artist,
this.album,
this.encoding,
this.imageBytes,
this.userDefines,
});
/// TIT2[Title/songname/content description]
final String? title;
/// TPE1[Lead performer(s)/Soloist(s)]
final String? artist;
/// TALB[Album/Movie/Show title]
final String? album;
/// APIC[Attached picture]
/// Support PNG, JPEG, TIFF, GIF, BMP
final List<int>? imageBytes;
/// TSSE[Software/Hardware and settings used for encoding]
final String? encoding;
/// TXXX[User defined text information frame]
final Map<String, String>? userDefines;
}
当你输入某个属性时,则表示你想添加某个数据帧或修改某个数据帧。属性对应的数据帧的ID已在其注释中标注。当属性传入null
时,表示不修改这个属性对应的frame
,而不是删除。
创建ID3体
关于ID3v2.3的数据结构我在文章已经做过介绍,因此这里简单过一遍。它的头部结构如下所示,占10个字节。
字段 | 长度/字节 | 值 | 说明 |
---|---|---|---|
File ID | 3 | “ID3” | 固定为“ID3” |
version | 2 | $03 00 | 版本 |
flags | 1 | %abc00000 | 额外信息 |
size | 4 | 4 * %0xxxxxxx | 除Header之外的剩余内容大小 |
其中,size字段表示除了头部之外的所有数据的大小。也就是总大小-头部(10字节)= size
。但是size
和数据帧大小有关,而数据帧我们还没有进行编码,无法计算其总大小,因此这里先把size
放一放,先去把数据帧的编码工作完成。
数据帧架的头部结构在v2.3中如下所示,由三部分组成,FrameID
,FrameSize
,Flags
。总共占10个字节。其中frameSize是指一个数据帧的内容部分的大小,不包括头部的10个字节。
字段 | 大小/字节 | 值 | 描述 |
---|---|---|---|
Frame ID | 4 | $xx xx xx xx (four characters) | 帧的标识符,4个字节 |
Frame size | 4 | $xx xx xx xx | 帧大小,除去frame header这10个字节后的大小。因此实际一个数据帧大小为 10+size |
Flags | 2 | $xx xx | 旗帜信息 |
字段 | 大小/字节 | 描述 |
---|---|---|
Frame Header | 10 | 数据帧头:frame ID,frameSize,flags |
Content | 不定 | 数据帧的内容 |
紧跟在数据帧头部后面的就是内容Content,它的长度保存在frameSize中,结构各不相同,具体可以参考v2.3.0文档。
知道了数据帧的数据格式,我们就可以根据给定内容来创建一个新的数据帧出来。我们拿歌曲标题为例子,歌曲标题在ID3v2.3的数据帧架集中对应的ID为TIT2
,描述为Title/songname/content description
。这个ID属于T开头,但是又不是TXXX,因此归类于Text information frame
。
<Header for 'Text information frame', ID: "T000" - "TZZZ",
excluding "TXXX" described in 4.2.2.>
Text encoding $xx
Information <text string according to encoding>
可以看到,Text information frame
由text encoding
和Information
组成,text encoding占1个字节,表示Inforamtion
的编码方式,在ID3v2.3中,encoding只会是0x00或0x01。
- 0x00: ISO-8859-1,在dart中就是
latin1
。它无法编码中文。 - 0x01: unicode,在这里用的是UTF-16编码,注意大小端问题。
由于歌曲标题可能为中文,因此我们需要用到UTF-16进行编码,在dart中,没有直接将字符串用UTF-16编码后转字节序的API。因此,我们需要自己做个转换。
首先要明确,UTF-16编码区分大小端,有BE和LE之分。
- BE 即 big-endian,大端的意思。大端就是将高位的字节放在低地址表示
- LE 即 little-endian,小端的意思。小端就是将高位的字节放在高地址表示
以0xFE,0xFF
开头的就是BE形式的UTF-16,以0xFF,0xFE
开头的是LE形式的UTF-16。
并且,UTF-16是2个字节2个字节读取进行编解码的。因此对于UTF-16的编码有下面的转换。
List<int> _encodeWithUTF16(String string, {String bom = 'BE'}) {
if (bom == 'BE') {
return _encodeWithUTF16BE(string);
} else {
return _encodeWithUTF16LE(string);
}
}
List<int> _encodeWithUTF16BE(String string) {
List<int> output = [0xFE, 0xFF];
final utf16Bytes = string.codeUnits;
for (int i = 0; i < utf16Bytes.length; i++) {
final utf16Byte = utf16Bytes[i];
output.add((utf16Byte & 0xFF00) >>> 8);
output.add((utf16Byte & 0xFF));
}
return output;
}
List<int> _encodeWithUTF16LE(String string) {
List<int> output = [0xFF, 0xFE];
final utf16Bytes = string.codeUnits;
for (int i = 0; i < utf16Bytes.length; i++) {
final utf16Byte = utf16Bytes[i];
output.add((utf16Byte & 0xFF));
output.add((utf16Byte & 0xFF00) >>> 8);
}
return output;
}
知道如何编码成UTF-16之后,我们就可以进行Content的整体编码工作了。
字段 | 大小/字节 | 内容 |
---|---|---|
text encoding | 1 | 0x01 |
information | 编码后的字节长度 | UTF-16对歌曲标题进行编码后的字节数组 |
具体实现如下,output
就是整个content表示的字节数组。
class _TextInfomationEncoder extends _ContentEncoder {
_TextInfomationEncoder(super.frameID);
@override
List<int> encode(content) {
List<int> output = [];
// set text encoding 'UTF16'
const defaultTextEncoding = 0x01;
final codec = ByteCodec(textEncodingByte: defaultTextEncoding);
// text encoding
output.add(defaultTextEncoding);
// information
final infoBytes = codec.encode(content);
output.addAll(infoBytes);
return output;
}
}
关于数据帧的编码我这里再将一个APIC
,也就是代表封面图的数据帧。APIC的数据帧结构略微复杂点,它需要我们传入图片的MIME type,图片类型,描述(这个一般都不传),图片的字节组。这里就涉及到MIME Type
的获取。
<Header for 'Attached picture', ID: "APIC">
Text encoding $xx
MIME type <text string> $00
Picture type $xx
Description <text string according to encoding> $00 (00)
Picture data <binary data>
图片编码方式获取
我们说桌面上有一张图片,它的类型不是文件名后缀是什么,它就是什么类型,而是要解析文件头来确定的。一般来说,我们只要获取到文件头的前两个字节就可以确认这张图片是什么类型。当然,这种方式只对图片文件有效。具体来说,看下面的映射关系:
- JPEG: 0xFF 0xD8
- PNG: 0x89 0x50
- GIF: 0x47 0x49
- BMP: 0x42 0x4D
- TIFF: 0x4D 0x4D或0x49 0x49
上面我只列出了常见的图片编码判断。当我们知道图片的编码方式后,也就知道了其MIME type。如一张png图片,它的MIME type是image/png
。
一旦获取到MIME type,接下来的事情就好办了。由于涉及图片的编码没有中文,因此,我们可以将text encoding设置为ISO-8859-1
,对应的标识为0x00
。在编码MIME type的时候,要注意结尾跟上一个0x00
,表示结束位置。而Picture type
对应的映射表在这里可查,默认传0x00即可,表示其他(Other)。那么对于APIC的编码就完成了,完整代码如下所示:
class _APICEncoder extends _ContentEncoder {
_APICEncoder(super.frameID);
@override
List<int> encode(content) {
List<int> output = [];
// set text encoding 'ISO_8859_1'
const defaultTextEncoding = 0x00;
final codec = ByteCodec(textEncodingByte: defaultTextEncoding);
// text encoding
output.add(defaultTextEncoding);
// MIME type
String mimeType = ImageCodec.getImageMimeType(content);
final mimeBytes =
codec.encode(mimeType, forceType: ByteCodecType.ISO_8859_1);
output.addAll(mimeBytes);
output.add(0x00);
// Picture type, default set to 'Other'
final pictypeBytes = 0x00;
output.add(pictypeBytes);
// Description
if (defaultTextEncoding == 0x01 || defaultTextEncoding == 0x02) {
output.addAll([0x00, 0x00]);
} else {
output.addAll([0x00]);
}
// Picture data
output.addAll(content);
return output;
}
}
计算总数据帧大小
我们根据传入的歌曲信息,进行数据帧的编码,最终得到一组数据帧字节framesBytes
。
final contentEncoder = ContentEncoder(body: MetadataV2_3Wrapper(data));
final framesBytes = contentEncoder.encode();
一般,为了方便编辑ID3的数据,而不影响音频数据,导致其平凡的被编排。我们往往会在结尾的位置插入Padding
。Padding
由一组0x00
组成,长度随意,我默认给它100的长度。而Padding+framesBytes
就是Header
中size的大小。我们在计算完大小后,需要重新修改Header
中记录size的值。
final size = framesBytes.length + padding.length;
List<int> sizeBytes = ByteUtil.toH0Bytes(size);
header.replaceRange(_calSizeStart, _calSizeStart + 4, sizeBytes);
到这里,新建一个ID3v2.3的所有工作就完成了。总结下,就是先编码ID3的Header部分,记录下size字段的起始位置,方便后续更改。然后编码数据帧部分,计算整个数据帧+padding的大小,将结果记录到Header的size字段中。
List<int> _createNewID3Body(MetadataV2_3Body data) {
int start = 0;
final codec = ByteCodec();
List<int> insetBytes = [];
// Create Header
final header = List.filled(10, 0x00);
final id3TagBytes = codec.encode('ID3', limitByteLength: 3);
header.replaceRange(start, start + 3, id3TagBytes);
start += 3;
// version + revision
header.replaceRange(start, start + 1, [3]);
start += 2;
// Flags
final flagsByte = 0x00;
header.replaceRange(start, start + 1, [flagsByte]);
start += 1;
// Size
// The ID3v2 tag size is the size of the complete tag after
// unsychronisation, including padding, excluding the header but not
// excluding the extended header (total tag size - 10)
//
// `size = extended header + frames + padding`
_calSizeStart = start;
start += 4;
// No Extended Header
// Frames
final contentEncoder = ContentEncoder(body: MetadataV2_3Wrapper(data));
final framesBytes = contentEncoder.encode();
// Padding
final padding = _defaultPadding;
// Calculate the size of `size` and store it in 4 bytes
final size = framesBytes.length + padding.length;
List<int> sizeBytes = ByteUtil.toH0Bytes(size);
header.replaceRange(_calSizeStart, _calSizeStart + 4, sizeBytes);
// package all bytes
insetBytes.addAll(header);
insetBytes.addAll(framesBytes);
insetBytes.addAll(padding);
return insetBytes;
}
修改ID3体
创建相对容易,编辑则比较复杂。因为这部分涉及到字节的重新排序。我们回到最初的逻辑分支中,扫描文件头时,发现了“ID3”标识符。也就是存在ID3v2版本,有可能是2.2,2.3或2.4。
List<int> encode(MetadataV2_3Body data) {
int start = 0;
final idBytes = _output.sublist(start, start + 3);
// 存在ID3v2
if (latin1.decode(idBytes) == 'ID3') {
start += 3;
_decodeDeep(start, data);
} else {
final insetBytes = _createNewID3Body(data);
_output.insertAll(0, insetBytes);
}
return _output;
}
在编码前,我们先思考下,不是v2.3的情况下怎么处理?
答案是抹去除Header之外的所有数据。因为v2.2在数据帧上和v2.3有很大的不同,单是frameID
在v2.2上是3个字节,到v2.3就有4个字节。而v2.4虽然数据帧的结构和v2.3基本一致,但是在种类上有差别。并且v2.3和v2.4的Extended Header
的结构完全不同,因此也不能简单的进行编辑。为了方便处理,一旦识别ID3的版本不是v2.3时,直接将后面的数据抹除。抹除之后就和创建一个新的ID3v2.3在处理数据帧上基本一致。
void _decodeDeep(int start, MetadataV2_3Body data) {
// version
final major = _output.sublist(start, start + 1).first;
start += 2;
// flags - %abc00000
final flags = _output.sublist(start, start + 1).first;
start += 1;
// We only need to pay attention to whether there is an Extended Header
final hasExtendedHeader = (flags & 0x40) != 0;
// size - 4 * %0xxxxxxx
List<int> sizeBytes = _output.sublist(start, start + 4);
_calSizeStart = start;
start += 4;
// The ID3v2 tag size is the size of the complete tag after
// unsychronisation, including padding, excluding the header but not
// excluding the extended header (total tag size - 10).
final size = ByteUtil.calH0Size(sizeBytes);
_size = size;
// If it is not v2.3 version, tag data will be erased
if (major != 3) {
// 编码全新的数据帧
final contentEncoder = ContentEncoder(body: MetadataV2_3Wrapper(data));
final framesBytes = contentEncoder.encode();
// 这里当新编码的数据帧小于原始数据帧大小时,将多余的部分(filledLength)全部替换为0x00
if (framesBytes.length <= size) {
final filledLength = size - framesBytes.length;
final insertBytes = framesBytes + List.filled(filledLength, 0x00);
_output.replaceRange(start, start + insertBytes.length, insertBytes);
} else {
// 新编码的数据比原始数据大,需要插入更多的占位0x00,长度为moveLength
final insertBytes = framesBytes + _defaultPadding;
final moveLength = insertBytes.length - size;
_output.insertAll(start + size, List.filled(moveLength, 0x00));
_output.replaceRange(start, start + insertBytes.length, insertBytes);
// new size
_size = insertBytes.length;
// 更新数据帧size大小
List<int> sizeBytes = ByteUtil.toH0Bytes(_size);
_output.replaceRange(_calSizeStart, _calSizeStart + 4, sizeBytes);
}
}
// ...
当ID3的版本就是v2.3时,这里就不能简单的抹除frames了。需要逐一解析,碰到FrameID
一致的,则进行编辑,并修改字节的编码书序。
首先,我们要明确,在Header中计算出来的size=frameSize+extendedSize+padding
。因此,我们在逐一解码并编辑的时候,需要减掉extendedSize
,才是frames的总大小。
没个frame都有一个2字节大小的flags
,它的结构为%abc00000 %ijk00000
。共有6个值。但是我们在做编辑的时候,只要关注其中两个值c
,i
即可。
- c: Read Only
- i: Compression
c
代表这个frame
是否只是只读的,如果存在c
不为0,那么,这个frame
不能被编辑。i
代表是否压缩,这里用的压缩算法时zlib
,在dart中可以直接调用,对象名就叫zlib
。
编辑数据帧的核心逻辑就是,先匹配FrameID,编码内容,比较重新编码后的数据帧Content和原始Content大小,在整个字节组上进行字节重排。然后更新frame的size大小,更新frame的content值。
// 内容编辑器
class ContentEditor {
const ContentEditor({required this.bytes});
// ...
}
// 编辑某一属性
EditorResult _editFrameWithProperty(int start, String frameID, int frameSize,
bool compression, MetadataProperty property) {
// 内容编码器,将歌曲信息编码成字节组
final contentEncoder = ContentEncoder();
List<int> contentBytes =
contentEncoder.encodeProperty(frameID: frameID, property: property);
// 编码不成功,有可能是没有对应的内容编码器导致的
if (contentBytes.isEmpty) {
return EditorResult.noEdit(frameID: frameID, start: start, frameSize: frameSize);
}
// 如果需要压缩,调用zlib
if (compression) {
// store 'decompressed size' in 4 bytes
final contentSize = contentBytes.length;
List<int> decompressedSizeBytes = ByteUtil.toH1Bytes(contentSize);
contentBytes = decompressedSizeBytes + zlib.encode(contentBytes);
}
final int contentLength = contentBytes.length;
// 重新编排字节序,保证后续的解码能正确进行
// adjust the size of frame content
if (contentLength <= frameSize) {
// shrink size
int shrinkStart = start + contentLength;
final shrinkSize = frameSize - contentLength;
bytes.removeRange(shrinkStart, shrinkStart + shrinkSize);
} else {
// expansion
final expansionSize = contentLength - frameSize;
int expansionStart = start + frameSize;
bytes.insertAll(expansionStart, List.filled(expansionSize, 0x00));
}
bytes.replaceRange(start, start + contentLength, contentBytes);
// 重新计算frame的大小,并更新到frame header中
// recalculate content size
frameSize = contentLength;
List<int> sizeBytes = ByteUtil.toH1Bytes(frameSize);
// 6 = size(4 bytes) + flags(2 bytes)
int frameSizeStart = start - 6;
bytes.replaceRange(frameSizeStart, frameSizeStart + 4, sizeBytes);
start += frameSize;
// 返回编码后的数据帧内容
return EditorResult(
frameID: frameID, start: start, modify: true, frameSize: frameSize);
}
上面的代码详细描述了如何根据传入的属性去编辑某一数据帧的逻辑,但是,有个问题,我们便利原始数据帧,一旦在MetadataV2_3Body
中有新的属性,那么必然会出现新增属性无法编码到ID3中的情况,毕竟都没匹配到。所以,我们在后续还要收集未编码的属性,重新在尾部添加进去。
// 存在未编码的属性
if (wrapperData.hasUnAttachedProperty()) {
final contentEncoder = ContentEncoder(body: data);
final attachedBytes = contentEncoder.encode();
if (attachedBytes.isNotEmpty) {
// 剩余的大小不够编码后的数据,因此需要扩容
if (remainingSize < attachedBytes.length) {
final expansionSize = attachedBytes.length - remainingSize;
_output.insertAll(start + remainingSize, List.filled(expansionSize, 0x00));
// calculate new `_size` to 4 bytes
// 重新计算总数据帧大小
_size += expansionSize + _defaultPadding.length;
List<int> sizeBytes = ByteUtil.toH0Bytes(_size);
// update size
_output.replaceRange(_calSizeStart, _calSizeStart + 4, sizeBytes);
}
// 将新的数据帧插入到ID3的原始frames后面
// insert new frames
_output.replaceRange(start, start + attachedBytes.length, attachedBytes);
}
}
到这里,我们就完成了编辑数据帧的工作,它的所有代码都在id3_encoder_impl.dart文件中,感兴趣的可以去仓库看完整代码。有不清楚的或错误的地方也希望大家能在评论区中指出。
验证
我们准备一首歌(最好是带有ID3v2信息的),Mac电脑自带ID3信息浏览功能,选中音乐,点击空格即可。看我下图中所示,显示出这首歌歌名为“老公赚钱老婆花”,艺人为“大庆小芳”,专辑为“老公赚钱老婆花”,还有一个花里花俏的封面图。当然你也可以选择右键选择显示简介,能够看到更相信的内容。
我们先用我们的程序id3_decoder
解析一下这个mp3文件,可以看到如下信息,嗯,是ID3v2.3,且歌曲标题,专辑,艺人等都能对上。
flutter: [ ID3MetaInfo ]
flutter: - Range: {0, 59663}
flutter: == Header ==
flutter: - FileID: ID3
flutter: - Version: v2.3.0
flutter: - Size: 59653
flutter: == Frames ==
flutter: - Frame ID: TCON[Content type]
flutter: - Frame Size: 4
flutter: - Frame Flags: a: 0, b: 0, c: 0, i: 0, j: 0, k: 0[%abc00000 %ijk00000]
flutter: - Content: {Information: Pop}
flutter: - Frame ID: TSSE[Software[/Hardware]() and settings used for encoding]
flutter: - Frame Size: 13
flutter: - Frame Flags: a: 0, b: 0, c: 0, i: 0, j: 0, k: 0[%abc00000 %ijk00000]
flutter: - Content: {Information: Lavf56.4.101}
flutter: - Frame ID: APIC[Attached picture]
flutter: - Frame Size: 58459
flutter: - Frame Flags: a: 0, b: 0, c: 0, i: 0, j: 0, k: 0[%abc00000 %ijk00000]
flutter: - Content: {MIME: image[/jpg](), PictureType: Other, Description: , Base64: <Has Picture Data>}
flutter: - Frame ID: TALB[Album[/Movie/Show]() title]
flutter: - Frame Size: 17
flutter: - Frame Flags: a: 0, b: 0, c: 0, i: 0, j: 0, k: 0[%abc00000 %ijk00000]
flutter: - Content: {Information: 老公赚钱老婆花}
flutter: - Frame ID: TIT2[Title[/songname/content]() description]
flutter: - Frame Size: 17
flutter: - Frame Flags: a: 0, b: 0, c: 0, i: 0, j: 0, k: 0[%abc00000 %ijk00000]
flutter: - Content: {Information: 老公赚钱老婆花}
flutter: - Frame ID: COMM[Comments]
flutter: - Frame Size: 539
flutter: - Frame Flags: a: 0, b: 0, c: 0, i: 0, j: 0, k: 0[%abc00000 %ijk00000]
flutter: - Content: {Language: XXX, Short content descrip: , The actual text: 163 key(Don't modify):L64FU3W4YxX3ZFTmbZ+8[/ZMEKmM7caMfLyAwYrpPJYhFT6AEWOrgZxyH/7s5ZDPF4quVX/AFaA4gymaih8RjvaTtK45CfOanxoXqWzvlzPL7VwLQSGc5BunknudHXiQ/xI1i0rvWNr4kNGVQawO2kh8Hr8hM4oMW5]()+lkYeM0Zy9ry[/zpZPxEDkm1PyBM0dqXGionXYMSuzqlEvtUoslNFQRSaOKrjt83c40nL]()+9O42jEKmuzLSJtlS4vxPco8YTj4IkOFE1bwLOl2MeIKuy0D+kSwx3gPwFACwMHRWg6Q84LpA6BDVCoWxm+gypTzNlcSNYJ0XPadMRIn9n8LO2GNXEwJdTYHF1[/cw1smooDToqvvLeQmTQ999SToH]()+ulR801BAKwhgqD5B9xqWgD9pIOMKIn1QzHyuN31s[/iakPGq1RcwcUUSeR2qxzOGUCmvqvtGSfhKKpkFwq2VynrZlwMv]()+6hof3IyWpd+Ie1W14rgfT[/WULNvuYr0OnHu4SkisU]()}
flutter: - Frame ID: TPE1[Lead performer(s)[/Soloist]()(s)]
flutter: - Frame Size: 11
flutter: - Frame Flags: a: 0, b: 0, c: 0, i: 0, j: 0, k: 0[%abc00000 %ijk00000]
flutter: - Content: {Information: 大庆小芳}
flutter: - Frame ID: TRCK[Track number[/Position]() in set]
flutter: - Frame Size: 5
flutter: - Frame Flags: a: 0, b: 0, c: 0, i: 0, j: 0, k: 0[%abc00000 %ijk00000]
flutter: - Content: {Information: 1}
flutter: == Padding ==
flutter: - Padding Size: 508
flutter: =========> All data received <=======
接下来,就是见证奇迹的时刻。
我们要对已知的ID3进行编辑,我们初始化ID3Encoder
,并将ID3编辑成v2.3。
final header = await rootBundle.load("assets/wx_header.png");
final headerBytes = header.buffer.asUint8List();
final encoder = ID3Encoder(bytes);
// ignore: prefer_const_constructors
final resultBytes = encoder.encodeSync(MetadataV2_3Body(
title: '听我说谢谢你!',
imageBytes: headerBytes,
artist: '歌手ijinfeng',
userDefines: {
"时长": '2:48',
"userId": "ijinfeng"
},
album: 'ijinfeng出产的专辑',
));
在上面,我们修改了歌曲的标题,艺人,专辑以及歌曲的封面图,并且,我们还插入了两组自定义数据,分别是时长和userId。
我们将返回的resultBytes
传入到ID3Decoder
中再次进行解码,检查下对应的frame是否有变,并且不影响其他数据帧的解析。
flutter: [ ID3MetaInfo ]
flutter: - Range: {0, 186331}
flutter: == Header ==
flutter: - FileID: ID3
flutter: - Version: v2.3.0
flutter: - Size: 186321
flutter: == Frames ==
flutter: - Frame ID: TCON[Content type]
flutter: - Frame Size: 4
flutter: - Frame Flags: a: 0, b: 0, c: 0, i: 0, j: 0, k: 0[%abc00000 %ijk00000]
flutter: - Content: {Information: Pop}
flutter: - Frame ID: TSSE[Software[/Hardware]() and settings used for encoding]
flutter: - Frame Size: 13
flutter: - Frame Flags: a: 0, b: 0, c: 0, i: 0, j: 0, k: 0[%abc00000 %ijk00000]
flutter: - Content: {Information: Lavf56.4.101}
flutter: - Frame ID: APIC[Attached picture]
flutter: - Frame Size: 185103
flutter: - Frame Flags: a: 0, b: 0, c: 0, i: 0, j: 0, k: 0[%abc00000 %ijk00000]
flutter: - Content: {MIME: image[/png](), PictureType: Other, Description: , Base64: <Has Picture Data>}
flutter: - Frame ID: TALB[Album[/Movie/Show]() title]
flutter: - Frame Size: 29
flutter: - Frame Flags: a: 0, b: 0, c: 0, i: 0, j: 0, k: 0[%abc00000 %ijk00000]
flutter: - Content: {Information: ijinfeng出产的专辑}
flutter: - Frame ID: TIT2[Title[/songname/content]() description]
flutter: - Frame Size: 17
flutter: - Frame Flags: a: 0, b: 0, c: 0, i: 0, j: 0, k: 0[%abc00000 %ijk00000]
flutter: - Content: {Information: 听我说谢谢你!}
flutter: - Frame ID: COMM[Comments]
flutter: - Frame Size: 539
flutter: - Frame Flags: a: 0, b: 0, c: 0, i: 0, j: 0, k: 0[%abc00000 %ijk00000]
flutter: - Content: {Language: XXX, Short content descrip: , The actual text: 163 key(Don't modify):L64FU3W4YxX3ZFTmbZ+8[/ZMEKmM7caMfLyAwYrpPJYhFT6AEWOrgZxyH/7s5ZDPF4quVX/AFaA4gymaih8RjvaTtK45CfOanxoXqWzvlzPL7VwLQSGc5BunknudHXiQ/xI1i0rvWNr4kNGVQawO2kh8Hr8hM4oMW5]()+lkYeM0Zy9ry[/zpZPxEDkm1PyBM0dqXGionXYMSuzqlEvtUoslNFQRSaOKrjt83c40nL]()+9O42jEKmuzLSJtlS4vxPco8YTj4IkOFE1bwLOl2MeIKuy0D+kSwx3gPwFACwMHRWg6Q84LpA6BDVCoWxm+gypTzNlcSNYJ0XPadMRIn9n8LO2GNXEwJdTYHF1[/cw1smooDToqvvLeQmTQ999SToH]()+ulR801BAKwhgqD5B9xqWgD9pIOMKIn1QzHyuN31s[/iakPGq1RcwcUUSeR2qxzOGUCmvqvtGSfhKKpkFwq2VynrZlwMv]()+6hof3IyWpd+Ie1W14rgfT[/WULNvuYr0OnHu4SkisU]()}
flutter: - Frame ID: TPE1[Lead performer(s)[/Soloist]()(s)]
flutter: - Frame Size: 23
flutter: - Frame Flags: a: 0, b: 0, c: 0, i: 0, j: 0, k: 0[%abc00000 %ijk00000]
flutter: - Content: {Information: 歌手ijinfeng}
flutter: - Frame ID: TRCK[Track number[/Position]() in set]
flutter: - Frame Size: 5
flutter: - Frame Flags: a: 0, b: 0, c: 0, i: 0, j: 0, k: 0[%abc00000 %ijk00000]
flutter: - Content: {Information: 1}
flutter: - Frame ID: TXXX[User defined text information frame]
flutter: - Frame Size: 19
flutter: - Frame Flags: a: 0, b: 0, c: 0, i: 0, j: 0, k: 0[%abc00000 %ijk00000]
flutter: - Content: {Description: 时长, Value: 2:48}
flutter: - Frame ID: TXXX[User defined text information frame]
flutter: - Frame Size: 35
flutter: - Frame Flags: a: 0, b: 0, c: 0, i: 0, j: 0, k: 0[%abc00000 %ijk00000]
flutter: - Content: {Description: userId, Value: ijinfeng}
flutter: == Padding ==
flutter: - Padding Size: 434
flutter: =========> All data received <=======
可以清晰的看到,代表歌曲标题的TIT2等都已经修改成了最新的值。说明我们的编辑是成功的。我们做次最后的确认,将编辑后的字节写入到文件,生成一首新的mp3文件,然后使用mac自带的预览功能查看下是否真的修改成功。
var file = File('${downloadDir?.path}/rewrite_song.mp3');
final exist = await file.exists();
if (!exist) {
await file.create(recursive: true);
}
await file.writeAsBytes(resultBytes);
打开预览,熟悉的音乐响起,老公赚钱老婆花~,🎉🎉🎉修改成功了。
总结
本文详细讲解了有关ID3的编码过程,内容涉及字节操作运算,ID3v1和ID3v2的数据格式,字符编码方式,UTF-16编解码,图片编码方式获取。本文只介绍了ID3v2中的v2.3版本的编码,还有v2.2(不常用),v2.4两个版本没有做讲解。由于篇幅有限,并且另外两个版本的编码编码过程和v2.3大同小异。因此,有关内容可以阅读本文涉及的id3_codec源码,请关注本仓库,我将持续更新,修复BUG。另外,附上最新支持ID3v2.3编码pub包引入方式:id3_codec: ^0.0.6
。谢谢大家观看。
暂无评论内容