Unity中Google Cloud TTS生成AudioClip的唇形同步错位问题
实时TTS音频唇形同步错位修复方案
问题背景
通过Google Cloud文本转语音生成实时AudioClip,同步调整角色嘴唇Transform的Y轴缩放,但播放时唇形动作与音频始终错位。已尝试调整更新间隔、使用RepeatInvoke、修改/移除响度阈值、设置AudioSource优先级为0等方法,均无效。音频内容由GPT-3实时生成,无法提前预知,现有代码已实现自动检测音频最小/最大响度以适配不同音频范围。
修复方案
- 补偿音频缓冲区延迟:AudioSource的
timeSamples对应播放位置与实际听到的声音存在缓冲区延迟,需计算偏移量修正采样起始点。 - 缩小采样缓冲区:放弃处理整个音频采样,改用固定小尺寸缓冲区(如256采样点),仅处理当前播放位置附近的最新音频数据,减少延迟。
- 使用FixedUpdate替代Update:避免Update帧率不稳定导致的检测间隔波动,保证固定检测频率。
- 滑动窗口统计响度范围:用滑动窗口存储近期响度值,避免实时更新最小/最大响度带来的前期范围偏差,提升同步稳定性。
修改后的代码
using UnityEngine; public class SyncMouthToAudio : MonoBehaviour { const float scaleYMin = 0.01f; const float scaleYMax = 0.05f; const int sampleBufferSize = 256; // 固定小缓冲区,减少处理延迟 TextToSpeechVoice voice = null; AudioSource audioSource = null; float[] clipData = new float[sampleBufferSize]; float detectedLoudnessMin = Mathf.Infinity; float detectedLoudnessMax = 0f; float[] recentLoudness = new float[10]; // 滑动窗口存储近期响度 int loudnessIndex = 0; void Start() { voice = GetComponentInParent<TextToSpeechVoice>(); voice.onStarts += OnVoiceStarts; voice.onEnds += OnVoiceEnds; } void FixedUpdate() { CheckLoudness(); } void CheckLoudness() { float loudness = 0f; if (audioSource != null && audioSource.isPlaying && audioSource.timeSamples > sampleBufferSize) { // 补偿音频缓冲区延迟,根据实际情况调整偏移量(0.05f为参考值) int offsetSamples = Mathf.RoundToInt(AudioSettings.outputSampleRate * 0.05f); int startSample = Mathf.Max(0, audioSource.timeSamples - offsetSamples); startSample = Mathf.Min(startSample, audioSource.clip.samples - sampleBufferSize); audioSource.clip.GetData(clipData, startSample); foreach (var sample in clipData) { loudness += Mathf.Abs(sample); } loudness /= sampleBufferSize; // 取平均响度,避免缓冲区大小影响 // 更新滑动窗口的响度统计 recentLoudness[loudnessIndex] = loudness; loudnessIndex = (loudnessIndex + 1) % recentLoudness.Length; detectedLoudnessMin = Mathf.Min(recentLoudness); detectedLoudnessMax = Mathf.Max(recentLoudness); } SetScaleByLoudness(loudness); } void SetScaleByLoudness(float loudness) { const float visibilityMultiplier = 15f; float scaleY = scaleYMin; bool hasValidRange = detectedLoudnessMin < Mathf.Infinity && detectedLoudnessMax > detectedLoudnessMin; if (hasValidRange && loudness > 0f) { float threshold = detectedLoudnessMin + (detectedLoudnessMax - detectedLoudnessMin) * 0.1f; if (loudness >= threshold) { // 使用InverseLerp简化归一化计算 float normalizedLoudness = Mathf.InverseLerp(detectedLoudnessMin, detectedLoudnessMax, loudness); scaleY = Mathf.Lerp(scaleYMin, scaleYMax, normalizedLoudness) * visibilityMultiplier; scaleY = Mathf.Clamp(scaleY, scaleYMin, scaleYMax); } } transform.SetLocalScaleY(scaleY); } void OnVoiceStarts(AudioSource audioSource) { this.audioSource = audioSource; // 重置响度统计数据 detectedLoudnessMin = Mathf.Infinity; detectedLoudnessMax = 0f; System.Array.Fill(recentLoudness, 0f); loudnessIndex = 0; } void OnVoiceEnds() { this.audioSource = null; // 语音结束后重置唇形 transform.SetLocalScaleY(scaleYMin); } }
关键修改说明
- 用
FixedUpdate替代Update,保证固定的检测频率,避免帧率波动影响同步 - 使用256采样点的小缓冲区,仅处理当前播放位置附近的音频数据,减少延迟
- 添加音频缓冲区延迟补偿,修正采样起始点,匹配实际听到的声音位置
- 滑动窗口统计近期响度,避免实时更新最小/最大响度带来的范围突变
- 简化响度归一化计算逻辑,提升唇形动作的响应速度
内容的提问来源于stack exchange,提问作者Philipp Lenssen
相关产品推荐
相关产品推荐

