/// Ogg Opus 时长解析 —— **不解码、不依赖播放器**。 /// /// Opus 的 granule position 恒以 **48kHz** 计样本(RFC 7845 §4,与音频本身的 /// 采样率无关),所以「最后一个 Ogg page 的 granulepos 减去 OpusHead 里的 /// pre-skip,再除以 48000」就是精确时长。 /// /// 为什么不用现成的播放器: /// - ⚠️ `just_audio` 的 `setFilePath` 对支架录的这种裸 Ogg Opus **两端都报 /// `Source error`**(Android/iOS 真机实测),拿不到时长; /// - ⚠️ 按码率估会明显失真:真机那条 38400 字节的录音实际 9.16s, /// 按 16kbps 估是 19.2s,差一倍,列表上一眼就看得出来。 library; import 'dart:io'; const List _oggS = [0x4F, 0x67, 0x67, 0x53]; // 'OggS' const List _opusHead = [ 0x4F, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64 // 'OpusHead' ]; /// Opus 的 granulepos 单位,**恒为 48000**,不是音频采样率。 /// 写成 16000 会把时长算成 3 倍。 const int _opusGranuleRate = 48000; /// 从「文件头若干字节」和「文件尾若干字节」解出时长(秒)。 /// /// [head] 至少要覆盖第一个 page(含 OpusHead);[tail] 至少要覆盖最后一个 /// page 的头部。不是 Ogg、或没有可用的 granulepos 时返回 null。 double? oggOpusSecondsFrom(List head, List tail) { if (head.length < 4 || _indexOf(head, _oggS) != 0) return null; // 不是 'OggS' final hi = _indexOf(head, _opusHead); // OpusHead 布局:magic(8) version(1) channels(1) pre-skip(2, LE) final preSkip = hi >= 0 && hi + 12 <= head.length ? head[hi + 10] | (head[hi + 11] << 8) : 0; final ti = _lastIndexOf(tail, _oggS); if (ti < 0 || ti + 14 > tail.length) return null; // page 头偏移 6 起的 8 字节小端就是 granulepos var granule = 0; for (var b = 7; b >= 0; b--) { granule = (granule << 8) | tail[ti + 6 + b]; } if (granule <= preSkip) return null; return (granule - preSkip) / _opusGranuleRate; } /// 读文件解时长。不是 Ogg Opus 返回 null。 /// /// 只读头 4KB 和尾 128KB —— Ogg page 最大 65307 字节,128KB 必然覆盖到 /// 最后一个 page,不用把整个文件读进内存(录音可能几十兆)。 Future oggOpusSeconds(File file) async { final raf = await file.open(); try { final len = await raf.length(); if (len < 47) return null; // 连一个 OpusHead page 都放不下 await raf.setPosition(0); final head = await raf.read(len < 4096 ? len : 4096); const tailLen = 128 * 1024; final from = len > tailLen ? len - tailLen : 0; await raf.setPosition(from); final tail = await raf.read(len - from); return oggOpusSecondsFrom(head, tail); } finally { await raf.close(); } } int _indexOf(List hay, List needle) { outer: for (var i = 0; i + needle.length <= hay.length; i++) { for (var j = 0; j < needle.length; j++) { if (hay[i + j] != needle[j]) continue outer; } return i; } return -1; } int _lastIndexOf(List hay, List needle) { outer: for (var i = hay.length - needle.length; i >= 0; i--) { for (var j = 0; j < needle.length; j++) { if (hay[i + j] != needle[j]) continue outer; } return i; } return -1; }