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:
216
pkg/audio/sinc_resampler_test.go
Normal file
216
pkg/audio/sinc_resampler_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user