关于音频变声算法,这个是一个很多人特别感兴趣的话题。
当然也有不少开源算法可以参阅学习,有基于时域,也有基于频域的算法。
最终算法想要达到的目的是一致。
最近也有不少网友问过关于变声算法的一些细节问题,邮件询问我。
要给出一个比较合理或者说通俗易懂的解释,看似简单,其实还蛮难的。
按照大概的一个逻辑思路,稍微理一理,所以这个主题必须加上“大话”这个前缀。
也不打算讲特别高深的,当然也是因为讲不来。
之于图像算法领域,非常重要的算法是高斯模糊,
当然也可以认为是卷积,高斯模糊是卷积的一种特例,这里就不展开了。
而之于音频,也许你也猜到了,基于时间的,毫无疑问,就是重采样算法。
音频采样率是指录音设备在一秒钟内对声音信号的采样次数,
采样频率越高声音的还原就越真实越自然。
在当今的主流采集卡上,采样频率一般共分为22.05KHz、44.1KHz、48KHz三个等级,
22.05KHz只能达到FM广播的声音品质,
44.1KHz则是理论上的CD音质界限,48KHz则更加精确一些。
看到这里,也许大多数人还是没法理解采样频率大概是什么意思。
换个角度来说的话,就是假设一个人说“你好”,花了20毫秒,而机器在这20毫秒内,
采集的数据多少就可以理解为采样率高低。
也就是说,20毫秒内,采集到的数据量就是可以大概认为目前的采样率,数据量越大,精度越高,采样率越高。
那么,我们再换一个思路,想一个问题。
如果在同样的速率的情况下,
一个人的语速快,一个人的语速慢,那也可能造成采样数据分布不一致。
这里就可以展开一个音频算法,就是变速。
嗯,是的,就是变速。
从原理上来讲的话,其实变速就是在同样的采样率环境下,对采样数据进行拉伸或压缩。
从算法的角度上来说的话,可以认为是插值或抽值。
如果你让一个人讲话的速度变得更快怎么做,
很明显,就是在同样的采样率下,抽掉一些样本。
反之,降速则是插入一些样本。
最终决定变速效果的就是插入样本和抽离样本的权重计算。
例如原来采样到的数据是
1234
加速的时候,抽离样本 1 和 4
23
降速的时候,增加样本
11223344
当然只是举个例子,便于大家理解这个概念逻辑。
看到这里,肯定有人会问,
那声音的大小呢?或者说信号的强弱呢?
其实也就是提升音量和降低音量,我想这个应该不用解释。
变速是时域变,空间不变。
而音量则反之,时域不变,空间变。
可以简单粗暴地理解,就是线性拉伸。
例如原来采样到的数据是
1234
每个样本+4,直接拉伸为
5678
也有采用乘法进行拉伸的,
例如 乘以2
2468
上面是增大音量,降低音量反之就是减和除。
而最终不管变速还是音量调节,
最终算法要做的事情就是确定对应位置的对应权重。
当然也要看最终想要达到什么样的效果,适配权重。
饶了这么一大圈,还是没有说到变声的问题。
其实,变声就是变速+音量调节。
以上变速也好,音量调节也好,相对而言都是线性拉伸,
直接的加减乘除然后插值抽值就能达到的。
而变声的概念其实也是类似的,
就是在在同一时域内同时调节对应时域的音量权重。
换言之就是在同一个采样率内,同时控制语速和音量在一个特定的权重内。
其实就是一个时域和空间的二维拉伸。
理解这个逻辑确实有点绕。
用采样算法来做一个简单的示例。
参阅前面的文章《简洁明了的插值音频重采样算法例子 (附完整C代码)》
这个示例中的采样函数是:
void resampler(char *in_file, char *out_file) { //音频采样率 uint32_t in_sampleRate = 0; //总音频采样数 uint64_t totalSampleCount = 0; int16_t *data_in = wavRead_int16(in_file, &in_sampleRate, &totalSampleCount); uint32_t out_sampleRate = in_sampleRate * 2; uint32_t out_size = (uint32_t) (totalSampleCount * ((float) out_sampleRate / in_sampleRate)); int16_t *data_out = (int16_t *) malloc(out_size * sizeof(int16_t)); //如果加载成功 if (data_in != NULL && data_out != NULL) { resampleData(data_in, in_sampleRate, (uint32_t) totalSampleCount, data_out, out_sampleRate); wavWrite_int16(out_file, data_out, out_sampleRate, (uint32_t) out_size); free(data_in); free(data_out); } else { if (data_in) free(data_in); if (data_out) free(data_out); } }
让我们稍微变通一下,设一个采样速率,用来调节声音的速度,同时保证采样率不变。
void resampler(char *in_file, char *out_file) { //音频采样率 uint32_t in_sampleRate = 0; //总音频采样数 uint64_t totalSampleCount = 0; int16_t *data_in = wavRead_int16(in_file, &in_sampleRate, &totalSampleCount); float speed = 0.88;//增加一个速度权重 uint32_t out_sampleRate = in_sampleRate * speed; uint32_t out_size = (uint32_t) (totalSampleCount * ((float) out_sampleRate / in_sampleRate)); int16_t *data_out = (int16_t *) malloc(out_size * sizeof(int16_t)); //如果加载成功 if (data_in != NULL && data_out != NULL) { resampleData(data_in, in_sampleRate, (uint32_t) totalSampleCount, data_out, out_sampleRate); //out_sampleRate改为输出一样的采样率in_sampleRate wavWrite_int16(out_file, data_out, in_sampleRate, (uint32_t) out_size); free(data_in); free(data_out); } else { if (data_in) free(data_in); if (data_out) free(data_out); } }
修改后是这个样子的。
有心的朋友发现了。out_size数值有可能增大或缩小了。
以上示例代码,就是一个简单的变速算法。
变速就是这么一个原理,音量增大降低就不做示例了。
而变声是一个什么算法呢?
说白了,就是变速的同时保证out_size还是原来的totalSampleCount。
那要怎么保证呢?
答案就是插值,如果简单粗暴一点,补0或者删0即可。
当然这样做的话,可能会导致音量不一致,最终发声不对的情况。
这肯定是不科学的,最终的插值时候的权重和对应的内容,产生的效果就看各家本领了。
以上原理,也说得差不多了,具体怎么实现的话,
大家自行参阅相关的开源代码,再去理解一下。
另外说一下前面《声音变调算法PitchShift(模拟汤姆猫) 附完整C++算法实现代码》
这篇文章中的sin和cos 没有在有效区间内,所以fastsin fastcos计算的结果是有问题的。
详情大家还是参阅作者原算法吧。
当然,后面有时间我会放出,
简单清晰的变声算法的完整c代码和对应的示例代码。
而关于基于傅里叶变换的重采样算法,《基于傅里叶变换的音频重采样算法 (附完整c代码)》
在对应的github 项目fftResample上,我也做了算法逻辑上的修正。
发表过的文章一般很少进行二次编辑了,
关于后期的一些修正和变更,大家还是关注一下github项目的更新比较直接一点。
具体变声的实现原理,
如上所述,希望通过这篇文章,
大家对音频变声算法能有比较直观的理解和认识。
以上,权当抛砖引玉。
独乐乐不如一起玩乐。
若有其他相关问题或者需求也可以邮件联系俺探讨。
邮箱地址是:
[email protected]