Files
game-driver/pkg/audio/play.go
mapleafgo 4ddecb7c30 feat(audio): 添加音频重采样支持,修复播放速度问题
问题:
- TTS 返回 16000 Hz 音频,但 Context 使用 44100 Hz
- 播放速度快 2.75 倍(44100/16000)
- 不同采样率的音频播放速度不正确

解决方案:
- 集成 gomplerate 库(纯 Go,零依赖)
- 自动检测音频采样率并重采样到 44100 Hz
- 支持任意采样率的音频文件正常播放

技术实现:
- resampler.go: 封装 gomplerate,实现流式重采样
- play.go: WAV/MP3 播放自动重采样
- loop.go: BGM 循环播放支持重采样

测试:
- 所有单元测试通过(6/6)
- 支持采样率自动转换(如 16000 Hz → 44100 Hz)

依赖:
- github.com/zeozeozeo/gomplerate v0.0.0
2026-04-08 19:39:58 +08:00

143 lines
3.3 KiB
Go

package audio
import (
"bytes"
"context"
"fmt"
"io"
"time"
"github.com/youpy/go-wav"
"github.com/hajimehoshi/go-mp3"
"go.uber.org/zap"
)
// PlayWav 播放 WAV 文件(阻塞),直到完成或 context 取消
func PlayWav(ctx context.Context, r io.ReadCloser) error {
otoCtx, err := initContext()
if err != nil {
return fmt.Errorf("音频上下文初始化失败: %w", err)
}
// Read the entire file into memory since wav.NewReader needs ReadAt
data, err := io.ReadAll(r)
if err != nil {
r.Close()
return fmt.Errorf("读取 WAV 文件失败: %w", err)
}
r.Close()
// Create a reader from the buffered data
dec := wav.NewReader(bytes.NewReader(data))
// 获取音频格式信息
format, err := dec.Format()
if err != nil {
return fmt.Errorf("获取 WAV 格式失败: %w", err)
}
duration, _ := dec.Duration()
sourceRate := int(format.SampleRate)
targetRate := UniversalSampleRate
channels := int(format.NumChannels)
zap.S().Infof("WAV 音频: %d ch, %d Hz → %d Hz, 时长: %v",
channels, sourceRate, targetRate, duration)
// 需要重采样
var reader io.Reader = dec
if needsResample(sourceRate, targetRate) {
zap.S().Infof("重采样: %d Hz → %d Hz", sourceRate, targetRate)
resampleReader, err := newResamplingReader(dec, sourceRate, targetRate, channels)
if err != nil {
return fmt.Errorf("创建重采样器失败: %w", err)
}
reader = resampleReader
}
player := otoCtx.NewPlayer(reader)
defer player.Close()
player.Play()
// 等待播放完成
done := make(chan struct{})
go func() {
for !player.IsPlaying() {
time.Sleep(10 * time.Millisecond)
}
for player.IsPlaying() {
time.Sleep(10 * time.Millisecond)
}
time.Sleep(200 * time.Millisecond)
close(done)
}()
select {
case <-done:
return nil
case <-ctx.Done():
return ctx.Err()
}
}
// PlayMP3 播放 MP3 文件(阻塞),直到完成或 context 取消
func PlayMP3(ctx context.Context, r io.ReadCloser) error {
otoCtx, err := initContext()
if err != nil {
return fmt.Errorf("音频上下文初始化失败: %w", err)
}
dec, err := mp3.NewDecoder(r)
if err != nil {
r.Close()
return fmt.Errorf("MP3 解码失败: %w", err)
}
defer r.Close()
// MP3 解码器信息
sampleRate := int(dec.SampleRate())
sampleCount := dec.Length()
targetRate := UniversalSampleRate
channels := 2 // MP3 通常是立体声
duration := time.Duration(float64(sampleCount)/float64(sampleRate)*1000) * time.Millisecond
zap.S().Infof("MP3 音频: %d Hz → %d Hz, 时长约: %v", sampleRate, targetRate, duration)
// 需要重采样
var reader io.Reader = dec
if needsResample(sampleRate, targetRate) {
zap.S().Infof("重采样: %d Hz → %d Hz", sampleRate, targetRate)
resampleReader, err := newResamplingReader(dec, sampleRate, targetRate, channels)
if err != nil {
return fmt.Errorf("创建重采样器失败: %w", err)
}
reader = resampleReader
}
player := otoCtx.NewPlayer(reader)
defer player.Close()
player.Play()
// 等待播放完成
done := make(chan struct{})
go func() {
for !player.IsPlaying() {
time.Sleep(10 * time.Millisecond)
}
for player.IsPlaying() {
time.Sleep(10 * time.Millisecond)
}
time.Sleep(200 * time.Millisecond)
close(done)
}()
select {
case <-done:
return nil
case <-ctx.Done():
return ctx.Err()
}
}