You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

93 lines
3.2 KiB

/// 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<int> _oggS = [0x4F, 0x67, 0x67, 0x53]; // 'OggS'
const List<int> _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<int> head, List<int> 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<double?> 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<int> hay, List<int> 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<int> hay, List<int> 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;
}