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>
185 lines
4.5 KiB
Go
185 lines
4.5 KiB
Go
package audio
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"io"
|
||
"os"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
func TestPlayWav(t *testing.T) {
|
||
// 跳过测试如果没有测试文件
|
||
testFile := "testdata/test.wav"
|
||
if _, err := os.Stat(testFile); os.IsNotExist(err) {
|
||
t.Skip("测试文件不存在:", testFile)
|
||
}
|
||
|
||
f, err := os.Open(testFile)
|
||
if err != nil {
|
||
t.Fatalf("打开测试文件失败: %v", err)
|
||
}
|
||
defer f.Close()
|
||
|
||
ctx := context.Background()
|
||
err = PlayWav(ctx, f)
|
||
if err != nil {
|
||
t.Fatalf("PlayWav 失败: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestPlayMP3(t *testing.T) {
|
||
testFile := "testdata/test.mp3"
|
||
if _, err := os.Stat(testFile); os.IsNotExist(err) {
|
||
t.Skip("测试文件不存在:", testFile)
|
||
}
|
||
|
||
f, err := os.Open(testFile)
|
||
if err != nil {
|
||
t.Fatalf("打开测试文件失败: %v", err)
|
||
}
|
||
defer f.Close()
|
||
|
||
ctx := context.Background()
|
||
err = PlayMP3(ctx, f)
|
||
if err != nil {
|
||
t.Fatalf("PlayMP3 失败: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestPlayContextCancellation(t *testing.T) {
|
||
testFile := "testdata/test.mp3"
|
||
if _, err := os.Stat(testFile); os.IsNotExist(err) {
|
||
t.Skip("测试文件不存在:", testFile)
|
||
}
|
||
|
||
f, err := os.Open(testFile)
|
||
if err != nil {
|
||
t.Fatalf("打开测试文件失败: %v", err)
|
||
}
|
||
defer f.Close()
|
||
|
||
// 创建一个会被快速取消的 context
|
||
ctx, cancel := context.WithCancel(context.Background())
|
||
|
||
// 启动播放后立即取消
|
||
done := make(chan error, 1)
|
||
go func() {
|
||
done <- PlayMP3(ctx, f)
|
||
}()
|
||
|
||
time.Sleep(10 * time.Millisecond)
|
||
cancel()
|
||
|
||
err = <-done
|
||
if err != context.Canceled {
|
||
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)
|
||
}
|
||
}
|