Java 修改 mp3 的标签(ID3V1 和 ID3V2)

编程入门 行业动态 更新时间:2024-10-10 04:23:02

Java 修改 mp3 的<a href=https://www.elefans.com/category/jswz/34/1770160.html style=标签(ID3V1 和 ID3V2)"/>

Java 修改 mp3 的标签(ID3V1 和 ID3V2)

不知道大家平时用手机听歌时都用什么 App,我一直挺喜欢 iPhone 自带的 Apple Music,但是有时候我会发现导入的 mp3 歌手会显示未知歌手,

同样的,在 PC 上使用 Windows 自带的 Groove 播放时也会发现有的歌曲展示不一样。大部分从网易云、酷我音乐这些音乐软件中下载下来的 mp3,歌手名、专辑、专辑图都一应俱全:

而一些从网上或者网盘下载的 mp3,则有可能是光秃秃的一片:

如果我们在 Windows 的文件查看布局中选择详情信息时,会发现这两个文件是有区别的:

如果查看文件属性的话,在详细信息标签页,这两个文件也是有区别的:

所以播放器展示的时候,其实是取的文件详细信息中的值,那这个值是怎么来的呢?这就要说到 mp3 的标签了。

一、mp3 标签 ID3

首先 mp3 是一种数据压缩格式,但是它压缩的只是音频!所以它的文件中只有音频数据,为了保存更多信息如歌曲名、歌手名、专辑等(这些信息对于更好的体验,相信还是很有必要的),于是就产生了 mp3 的标签信息 ID3,根据不同版本又分为 ID3V1 和 ID3V2,其中 ID3V2 还细分为 4 个版本,目前主要流行的是 ID3V2 的第三个版本,即 ID3V2.3 版本。那 ID3V1 和 ID3V2 又有什么区别呢?莫慌,让我们一个一个来说。

二、ID3V1

上面说到 mp3 只是保存的音频数据信息,但为了更好的体验,我们在播放器中通常需要展示歌曲名、歌手名、专辑等,于是 1996 年,一个叫 Eric Kemp 的人发明了 mp3 标签格式 ID3,也就是 ID3V1。

ID3V1 是一组附加在音乐文件后面的数据,它的长度是固定的128 字节,这短短的 128 字节,按照固定的格式,包含了我们需要的一些信息,格式定义如下:

  • Identifier:开头是固定长度 3 个字节的标识,固定内容 TAG,如果没有这个标识则认为没有 ID3V1 标签。
  • Title:标题,30 字节,不足 30 字节时用 \0 补足。
  • Artist:歌手,30 字节,不足 30 字节时用 \0 补足。
  • Album:专辑,30 字节,不足 30 字节时用 \0 补足。
  • Year:年份,4 字节。
  • Comment:它有些特殊,分为 28+1+1,有时候会占 28 字节,有时候会占 30 字节,这是因为在 ID3V1.1 时 Comment 切割出来了最后两个字节,用于存放曲目序号,倒数第 2 个字节为 Reversed,如果它为 0,则表示有曲目序号,倒数第 1 个字节为曲目序号,此时 Comment 就占 28 个字节。如果它不为 0,则应该是 Comment 中的内容,就没有曲目序号,此时 Comment 占 30 个字节。
  • Genre:音乐风格,1 字节,这个风格会有一个对照表,大家可以百度一下。

了解 ID3V1 的结构后,我们知道它里面的每个部分是顺序存放的,每个部分也是有固定长度的,当这个部分不足它所占字节时,就会用 \0 补足。ID3V1 的优点时它占用空间小,而且在文件尾部,并不会影响音乐的播放。但是缺点也显而易见,那就是扩展很难,ID3V1_1 扩展了一个字节来存放音轨都这么麻烦,而且固定的 128 长度,意味着没办法存太多附加信息。

再以刚才的 mp3 为例,我们查看一下它的二进制信息:

