Files
game-driver/pkg/audio/sinc_resampler.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

149 lines
3.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package audio
import (
"io"
resampling "github.com/tphakala/go-audio-resampler"
"go.uber.org/zap"
)
// minProcessSamples 是 FIR 滤波器产生可靠输出所需的最小输入样本数
const minProcessSamples = 64
// needsResampling 检查是否需要重采样
func needsResampling(sourceRate int) bool {
return sourceRate != UniversalSampleRate
}
// sincResampler 基于 go-audio-resampler 的高质量重采样器
// 使用 Windowed Sinc + Polyphase FIR 算法,专业级音质
type sincResampler struct {
decoder io.Reader
resampler resampling.Resampler
inputBuf []float64 // 输入缓冲区int16→float64 转换后暂存
outputBuf []float64 // 输出缓冲区Process/Flush 产出但未消费的样本
inputBytes []byte // 复用的字节读取缓冲区
flushed bool // 是否已完成 Flush
eof bool // 上游是否已返回 EOF
}
// newSincResampler 创建高质量 Sinc 重采样器
// 使用场景:大广场音效、高保真音乐
func newSincResampler(src io.Reader, inRate, outRate, channels int) io.Reader {
if inRate == outRate {
return src
}
config := &resampling.Config{
InputRate: float64(inRate),
OutputRate: float64(outRate),
Channels: channels,
Quality: resampling.QualitySpec{
Preset: resampling.QualityVeryHigh,
},
}
r, err := resampling.New(config)
if err != nil {
zap.S().Warnf("Sinc 重采样器创建失败,降级为透传: %v", err)
return src
}
return &sincResampler{
decoder: src,
resampler: r,
inputBuf: make([]float64, 0, 4096),
outputBuf: make([]float64, 0, 4096),
inputBytes: make([]byte, 1024),
}
}
func (r *sincResampler) Read(p []byte) (int, error) {
if len(p) < 2 {
return 0, io.ErrShortBuffer
}
maxSamples := len(p) / 2
// 主循环:直到有足够输出数据或 EOF
for len(r.outputBuf) < maxSamples {
// 阶段1从上游读取数据累积到 inputBuf
for len(r.inputBuf) < minProcessSamples && !r.eof {
nn, readErr := r.decoder.Read(r.inputBytes)
if readErr != nil && readErr != io.EOF {
return 0, readErr
}
if readErr == io.EOF || nn == 0 {
r.eof = true
break
}
sampleCount := nn / 2
for i := range sampleCount {
sample := int16(r.inputBytes[i*2]) | int16(r.inputBytes[i*2+1])<<8
r.inputBuf = append(r.inputBuf, float64(sample)/32768.0)
}
}
// 阶段2处理输入数据
if len(r.inputBuf) > 0 {
output, err := r.resampler.Process(r.inputBuf)
if err != nil {
return 0, err
}
r.inputBuf = r.inputBuf[:0]
if len(output) > 0 {
r.outputBuf = append(r.outputBuf, output...)
}
continue
}
// 阶段3EOF 且 inputBuf 为空,调用 Flush 获取尾部残留
if r.eof && !r.flushed {
r.flushed = true
flushed, err := r.resampler.Flush()
if err != nil {
return 0, err
}
if len(flushed) > 0 {
r.outputBuf = append(r.outputBuf, flushed...)
}
continue
}
// 无更多数据可获取
break
}
if len(r.outputBuf) == 0 {
return 0, io.EOF
}
// 写入输出
n := min(len(r.outputBuf), maxSamples)
writeFloat64ToLE16(p, r.outputBuf[:n])
if n < len(r.outputBuf) {
r.outputBuf = r.outputBuf[n:]
} else {
r.outputBuf = r.outputBuf[:0]
}
return n * 2, nil
}
// writeFloat64ToLE16 将 float64 样本转换为 int16 LE 写入 buf
func writeFloat64ToLE16(buf []byte, samples []float64) {
for i, s := range samples {
if s > 1.0 {
s = 1.0
} else if s < -1.0 {
s = -1.0
}
v := int32(s * 32768.0)
if v > 32767 {
v = 32767
}
buf[i*2] = byte(v)
buf[i*2+1] = byte(v >> 8)
}
}