Skip to content

Commit

Permalink
update readme
Browse files Browse the repository at this point in the history
  • Loading branch information
huailiang committed May 14, 2020
1 parent e64725d commit d62af0c
Show file tree
Hide file tree
Showing 3 changed files with 17 additions and 26 deletions.
Binary file removed .github/im10.png
Binary file not shown.
Binary file removed .github/im4.png
Binary file not shown.
43 changes: 17 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
## LipSync是什么?

LipSync是一个基于Unity的独立、轻量化口型匹配解决方案。它可以帮助开发者在Unity上,用相对少的时间精力实现效果相对令人满意的“口型匹配”功能。

LipSync可以运用已有的人物模型、口型动画以及语音资源,实现即时的口型匹配功能。你只需要告诉LipSync语音数据的来源、带有口型BlendShape的目标对象以及BlendShape属性名,并进行简单的设置,就能够让你的人物随着语音的播放动起他/她/它的嘴巴。

LipSync没有借助第三方插件,所有的逻辑都在插件内部得以实现,因此它是不受平台限制的(或者更准确地说,Unity支持的平台,它也同样支持)。
LipSync是一个基于Unity的独立、轻量化口型匹配解决方案。它可以帮助开发者在Unity上,用相对少的时间精力实现效果相对令人满意的“口型匹配”功能。LipSync可以运用已有的人物模型、口型动画以及语音资源,实现即时的口型匹配功能。你只需要告诉LipSync语音数据的来源、带有口型BlendShape的目标对象以及BlendShape属性名,并进行简单的设置,就能够让你的人物随着语音的播放动起他/她/它的嘴巴。

不过LipSync并不是万能的。为了能够愉快而有效地使用LipSync,你需要知道它目前做得到的事情有:

Expand All @@ -19,8 +15,9 @@ LipSync没有借助第三方插件,所有的逻辑都在插件内部得以实
* 分析噪声过于严重的语音数据。
* 分辨某一段声音是语音还是和语音不相关的其他声音。

LipSync文件夹中的内容是本插件的主体部分,而UnityChan文件夹中的内容并不是本插件的一部分,它是为了演示LipSync的效果而附带的一套模型资源。她其实是Unity Technology Japan为Unity开发的一个官方形象。如果你想进一步了解UnityChan,可以在这里获得她的信息:http://unity-chan.com/
LipSync文件夹中的内容是本插件的主体部分,而UnityChan文件夹中的内容并不是本插件的一部分,它是为了演示LipSync的效果而附带的一套模型资源。她其实是Unity Technology Japan为Unity开发的一个官方形象。如果你想进一步了解UnityChan,可以在[这里][i2]获得她的信息。

查看实现的效果展示视频, 点击[这里][i1]

## 使用LipSync前需要做什么准备?

Expand All @@ -42,10 +39,7 @@ __2) 带有口型BlendShape的3D模型。__
你需要找到你的模型上带有口型BlendShape的那个GameObject。它在Unity里表现为SkinnedMeshRenderer组件。以UnityChan模型为例。把这个GameObject赋予到LipSync的TargetBlendShape上。为了阐述方便,后文我们就把这个“带有口型BlendShape的那个GameObject”称之为“目标对象”。

这里先观察一下这个目标对象,展开BlendShapes项,可以看到它提供的BlendShapes属性值。这个UnityChan模型中提供的口型BlendShape的属性值即为:blendShapes1.MTH_A,blendShapes1.MTH_I,blendShapes1.MTH_U,blendShapes1.MTH_E,blendShapes1.MTH_O,对应的正好是日语中的5个元音。接下来就把这5个属性名一一对应地填到LipSync中的VowelPropertyNames当中。

<br><img src='.github/im4.png'><br>

至此,你的LipSync组件应该变得像如图所示。

<br><img src='.github/im5.png'><br>

Expand All @@ -56,7 +50,6 @@ AmplitudeThreshold的含义是能量阈值,它决定一个数据帧中所有
MoveTowardsSpeed的含义是平滑过渡的速度。语音帧与语音帧之间的识别结果,并不是直接就赋予到BlendShape属性值上的,而是经过了平滑过渡操作,实际赋予的属性值是经过插值的。从效果上看,这个速度越低,角色的嘴巴就动得越慢,给人的感觉是不太灵活;反之则越快,给人感觉移动非常夸张。一般来说,这个值可以取在6到10之间。

