下面将展示如何使用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  # 一个或多个音频帧

image-20220506221137

FLAC格式其实可以简单用下面的结构表示

  • fLaC 4bytes
  • metadata block
    • header
    • content
  • metadata block
    • header
    • content

STREAMINFO block的结构实际上和metadata block结构一样,只是内容有些特殊,它包含采样率、通道数等信息,以及可以帮助解码器管理其缓冲区的数据,这个数据块是必须要有的。

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);
    }
}

参考资料