diff --git a/app/lib/utils/audio/wav_combiner.dart b/app/lib/utils/audio/wav_combiner.dart index f5de05bc638..0509a313e1e 100644 --- a/app/lib/utils/audio/wav_combiner.dart +++ b/app/lib/utils/audio/wav_combiner.dart @@ -18,16 +18,27 @@ class WavCombiner { throw Exception('No WAV files to combine'); } - if (wavFiles.length == 1) { - final outputFile = File(outputPath); - await wavFiles.first.copy(outputPath); - return outputFile; - } - + // Skip parts that aren't valid WAV so one corrupt artifact doesn't fail the + // whole export; combine the parts that do parse. + final validFiles = []; final metadataList = []; for (final file in wavFiles) { - final metadata = await getMetadata(file); - metadataList.add(metadata); + try { + metadataList.add(await getMetadata(file)); + validFiles.add(file); + } catch (e) { + Logger.debug('Skipping unreadable WAV part ${file.path}: $e'); + } + } + + if (validFiles.isEmpty) { + throw Exception('No valid WAV files to combine'); + } + + if (validFiles.length == 1) { + final outputFile = File(outputPath); + await validFiles.first.copy(outputPath); + return outputFile; } final isCompatible = await validateCompatibility(metadataList); @@ -50,7 +61,7 @@ class WavCombiner { ); sink.add(header); - for (final file in wavFiles) { + for (final file in validFiles) { final stream = file.openRead(44); await sink.addStream(stream); } diff --git a/app/test/utils/wav_combiner_test.dart b/app/test/utils/wav_combiner_test.dart new file mode 100644 index 00000000000..59735e70eb3 --- /dev/null +++ b/app/test/utils/wav_combiner_test.dart @@ -0,0 +1,89 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; + +import 'package:omi/utils/audio/wav_combiner.dart'; + +Uint8List _wav(int dataBytes, {int sampleRate = 16000, int channels = 1, int bits = 16}) { + final byteRate = sampleRate * channels * (bits ~/ 8); + final blockAlign = channels * (bits ~/ 8); + final b = BytesBuilder(); + void str(String s) => b.add(s.codeUnits); + void u32(int v) => b.add((ByteData(4)..setUint32(0, v, Endian.little)).buffer.asUint8List()); + void u16(int v) => b.add((ByteData(2)..setUint16(0, v, Endian.little)).buffer.asUint8List()); + + str('RIFF'); + u32(36 + dataBytes); + str('WAVE'); + str('fmt '); + u32(16); + u16(1); + u16(channels); + u32(sampleRate); + u32(byteRate); + u16(blockAlign); + u16(bits); + str('data'); + u32(dataBytes); + b.add(List.filled(dataBytes, 1)); + return b.toBytes(); +} + +void main() { + late Directory dir; + + setUp(() async { + dir = await Directory.systemTemp.createTemp('wav_combiner_test'); + }); + + tearDown(() async { + if (await dir.exists()) await dir.delete(recursive: true); + }); + + File _write(String name, List bytes) => File('${dir.path}/$name')..writeAsBytesSync(bytes); + + test('combines valid WAV parts', () async { + final a = _write('a.wav', _wav(100)); + final b = _write('b.wav', _wav(60)); + + final out = await WavCombiner.combineWavFiles([a, b], '${dir.path}/out.wav'); + final bytes = await out.readAsBytes(); + + expect(String.fromCharCodes(bytes.sublist(0, 4)), 'RIFF'); + expect(bytes.length, 44 + 100 + 60); + }); + + test('skips a corrupt part (missing RIFF) and combines the rest', () async { + final a = _write('a.wav', _wav(100)); + final bad = _write('bad.wav', List.filled(200, 7)); // no RIFF header + final c = _write('c.wav', _wav(60)); + + final out = await WavCombiner.combineWavFiles([a, bad, c], '${dir.path}/out.wav'); + final bytes = await out.readAsBytes(); + + expect(String.fromCharCodes(bytes.sublist(0, 4)), 'RIFF'); + expect(bytes.length, 44 + 100 + 60); + }); + + test('one valid part among corrupt parts is returned as-is', () async { + final bad = _write('bad.wav', List.filled(200, 7)); + final tooSmall = _write('small.wav', List.filled(10, 3)); + final good = _write('good.wav', _wav(80)); + + final out = await WavCombiner.combineWavFiles([bad, good, tooSmall], '${dir.path}/out.wav'); + final bytes = await out.readAsBytes(); + + expect(bytes.length, 44 + 80); + }); + + test('throws when no part is a valid WAV', () async { + final bad1 = _write('b1.wav', List.filled(200, 7)); + final bad2 = _write('b2.wav', List.filled(10, 3)); + + expect( + () => WavCombiner.combineWavFiles([bad1, bad2], '${dir.path}/out.wav'), + throwsException, + ); + }); +}