FFmpeg连载6-音频重采样

news2024/11/18 12:46:03

今天我们的实战内容是将音频解码成PCM,并将PCM重采样成特定的采样率,然后输出到本地文件进行播放。


什么是重采样?


所谓重采样,一句话总结就是改变音频的三元素,也就是通过重采样改变音频的采样率、采样格式或者声道数。
例如音频A是采样率48000hz、采样格式为f32le、声道数为1,通过重采样可以将音频A的采样率变更为采样率44100hz、采样格式为s16le、声道数为2等。


为什么需要重采样?


一般进行重采样有两个原因,一是播放设备需要,二是音频合并、或编码器等需要。
例如有些声音设备只能播放44100hz的采样率、16位采样格式的音频数据,因此如果音频不是这些格式的,就需要进行重采样才能正常播放了。


例如FFmpeg默认的AAC编码器输入的PCM格式为:AV_SAMPLE_FMT_FLTP,如果需要使用FFMpeg默认的AAC编码器则需要进行重采样了。又比有些需要进行混音的业务需求,需要保证PCM三要素相同才能进行正常混音。


如何进行音频重采样?


在重采样的过程中我们要坚守一个原则就是音频经过重采样后它的播放时间是不变的,如果一个10s的音频经过重采样后变成了15,那肯定就是不行的。


影响音频播放时长的因素是每帧的采样数和采样率,下面举一个例子简单介绍下音频播放时长的问题:
假如现有mp3,它的采样率是采样率48000,mp3每帧采样点数是1152,那么每帧mp3的播放时长就是 1152/48000,每一个采样点的播放时长就是1/48000。


假如现有mp3,它的采样率是采样率44100,aac每帧采样点数是1024,那么每帧aac的播放时长就是 1024/44100,每个采样点的播放时长就是1/44100。

从上面的例子中我们可以看出,对于采样率不同的两个音频,不可能1帧mp3转换出1帧aac,它们的比例不是1:1的,对于上面的例子,那么1帧mp3能重采样出多少个aac的采样点呢? 以时间不变为基础,可以有这样的一个公式:

1152 / 48000 = 目标采样点数 / 44100
也就是说:目标采样点数 = 1152 * 44100 / 48000

这条公式可以用FFmpeg中的函数av_rescale_rnd来实现...


有了计算公式,下面我们说说FFmpeg重采样的步骤:


1、分配SwrContext并配置音频输出输出参数
这里可以直接使用函数swr_alloc_set_opts实现,也可以使用swr_alloc、av_opt_set_channel_layout、av_opt_set_int、av_opt_set_sample_fmt等组合函数分步实现,


2、初始化SwrContext
分配好SwrContext 后,通过函数swr_init进行重采样上下文初始化。


3、swr_convert重采样
FFmpeg真正进行重采样的函数是swr_convert。它的返回值就是重采样输出的点数。使用FFmpeg进行重采样时内部是有缓存的,而内部缓存了多少个采样点,可以用函数swr_get_delay获取。 也就是说调用函数swr_convert时你传递进去的第三个参数表示你希望输出的采样点数,但是函数swr_convert的返回值才是真正输出的采样点数,这个返回值一定是小于或等于你希望输出的采样点数。

【免费分享】音视频学习资料包、大厂面试题、技术视频和学习路线图,资料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以点击788280672加群免费领取~

下面是完整代码:

#ifndef AUDIO_TARGET_SAMPLE
#define AUDIO_TARGET_SAMPLE 48000
#endif

#include <iostream>

extern "C" {
#include "libavformat/avformat.h"
#include <libswresample/swresample.h>
#include <libavcodec/avcodec.h>
#include <libavutil/frame.h>
#include <libavutil/opt.h>
#include <libavutil/channel_layout.h>
}

