Java 读取、修改MP3标签

发布时间 2023-12-19 23:00:12作者: laremehpe
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import pojo.Id3v1;
import pojo.Id3v2;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

public class MP3MetadataReader {//MP3MetadataReader
    final String charset = "UTF-8";

    public int mp3Type(RandomAccessFile file) throws IOException { // 0: cannot determine mp3 version, 1: ID3V1, 2: ID3V2
        byte[] bytes = new byte[3];
        file.seek(0);
        file.read(bytes);
        if ("ID3".equals(new String(bytes)))
            return 2;
        file.seek(file.length() - 128);
        file.read(bytes);
        if ("TAG".equals(new String(bytes)))
            return 1;

        return 0;
    }


    private 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 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 byte[] removeZero(byte[] data) {
        int prefix = 0, affix = data.length - 1;
        for (; prefix < data.length; prefix++) {
            if (data[prefix] != 0) break;
        }
        for (; affix > prefix; affix--) {
            if (data[affix] != 0) break;
        }
        int len = affix - prefix;
        len = Math.max(len, 0);
        byte[] result = new byte[len];
        for (int i = 0; i < len; i++) {
            result[i] = data[i + prefix];
        }
        return result;
    }

    public Id3v1 readID3V1(RandomAccessFile file) throws IOException {
        Id3v1 meta = new Id3v1();
        file.seek(file.length() - (128 - 3));
        byte[] bytes = new byte[30];
        file.read(bytes);
        meta.setTitle(new String(removeZero(bytes), charset));
        file.read(bytes);
        meta.setArtist(new String(removeZero(bytes), charset));
        file.read(bytes);
        meta.setAlbum(new String(removeZero(bytes), charset));
        bytes = new byte[4];
        file.read(bytes);
        meta.setYear(bytes2Int(bytes));
        bytes = new byte[30];
        file.read(bytes);
        if (bytes[28] == 0)
            meta.setComment(new String(removeZero(bytes), charset));
        else
            meta.setComment(new String(removeZero(bytes), charset));
        bytes = new byte[1];
        file.read(bytes);
        meta.setGenre(bytes[0]);
        return meta;
    }

    public void setID3V1(RandomAccessFile file, Id3v1 data) throws IOException {
        file.seek(file.length() - 128);
        file.write("TAG".getBytes());

        byte[] bytes = new byte[30];

        byte[] titleBytes = data.getTitle().getBytes(charset);
        System.arraycopy(titleBytes, 0, bytes, 0, Math.min(titleBytes.length, 30));
        file.write(bytes);

        byte[] artistBytes = data.getArtist().getBytes(charset);
        System.arraycopy(artistBytes, 0, bytes, 0, Math.min(artistBytes.length, 30));
        file.write(bytes);

        byte[] albumBytes = data.getAlbum().getBytes(charset);
        System.arraycopy(albumBytes, 0, bytes, 0, Math.min(albumBytes.length, 30));
        file.write(bytes);

        bytes = new byte[4];
        byte[] yearBytes = int2Bytes(data.getYear());
        System.arraycopy(yearBytes, 0, bytes, 0, Math.min(yearBytes.length, 4));
        file.write(bytes);

        bytes = new byte[30];
        byte[] commentBytes = data.getComment().getBytes(charset);
        System.arraycopy(commentBytes, 0, bytes, 0, Math.min(commentBytes.length, 30));
        file.write(bytes);

        bytes = new byte[1];
        if (data.getGenre() == null) {
            bytes[0] = -1;
        } else {
            bytes[0] = data.getGenre();
        }
        file.write(bytes);

        file.close();
    }


    private int decodeSize(int num) {
        int mask = 0x7F;
        while (mask < Integer.MAX_VALUE) {
            num = ((num & ~mask) >> 1) | (num & mask);
            mask = ((mask + 1) << 8) - 1;
        }
        return num;
    }

    private int encodeSize(int num) {
        int mask = 0x7F;
        while (mask < Integer.MAX_VALUE) {
            num = ((num & ~mask) << 1) | num & mask;
            mask = ((mask + 1) << 8) - 1;
        }
        return num;
    }

