222 lines
7.5 KiB
Dart
222 lines
7.5 KiB
Dart
import 'dart:io';
|
||
import 'dart:typed_data';
|
||
import 'package:flutter_test/flutter_test.dart';
|
||
import 'package:yx_asr/yx_asr.dart';
|
||
import '../mocks/mock_speech_service.dart';
|
||
|
||
void main() {
|
||
group('音频文件测试', () {
|
||
late MockSpeechService mockService;
|
||
|
||
setUp(() {
|
||
mockService = MockSpeechService();
|
||
});
|
||
|
||
tearDown(() async {
|
||
await mockService.dispose();
|
||
});
|
||
|
||
test('应该能够读取测试音频文件', () async {
|
||
// 检查测试音频文件是否存在
|
||
final audioFiles = [
|
||
'test/test_wavs/0.wav',
|
||
'test/test_wavs/1.wav',
|
||
'test/test_wavs/8k.wav',
|
||
];
|
||
|
||
for (final filePath in audioFiles) {
|
||
final file = File(filePath);
|
||
expect(file.existsSync(), true, reason: '音频文件 $filePath 应该存在');
|
||
|
||
// 检查文件大小
|
||
final fileSize = await file.length();
|
||
expect(fileSize, greaterThan(0), reason: '音频文件 $filePath 不应该为空');
|
||
|
||
print('✅ $filePath: ${fileSize} bytes');
|
||
}
|
||
});
|
||
|
||
test('应该能够读取音频文件数据', () async {
|
||
final file = File('test/test_wavs/0.wav');
|
||
expect(file.existsSync(), true);
|
||
|
||
// 读取音频数据
|
||
final audioData = await file.readAsBytes();
|
||
expect(audioData.length, greaterThan(44), reason: 'WAV文件应该至少包含44字节的头部');
|
||
|
||
// 验证WAV文件头
|
||
final header = String.fromCharCodes(audioData.sublist(0, 4));
|
||
expect(header, 'RIFF', reason: '应该是有效的WAV文件');
|
||
|
||
final format = String.fromCharCodes(audioData.sublist(8, 12));
|
||
expect(format, 'WAVE', reason: '应该是WAVE格式');
|
||
|
||
print('✅ 音频文件格式验证通过');
|
||
print(' - 文件大小: ${audioData.length} bytes');
|
||
print(' - 格式: $header/$format');
|
||
});
|
||
|
||
test('应该能够解析WAV文件头信息', () async {
|
||
final file = File('test/test_wavs/0.wav');
|
||
final audioData = await file.readAsBytes();
|
||
|
||
// 解析WAV文件头
|
||
final wavInfo = _parseWavHeader(audioData);
|
||
|
||
expect(wavInfo['sampleRate'], 16000, reason: '采样率应该是16kHz');
|
||
expect(wavInfo['channels'], 1, reason: '应该是单声道');
|
||
expect(wavInfo['bitsPerSample'], 16, reason: '应该是16位');
|
||
|
||
print('✅ WAV文件信息:');
|
||
print(' - 采样率: ${wavInfo['sampleRate']} Hz');
|
||
print(' - 声道数: ${wavInfo['channels']}');
|
||
print(' - 位深度: ${wavInfo['bitsPerSample']} bit');
|
||
print(' - 数据长度: ${wavInfo['dataLength']} bytes');
|
||
});
|
||
|
||
test('应该能够处理不同采样率的音频文件', () async {
|
||
final testFiles = {
|
||
'test/test_wavs/0.wav': 16000,
|
||
'test/test_wavs/1.wav': 16000,
|
||
'test/test_wavs/8k.wav': 8000,
|
||
};
|
||
|
||
for (final entry in testFiles.entries) {
|
||
final file = File(entry.key);
|
||
final expectedSampleRate = entry.value;
|
||
|
||
if (file.existsSync()) {
|
||
final audioData = await file.readAsBytes();
|
||
final wavInfo = _parseWavHeader(audioData);
|
||
|
||
expect(wavInfo['sampleRate'], expectedSampleRate,
|
||
reason: '${entry.key} 的采样率应该是 ${expectedSampleRate}Hz');
|
||
|
||
print('✅ ${entry.key}: ${wavInfo['sampleRate']}Hz');
|
||
}
|
||
}
|
||
});
|
||
|
||
test('应该能够提取音频PCM数据', () async {
|
||
final file = File('test/test_wavs/0.wav');
|
||
final audioData = await file.readAsBytes();
|
||
|
||
// 提取PCM数据
|
||
final pcmData = _extractPcmData(audioData);
|
||
expect(pcmData.length, greaterThan(0), reason: 'PCM数据不应该为空');
|
||
|
||
// 转换为Float32格式(模拟sherpa_onnx需要的格式)
|
||
final float32Data = _convertToFloat32(pcmData);
|
||
expect(float32Data.length, pcmData.length ~/ 2,
|
||
reason: 'Float32数据长度应该是Int16数据长度的一半');
|
||
|
||
print('✅ PCM数据提取成功:');
|
||
print(' - 原始数据: ${pcmData.length} bytes');
|
||
print(' - Float32数据: ${float32Data.length} samples');
|
||
|
||
// 验证数据范围
|
||
bool validRange = true;
|
||
for (final sample in float32Data) {
|
||
if (sample < -1.0 || sample > 1.0) {
|
||
validRange = false;
|
||
break;
|
||
}
|
||
}
|
||
expect(validRange, true, reason: 'Float32样本应该在-1.0到1.0范围内');
|
||
});
|
||
|
||
test('模拟使用音频文件进行识别测试', () async {
|
||
final file = File('test/test_wavs/0.wav');
|
||
final audioData = await file.readAsBytes();
|
||
final pcmData = _extractPcmData(audioData);
|
||
final float32Data = _convertToFloat32(pcmData);
|
||
|
||
// 模拟识别过程
|
||
bool resultReceived = false;
|
||
String recognizedText = '';
|
||
|
||
mockService.onResult.listen((result) {
|
||
resultReceived = true;
|
||
recognizedText = result.recognizedWords;
|
||
});
|
||
|
||
// 模拟开始识别
|
||
await mockService.startListening();
|
||
expect(mockService.isListening, true);
|
||
|
||
// 模拟发送音频数据(在真实场景中,这会是sherpa_onnx处理的)
|
||
// 这里我们直接模拟识别结果
|
||
mockService.mockResult('测试音频识别结果');
|
||
|
||
// 验证结果
|
||
await Future.delayed(const Duration(milliseconds: 10));
|
||
expect(resultReceived, true, reason: '应该接收到识别结果');
|
||
expect(recognizedText, '测试音频识别结果');
|
||
|
||
print('✅ 音频识别模拟测试通过');
|
||
print(' - 音频数据: ${float32Data.length} samples');
|
||
print(' - 识别结果: $recognizedText');
|
||
});
|
||
});
|
||
}
|
||
|
||
/// 解析WAV文件头信息
|
||
Map<String, int> _parseWavHeader(Uint8List data) {
|
||
final view = ByteData.sublistView(data);
|
||
|
||
// 跳过RIFF头部,找到fmt chunk
|
||
int offset = 12;
|
||
while (offset < data.length - 8) {
|
||
final chunkId = String.fromCharCodes(data.sublist(offset, offset + 4));
|
||
final chunkSize = view.getUint32(offset + 4, Endian.little);
|
||
|
||
if (chunkId == 'fmt ') {
|
||
final sampleRate = view.getUint32(offset + 12, Endian.little);
|
||
final channels = view.getUint16(offset + 10, Endian.little);
|
||
final bitsPerSample = view.getUint16(offset + 22, Endian.little);
|
||
|
||
// 找到data chunk
|
||
int dataOffset = offset + 8 + chunkSize;
|
||
while (dataOffset < data.length - 8) {
|
||
final dataChunkId = String.fromCharCodes(data.sublist(dataOffset, dataOffset + 4));
|
||
if (dataChunkId == 'data') {
|
||
final dataLength = view.getUint32(dataOffset + 4, Endian.little);
|
||
return {
|
||
'sampleRate': sampleRate,
|
||
'channels': channels,
|
||
'bitsPerSample': bitsPerSample,
|
||
'dataLength': dataLength,
|
||
'dataOffset': dataOffset + 8,
|
||
};
|
||
}
|
||
dataOffset += 8 + view.getUint32(dataOffset + 4, Endian.little);
|
||
}
|
||
break;
|
||
}
|
||
offset += 8 + chunkSize;
|
||
}
|
||
|
||
throw Exception('无法解析WAV文件头');
|
||
}
|
||
|
||
/// 提取PCM数据
|
||
Uint8List _extractPcmData(Uint8List wavData) {
|
||
final wavInfo = _parseWavHeader(wavData);
|
||
final dataOffset = wavInfo['dataOffset']!;
|
||
final dataLength = wavInfo['dataLength']!;
|
||
|
||
return wavData.sublist(dataOffset, dataOffset + dataLength);
|
||
}
|
||
|
||
/// 将PCM数据转换为Float32格式
|
||
Float32List _convertToFloat32(Uint8List pcmData) {
|
||
final int16Data = Int16List.view(pcmData.buffer);
|
||
final float32Data = Float32List(int16Data.length);
|
||
|
||
for (int i = 0; i < int16Data.length; i++) {
|
||
float32Data[i] = int16Data[i] / 32768.0;
|
||
}
|
||
|
||
return float32Data;
|
||
}
|