You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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时,需要注意两个点:

  1. 音频播放必须在主线程:Unity的AudioClip相关操作不能在子线程,所以播放音频时需要把逻辑切回主线程,可以用UnityMainThreadDispatcher这类工具,或者自己实现一个主线程任务队列
  2. Polly初始化:Unity中不要在静态构造函数里初始化Polly客户端,最好在Awake()或Start()方法里初始化,避免线程问题

内容的提问来源于stack exchange,提问作者leonization2050

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 21:32:26