feat(audio): 使用 Windowed Sinc 高质量重采样器替代线性插值
Some checks failed
ci/woodpecker/tag/woodpecker Pipeline failed
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:
148
pkg/audio/sinc_resampler.go
Normal file
148
pkg/audio/sinc_resampler.go
Normal 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
|
||||
}
|
||||
|
||||
// 阶段3:EOF 且 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user