音视频系列九 使用soundTouch实现音视频变速

前言

视频变速

视频比较简单,直接控制视频帧的显示时间就行了,减速就延长显示时间,加速就缩减显示时间。问题就是如果加速过快解码速度跟不上的话就会出现音视频不同步(这个貌似无解,除非限制速度强制音频同步视频,毕竟机器性能是有极限的[大笑]。顺便吐槽一下qq浏览器的视频加速到不同步后好像就恢复不过来了,只能seek同步一下。)

音频变速

音频的话就麻烦了一点,我曾天真的以为固定播放器的采样率,改变音频的采样率就能实现音频变速。变速是实现变速了,但是想象一下一个直径一米的半圆,y轴不变,x轴乘二,他在一米里面会是怎样的,在两米里又是怎样的。可见单纯的改变采样率会导致音调发生改变,失去声音原来的形状。
下面简单介绍下相关算法(个人理解),想具体深入了解的另行查找资料(建议认真读一下第一个参考链接的论文),另外如果有错漏还望指出。

TSM算法(时域压扩算法)

变速不变调的经典算法叫TSM(Time-Scale Modifacaiton)
主要流程是分帧,合帧。分帧一来是无法处理无限长信号,二来是音频在短时域上是周期性的。帧长度一般为20ms~50ms。分帧操作中帧与帧之间会有重叠(overlap),也就是每一帧的起点间隔不等于帧长度(两帧的起点间隔称为帧移,比如重叠75%,那帧移就是帧长度的25%)。合帧就是把分出来的帧重新连接还原成连续信号,而TSM的基本原理就是固定合帧时的重叠比例(一般是50%或者75%),改变分帧时的重叠比例从而实现时域压扩。如下图。
TSM
但是会造成一个问题就是两帧连接处波形不连续(基音断裂),如下图。
在这里插入图片描述

OLA(Overlap-and-Add)

OLA(Overlap-and-Add, OLA)重叠相加算是音频变速算法中最简单的时域方法,它是后续时域算法(SOLA, SOLA-FS, TD-PSOLA, WSOLA)的基础。OLA算法通过加窗解决波形不连续问题。
加窗可以让一帧信号的幅度在两端渐变到 0,获得周期性信号,减轻频谱泄漏。加窗有矩形窗,汉宁窗(Hanning),海明窗(Hamming)等,一般应用海明窗。一段信号经过海明窗处理如下。
加窗处理
加窗就是信号与一个「窗函数」相乘,从上图可以看到信号两边渐变到0,呈现周期性,方便傅里叶变换。
OLA
OLA流程如上图,x是原信号,y是处理(加速)后的信号, X m X_{m} Xm是第一帧,红色w是窗函数,图(b)就是第一帧加窗, X m + 1 X_{m+1} Xm+1就是第二帧, H a H_{a} Ha就是分帧的帧移,图(d)就是第二帧加窗然后与第一帧叠加, H S H_{S} HS就是合帧的帧移。两帧之间的信号被忽略掉了,从而实现了加速。OLA算法通过加窗然后重叠部分相加解决了波形不连续的问题,但是无法保证每一个帧都能覆盖完整周期并保证其相位对齐,这种失真也叫相位跳跃失真(phase jump artifacts),如下图,原本周期性的信号变得不周期了。
在这里插入图片描述

WSOLA(Waveform Similarity Overlap-Add)

相似帧叠加算法,是OLA算法的改进版,不是直接暴力叠加帧,而是在一定范围内寻找最相似的帧叠加。
在这里插入图片描述
如上图,蓝色虚线框是正常不变速处理时的第二个分帧, X m + 1 X_{m+1} Xm+1就是压扩(加速)处理时第二帧,蓝色实线框就是范围,在这个范围内找与蓝色虚线框最相似的一帧,图(c)中x信号上的第二个红色实线框就是在范围内找到的最相似帧。图(d)就是相似帧加窗然后与第一帧重叠相加。
WSOLA通过寻找相似帧解决了信号失真问题,WSOLA处理结果如下图,减速后的信号与原信号一致。
在这里插入图片描述
WSOLA的应用主要有两个开源库,一个是sonic(使用定位基音周期法寻找相似帧),一个是soundTouch(使用寻找相关峰法寻找相似帧)。sonic在处理纯人声时效果比较好,soundTouch在处理环境等综合性声音时效果比较好,不是特殊要求的人声处理一般采用soundTouch。
算法的介绍到此为止。
顺口提一句音频变调就是通过改变重采样实现变调变速,然后再通过上面的变速算法恢复速度。