    private static byte[] reverse(byte[] origin) {
        for (int i = 0, len = origin.length / 2; i < len; i++) {
            byte temp = origin[i];
            origin[i] = origin[origin.length - i - 1];
            origin[origin.length - i - 1] = temp;
        }
        return origin;
    }


    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    private class Frame {
        private String mId = "";
        private int mSize = 0;
        private byte[] mFlag = new byte[]{0, 0};
        private byte[] mContent = new byte[0];
    }

    /**
     * TCOP: 版权         TOPE: 原艺术家
     * TDAT: 日期         TPE3: 指挥者
     * TPE1: 艺术家        TYER: 专辑发行年代
     * USLT: 歌词         TALB: 专辑名称
     * TIT2: 歌曲名称      TCON: 流派
     * COMM: 注释         TRCK: 音轨号/综合音轨号
     *
     * @param file
     * @return
     * @throws IOException
     */
    public Id3v2 readID3V2(RandomAccessFile file) throws IOException {
        Id3v2 meta = new Id3v2();
        file.seek(6);
        byte[] bytes = new byte[4];
        file.read(bytes);

        while (true) {
            bytes = new byte[4];
            file.read(bytes);//frame id
            String frameId = new String(bytes);
            if (!frameId.matches("([A-Z]|[0-9]){4}")) break;
            file.read(bytes);//frame size
            int frameSize = bytes2Int(reverse(bytes));
            file.read(new byte[2]);//flag
            bytes = new byte[frameSize];
            file.read(bytes);//content
            switch (frameId) {
                case "TIT2": // title
                    meta.setTitle(new String(bytes, 1, bytes.length - 1));
                    break;
                case "TPE1": // artist
                    meta.setArtist(new String(bytes, 1, bytes.length - 1));
                    break;
                case "TALB": // album
                    meta.setAlbum(new String(bytes, 1, bytes.length - 1));
                    break;
                case "APIC": // album img
                    meta.setAlbumImg(Arrays.copyOf(bytes, bytes.length));
                    break;
            }
        }
        return meta;
    }