class AudioResample {
public:
    // 将PCM数据重采样
    void decode_audio_resample(const char *media_path, const char *pcm_path) {
        avFormatContext = avformat_alloc_context();
        int ret = avformat_open_input(&avFormatContext, media_path, nullptr, nullptr);
        if (ret < 0) {
            std::cout << "输入打开失败" << std::endl;
            return;
        }
        // 寻找视频流
        int audio_index = av_find_best_stream(avFormatContext, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0);
        if (audio_index < 0) {
            std::cout << "没有可用的音频流" << std::endl;
            return;
        }
        // 配置解码相关
        const AVCodec *avCodec = avcodec_find_decoder(avFormatContext->streams[audio_index]->codecpar->codec_id);
        avCodecContext = avcodec_alloc_context3(avCodec);
        avcodec_parameters_to_context(avCodecContext, avFormatContext->streams[audio_index]->codecpar);
        ret = avcodec_open2(avCodecContext, avCodec, nullptr);
        if (ret < 0) {
            std::cout << "解码器打开失败" << std::endl;
            return;
        }
        // 分配包和帧数据结构
        avPacket = av_packet_alloc();
        avFrame = av_frame_alloc();

        // 打开yuv输出文件
        pcm_out = fopen(pcm_path, "wb");
        // 读取数据解码
        while (true) {
            ret = av_read_frame(avFormatContext, avPacket);
            if (ret < 0) {
                std::cout << "音频包读取完毕" << std::endl;
                break;
            } else {
                if (avPacket->stream_index == audio_index) {
                    // 只处理音频包
                    ret = avcodec_send_packet(avCodecContext, avPacket);
                    if (ret < 0) {
                        std::cout << "发送解码包失败" << std::endl;
                        return;
                    }
                    while (true) {
                        ret = avcodec_receive_frame(avCodecContext, avFrame);
                        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
                            break;
                        } else if (ret < 0) {
                            std::cout << "获取解码数据失败" << std::endl;
                            return;
                        } else {
                            std::cout << "重采样解码数据" << std::endl;
                            resample();
                        }
                    }
                }
            }
            av_packet_unref(avPacket);
        }
    }

    ~AudioResample() {
        // todo 释放资源
    }

private:

    AVFormatContext *avFormatContext = nullptr;
    AVCodecContext *avCodecContext = nullptr;
    AVPacket *avPacket = nullptr;
    AVFrame *avFrame = nullptr;
    FILE *pcm_out = nullptr;
    SwrContext *swrContext = nullptr;
    AVFrame *out_frame = nullptr;
    int64_t max_dst_nb_samples;

    /**
     * 重采样并输出到文件
     */
    void resample() {
        if (nullptr == swrContext) {
            /**
             * 以下可以使用 swr_alloc、av_opt_set_channel_layout、av_opt_set_int、av_opt_set_sample_fmt
             * 等API设置,更加灵活
             */
            swrContext = swr_alloc_set_opts(nullptr, AV_CH_LAYOUT_STEREO, AV_SAMPLE_FMT_FLTP, AUDIO_TARGET_SAMPLE,
                                            avFrame->channel_layout, static_cast<AVSampleFormat>(avFrame->format),
                                            avFrame->sample_rate, 0, nullptr);
            swr_init(swrContext);
        }
        // 进行音频重采样
        int src_nb_sample = avFrame->nb_samples;
        // 为了保持从采样后 dst_nb_samples / dest_sample = src_nb_sample / src_sample_rate
        max_dst_nb_samples = av_rescale_rnd(src_nb_sample, AUDIO_TARGET_SAMPLE, avFrame->sample_rate, AV_ROUND_UP);
        // 从采样器中会缓存一部分,获取缓存的长度
        int64_t delay = swr_get_delay(swrContext, avFrame->sample_rate);
        int64_t dst_nb_samples = av_rescale_rnd(delay + avFrame->nb_samples, AUDIO_TARGET_SAMPLE, avFrame->sample_rate,
                                                AV_ROUND_UP);
        if(nullptr == out_frame){
            init_out_frame(dst_nb_samples);
        }

        if (dst_nb_samples > max_dst_nb_samples) {
            // 需要重新分配buffer
            std::cout << "需要重新分配buffer" << std::endl;
            init_out_frame(dst_nb_samples);
            max_dst_nb_samples = dst_nb_samples;
        }
        // 重采样
        int ret = swr_convert(swrContext, out_frame->data, dst_nb_samples,
                              const_cast<const uint8_t **>(avFrame->data), avFrame->nb_samples);

        if(ret < 0){
            std::cout << "重采样失败" << std::endl;
        } else{
            // 每帧音频数据量的大小
            int data_size = av_get_bytes_per_sample(static_cast<AVSampleFormat>(out_frame->format));

            std::cout << "重采样成功:" << ret << "----dst_nb_samples:" << dst_nb_samples  << "---data_size:" << data_size << std::endl;
            // 交错模式保持写入
            // 注意不要用 i < out_frame->nb_samples, 因为重采样出来的点数不一定就是out_frame->nb_samples
            for (int i = 0; i < ret; i++) {
                for (int ch = 0; ch < out_frame->channels; ch++) {
                    // 需要储存为pack模式
                    fwrite(out_frame->data[ch] + data_size * i, 1, data_size, pcm_out);
                }
            }
        }
    }

    void init_out_frame(int64_t dst_nb_samples){
        av_frame_free(&out_frame);
        out_frame = av_frame_alloc();
        out_frame->sample_rate = AUDIO_TARGET_SAMPLE;
        out_frame->format = AV_SAMPLE_FMT_FLTP;
        out_frame->channel_layout = AV_CH_LAYOUT_STEREO;
        out_frame->nb_samples = dst_nb_samples;
        // 分配buffer
        av_frame_get_buffer(out_frame,0);
        av_frame_make_writable(out_frame);
    }
};

