使用FFmpeg进行音频重采样

46 阅读5分钟

音频重采样说明

音频重采样(Audio Resample):将音频A转换成音频B,并且音频A、B的参数(采样率、采样格式、声道数)并不完全相同。比如:

  • 音频A的参数

    • 采样率:48000
    • 采样格式:s32le
    • 声道数:2
  • 音频B的参数

    • 采样率:44100
    • 采样格式:f16le
    • 声道数:1

创建一个Qt项目

修改CMakeLists.txt

添加FFmpeg头文件和库文件路径

set(ffmpeg444 "../ffmpeg444")

# 引入头文件目录
include_directories(${ffmpeg444}/include/)
# 引入库文件目录,注意link_directories要放在add_executable之前
link_directories(${ffmpeg444}/lib/)

target_link_libraries(audio_resample PRIVATE Qt${QT_VERSION_MAJOR}::Widgets swresample avutil)

添加按钮

image.png

创建一个线程类

头文件

#ifndef AUDIOTHREAD_H
#define AUDIOTHREAD_H

#include <QThread>

class AudioThread: public QThread
{
    Q_OBJECT
private:
    void run();
public:
    explicit AudioThread(QObject *parent = nullptr);
    ~AudioThread();
};

#endif // AUDIOTHREAD_H

实现文件

#include "audiothread.h"

#include <QDebug>
#include <QFile>

extern "C" {
#include <libswresample/swresample.h>
#include <libavutil/avutil.h>
}

AudioThread::AudioThread(QObject *parent): QThread{parent} {
    connect(this, &AudioThread::finished, this, &AudioThread::deleteLater);
}

AudioThread::~AudioThread() {
    disconnect();
    requestInterruption();
    quit();
    wait();
    qDebug() << this << "析构了";
}


