package tts import ( "bytes" "context" "errors" "fmt" "game-driver/config" "game-driver/leaf" "game-driver/pkg/audio" "io" "log" "sync" "time" "go.uber.org/zap" nls "github.com/aliyun/alibabacloud-nls-go-sdk" ) // AliTTS 阿里云语音合成 // 同一时间只能播放一个 TTS type AliTTS struct { config.AliyunConfig tokenResult nls.TokenResult mu sync.Mutex // 互斥锁,确保同时只播放一个 } type result struct { Data io.ReadWriter Error error } var DefaultTTS = &AliTTS{} // onTaskFailed TTS 合成失败回调 func (tts *AliTTS) onTaskFailed(text string, param any) { p, _ := param.(*result) p.Error = fmt.Errorf("语音合成异常: %v", text) } // onSynthesisResult TTS 合成数据回调 func (tts *AliTTS) onSynthesisResult(data []byte, param any) { p, _ := param.(*result) p.Data.Write(data) } // Sound 播放 TTS // 如果已有 TTS 在播放,会等待当前播放完成后再播放新的 func (tts *AliTTS) Sound(ctx context.Context, text string) { if text == "" { return } zap.S().Infof("[TTS] 开始播放: %s", text) buf, err := tts.Get(ctx, text) if err != nil { zap.S().Errorw("[TTS] 合成失败", "text", text, "error", err) return } size := buf.(*bytes.Buffer).Len() zap.S().Debugf("[TTS] 合成成功: %s (%d字节)", text, size) // 获取锁,阻塞等待直到可以播放 zap.S().Debugf("[TTS] 等待播放锁: %s", text) tts.mu.Lock() defer tts.mu.Unlock() err = audio.PlayWav(ctx, io.NopCloser(buf)) if err != nil && !errors.Is(err, context.Canceled) { zap.S().Errorf("TTS 播放失败: %v", err) } // 检查是否被取消 if ctx.Err() != nil { zap.S().Debugf("[TTS] 播放被取消: %s", text) return } zap.S().Infof("[TTS] 播放完成: %s", text) } // getToken 获取阿里云 TTS Token func (tts *AliTTS) getToken() error { // Token 未过期则复用 if tts.tokenResult.ExpireTime != 0 && time.Unix(tts.tokenResult.ExpireTime, 0).After(time.Now()) { return nil } tts.tokenResult = nls.TokenResult{} resultMessage, err := nls.GetToken("cn-shanghai", "nls-meta.cn-shanghai.aliyuncs.com", tts.AccessKeyID, tts.AccessKeySecret, "2019-02-28") if err != nil { return err } if resultMessage.ErrMsg != "" { return fmt.Errorf("获取Token失败: %s", resultMessage.ErrMsg) } tts.tokenResult = resultMessage.TokenResult return nil } // Get 合成语音文本(内部方法) func (tts *AliTTS) Get(ctx context.Context, text string) (io.Reader, error) { param := nls.DefaultSpeechSynthesisParam() param.Volume = tts.Volume param.Voice = tts.Voice param.SpeechRate = tts.SpeechRate if err := tts.getToken(); err != nil { return nil, err } connectConfig := nls.NewConnectionConfigWithToken(nls.DEFAULT_URL, tts.AppKey, tts.tokenResult.Id) logger := nls.NewNlsLogger(leaf.DefaultWriter, "", log.LstdFlags|log.Ltime) logger.SetLogSil(false) logger.SetDebug(true) ttsData := &result{ Data: &bytes.Buffer{}, } synthesis, err := nls.NewSpeechSynthesis( connectConfig, logger, false, tts.onTaskFailed, tts.onSynthesisResult, nil, nil, nil, ttsData, ) if err != nil { return ttsData.Data, err } defer synthesis.Shutdown() ch, err := synthesis.Start(text, param, nil) if err != nil { return ttsData.Data, err } // 等待合成完成 select { case done := <-ch: if !done { return ttsData.Data, fmt.Errorf("TTS合成失败") } size := ttsData.Data.(*bytes.Buffer).Len() if size == 0 { return ttsData.Data, fmt.Errorf("TTS合成数据为空") } return ttsData.Data, nil case <-time.After(time.Duration(tts.Timeout) * time.Second): return ttsData.Data, fmt.Errorf("TTS合成超时") case <-ctx.Done(): return ttsData.Data, fmt.Errorf("请求被取消") } } // New 创建 TTS 实例 func New(config config.AliyunConfig) *AliTTS { return &AliTTS{ AliyunConfig: config, } }