本文使用FFmpeg + SoundTouch實現將音頻解碼後,進行變調變速處理,並將處理後的結果保存爲WAV文件。
主要有如下內容:html
本小節實現從視頻文件中提取音頻,解碼並保存爲WAV文件。
在使用FFmpeg解碼時,通常的流程是:ios
AVCodecContext
進行完以上操做後,就獲得解碼所需的各類信息:AVFormateContext
、AVCodecContext
以及對應流的index。也就說,這些數據是解碼多媒體流的必須信息,因此這裏對上述操做作一個封裝,提供一個單一接口來獲取解碼所需的信息。git
在使用FFmpeg進行解碼的時候,所須要的信息以下:github
AVFormatContext
AVCodecContext
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
等。至於具體的實現,可參考前面的文章 ,在最後會提供本文使用的代碼,這裏再也不多說。ide
使用上面的提供的MediaInfo
工具類,首先根據視頻文件路徑填充MediaInfo
的各個字段函數
char* filename = "E:\\Wildlife.wmv"; CMediaInfo media(MEDIA_TYPE::AUDIO); media.open(filename);
在真正的提取解碼以前,須要首先設置好要保存的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:音頻格式轉換。學習
使用MediaInfo
獲取到關於解碼的相關信息,而且設置好格式轉換須要的SwrContext
,而後調用av_read_frame
從流中讀取packet,解碼。最後將解碼後的數據進行格式轉換後,將轉換後的數據寫入WAV文件。ui
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文件讀寫
SoundTouch 是一個開源的音頻庫,主要有如下功能:
從SoundTouch下載源代碼,解壓後在README.html中給出了具體的編譯方法,在Windows下有兩種方法來編譯源代碼:
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
對編譯後庫的使用須要注意如下兩點:
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。
獲得編譯後的靜態連接庫後,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。
使用的時候須要注意如下幾點
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
。
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]有了前面的實現,只須要在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%獲得的波形圖
本文使用FFmepg + SoundTouch相結合的方式,將音頻從視頻從提取出來,進行變調變速處理後保存爲WAV文件。結合前面的學習總結,能夠很容易的實現音頻的變調變速播放。
本文中的使用的代碼: