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

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

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

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

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

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

進行完以上操做後,就獲得解碼所需的各類信息:AVFormateContextAVCodecContext以及對應流的index。也就說,這些數據是解碼多媒體流的必須信息,因此這裏對上述操做作一個封裝,提供一個單一接口來獲取解碼所需的信息。git

1.1 MediaInfo工具類

在使用FFmpeg進行解碼的時候,所須要的信息以下:github

  • 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等。
  • 字段 爲解碼所需的各種信息。

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

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文件。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文件讀寫

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文件。結合前面的學習總結,能夠很容易的實現音頻的變調變速播放。
本文中的使用的代碼:

相關文章
相關標籤/搜索