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; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 周边上新:园子的第一款马克杯温暖上架
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?
· 使用C#创建一个MCP客户端