yx_speech_to_text_flutter/test/audio/audio_file_test.dart

222 lines
7.3 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:io';
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.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;
}