feat(audio): 添加单声道转立体声转换功能
All checks were successful
ci/woodpecker/tag/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/tag/woodpecker Pipeline was successful
- 新增 monoToStereoReader 将单声道 WAV 实时转换为立体声 - PlayWav 自动检测单声道并应用转换管线 - 添加完整的单元测试覆盖转换逻辑 - 整理 import 顺序(goimports) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
package audio
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -75,3 +77,108 @@ func TestPlayContextCancellation(t *testing.T) {
|
||||
t.Errorf("期望 context.Canceled 错误,得到: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMonoToStereoReader 测试单声道转立体声
|
||||
func TestMonoToStereoReader(t *testing.T) {
|
||||
// 创建测试数据:4个单声道样本(8字节)
|
||||
monoData := []byte{
|
||||
0x00, 0x10, // 样本1: 0x1000 = 4096
|
||||
0x00, 0x20, // 样本2: 0x2000 = 8192
|
||||
0x00, 0x30, // 样本3: 0x3000 = 12288
|
||||
0x00, 0x40, // 样本4: 0x4000 = 16384
|
||||
}
|
||||
|
||||
reader := &monoToStereoReader{src: bytes.NewReader(monoData)}
|
||||
output := make([]byte, 16) // 应该产生8个样本(16字节)
|
||||
|
||||
n, err := reader.Read(output)
|
||||
if err != nil {
|
||||
t.Fatalf("读取失败: %v", err)
|
||||
}
|
||||
|
||||
if n != 16 {
|
||||
t.Fatalf("期望读取16字节,实际读取%d字节", n)
|
||||
}
|
||||
|
||||
// 验证立体声输出(每个单声道样本被复制到左右声道)
|
||||
expected := []byte{
|
||||
0x00, 0x10, 0x00, 0x10, // 样本1: 左=0x1000, 右=0x1000
|
||||
0x00, 0x20, 0x00, 0x20, // 样本2: 左=0x2000, 右=0x2000
|
||||
0x00, 0x30, 0x00, 0x30, // 样本3: 左=0x3000, 右=0x3000
|
||||
0x00, 0x40, 0x00, 0x40, // 样本4: 左=0x4000, 右=0x4000
|
||||
}
|
||||
|
||||
if !bytes.Equal(output, expected) {
|
||||
t.Errorf("立体声转换不正确\n期望: %x\n实际: %x", expected, output)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMonoToStereoReaderStreaming 测试流式读取
|
||||
func TestMonoToStereoReaderStreaming(t *testing.T) {
|
||||
// 创建较大的测试数据
|
||||
monoData := make([]byte, 1000)
|
||||
for i := range monoData {
|
||||
monoData[i] = byte(i % 256)
|
||||
}
|
||||
|
||||
reader := &monoToStereoReader{src: bytes.NewReader(monoData)}
|
||||
totalRead := 0
|
||||
buf := make([]byte, 32) // 小缓冲区
|
||||
|
||||
for {
|
||||
n, err := reader.Read(buf)
|
||||
totalRead += n
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("流式读取失败: %v", err)
|
||||
}
|
||||
if n == 0 {
|
||||
t.Fatal("读取返回0字节但未EOF")
|
||||
}
|
||||
}
|
||||
|
||||
// 1000字节单声道应该转换为2000字节立体声
|
||||
expectedTotal := 2000
|
||||
if totalRead != expectedTotal {
|
||||
t.Fatalf("期望总共读取%d字节,实际读取%d字节", expectedTotal, totalRead)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMonoToStereoReaderPartialRead 测试部分读取
|
||||
func TestMonoToStereoReaderPartialRead(t *testing.T) {
|
||||
monoData := []byte{0x00, 0x10, 0x00, 0x20, 0x00, 0x30} // 3个单声道样本
|
||||
reader := &monoToStereoReader{src: bytes.NewReader(monoData)}
|
||||
|
||||
// 第一次读取:请求6字节输出(只能读取1个单声道样本=4字节输出)
|
||||
buf1 := make([]byte, 6)
|
||||
n1, err := reader.Read(buf1)
|
||||
if err != nil {
|
||||
t.Fatalf("第一次读取失败: %v", err)
|
||||
}
|
||||
if n1 != 4 {
|
||||
t.Fatalf("第一次读取期望4字节,实际%d字节", n1)
|
||||
}
|
||||
|
||||
// 第二次读取:请求10字节输出(读取剩余2个单声道样本=8字节输出)
|
||||
buf2 := make([]byte, 10)
|
||||
n2, err := reader.Read(buf2)
|
||||
if err != nil {
|
||||
t.Fatalf("第二次读取失败: %v", err)
|
||||
}
|
||||
// 剩余2个单声道样本转换为8字节立体声
|
||||
if n2 != 8 {
|
||||
t.Fatalf("第二次读取期望8字节,实际%d字节", n2)
|
||||
}
|
||||
|
||||
// 第三次读取:应该返回EOF
|
||||
buf3 := make([]byte, 10)
|
||||
n3, err := reader.Read(buf3)
|
||||
if err != io.EOF {
|
||||
t.Fatalf("第三次读取期望EOF,实际: %v", err)
|
||||
}
|
||||
if n3 != 0 {
|
||||
t.Fatalf("第三次读取EOF时期望0字节,实际%d字节", n3)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user