void AudioThread::run() {
    const char* inFileName = "/home/gillbert/Music/in.pcm";
    const char* outFileName = "/home/gillbert/Music/out.pcm";

    // 输入参数
    AVSampleFormat inSampleFmt = AV_SAMPLE_FMT_S16;
    int inSampleRate = 44100;
    int inChLayout = AV_CH_LAYOUT_STEREO;

    // 输出参数
    AVSampleFormat outSampleFmt = AV_SAMPLE_FMT_FLT;
    int outSampleRate = 44100;
    int outChLayout = AV_CH_LAYOUT_MONO;


    // 创建重采样上下文
    SwrContext* ctx = swr_alloc_set_opts(nullptr,
                                         outChLayout,
                                         outSampleFmt,
                                         outSampleRate,
                                         inChLayout,
                                         inSampleFmt,
                                         inSampleRate,
                                         0,
                                         nullptr);

    if (!ctx) {
        qDebug() << "swr_alloc_set_opts create ctx error";
        return;
    }

    // 初始化重采样上下文
    int ret = swr_init(ctx);

    if (ret < 0) {
        char errbuf[1024];
        av_strerror(ret, errbuf, sizeof(errbuf));
        qDebug() << "swr_init error:" << errbuf;


        // 释放重采样上下文
        swr_free(&ctx);

        return;
    }


    // 创建输入缓冲区
    uint8_t **inData = nullptr;  // 指向缓冲区的指针
    int inLineSize = 0;          // 缓冲区的大小
    int inChs = av_get_channel_layout_nb_channels(inChLayout);  // av_get_channel_layout_nb_channels 函数可以根据声道布局获取声道数
    int inSample = 1024;      // 输入缓冲区的样本数

    int inBytesPerSample = inChs * av_get_bytes_per_sample(inSampleFmt);  // 一个样本占用多少字节

    ret = av_samples_alloc_array_and_samples(&inData, &inLineSize, inChs, inSample, inSampleFmt, 1);  // 创建缓冲区

    if (ret < 0) {
        char errbuf[1024];
        av_strerror(ret, errbuf, sizeof(errbuf));
        qDebug() << "av_samples_alloc_array_and_samples error:" << errbuf;

        // 释放重采样上下文
        swr_free(&ctx);

        return;
    }

    // 创建输出缓冲区
    uint8_t **outData = nullptr;  // 指向缓冲区的指针
    int outLineSize = 0;          // 缓冲区的大小
    int outChs = av_get_channel_layout_nb_channels(outChLayout);  // av_get_channel_layout_nb_channels 函数可以根据声道布局获取声道数
    int outSample = av_rescale_rnd(outSampleRate, inSample, inSampleRate, AV_ROUND_UP);      // 输入缓冲区的样本数,因为重采样了,需要重新计算下
    int outBytesPerSample = outChs * av_get_bytes_per_sample(outSampleFmt);  // 一个样本占用多少字节
    ret = av_samples_alloc_array_and_samples(
        &outData,
        &outLineSize,
        outChs,
        outSample,
        outSampleFmt,
        1  // 1表示不需要内存对齐
        );  // 创建缓冲区

    if (ret < 0) {
        char errbuf[1024];
        av_strerror(ret, errbuf, sizeof(errbuf));
        qDebug() << "av_samples_alloc_array_and_samples error:" << errbuf;

        // 释放输入缓冲区
        av_freep(&inData);

        // 释放重采样上下文
        swr_free(&ctx);

        return;
    }

    // 读取数据进行重采样
    QFile inFile(inFileName);  // 打开输入文件
    if (!inFile.open(QFile::ReadOnly)) {
        qDebug() << "file open error: " << inFileName;

        // 释放输出缓冲区
        av_freep(&outData);

        // 释放输入缓冲区
        av_freep(&inData);

        // 释放重采样上下文
        swr_free(&ctx);

        return;
    }

    QFile outFile(outFileName);  // 打开输入文件
    if (!outFile.open(QFile::WriteOnly)) {
        qDebug() << "file open error: " << outFileName;

        // 释放输出缓冲区
        av_freep(&outData);

        // 释放输入缓冲区
        av_freep(&inData);

        // 释放重采样上下文
        swr_free(&ctx);

        return;
    }

    int len = 0;  // 读取的文件数据大小
    while ((len = inFile.read((char *)inData[0], inLineSize)) > 0) {
        // 读取的样本数量
        int readInSample = len / inBytesPerSample;
        // 重采样, 返回值为输出缓冲区中的样本数量
        ret = swr_convert(ctx, outData, outSample, (const uint8_t **)inData, readInSample);

        if (ret < 0) {
            char errbuf[1024];
            av_strerror(ret, errbuf, sizeof(errbuf));
            qDebug() << "swr_convert error:" << errbuf;

            if (outData) {
                av_freep(&outData[0]);
            }

            // 释放输出缓冲区
            av_freep(&outData);

            if (inData) {
                av_freep(&inData[0]);
            }

            // 释放输入缓冲区
            av_freep(&inData);

            // 释放重采样上下文
            swr_free(&ctx);

            return;
        }

        // 将转换后的数据写入文件
        outFile.write((char *)outData[0], ret * outBytesPerSample);


    }

    // 预防输出缓冲区还有残留
    while ((ret = swr_convert(ctx, outData, outSample, nullptr, 0)) > 0) {
      outFile.write((char *)outData[0], ret * outBytesPerSample);
    }


    // 关闭文件
    outFile.close();
    inFile.close();

    if (outData) {
        av_freep(&outData[0]);
    }

    // 释放输出缓冲区
    av_freep(&outData);

    if (inData) {
        av_freep(&inData[0]);
    }

    // 释放输入缓冲区
    av_freep(&inData);

    // 释放重采样上下文
    swr_free(&ctx);


}


点击的槽函数代码

点击按钮,创建新的线程开始重采样进行转换


void MainWindow::on_resampleButton_clicked()
{
    _audioThread = new AudioThread(this);
    _audioThread->start();
}

image.png

测试

查看PCM对应的格式

在代码中我们的输出文件采样格式为AV_SAMPLE_FMT_FLT, 因此在使用命令行进行播放时候我们需要指定-f f32le参数

➜   ffmpeg -hide_banner -formats | grep PCM
 DE alaw            PCM A-law
 DE f32be           PCM 32-bit floating-point big-endian
 DE f32le           PCM 32-bit floating-point little-endian
 DE f64be           PCM 64-bit floating-point big-endian
 DE f64le           PCM 64-bit floating-point little-endian
 DE mulaw           PCM mu-law
 DE s16be           PCM signed 16-bit big-endian
 DE s16le           PCM signed 16-bit little-endian
 DE s24be           PCM signed 24-bit big-endian
 DE s24le           PCM signed 24-bit little-endian
 DE s32be           PCM signed 32-bit big-endian
 DE s32le           PCM signed 32-bit little-endian
 DE s8              PCM signed 8-bit
 DE u16be           PCM unsigned 16-bit big-endian
 DE u16le           PCM unsigned 16-bit little-endian
 DE u24be           PCM unsigned 24-bit big-endian
 DE u24le           PCM unsigned 24-bit little-endian
 DE u32be           PCM unsigned 32-bit big-endian
 DE u32le           PCM unsigned 32-bit little-endian
 DE u8              PCM unsigned 8-bit
 DE vidc            PCM Archimedes VIDC

播放

ffplay -ar 44100 -ac 1 -f f32le  out.pcm