You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 23:25:18