本文使用FFmpeg + SoundTouch實現將音頻解碼后,進行變調變速處理,並將處理后的結果保存為WAV文件。
主要有以下內容:
- 實現一個FFmpeg的工具類,保存多媒體文件所需的解碼信息
- 將解碼后的音頻保存為WAV文件
- SoundTouch的使用指南
1.從視頻文件中提取音頻保存為WAV文件
本小節實現從視頻文件中提取音頻,解碼並保存為WAV文件。
在使用FFmpeg解碼時,一般的流程是:
- 打開一個多媒體文件流
- 得到媒體流信息
- 查找視頻、音頻流的index
- 根據流的index查找相應的的CODEC,打開
AVCodecContext
進行完以上操作后,就得到解碼所需的各種信息:AVFormateContext
、AVCodecContext
以及對應流的index。也就說,這些數據是解碼多媒體流的必須信息,所以這里對上述操作做一個封裝,提供一個單一接口來獲取解碼所需的信息。
1.1 MediaInfo工具類
在使用FFmpeg進行解碼的時候,所需要的信息如下:
AVFormatContext
AVCodecContext
- 流的index
MediaInfo
的聲明如下:
class CMediaInfo
{
public:
CMediaInfo();
CMediaInfo(MEDIA_TYPE media);
~CMediaInfo();
public:
ERROR_TYPE open(const char *filename);
void close();
void error_message(ERROR_TYPE error);
public:
MEDIA_TYPE type;
AVFormatContext *pFormatContext;
AVCodecContext *pVideo_codec_context;
AVCodecContext *pAudio_codec_context;
int video_stream_index;
int audio_stream_index;
};
- 構造函數需要一個參數,指出該類中包含的信息為視頻、音頻或者音視頻都包含;
open
方法,根據傳入的多媒體文件填充各個字段信息;close
方法,關閉打開的AVFormatContext
和AVCodecContext
等。- 字段 為解碼所需的各類信息。
至於具體的實現,可參考前面的文章 ,在最后會提供本文使用的代碼,這里不再多說。
1.2 從視頻中提取音頻
1.2.1 獲取解碼所需的信息
使用上面的提供的MediaInfo
工具類,首先根據視頻文件路徑填充MediaInfo
的各個字段
char* filename = "E:\\Wildlife.wmv";
CMediaInfo media(MEDIA_TYPE::AUDIO);
media.open(filename);
1.2.2 設置音頻的保存格式
在真正的提取解碼之前,需要首先設置好要保存的WAV的音頻格式。FFmpeg使用SwrContext
設置音頻的轉換格式,具體代碼如下:
AVSampleFormat dst_format = AV_SAMPLE_FMT_S16;
uint8_t dst_channels = 2;
auto dst_layout = av_get_default_channel_layout(dst_channels);
auto audio_ctx = media.pAudio_codec_context;
if (audio_ctx->channel_layout <= 0)
audio_ctx->channel_layout = av_get_default_channel_layout(audio_ctx->channels);
SwrContext *swr_ctx = swr_alloc();
swr_alloc_set_opts(swr_ctx, dst_layout, dst_format, audio_ctx->sample_rate,
audio_ctx->channel_layout, audio_ctx->sample_fmt, audio_ctx->sample_rate, 0, nullptr);
if (!swr_ctx || swr_init(swr_ctx))
return -1;
這里設置音頻的sample格式為16位的有符號整數,通道數為2通道,采樣率不變,具體關於音頻格式的轉換可參考:FFmpeg學習4:音頻格式轉換。
1.2.3 解碼,並保存為WAV文件
使用MediaInfo
獲取到關於解碼的相關信息,並且設置好格式轉換需要的SwrContext
,然后調用av_read_frame
從流中讀取packet,解碼。最后將解碼后的數據進行格式轉換后,將轉換后的數據寫入WAV文件。
int pcm_data_size = 0;
while (av_read_frame(media.pFormatContext, packet) >= 0)
{
if (packet->stream_index == media.audio_stream_index)
{
auto ret = avcodec_send_packet(media.pAudio_codec_context, packet);
if (ret < 0 && ret != AVERROR(EAGAIN) && ret != AVERROR_EOF)
return -1;
ret = avcodec_receive_frame(media.pAudio_codec_context, frame);
if (ret < 0 && ret != AVERROR_EOF)
return -1;
auto nb = swr_convert(swr_ctx, &buffer, 192000, (const uint8_t **)frame->data, frame->nb_samples);
auto length = nb * dst_channels * av_get_bytes_per_sample(dst_format);
ofs.write((char*)buffer, length);
pcm_data_size += length;
}
}
在寫入文件的時候要使用二進制的方式,並且要記錄好寫入的音頻的數據的字節數,在最后寫WAV文件頭的時候需要。
寫入WAV文件頭
// 寫Wav文件頭
Wave_header header(dst_channels, audio_ctx->sample_rate, av_get_bytes_per_sample(dst_format) * 8);
header.data->cb_size = ((pcm_data_size + 1) / 2) * 2;
header.riff->cb_size = 4 + 4 + header.fmt->cb_size + 4 + 4 + header.data->cb_size + 4;
ofs.seekp(0, ios::beg);
CWaveFile::write_header(ofs, header);
首先將音頻的PCM數據寫入文件,然后根據PCM數據的長度填充WAV文件頭的相關字段。具體關於WAV的文件格式及其讀寫方法可參考RIFF和WAVE音頻文件格式和C++標准庫實現WAV文件讀寫
2.SoundTouch使用指南
SoundTouch 是一個開源的音頻庫,主要有以下功能:
- 變速不變調(TSM,Time Scale Modification),改變音頻的播放速度(快或者慢)同時不影響音頻的聲調(Pitch)。
- 變調不變速 Pitch Shifting ,改變音頻聲調的同時保持音頻的播放速度不變
- 變調變速,同時改變音頻的聲調和速度
2.1 編譯
從SoundTouch下載源代碼,解壓后在README.html中給出了具體的編譯方法,在Windows下有兩種方法來編譯源代碼:
- 執行解壓文件夾下面的make-win.bat腳本。試過這種方法沒有成功,看了下make-win.bat腳本的內容,應該是沒有找到相關的環境變量(VS2008)。該腳本主要是執行下面命令
devenv source\SoundStretch\SoundStretch.vcproj /upgrade
devenv source\SoundStretch\SoundStretch.vcproj /build debug
devenv source\SoundStretch\SoundStretch.vcproj /build release
devenv source\SoundStretch\SoundStretch.vcproj /build releasex64
- 使用Visudl Studio IDE來編譯,打開source\Soundtouch下面的SoundTouch.sln,然后編譯即可。SoundTouch.sln編譯出來的是靜態鏈接庫,使用VS版本為Visual Studio 2008。
對編譯后庫的使用需要注意以下兩點:
- VS2008編譯出來的靜態鏈接庫在VS2013調用會出現問題,提示ERROR LINK2019錯誤找不到相關的符號。
- 在source目錄下有個SoundTouchDLL項目,一看名字就是編譯動態鏈接庫dll的。編譯,配置相應的參數(dll,lib),然后實例化
SoundTouch s_touch
。這時候又會提示ERROR LINK2019,一直以為是環境沒有配置好,找不到相應的dll文件。結果,是動態鏈接庫dll的導出的不是整個SoundTouch
類,只是其中的一些方法。
/// Sets new rate control value. Normal rate = 1.0, smaller values
/// represent slower rate, larger faster rates.
SOUNDTOUCHDLL_API void __cdecl soundtouch_setRate(HANDLE h, float newRate);
/// Sets new tempo control value. Normal tempo = 1.0, smaller values
/// represent slower tempo, larger faster tempo.
SOUNDTOUCHDLL_API void __cdecl soundtouch_setTempo(HANDLE h, float newTempo);
/// Sets new rate control value as a difference in percents compared
/// to the original rate (-50 .. +100 %);
SOUNDTOUCHDLL_API void __cdecl soundtouch_setRateChange(HANDLE h, float newRate);
后來,看了下Android的示例,這個動態鏈接庫導出的函數應該是提供給Android使用的API。
2.2 使用
得到編譯后的靜態鏈接庫后,SoundTouch的使用還是很簡單的,其外部API封裝在了類SoundTouch
中。在使用的時候只需要下面三個步驟:
- 實例話
SoundTouch
類 - 設置相關的參數(速度,音調的改變)
- 調用
putSamples
方法傳入處理的Audio Sample;調用receiveSamples
接收處理后的Sample。 - 在處理完成后,調用
soundtouch.fflush()
接收管道內余下的sample
使用實例如下:
////////////////////////////////////////////////////////////////////
// 1. 設置SoundTouch,配置變調變速參數
soundtouch::SoundTouch s_touch;
s_touch.setSampleRate(audio_ctx->sample_rate); // 設置采樣率
s_touch.setChannels(audio_ctx->channels); // 設置通道數
////////////////////////////////////////////
// 2. 設置 rate或者pitch的改變參數
//s_touch.setRate(0.5); // 設置速度為0.5,原始的為1.0
s_touch.setRateChange(-50.0);
//////////////////////////////////////////////////////////////
// 3. 傳入sample,並接收處理后的sample
// 將解碼后的buffer(uint8*)轉換為soundtouch::SAMPLETYPE,也就是singed int 16
auto len = nb * dst_channels * av_get_bytes_per_sample(dst_format);
for (auto i = 0; i < len; i++)
{
touch_buffer[i] = (buffer[i * 2] | (buffer[i * 2 + 1] << 8));
}
// 傳入Sample
s_touch.putSamples(touch_buffer, nb);
do
{
// 接收處理后的sample
nb = s_touch.receiveSamples(touch_buffer, 96000);
auto length = nb * dst_channels * av_get_bytes_per_sample(dst_format);
ofs.write((char*)touch_buffer, length);
pcm_data_size += length;
} while (nb != 0);
///////////////////////////////////////////////
// 4. 接收管道內余下的處理后數據
s_touch.flush();
int nSamples;
do
{
nSamples = s_touch.receiveSamples(touch_buffer, 96000);
auto length = nSamples * dst_channels * av_get_bytes_per_sample(dst_format);
ofs.write((char*)touch_buffer, length);
pcm_data_size += length;
} while (nSamples != 0);
SoundTouch內部使用通道的方式來管理sample數據,所以在主循環接收好,要接收管道內剩余的sample。
使用的時候需要注意以下幾點
- sample的類型。SoundTouch支持兩種類型sample類型:16位有符號整數和32位浮點數,默認使用的是32為浮點數。其sample類型在頭文件
STTypes.h
中聲明為SAMPLETYPE
。在該文件的開始位置,使用宏SOUNDTOUCH_INTEGER_SAMPLES
和SOUNDTOUCH_FLOAT_SAMPLES
來決定使用那種sample類型。
#define SOUNDTOUCH_INTEGER_SAMPLES 1 //< 16bit integer samples
//#define SOUNDTOUCH_FLOAT_SAMPLES 1 //< 32bit float samples
另外,為了防止計算時有溢出,也支持32為有符號整數和64位浮點數,其類型為LONG_SAMPLETYPE
。
- 速度和pitch參數的設置
-
變調不變速
setPitch(double newPitch)
源pitch = 1.0,小於1音調變低;大於1音調變高setPitchOctaves(double newPitch)
在源pitch的基礎上,使用八度音(Octave)設置新的pitch [-1.00, 1.00]。setPitchSemiTones(double or int newPitch)
在源pitch的基礎上,使用半音(Semitones)設置新的pitch [-12.0,12.0]
-
變速不變調
setRate(double newRate)
設置新的rate,源rate=1.0,小於1變慢;大於1變快setRateChange(double newRate)
在源rate的基礎上,以百分比設置新的rate[-50,100]setTempo(double newTempo)
設置新的節拍tempo,源tempo=1.0,小於1則變慢;大於1變快setTempoChange(double newTempo)
在源tempo的基礎上,以百分比設置新的tempo[-50,100]
-
3. FFmpeg + SoundTouch 變調、變速
有了前面的實現,只需要在FFmepg解碼后,將解碼后的數據發送到SoundTouch
中進行處理即可。有一點需要注意,FFmpeg解碼后的數據存放在類型為uint8
的緩存中,在將sample發送給SoundTouch
處理前,需要根據SoundTouch
的SAMPLETYPE進行相應的轉換。本文使用的SAMPLETYPE的是S16,首先將uint8
兩個字節組合一個S16(小端)
// 將解碼后的buffer(uint8*)轉換為soundtouch::SAMPLETYPE,也就是singed int 16
auto len = nb * dst_channels * av_get_bytes_per_sample(dst_format);
for (auto i = 0; i < len; i++)
{
touch_buffer[i] = (buffer[i * 2] | (buffer[i * 2 + 1] << 8));
}
首先計算緩存中的字節數,然后按照小端的方式組合為16為有符號整數。然后將轉換后的buffer傳送給SoundTouch
即可。
s_touch.putSamples(touch_buffer, nb);
do
{
// 接收處理后的sample
nb = s_touch.receiveSamples(touch_buffer, 96000);
auto length = nb * dst_channels * av_get_bytes_per_sample(dst_format);
ofs.write((char*)touch_buffer, length);
pcm_data_size += length;
} while (nb != 0);
變調變速的處理結果如下圖:
頻譜圖,上圖為原始音頻的頻譜;下圖為使用setPitch(0.1)
將pitch設為原始的10%得到的頻譜圖
波形圖,上圖為原始的波形圖;下圖為使用setRateChange(-50.0)
設置速度減少50%得到的波形圖
4. 總結
本文使用FFmepg + SoundTouch相結合的方式,將音頻從視頻從提取出來,進行變調變速處理后保存為WAV文件。結合前面的學習總結,可以很容易的實現音頻的變調變速播放。
本文中的使用的代碼:
- CSDN下載 SoundTouch VS2013 Project 官網下的為VS2008版本,編譯出來的靜態鏈接庫在VS2013使用一直出現LINK2019錯誤。
- CSDN 下載FFmpeg + SoundTouch 變速變調 需要配置FFmpeg的開發環境
- Github 學習過程使用的代碼庫。