You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
96 lines
3.0 KiB
Dart
96 lines
3.0 KiB
Dart
import 'dart:convert';
|
|
import 'dart:typed_data';
|
|
|
|
/// Parsed RIFF/WAVE chunks we care about from a single clip.
|
|
class _WavChunks {
|
|
final Uint8List? fmt;
|
|
final Uint8List data;
|
|
|
|
const _WavChunks({required this.fmt, required this.data});
|
|
}
|
|
|
|
_WavChunks _parseWav(Uint8List clip) {
|
|
Uint8List? fmtChunk;
|
|
final dataChunks = BytesBuilder();
|
|
if (clip.length < 12) return _WavChunks(fmt: fmtChunk, data: dataChunks.toBytes());
|
|
|
|
final data = ByteData.sublistView(clip);
|
|
int offset = 12; // skip "RIFF" size "WAVE"
|
|
while (offset + 8 <= clip.length) {
|
|
final chunkId = ascii.decode(clip.sublist(offset, offset + 4), allowInvalid: true);
|
|
final chunkSize = data.getUint32(offset + 4, Endian.little);
|
|
final chunkStart = offset + 8;
|
|
final chunkEnd = (chunkStart + chunkSize).clamp(0, clip.length);
|
|
|
|
if (chunkId == 'fmt ') {
|
|
fmtChunk ??= clip.sublist(chunkStart, chunkEnd);
|
|
} else if (chunkId == 'data') {
|
|
dataChunks.add(clip.sublist(chunkStart, chunkEnd));
|
|
}
|
|
|
|
offset = chunkStart + chunkSize + (chunkSize.isOdd ? 1 : 0);
|
|
}
|
|
|
|
return _WavChunks(fmt: fmtChunk, data: dataChunks.toBytes());
|
|
}
|
|
|
|
/// Merges multiple small WAV clips (as received one-per-sentence from the
|
|
/// Push-to-Talk hub's `AudioReceived` event) into a single playable WAV byte
|
|
/// buffer.
|
|
///
|
|
/// Assumes all clips share the same PCM format (sample rate/channels/bit
|
|
/// depth), which holds true since they all come from the same TTS voice
|
|
/// within a single response.
|
|
Uint8List mergeWavClips(List<Uint8List> clips) {
|
|
if (clips.isEmpty) return Uint8List(0);
|
|
if (clips.length == 1) return clips.first;
|
|
|
|
Uint8List? fmtChunk;
|
|
final pcmData = BytesBuilder();
|
|
|
|
for (final clip in clips) {
|
|
final parsed = _parseWav(clip);
|
|
fmtChunk ??= parsed.fmt;
|
|
pcmData.add(parsed.data);
|
|
}
|
|
|
|
fmtChunk ??= Uint8List(16);
|
|
final pcmBytes = pcmData.toBytes();
|
|
final fmtSize = fmtChunk.length;
|
|
final dataSize = pcmBytes.length;
|
|
final riffSize = 4 + (8 + fmtSize) + (8 + dataSize);
|
|
|
|
final header = BytesBuilder();
|
|
header.add(ascii.encode('RIFF'));
|
|
header.add(_uint32le(riffSize));
|
|
header.add(ascii.encode('WAVE'));
|
|
header.add(ascii.encode('fmt '));
|
|
header.add(_uint32le(fmtSize));
|
|
header.add(fmtChunk);
|
|
header.add(ascii.encode('data'));
|
|
header.add(_uint32le(dataSize));
|
|
|
|
return Uint8List.fromList([...header.toBytes(), ...pcmBytes]);
|
|
}
|
|
|
|
/// Computes a WAV clip's playback duration directly from its header
|
|
/// (dataSize / byteRate), without needing to load it into an audio engine.
|
|
Duration wavDuration(Uint8List clip) {
|
|
final parsed = _parseWav(clip);
|
|
final fmt = parsed.fmt;
|
|
if (fmt == null || fmt.length < 12 || parsed.data.isEmpty) return Duration.zero;
|
|
|
|
final byteRate = ByteData.sublistView(fmt).getUint32(8, Endian.little);
|
|
if (byteRate == 0) return Duration.zero;
|
|
|
|
final seconds = parsed.data.length / byteRate;
|
|
return Duration(milliseconds: (seconds * 1000).round());
|
|
}
|
|
|
|
Uint8List _uint32le(int value) {
|
|
final b = ByteData(4);
|
|
b.setUint32(0, value, Endian.little);
|
|
return b.buffer.asUint8List();
|
|
}
|
|
|