完成了这些设置值之后,尝试运行一下,你应该就可以看到UnityChan随着语音的播放动起了她的嘴巴。
以上,则是使用LipSync最简单的方法。

在这个例子中,所识别的语音是日语,正如RecognizerLanguage所示的Japanese。如果你需要识别汉语,可以把RecognizerLanguage项切换成Chinese,此时VowelPropertyNames中会多出一个“v”的元音。除此之外没有其他变化。

Expand All @@ -71,11 +64,7 @@ MoveTowardsSpeed的含义是平滑过渡的速度。语音帧与语音帧之间
<br><img src='.github/im8.png'><br>

在这里,你可以导入一个文件夹里的语音文件,并把它们的口型匹配数据烘焙到本地文件上。这里利用了Unity的动画系统,输出的文件就是AnimationClip与AnimatorController。
先从AudioClipInputFolderPath开始,这里点击“Browse...”按钮可以选择一个路径。当然,你只能选择位于Assets文件夹内的路径。之后,LipSync会搜索该文件夹内所有的音频文件,并记录到AudioClipList中。

<br><img src='.github/im10.png'><br>

右边部分的设置,与实时模式下的设置基本相同,就是多出了两项AnimatorName和TargetRelativePath。AnimatorName是烘焙完毕后,生成的Animator的名称。你可以自行进行指定。TargetRelativePath是为了应对特殊情况,一般来说生成的Animator会被挂载在目标对象上,但是可能会因为某些原因,导致只能挂载在它的某一个父级GameObject。这个TargetRelativePath就是用来指定相对路径用的。通常情况下不会用到它,这里我们选择不填写。
先从AudioClipInputFolderPath开始,这里点击“Browse...”按钮可以选择一个路径。当然,你只能选择位于Assets文件夹内的路径。之后,LipSync会搜索该文件夹内所有的音频文件,并记录到AudioClipList中。AnimatorName是烘焙完毕后,生成的Animator的名称。你可以自行进行指定。TargetRelativePath是为了应对特殊情况,一般来说生成的Animator会被挂载在目标对象上,但是可能会因为某些原因,导致只能挂载在它的某一个父级GameObject。这个TargetRelativePath就是用来指定相对路径用的。通常情况下不会用到它,这里我们选择不填写。
另外,AdvancedOptions里也多出了一项ShiftStepSize,这决定了采样时数据帧与数据帧之间的间隔。在实时匹配时,由于LipSync可以直接采样当前帧(渲染意义上)正在播放的声音片段,所以不需要这一个属性,而烘焙时声音并没有被真正播放,所以需要指定这个值。一般来说,取窗口长度的一半可以得到非常精确的结果,小于这个值的意义不大,烘焙时间却会增长。
全部设置完毕后,你可以点击“Bake”按钮。选择一个Assets文件夹内的路径,即可开始烘焙工作。烘焙工作需要的时间比较长,限于目前采用的计算方法,可能会长于语音文件本身的时间长度,请耐心等待一下。经过漫长的等待后,你可以在刚才指定的路径中找到烘焙结果。

Expand All @@ -91,26 +80,24 @@ MoveTowardsSpeed的含义是平滑过渡的速度。语音帧与语音帧之间
LipSync主要涉及到的是信号处理与语音识别领域的理论知识。这一部分将进行简单的解说,如果想深入了解,可以自行查阅相关概念。
它的基本工作流程如下:

1、从AudioSource或者AudioClip处获取语音数据
#### 1、从AudioSource或者AudioClip处获取语音数据

从AudioSource处获取是实时匹配时采用的方法。AudioSource本身提供了一个GetOutputData函数,可以获取当前正在播放的语音数据段。
从AudioClip处获取是烘焙是采用的方法。AudioClip本身其实是对语音文件的一个封装,可以使用GetData函数直接获得语音数据。
这过程中也包含了分帧与窗口化的步骤。

2、剔除无声帧
#### 2、剔除无声帧

