FFmpeg之三 录制音频并保存, API编解码从理论到实战

在学习FFmpeg的时候,想拿demo来练习,官方虽有示例,但更像是工具演示,新手不好掌握,在网上找不到有文章,能给出完整的示例和关键点的分析说明,一步一个错误,慢慢啃过来的,本文就把重要经验和完整代码全部分享出来。

文章目录

音频的基本概念

1. 采样率 (Sample Rate)

解释

采样率是指在将连续的模拟音频信号转换为数字信号时,每秒钟对其幅度进行测量的次数(样本数)。可以将其想象为给连续的声音波形拍摄快照,采样率就是每秒拍摄快照的数量。采样率越高,意味着捕捉到的声音信息越精细,尤其是在高频部分,能够还原的声音频率上限也越高(根据奈奎斯特理论,最高可还原频率约为采样率的一半)。

单位

赫兹 (Hz) 或千赫兹 (kHz)。

示例

  • 8000 Hz (8 kHz): 电话音质,足以识别人声,但听起来比较模糊。
  • 16000 Hz (16 kHz): 广泛用于 VoIP(网络电话)和一些语音识别应用,比电话音质好。
  • 44100 Hz (44.1 kHz): CD 音质标准。可以很好地覆盖人耳能听到的绝大部分频率范围(约 20 Hz - 20 kHz)。
  • 48000 Hz (48 kHz): 专业音频、DVD 和蓝光视频、数字电视广播中常用的标准。
  • 96000 Hz (96 kHz) / 192000 Hz (192 kHz): 高解析度音频(Hi-Res Audio)标准,理论上能提供超越 CD 的音质细节和频率响应,但文件体积也更大。

2. 声道数 (Channel Count)

解释

声道数是指音频信号中包含的独立声轨的数量。它决定了声音的空间感和来源方向。

示例

  • 1 (Mono / 单声道): 所有声音混合在一个声道中,没有方向感。适用于语音录制、一些老式录音或 AM 广播。
  • 2 (Stereo / 立体声 / 双声道): 包含左、右两个声道,可以营造出声音从不同方向传来的空间感。
  • 5.1 声道: 包含 5 个全频带声道(前左、前中、前右、后左、后右)和 1 个低频效果声道(LFE,即 ".1"),用于家庭影院环绕声。
  • 7.1 声道: 在 5.1 的基础上增加了两个侧环绕声道。

3. 采样位数 / 位深度 (Bit Depth)

解释

位深度(Bit Depth)描述了用来表示每个音频样本(采样点)的振幅(响度)的二进制位数(bits)。它决定了音频信号的动态范围(最响和最轻声音之间的范围)和量化噪声的大小。位数越多,表示振幅的精度就越高,动态范围越大,声音细节越丰富

单位

比特 (bit)。

示例

  • 8 bit: 动态范围较小,量化噪声明显。常见于早期的游戏、一些电话系统或特定效果。
  • 16 bit: CD 音质标准。提供了约 96 dB 的动态范围,对大多数听音环境和音乐类型来说已经足够好。
  • 24 bit: 专业音频录制和处理中广泛使用。提供了约 144 dB 的巨大动态范围,可以记录非常细微的声音细节,并在后期处理中有更大裕量。
  • 32 bit float (浮点): 主要在音频制作和处理软件内部使用。它提供了极大的动态范围,并且可以避免在处理过程中因信号过载而产生削波失真(clipping)。最终成品通常会转换回 16 bit 或 24 bit 整数。

4、音频帧

解释

"音频帧"(Audio Frame)是指编码器处理和输出的一个基本单元。它包含了一定数量的连续音频样本(每个声道)。

与前面提到的单个"样本"(Sample)不同,编码器(如 AAC, MP3, Opus 等)为了提高压缩效率和利用心理声学模型,通常会把一小段时间的音频样本打包在一起进行处理和压缩。这个"包"就是一个编码后的音频帧。

在 FFmpeg 的 AVCodecContext 结构中,有一个名为 frame_size 的成员。对于大多数有损压缩编码器,这个 frame_size 指的是该编码器每个输出帧所包含的单个声道的样本数量。对于特定编码器(或其特定配置)来说这个值是固定的

