Files
game-driver/pkg/audio/play.go
mapleafgo ec168be827
Some checks failed
ci/woodpecker/tag/woodpecker Pipeline failed
feat(audio): 使用 Windowed Sinc 高质量重采样器替代线性插值
统一音频输出采样率为 44100Hz,使用 go-audio-resampler 库实现
Windowed Sinc + Polyphase FIR 算法(VeryHigh 28-bit 精度),
替代原有的线性插值透传方案。

主要变更:
- 新增 sincResampler:三阶段 Read 循环(填充→处理→Flush)
- 双缓冲区架构避免输出样本丢失,复用内存减少 GC 压力
- WAV/MP3/BGM 播放管线全部接入 Sinc 重采样器
- 移除旧的 linearResampler 和透传模式

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 01:35:04 +08:00

133 lines
3.0 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 {
// 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)
zap.S().Infof("WAV 音频: %d ch, %d Hz, 时长: %v",
format.NumChannels, sourceRate, duration)
// 需要重采样(使用 Sinc 高质量重采样)
var reader io.Reader = dec
if needsResampling(sourceRate) {
zap.S().Infof("Sinc 重采样: %d Hz → %d Hz", sourceRate, UniversalSampleRate)
reader = newSincResampler(dec, sourceRate, UniversalSampleRate, int(format.NumChannels))
}
otoCtx, err := initContext()
if err != nil {
return fmt.Errorf("音频上下文初始化失败: %w", err)
}
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 {
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()
channels := 2 // MP3 通常是立体声
duration := time.Duration(float64(sampleCount)/float64(sampleRate)*1000) * time.Millisecond
zap.S().Infof("MP3 音频: %d Hz → %d Hz, 时长约: %v",
sampleRate, UniversalSampleRate, duration)
// 需要重采样(使用 Sinc 高质量重采样)
var reader io.Reader = dec
if needsResampling(sampleRate) {
zap.S().Infof("Sinc 重采样: %d Hz → %d Hz", sampleRate, UniversalSampleRate)
reader = newSincResampler(dec, sampleRate, UniversalSampleRate, channels)
}
otoCtx, err := initContext()
if err != nil {
return fmt.Errorf("音频上下文初始化失败: %w", err)
}
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()
}
}