FFmpeg + SoundTouch實現音頻的變調變速


本文使用FFmpeg + SoundTouch實現將音頻解碼后,進行變調變速處理,並將處理后的結果保存為WAV文件。
主要有以下內容:

  • 實現一個FFmpeg的工具類,保存多媒體文件所需的解碼信息
  • 將解碼后的音頻保存為WAV文件
  • SoundTouch的使用指南

1.從視頻文件中提取音頻保存為WAV文件

本小節實現從視頻文件中提取音頻,解碼並保存為WAV文件。
在使用FFmpeg解碼時,一般的流程是:

  • 打開一個多媒體文件流
  • 得到媒體流信息
  • 查找視頻、音頻流的index
  • 根據流的index查找相應的的CODEC,打開AVCodecContext

進行完以上操作后,就得到解碼所需的各種信息:AVFormateContextAVCodecContext以及對應流的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方法,關閉打開的AVFormatContextAVCodecContext等。
  • 字段 為解碼所需的各類信息。

至於具體的實現,可參考前面的文章 ,在最后會提供本文使用的代碼,這里不再多說。

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_SAMPLESSOUNDTOUCH_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處理前,需要根據SoundTouchSAMPLETYPE進行相應的轉換。本文使用的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文件。結合前面的學習總結,可以很容易的實現音頻的變調變速播放。
本文中的使用的代碼:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM