Files
game-driver/pkg/tts/aliyun.go
mapleafgo 2f06c696fa refactor: replace interface{} with any for modern Go style
Update callback function signatures to use the any alias introduced in Go 1.18,
improving code readability and following current Go conventions.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-04-09 16:52:48 +08:00

167 lines
3.8 KiB
Go

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,
}
}