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 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(); }