当前位置: 首页 > news >正文

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

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

文章目录

  • 音频的基本概念
    • 1. 采样率 (Sample Rate)
      • 解释
      • 单位
      • 示例
    • 2. 声道数 (Channel Count)
      • 解释
      • 示例
    • 3. 采样位数 / 位深度 (Bit Depth)
      • 解释
      • 单位
      • 示例
    • 4、音频帧
      • 解释
      • 为什么需要知道 frame_size?
  • 分片(plane)和打包(packed)
  • 重采样
  • AVAudioFifo
  • PTS
  • 代码实现:

音频的基本概念

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,下面贴出两个段的代码,

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

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,dtsprintf("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 FIFOret = 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 availablewhile (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进行编码、写文件:


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

相关文章:

  • C++初阶-STL简介
  • Unity 和 Unreal Engine(UE) 两大主流游戏引擎的核心使用方法
  • 司法大模型构建指南
  • 模方ModelFun工程操作教程
  • Deep Dark Sea 局域網文件共享即時匿名聊天去數據庫部署
  • 1、Linux操作系统下,ubuntu22.04版本切换中英文界面
  • mAh 与 Wh:电量单位的深度解析
  • 学习海康VisionMaster之路径提取
  • self-attention计算过程
  • JavaEE-多线程实战02
  • 计算机图形学(一):基础
  • err: Error: Request failed with status code 400
  • chrony服务器(2)
  • Azure Devops - 尝试一下在Pipeline中使用Self-hosted Windows agent
  • MongoDB与PHP7的集成与优化
  • 如何让自己的博客可以在百度、谷歌、360上搜索到(让自己写的CSDN博客可以有更多的人看到)
  • 电子病历高质量语料库构建方法与架构项目(智能质控体系建设篇)
  • 英文中数字读法规则
  • 【黑马JavaWeb+AI知识梳理】前端Web基础02 - JS+Vue+Ajax
  • 通过数据增强打造抗噪音多模态大模型
  • 央行回应美债波动:单一市场、单一资产变动对我国外储影响总体有限
  • 大学2025丨专访南开人工智能学院院长赵新:人工智能未来会变成通识类课程
  • 十四届全国人大常委会第十五次会议在京举行,审议民营经济促进法草案等
  • 上海虹桥至福建三明直飞航线开通,飞行时间1小时40分
  • 政治局会议深度|提出“设立新型政策性金融工具”有何深意?
  • 经济日报:上海车展展现独特魅力