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