使用ffplay播放以下重采样后的PCM文件是否正常,播放命令是:

// -ar 表示采样率
// -ac 表示音频通道数
// -f 表示 pcm 格式,sample_fmts + le(小端)或者 be(大端)  f32le表示的是 AV_SAMPLE_FMT_FLTP 的小端模式
// sample_fmts可以通过ffplay -sample_fmts来查询
// -i 表示输入文件,这里就是 pcm 文件
ffplay -ar 44100 -ac 2 -f f32le -i pcm文件路径

原文链接  FFmpeg连载6-音频重采样 - 掘金 

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

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

相关文章

PHP项目添加分布式锁,这里是ThinkPHP8框架实现分布式锁

背景&#xff1a;公司旧项目&#xff0c;最初访问量不多&#xff0c;单机部署的。后来&#xff0c;访问量上来了&#xff0c;有阵子很卡&#xff0c;公司决定横向扩展&#xff0c;后端代码部署了三台服务器。部署调整后&#xff0c;有用户反馈&#xff0c;一个订单支付了三次。…

【机器学习入门】机器学习基础概念与原理

*&#xff08;本篇文章旨在帮助新手了解机器学习的基础概念和原理&#xff0c;不深入讨论算法及核心公式&#xff09; 目录 一、机器学习概念 1、什么是机器学习&#xff1f; 2、常见机器学习算法和模型 3、使用Python编程语言进行机器学习实践 4、机器学习的应用领域 二…

从零学Java 多线程的三个特性

多线程的三个特性 多线程要保证并发线程正确执行&#xff0c;必须要保证三个特性。 1 原子性&#xff08;互斥性&#xff09;&#xff1a; 一个或多个操作不能被分割&#xff0c;要么全部执行&#xff0c;要么就都不执行。 2 可见性&#xff1a; 多个线程访问同一个变量&a…

续航50年原子能电池揭秘

我国公司Betavolt最近宣布推出一款面向消费者市场的原子能电池&#xff0c;声称其使用寿命长达50年。首款采用该公司新型原子电池技术的产品名为Betavolt BV100&#xff0c;主要构造材料包括镍-63同位素和金刚石半导体材料。Betavolt表示&#xff0c;其核电池将主要应用于航空航…

【Linux技术专题】「夯实基本功系列」带你一同学习和实践操作Linux服务器必学的Shell指令(排查问题指令 - 下)

带你一同学习和实践操作Linux服务器必学的Shell指令 前提介绍more和less命令用法more命令命令格式命令参数 常用操作命令案例分析显示文件中从第3行起的内容将日志内容设置为每屏显示4行快速定位和显示文件中包含特定字符串结合管道和more命令来分页显示 less指令命令格式搜索指…

【C++进阶】心心念念的红黑树,它来了!

&#x1f466;个人主页&#xff1a;Weraphael ✍&#x1f3fb;作者简介&#xff1a;目前学习C和算法 ✈️专栏&#xff1a;C航路 &#x1f40b; 希望大家多多支持&#xff0c;咱一起进步&#xff01;&#x1f601; 如果文章对你有帮助的话 欢迎 评论&#x1f4ac; 点赞&#x1…

codesys【看门狗】

看门狗&#xff1a; 时间&#xff1a; 看门狗饿死时间。灵敏度&#xff1a;是看门狗时间的倍数。看门狗1秒&#xff0c;灵敏度5&#xff0c;结果就是5秒。Task类型&#xff1a; 循环&#xff1a;用于现场总线。惯性滑行&#xff1a;CPU空闲就运行&#xff0c;主程序用这个。 P…

【模板规范】会议纪要模板

文章目录 1、简介2、纪要模板2.1、表格类会议纪要2.2、文档类会议纪要2.3、简易版项目纪要 3、会议纪要3.1、作用3.2、特点3.2.1、工作会议纪要3.2.2、代表会议纪要3.2.3、座谈会议纪要3.2.4、联席会议纪要3.2.5、办公会议纪要3.2.6、汇报会议纪要3.2.7、技术鉴定会议纪要 3.3、…

HCS私有云简介