为什么需要知道 frame_size?

  • 缓冲管理: 在使用 libavcodec 进行编码时,你需要确保提供给编码器的 PCM 样本数量通常是 frame_size 的整数倍(或者至少满足编码器的最小输入要求)。

  • 时间戳计算: 音频帧的持续时间可以通过 frame_size / sample_rate 计算得出,这对于精确控制播放时间和同步至关重要。

  • 理解编码器行为: 知道帧大小有助于理解编码器的内部工作方式和潜在的延迟(通常至少是一个帧的长度)。

  • 重要区别: 对于未压缩的 PCM 数据,"帧"的概念不那么严格,你可以按任意数量的样本进行处理。但一旦涉及压缩编码器,它们通常强制要求以特定的 frame_size 来组织数据。

FFmpeg 中常见编码器的 frame_size 示例,frame_size 的具体值取决于所使用的编码器:

  • AAC (Advanced Audio Coding):

    常见的 FFmpeg 内置 aac 编码器或 libfdk_aac 编码器,其 frame_size 通常是 1024 个样本/声道。

    某些 AAC 变种,如 AAC-LD (Low Delay),frame_size 可能是 512 或 480。

  • MP3 (MPEG-1 Audio Layer III):

    使用 libmp3lame 编码器时,frame_size 通常是 1152 个样本/声道。

  • Opus:

    Opus 编码器比较灵活,它工作在不同的帧持续时间上(如 2.5ms, 5ms, 10ms, 20ms, 40ms, 60ms)。其 frame_size(样本数)等于 sample_rate * frame_duration_in_seconds。

    例如,在 48 kHz 采样率下,一个 20ms 的 Opus 帧包含 48000 * 0.020 = 960 个样本/声道。FFmpeg 的 frame_size 常常报告这个常用的 960 值。

  • PCM (Pulse Code Modulation - 未压缩):

    对于 PCM 这种未压缩格式,frame_size 的概念不那么适用。FFmpeg 可能会报告 frame_size 为 1,表示可以按样本处理,或者在某些封装上下文中可能有不同的表示。但编码意义上的固定帧大小通常不存在。

分片(plane)和打包(packed)

在ffmepeg 的AVCodecContext 有成员变量 sample_fmt,表示采样格式, 可选择位深度和存储样式。 位深度,例如:8bit、16bit,float 等等, 每种位深度都有两种类型,例如float型的有: AV_SAMPLE_FMT_FLT, AV_SAMPLE_FMT_FLTP

  • 带P(plane)的数据格式在存储时,其左声道和右声道的数据是分开存储的,左声道的数据存储在data[0],右声道的数据存储在data[1],每个声道的所占用的字节数为linesize[0]和linesize[1],frame.data[i]或者frame.extended_data[i]表示第i个声道的数据;

  • 不带P(packed)的音频数据在存储时,是按照LRLRLR...的格式交替存储在data[0]中,linesize[0]表示总的数据量,frame.data[0]或frame.extended_data[0]包含所有的音频数据中。

重采样

在这篇文章 FFmpeg之一------常用命令 ,有提到数据处理流程, 那里并没有说重采样。重采样流程是:decoder解析 得到frame -> 对frame重采样 得到frame1 -> 对frame1 进行encoder。 为什么还要经过重采样?

在理想情况下是可行的。不用重采样也是可以的,例如这种情况:

  1. 音频源(录音设备)输出的音频参数(采样率、位深度/样本格式、声道布局)与
  2. 你选择的编码器所支持(或你希望最终文件拥有)的音频参数完全一致。

在实际应用中,这种情况并不总是发生,因此需要进行重采样 (Resampling) 或其他格式转换。以下是主要原因:

  1. 采样率不匹配 (Sample Rate Mismatch):

来源: 你的录音设备(如麦克风)可能以一个特定的采样率工作,比如 44100 Hz 或 16000 Hz。

目标: 保存的文件是标准的 48000 Hz,或者你选用的编码器在 48000 Hz 时效果最好/效率最高

解决: 重采样,将音频数据的采样率从来源(如 44.1k)转换到目标(如 48k)。FFmpeg 的 libavresample 或 libswresample 库就是做这个的(通常通过 -ar 参数或 aresample filter 隐式或显式调用)。

  1. 样本格式不匹配 (Sample Format Mismatch):

来源: 音频设备或解码器可能输出一种特定的样本格式,例如 16 位有符号整数 (s16)、32 位浮点数 (flt)、或者平面格式的 32 位浮点数 (fltp)。