结果发现它的尾部确实没有 TAG 开头的 ID3V1 标签信息,所以在播放器中是没有歌手、专辑之类的信息的。有了上面这些对 ID3V1 的基础认知后,我们就可以通过代码来操作 ID3V1 了,这里我以 Java 为例来为这个 mp3 文件添加 ID3V1 标签:

import java.io.*;
import java.nio.charset.Charset;/*** Author: MrQinshou* Email: cqflqinhao@126* Date: 2023/3/10 11:30* Description: 类描述*/
public class Mp3Util {private static final Charset sCharset = Charset.forName("GBK");private static final int ID3V1_TAG_LENGTH = 128;private static final String ID3V1_TAG_START = "TAG";public static void main(String[] args) throws IOException {File src = new File("C:\\Users\\admin\\Desktop\\新建文件夹\\孤勇者bkp.mp3");setID3V1(src, "孤勇者", "陈奕迅", "孤勇者", null, null, null, null);}private static byte[] int2Bytes(int i) {byte[] byteArray = new byte[4];byteArray[0] = (byte) (i & 0xFF);byteArray[1] = (byte) ((i & 0xFF00) >> 8);byteArray[2] = (byte) ((i & 0xFF0000) >> 16);byteArray[3] = (byte) ((i & 0xFF000000) >> 24);return byteArray;}private static int bytes2Int(byte[] bytes) {if (bytes == null || bytes.length < 4) {return 0;}return (0xFF & bytes[0]) | (0xFF00 & (bytes[1] << 8)) | (0xFF0000 & (bytes[2] << 16)) | (0xFF000000 & (bytes[3] << 24));}public static void setID3V1(File src, String title, String artist, String album, Integer year, String comment, Byte track, Byte genre) throws IOException {RandomAccessFile randomAccessFile = new RandomAccessFile(src, "rw");randomAccessFile.seek(randomAccessFile.length() - ID3V1_TAG_LENGTH);byte[] bytes = new byte[3];randomAccessFile.read(bytes);String tag = new String(bytes);if (ID3V1_TAG_START.equals(tag)) {// 以前有 ID3V1,则先获取之前的信息,再修改需要修改的// Title 占 30 字节bytes = new byte[30];randomAccessFile.read(bytes);if (title == null) {title = new String(bytes, sCharset);}// Artist 占 30 字节randomAccessFile.read(bytes);if (artist == null) {artist = new String(bytes, sCharset);}// Album 占 30 字节randomAccessFile.read(bytes);if (album == null) {album = new String(bytes, sCharset);}// Year 占 4 字节bytes = new byte[4];randomAccessFile.read(bytes);if (year == null) {year = bytes2Int(bytes);}// Comment 占 28 字节,没有曲目序号时占 30 字节bytes = new byte[30];randomAccessFile.read(bytes);// Reserved 占 1 字节,为 0 表示有曲目序号,下一字节为曲目序号if (bytes[28] == 0) {if (comment == null) {comment = new String(bytes, 0, 28, sCharset);}if (track == null) {track = bytes[29];}} else {if (comment == null) {comment = new String(bytes, sCharset);}}// Genre 占 1 字节,歌曲风格,-1 表示没有风格bytes = new byte[1];randomAccessFile.read(bytes);if (genre == null) {genre = bytes[0];}randomAccessFile.seek(randomAccessFile.length() - ID3V1_TAG_LENGTH);} else {// 没有则直接定位到文件末尾追加randomAccessFile.seek(randomAccessFile.length());}// TAG 3 个字符开头,占 3 个字节bytes = ID3V1_TAG_START.getBytes();randomAccessFile.write(bytes);// Title 占 30 字节bytes = new byte[30];if (title != null) {byte[] tmp = title.getBytes(sCharset);System.arraycopy(tmp, 0, bytes, 0, Math.min(tmp.length, 30));}randomAccessFile.write(bytes);// Artist 占 30 字节bytes = new byte[30];if (artist != null) {byte[] tmp = artist.getBytes(sCharset);System.arraycopy(tmp, 0, bytes, 0, Math.min(tmp.length, 30));}randomAccessFile.write(bytes);// Album 占 30 字节bytes = new byte[30];if (album != null) {byte[] tmp = album.getBytes(sCharset);System.arraycopy(tmp, 0, bytes, 0, Math.min(tmp.length, 30));}randomAccessFile.write(bytes);// Year 占 4 字节bytes = new byte[4];if (year != null) {byte[] tmp = int2Bytes(year);System.arraycopy(tmp, 0, bytes, 0, Math.min(tmp.length, 4));}randomAccessFile.write(bytes);// Comment 占 28 字节,没有曲目序号时占 30 字节if (track == null) {// 没有曲目序号bytes = new byte[30];if (comment != null) {byte[] tmp = comment.getBytes(sCharset);System.arraycopy(tmp, 0, bytes, 0, Math.min(tmp.length, 30));}randomAccessFile.write(bytes);} else {// 有曲目序号bytes = new byte[28];if (comment != null) {byte[] tmp = comment.getBytes(sCharset);System.arraycopy(tmp, 0, bytes, 0, Math.min(tmp.length, 28));}randomAccessFile.write(bytes);// Reserved 占 1 字节,为 0 表示有曲目序号,下一字节为曲目序号bytes = new byte[]{0};randomAccessFile.write(bytes);// Track 占 1 字节,曲目序号bytes = new byte[]{track};randomAccessFile.write(bytes);}// Genre 占 1 字节,歌曲风格,-1 表示没有风格bytes = new byte[1];if (genre == null) {bytes[0] = -1;} else {bytes[0] = genre;}randomAccessFile.write(bytes);randomAccessFile.close();}public static void removeID3V1(File src) throws IOException {System.out.println("Remove ID3V1 start.");RandomAccessFile randomAccessFile = new RandomAccessFile(src, "rw");randomAccessFile.seek(randomAccessFile.length() - ID3V1_TAG_LENGTH);byte[] bytes = new byte[3];randomAccessFile.read(bytes);String tag = new String(bytes);if (!ID3V1_TAG_START.equals(tag)) {randomAccessFile.close();System.out.println("Remove ID3V1 end.");return;}randomAccessFile.setLength(randomAccessFile.length() - ID3V1_TAG_LENGTH);System.out.println("Remove ID3V1 end.");}
}

需要注意一下,在上面的 new String() 和 String.getBytes() 时,都指定了字符集为 GBK 编码而不是 UTF-8,否则是会乱码的。在执行上面的 main() 方法后,再查看它的二进制信息,可以看到 ID3V1 标签已经被添加到尾部了:

用播放器打开:

可以看到已经能展示 mp3 的歌曲名、歌手和专辑了,但是还没有专辑图,这个事已经超出了 ID3V1 的能力范围了,所以我们需要引入 ID3V2。

三、ID3V2

由于 ID3V1 的难以扩展,于是在 ID3V1 制定仅仅一年多后,1998 年 id3 的一群贡献者就制定了另一种标签格式来解决这个问题,即 ID3V2。由于 ID3V1 存放在了 mp3 文件尾部,所以 ID3V2 就只能存放在 mp3 文件头部了,因此,操作 ID3V2 时我们通常需要修改整个文件,所以效率会低一点,而且 ID3V2 的结构也会更复杂一点,但相较于 ID3V1,它有极强的扩展性,所以它还是那个被主要推广的一种标签格式,它的格式定义如下:

ID3V2 分为一个标签头和若干个标签帧,每一个标签帧又分为帧头和帧内容。

标签头固定长度 10 个字节:

  • Identifier:同 ID3V1 一样,开头是固定长度 3 个字节的标识,固定内容 ID3,如果没有这个标识则认为没有 ID3V2 标签。
  • Version:1 字节,主版本号,通常为 3。
  • Subversion:1 字节,副版本号,通常为 0。
  • Flag:1 字节,意义不大,通常为 0。
  • Size:4 字节,ID3V2 标签的长度。

这个 size 有些特殊,首先它是不包括标签头的 10 个字节,然后按照 ID3V2 标准.3.0#ID3v2_header 的要求,每个字节只用 7 位,最高位不使用,恒为 0,所以需要将 size 每个字节的最高位 0 位丢弃,而且它是高位在前。举个栗子:如果一个 size 是 43396,转成二进制应该是:

00000000000000001010101110111110

最后一个字节取后 7 位,即 10111110 取后 7 位 0111110,最高位补 0,变成 00111110,然后其余位左移,就变成了:

00000000000000010101011100111110

接着倒数第二个字节取后 7 位,即 01010111 取后 7 位 1010111,最高位补 0,变成 01010111,然后其余位左移,就变成了:

00000000000000100101011100111110

接着倒数第三个字节取后 7 位,即 00000010 取后 7 位 0000010,最高位补 0,变成 00000010,然后其余位左移,就变成了:

00000000000000100101011100111110

接着倒数第四个字节取后 7 位,即 00000000 取后 7 位 00000000,最高位补 0,变成 00000000,就变成了:

00000000000000100101011100111110

这是我们在写入 ID3V2 时需要做的操作,同样的,在读取的时候,需要将这个操作反过来,才能获取到正确的 ID3V2 的 size,在操作标签头的时候需要格外注意这个 size,Java 版的转换方法我先贴出来:

/*** Author:MrQinshou* Email:cqflqinhao@126* Date: 2023/3/10 11:40* Description: 写入 size 时编码*/
private static int syncIntEncode(int value) {int result = 0;int mask = 0x7F;while ((mask ^ 0x7FFFFFFF) > 0) {result = value & ~mask;result <<= 1;result |= value & mask;mask = ((mask + 1) << 8) - 1;value = result;}return result;
}/*** Author:MrQinshou* Email:cqflqinhao@126* Date: 2023/3/10 11:41* Description: 读取 size 时解码*/
private static int syncIntDecode(int value) {int a = 0;int b = 0;int c = 0;int d = 0;int result = 0x0;a = value & 0xFF;b = (value >> 8) & 0xFF;c = (value >> 16) & 0xFF;d = (value >> 24) & 0xFF;result = result | a;result = result | (b << 7);result = result | (c << 14);result = result | (d << 21);return result;
}

然后就是标签帧了,标签帧同样有一个帧头,帧头也是固定长度 10 个字节:

