本文使用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文件。结合前面的学习总结,能够很容易的实现音频的变调变速播放。
本文中的使用的代码: