基于Amazon Polly的异步语音合成请求队列与顺序音频输出实现方案问询
解决异步文本队列+Polly语音合成+顺序播放的阻塞问题
我明白你的需求:要做一个不阻塞控制台输入的异步队列,文本进来先排队,用Polly合成音频后按顺序播放,还要支持一键停止所有任务和队列。咱们一步一步来改代码,完全贴合你的需求,同时帮你理解C#异步和线程的核心点。
核心思路拆解
咱们需要三个核心组件:
- 线程安全的队列:存放待处理的文本,保证输入线程和处理线程能安全访问
- 取消令牌(CancellationToken):用来统一触发"停止"指令,取消正在合成的请求、中断播放、清空队列
- 独立的消费任务:单独负责从队列取文本、调用Polly合成、按顺序播放,不阻塞输入线程
改造后的完整代码
1. 升级PollyVoiceUnity类(加入队列、取消机制、异步播放)
using UnityEngine; using Amazon.Polly; using Amazon.Polly.Model; using NAudio.Wave; using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; namespace UnityTTS; public class PollyVoiceUnity : MonoBehaviour { private static string awsAccessKey = "myKey"; private static string awsAccessSecret = "mySecretKey"; private static AmazonPollyClient client; // 线程安全的文本队列,输入和处理线程都能安全操作 private static readonly ConcurrentQueue<string> _speechQueue = new ConcurrentQueue<string>(); // 取消令牌源,用来发送停止信号 private static CancellationTokenSource _cts; // 标记是否正在运行处理任务 private static bool _isProcessing; public static void Init() { client = new AmazonPollyClient(awsAccessKey, awsAccessSecret, Amazon.RegionEndpoint.EUWest2); _cts = new CancellationTokenSource(); } // 对外暴露的添加文本到队列的方法,完全不阻塞 public static void AddSpeechToQueue(string s) { if (!string.IsNullOrEmpty(s) && !_cts.Token.IsCancellationRequested) { _speechQueue.Enqueue(s); // 如果还没开始处理队列,启动处理任务 if (!_isProcessing) { _ = ProcessQueueAsync(_cts.Token); } } } // 停止所有操作:取消令牌、清空队列、中断播放 public static void StopAll() { _cts.Cancel(); _speechQueue.Clear(); _isProcessing = false; // 重置令牌源,方便后续重新启动(可选) _cts = new CancellationTokenSource(); } // 核心:异步处理队列,按顺序合成+播放 private static async Task ProcessQueueAsync(CancellationToken token) { _isProcessing = true; try { while (!token.IsCancellationRequested) { // 尝试从队列取文本,没有的话等待100ms再重试,避免空循环占CPU if (_speechQueue.TryDequeue(out var text)) { try { // 调用Polly合成,传入取消令牌,支持中途取消 var response = await GetSpeechResponse(text, token); // 异步播放音频,等待播放完成再处理下一个,保证顺序 await PlayAudioAsync(response, token); } catch (OperationCanceledException) { // 捕获取消异常,直接退出循环 break; } catch (Exception ex) { // 处理其他异常,比如Polly调用失败 Console.WriteLine($"处理文本失败: {ex.Message}"); } } else { await Task.Delay(100, token); } } } finally { _isProcessing = false; } } // 带取消令牌的Polly合成方法 private static async Task<SynthesizeSpeechResponse> GetSpeechResponse(string s, CancellationToken token) { var synthesizeSpeechRequest = new SynthesizeSpeechRequest() { OutputFormat = OutputFormat.Pcm, VoiceId = VoiceId.Brian, TextType = TextType.Text, Text = s, SampleRate = "16000" }; // 传入取消令牌,支持中途取消合成请求 return await client.SynthesizeSpeechAsync(synthesizeSpeechRequest, token); } // 异步播放音频,不用阻塞线程 private static Task PlayAudioAsync(SynthesizeSpeechResponse response, CancellationToken token) { var tcs = new TaskCompletionSource<bool>(); using (var ms = new MemoryStream()) { response.AudioStream.CopyTo(ms); byte[] buf = ms.GetBuffer(); var source = new BufferedWaveProvider(new WaveFormat(16000, 16, 1)); source.ReadFully = false; source.AddSamples(buf, 0, buf.Length); using (var waveOut = new WaveOutEvent()) { waveOut.Init(source); // 播放停止时完成任务 waveOut.PlaybackStopped += (sender, e) => { if (e.Exception != null) { tcs.SetException(e.Exception); } else { tcs.SetResult(true); } }; // 监听取消令牌,触发停止播放 token.Register(() => waveOut.Stop()); waveOut.Play(); } } return tcs.Task; } }
2. 改造主程序(非阻塞输入)
using UnityTTS; using System.Threading.Tasks; class Program { public static async Task Main() { PollyVoiceUnity.Init(); Console.WriteLine("输入文本开始合成,输入'stop'停止所有操作"); // 启动输入监听任务,完全独立于处理队列的任务 var inputTask = Task.Run(() => { string? input; while (true) { input = Console.ReadLine(); if (input == "stop") { PollyVoiceUnity.StopAll(); Console.WriteLine("已停止所有操作"); break; } else if (!string.IsNullOrEmpty(input)) { // 只把文本加入队列,立即返回,不阻塞输入 PollyVoiceUnity.AddSpeechToQueue(input); Console.WriteLine($"已加入队列: {input}"); } } }); // 等待输入任务结束 await inputTask; } }
关键知识点解释
咱们来拆解一下为什么这么改:
- ConcurrentQueue:C#自带的线程安全队列,多个线程同时读写也不会出问题,完美适配输入线程和处理线程的场景
- CancellationTokenSource:相当于一个全局的"停止开关",调用
Cancel()后,所有关联的异步操作都会收到停止信号,包括Polly的合成请求和播放任务 - 异步播放(PlayAudioAsync):用
TaskCompletionSource把NAudio的事件回调转换成Task,这样处理队列的任务可以await播放完成,保证音频按顺序播放,但又不会阻塞整个线程 - 独立的输入任务:用
Task.Run()把控制台输入放到单独的线程,这样即使处理队列在合成/播放,输入也完全不受影响
Unity适配注意事项(提前给你划重点)
后续移植到Unity时,需要注意两个点:
- 音频播放必须在主线程:Unity的AudioClip相关操作不能在子线程,所以播放音频时需要把逻辑切回主线程,可以用
UnityMainThreadDispatcher这类工具,或者自己实现一个主线程任务队列 - Polly初始化:Unity中不要在静态构造函数里初始化Polly客户端,最好在
Awake()或Start()方法里初始化,避免线程问题
内容的提问来源于stack exchange,提问作者leonization2050
相关产品推荐
相关产品推荐

