Bootstrap

音频文件格式——AAC、OGG和FLAC

3.AAC文件格式

3.1 封装格式解析

高级音频编码 (Advanced Audio Coding) 是一种用于有损数字音频压缩的音频编码标准。它被设计为 MP3 格式的继承者,在相同比特率下通常可以获得比 MP3 更高的音质。AAC有两种封装格式:

  • ADIF(Audio Data Interchange Format),音频数据交换格式,这种格式的特点是只在文件头部存储用于音频解码播放的头信息(例如采样率,通道数等),它的解码播放必须从文件头部开始,一般用于存储在本地磁盘中播放。
  • ADTS(Audio Data Transport Stream),音频数据传输流,这种格式的特点是可以将数据看做一个个的音频帧,而每帧都存储了用于音频解码播放的头信息(例如采样率,通道数等),即可以从任何帧位置解码播放,更适用于流媒体传输。

目前在网络传输中常用的就是ADTS格式的封装,所以我们常见的AAC原始码流是由一个一个的ADTS frame(音频数据传输流帧)组成,每一帧由ADTS的帧头和原始数据块(MEPG2 TS),也就是说每个ADTS frame都可以单独去解码。

对于AAC的头部由7/9个字节组成,包含以下信息:

序号长度(位)字段描述
A12syncword同步标志位,所有位都必须设置为1,固定为0xFF,表示ADTS的帧开始
B1idMPEG版本,0:MPEG-4;1:MPEG-2
C2layer图层,始终设置为0。
D1Protection absenceCRC校验标识,0:使用CRC校验;1:不使用CRC校验
E2Profile音频对象类型,按照MPEG-4的音频对象类型序号减1
F4sampling frequency index音频采样频率索引,如下所示:
0: 96000 Hz
1: 88200 Hz
2 : 64000 Hz
3 : 48000 Hz
4: 44100 Hz
...
G1private bit私有位,编码设置为0,解码时忽略
H3channel configuration声道配置
0: Defined in AOT Specifc Config
1: 1 channel : front - center
2 : 2 channels : front - left, front - right
3 : 3 channels : front - center, front - left, front - right
...
I1originality设置为1表示音频的原创性,否则设置为0
J1home编码是设置为0,解码时忽略
K1copyright id bit版权ID位
L1copyright id start通过设置1和0来表示此帧的版权ID位的第一位
M13frame length一个ADTS帧的⻓度,包括ADTS头和AAC原始流
O11buffer fullness缓冲区充满度,0x7FF说明是码率可变的码流,不需要此字段。CBR可能需要此字段,不同编码器使用情况不同
P2num raw data blocksADTS帧的AAC帧数(原始数据块)减1.为了获取最大的兼容性,始终为每个ADTS帧使用一个AAC帧
Q16crc如果CRC校验标识为0,进行CRC检查。

3.2 解析文件的报头信息

下面的程序会使用ffmpeg库解析AAC文件的报头信息并打印出来

#include <stdio.h>
#include <libavformat/avformat.h>

void print_adts_header(const uint8_t *header) {
    int syncword = (header[0] << 4) | (header[1] >> 4);
    int id = (header[1] >> 3) & 0x01;
    int layer = (header[1] >> 1) & 0x03;
    int protection_absent = header[1] & 0x01;
    int profile = (header[2] >> 6) & 0x03;
    int sampling_frequency_index = (header[2] >> 2) & 0x0F;
    int private_bit = (header[2] >> 1) & 0x01;
    int channel_configuration = ((header[2] & 0x01) << 2) | (header[3] >> 6);
    int original_copy = (header[3] >> 5) & 0x01;
    int home = (header[3] >> 4) & 0x01;
    int copyright_identification_bit = (header[3] >> 3) & 0x01;
    int copyright_identification_start = (header[3] >> 2) & 0x01;
    int frame_length = ((header[3] & 0x03) << 11) | (header[4] << 3) | (header[5] >> 5);
    int adts_buffer_fullness = ((header[5] & 0x1F) << 6) | (header[6] >> 2);
    int number_of_raw_data_blocks_in_frame = header[6] & 0x03;

    printf("Syncword: 0x%X\n", syncword);
    printf("ID: %d\n", id);
    printf("Layer: %d\n", layer);
    printf("Protection absent: %d\n", protection_absent);
    printf("Profile: %d\n", profile);
    printf("Sampling frequency index: %d\n", sampling_frequency_index);
    printf("Private bit: %d\n", private_bit);
    printf("Channel configuration: %d\n", channel_configuration);
    printf("Original/copy: %d\n", original_copy);
    printf("Home: %d\n", home);
    printf("Copyright identification bit: %d\n", copyright_identification_bit);
    printf("Copyright identification start: %d\n", copyright_identification_start);
    printf("Frame length: %d\n", frame_length);
    printf("ADTS buffer fullness: %d\n", adts_buffer_fullness);
    printf("Number of raw data blocks in frame: %d\n", number_of_raw_data_blocks_in_frame);
}

