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

news2025/4/30 0:50:58

在学习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,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进行编码、写文件:


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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2345672.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Jenkins Pipeline 构建 CI/CD 流程

文章目录 jenkins 安装jenkins 配置jenkins 快速上手在 jenkins 中创建一个新的 Pipeline 作业配置Pipeline运行 Pipeline 作业 Pipeline概述Declarative PipelineScripted Pipeline jenkins 安装 安装环境&#xff1a; Linux CentOS 10&#xff1a;Linux CentOS9安装配置Jav…

AJAX 介绍

一、什么是AJAX ? AJAX 是 异步的 JavaScript 和 XML&#xff08;Asynchronous JavaScript And XML&#xff09; 的缩写&#xff0c;是一种实现浏览器与服务器进行数据通信的技术。其核心是通过 XMLHttpRequest 对象在不重新刷新页面的前提下&#xff0c;与服务器交换数据并更…

promis(resolve,reject)入门级别

JavaScript Promise 的定义 Promise 是一种用于处理异步操作的对象&#xff0c;表示一个可能已经完成或者尚未完成的操作的结果。它的核心作用在于简化复杂的回调嵌套问题&#xff08;即所谓的“回调地狱”&#xff09;&#xff0c;使异步代码更加清晰易读。 Promise 的状态 …

w~嵌入式C语言~合集6

我自己的原文哦~ https://blog.51cto.com/whaosoft/13870384 一、开源MCU简易数字示波器项目 这是一款采用STC8A8K MCU制造的简单示波器&#xff0c;只有零星组件&#xff0c;易于成型。这些功能可以涵盖简单的测量&#xff1a; 该作品主要的规格如下&#xff1a; 单片机…

学习海康VisionMaster之路径提取

一&#xff1a;进一步学习了 今天学习下VisionMaster中的路径提取&#xff1a;可在绘制的路径上等间隔取点或查找边缘点 二&#xff1a;开始学习 1&#xff1a;什么是路径提取&#xff1f; 相当于事先指定一段路径&#xff0c;然后在对应的路径上查找边缘&#xff0c;这个也是…

第35课 常用快捷操作——用“鼠标左键”拖动图元

概述 拖动某个图元&#xff0c;是设计过程中常需要用到的操作&#xff0c;我们可以在原理图中拖动某个元器件符号&#xff0c;也可以在PCB图中拖动某个焊盘。 和常用的软件类似&#xff0c;用按住鼠标左键的方式来完成拖动操作。 用鼠标左键拖动图元 在想要拖动的图元上&…

二、Web服务常用的I/O操作

一、单个或者批量上传文件 前端&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>文件…

「Mac畅玩AIGC与多模态04」开发篇01 - 创建第一个 LLM 对话应用

一、概述 本篇介绍如何在 macOS 环境下&#xff0c;基于已部署完成的 Dify 平台和本地 LLM 模型&#xff08;如 DeepSeek&#xff09;&#xff0c;创建并测试第一个基础对话应用&#xff0c;实现快速验证推理服务与平台交互功能。 二、应用创建流程 1. 通过首页创建应用 打…

深度探究获取淘宝商品数据的途径|API接口|批量自动化采集商品数据

在电商行业竞争日益激烈的今天&#xff0c;淘宝商品数据如同蕴藏巨大价值的宝藏&#xff0c;无论是商家进行竞品分析、优化商品策略&#xff0c;还是数据分析师挖掘市场趋势&#xff0c;都离不开对这些数据的获取与分析。本文将深入探讨获取淘宝商品数据的多种途径&#xff0c;…

马哥教育Linux云计算运维课程

课程大小&#xff1a;19.1G 课程下载&#xff1a;https://download.csdn.net/download/m0_66047725/90640128 更多资源下载&#xff1a;关注我 你是否找了很多资料看了很多视频聊了很多群友&#xff0c;却发现自己技术仍然原地踏步&#xff1f;本教程联合BAT一线导师倾囊相授…

FPGA与边缘AI:计算革命的前沿力量

在数字化转型浪潮中&#xff0c;边缘计算和人工智能正引领着技术革命。而FPGA&#xff08;现场可编程门阵列&#xff09;作为一种独特的硬件架构&#xff0c;正逐渐成为边缘AI领域的关键推动力。本文将探讨FPGA与边缘AI的结合如何重塑我们的数字世界&#xff0c;以及这一技术融…

Kafka 架构设计和组件介绍

什么是Apache Kafka&#xff1f; Apache Kafka 是一个强大的开源分布式事件流平台。它最初由 LinkedIn 开发&#xff0c;最初是一个消息队列&#xff0c;后来发展成为处理各种场景数据流的工具。 Kafka 的分布式系统架构支持水平扩展&#xff0c;使消费者能够按照自己的节奏检…

【Node.js 】在Windows 下搭建适配 DPlayer 的轻量(简陋)级弹幕后端服务

一、引言 DPlayer官网&#xff1a;DPlayer 官方弹幕后端服务&#xff1a;DPlayer-node MoePlayer/DPlayer-node&#xff1a;使用 Docker for DPlayer Node.js 后端&#xff08;https://github.com/DIYgod/DPlayer&#xff09; 本来想直接使用官网提供的DPlayer-node直接搭建…

OpenSSH配置连接远程服务器MS ODBC驱动与Navicat数据库管理

OpenSSH配置连接远程服务器MS ODBC驱动与Navicat数据库管理 目录 OpenSSH配置连接远程服务器MS ODBC驱动与Navicat数据库管理 一、MS ODBC驱动 1.1、安装到Windows后的表现形式 1.2、版本的互斥性 1.3、安装程序 1.4、配置后才可用 二、Navicat数据库管理工具 2.1、安…

操作系统:计算机世界的基石与演进

一、操作系统的本质与核心功能 操作系统如同计算机系统的"总管家"&#xff0c;在硬件与应用之间架起关键桥梁。从不同视角观察&#xff0c;其核心功能呈现多维价值&#xff1a; 硬件视角的双重使命&#xff1a; 硬件管理者&#xff1a;通过内存管理、进程调度和设…

Codeium 免费的AI编程助手

Codeium 由 Exafunction 团队&#xff08;主要也是美国华人&#xff09;开发的一款免费AI编程助手&#xff0c;是一个建立在顶尖AI技术上的代码加速工具&#xff0c;其背后的老板非常厉害&#xff0c;据说投资过马斯克的SpaceX。Codeium 本身具有颇多的亮点&#xff0c;支持70种…

在MySQL Shell里 重启MySQL 8.4实例

前一段时间看到MySQL官方视频的Oracle工程师在mysql shell里面重启mysql实例&#xff0c;感觉这个操作很方便&#xff0c;所以来试试&#xff0c;下面为该工程师的操作截图 1.MySQL Shell 通过root用户连上mysql&#xff0c;shutdown mysql实例 [rootmysql8_3 bin]# mysqlshMy…

FANUC机器人GI与GO位置数据传输设置

FANUC机器人GI与GO位置数据传输设置&#xff08;整数小数分开发&#xff09; 一、概述 在 Fanuc 机器人应用中&#xff0c;如果 IO 点位足够&#xff0c;可以利用机器人 IO 传输位置数据及偏移位置数据等。 二、操作步骤 1、确认通讯软件安装 首先确认机器人控制柜已经安装…

LeetCode 24 两两交换链表中的节点

​给你一个链表&#xff0c;两两交换其中相邻的节点&#xff0c;并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题&#xff08;即&#xff0c;只能进行节点交换&#xff09;。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4] 输出&#xff1a;[2,1…

低代码平台开发手机USB-HID调试助手

项目介绍 USB-HID调试助手是一种专门用于调试和测试USB-HID设备的软件工具。USB-HID设备是一类通过USB接口与计算机通信的设备&#xff0c;常见的HID设备包括键盘、鼠标、游戏控制器、以及一些专用的工业控制设备等。 主要功能包括&#xff1a; 数据监控&#xff1a;实时监控和…