feat(audio): 使用 Windowed Sinc 高质量重采样器替代线性插值
Some checks failed
ci/woodpecker/tag/woodpecker Pipeline failed

统一音频输出采样率为 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>
This commit is contained in:
2026-04-09 01:35:04 +08:00
parent 1feb9f1e75
commit ec168be827
10 changed files with 415 additions and 192 deletions

148
pkg/audio/sinc_resampler.go Normal file
View File

@@ -0,0 +1,148 @@
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)
}
}