import 'package:eaimar/data/utils/ogg_opus_duration.dart'; import 'package:flutter_test/flutter_test.dart'; /// Ogg Opus 时长解析的金测试。 /// /// 用例是 **EaiRec 支架真机 TF 卡上那条录音的原始字节**(2026-09-10 拉下来的 /// `MIC0005.OGG`,38400 字节),不是构造出来的样本。改这里的解析逻辑前先看 /// 这个测试为什么红。 void main() { // 文件头 48 字节:第一个 page(含 OpusHead) const head = [ 0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, // 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x17, 0x30, 0x89, 0x00, 0x00, 0x00, 0x00, 0x21, 0x8B, 0x55, 0x8A, 0x01, 0x13, 0x4F, 0x70, 0x75, 0x73, 0x48, 0x65, 0x61, 0x64, 0x01, 0x01, 0x00, 0x00, 0x80, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4F, ]; // 最后一个 page 的头 27 字节:granulepos = 0x0006B580 = 439680 const tail = [ 0x4F, 0x67, 0x67, 0x53, 0x00, 0x04, 0x80, 0xB5, // 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x17, 0x30, 0x89, 0x1E, 0x00, 0x00, 0x00, 0xF3, 0xEE, 0x70, 0xEC, 0x0A, ]; test('真机 MIC0005.OGG:granulepos 439680 → 9.16 秒', () { final secs = oggOpusSecondsFrom(head, tail); expect(secs, isNotNull); // 439680 / 48000 = 9.16 expect(secs!, closeTo(9.16, 0.01)); }); test('⚠️ 除数必须是 48000 而不是音频的 16000', () { // OpusHead 里写的输入采样率是 16000(head 偏移 40 起的 0x00003E80), // 但 granulepos 恒以 48kHz 计。用 16000 去除会得到 27.48s —— 3 倍。 final secs = oggOpusSecondsFrom(head, tail)!; expect(secs, lessThan(10)); }); test('pre-skip 要从 granulepos 里扣掉', () { // 把 OpusHead 的 pre-skip 从 0 改成 48000(1 秒) final h = List.from(head); h[38] = 0x80; // pre-skip 低字节 h[39] = 0xBB; // 0xBB80 = 48000 final secs = oggOpusSecondsFrom(h, tail)!; expect(secs, closeTo(8.16, 0.01)); }); test('不是 Ogg 就返回 null,不猜', () { // RIFF/WAV 头 const wav = [0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00]; expect(oggOpusSecondsFrom(wav, tail), isNull); }); test('granulepos 不大于 pre-skip 时返回 null(空文件/只有头)', () { // 最后一个 page 的 granulepos 置 0 final t = List.from(tail); for (var i = 6; i < 14; i++) { t[i] = 0; } expect(oggOpusSecondsFrom(head, t), isNull); }); }