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

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

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

Java 修改 mp3 的标签(ID3V1 和 ID3V2)_第1张图片

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

Java 修改 mp3 的标签(ID3V1 和 ID3V2)_第2张图片

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

Java 修改 mp3 的标签(ID3V1 和 ID3V2)_第3张图片

如果查看文件属性的话,在详细信息标签页,这两个文件也是有区别的:Java 修改 mp3 的标签(ID3V1 和 ID3V2)_第4张图片Java 修改 mp3 的标签(ID3V1 和 ID3V2)_第5张图片

所以播放器展示的时候,其实是取的文件详细信息中的值,那这个值是怎么来的呢?这就要说到 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 字节,按照固定的格式,包含了我们需要的一些信息,格式定义如下:Java 修改 mp3 的标签(ID3V1 和 ID3V2)_第6张图片

  • 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 为例,我们查看一下它的二进制信息:

Java 修改 mp3 的标签(ID3V1 和 ID3V2)_第7张图片

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

import java.io.*;
import java.nio.charset.Charset;

/**
 * Author: MrQinshou
 * Email: [email protected]
 * 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 标签已经被添加到尾部了:Java 修改 mp3 的标签(ID3V1 和 ID3V2)_第8张图片

用播放器打开:

Java 修改 mp3 的标签(ID3V1 和 ID3V2)_第9张图片

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

三、ID3V2

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

Java 修改 mp3 的标签(ID3V1 和 ID3V2)_第10张图片

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

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

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

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

00000000 00000000 10101011 10111110

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

00000000 00000001 01010111 00111110

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

00000000 00000010 01010111 00111110

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

00000000 00000010 01010111 00111110

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

00000000 00000010 01010111 00111110

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

/**
 * Author:MrQinshou
 * Email:[email protected]
 * 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:[email protected]
 * 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 标准 https://id3.org/id3v2.3.0#Declared_ID3v2_frames。
  • Size:4 字节,帧内容长度,也是不包括帧头的 10 个字节,也是高位在前,但是没有别的骚操作了。
  • Flag:2 字节,意义不大。

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

  1. Text encoding   $xx
  2. MIME type      
  3. $00
  4. Picture type    $xx
  5. Description     $00 (00)
  6. Picture 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: [email protected]
 * 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 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 标准 https://id3.org/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 content
                bytes = 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) {
            // Title
            ByteArrayOutputStream 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) {
            // Artist
            ByteArrayOutputStream 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) {
            // Album
            ByteArrayOutputStream 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
             
Text encoding $xx MIME type $00 Picture type $xx Description $00 (00) Picture data */ if (albumImg != null) { // Album Image ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); // Text encoding byteArrayOutputStream.write(0); // Mime type. byteArrayOutputStream.write("image/jpeg".getBytes(StandardCharsets.UTF_8)); // 00 byteArrayOutputStream.write(0); // Picture type byteArrayOutputStream.write(0); // Description byteArrayOutputStream.write(0); // Picture data InputStream 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 size int 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 header ByteArrayOutputStream id3v2TagHeader = new ByteArrayOutputStream(10); // TAG 3 个字符开头,占 3 个字节 id3v2TagHeader.write(ID3V2_TAG_START.getBytes()); // 版本号 id3v2TagHeader.write(3); // 副版本号 id3v2TagHeader.write(0); // 标志,意义不大 id3v2TagHeader.write(0); // 标签内容长度,不包括标签头的 10 个字节,按照 ID3V2 标准 https://id3.org/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 content byteArrayOutputStream.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)) { // 没有则直接 return randomAccessFile.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 标准 https://id3.org/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 标签已经被添加到头部了:

Java 修改 mp3 的标签(ID3V1 和 ID3V2)_第11张图片

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

四、总结

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

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

你可能感兴趣的:(走在自己的Android之路上,java)