文字转语音
UNITY_WSA平台
- UNITY_WSA平台
- 处理过程和实现原理
- 中文需要设置声音
- 上代码
- 使用方法
-
- 一、部署好Unity3d HoloLens项目工程
- 二、新建节点
- 三、添加TTS组件并配置
- 四、调用TTS功能
- 五、测试、打包和运行
- 参考地址
- 源码地址:
HoloLens2 是微软通用平台,使用的类主要是Windows.Media.SpeechSynthesis;
处理过程和实现原理是把string内容转换成语音文件,并转换为audio clip 通过audio source组件进行播放。具体请看 代码,注释比较详细,请自行研究。
中文需要设置声音声音如下TextToSpeechVoice枚举:
public enum TextToSpeechVoice { /// ///系统默认声音. /// Default, /// /// 大卫 移动 /// David, /// ///马克 移动 /// Mark, /// /// 兹拉 移动 /// Zira, /// /// 瑶瑶(谐音) 中文 /// Yaoyao, /// /// 灰灰(谐音) 中文 /// Huihui, /// /// 康康(谐音) 中文 /// Kangkang, }
中文选用Yaoyao,HuiHui或者KangKang。
上代码using System; using UnityEngine; #if !UNITY_EDITOR && UNITY_WSA using Windows.Foundation; using Windows.Media.SpeechSynthesis; using Windows.Storage.Streams; using System.Linq; using System.Threading.Tasks; #endif namespace HoloToolkit.Unity { /// /// 知名的可用声音. /// public enum TextToSpeechVoice { /// ///系统默认声音. /// Default, /// /// 大卫 移动 /// David, /// ///马克 移动 /// Mark, /// /// 兹拉 移动 /// Zira, /// /// 瑶瑶(谐音) 中文 /// Yaoyao, /// /// 灰灰(谐音) 中文 /// Huihui, /// /// 康康(谐音) 中文 /// Kangkang, } /// ///生成语音. /// 这个类将流转换为UnityAudioClip 并使用你在inspector中提供的AudioSource播放 /// 这可以让你在3D空间中定位声音。推荐的方法是将AudioSource放置在空对象上, /// 并将它设为主摄像头的子对象,将其放置在相机上方的大约0.6个单位。 /// 这个听起来类似于Cortana在操作系统中的讲话。 /// [RequireComponent(typeof(AudioSource))] public class TextToSpeech : MonoBehaviour { [Tooltip("播放语音的AudioSource")] [SerializeField] private AudioSource audioSource; public static TextToSpeech Instance = null; /// /// 获取或设置播放语音的AudioSource. /// public AudioSource AudioSource { get { return audioSource; } set { audioSource = value; } } /// ///获取或设置用于生成语音的声音. /// public TextToSpeechVoice Voice { get { return voice; } set { voice = value; } } [Tooltip("生成语音的声音")] [SerializeField] private TextToSpeechVoice voice; #if !UNITY_EDITOR && UNITY_WSA private SpeechSynthesizer synthesizer; private VoiceInformation voiceInfo; private bool speechTextInQueue = false; #endif /// /// 转换2个字节为-1至1的浮点数 /// ///第一个字节 ///第二个字节 ///转换的值 private static float BytesToFloat(byte firstByte, byte secondByte) { // 转换两个字节为short(从小到大) short s = (short)((secondByte << 8) | firstByte); // 转换为 -1 至 (略低于) 1 return s / 32768.0F; } /// /// 转换字节数组为int. /// ///字节数组 ///读取偏移. ///转换后的int. private static int BytesToInt(byte[] bytes, int offset = 0) { int value = 0; for (int i = 0; i < 4; i++) { value |= ((int)bytes[offset + i]) << (i * 8); } return value; } /// /// 动态创建一个AudioClip音频数据。 /// ///动态生成的AudioClip的名称。 ///音频数据. ///音频数据中的样本数。 ///音频数据的频率。 ///AudioClip private static AudioClip ToClip(string name, float[] audioData, int sampleCount, int frequency) { var clip = AudioClip.Create(name, sampleCount, 1, frequency, false); clip.SetData(audioData, 0); return clip; } /// /// 转换原始WAV数据为统一格式的音频数据。 /// ///WAV数据. ///音频数据中的样本数. ///音频数据的频率. ///统一格式的音频数据. private static float[] ToUnityAudio(byte[] wavAudio, out int sampleCount, out int frequency) { // 确定是单声道还是立体声 int channelCount = wavAudio[22]; // 获取频率 frequency = BytesToInt(wavAudio, 24); // 通过所有其他子块,以获得数据子块: int pos = 12; // 第一个子块ID从12到16 // 不断迭代,直到找到数据块 (即 64 61 74 61 ...... (即 100 97 116 97 十进制)) while (!(wavAudio[pos] == 100 && wavAudio[pos + 1] == 97 && wavAudio[pos + 2] == 116 && wavAudio[pos + 3] == 97)) { pos += 4; int chunkSize = wavAudio[pos] + wavAudio[pos + 1] * 256 + wavAudio[pos + 2] * 65536 + wavAudio[pos + 3] * 16777216; pos += 4 + chunkSize; } pos += 8; // Pos现在被定位为开始实际声音数据。 sampleCount = (wavAudio.Length - pos) / 2; // 每个样本2字节(16位单声道) if (channelCount == 2) { sampleCount /= 2; } // 每个样本4字节(16位立体声) // 分配内存(仅支持左通道) var unityData = new float[sampleCount]; //写入数组: int i = 0; while (pos < wavAudio.Length) { unityData[i] = BytesToFloat(wavAudio[pos], wavAudio[pos + 1]); pos += 2; if (channelCount == 2) { pos += 2; } i++; } return unityData; } #if !UNITY_EDITOR && UNITY_WSA /// /// 执行一个生成语音流的函数,然后在Unity中转换并播放它。 /// /// /// 内容. /// /// /// 执行以生成语音的实际函数 /// private void PlaySpeech(string text, Func<IAsyncOperation<SpeechSynthesisStream>> speakFunc) { //确保有内容 if (speakFunc == null) throw new ArgumentNullException(nameof(speakFunc)); if (synthesizer != null) { try { speechTextInQueue = true; // 需要await,因此大部分将作为一个新任务在自己的线程中运行。 // 这是件好事,因为它解放了Unity,让它可以继续运行。 Task.Run(async () => { // 换声? if (voice != TextToSpeechVoice.Default) { // 获得名称 var voiceName = Enum.GetName(typeof(TextToSpeechVoice), voice); // 查看它是一直没被找到还是有改变 if ((voiceInfo == null) || (!voiceInfo.DisplayName.Contains(voiceName))) { // 搜索声音信息 voiceInfo = SpeechSynthesizer.AllVoices.Where(v => v.DisplayName.Contains(voiceName)).FirstOrDefault(); // 如果找到则选中 if (voiceInfo != null) { synthesizer.Voice = voiceInfo; } else { Debug.LogErrorFormat("TTS 无法找到声音 {0}。", voiceName); } } } // 播放语音并获得流 var speechStream = await speakFunc(); // 获取原始流的大小 var size = speechStream.Size; // 创建 buffer byte[] buffer = new byte[(int)size]; // 获取输入流和原始流的大小 using (var inputStream = speechStream.GetInputStreamAt(0)) { // 关闭原始的语音流,释放内存 speechStream.Dispose(); // 从输入流创建一个新的DataReader using (var dataReader = new DataReader(inputStream)) { //将所有字节加载到reader await dataReader.LoadAsync((uint)size); // 复制reader到buffer dataReader.ReadBytes(buffer); } } // 转换原始WAV数据为统一格式的音频数据 int sampleCount = 0; int frequency = 0; var unityData = ToUnityAudio(buffer, out sampleCount, out frequency); // 剩下的工作须在Unity的主线程中完成 UnityEngine.WSA.Application.InvokeOnAppThread(() => { // 转换为audio clip var clip = ToClip("Speech", unityData, sampleCount, frequency); // 设置audio clip的语音 audioSource.clip = clip; // 播放声音 audioSource.Play(); speechTextInQueue = false; }, false); }); } catch (Exception ex) { speechTextInQueue = false; Debug.LogErrorFormat("语音生成错误: \"{0}\"", ex.Message); } } else { Debug.LogErrorFormat("语音合成器未初始化. \"{0}\"", text); } } #endif private void Awake() { try { if (audioSource == null) { audioSource = GetComponent<AudioSource>(); } #if !UNITY_EDITOR && UNITY_WSA synthesizer = new SpeechSynthesizer(); #endif Instance = this; } catch (Exception ex) { Debug.LogError("不能开始语音合成: " + ex.Message); } } // 公共方法 /// /// 播放指定SSML标记语音. /// ///SSML标记 public void SpeakSsml(string ssml) { // 确保内容不为空 if (string.IsNullOrEmpty(ssml)) { return; } // 传递给辅助方法 #if !UNITY_EDITOR && UNITY_WSA PlaySpeech(ssml, () => synthesizer.SynthesizeSsmlToStreamAsync(ssml)); #else Debug.LogWarningFormat("文字转语音在编辑器下不支持.\n\"{0}\"", ssml); #endif } /// /// 播放指定文本语音. /// ///文本内容 public void StartSpeaking(string text) { // 确保内容不为空 if (string.IsNullOrEmpty(text)) { return; } // 传递给辅助方法 #if !UNITY_EDITOR && UNITY_WSA PlaySpeech(text, ()=> synthesizer.SynthesizeTextToStreamAsync(text)); #else Debug.LogWarningFormat("文字转语音在编辑器下不支持.\n\"{0}\"", text); #endif } /// /// 返回一个文本是否被提交并被PlaySpeech方法处理 /// 方便避免当文本提交,但音频剪辑还没有准备好,因为音频源还没有播放的情况。 /// /// public bool SpeechTextInQueue() { #if !UNITY_EDITOR && UNITY_WSA return speechTextInQueue; #else return false; #endif } /// /// 是否在播放语音. /// /// /// True, 在播放. False,未播放. /// public bool IsSpeaking() { if (audioSource != null) { return audioSource.isPlaying; } return false; } /// /// 停止播放语音. /// public void StopSpeaking() { if (IsSpeaking()) { audioSource.Stop(); } } } }使用方法 一、部署好Unity3d HoloLens项目工程
这一步请自行完成二、新建节点
如上图 在camera节点下新建节点,并添加audio source组件,将position.y设置为 0.6,这样听起来类似于Cortana在操作系统中的讲话。
选择voice 为YaoYao
if (TextToSpeech.Instance) TextToSpeech.Instance.StartSpeaking("这是一条测试语音");五、测试、打包和运行
上图是编辑器下运行的效果图
https://www.roadtomr.com/2016/05/04/1601/text-to-speech-for-hololens/
源码地址:https://github.com/microsoft/MixedRealityToolkit-Unity/blob/htk_release/Assets/HoloToolkit/Utilities/Scripts/TextToSpeech.cs