1、HCS简介和发展史 华为云产品&#xff1a;私有云和公有云&#xff0c;现在的私有云已经和公有云越来越像了FusionSphere是华为的一个品牌2016年&#xff0c;在5.0版本的时候&#xff0c;华为Openstack叫FusionSphere Openstack 5.0&#xff0c;底层用的是suse操作系统&#…

一天一个设计模式---组合模式

基本概念 组合模式是一种结构型设计模式&#xff0c;它允许客户端统一对待单个对象和对象的组合。组合模式通过将对象组织成树形结构&#xff0c;使得客户端可以一致地使用单个对象和组合对象。 主要角色&#xff1a; Component&#xff08;组件&#xff09;&#xff1a; 定…

Express安装与基础使用

一、express 介绍 express 是一个基于 Node.js 平台的极简、灵活的 WEB 应用开发框架&#xff0c; 官方网站&#xff1a; Express - 基于 Node.js 平台的 web 应用开发框架 - Express中文文档 | Express中文网 中文文档&#xff1a; 路由 - Express 中文文档 简单来说&am…

Vulnhub靶机:driftingblues 2

一、介绍 运行环境&#xff1a;Virtualbox 攻击机&#xff1a;kali&#xff08;10.0.2.15&#xff09; 靶机&#xff1a;driftingblues2&#xff08;10.0.2.18&#xff09; 目标&#xff1a;获取靶机root权限和flag 靶机下载地址&#xff1a;https://www.vulnhub.com/entr…

逸学Docker【java工程师基础】3.3Docker安装nacos

docker pull nacos/nacos-server docker network create nacos_network #创建容器网络 docker run -d \ --name nacos \ --privileged \ --cgroupns host \ --env JVM_XMX256m \ --env MODEstandalone \ --env JVM_XMS256m \ -p 8848:8848/tcp \ -p 9848:9848…

一文教你使用 ChatGPT API function calling

一文教你使用 ChatGPT API function calling Function call如何理解Function call如何调用&#xff1f; Function call 如何理解Function call 函式呼叫(function calling) 可说是这次ChatGPT API 更新的杀手级更新。所谓函式呼叫&#xff0c;就是让你把外部函式的形状写入Cha…

【Java IO分类】从传输方式和数据操作上理解 Java IO分类

Java IO - 分类 IO理解分类 - 从传输方式上字节流字符流字节流和字符流的区别字节转字符Input/OutputStreamReader/Writer IO理解分类 - 从数据操作上文件(file)数组([])管道操作基本数据类型缓冲操作打印对象序列化反序列化转换 IO理解分类 - 从传输方式上 从数据传输方式或者…

CSS3十大滤镜效果详解

滤镜效果 类似于美颜相机、美图秀秀这样的美颜工具&#xff0c;能够让我们轻松地应用多种特效&#xff0c;例如转换为黑白照片、复古风格化、调整亮度等。在之前仅凭CSS几乎很难做到这些效果。 但在CSS3的语法中&#xff0c;所有的这些视觉特效都是通过“filter”属性来快速实…

最全 chrome driver

超全chrome和driver的对应链接 https://raw.githubusercontent.com/GoogleChromeLabs/chrome-for-testing/main/data/latest-patch-versions-per-build-with-downloads.json 从这个链接中直接找对应的chrome和driver 复制链接下载。 注意最新版的在一些文章里的链接是找不到的…

论文笔记(四十)Goal-Auxiliary Actor-Critic for 6D Robotic Grasping with Point Clouds

Goal-Auxiliary Actor-Critic for 6D Robotic Grasping with Point Clouds 文章概括摘要1. 介绍2. 相关工作3. 学习 6D 抓握政策3.1 背景3.2 从点云抓取 6D 策略3.3 联合运动和抓握规划器的演示3.4 行为克隆和 DAGGER3.5 目标--辅助 DDPG3.6 对未知物体进行微调的后视目标 4. 实…

深度学习记录--偏差/方差(bias/variance)

误差问题 拟合神经网络函数过程中会出现两种误差&#xff1a;偏差(bias)和方差(variance) 偏差和误差的区别 欠拟合(underfitting) 当偏差(bias)过大时&#xff0c;如左图&#xff0c;拟合图像存在部分不符合值&#xff0c;称为欠拟合(underfitting) 过拟合(overfitting) …

软件测试|使用Python生成PDF文件

简介 PDF&#xff08;Portable Document Format&#xff09;是一种常用的文档格式&#xff0c;具有跨平台兼容性、保真性、安全性和交互性等特点。我们日常生活工作中的合同、报告、论文等通常都采用PDF格式&#xff0c;以确保文档在不同的操作系统&#xff08;例如 Windows、…