int main(int argc, char *argv[]) {
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <input.aac>\n", argv[0]);
        return -1;
    }

    av_register_all();

    AVFormatContext *fmt_ctx = NULL;
    if (avformat_open_input(&fmt_ctx, argv[1], NULL, NULL) < 0) {
        fprintf(stderr, "Could not open input file '%s'\n", argv[1]);
        return -1;
    }

    if (avformat_find_stream_info(fmt_ctx, NULL) < 0) {
        fprintf(stderr, "Could not find stream information\n");
        avformat_close_input(&fmt_ctx);
        return -1;
    }

    AVPacket pkt;
    av_init_packet(&pkt);

    while (av_read_frame(fmt_ctx, &pkt) >= 0) {
        if (pkt.stream_index == 0) { // Assuming the first stream is audio
            print_adts_header(pkt.data);
            break;
        }
        av_packet_unref(&pkt);
    }

    avformat_close_input(&fmt_ctx);
    return 0;
}

执行效果为:

3.2 读取文件的ADTS帧数

下面的程序会使用FFmpeg库读取AAC文件中所有ADTS的帧,并打印其数量和帧率。

#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>

int main(int argc, char *argv[]) {
    if (argc < 2) {
        printf("Usage: %s <input file>\n", argv[0]);
        return -1;
    }

    const char *input_filename = argv[1];
    AVFormatContext *format_ctx = NULL;
    AVCodecContext *codec_ctx = NULL;
    AVPacket packet;
    int ret, stream_index;

    av_register_all();

    // Open input file and allocate format context
    if (avformat_open_input(&format_ctx, input_filename, NULL, NULL) < 0) {
        fprintf(stderr, "Could not open input file '%s'\n", input_filename);
        return -1;
    }

    // Retrieve stream information
    if (avformat_find_stream_info(format_ctx, NULL) < 0) {
        fprintf(stderr, "Could not find stream information\n");
        return -1;
    }

    // Find the first audio stream
    stream_index = av_find_best_stream(format_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
    if (stream_index < 0) {
        fprintf(stderr, "Could not find audio stream in the input file\n");
        return -1;
    }

    codec_ctx = format_ctx->streams[stream_index]->codec;

    // Initialize packet
    av_init_packet(&packet);
    packet.data = NULL;
    packet.size = 0;

    // Read frames and print information
    int frame_count = 0;
    while (av_read_frame(format_ctx, &packet) >= 0) {
        if (packet.stream_index == stream_index) {
            frame_count++;
            printf("Frame %d: size=%d, sample_rate=%d\n", frame_count, packet.size, codec_ctx->sample_rate);
        }
        av_packet_unref(&packet);
    }

    // Clean up
    avcodec_close(codec_ctx);
    avformat_close_input(&format_ctx);

    return 0;
}

4.OGG文件格式

参考链接:

OGG 是由 Xiph.Org Foundation 维护的免费开放容器格式。OGG 格式的作者表示,它不受软件专利的限制,旨在提供高质量的数字多媒体的高效流式处理和操作。它用途广泛且灵活,旨在容纳不同类型的媒体数据,如音频、视频、文本和元数据。OGG 用于流式传输和本地播放,并以其开源性质而闻名。对于音频而言,OGG支持的音频格式有:

有损

  • Speex:以低比特率 (~2.1–32 kbit/s/通道) 处理语音数据
  • Vorbis:以中高级可变比特率(每通道 ≈16–500 kbit/s)处理一般音频数据
  • Opus:以低和高可变比特率(每通道 ≈6–510 kbit/s)处理语音、音乐和通用音频

无损

  • FLAC 处理存档和高保真音频数据。
  • OggPCM 允许在 Ogg 容器中存储标准未压缩的 PCM 音频。

当然OGG是支持视频格式的封装,这里暂时不探讨。

4.1 文件格式

OGG文件是由一个个大小可变的页(Page)组成,页的大小通常为4-8KB,最大为65307字节。每个页中包含被拆分的数据包,由于数据包可能会很大,所以可能一个数据包被拆分到不同的页中存储。所以每个页是由页的头部(page header)和被拆分的包数据组成。

下面我们参考官方文档了解其封装过程:

  1. 编码后的逻辑码流会分包给OGG,具体分包的大小由编码格式而定
  2. 每个包会被分割为若干段,每段通常为255字节,最后一段可能小于255字节。
  3. 将一组连续的段打包成一个页(Page)。每个页都有一个页头(Page Header),包含页序号、版本号、标志位等。
  4. 将多个页按照顺序组合成一个物理比特流。

4.2 页面结构

类型长度(bits)描述
Capture pattern (捕获模式)32捕获模式或同步代码,表示页面开始,用于确保在解析 Ogg 文件时同步。每个页面都以四个 ASCII 字符序列 “OggS” 开头。这有助于在数据丢失或损坏的情况下重新同步解析器,并且是在开始解析页面结构之前进行健全性检查。
Version(版本)8此字段表示 Ogg 文件格式的版本,以允许将来扩展。目前强制要求为 0。
Header Type (头部类型)8标志位,用于表示页面的类型。
位 值 标志位 页面类型
0 0x0 延续 此页面的第一个packet是前一个packet的延续
1 0x2 BOS 比特流的开始,表示此页面位比特流的第一页
2 0x4 EOS 比特流的结束,表示此页面位比特流的最后一页
Granule position(位置信息)64位置信息字段,对于音频流保存此页面的编码后PCM样本数;对于视频流保存此页面的编码后视频帧的总数。
Bitstream serial number(比特流序列号)32比特流序列号,包含唯一序列号的4字节字段,用于将 page 标识为属于特定逻辑比特流
Page sequence number(页面序列号)32页面序列号,包含唯一序列号的4字节字段,每个序列号随着逻辑比特流单调递增,假设第一页位0,第二页为1,依次类推。
Checksum(校验和)32校验和,此字段提供整个页面(包括页眉,在校验和字段设置为 0 的情况下计算)中数据的 CRC32校验和。这允许验证数据自创作以来是否未损坏。未通过校验和的页面应被丢弃。校验和是使用多项式值 0x04C11DB7 生成的。
Page Segments(页段)8页段,表示此页面存在的区段数量。它还指示此字段后面的区段表中有多少字节。任何一个页面中最多可以有 255 个区段。
Segment table(段表)n段表是一个 8 位值的数组,每个值指示页面正文中相应段的长度。区段数由 preceding page segments 字段确定。

4.3 解析OGG格式的报头信息

使用ffmpeg将pcm文件进行opus编码并使用ogg文件进行封装:

ffmpeg -f s16le -ar 44100 -ac 2 -i input.pcm -c:a libopus output.ogg
  • -f s16le:指定输入格式为16位小端PCM。
  • -ar 44100:指定采样率为44100Hz。
  • -ac 2:指定音频通道数为2(立体声)。
  • -i input.pcm:指定输入文件为input.pcm
  • -c:a libopus:指定音频编解码器为Opus。
  • output.ogg:指定输出文件为output.ogg

下面的程序将会读取OGG文件的前5页,并打印前5页的报头信息。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define ReadframNum 5

// 将8字节数组转换为unsigned long long
unsigned long long convertToULL(unsigned char num[8], int len) {
    unsigned long long result = 0;
    if (len == 8) {
        for (int i = 0; i < len; i++) {
            result |= ((unsigned long long)num[i] << (i * 8));
        }
    }
    return result;
}

// 将4字节数组转换为unsigned int
unsigned int convertToUInt(unsigned char num[4], int len) {
    unsigned int result = 0;
    if (len == 4) {
        for (int i = 0; i < len; i++) {
            result |= ((unsigned int)num[i] << (i * 8));
        }
    }
    return result;
}

// 读取OGG文件的页面头部信息
int readOggPageHeaders(const char *oggFile) {
    // 定义页面头部结构
    typedef struct {
        char capturePattern[4];          // 捕获模式
        unsigned char version;           // 版本
        unsigned char headerType;        // 头部类型
        unsigned char granulePosition[8];// 粒度位置
        unsigned char serialNumber[4];   // 比特流序列号
        unsigned char pageSequence[4];   // 页面序列号
        unsigned char checksum[4];       // 校验和
        unsigned char segmentCount;      // 页面段数
        unsigned char segmentTable[];    // 段表
    } OggPageHeader;

    // 打开OGG文件
    FILE *file = fopen(oggFile, "rb");
    if (!file) {
        perror("Failed to open file");
        return 1;
    }

    int frameCount = 0;
    // 循环读取前5帧的页面头部信息
    while (!feof(file) && frameCount < ReadframNum) {
        // 读取页面头部
        OggPageHeader pageHeader;
        if (1 != fread(&pageHeader, sizeof(pageHeader), 1, file))
            break;

        // 打印页面头部信息
        printf("Capture pattern: %c%c%c%c\n", pageHeader.capturePattern[0], pageHeader.capturePattern[1], pageHeader.capturePattern[2], pageHeader.capturePattern[3]);
        printf("Version: %d\n", pageHeader.version);
        printf("Header Type: %d\n", pageHeader.headerType);
        printf("Granule position: %llu\n", convertToULL(pageHeader.granulePosition, 8));
        printf("Bitstream serial number: %u\n", convertToUInt(pageHeader.serialNumber, 4));
        printf("Page sequence number: %u\n", convertToUInt(pageHeader.pageSequence, 4));
        printf("Checksum: %u\n", convertToUInt(pageHeader.checksum, 4));
        printf("Page Segments: %d\n", pageHeader.segmentCount);

        // 读取段表
        unsigned char *segmentTable = (unsigned char *)malloc(pageHeader.segmentCount);
        fread(segmentTable, sizeof(unsigned char), pageHeader.segmentCount, file);

        // 打印段表
        printf("Segment table: ");
        for (int i = 0; i < pageHeader.segmentCount; i++) {
            printf("%d ", segmentTable[i]);
        }
        printf("\n");

        // 计算段数据总大小
        unsigned int totalSegmentSize = 0;
        for (int i = 0; i < pageHeader.segmentCount; i++) {
            totalSegmentSize += segmentTable[i];
        }
        printf("Total Segment Size: %d\n", totalSegmentSize);

        // 读取段数据
        unsigned char *segmentData = (unsigned char *)malloc(totalSegmentSize);
        fread(segmentData, sizeof(unsigned char), totalSegmentSize, file);

        // 如果头部类型标志为4,打印最后4个字节
        if (pageHeader.headerType == 4)
            printf("Last 4 Bytes: %x %x %x %x\n", segmentData[totalSegmentSize - 4], segmentData[totalSegmentSize - 3], segmentData[totalSegmentSize - 2], segmentData[totalSegmentSize - 1]);

        // 释放内存
        free(segmentData);
        free(segmentTable);

        frameCount++;
    }

    // 关闭文件
    fclose(file);
    return 0;
}

int main(int argc, char *argv[]) {
    // 检查命令行参数
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <input.ogg>\n", argv[0]);
        return 1;
    }

    // 读取OGG文件的页面头部信息
    return readOggPageHeaders(argv[1]);
}

