下面将展示如何使用Java解析音频文件的FLAC标签信息
1. 简介
FLAC格式的歌曲有自己的一套标签定义标注,因此并不兼容其他格式的标签,比如ID3,APEv2,虽然官网推荐的读写FLAC的工具可以识别并跳过ID3标签,但并不推荐在FLAC歌曲中写入ID3标签。
2. 结构
The four byte string "fLaC" # 四字节串“fLaC”
The STREAMINFO metadata block # STREAMINFO元数据块
Zero or more other metadata blocks # 零个或多个其他元数据块
One or more audio frames # 一个或多个音频帧
FLAC格式其实可以简单用下面的结构表示
- fLaC 4bytes
- metadata block
- header
- content
- metadata block
- header
- content
- …
STREAMINFO block的结构实际上和metadata block结构一样,只是内容有些特殊,它包含采样率、通道数等信息,以及可以帮助解码器管理其缓冲区的数据,这个数据块是必须要有的。
Header
header部分的结构如下
- 1 bytes
- 1 bit(最高位) 0表示后面还有metadata block,1表示是最后一个metadata block
- 7 bit(0-6bit) 代表block的类型,不同的值有不同的含义
- 0 STREAMINFO
- 1 PADDING
- 2 APPLICATION
- 3 SEEKTABLE
- 4 VORBIS_COMMENT
- 5 CUESHEET
- 6 PICTURE
- 7-126 reserved
- 127 invalid
- 3bytes
- 24bit 表示这个block content的大小,不包括header
- 计算方式如下
- 00000000 00000000 00000000
- size = ((buffer[1] & 0xff) << 16) +
((buffer[2] & 0xff) << 8) +
((buffer[3] & 0xff));
Content
content为具体的内容,根据header的类型标志位进行标识,目前已经定义的共有七种类型,这里只介绍其中两种
VORBIS_COMMENT
存储了一系列的键值对,用来存储歌曲的相关信息,比如歌名,歌手名,歌词等,结构如下
- 4 bytes
- 表示vendor的长度
- vendor 注意这里采用UTF-8编码,长度为上面计算出的长度
- 4 bytes
- 表示共有多少键值对
- 4 bytes
- 键值对长度
- 键值对
- 4 bytes
- 键值对长度
- 键值对
- 4 bytes
- 键值对长度
- 键值对
...
-
vorbis comment规范全都使用UTF-8进行编码,因此这里默认的字符编码就是UTF-8
-
vorbis 采用小端编码的方式计算大小或数量,示例如下
(buffer[0] & 0xff) + ((buffer[1] & 0xff) << 8) + ((buffer[2] & 0xff) << 16) + ((buffer[3] & 0xff) << 24);
PICTURE
存储了歌曲的封面等信息,结构如下
- 4 bytes 图片类型 0-20,计算方式与content size计算方式相同
- 4 bytes MIME内容长度 计算方式同上
- MIME内容,编码格式为UTF-8
- 4 bytes 描述符长度,计算方式同上
- 描述符
- 4 bytes 图片宽度
- 4 bytes 图片长度
- 4 bytes 图片颜色深度
- 4 bytes 索引图使用的颜色数目,0非索引图
- 4 bytes 图片数据长度
- 图片数据
3. 实现代码
package com.flywinter;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
/**
* Created by IntelliJ IDEA
* User:Zhang Xingkun
* Date:2022/5/2 12:55
* Description:
* <1> Last-metadata-block flag: '1' if this block is the last metadata block before the audio blocks, '0' otherwise.
* <7> BLOCK_TYPE
* 0 : STREAMINFO
* 1 : PADDING
* 2 : APPLICATION
* 3 : SEEKTABLE
* 4 : VORBIS_COMMENT
* 5 : CUESHEET
* 6 : PICTURE
* 7-126 : reserved
* 127 : invalid, to avoid confusion with a frame sync code
* <24> Length (in bytes) of metadata to follow (does not include the size of the METADATA_BLOCK_HEADER)
* 部分 大小 说明
* 头部 是否是最后一个块 1字节 1位 0:不是最后一个块
* 1:是最后一个块
* 块的类型 7位 0:流信息
* 1:填充块
* 2:应用程序数据
* 3:定位表
* 4:标签信息
* 5:索引表
* 6:图片
* 数据的大小 3字节(24位) 头部后面所跟的数据的大小,不包括头部,单位是字节
* 数据 头部中“数据的大小”字段规定的大小 与“块的类型”一项相符的数据,各类元数据块的数据结构会在之后说明
* https://xiph.org/flac/format.html#metadata_block_vorbis_comment
*/
public class FLACParser {
private static long tmpPos = 0;
public static void main(String[] args) throws IOException {
try (var randomAccessFile = new RandomAccessFile("牧野由依 - ふわふわ♪ (轻飘飘♪).flac", "r")) {
var buffer = new byte[4];
randomAccessFile.read(buffer);
final var tag = new String(buffer, 0, 4);
if (tag.equals("fLaC")) {
tmpPos += 4;
getMetadata(randomAccessFile);
}
}
}
private static void getMetadata(RandomAccessFile randomAccessFile) throws IOException {
byte[] buffer;
var isLast = false;
do {
buffer = getBuffer(randomAccessFile, 4);
final var type = buffer[0] & 0x7f;
final var size = getDataSize(buffer);
var lastFlag = (buffer[0] & 0xff) >> 7;
isLast = lastFlag == 1;
System.out.print(size + " ");
tmpPos += 4;
buffer = getBuffer(randomAccessFile, size);
getDataByType(buffer, type);
tmpPos += size;
} while (!isLast);
}
private static byte[] getBuffer(RandomAccessFile randomAccessFile, int size) throws IOException {
byte[] buffer;
buffer = new byte[size];
randomAccessFile.seek(tmpPos);
randomAccessFile.read(buffer);
return buffer;
}
private static void getDataByType(byte[] buffer, int type) throws IOException {
switch (type) {
case 0 -> System.out.println("STREAMINFO");
case 1 -> System.out.println("PADDING");
case 2 -> System.out.println("APPLICATION");
case 3 -> System.out.println("SEEKTABLE");
case 4 -> getVorbisComment(buffer);
case 5 -> System.out.println("CUESHEET");
case 6 -> getPictureInfo(buffer);
case 127 -> System.out.println("Invalid");
default -> System.out.println("7-126 : reserved");
}
}
private static int getDataSize(byte[] buffer) {
return ((buffer[1] & 0xff) << 16) +
((buffer[2] & 0xff) << 8) +
((buffer[3] & 0xff));
}
/**
* vendorSize 4bytes
* vendorName vendorSize bytes
* fieldNumber 4bytes
* fieldSize1 4bytes
* key=value fieldSize1
* fieldSize2 4bytes
* key=value fieldSize2
* .....
* https://www.xiph.org/vorbis/doc/v-comment.html
* https://blog.csdn.net/qq_34600635/article/details/113703588
*/
private static void getVorbisComment(byte[] buffer) {
System.out.print("VORBIS_COMMENT");
var vendorSize = getVorbisSize(buffer, 0);
final var vendorName = new String(buffer, 4, vendorSize, StandardCharsets.UTF_8);
System.out.println(" vendorSize " + vendorSize + " vendorName " + vendorName + " ");
var vendorLength = vendorSize + 4;
var number = getVorbisSize(buffer, vendorLength);
System.out.println("tagNum:" + number);
var tagLength = vendorLength + 4;
for (int i = 0; i < number; i++) {
var tagSize = getVorbisSize(buffer, tagLength);
tagLength += 4;
System.out.println(new String(buffer, tagLength, tagSize, StandardCharsets.UTF_8));
tagLength += tagSize;
}
}
private static void getPictureInfo(byte[] buffer) throws IOException {
var index = 0;
System.out.println("PICTURE");
var pictureIndex = getImageMetaSize(buffer, index);
System.out.println(getPictureType(pictureIndex));
index += 4;
var mimeSize = getImageMetaSize(buffer, index);
index += 4;
var mimeContent = new String(buffer, index, mimeSize, StandardCharsets.UTF_8);
System.out.println("mimeContent: " + mimeContent);
index += mimeSize;
var descriptionSize = getImageMetaSize(buffer, index);
index += 4;
System.out.println("description: " + new String(buffer, index, descriptionSize, StandardCharsets.UTF_8));
index += descriptionSize;
System.out.println("width: " + getImageMetaSize(buffer, index));
index += 4;
System.out.println("height: " + getImageMetaSize(buffer, index));
index += 4;
System.out.println("colorDep: " + getImageMetaSize(buffer, index));
index += 4;
System.out.println("colorNum: " + getImageMetaSize(buffer, index));
index += 4;
var imageSize = getImageMetaSize(buffer, index);
System.out.println("imageSize: " + imageSize);
index += 4;
final var imageData = new byte[imageSize];
System.arraycopy(buffer, index, imageData, 0, imageSize);
Files.write(Path.of("./test." + mimeContent.split("/")[1]), imageData);
}
private static int getVorbisSize(byte[] buffer, int index) {
return (buffer[index] & 0xff) +
((buffer[index + 1] & 0xff) << 8) +
((buffer[index + 2] & 0xff) << 16) +
((buffer[index + 3] & 0xff) << 24);
}
private static int getImageMetaSize(byte[] buffer, int index) {
return ((buffer[index] & 0xff) << 24) +
((buffer[index + 1] & 0xff) << 16) +
((buffer[index + 2] & 0xff) << 8) +
(buffer[index + 3] & 0xff);
}
/**
* $00 Other
* $01 32x32 pixels 'file icon' (PNG only)
* $02 Other file icon
* $03 Cover (front)
* $04 Cover (back)
* $05 Leaflet page
* $06 Media (e.g. lable side of CD)
* $07 Lead artist/lead performer/soloist
* $08 Artist/performer
* $09 Conductor
* $0A Band/Orchestra
* $0B Composer
* $0C Lyricist/text writer
* $0D Recording Location
* $0E During recording
* $0F During performance
* $10 Movie/video screen capture
* $11 A bright coloured fish
* $12 Illustration
* $13 Band/artist logotype
* $14 Publisher/Studio logotype
*
* @param index index
* @return Picture type
*/
private static String getPictureType(int index) {
final var list = List.of(
"Other",
"32x32 pixels 'file icon' (PNG only)",
"Other file icon",
"Cover(front)",
"Cover(back)",
"Leaflet page",
"Media(e.g.lable side of CD)",
"Lead artist / lead performer / soloist",
"Artist / performer",
"Conductor",
"Band / Orchestra",
"Composer",
"Lyricist / text writer",
"Recording Location",
"During recording",
"During performance",
"Movie / video screen capture",
"A bright coloured fish",
"Illustration",
"Band / artist logotype",
"Publisher / Studio logotype"
);
return list.get(index);
}
}
参考资料
赏
Alipay
