什麼叫音訊重取樣
音訊重取樣(Audio Resample):將音訊A轉換成音訊B,並且音訊A、B的引數(取樣率、取樣格式、聲道數)並不完全相同。比如:
-
音訊A的引數
- 取樣率:48000
- 取樣格式:f32le
- 聲道數:1
-
音訊B的引數
- 取樣率:44100
- 取樣格式:s16le
- 聲道數:2
為什麼需要音訊重取樣
這裡列舉一個音訊重取樣的經典用途。
有些音訊編碼器對輸入的原始PCM資料是有特定引數要求的,比如要求必須是44100_s16le_2。但是你提供的PCM引數可能是48000_f32le_1。這個時候就需要先將48000_f32le_1轉換成44100_s16le_2,然後再使用音訊編碼器對轉換後的PCM進行編碼。
命令列
通過下面的命令列可以將44100_s16le_2轉換成48000_f32le_1。
ffmpeg -ar 44100 -ac 2 -f s16le -i 44100_s16le_2.pcm -ar 48000 -ac 1 -f f32le 48000_f32le_1.pcm
程式設計
音訊重取樣需要用到2個庫:
- swresample
- avutil
函式宣告
為了讓音訊重取樣功能更加通用,設計成以下函式:
// 音訊引數
typedef struct {
const char *filename;
int sampleRate;
AVSampleFormat sampleFmt;
int chLayout;
} ResampleAudioSpec;
class FFmpegs {
public:
static void resampleAudio(ResampleAudioSpec &in,
ResampleAudioSpec &out);
static void resampleAudio(const char *inFilename,
int inSampleRate,
AVSampleFormat inSampleFmt,
int inChLayout,
const char *outFilename,
int outSampleRate,
AVSampleFormat outSampleFmt,
int outChLayout);
};
// 匯入標頭檔案
extern "C" {
#include <libswresample/swresample.h>
#include <libavutil/avutil.h>
}
// 處理錯誤碼
#define ERROR_BUF(ret) \
char errbuf[1024]; \
av_strerror(ret, errbuf, sizeof (errbuf));
void FFmpegs::resampleAudio(ResampleAudioSpec &in,
ResampleAudioSpec &out) {
resampleAudio(in.filename, in.sampleRate, in.sampleFmt, in.chLayout,
out.filename, out.sampleRate, out.sampleFmt, out.chLayout);
}
函式呼叫
// 輸入引數
ResampleAudioSpec in;
in.filename = "F:/44100_s16le_2.pcm";
in.sampleFmt = AV_SAMPLE_FMT_S16;
in.sampleRate = 44100;
in.chLayout = AV_CH_LAYOUT_STEREO;
// 輸出引數
ResampleAudioSpec out;
out.filename = "F:/48000_f32le_1.pcm";
out.sampleFmt = AV_SAMPLE_FMT_FLT;
out.sampleRate = 48000;
out.chLayout = AV_CH_LAYOUT_MONO;
// 進行音訊重取樣
FFmpegs::resampleAudio(in, out);
函式實現
變數定義
為了簡化釋放資源的程式碼,函式中用到了goto語句,所以把需要用到的變數都定義到了前面。
// 檔名
QFile inFile(inFilename);
QFile outFile(outFilename);
// 輸入緩衝區
// 指向緩衝區的指標
uint8_t **inData = nullptr;
// 緩衝區的大小
int inLinesize = 0;
// 聲道數
int inChs = av_get_channel_layout_nb_channels(inChLayout);
// 一個樣本的大小
int inBytesPerSample = inChs * av_get_bytes_per_sample(inSampleFmt);
// 緩衝區的樣本數量
int inSamples = 1024;
// 讀取檔案資料的大小
int len = 0;
// 輸出緩衝區
// 指向緩衝區的指標
uint8_t **outData = nullptr;
// 緩衝區的大小
int outLinesize = 0;
// 聲道數
int outChs = av_get_channel_layout_nb_channels(outChLayout);
// 一個樣本的大小
int outBytesPerSample = outChs * av_get_bytes_per_sample(outSampleFmt);
// 緩衝區的樣本數量(AV_ROUND_UP是向上取整)
int outSamples = av_rescale_rnd(outSampleRate, inSamples, inSampleRate, AV_ROUND_UP);
/*
inSampleRate inSamples
------------- = -----------
outSampleRate outSamples
outSamples = outSampleRate * inSamples / inSampleRate
*/
// 返回結果
int ret = 0;
建立重取樣上下文
// 建立重取樣上下文
SwrContext *ctx = swr_alloc_set_opts(nullptr,
// 輸出引數
outChLayout, outSampleFmt, outSampleRate,
// 輸入引數
inChLayout, inSampleFmt, inSampleRate,
0, nullptr);
if (!ctx) {
qDebug() << "swr_alloc_set_opts error";
goto end;
}
初始化重取樣上下文
// 初始化重取樣上下文
int ret = swr_init(ctx);
if (ret < 0) {
ERROR_BUF(ret);
qDebug() << "swr_init error:" << errbuf;
goto end;
}
建立緩衝區
// 建立輸入緩衝區
ret = av_samples_alloc_array_and_samples(
&inData,
&inLinesize,
inChs,
inSamples,
inSampleFmt,
1);
if (ret < 0) {
ERROR_BUF(ret);
qDebug() << "av_samples_alloc_array_and_samples error:" << errbuf;
goto end;
}
// 建立輸出緩衝區
ret = av_samples_alloc_array_and_samples(
&outData,
&outLinesize,
outChs,
outSamples,
outSampleFmt,
1);
if (ret < 0) {
ERROR_BUF(ret);
qDebug() << "av_samples_alloc_array_and_samples error:" << errbuf;
goto end;
}
讀取檔案資料
// 開啟檔案
if (!inFile.open(QFile::ReadOnly)) {
qDebug() << "file open error:" << inFilename;
goto end;
}
if (!outFile.open(QFile::WriteOnly)) {
qDebug() << "file open error:" << outFilename;
goto end;
}
// 讀取檔案資料
// inData[0] == *inData
while ((len = inFile.read((char *) inData[0], inLinesize)) > 0) {
// 讀取的樣本數量
inSamples = len / inBytesPerSample;
// 重取樣(返回值轉換後的樣本數量)
ret = swr_convert(ctx,
outData, outSamples,
(const uint8_t **) inData, inSamples
);
if (ret < 0) {
ERROR_BUF(ret);
qDebug() << "swr_convert error:" << errbuf;
goto end;
}
// 將轉換後的資料寫入到輸出檔案中
// outData[0] == *outData
outFile.write((char *) outData[0], ret * outBytesPerSample);
}
重新整理輸出緩衝區
// 檢查一下輸出緩衝區是否還有殘留的樣本(已經重取樣過的,轉換過的)
while ((ret = swr_convert(ctx,
outData, outSamples,
nullptr, 0)) > 0) {
outFile.write((char *) outData[0], ret * outBytesPerSample);
}
回收釋放資源
end:
// 釋放資源
// 關閉檔案
inFile.close();
outFile.close();
// 釋放輸入緩衝區
if (inData) {
av_freep(&inData[0]);
}
av_freep(&inData);
// 釋放輸出緩衝區
if (outData) {
av_freep(&outData[0]);
}
av_freep(&outData);
// 釋放重取樣上下文
swr_free(&ctx);