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>
149 lines
3.5 KiB
Go
149 lines
3.5 KiB
Go
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)
|
||
}
|
||
}
|