乐趣区

关于c++:FFmpeg连载6音频重采样

明天咱们的实战内容是将音频解码成 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 文件门路

系列举荐

FFmpeg 连载 1 - 开发环境搭建
FFmpeg 连载 2 - 拆散视频和音频
FFmpeg 连载 3 - 视频解码
FFmpeg 连载 4 - 音频解码
FFmpeg 连载 5 - 音视频编码

关注我,一起提高,人生不止 coding!!!

退出移动版