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

View File

@@ -0,0 +1,216 @@
package audio
import (
"bytes"
"io"
"math"
"testing"
)
// TestSincResamplerUpsampling 测试上采样 16000Hz → 44100Hz
func TestSincResamplerUpsampling(t *testing.T) {
// VeryHigh 质量 FIR 延迟约 969 输入样本,数据量需远超延迟
inputSamples := make([]int16, 8000)
for i := range inputSamples {
inputSamples[i] = int16(math.Sin(2*math.Pi*440.0*float64(i)/16000.0) * 8000)
}
inputData := encodeInt16LE(inputSamples)
r := newSincResampler(inputData, 16000, 44100, 2).(*sincResampler)
outputSamples := readAllSamples(t, r)
expectedSamples := int(float64(len(inputSamples)) * 44100.0 / 16000.0)
t.Logf("输入: %d 样本 @ 16000Hz", len(inputSamples))
t.Logf("输出: %d 样本 @ 44100Hz (期望 ~%d)", outputSamples, expectedSamples)
if outputSamples == 0 {
t.Fatal("没有输出数据")
}
// 上采样:输出应多于输入
if outputSamples <= len(inputSamples) {
t.Errorf("上采样失败:输出(%d) 应多于输入(%d)", outputSamples, len(inputSamples))
}
assertWithinTolerance(t, outputSamples, expectedSamples, 0.15)
}
// TestSincResamplerPassthrough 测试采样率相同时直接透传
func TestSincResamplerPassthrough(t *testing.T) {
inputSamples := []int16{100, 200, 300, 400, 500, 600}
inputData := encodeInt16LE(inputSamples)
r := newSincResampler(inputData, 16000, 16000, 2)
if _, ok := r.(*bytes.Buffer); !ok {
t.Error("采样率相同时应该直接透传原始 reader")
}
}
// TestSincResamplerDownsampling 测试下采样 44100Hz → 16000Hz
func TestSincResamplerDownsampling(t *testing.T) {
inputSamples := make([]int16, 8000)
for i := range inputSamples {
inputSamples[i] = int16(math.Sin(2*math.Pi*440.0*float64(i)/44100.0) * 8000)
}
inputData := encodeInt16LE(inputSamples)
r := newSincResampler(inputData, 44100, 16000, 2).(*sincResampler)
outputSamples := readAllSamples(t, r)
expectedSamples := int(float64(len(inputSamples)) * 16000.0 / 44100.0)
t.Logf("输入: %d 样本 @ 44100Hz", len(inputSamples))
t.Logf("输出: %d 样本 @ 16000Hz (期望 ~%d)", outputSamples, expectedSamples)
if outputSamples == 0 {
t.Fatal("没有输出数据")
}
// 下采样:输出应少于输入
if outputSamples >= len(inputSamples) {
t.Errorf("下采样失败:输出(%d) 应少于输入(%d)", outputSamples, len(inputSamples))
}
assertWithinTolerance(t, outputSamples, expectedSamples, 0.15)
}
// TestSincResamplerFlush 测试小数据量时 Flush 获取尾部残留
func TestSincResamplerFlush(t *testing.T) {
// 小数据集:输入少于 FIR 延迟,输出主要来自 Flush
inputSamples := make([]int16, 500)
for i := range inputSamples {
inputSamples[i] = int16(i * 100)
}
inputData := encodeInt16LE(inputSamples)
r := newSincResampler(inputData, 16000, 44100, 2).(*sincResampler)
outputSamples := readAllSamples(t, r)
t.Logf("小数据输入: %d 样本, 输出: %d 样本 (来自 Flush)", len(inputSamples), outputSamples)
// 即使输入小于延迟Flush 也应产出数据
if outputSamples == 0 {
t.Fatal("Flush 未产生任何数据")
}
}
// TestSincResamplerShortBuffer 测试 io.Reader 边界行为
func TestSincResamplerShortBuffer(t *testing.T) {
inputSamples := make([]int16, 2000)
for i := range inputSamples {
inputSamples[i] = int16(i)
}
inputData := encodeInt16LE(inputSamples)
r := newSincResampler(inputData, 16000, 44100, 2).(*sincResampler)
// 1 字节 buffer → ErrShortBuffer
_, err := r.Read(make([]byte, 1))
if err != io.ErrShortBuffer {
t.Errorf("期望 io.ErrShortBuffer得到: %v", err)
}
// 2 字节 buffer → 正常工作
buf := make([]byte, 2)
n, err := r.Read(buf)
if n != 2 || err != nil {
t.Errorf("2 字节 buffer 应正常读取: n=%d, err=%v", n, err)
}
}
// TestSincResamplerStreaming 测试流式多次 Read 的正确性
func TestSincResamplerStreaming(t *testing.T) {
inputSamples := make([]int16, 10000)
for i := range inputSamples {
inputSamples[i] = int16(math.Sin(2*math.Pi*440.0*float64(i)/16000.0) * 8000)
}
inputData := encodeInt16LE(inputSamples)
r := newSincResampler(inputData, 16000, 44100, 2).(*sincResampler)
// 小 buffer 模拟流式读取
buf := make([]byte, 128)
totalSamples := 0
readCount := 0
for {
n, err := r.Read(buf)
if n > 0 {
totalSamples += n / 2
readCount++
}
if err == io.EOF {
break
}
if err != nil {
t.Fatalf("读取失败: %v", err)
}
}
expectedSamples := int(float64(len(inputSamples)) * 44100.0 / 16000.0)
t.Logf("流式读取: %d 次, 共 %d 样本 (期望 ~%d)", readCount, totalSamples, expectedSamples)
if readCount < 50 {
t.Errorf("流式读取次数过少: %d", readCount)
}
assertWithinTolerance(t, totalSamples, expectedSamples, 0.15)
}
// TestSincResamplerSineWave 测试已知正弦波信号的重采样
func TestSincResamplerSineWave(t *testing.T) {
const freq = 440.0
const inRate = 16000
inputSamples := make([]int16, inRate/4) // 0.25 秒
for i := range inputSamples {
inputSamples[i] = int16(math.Sin(2*math.Pi*freq*float64(i)/float64(inRate)) * 16000)
}
inputData := encodeInt16LE(inputSamples)
r := newSincResampler(inputData, inRate, 44100, 2).(*sincResampler)
output := readAllSamples(t, r)
expected := int(float64(len(inputSamples)) * 44100.0 / float64(inRate))
t.Logf("440Hz 正弦波: %d → %d 样本 (期望 ~%d)", len(inputSamples), output, expected)
if output == 0 {
t.Fatal("正弦波重采样无输出")
}
assertWithinTolerance(t, output, expected, 0.15)
}
// --- 辅助函数 ---
func encodeInt16LE(samples []int16) *bytes.Buffer {
buf := bytes.NewBuffer(nil)
for _, s := range samples {
buf.Write([]byte{byte(s), byte(s >> 8)})
}
return buf
}
func readAllSamples(t *testing.T, r io.Reader) int {
t.Helper()
outputData := bytes.NewBuffer(nil)
buf := make([]byte, 4096)
for {
n, err := r.Read(buf)
if n > 0 {
outputData.Write(buf[:n])
}
if err == io.EOF {
break
}
if err != nil {
t.Fatalf("读取失败: %v", err)
}
}
return outputData.Len() / 2
}
func assertWithinTolerance(t *testing.T, actual, expected int, tolerance float64) {
t.Helper()
delta := math.Abs(float64(actual - expected))
maxDelta := float64(expected) * tolerance
if delta > maxDelta && delta > 10 {
t.Errorf("超出容忍度: 实际 %d, 期望 %d (差: %.0f, 上限: %.0f)",
actual, expected, delta, maxDelta)
}
}