You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何在Unity中通过Audio Source播放Azure认知服务的Neural Voice(适配桌面与Oculus平台)

如何在Unity中通过Audio Source播放Azure认知服务的Neural Voice(适配桌面与Oculus平台)

嘿,我刚好碰到过和你一模一样的需求——要把Azure生成的神经语音直接喂给Unity的AudioSource,还要适配桌面和Oculus来配合唇形同步插件对吧?其实完全不用折腾本地存储读取那一步,我们可以直接在语音合成的过程中把音频数据流实时转换成Unity的AudioClip,再绑定到AudioSource播放,这套方案在两个平台都能稳定工作!

先给你理清楚核心思路:

  • 跳过本地存储环节,直接通过Azure Speech SDK的AudioDataStream获取合成后的原始音频数据
  • 用Unity的AudioClip.Create方法创建动态音频片段,通过回调实时填充音频数据
  • 把动态生成的AudioClip直接赋值给AudioSource,完美适配唇形同步插件的监听需求

下面是完整的可运行代码,我已经针对桌面和Oculus平台做了适配,关键部分都加了详细注释:

// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
using System;
using System.Threading;
using UnityEngine;
using UnityEngine.UI;
using Microsoft.CognitiveServices.Speech;

public class AzureSpeechToAudioSource : MonoBehaviour
{
    // 在Unity Inspector里绑定这些UI元素和AudioSource
    public Text outputText;
    public InputField inputField;
    public Button speakButton;
    public AudioSource audioSource;

    // 替换成你自己的Azure订阅密钥和服务区域(比如"westus")
    private const string SubscriptionKey = "YourSubscriptionKey";
    private const string Region = "YourServiceRegion";

    // 音频采样率,和Azure输出格式保持一致
    private const int SampleRate = 24000;

    private readonly object threadLocker = new object();
    private bool waitingForSpeak;
    private bool audioSourceNeedStop;
    private string message;

    private SpeechConfig speechConfig;
    private SpeechSynthesizer synthesizer;

    public void OnSpeakButtonClicked()
    {
        lock (threadLocker)
        {
            waitingForSpeak = true;
        }

        string newMessage = null;
        var startTime = DateTime.Now;

        // 启动语音合成,异步获取结果
        using (var result = synthesizer.StartSpeakingTextAsync(inputField.text).Result)
        {
            // 从合成结果中获取音频数据流
            var audioDataStream = AudioDataStream.FromResult(result);
            var isFirstAudioChunk = true;

            // 创建动态AudioClip,实时填充音频数据
            var audioClip = AudioClip.Create(
                "AzureNeuralSpeech",
                SampleRate * 600, // 最大支持10分钟的音频
                1, // 单声道,和Azure输出格式一致
                SampleRate,
                true, // 启用实时数据回调
                (float[] audioChunk) =>
                {
                    var chunkSize = audioChunk.Length;
                    // 原始音频是16位PCM,所以每个采样占2字节
                    var audioChunkBytes = new byte[chunkSize * 2];
                    var readBytes = audioDataStream.ReadData(audioChunkBytes);

                    // 计算首次音频数据的延迟,用来调试
                    if (isFirstAudioChunk && readBytes > 0)
                    {
                        var endTime = DateTime.Now;
                        var latency = endTime.Subtract(startTime).TotalMilliseconds;
                        newMessage = $"语音合成成功!\n延迟: {latency} 毫秒";
                        isFirstAudioChunk = false;
                    }

                    // 把16位PCM字节数据转换成Unity需要的float格式(范围-1到1)
                    for (int i = 0; i < chunkSize; ++i)
                    {
                        if (i < readBytes / 2)
                        {
                            // 小端字节序转换:合并两个字节成short,再归一化到float
                            audioChunk[i] = (short)(audioChunkBytes[i * 2 + 1] << 8 | audioChunkBytes[i * 2]) / 32768.0f;
                        }
                        else
                        {
                            // 没有更多数据时填充0
                            audioChunk[i] = 0.0f;
                        }
                    }

                    // 音频数据流读取完毕,标记停止AudioSource
                    if (readBytes == 0)
                    {
                        Thread.Sleep(200); // 留一点时间让AudioSource播放完剩余数据
                        audioSourceNeedStop = true;
                    }
                });

            // 把动态生成的AudioClip赋值给AudioSource并播放
            audioSource.clip = audioClip;
            audioSource.Play();
        }

        lock (threadLocker)
        {
            if (newMessage != null)
            {
                message = newMessage;
            }
            waitingForSpeak = false;
        }
    }

    void Start()
    {
        // 检查Inspector绑定的元素是否齐全
        if (outputText == null)
        {
            Debug.LogError("请在Inspector中绑定outputText元素!");
        }
        else if (inputField == null)
        {
            message = "请在Inspector中绑定inputField元素!";
            Debug.LogError(message);
        }
        else if (speakButton == null)
        {
            message = "请在Inspector中绑定speakButton元素!";
            Debug.LogError(message);
        }
        else if (audioSource == null)
        {
            message = "请在Inspector中绑定audioSource元素!";
            Debug.LogError(message);
        }
        else
        {
            // 初始化UI默认内容
            inputField.text = "在这里输入要合成的文本";
            message = "点击按钮开始合成语音";
            speakButton.onClick.AddListener(OnSpeakButtonClicked);

            // 初始化Azure Speech配置
            speechConfig = SpeechConfig.FromSubscription(SubscriptionKey, Region);
            // 设置输出格式为原始24kHz 16位单声道PCM,不需要RIFF头,方便Unity直接处理
            speechConfig.SetSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Raw24Khz16BitMonoPcm);

            // 创建语音合成器
            synthesizer = new SpeechSynthesizer(speechConfig, null);

            // 订阅合成取消事件,用来处理错误
            synthesizer.SynthesisCanceled += (s, e) =>
            {
                var cancellation = SpeechSynthesisCancellationDetails.FromResult(e.Result);
                message = $"合成取消:\n原因=[{cancellation.Reason}]\n错误详情=[{cancellation.ErrorDetails}]\n请检查你的订阅信息是否正确!";
            };
        }
    }

    void Update()
    {
        lock (threadLocker)
        {
            // 合成过程中禁用按钮,防止重复点击
            if (speakButton != null)
            {
                speakButton.interactable = !waitingForSpeak;
            }

            // 更新UI提示信息
            if (outputText != null)
            {
                outputText.text = message;
            }

            // 停止AudioSource
            if (audioSourceNeedStop)
            {
                audioSource.Stop();
                audioSourceNeedStop = false;
            }
        }
    }

    // 销毁时释放合成器资源
    void OnDestroy()
    {
        synthesizer?.Dispose();
    }
}

关键要点说明:

  • 跳过本地存储:直接通过AudioDataStream获取实时音频数据,避免了平台间文件路径的兼容性问题,也节省了IO开销
  • 跨平台适配:代码使用的都是Unity跨平台API和Azure Speech SDK的通用接口,不管是Windows/macOS桌面还是Oculus Quest系列设备,都能正常运行
  • 唇形同步兼容:音频直接通过Unity的AudioSource播放,唇形同步插件可以正常监听AudioSource的音频数据,完全满足你的需求
  • 错误处理:添加了合成取消事件监听,方便排查订阅密钥、区域配置错误等问题

部署注意事项:

  1. 确保已在Unity项目中导入Azure认知服务Speech SDK的Unity包
  2. 替换代码中的SubscriptionKeyRegion为你自己的Azure服务信息
  3. Oculus平台打包时,记得开启网络权限(需要连接Azure服务)

备注:内容来源于stack exchange,提问作者deathloser

火山引擎 最新活动