如何在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的音频数据,完全满足你的需求
- 错误处理:添加了合成取消事件监听,方便排查订阅密钥、区域配置错误等问题
部署注意事项:
- 确保已在Unity项目中导入Azure认知服务Speech SDK的Unity包
- 替换代码中的
SubscriptionKey和Region为你自己的Azure服务信息 - Oculus平台打包时,记得开启网络权限(需要连接Azure服务)
备注:内容来源于stack exchange,提问作者deathloser




