变声算法实现(基频追踪+SOLA)

实时变声算法实现(基频追踪+信号重构)

  • Voice Morph 简介
    • 思路简介
    • 基频追踪
      • 预处理
      • 基频计算
    • 信号重构
    • 总结

Voice Morph 简介

本人在语音信号处理领域工作了一年半时间,希望通过博客的方式来记录一下工作中的一些项目,以及自己在平时学习的一些心得。先从最近的一些项目开始写,前面的一些项目会慢慢补上。希望能与大家互相探讨,共同进步。
最近在工作中接触了一个变声算法,是从github上的一个开源项目改过来的。这个算法能够实现音高和音色的改变,同时保持语速不变,实现变声器的功能。算法分为两个互相独立的模块,分别是基频追踪和信号重构。与这个功能相对的是改变语速但保持音高音色不变,即变速不变调,这个大家在看视频的时候应该能体会到,不过这个算法就不介绍了,下面还是着重介绍变声算法。

思路简介

如果想实现男声变女声,要改变语音信号的两个特征,分别是音高和音色:
1.音高是指说话人的音调,这个主要是由声带振动频率决定的,一般来说女声的音高是高于男声的,不过在信号处理里面我们更喜欢把音调说成基频,代表了一段语音的基础频率,也就是频谱图最下面那条;
2.音色是指说话人所具有的个人特征,这个在频谱上表现为共振峰,反应的是声道的特性。一般来说男生的音色会更加雄厚,而女生的则更加清爽一些。
下面就来介绍如何去改变这两个特征,主要分为基频追踪和信号重构两个部分。

基频追踪

预处理

一般来说变声算法都需要对原信号的基频进行计算,且基频的计算越精确,最后的变声效果也会越好。计算基频的方式有很多种,这里就不多介绍了,我用了自相关函数法来追踪基频,但是在计算之前需要对信号进行预处理,避免不必要的麻烦,具体如下:
a)去直流
去除一帧信号的直流分量,尽可能减少直流分量带来的影响,下面的一些预处理都需要在去直流以后进行;
b)过零率检测
语音分为清音和浊音,通常说话的爆破声都属于清音,清音是没有基频的,所以需要在计算基频前先判断是否是清音,如果是清音就直接跳过基频计算部分,把这一段作为噪声处理。
那么如何去检测清音还是浊音,最简单有效的办法就是过零率检测,即检测信号在一帧内由正到负或者由负到正的次数,根据次数判断是清音还是浊音,一般来说清音的过零率会远大于浊音;
c)低通滤波
由于语音信号中可能有高频噪声,或者是语音本身的共振峰,都会影响自相关函数的计算。当信号中有高频成分时,自相关函数会计算出多个波峰,而基频的计算是根据自相关函数的波峰位置确定的,波峰太多会提高后续基频筛选的难度,因此需要用低通滤波滤除。低通滤波的截止频率一般选取600Hz,这是考虑到有些人在说话时基频可能能够达到600Hz;
d)峰值检测
除了排除清音以外,如果判断到当前帧是噪声,也需要跳过基频计算的步骤,这样可以避免错误的基频。我在算法中用了最简单的峰值检测来判断是否是噪声,这是基于一个假设:噪声段的信号强度一般小于语音段的信号强度,只要检测一帧内的最大峰值是否大于某个阈值,就可以判断这一帧是否是噪声。

基频计算

经过预处理以后,我们就知道哪些帧是需要计算基频的,这里采用了计算自相关函数来计算基频,计算的一般公式如下:
s ( n ) = ∑ m = 0 N − 1 f ( m ) ∗ f ( m + n ) s(n)=\sum_{m=0}^{N-1}f(m)*f(m+n) s(n)=m=0N1f(m)f(m+n)
上面是用时域的方法,为了节省运算量,也可以利用自相关函数与功率谱密度互为傅里叶变换对的特性来计算自相关函数。先对这一帧信号进行加窗,做FFT,利用帕斯瓦尔定理计算功率谱,再把该结果经过IFFT就得到了自相关函数。
如果原信号是比较周期的信号,则自相关函数也会呈现周期性,每隔一段时间就会产生波峰,如下图所示:
变声算法实现(基频追踪+SOLA)_第1张图片
上面的图是原信号,下面的图是自相关函数,可以看到自相关函数在第一个点时值最大,因为自己与自己的相关性肯定是最大的,后续每次相关性的波峰出现的位置都正好对应了原信号的一个周期,只要寻找到自相关函数最大的那个波峰,计算与原点的距离,就能算出原信号的基频周期T。
实际计算时,会在一段较长的窗口内寻找自相关函数的波峰值,并记录对应的基频周期和自相关函数值,形成多个基频候选,在后续算法中再进行选择。基频候选点中比较常见的有半频候选,也叫半频误差,就是实际基频的一半(即周期为两倍)的频率也会呈现较高的自相关值,就如上面图中,除了在T位置有自相关的波峰,在2T位置也有,因此怎么去排除这种错误的候选基频也是基频追踪精准度的问题之一。
我选择了计算自相关函数的同时,进行原信号(低通滤波以后)波峰位置的估计,在一个窗口内采集若干个波峰,计算波峰之间的平均距离,在候选基频得分相近,且呈现出半频误差的形态时,根据波峰的平均距离选取可能性最大的基频。

信号重构

完成基频追踪以后就可以进行信号重构,信号重构也分为两个部分,分别是确定原信号波峰位置以及重构信号的计算。简单介绍一下信号重构的思路,与SOLA的方法有些类似:
变声算法实现(基频追踪+SOLA)_第2张图片
图中从原信号重构输出信号,按照以下步骤进行:
a)确定重构信号的新基频;
b)确定重构信号的波峰位置(时间坐标),重构信号的第一个波峰与原信号的第一个波峰位置相同(理想情况下),然后根据新基频来确定后续的波峰位置,也就是每隔一个新的周期就是一个新的波峰位置;
c)以新的波峰位置为中心,计算两边的重构信号,具体思路与overlap加窗有点类似,先生成一个长度为原信号基频周期的余弦窗(或直接用余弦函数),然后用这个窗与原信号相乘,窗顶点与原信号的波峰要对应,计算得到的数据存在重构信号中。当前帧的前半帧的计算结果与上一帧的后半帧的计算结果叠加,就得到了最后的输出信号。这种重构方式能够一定程度上改善帧与帧之间的不连续性,避免高频噪声的产生。

完成上面的步骤以后就可以完成基频的更改,也就是能够获得一个音高被改变的音频。但是如果男声变女声的时候只改变音高,那你只能得到一个音调变高的厚重的男声…接下来就要对共振峰进行改变,才能达到最后的目的。

共振峰说白了就是基频上的一些高频分量,给一张图直观感受一下:
变声算法实现(基频追踪+SOLA)_第3张图片
图中的基频波峰就是信号重构时需要找到的波峰,波峰之间的距离决定了音高,而波峰之间的高频分量就导致了共振峰的产生。如果按照上面信号重构的方法来计算,基频波峰之间的距离会被改变,但是波峰之间的高频分量的频率却不会被改变(因为按照算法来看,相当于原信号的数据乘上一个系数就被拿来重构信号了,那么原信号数据本身带有的频率信息是没有被改变的)。为了解决这个问题,就需要在重构时对整个信号进行拉伸或压缩,使得这些高频分量也会被改变频率。下面举个例子帮助理解:
现在有一个原音频,采样率为44.1k,男声。如果我需要把男声变女声,那么在进行信号重构时,需要进行上采样。如果进行1.2倍上采样(即采样率上升到52.92k),就在确定重构信号的波峰位置时,把波峰位置相对上个波峰的距离变为新基频的1.2倍,这样波峰之间的采样点数就是原来的1.2倍。在这种情况下再进行overlap加窗重构等操作,如果高频分量原来是800Hz(44.1k采样的情况下),那么现在就变成了960Hz。也就达到了改变共振峰频率的目的。最后我们得到了一个52.92k采样的音频,再经过重采样到44.1k。就完成了整个计算。

总结

这个算法整体的思路还是比较清晰的,主要难点在于基频的追踪,如何能更准确的追踪基频决定了算法的最后效果。之前有一些版本用了动态规划来做,但是动态规划有回溯的过程,这种思路在实时计算中比较难以实现(就算分块动态规划,延迟也稍微偏大了一些)。所以只能考虑多个角度来计算基频,通过多个计算结果来综合决定选取哪个候选基频,后续有机会的话可能考虑做一个代价函数来简化这一过程。

你可能感兴趣的:(音效处理)