编译

源码地址
我编译的时候是基于2.3.1版本的。下载源码下来后把source/SoundTouch目录下的所有文件复制到工程的cpp/soundtouch目录下,如下
在这里插入图片描述
将include文件夹下的所有文件复制到工程的include/soundtouch目录下,如下
在这里插入图片描述
然后将上面两步复制过来的源文件和头文件配置进CMakeLists.txt里,关键部分示例如下

aux_source_directory(. DIR_SRC)
aux_source_directory(./soundtouch DIR_SRC_SOUND_TOUCH)
add_library(
        native-lib
        SHARED
        ${
    
    DIR_SRC} ${
    
    DIR_SRC_SOUND_TOUCH})

set(DIR ${
    
    CMAKE_SOURCE_DIR}/../../../libs)
include_directories(${
    
    DIR}/include/soundtouch)

完成上面的配置后我们就能在工程里面使用soundTouch库了。

使用

官方有个demo在source/Android-lib目录下,具体用法可以参考其中的soundtouch-jni.cpp。大体的步骤如下。
1.初始化对象

    soundtouch::SoundTouch *soundtouch = new soundtouch::SoundTouch();
    //输入音频数据的采样率
    soundtouch->setSampleRate(OUT_SAMPLE_RATE);
    //输入音频的通道数
    soundtouch->setChannels(OUT_CHANNEL_NUMBER);

2.配置,下面的这些配置是可以在输入输出数据时更改的即时生效。

//变调
//    soundtouch->setPitch(playSpeed);
//变速变调
//    soundtouch->setRate(playSpeed);
//变速
    soundtouch->setTempo(speed);

3.输入输出数据

			//ffmpeg的音频转化(比如重采样)
            int outSample = swr_convert(audioSwrContext, &audioBuffer, 4096 * 2,
                                        (const uint8_t **) (audioFrame->data),
                                        audioFrame->nb_samples);
            int size = 0;
			//外层其实有个While循环不停的读取audioFrame
            soundtouch->putSamples(reinterpret_cast<const soundtouch::SAMPLETYPE *>(audioBuffer),
                                   outSample);
            int soundOutSample = 0;
            do {
    
    
                soundOutSample = soundtouch->receiveSamples(
                        reinterpret_cast<soundtouch::SAMPLETYPE *>(audioBuffer + size),
                        OUT_SAMPLE_RATE / OUT_CHANNEL_NUMBER);
                size += soundOutSample * OUT_CHANNEL_NUMBER * BYTES_PER_SAMPLE;
            } while (soundOutSample != 0);
            if (size == 0) {
    
    
                av_frame_free(&audioFrame);
                continue;
            }

就是通过putSamples传入数据,receiveSamples读取数据。其实如果receiveSamples的第二个参数足够大,并不需要do-while循环。需要注意的是soundtouch::SAMPLETYPE,代表声音信号的数据类型,有整形和浮点形,由SOUNDTOUCH_FLOAT_SAMPLES和SOUNDTOUCH_INTEGER_SAMPLES控制,具体查看STTypes.h。因为我输出的音频格式是ffmpeg的AV_SAMPLE_FMT_S16和我使用的soundTouch的SAMPLETYPE一致所以不需要转换数据,如果数据格式不一样可能(也许)需要转换,贴下ijkplayer的代码

			//将8位转16位
            for (int i = 0; i < (resampled_data_size / 2); i++)
            {
    
    
                is->audio_new_buf[i] = (is->audio_buf1[i * 2] | (is->audio_buf1[i * 2 + 1] << 8));
            }

4.冲出残留数据。在音频流的最后需要冲出残留的缓存数据,我没有处理,贴下官方的示例代码

    pSoundTouch->flush();
    do
    {
    
    
        nSamples = pSoundTouch->receiveSamples(sampleBuffer, buffSizeSamples);
        outFile.write(sampleBuffer, nSamples * nChannels);
    } while (nSamples != 0);

参考

A Review of Time-Scale Modification of Music Signals
你真的懂语音特征背后的原理吗?
音频变速变调原理及 soundtouch 代码分析

猜你喜欢

转载自blog.csdn.net/Welcome_Word/article/details/124434675
今日推荐