Unity C# 之 Azure 微软SSML语音合成TTS流式获取音频数据以及表情嘴型 Animation 的简单整理
目录
Unity C# 之 Azure 微软SSML语音合成TTS流式获取音频数据以及表情嘴型 Animation 的简单整理
一、简单介绍
Unity 工具类,自己整理的一些游戏开发可能用到的模块,单独独立使用,方便游戏开发。
本节介绍,这里在使用微软的Azure 使用SSML进行SS语音合成的音频,并且获取表情嘴型Animation 数据,并且保存到本地,在特定的情况下,用于本地读取音频和表情嘴型Animation 数据,直接使用,避免可能网络访问造成的延迟问题,这里简单说明,如果你有更好的方法,欢迎留言交流。
SSML 语音和声音
语音合成标记语言 (SSML) 的语音和声音 - 语音服务 - Azure AI services | Microsoft Learn
官网注册:
面向学生的 Azure - 免费帐户额度 | Microsoft Azure
官网技术文档网址:
官网的TTS:
文本转语音快速入门 - 语音服务 - Azure Cognitive Services | Microsoft Learn
Azure Unity SDK 包官网:
安装语音 SDK - Azure Cognitive Services | Microsoft Learn
SDK具体链接:
https://aka.ms/csspeech/unitypackage
二、实现原理
1、官网申请得到语音合成对应的 SPEECH_KEY 和 SPEECH_REGION
2、然后对应设置 语言 和需要的声音 配置
3、使用 SSML 带有流式获取得到音频数据,在声源中播放或者保存即可,样例如下
public static async Task SynthesizeAudioAsync()
{
var speechConfig = SpeechConfig.FromSubscription("YourSpeechKey", "YourSpeechRegion");
using var speechSynthesizer = new SpeechSynthesizer(speechConfig, null);
var ssml = File.ReadAllText("./ssml.xml");
var result = await speechSynthesizer.SpeakSsmlAsync(ssml);
using var stream = AudioDataStream.FromResult(result);
await stream.SaveToWaveFileAsync("path/to/write/file.wav");
}
4、本地保存音频,以及表情嘴型 Animation 数据
// 获取到视频的数据,保存为 .wav
using var stream = AudioDataStream.FromResult(speechSynthesisResult);
await stream.SaveToWaveFileAsync($"./{fileName}.wav");
/// <summary>
/// 嘴型 animation 数据,本地保存为 json 数据
/// </summary>
/// <param name="fileName">保存文件名</param>
/// <param name="content">保存内容</param>
/// <returns></returns>
static async Task CommitAsync(string fileName,string content)
{
var bits = Encoding.UTF8.GetBytes(content);
using (var fs = new FileStream(
path: @$"d:\temp\{fileName}.json",
mode: FileMode.Create,
access: FileAccess.Write,
share: FileShare.None,
bufferSize: 4096,
useAsync: true))
{
await fs.WriteAsync(bits, 0, bits.Length);
}
}
三、注意事项
1、不是所有的 speechSynthesisVoiceName 都能生成对应的 表情嘴型 Animation 数据
四、实现步骤
这里是直接使用 .Net VS 中进行代码测试
1、在 NuGet 中安装 微软的 Speech 包
2、代码编写实现 SSML 合成语音,并且本地保存对应的 音频文件和表情嘴型 Animation json 数据
3、运行代码,运行完后,就会本地保存对应的 音频文件和表情嘴型 Animation json 数据
4、本地查看保存的数据
五、关键代码
using Microsoft.CognitiveServices.Speech;
using System.Text;
class Program
{
// This example requires environment variables named "SPEECH_KEY" and "SPEECH_REGION"
static string speechKey = "YOUR_SPEECH_KEY";
static string speechRegion = "YOUR_SPEECH_REGION";
static string speechSynthesisVoiceName = "zh-CN-XiaoxiaoNeural";
static string fileName = "Test" + "Hello";
static string InputAudioContent = "黄河之水天上来,奔流到海不复回"; // 生成的
static int index = 0; // 记录合成的表情口型动画的数据数组个数
static string content="["; // [ 是为了组成 json 数组
async static Task Main(string[] args)
{
var speechConfig = SpeechConfig.FromSubscription(speechKey, speechRegion);
// 根据需要可以使用更多 xml 配置,让合成的声音更加生动立体
var ssml = @$"<speak version='1.0' xml:lang='zh-CN' xmlns='http://www.w3.org/2001/10/synthesis' xmlns:mstts='http://www.w3.org/2001/mstts'>
<voice name='{speechSynthesisVoiceName}'>
<mstts:viseme type='FacialExpression'/>
<mstts:express-as style='friendly'>{InputAudioContent}</mstts:express-as>
</voice>
</speak>";
// Required for sentence-level WordBoundary events
speechConfig.SetProperty(PropertyId.SpeechServiceResponse_RequestSentenceBoundary, "true");
using (var speechSynthesizer = new SpeechSynthesizer(speechConfig))
{
// Subscribe to events
// 注册表情嘴型数据
speechSynthesizer.VisemeReceived += async (s, e) =>
{
Console.WriteLine($"VisemeReceived event:" +
$"\r\n\tAudioOffset: {(e.AudioOffset + 5000) / 10000}ms"
+ $"\r\n\tVisemeId: {e.VisemeId}"
// + $"\r\n\tAnimation: {e.Animation}"
);
if (string.IsNullOrEmpty( e.Animation)==false)
{
// \r\n, 是为了组合 json 格式
content += e.Animation + "\r\n,";
index++;
}
};
// 注册合成完毕的事件
speechSynthesizer.SynthesisCompleted += async (s, e) =>
{
Console.WriteLine($"SynthesisCompleted event:" +
$"\r\n\tAudioData: {e.Result.AudioData.Length} bytes" +
$"\r\n\tindex: {index} " +
$"\r\n\tAudioDuration: {e.Result.AudioDuration}");
content = content.Substring(0, content.Length-1);
content += "]";
await CommitAsync(fileName, content);
};
// Synthesize the SSML
Console.WriteLine($"SSML to synthesize: \r\n{ssml}");
var speechSynthesisResult = await speechSynthesizer.SpeakSsmlAsync(ssml);
// 获取到视频的数据,保存为 .wav
using var stream = AudioDataStream.FromResult(speechSynthesisResult);
await stream.SaveToWaveFileAsync(@$"d:\temp\{fileName}.wav");
// Output the results
switch (speechSynthesisResult.Reason)
{
case ResultReason.SynthesizingAudioCompleted:
Console.WriteLine("SynthesizingAudioCompleted result");
break;
case ResultReason.Canceled:
var cancellation = SpeechSynthesisCancellationDetails.FromResult(speechSynthesisResult);
Console.WriteLine($"CANCELED: Reason={cancellation.Reason}");
if (cancellation.Reason == CancellationReason.Error)
{
Console.WriteLine($"CANCELED: ErrorCode={cancellation.ErrorCode}");
Console.WriteLine($"CANCELED: ErrorDetails=[{cancellation.ErrorDetails}]");
Console.WriteLine($"CANCELED: Did you set the speech resource key and region values?");
}
break;
default:
break;
}
}
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
/// <summary>
/// 嘴型 animation 数据,本地保存为 json 数据
/// </summary>
/// <param name="fileName">保存文件名</param>
/// <param name="content">保存内容</param>
/// <returns></returns>
static async Task CommitAsync(string fileName,string content)
{
var bits = Encoding.UTF8.GetBytes(content);
using (var fs = new FileStream(
path: @$"d:\temp\{fileName}.json",
mode: FileMode.Create,
access: FileAccess.Write,
share: FileShare.None,
bufferSize: 4096,
useAsync: true))
{
await fs.WriteAsync(bits, 0, bits.Length);
}
}
}