  • Id:4 字节,表示不同的帧标识,常用有 APIC(专辑图)、TALB(专辑)、TIT1(内容组描述)、TIT2(标题)、TIT3(副标题)、TPE1(艺术家),更多的 frameId 可以参考 ID3V2 标准 .3.0#Declared_ID3v2_frames。
  • Size:4 字节,帧内容长度,也是不包括帧头的 10 个字节,也是高位在前,但是没有别的骚操作了。
  • Flag:2 字节,意义不大。

帧内容,就是我们需要写入的数据了,需要注意的是不同的信息可能会有特殊的格式,在需要写入不同的标签时,大家参考一下 ID3V2 的规范即可。如专辑图,需要按照这样的格式顺序来写入:

  1. <Header for 'Attached picture', ID: "APIC">
  2. Text encoding   $xx
  3. MIME type       <text string>
  4. $00
  5. Picture type    $xx
  6. Description     <text string according to encoding> $00 (00)
  7. Picture data    <binary data>

有了上面这些对 ID3V2 的基础认知后,我们就可以通过代码来操作 ID3V2 了,还是以 Java 为例来为这个 mp3 文件添加 ID3V2 标签:

import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.*;/*** Author: MrQinshou* Email: cqflqinhao@126* Date: 2023/3/10 11:30* Description: 类描述*/
public class Mp3Util {private static final String TAG = Mp3Util.class.getSimpleName();private static final Charset sCharset = Charset.forName("GBK");private static final int ID3V1_TAG_LENGTH = 128;private static final String ID3V1_TAG_START = "TAG";private static final String ID3V2_TAG_START = "ID3";public static void main(String[] args) throws IOException {File src = new File("C:\\Users\\admin\\Desktop\\新建文件夹\\孤勇者bkp.mp3");File albumImg = new File("C:\\Users\\admin\\Desktop\\新建文件夹\\孤勇者.jpg");removeID3V2(src);setID3V2(src, "孤勇者", "陈奕迅", "孤勇者", albumImg);}public static byte[] reverse(byte[] origin) {for (int i = 0; i < origin.length / 2; i++) {byte temp = origin[i];origin[i] = origin[origin.length - i - 1];origin[origin.length - i - 1] = temp;}return origin;}private static byte[] int2Bytes(int i) {byte[] byteArray = new byte[4];byteArray[0] = (byte) (i & 0xFF);byteArray[1] = (byte) ((i & 0xFF00) >> 8);byteArray[2] = (byte) ((i & 0xFF0000) >> 16);byteArray[3] = (byte) ((i & 0xFF000000) >> 24);return byteArray;}private static int bytes2Int(byte[] bytes) {if (bytes == null || bytes.length < 4) {return 0;}return (0xFF & bytes[0]) | (0xFF00 & (bytes[1] << 8)) | (0xFF0000 & (bytes[2] << 16)) | (0xFF000000 & (bytes[3] << 24));}private static int syncIntEncode(int value) {int result = 0;int mask = 0x7F;while ((mask ^ 0x7FFFFFFF) > 0) {result = value & ~mask;result <<= 1;result |= value & mask;mask = ((mask + 1) << 8) - 1;value = result;}return result;}private static int syncIntDecode(int value) {int a = 0;int b = 0;int c = 0;int d = 0;int result = 0x0;a = value & 0xFF;b = (value >> 8) & 0xFF;c = (value >> 16) & 0xFF;d = (value >> 24) & 0xFF;result = result | a;result = result | (b << 7);result = result | (c << 14);result = result | (d << 21);return result;}private static class Frame {private String mId;private int mSize;private byte[] mFlag;private byte[] mContent;public Frame() {}public Frame(String id, int size, byte[] flag, byte[] content) {mId = id;mSize = size;mFlag = flag;mContent = content;}}public static void setID3V2(File src, String title, String artist, String album, File albumImg) throws IOException {RandomAccessFile randomAccessFile = new RandomAccessFile(src, "r");randomAccessFile.seek(0);byte[] bytes = new byte[3];randomAccessFile.read(bytes);String tag = new String(bytes);Map<String, Frame> frames = new HashMap<>();if (ID3V2_TAG_START.equals(tag)) {// 版本号bytes = new byte[1];randomAccessFile.read(bytes);// 副版本号bytes = new byte[1];randomAccessFile.read(bytes);// 标志,意义不大bytes = new byte[1];randomAccessFile.read(bytes);// 标签内容长度,高位在前,不包括标签头的 10 个字节bytes = new byte[4];randomAccessFile.read(bytes);// 标签内容长度,不包括标签头的 10 个字节,按照 ID3V2 标准 .3.0#ID3v2_header 的要求,每个字节只用 7 位,// 最高位不使用,恒为 0,所以需要将 size 每个字节的最高位 0 位丢弃,而且它是高位在前。int size = bytes2Int(reverse(bytes));size = syncIntDecode(size);while (true) {// Frame Id,4 字节bytes = new byte[4];randomAccessFile.read(bytes);String frameId = new String(bytes);if (!frameId.matches("([A-Z]|[0-9]){4}")) {break;}Frame frame = new Frame();frame.mId = frameId;// Frame size,4 字节bytes = new byte[4];randomAccessFile.read(bytes);int frameSize = bytes2Int(reverse(bytes));frame.mSize = frameSize;// Frame flag,2 字节,意义不大bytes = new byte[2];randomAccessFile.read(bytes);frame.mFlag = Arrays.copyOf(bytes, bytes.length);// Frame contentbytes = new byte[frameSize];randomAccessFile.read(bytes);frame.mContent = Arrays.copyOf(bytes, bytes.length);frames.put(frameId, frame);}// 加上标签头的 10 个字节,srcRandomAccessFile seek 到 ID3V2 tag header 之后数据帧开始的位置,用于后面拷贝 mp3 数据帧randomAccessFile.seek(size + 10);}if (title != null) {// TitleByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();// Text encoding.byteArrayOutputStream.write(0);// Title data.byteArrayOutputStream.write(title.getBytes(sCharset));frames.put("TIT2", new Frame("TIT2", byteArrayOutputStream.size(), new byte[2], byteArrayOutputStream.toByteArray()));byteArrayOutputStream.close();}if (artist != null) {// ArtistByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();// Text encoding.byteArrayOutputStream.write(0);// Artist data.byteArrayOutputStream.write(artist.getBytes(sCharset));frames.put("TPE1", new Frame("TPE1", byteArrayOutputStream.size(), new byte[2], byteArrayOutputStream.toByteArray()));byteArrayOutputStream.close();}if (album != null) {// AlbumByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();// Text encoding.byteArrayOutputStream.write(0);// Album data.byteArrayOutputStream.write(album.getBytes(sCharset));frames.put("TALB", new Frame("TALB", byteArrayOutputStream.size(), new byte[2], byteArrayOutputStream.toByteArray()));byteArrayOutputStream.close();}/*Album Image<Header for 'Attached picture', ID: "APIC">Text encoding   $xxMIME type       <text string>$00Picture type    $xxDescription     <text string according to encoding> $00 (00)Picture data    <binary data>*/if (albumImg != null) {// Album ImageByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();// Text encodingbyteArrayOutputStream.write(0);// Mime type.byteArrayOutputStream.write("image/jpeg".getBytes(StandardCharsets.UTF_8));// 00byteArrayOutputStream.write(0);// Picture typebyteArrayOutputStream.write(0);// DescriptionbyteArrayOutputStream.write(0);// Picture dataInputStream inputStream = null;try {inputStream = new FileInputStream(albumImg);byte[] buf = new byte[1024 * 8];int len = 0;while ((len = inputStream.read(buf)) != -1) {byteArrayOutputStream.write(buf, 0, len);byteArrayOutputStream.flush();}} catch (IOException e) {throw new RuntimeException(e);} finally {if (inputStream != null) {try {inputStream.close();} catch (IOException ignored) {}}}frames.put("APIC", new Frame("APIC", byteArrayOutputStream.size(), new byte[2], byteArrayOutputStream.toByteArray()));byteArrayOutputStream.close();}// Calculate ID3V2 sizeint id3V2Size = 0;for (Frame value : frames.values()) {// 每一个 Frame Header 为 10 字节id3V2Size += 10;id3V2Size += value.mSize;}// ID3V2 可以先预留一些空白标签帧,这样的好处是今后如果需要增加帧只需要覆盖空白字节即可// ,否则今后再想增加标签帧就需要又重写整个文件
//        byte[] empty = new byte[0];byte[] empty = new byte[100];id3V2Size += empty.length;// ID3V2 tag headerByteArrayOutputStream id3v2TagHeader = new ByteArrayOutputStream(10);// TAG 3 个字符开头,占 3 个字节id3v2TagHeader.write(ID3V2_TAG_START.getBytes());// 版本号id3v2TagHeader.write(3);// 副版本号id3v2TagHeader.write(0);// 标志,意义不大id3v2TagHeader.write(0);// 标签内容长度,不包括标签头的 10 个字节,按照 ID3V2 标准 .3.0#ID3v2_header 的要求,每个字节只用 7 位,// 最高位不使用,恒为 0,所以需要将 size 每个字节的最高位 0 位丢弃,而且它是高位在前。int syncIntEncode = syncIntEncode(id3V2Size);byte[] reverse = reverse(int2Bytes(syncIntEncode));id3v2TagHeader.write(reverse);File dst = new File(src.getAbsolutePath() + ".tmp");FileOutputStream fileOutputStream = new FileOutputStream(dst);fileOutputStream.write(id3v2TagHeader.toByteArray());fileOutputStream.flush();for (Frame value : frames.values()) {// 每一个 Frame Header 为 10 字节ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(10 + value.mSize);// Frame Id,4 字节byteArrayOutputStream.write(value.mId.getBytes());// Frame size,4 字节byteArrayOutputStream.write(reverse(int2Bytes(value.mSize)));// Frame flag,2 字节,意义不大byteArrayOutputStream.write(value.mFlag);// Frame contentbyteArrayOutputStream.write(value.mContent);fileOutputStream.write(byteArrayOutputStream.toByteArray());fileOutputStream.flush();}fileOutputStream.write(empty);fileOutputStream.flush();bytes = new byte[1024 * 8];int len = 0;while ((len = randomAccessFile.read(bytes)) != -1) {fileOutputStream.write(bytes, 0, len);fileOutputStream.flush();}randomAccessFile.close();src.delete();fileOutputStream.close();dst.renameTo(new File(src.getAbsolutePath()));}public static void removeID3V2(File src) throws IOException {RandomAccessFile randomAccessFile = new RandomAccessFile(src, "r");randomAccessFile.seek(0);byte[] bytes = new byte[3];randomAccessFile.read(bytes);String tag = new String(bytes);if (!ID3V2_TAG_START.equals(tag)) {// 没有则直接 returnrandomAccessFile.close();return;}// 版本号bytes = new byte[1];randomAccessFile.read(bytes);// 副版本号bytes = new byte[1];randomAccessFile.read(bytes);// 标志,意义不大bytes = new byte[1];randomAccessFile.read(bytes);// 标签内容长度,高位在前,不包括标签头的 10 个字节bytes = new byte[4];randomAccessFile.read(bytes);// 标签内容长度,不包括标签头的 10 个字节,按照 ID3V2 标准 .3.0#ID3v2_header 的要求,每个字节只用 7 位,// 最高位不使用,恒为 0,所以需要将 size 每个字节的最高位 0 位丢弃,而且它是高位在前。int size = bytes2Int(reverse(bytes));size = syncIntDecode(size);// 加上标签头的 10 个字节,srcRandomAccessFile seek 到 ID3V2 tag header 之后数据帧开始的位置,用于后面拷贝 mp3 数据帧randomAccessFile.seek(size + 10);File dst = new File(src.getAbsolutePath() + ".tmp");FileOutputStream fileOutputStream = new FileOutputStream(dst);bytes = new byte[1024 * 8];int len = 0;while ((len = randomAccessFile.read(bytes)) != -1) {fileOutputStream.write(bytes, 0, len);fileOutputStream.flush();}randomAccessFile.close();src.delete();fileOutputStream.close();dst.renameTo(new File(src.getAbsolutePath()));}
}

同样的,在 new String() 和 String.getBytes() 时,都指定了字符集为 GBK 编码而不是 UTF-8,否则是会乱码的。在执行上面的 main() 方法后,再查看它的二进制信息,可以看到 ID3V2 标签已经被添加到头部了:

 再用播放器打开就可以看到专辑图也成功加上了:

四、总结

我最开始其实是因为想修改 mp3 的信息只能在电脑上一个个操作,效率太慢而且太累了,所以才想去了解 mp3 的标签到底是怎么回事,电脑在修改标签时到底改了啥。知其然知其所以然,在了解一个东西展示的原理和依据后,要去修改它其实就很简单了,现在通过代码去操作 mp3 的标签信息简直易如反掌。

事实上除了这个规范中的头,我们也可以添加一些自己自定义的头,比如把歌词文件放到 ID3V2 中,然后写一个自定义播放器去解析它,这样就不用将歌词再放到一个单独的 lrc 文件了。

更多推荐

Java 修改 mp3 的标签(ID3V1 和 ID3V2)

本文发布于:2024-02-14 15:28:20,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1764219.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:标签   Java

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!