Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions app/lib/utils/audio/wav_combiner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <File>[];
final metadataList = <WavMetadata>[];
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);
Expand All @@ -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);
}
Expand Down
89 changes: 89 additions & 0 deletions app/test/utils/wav_combiner_test.dart
Original file line number Diff line number Diff line change
@@ -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<int> 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,
);
});
}
Loading