5.FLAC文件格式

参考资料:

FLAC,全称为 Free Lossless Audio Codec,是一种无损音频压缩格式。它的主要特点是能够在保持原始音频质量的同时大幅减少文件大小,通常可以达到原始文件大小的 50% 至 70% 左右。

其编码算法流程:

  1. 输入音频被分割成块。如果音频包含多个声道,则每个声道将单独编码为一个子块。
  2. 编码器尝试通过拟合简单的多项式或通过一般线性预测编码来找到块的良好数学近似值。然后编写近似值的描述,其长度仅为几个字节。
  3. 近似值和输入值之间的差异(称为残差)使用 Rice 编码进行编码。在许多情况下,与使用脉冲编码调制相比,近似值和编码残差的描述占用的空间更少。

编码后的音频被划分为多个帧,每个帧由一个标头、一个元数据块和一个音频编码数据和组成。每个帧都是彼此独立的编码。帧标头以同步字开头,用于标识有效帧的开头。标头的其余部分包含样本数、帧位置、通道分配以及可选的采样率和位深度。数据块包含音频信息。

文件头(File Header)
标志(Magic Number):FLAC文件的开头是一个4字节的标志“fLaC”,用于标识文件类型。

元数据块区域(Metadata Block Area)
元数据块区域包含描述FLAC音频流的信息以及一些附加信息。每个元数据块都有一个类型标识符和长度字段,以下是常见的元数据块类型:

  • 流信息块(STREAMINFO):包含整个音频流的基本信息,如采样率、声道数、总采样数等。这是FLAC文件中必须存在的第一个元数据块。
  • 填充块(PADDING):用于在文件中预留空间,以便以后添加元数据。
  • 应用程序数据块(APPLICATION):包含特定应用程序的信息。
  • 定位表块(SEEKTABLE):保存快速定位点,用于快速跳转到文件中的特定位置。
  • 标签信息块(VORBIS_COMMENT):存储一系列可读的“名/值”键值对,通常用于存储标签信息。
  • 索引表块(CUESHEET):存储用于CD刻录的索引信息。
  • 图片块(PICTURE):保存相关图片信息,如专辑封面。

音频帧区域(Audio Frame Area)
音频帧区域包含实际的音频数据,每个音频帧由帧头、子帧和帧尾组成:

  • 帧头(Frame Header):记录帧的相关信息,如同步码、采样率、声道数、采样深度等。
  • 子帧(Subframe):每个子帧对应一个声道的数据,包含子帧头和编码后的音频数据。
  • 帧尾(Frame Footer):包含CRC-16校验码,用于校验帧数据的完整性。

参考链接:FLAC - Format

;