    public void setID3V2(RandomAccessFile file, Id3v2 data) throws IOException {
        file.seek(6);
        byte[] bytes = new byte[4];
        file.read(bytes);
        int size = decodeSize(bytes2Int(reverse(bytes)));
        Map<String, Frame> frames = new HashMap<>();
        while (true) {
            // Frame Id,4 字节
            bytes = new byte[4];
            file.read(bytes);
            String frameId = new String(bytes);
            if (!frameId.matches("([A-Z]|[0-9]){4}")) {
                break;
            }
            Frame frame = new Frame();
            frame.setMId(frameId);
            // Frame size,4 字节
            bytes = new byte[4];
            file.read(bytes);
            frame.setMSize(bytes2Int(reverse(bytes)));
            // Frame flag,2 字节,意义不大
            file.read(new byte[2]);
            // Frame content
            bytes = new byte[frame.getMSize()];
            file.read(bytes);
            frame.mContent = Arrays.copyOf(bytes, bytes.length);
            frames.put(frameId, frame);
        }
        // 加上标签头的 10 个字节,src RandomAccessFile seek 到 ID3V2 tag header 之后数据帧开始的位置,用于后面拷贝 mp3 数据帧
        file.seek(size + 10);
        // title
        ByteArrayOutputStream bas = new ByteArrayOutputStream();
        bas.write(0);
        bas.write(data.getTitle().getBytes(charset));
        frames.put("TIT2", new Frame("TIT2", bas.size(), new byte[2], bas.toByteArray()));
        bas.close();
        // artist
        bas = new ByteArrayOutputStream();
        bas.write(0);
        bas.write(data.getArtist().getBytes(charset));
        frames.put("TPE1", new Frame("TPE1", bas.size(), new byte[2], bas.toByteArray()));
        bas.close();
        // album
        bas = new ByteArrayOutputStream();
        bas.write(0);
        bas.write(data.getAlbum().getBytes(charset));
        frames.put("TALB", new Frame("TALB", bas.size(), new byte[2], bas.toByteArray()));
        bas.close();
        // album img
        if (data.getAlbumImgSrc() != null) {
            bas = new ByteArrayOutputStream();
            bas.write(0);
            bas.write("image/jpeg".getBytes(StandardCharsets.UTF_8));
            // 00
            bas.write(0);
            // Picture type
            bas.write(0);
            // Description
            bas.write(0);
            // Picture data
            InputStream inputStream = null;
            try {
                inputStream = new FileInputStream(data.getAlbumImgSrc());
                byte[] buf = new byte[1024 * 8];
                int len = 0;
                while ((len = inputStream.read(buf)) != -1) {
                    bas.write(buf, 0, len);
                    bas.flush();
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            } finally {
                if (inputStream != null) {
                    try {
                        inputStream.close();
                    } catch (IOException ignored) {
                    }
                }
            }
            frames.put("APIC", new Frame("APIC", bas.size(), new byte[2], bas.toByteArray()));
            bas.close();
        }
        // Calculate ID3V2 size
        int id3V2Size = 0;
        for (Frame value : frames.values()) {
            // 每一个 Frame Header 为 10 字节
            id3V2Size += 10;
            id3V2Size += value.getMSize();
        }
        // preserved empty space
        byte[] empty = new byte[100];
        id3V2Size += empty.length;
        ByteArrayOutputStream id3v2TagHeader = new ByteArrayOutputStream(10);
        id3v2TagHeader.write("ID3".getBytes());
        // version
        id3v2TagHeader.write(3);
        // sub version
        id3v2TagHeader.write(0);
        // flag
        id3v2TagHeader.write(0);
        int syncIntEncode = encodeSize(id3V2Size);
        byte[] reverse = reverse(int2Bytes(syncIntEncode));
        id3v2TagHeader.write(reverse);
        String tmpFileSrc = data.getFileSrc() + ".tmpFileSrc";
        FileOutputStream fileOutputStream = new FileOutputStream(tmpFileSrc);
        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.getMId().getBytes());
            // Frame size,4 字节
            byteArrayOutputStream.write(reverse(int2Bytes(value.getMSize())));
            // Frame flag,2 字节,意义不大
            byteArrayOutputStream.write(value.getMFlag());
            // Frame content
            byteArrayOutputStream.write(value.getMContent());
            fileOutputStream.write(byteArrayOutputStream.toByteArray());
            fileOutputStream.flush();
        }
        fileOutputStream.write(empty);
        fileOutputStream.flush();
        bytes = new byte[1024 * 8];
        int len = 0;
        while ((len = file.read(bytes)) != -1) {
            fileOutputStream.write(bytes, 0, len);
            fileOutputStream.flush();
        }
        file.close();
        File musicFile = new File(data.getFileSrc());
        musicFile.delete();
        fileOutputStream.close();
        new File(tmpFileSrc).renameTo(musicFile);
    }


    public static void main(String[] args) throws IOException {
        String fileLocation = "C:\\Users\\djatm\\Desktop\\develop\\test\\2.mp3";
        String fileLocation2 = "C:\\Users\\djatm\\Desktop\\develop\\test\\3.mp3";
        MP3MetadataReader reader = new MP3MetadataReader();
//        RandomAccessFile file1 = new RandomAccessFile(fileLocation, "rw");
        RandomAccessFile file2 = new RandomAccessFile(fileLocation2, "rw");
//        System.out.println(reader.readID3V1(file1));
        System.out.println(reader.readID3V2(file2));
//        Id3v1 v1 = new Id3v1();
//        v1.setTitle("testTitle");
//        v1.setAlbum("testAlbum");
//        v1.setArtist("testArtist");
//        v1.setYear(2023);
//        v1.setComment("testComment");
//        reader.setID3V1(file1, v1);
        Id3v2 v2 = new Id3v2();
        v2.setTitle("testTitle");
        v2.setAlbum("testAlbum");
        v2.setArtist("testArtist");
        v2.setYear(2023);
        v2.setComment("testComment");
        v2.setFileSrc(fileLocation2);
        reader.setID3V2(file2, v2);
        System.out.println("------------------------------------ read");
//        file1 = new RandomAccessFile(fileLocation, "rw");
        file2 = new RandomAccessFile(fileLocation2, "rw");
//        System.out.println(reader.readID3V1(file1));
        System.out.println(reader.readID3V2(file2));
    }
}
package pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Id3v1 {
    private String fileSrc;
    private String title = "";
    private String artist = "";
    private String album = "";
    private int year = 0;
    private String comment = "";
    private Byte genre = 0;
}
package pojo;

import lombok.Data;
import lombok.ToString;

@Data
@ToString(callSuper = true)
public class Id3v2 extends Id3v1 {
    private byte[] albumImg;
    private String albumImgSrc;
}