从信号处理的角度上说,这一步是一种时域分析方法。对数据帧中的所有值进行求和,如果结果大于用户预设的一个阈值(也就是AmplitudeThreshold),那么就认为这一帧是没有声音的,不对它进行后续处理。这可以节省不必要的分析过程。如果适当调高阈值,一定程度上可以降噪。

3、获取语音数据的频域信息
#### 3、获取语音数据的频域信息

你在使用一些音乐播放器时,有时候会看到一根根跳动的长条,这就是“频谱”的一种表现方式,频域信息指的就是频谱。这对于语音识别来说是非常重要的信息。
实时匹配时,AudioSource的GetSpecturmData函数带来了极大的帮助,这个函数本身可以高效地获取当前播放的语音数据频谱。

然而在烘焙时,并没有这样便利的函数可以用。所以,LipSync借助了一个数学工具——离散余弦变换(DCT),它可以用来获取一个时域信息段的频域信息。它与另一个著名的数学工具——傅里叶变换是等价的,所不同的是余弦变换只获取频率信息,而舍弃了相位信息。实际上这就够了,我们并不需要相位信息。这个数学工具的实现可以在MathToolBox.DiscreteCosineTransform中找到。

4、提取共振峰。

这里先简单介绍一下人类发声的原理。
#### 4、提取共振峰

人在发声时,肺部收缩送出一股直流空气,经器官流至喉头声门处(即声带),使声带产生振动,并且具有一定的振动周期,从而带动原先的空气发生振动,这可以称为气流的激励过程。之后,空气经过声带以上的主声道部分(包括咽喉、口腔)以及鼻道(包括小舌、鼻腔),不同的发音会使声道的肌肉处在不同的部位,这形成了各种语音的不同音色,这可以称为气流在声道的冲激响应过程。

Expand All @@ -119,27 +106,31 @@ LipSync主要涉及到的是信号处理与语音识别领域的理论知识。

LipSync的核心步骤正是如此。提取共振峰的方法是,在前一步骤中获取的频谱上求出局部最大值的最大值,具体实现在MathToolBox.FindLocalLargestPeaks中可以找到。

5、把共振峰映射为元音特征值,进行平滑过渡处理,再赋予到目标对象上
#### 5、把共振峰映射为元音特征值,进行平滑过渡处理,再赋予到目标对象上
后续步骤则比较容易理解,无非是一些映射操作与平滑过渡处理。平滑过渡的方法直接使用了Mathf.MoveTowards。

## 我想要改进LipSync,可以做些什么?

目前,有以下几个可以改进的方向:

1、更优化的DCT算法
#### 1、更优化的DCT算法

目前采用的DCT算法,是单线程直接计算了所有需要计算的值,时间复杂度为O(n^2)。这也是为什么烘焙的速度非常慢。一方面,可以考虑借鉴快速傅里叶变换所采用的算法(比如蝶形变换),把时间复杂度降到O(nlogn);另一方面,可以考虑利用多线程计算。还可以考虑对余弦计算结果进行缓存。

2、整体数学运算优化
#### 2、整体数学运算优化

这可以说是上一个问题的泛化版本。笔者在撰写数学函数时,几乎没有考虑步骤上的优化,所有步骤都很耿直地写上去了,所以应该有许多可以优化的地方。
另外,由于存在着各种浮点运算,对GPU的利用也是一个考虑方向。

3、元音项的数据化,或者是更好的管理方法
#### 3、元音项的数据化,或者是更好的管理方法

如果你阅读过语音识别部分的代码,你可以看到所支持的两种语言的元音项都是写死的,显然这不太“优雅”。笔者的打算是把它们数据化,写到本地文件中,使用时动态进行读取,这既有利于管理,也有利于对更多的语言进行支持。
当然这不一定是最好的管理方法,如果你有什么高见,希望能够提出来。

4、更加精确的语音识别方法
#### 4、更加精确的语音识别方法

如果你对语音识别有一些研究,你应该知道,目前的语音识别方法中并没有去除基频的影响。如果基频的能量很高,会明显影响共振峰的识别。


[i1]: https://www.bilibili.com/video/BV1BK411578P
[i2]: http://unity-chan.com/

0 comments on commit d62af0c

Please sign in to comment.