diff --git a/.github/im10.png b/.github/im10.png deleted file mode 100644 index f9f1b27..0000000 Binary files a/.github/im10.png and /dev/null differ diff --git a/.github/im4.png b/.github/im4.png deleted file mode 100644 index f4ad09b..0000000 Binary files a/.github/im4.png and /dev/null differ diff --git a/README.md b/README.md index 23a1872..53bc1ac 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,6 @@ ## LipSync是什么? -LipSync是一个基于Unity的独立、轻量化口型匹配解决方案。它可以帮助开发者在Unity上,用相对少的时间精力实现效果相对令人满意的“口型匹配”功能。 - -LipSync可以运用已有的人物模型、口型动画以及语音资源,实现即时的口型匹配功能。你只需要告诉LipSync语音数据的来源、带有口型BlendShape的目标对象以及BlendShape属性名,并进行简单的设置,就能够让你的人物随着语音的播放动起他/她/它的嘴巴。 - -LipSync没有借助第三方插件,所有的逻辑都在插件内部得以实现,因此它是不受平台限制的(或者更准确地说,Unity支持的平台,它也同样支持)。 +LipSync是一个基于Unity的独立、轻量化口型匹配解决方案。它可以帮助开发者在Unity上,用相对少的时间精力实现效果相对令人满意的“口型匹配”功能。LipSync可以运用已有的人物模型、口型动画以及语音资源,实现即时的口型匹配功能。你只需要告诉LipSync语音数据的来源、带有口型BlendShape的目标对象以及BlendShape属性名,并进行简单的设置,就能够让你的人物随着语音的播放动起他/她/它的嘴巴。 不过LipSync并不是万能的。为了能够愉快而有效地使用LipSync,你需要知道它目前做得到的事情有: @@ -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前需要做什么准备? @@ -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当中。 - -

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

@@ -56,7 +50,6 @@ AmplitudeThreshold的含义是能量阈值,它决定一个数据帧中所有 MoveTowardsSpeed的含义是平滑过渡的速度。语音帧与语音帧之间的识别结果,并不是直接就赋予到BlendShape属性值上的,而是经过了平滑过渡操作,实际赋予的属性值是经过插值的。从效果上看,这个速度越低,角色的嘴巴就动得越慢,给人的感觉是不太灵活;反之则越快,给人感觉移动非常夸张。一般来说,这个值可以取在6到10之间。 完成了这些设置值之后,尝试运行一下,你应该就可以看到UnityChan随着语音的播放动起了她的嘴巴。 -以上,则是使用LipSync最简单的方法。 在这个例子中,所识别的语音是日语,正如RecognizerLanguage所示的Japanese。如果你需要识别汉语,可以把RecognizerLanguage项切换成Chinese,此时VowelPropertyNames中会多出一个“v”的元音。除此之外没有其他变化。 @@ -71,11 +64,7 @@ MoveTowardsSpeed的含义是平滑过渡的速度。语音帧与语音帧之间

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

- -右边部分的设置,与实时模式下的设置基本相同,就是多出了两项AnimatorName和TargetRelativePath。AnimatorName是烘焙完毕后,生成的Animator的名称。你可以自行进行指定。TargetRelativePath是为了应对特殊情况,一般来说生成的Animator会被挂载在目标对象上,但是可能会因为某些原因,导致只能挂载在它的某一个父级GameObject。这个TargetRelativePath就是用来指定相对路径用的。通常情况下不会用到它,这里我们选择不填写。 +先从AudioClipInputFolderPath开始,这里点击“Browse...”按钮可以选择一个路径。当然,你只能选择位于Assets文件夹内的路径。之后,LipSync会搜索该文件夹内所有的音频文件,并记录到AudioClipList中。AnimatorName是烘焙完毕后,生成的Animator的名称。你可以自行进行指定。TargetRelativePath是为了应对特殊情况,一般来说生成的Animator会被挂载在目标对象上,但是可能会因为某些原因,导致只能挂载在它的某一个父级GameObject。这个TargetRelativePath就是用来指定相对路径用的。通常情况下不会用到它,这里我们选择不填写。 另外,AdvancedOptions里也多出了一项ShiftStepSize,这决定了采样时数据帧与数据帧之间的间隔。在实时匹配时,由于LipSync可以直接采样当前帧(渲染意义上)正在播放的声音片段,所以不需要这一个属性,而烘焙时声音并没有被真正播放,所以需要指定这个值。一般来说,取窗口长度的一半可以得到非常精确的结果,小于这个值的意义不大,烘焙时间却会增长。 全部设置完毕后,你可以点击“Bake”按钮。选择一个Assets文件夹内的路径,即可开始烘焙工作。烘焙工作需要的时间比较长,限于目前采用的计算方法,可能会长于语音文件本身的时间长度,请耐心等待一下。经过漫长的等待后,你可以在刚才指定的路径中找到烘焙结果。 @@ -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、提取共振峰 人在发声时,肺部收缩送出一股直流空气,经器官流至喉头声门处(即声带),使声带产生振动,并且具有一定的振动周期,从而带动原先的空气发生振动,这可以称为气流的激励过程。之后,空气经过声带以上的主声道部分(包括咽喉、口腔)以及鼻道(包括小舌、鼻腔),不同的发音会使声道的肌肉处在不同的部位,这形成了各种语音的不同音色,这可以称为气流在声道的冲激响应过程。 @@ -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/ \ No newline at end of file