目标: 但你选择的编码器可能只接受(或优化于)特定的样本格式。例如,很多 AAC 编码器内部处理或接受 fltp 格式效率更高。有些旧编码器可能只接受 s16。

解决: 这就需要进行样本格式转换,将音频数据的表示方式从一种格式转为另一种。FFmpeg 通过 -sample_fmt 参数或 aformat filter 来处理。这严格来说不叫"重采样",但属于格式转换,通常和重采样一起讨论,因为都是预处理步骤。

  1. 声道布局不匹配 (Channel Layout Mismatch):

来源: 你可能从单声道麦克风录音 (mono)。

目标: 但你想保存为标准的双声道文件 (stereo),即使两个声道内容一样。或者反过来,从立体声源录制但只想保存单声道。

解决: 这就需要进行声道布局转换,比如将单声道复制成双声道,或将双声道混合为单声道。FFmpeg 通过 -ac 参数或 channelmap/pan 等 filter 来处理。

编码器要求/优化:

某些编码器对特定的输入参数组合有优化,或者干脆不支持某些组合。为了获得最佳压缩效率或兼容性,开发者可能会选择将输入音频转换到编码器最适合的格式。

AVAudioFifo

每个AVCodecContext (编解码器)都有对应的frame_size,在编码时,需要读取到frame_size 个sample后,才能编码一个音频帧。但是在音频转换过程中, 解码器对应的音频帧的采样数量 和 编码器对样的音频帧的采样数量可能不一样, 此时就需要累计音频采样数据到一定量后(编码器的音频帧大小)后,再写入一帧数据。否则会报错。

这个累计可以自己手动实现,也可以直接使用AVAudioFifo,它是一个缓存队列,可以把音频的采样先存入(av_audio_fifo_write),到底目标数量后(av_audio_fifo_size), 取出数据(av_audio_fifo_read)

PTS

一个音频帧的AVFrame有nb_samples个sample (和AVCodecContext 的frame_size值对应),

一个AVFrame 时长= nb_samples / sample_rate 秒

用frameIndex 表示当前帧的索引,PTS = frameIndex * 一个AVFrame 时长

代码实现:

实现步骤:

1、打开设备并初始化解码器

2、打开音频编码器

3、创建输出上下文并初始化 流、写入头文件

4、初始化重采样上下文

5、创建重采样上下文

6、使用av_read_frame 循环读取音频PCM数据,重复以下步骤:

6.1、音频解码 avcodec_send_packet-> avcodec_receive_frame, 得到解码帧decoded_frame

6.2、swr_convert_frame音频帧重采样 ,得到重采样后的音频帧 swr_frame

6.3、av_audio_fifo_write写入 FIFO队列

6.4、判断FIFO队列中的数据大小 av_audio_fifo_size,是否满足编码器帧大小

6.5、av_audio_fifo_read 读取FIFO中音频sample数据

6.6、对音频sample数据 进行编码

6.7、对编码后的音频帧 写文件

完整代码在 Github,下面贴出两个段的代码,

获取到音频帧的循环处理过程:

c 复制代码
static void process_frame_use_decode(AVPacket* pkt, AVFrame* decoded_frame, AVFrame* swr_frame, AVAudioFifo* fifo,
                                     AVCodecContext* decoder_ctx, AVCodecContext* encoder_ctx,
                                     AVFormatContext* ofmtCtx, struct SwrContext* swr_ctx, AVPacket* out_packet, int* frameIndex)
{
    int ret = avcodec_send_packet(decoder_ctx, pkt);

    if (ret < 0)
    {
        char error[1024] = {0};
        av_strerror(ret, error, 1024);
        fprintf(stderr, "Decode error: %s\n", error);
        return;
    }

    int received_frame = 0;

    while (avcodec_receive_frame(decoder_ctx, decoded_frame) == 0)
    {
        received_frame++;

        // swr_frame->ch_layout = encoder_ctx->ch_layout;
        //
        // swr_frame->format = encoder_ctx->sample_fmt;
        // swr_frame->sample_rate = 48000;
        // swr_frame->nb_samples = encoder_ctx->frame_size; //与编码器帧大小保持一致


        ret = swr_convert_frame(swr_ctx, swr_frame, decoded_frame);

        // 打印pts,dts
        printf("decoded_frame pts: %ld, dts: %ld\n", decoded_frame->pts, decoded_frame->pkt_dts);
        printf("swr_frame pts: %ld, dts: %ld\n", swr_frame->pts, swr_frame->pkt_dts);

        if (ret < 0)
        {
            char error[1024] = {0};
            av_strerror(ret, error, 1024);
            fprintf(stderr, "Error while resampling: %s\n", error);
            return;
        }

        // Add resampled samples to FIFO
        ret = av_audio_fifo_write(fifo, (void**)swr_frame->data, swr_frame->nb_samples);
        if (ret < swr_frame->nb_samples)
        {
            fprintf(stderr, "Failed to write all samples to FIFO\n");
            return;
        }
        printf(" frame received this time num: %d, Added %d samples to FIFO, current size: %d\n", received_frame, swr_frame->nb_samples, av_audio_fifo_size(fifo));

        // Encode when enough samples are available
        while (av_audio_fifo_size(fifo) >= encoder_ctx->frame_size)
        {
            printf("FIFO size: %d, frame size: %d\n", av_audio_fifo_size(fifo), encoder_ctx->frame_size);
            swr_frame->nb_samples = encoder_ctx->frame_size;
            ret = av_audio_fifo_read(fifo, (void**)swr_frame->data, encoder_ctx->frame_size);
            if (ret < encoder_ctx->frame_size)
            {
                fprintf(stderr, "Failed to read enough samples from FIFO\n");
                return;
            }

            swr_frame->pts = *frameIndex * encoder_ctx->frame_size;
            printf("in fifo swr_frame pts: %ld, dts: %ld\n", swr_frame->pts, swr_frame->pkt_dts);

            encode_and_write_frame(encoder_ctx, swr_frame, ofmtCtx, out_packet, *frameIndex);
            (*frameIndex)++;
        }
    }
}

对重采样的音频sample进行编码、写文件:

c 复制代码
static void encode_and_write_frame(AVCodecContext* encoder_ctx, AVFrame* swr_frame, AVFormatContext* ofmtCtx, AVPacket* out_packet, int frameIndex)
{
    int ret = avcodec_send_frame(encoder_ctx, swr_frame);


    if (ret < 0)
    {
        // 获取错误信息
        char error[1024] = {0};
        av_strerror(ret, error, 1024);
    }

    // 将重采样后的帧发送给编码器
    if (ret == 0)
    {
        while (avcodec_receive_packet(encoder_ctx, out_packet) == 0)
        {
            // 正确设置数据包中的流索引
            out_packet->stream_index = ofmtCtx->streams[0]->index;

            // 调整时间戳,使其基于输出流的时间基
            av_packet_rescale_ts(out_packet, encoder_ctx->time_base, ofmtCtx->streams[0]->time_base);

            // 写入一个编码的数据包到输出文件
            if (av_interleaved_write_frame(ofmtCtx, out_packet) < 0)
            {
                fprintf(stderr, "Error while writing output packet\n");
                break;
            }
            av_packet_unref(out_packet);
        }
    }
}
相关推荐
Antonio9152 小时前
【音视频】SDL简介
音视频·sdl
算家云4 小时前
AI音频核爆!Kimi开源“六边形战士”Kimi-Audio,ChatGPT语音版?
人工智能·音视频·kimi·算家云·kimi-audio·租算力,到算家云
Everbrilliant896 小时前
音视频之H.265/HEVC熵编码
音视频·h.265·算术编码·哈夫曼编码·熵编码·指数哥伦布编码·熵编码的基本原理
Panesle6 小时前
月之暗面开源-音频理解、生成和对话生成模型:Kimi-Audio-7B-Instruct
人工智能·音视频·语音生成
Antonio9158 小时前
【音视频】音频编码实战
ffmpeg·音视频
BO_S__17 小时前
python调用ffmpeg对截取视频片段,可批量处理
python·ffmpeg·音视频
亦双城的双子娴17 小时前
通过音频的pcm数据格式利用canvas绘制音频波形图
音视频·pcm·canva可画
Antonio9151 天前
【音视频】⾳频处理基本概念及⾳频重采样
ffmpeg·音视频·aac
电子科技圈1 天前
XMOS空间音频——在任何设备上都能提供3D沉浸式空间音频且实现更安全地聆听
经验分享·设计模式·性能优化·计算机外设·音视频