背景
最近做一个基于Qt + FFmpeg实时音视频推流的项目,利用OpenCV实时美颜已经OK。计划用SoundTouch再做变声处理,本来希望男声变女声,大叔变萝莉、单车变摩托的效果,但是多次测试后梦醒了,基本不太可能,只能实现基本的变声,这种应该需要比较专业的算法处理在里面。
但不可否认SoundTouch是个很好的音频处理库,实现基本的变调、变速效果还是不错的,就不勉强更理想的效果了,毕竟定位就不同,常见的音频处理需求SoundTouch可以胜任。
说明
现在看SoundTouch的使用还是比较简单的,但还是摸索一两天才玩透彻。网上的资料在搜索的时候感觉并不多,而且好多是Android相关的。并且还受到了不少误导,因此记录一下,也希望需要的人少走弯路有所帮助。
库的编译
官网很好找,直接下载的库看到有动态库dll,直接使用报错,应该是有问题,最好自己编译静态库。
方式:
下载最新源码,结构如下图,使用CMake进行编译,然后如VS2017 IDE,再打开SoundTouch文件夹,一般需要重定解决方案,选中项目右键点击生成,默认配置已经设置好,即可得到静态库。比较简单,不具体叙述。
注意:
直接用VS2017编译生成的静态库使用中存在链接报错,可能我的编译选项设置有问题。建议用CMake先执行下再编译生成就正常了,仍有问题可留言。
库的使用
三方库的配置都是类似的,可以移步另一篇讲库的配置的文章,重头戏是代码。
网上好不容易找到SoundTouch相关的文章有的是互相照抄的,有的不知道是搬运的哪的代码,缺头少尾不知道变量从哪来的, 什么作用,就找到两三篇比较合适的文章。还是转头去找SoundTouch本身提供的例子(官方的最权威,其它库也一样),是一个把变速、变调等功能做成命令行程序的源码,此源码是对输入的Wav音频文件做变速、变调处理后再输出为一个新的Wav文件。不断地查看源码算是理清的头绪,其实就是把一个个采样点的音频数据,送到SoundTouch里进行处理,然后处理完返回一定个数的采样点数据,返回得到的数据就是变速、变调处理后的数据,就可以进行其它操作。库内部的处理算法复杂没看太懂,不过不影响使用。
下面是梳理清楚后写的一份简洁的代码:文件中读取PCM数据处理后输出一个新的PCM文件,目的是表现出最核心的逻辑,先不迷失在其它的一些细节上。理清逻辑后完全可以从内存里取数据,处理后再写到内存,或者其它复杂的处理,逻辑都是一样的。注释很详细,阅读代码即可。
//源码默认6720 改动为最大4*2*1024 (32位 2通道 1024帧大小)
#define MAX_BUFF_SIZE 8192
//打开文件
QString inPcmFileName = "48000_2_s16le.pcm";
QFile inPcmFile(inPcmFileName);
if(!inPcmFile.open(QIODevice::ReadOnly))
{
qDebug() << "read inPcmFile error";
return;
}
QString outPcmFileName = "outPcm.pcm";
QFile outPcmFile(outPcmFileName);
outPcmFile.open(QIODevice::WriteOnly);
//音频数据参数
int sampleRate = 48000;
int channel = 2;
int bytePerSample = 16 / 8;
//音频数据缓存 读取一帧原始音频数据(可修改)
int inPcmSize = channel * bytePerSample * 1024;
uint8_t *inPcmBuf = new uint8_t[inPcmSize];
//soundtouch数据缓存 格式需要为SAMPLETYPE(short)
SAMPLETYPE touchBuffer[MAX_BUFF_SIZE];
//soundTouch设置
soundtouch::SoundTouch soundTouch;
soundTouch.setSampleRate(sampleRate); // 设置采样率
soundTouch.setChannels(channel); // 设置通道数
soundTouch.setTempo(1.0); //变速
soundTouch.setPitch(0.8); //变调
//接收处理后sample个数的最大值
int maxRecv = MAX_BUFF_SIZE / channel / bytePerSample;
//sample个数
int nSamples = 0;
//循环读取文件信息
while(1)
{
int readLen = inPcmFile.read((char *)inPcmBuf, inPcmSize);
if(readLen <= 0)
break;
//char*类型读取的内容进行转换 uint8转short16
for (int i=0; i<readLen/2; i++)
{
touchBuffer[i] = (inPcmBuf[i * 2] | (inPcmBuf[i * 2 + 1] << 8));
}
//计算此次读取数据中包含的sample个数
nSamples = readLen / channel / bytePerSample;
//将nSamples个数的样本写入soundTouch进行处理 注:需要数据缓冲,可能putSamples多次,才能receiveSamples收到处理后的音频数据
soundTouch.putSamples(touchBuffer, nSamples);
//循环接收处理后的音频数据 因为putSamples与receiveSamples的个数不一定对等,存在多次才接收完的情况
while(1)
{
//返回处理好的样本个数 每次接收个数不会超过最大值maxRecv 防止一次接收的数据量过大
nSamples = soundTouch.receiveSamples(touchBuffer, maxRecv);
if(nSamples == 0)
break;
//根据样本数,计算处理后的数据长度
int length = nSamples * channel * bytePerSample;
outPcmFile.write((char*)touchBuffer, length);
}
}
//存在剩余的处理好的数据,冲刷一下
soundTouch.flush();
while(1)
{
//接收处理后的数据,步骤与上相同
nSamples = soundTouch.receiveSamples(touchBuffer, maxRecv);
if(nSamples == 0)
break;
int length = nSamples * channel * bytePerSample;
outPcmFile.write((char*)touchBuffer, length);
}
qDebug() << "Finish";
//释放资源
inPcmFile.close();
outPcmFile.close();
delete[] inPcmBuf;
inPcmBuf = nullptr;