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)的数据格式在存储时,其左声道和右声道的数据是分开存储的,左声道的数据存储在data0,右声道的数据存储在data1,每个声道的所占用的字节数为linesize0和linesize1,frame.datai或者frame.extended_datai表示第i个声道的数据;

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

重采样

在这篇文章 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);
        }
    }
}
相关推荐
hflmwl88 小时前
Wise Data Recovery找回误删的文档照片音乐和视频
音视频·datarecovery·找回误删的文档照片音乐和视频·找回误删的文档照片音乐视频
技灵AI8 小时前
AI 视频转场提示词怎么写?让前后画面衔接更自然
人工智能·prompt·aigc·音视频
youandyouyou10 小时前
跨区培训的屏幕共享怎么做才稳?同屏同步与弱网优化实践
音视频·网络通信
开开心心就好10 小时前
课堂随机点名工具,绿色版免安装双击即用
java·服务器·前端·ffmpeg·音视频·perl·symfony
美狐美颜SDK开放平台10 小时前
直播APP集成美颜SDK,如何梳理第三方组件的数据权限?
开发语言·人工智能·深度学习·音视频·第三方美颜sdk
小鹿软件办公11 小时前
不依赖 FFmpeg 也能录屏?ShareX 22 新增录屏等十余款新工具
ffmpeg·sharex·免费录屏工具
l143723326712 小时前
批量视频硬字幕擦除多少钱?影响报价的关键因素全解析
音视频
重生六道12 小时前
B站视频时长统计:一段JS脚本搞定分P/合集总时长(附倍速计算)
开发语言·javascript·音视频
音视频牛哥12 小时前
不换摄像机,也能接入 WebRTC?SmartMediaKit RTSP/RTMP 转 WHIP 技术实践
音视频·webrtc·smartmediakit·rtsp转whip·rtmp转whip·whip推流·rtsp转webrtc播放
lll235269177913 小时前
Android APP短视频音频提取,主流工具怎么选
ffmpeg·音视频