下面将展示如何使用Java解析音频文件的ID3v1标签信息

1. 概述

ID3标签有两个主要版本,分别是ID3V1ID3V2,下面将介绍这两个版本的不同之处。

2. 位置

ID3V1通过将歌曲的信息写入音频文件的最后128字节,对音频文件进行标识,分为ID3V1ID3V1.1两个版本,ID3V1.1版本是对ID3V1版本的补充。

image-20220429145742799

3. 编码

ID3v1使用ANSI方式对tag进行编码,

ANSI并不是某一种特定的字符编码,而是在不同的系统中,根据当前系统使用的字符集进行编码,那么写入tag的字符编码就有可能是任意的标准,比如ASCIIUNICODEUTF-8GBKJISEUC-KR等编码,所以,读取tag时,需要判断字符的编码格式,这样读出来的内容才不会乱码。

4. 结构

ID3v1ID3v1.1的具体结构如下所示

 * 标签头 TAG  3byte
 * 标题(Tilte)        30byte
 * 作者(Artist)        30byte
 * 专辑(Album)        30byte
 * 出版年份(Year)     4byte
 * for ID3V1
 *      备注(Comment) 30 byte
 * for ID3V1.1
 *      备注(Comment)        28byte
 *      保留        1byte
 *      音轨(Track)        1byte
 * 流派/类型(Genre)        1byte

其中

  • 最开始的三个字节一直是TAG,只有存在这个标记,才表明这是一个ID3V1ID3v1.1标签

  • 对于tag的具体内容,如果某段信息没有被填满,比如标题只有8个字节,那么剩下的22个字节都用零字节进行填充,这样在读取的时候,只要读到零,就代表这段信息已经结束了。

  • 由于ID3V1里面没有音轨,所以ID3v1.1进行了改进,将Comment限制为28个字节,如果第29个字节为零,且第30个字节不为零,那么第30个字节的无符号整型值代表的就是音轨

  • 对于Track,由于大多数文件都是以无符号整型值-1作为结尾,所以很多时候,往往无法获取流派的信息

  • 其中流派中,无符号整型值对应的流派如下所示

  • ``` 0.Blues 1.Classic Rock 2.Country 3.Dance 4.Disco 5.Funk 6.Grunge 7.Hip-Hop 8.Jazz 9.Metal 10.New Age 11.Oldies 12.Other 13.Pop 14.R&B 15.Rap 16.Reggae 17.Rock 18.Techno 19.Industrial 20.Alternative 21.Ska 22.Death Metal 23.Pranks 24.Soundtrack 25.Euro-Techno 26.Ambient 27.Trip-Hop 28.Vocal 29.Jazz+Funk 30.Fusion 31.Trance 32.Classical 33.Instrumental 34.Acid 35.House 36.Game 37.Sound Clip 38.Gospel 39.Noise 40.AlternRock 41.Bass 42.Soul 43.Punk 44.Space 45.Meditative 46.Instrumental Pop 47.Instrumental Rock 48.Ethnic 49.Gothic 50.Darkwave 51.Techno-Industrial 52.Electronic 53.Pop-Folk 54.Eurodance 55.Dream 56.Southern Rock 57.Comedy 58.Cult 59.Gangsta 60.Top 40 61.Christian Rap 62.Pop/Funk 63.Jungle 64.Native American 65.Cabaret 66.New Wave 67.Psychadelic 68.Rave 69.Showtunes 70.Trailer 71.Lo-Fi 72.Tribal 73.Acid Punk 74.Acid Jazz 75.Polka 76.Retro 77.Musical 78.Rock & Roll 79.Hard Rock 80.Folk 81.Folk-Rock 82.National Folk 83.Swing 84.Fast Fusion 85.Bebob 86.Latin 87.Revival 88.Celtic 89.Bluegrass 90.Avantgarde 91.Gothic Rock 92.Progressive Rock 93.Psychedelic Rock 94.Symphonic Rock 95.Slow Rock 96.Big Band 97.Chorus 98.Easy Listening 99.Acoustic 100.Humour 101.Speech 102.Chanson 103.Opera 104.Chamber Music 105.Sonata 106.Symphony 107.Booty Bass 108.Primus 109.Porn Groove 110.Satire 111.Slow Jam 112.Club 113.Tango 114.Samba 115.Folklore 116.Ballad 117.Power Ballad 118.Rhythmic Soul 119.Freestyle 120.Duet 121.Punk Rock 122.Drum Solo 123.A capella 124.Euro-House 125.Dance Hall

    1. 80 Folk and after following genres are Winamp extensions Deprecated Gradle features were used in this build, making it incompatible with Gradle 8.0. ```

5. 实现代码

package com.flywinter;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Created by IntelliJ IDEA
 * User:Zhang Xingkun
 * Date:2022/4/29 1:36
 * Description:
 * ID3V1,ID3V1.1通常用于标识mp3的相关信息
 * ID3V1 包含了作者 、作曲 专辑信息等,长度128 byte
 * 位于music文件最后的128byte
 * 标签头 TAG  3byte
 * 标题        30byte
 * 作者        30byte
 * 专辑        30byte
 * 出版年份     4byte
 * for ID3V1
 * 备注 30 byte
 * for ID3V1.1
 * 备注        28byte
 * 保留        1byte
 * 音轨        1byte
 * 流派/类型        1byte
 */
public class ID3V1Parser {

    private final List<String> GENRES = List.of(
            "Blues", "Classic Rock", "Country", "Dance", "Disco",
            "Funk", "Grunge", "Hip-Hop", "Jazz", "Metal",
            "New Age", "Oldies", "Other", "Pop", "R&B",
            "Rap", "Reggae", "Rock", "Techno", "Industrial",
            "Alternative", "Ska", "Death Metal", "Pranks", "Soundtrack",
            "Euro-Techno", "Ambient", "Trip-Hop", "Vocal", "Jazz+Funk",
            "Fusion", "Trance", "Classical", "Instrumental", "Acid",
            "House", "Game", "Sound Clip", "Gospel", "Noise",
            "AlternRock", "Bass", "Soul", "Punk", "Space",
            "Meditative", "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic",
            "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk", "Eurodance",
            "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta",
            "Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American",
            "Cabaret", "New Wave", "Psychadelic", "Rave", "Showtunes",
            "Trailer", "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz",
            "Polka", "Retro", "Musical", "Rock & Roll", "Hard Rock",
            "Folk", "Folk-Rock", "National Folk", "Swing", "Fast Fusion",
            "Bebob", "Latin", "Revival", "Celtic", "Bluegrass",
            "Avantgarde", "Gothic Rock", "Progressive Rock", "Psychedelic Rock", "Symphonic Rock",
            "Slow Rock", "Big Band", "Chorus", "Easy Listening", "Acoustic",
            "Humour", "Speech", "Chanson", "Opera", "Chamber Music",
            "Sonata", "Symphony", "Booty Bass", "Primus", "Porn Groove",
            "Satire", "Slow Jam", "Club", "Tango", "Samba",
            "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", "Freestyle",
            "Duet", "Punk Rock", "Drum Solo", "A capella", "Euro-House",
            "Dance Hall", " 80 Folk and after  following genres are Winamp extensions"
    );

    public static void main(String[] args) throws IOException {
        final var filePath = "./AuRa - Ghost (Acoustic)(1).mp3";
        final var id3V1 = new ID3V1Parser();
        Map<String, String> map = id3V1.getID3V1AndV1_1(filePath);
        System.out.println(map);
    }

    private Map<String, String> getID3V1AndV1_1(String filePath) throws IOException {
        Map<String, String> map = new HashMap<>();
        final var buffer = new byte[128];
        try (var randomAccessFile = new RandomAccessFile(filePath, "r")) {
            randomAccessFile.seek(randomAccessFile.length() - 128);
            randomAccessFile.read(buffer, 0, 128);
            final var TAG = new String(copyBytesRangeAndTrimZero(buffer, 0, 3));
            if (TAG.equals("TAG")) {
                putValueByField(buffer, 3, 30, map, "SongName");
                putValueByField(buffer, 33, 30, map, "Artist");
                putValueByField(buffer, 63, 30, map, "Album");
                putValueByField(buffer, 93, 4, map, "Year");
                if (buffer[125] != 0 || buffer[126] == 0) {
//                    V1
                    putValueByField(buffer, 93, 30, map, "Comment");
                } else {
//                    V1.1
                    putValueByField(buffer, 93, 28, map, "Comment");
                    map.put("Track", String.valueOf(buffer[126]));
                }
                if (buffer[127] > -1) {
                    map.put("Genre", GENRES.get(buffer[127]));
                }
            }
        }
        return map;
    }

    private void putValueByField(byte[] buffer, int start, int length, Map<String, String> map, String field) {
        final var album = copyBytesRangeAndTrimZero(buffer, start, length);
        if (album.length > 0) {
            map.put(field, new String(album));
        }
    }

    private byte[] copyBytesRangeAndTrimZero(byte[] bytes, int start, int length) {
        int resultLength = 0;
        for (int i = start; i < start + length; i++) {
            if (bytes[i] == 0) {
                resultLength = i - start;
                break;
            }
            resultLength = length;
        }
        byte[] result = new byte[resultLength];
        System.arraycopy(bytes, start, result, 0, resultLength);
        return result;
    }
}

参考资料