FFmpeg连载6-音频重采样

at 1年前  ca FFmpeg  pv 730  by touch  

今天我们的实战内容是将音频解码成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的返回值才是真正输出的采样点数,这个返回值一定是小于或等于你希望输出的采样点数。

下面是完整代码:

#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文件路径

作者:思想觉悟

链接:https://juejin.cn/post/7090203150740619294

来源:稀土掘金


版权声明

本文仅代表作者观点,不代表码农殇立场。
本文系作者授权码农殇发表,未经许可,不得转载。

 

扫一扫在手机阅读、分享本文

已有0条评论
您是本站第12540名访客 今日有0篇新文章 当前在线 9 人