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 _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; }