2 Commits

Author SHA1 Message Date
2331d0c73f fix(tts): 修复 TTS 播放卡死问题并增强日志
All checks were successful
ci/woodpecker/tag/woodpecker Pipeline was successful
- 添加 SoundWithContext 方法,使用请求 context 而非全局 context
- 修复 TTS 使用服务器全局 context 导致无法取消的问题
- 添加详细的诊断日志(解码、播放、TTS 合成各阶段)
- 检测并记录 TTS 合成数据为空的情况

修复前 TTS 播放使用全局 context,当播放卡住时无法通过超时
或取消机制中断,导致后续任务永远无法执行。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 12:07:39 +08:00
5ee8e15965 fix(audio): 修复音频播放死循环并增强错误日志
All checks were successful
ci/woodpecker/tag/woodpecker Pipeline was successful
- 修复 PlayWav 和 PlayMP3 在 context 取消时的死循环 bug
- 添加 WAV/MP3 解码失败的错误日志
- 添加 TTS 播放开始/完成的日志,便于排查问题

修复前 context 取消会导致无限循环,阻塞后续任务执行。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 11:34:27 +08:00
3 changed files with 50 additions and 4 deletions

View File

@@ -10,16 +10,24 @@ import (
func SoundStart() leaf.HandlerFunc {
return func(c *leaf.Context) {
pm := leaf.Value[*schema.PlayModal](c, PayloadJSONKey)
tts.DefaultTTS.Sound(pm.TTS.Start)
// 使用请求的 context支持取消和超时
if pm.TTS.Start != "" {
tts.DefaultTTS.SoundWithContext(c, pm.TTS.Start)
}
defer func() {
var text string
switch leaf.Value[leaf.EndType](c, leaf.EndKey) {
case leaf.End:
tts.DefaultTTS.Sound(pm.TTS.End)
text = pm.TTS.End
case leaf.EndTimeout:
tts.DefaultTTS.Sound(pm.TTS.Timeout)
text = pm.TTS.Timeout
case leaf.EndStop:
tts.DefaultTTS.Sound(pm.TTS.Stop)
text = pm.TTS.Stop
}
if text != "" {
tts.DefaultTTS.SoundWithContext(c, text)
}
}()

View File

@@ -22,29 +22,37 @@ func init() {
}
func PlayWav(c context.Context, r io.Reader) {
zap.S().Debugln("开始 WAV 解码")
streamer, format, err := wav.Decode(r)
if err != nil {
zap.S().Errorln("WAV解码失败: ", err)
return
}
defer streamer.Close()
zap.S().Debugln("WAV解码成功采样率:", format.SampleRate)
s := beep.Resample(4, format.SampleRate, DefaultSampleRate, streamer)
ctrl := &beep.Ctrl{Streamer: s}
done := make(chan struct{})
speaker.Play(beep.Seq(ctrl, beep.Callback(func() {
zap.S().Debugln("音频播放完成")
close(done)
})))
zap.S().Debugln("等待音频播放完成...")
for {
select {
case <-done:
zap.S().Infoln("音频播放正常结束")
return
case <-c.Done():
{
zap.S().Infoln("音频播放被 context 取消")
speaker.Lock()
ctrl.Streamer = nil
speaker.Unlock()
return
}
}
}
@@ -53,6 +61,7 @@ func PlayWav(c context.Context, r io.Reader) {
func PlayMP3(c context.Context, r io.ReadCloser) {
streamer, format, err := mp3.Decode(r)
if err != nil {
zap.S().Errorln("MP3解码失败: ", err)
return
}
defer streamer.Close()
@@ -74,6 +83,7 @@ func PlayMP3(c context.Context, r io.ReadCloser) {
speaker.Lock()
ctrl.Streamer = nil
speaker.Unlock()
return
}
}
}

View File

@@ -45,9 +45,28 @@ func (tts *AliTTS) Sound(text string) {
if text == "" {
return
}
zap.S().Infof("开始播放TTS: %s", text)
buf, err := tts.Get(text)
if err == nil && buf != nil {
zap.S().Debugln("TTS合成成功开始播放")
audio.PlayWav(tts.ctx, buf)
zap.S().Infof("TTS播放完成: %s", text)
} else {
zap.S().Errorln("AliTTS 请求异常: ", err)
}
}
// SoundWithContext 使用指定的 context 播放 TTS支持取消和超时
func (tts *AliTTS) SoundWithContext(ctx context.Context, text string) {
if text == "" {
return
}
zap.S().Infof("开始播放TTS: %s", text)
buf, err := tts.Get(text)
if err == nil && buf != nil {
zap.S().Debugln("TTS合成成功开始播放")
audio.PlayWav(ctx, buf)
zap.S().Infof("TTS播放完成: %s", text)
} else {
zap.S().Errorln("AliTTS 请求异常: ", err)
}
@@ -110,13 +129,22 @@ func (tts *AliTTS) Get(text string) (io.Reader, error) {
case done := <-ch:
{
if !done {
zap.S().Errorln("TTS合成失败: done=false")
return ttsData.Data, errorsx.ThirdPartyErr
}
size := ttsData.Data.(*bytes.Buffer).Len()
zap.S().Debugf("TTS合成成功数据大小: %d bytes", size)
if size == 0 {
zap.S().Errorln("TTS合成数据为空")
return ttsData.Data, errorsx.ThirdPartyErr
}
return ttsData.Data, nil
}
case <-time.After(time.Duration(tts.Timeout) * time.Second):
zap.S().Errorln("TTS合成超时")
return ttsData.Data, errorsx.DriverTimeoutErr
case <-tts.ctx.Done():
zap.S().Errorln("TTS合成被取消")
return ttsData.Data, errorsx.DriverCancelErr
}
}