# FFmpeg → MoonBit 移植过程记录

本文保留历次实现说明、对拍向量和排错记录。文中的“当前”“剩余”及测试数量对应各段记录时点；项目的现状请以 [README.md](README.md) 为准。

用 MoonBit 重写 FFmpeg 9.0.2。仓库是完整的 FFmpeg 源码（4652 个 C/H 文件、约 66 MB 源码、
约 160 万行 C 代码，实测 1,595,275 行），本文档记录这项工作的**范围界定、重构计划、当前进度与移植约定**。

MoonBit 模块位于本目录（`moonbit/`），与 FFmpeg 的 C 源码并存，互不修改。

---

## 1. 范围与策略

一次重写整个 FFmpeg 不现实：

- libavcodec 单个库就有 38 MB 源码（占总量的 57%）；
- 大量代码依赖 x86/ARM 内联汇编、SIMD intrinsics、指针运算、宏元编程与平台特定实现；
- 这些部分**大多无法逐行"翻译"**成 MoonBit（其中**向量化内核改用 MoonBit `@v128` 重写**，
  见下方边界原则）。

因此采用**垂直切片（vertical slice）**策略：从自包含、可独立验证的纯算法模块入手，
每个模块都对照 C 源码逐行移植，并用 FFmpeg **自己的测试与 FATE 参考输出**做向量校验。
编译目标为 `wasm`/`wasm-gc`（纯算法无需 C 工具链，`moon test` 可直接验证）。

**边界原则（2026-10 修订）**：纯算法 → MoonBit 逐行重写；**向量化（SIMD）内核 → 用
MoonBit 的 `@v128`（`moonbitlang/core/v128`，对应 Wasm SIMD128 / native SIMD）重写**。
FFmpeg 的 `*.asm` 是 x86 汇编（NASM），**无法编译进本项目当前的 `wasm` 目标**，因此它们
不是"留作 FFI 的黑盒"，而是 SIMD 化的**目标对象**：实现时用
`#cfg(any(target="native", target="wasm"))` 门控并配标量回退。仅当指令在 MoonBit 中
无法表达（AES-NI / SHA-NI / PCLMULQDQ 等 x86 专有指令）或属线程/平台范畴（`pthread`、
随机源、文件/网络 IO）时，才保留 C 实现并通过 `extern "C"` FFI 复用。

参考：全树共 **186 个 NASM 文件**（`libavcodec/x86` 113、`libavfilter/x86` 42、
`libswscale/x86` 13、`libavutil/x86` 12、`libswresample/x86` 3）；MoonBit core 自身的
`encoding/hex/encode_v128.mbt`、`encoding/base64/*_v128.mbt` 是 `@v128` 用法范本。

---

## 2. 目录结构

```
moonbit/
├── moon.mod                  # 模块声明：name = "ffmpeg/moonbit"，preferred_target = "wasm"
├── COPYING.LGPLv2.1          # GNU LGPL 2.1 正文（与 FFmpeg 根同名文件逐字节一致）
├── COPYING.LGPLv3            # GNU LGPL 3.0 正文（同上）
├── libavutil/                # 包：ffmpeg/moonbit/libavutil
│   ├── moon.pkg              # 包配置（空 = 默认）
│   ├── mathematics.mbt       # av_gcd / av_rescale_rnd / av_log2 / ...
│   ├── rational.mbt          # AVRational 全部运算
│   ├── display.mbt           # av_display_rotation_set / av_display_matrix_flip
│   ├── crc.mbt               # CRC 表生成与计算
│   ├── adler32.mbt           # Adler-32
│   ├── base64.mbt            # Base64 编解码
│   ├── md5.mbt               # MD5
│   ├── sha.mbt               # SHA-1 / SHA-224 / SHA-256
│   ├── avstring.mbt          # 前缀/子串查找、strlcpy/strlcat、token、路径、转义、UTF-8
│   ├── rc4.mbt               # RC4 流密码（KSA + 密钥流/加解密）
│   ├── aes.mbt               # AES-128/192/256，ECB/CBC（S-box 表生成逐行移植）
│   ├── xtea.mbt              # XTEA 32 轮分组密码（大端/小端，ECB/CBC）
│   ├── tea.mbt               # TEA 64 轮分组密码（rounds 参数，ECB/CBC）
│   ├── blowfish.mbt          # Blowfish 16 轮 Feistel（P-array/S-box 表，ECB/CBC）
│   ├── cast5.mbt             # CAST-128（RFC 2144，40/80/128 位密钥，ECB/CBC）
│   ├── aes_ctr.mbt           # AES-CTR（keystream、分部块状态、64 位计数器）
│   └── *_wbtest.mbt          # 与模块同名的白盒测试
├── libavformat/              # 包：ffmpeg/moonbit/libavformat（依赖 libavutil + libavcodec）
│   ├── moon.pkg              # 声明上述依赖（libavformat→libavcodec 与 FFmpeg 方向一致）
│   ├── aviobuf.mbt           # 流式 AVIO 层（aviobuf.c：缓冲读/写 + dyn buf + 校验和）
│   ├── mov.mbt               # MP4/MOV 解复用器：box 遍历 + 采样表 → 每样本索引（跑在 AvioContext 上）
│   ├── extradata.mbt         # mov_read_glbl（avcC/hvcC/av1C 原样）+ esds 描述符链 + AAC 配置
│   ├── frag.mbt              # sidx 参考 + mfra/tfra 随机访问表（含 mfro 探测）
│   ├── seek.mbt              # ff_index_search_timestamp + mov_seek_stream
│   ├── aviobuf_wbtest.mbt    # AVIO 白盒测试（分块源/seek 路径/dyn buf/get_str/流式 MP4）
│   ├── segment_io.mbt        # HLS/DASH 初始化/媒体/密钥回调与范围读取、缓存和重试
│   ├── segment_io_wbtest.mbt # 真实 TS/fMP4 分片读取→解复用→H.264 的逐帧 MD5
│   ├── mp4_group_wbtest.mbt  # 第三切片白盒测试（sgpd/sbgp、open-GOP seek、多 sample description）
│   ├── mp4_editlist_wbtest.mbt # 第四切片白盒测试（advanced_editlist：mov_fix_index 全量）
│   ├── mp4_wbtest.mbt        # 合成 MP4 构造器与白盒测试
│   ├── mp4_extra_wbtest.mbt  # 第二切片白盒测试（extradata/sidx/mfra/senc/saiz/seek/音频分块）
│   ├── mp4_real_wbtest.mbt   # 真实文件交叉验证（docker ffmpeg/ffprobe，base64 内嵌）
│   ├── ebml.mbt              # Matroska/WebM EBML 层（变长整数原语 + 通用元素游走器）
│   ├── matroska.mbt          # Matroska 切片 1+2：EBML 头 + Segment/Info/SeekHead/resync
│   ├── matroska_track.mbt    # Matroska 切片 3：TrackEntry 全表（video/audio/encodings）
│   ├── matroska_block.mbt    # Matroska 切片 4：Cluster/Block 与三种 lacing
│   ├── ebml_wbtest.mbt       # EBML 白盒测试（VINT 规范向量/游走器/合成与损坏头部）
│   ├── ebml_real_wbtest.mbt  # 真实 webm/mkv 头交叉验证（docker ffmpeg，base64 内嵌）
│   ├── mkv_segment_wbtest.mbt # Segment 切片测试（合成布局 + 真实 1024 字节前缀）
│   ├── mkv_track_wbtest.mbt  # Tracks 切片测试（合成轨道 + 真实 webm/mkv 轨道）
│   ├── mkv_cluster_wbtest.mbt # Cluster 切片测试（lacing 三模式 + 真实 packet 表）
│   ├── mkv_cluster_realdata_wbtest.mbt # Cluster 真实文件 base64 素材
│   ├── rawaudio.mbt        # 裸 ADTS AAC / MPEG 音频分帧（ID3v2/v1、Xing/Info/VBRI）
│   ├── rawaudio_wbtest.mbt # 真实 AAC/MP3 逐帧比对 ffprobe + ID3 前缀/截断用例
│   ├── codec_tags.mbt      # Matroska CodecID / MP4 fourcc → 编解码器名
│   ├── codec_parameters.mbt# 容器 → CodecParameters（avcC/hvcC/ASC 解析桥）
│   ├── aac_asc_wbtest.mbt  # MP4 → ASC/PCE 初始化 → 原始 AAC 包 → PCM 全链路差分
│   ├── aac_asc_realdata_wbtest.mbt # 真实 stereo/PCE 5.1/7.1 MP4、ASC、ffprobe 包索引与 PCM
│   └── codec_parameters_wbtest.mbt # avcC/hvcC/ASC + 真实 MP4 AAC / MKV H.264 用例
└── libavcodec/               # 包：ffmpeg/moonbit/libavcodec（依赖 libavutil，仅白盒测试用）
    ├── moon.pkg              # moonbitlang/core/math + libavutil（`for "wbtest"`）
    ├── bitstream.mbt         # MSB-first 位读取 + Exp-Golomb（get_bits.h/golomb.h）
    ├── av1_tiles.mbt         # AV1 tile group 连续覆盖与压缩 payload 边界提取
    ├── opus_rc.mbt           # Opus SILK/CELT 共享熵解码（448 条 C 状态对拍）
    ├── opus_head.mbt         # OpusHead 四类通道映射与完整多流包拆分
    ├── opus_pvq.mbt          # CELT PVQ 脉冲向量、频带归一化/spread/collapse mask
    ├── opus_pvq_tables.mbt   # 从原 C 提取的 1272 项 PVQ U 表（.mbtx --check）
    ├── opus_celt_alloc.mbt   # CELT 动态比特分配、跳带、立体声与 fine/PVQ 预算
    ├── opus_celt_energy.mbt  # CELT setup：静音/后滤波参数、粗细能量与 TF 切换
    ├── opus_celt_tables.mbt  # 17 张原 C CELT 整数表（.mbtx --check）
    ├── opus_celt_bands.mbt   # 递归 PVQ 分带、theta、折叠/噪声与立体声
    ├── opus_celt_math.mbt    # Hadamard/Haar、theta 定点核与归一化
    ├── opus_celt_spectrum.mbt# CELT 整帧归一化频谱与 collapse masks
    ├── opus_celt_coefficients.mbt # 最终能量、anti-collapse、缩放与连续帧历史
    ├── opus_celt_synthesis.mbt # 半逆 MDCT、120 样本 overlap/add、后滤波与去加重
    ├── opus_celt_decode.mbt  # 有状态 CELT-only 帧 → 48 kHz planar Float PCM
    ├── opus_celt_windows.mbt # 两张原 C 合成窗（.mbtx --check）
    ├── vp9_packet_decode.mbt # superframe、隐藏关键帧与八槽 show-existing 输出
    ├── vp9_decode.mbt        # VP9 帧解码入口、概率更新、tile 编排与 YUV 输出
    ├── vp9_block.mbt         # 分区/模式/系数解码，MERGE/SPLAT 非零上下文
    ├── vp9_recon.mbt         # 帧内参考样本准备与 TU 重建
    ├── vp9_dsp.mbt           # 帧内预测、DCT/ADST/WHT 与去块内核
    ├── vp9_lpf.mbt           # 边掩码、滤波强度与逐超块去块驱动
    ├── vp9_tables.mbt        # 从 vp9data.c 提取的 33 张表（脚本 --check）
    ├── vp9_kf_wbtest.mbt     # 五流 12 帧 YUV MD5 与具体错误串负例
    ├── vp9_kf_realdata_wbtest.mbt # 内嵌 libvpx-vp9 码流与 FFmpeg 参考 MD5
    ├── vp9_dsp_wbtest.mbt    # DC 缩放/清系数、HOR_UP 坐标与饱和裁剪回归
    ├── vp9_decode_test.mbt   # 公共解码入口黑盒测试
    ├── h2645_parse.mbt       # H.264/HEVC NAL 拆分：去防竞争字节、Annex B/长度前缀
    ├── h2645_vui.mbt         # H.264/HEVC 公共 VUI（SAR/色彩/chroma location）
    ├── h264_ps.mbt           # H.264 SPS/PPS/VUI/缩放矩阵 + avcC extradata
    ├── h264_slice.mbt        # I/P/B/SP/SI header、L0/L1 重排与显式权重
    ├── h264_picture_slice.mbt # 多 slice 共享画面、独立熵状态与完整性检查
    ├── h264_slice_neighbors.mbt # slice/受限帧内预测的邻居可用性
    ├── h264_cavlc8.mbt       # CAVLC 8×8 的四路交错残差与熵计数
    ├── h264_pcm.mbt          # CABAC I_PCM escape、原始样本和算术重启
    ├── h264_poc.mbt          # POC 0/1/2 与 frame_num 回绕
    ├── h264_reference_marking.mbt # MMCO 1–6、长参考与滑窗标记
    ├── h264_cavlc.mbt        # CAVLC 残差解码 + 去量化/chroma QP 派生表（含 8x8 dequant8 表）
    ├── h264_idct.mbt         # H.264 4x4/8x8 IDCT + luma/chroma DC Hadamard（8-bit）
    ├── h264_pred.mbt         # 帧内预测 4x4/16x16/chroma 8x8 + 可用性改判
    ├── h264_pred8x8l.mbt     # High profile 8x8 帧内预测（pred8x8l 九模式 + DC 边缘变体）
    ├── h264_decode.mbt       # Baseline I 帧重建（CAVLC→dequant→IDCT→预测→写回）+ Annex B 入口
    ├── h264_deblock.mbt      # 环路去块滤波（h264dsp 核 + h264_loopfilter 边界强度推导）
    ├── h264_sei.mbt          # H.264 SEI：消息框架 + BP/PT/恢复点/green + 公共消息集
    ├── itut35.mbt           # ITU-T T.35 注册用户数据：provider 分发 + A53/AFD/LCEVC/HDR Vivid/AOM 颗粒
    ├── h264_side_data.mbt   # SEI→帧 side-data 导出与访问单元边界语义（stereo3d/矩阵/mastering 等）
    ├── h264_mc.mbt           # 运动补偿：luma 6 抽头 qpel + chroma 1/8 双线性 + 边界复制
    ├── h264_mvpred.mbt       # MV 预测：A/B/C 邻块、mid_pred、P_SKIP 捷径
    ├── h264_inter.mbt        # P 宏块层（CABAC+CAVLC）：skip/type/ref/mvd + inter 残差
    ├── h264_gap.mbt          # frame_num 间隙合成、前参考像素复制/灰图与参考缓冲身份
    ├── h264_gap_wbtest.mbt   # 间隙/回绕/灰图/PAFF/复位与失败重试状态回归
    ├── h264_gap_realdata_wbtest.mbt # 19 流/86 帧，默认滤波与跳过滤波 FFmpeg MD5
    ├── h264_dpb.mbt          # 滑窗 DPB、PAFF 互补场配对与共享平面/运动历史（H264Decoder）
    ├── h264_ref_list.mbt     # 帧/场默认短长参考表、场交错、重排与缺失参考替换
    ├── h264_b_mb.mbt         # B 宏块层：类型/子分区/L0L1 ref+mvd/直接模式
    ├── h264_b_mc.mbt         # B 双向 MC：显式/隐式加权与 (a+b+1)>>1 平均
    ├── h264_b_refs.mbt       # B 默认 L0/L1 列表（短/长参考排序）
    ├── h264_b_direct.mbt     # 帧/场空间/时间 direct、异场 colocated 行偏移与缩放
    ├── h264_colocated.mbt    # 帧↔场 colocated 抽取、参考身份映射与场奇偶选择
    ├── h264_paff_realdata_wbtest.mbt # JM 19.0 PAFF/帧场混编 24 流/276 帧，滤波与未滤波 MD5
    ├── h264_paff_wbtest.mbt   # 场配对/POC、slice 隔离、B 表场交换、MMCO 场掩码/超限
    ├── h264_depth.mbt        # 8–14 bit 的样本截断与 QP 上限
    ├── h264_output.mbt       # 显示重排：POC 排序、epoch、reorder_depth drain
    ├── h264_chroma_realdata_wbtest.mbt # 4:2:2/4:4:4 真实码流逐帧 MD5
    ├── h264_mono_realdata_wbtest.mbt # 4:0:0 真实码流逐帧 MD5（含 10-bit/B 帧）
    ├── h264_mbaff_realdata_wbtest.mbt # MBAFF 隔行帧图真实码流逐帧 MD5（I/P/B、混合 field/frame 宏块对、多参考/B-pyramid、CQM、4:2:0/4:2:2/4:4:4、8/10-bit、多 slice）
    ├── h264_bypass_realdata_wbtest.mbt # High 4:4:4 lossless transform bypass 真实码流逐帧 MD5
    ├── aac_ics.mbt           # AAC ICS 语法：ics_info/section 数据/TNS/pulse/gain-control
    ├── aac_mdct.mbt          # AAC IMDCT/加窗 + SCE/CPE/LFE 多元素帧入口与标签状态绑定
    ├── aac_coupling.mbt      # CCE 增益、频域/时域耦合及声道选择
    ├── aac_stereo.mbt        # CPE 共享/独立窗口、M/S 与强度立体声、双声道频谱编排
    ├── aac_layout.mbt        # 默认配置 1..7/11..14、三层 PCE 与 FFmpeg 声道排序、DSE 跳过
    ├── aac_config.mbt        # AAC-LC GASpecificConfig/ASC PCE 与直接解码器初始化
    ├── aac_spectrum.mbt      # AAC 频谱解码（码书 1..11/scalefactor/pulse/PNS/TNS）
    ├── aac_tables.mbt        # AAC 解码表（脚本批量提取，与 C 源逐值 diff）
    ├── hevc_ps.mbt           # HEVC VPS/SPS/PPS/PTL/RPS/VUI + hvcC extradata
    ├── hevc_slice.mbt        # HEVC slice segment header（I/P/B、RPS、权重表、对齐）
    ├── hevc_sei.mbt          # HEVC SEI（前缀/后缀、CLL/mastering display 等）
    ├── hevc_access_unit.mbt  # HEVC 访问单元组装与 POC 跟踪
    ├── hevc_cabac.mbt        # HEVC CABAC 引擎（3×199 init_values 与状态迁移）
    ├── hevc_cabac_syntax.mbt # HEVC CABAC 语法元素（split/cbf/merge/mvd/系数）
    ├── hevc_intra.mbt        # HEVC 帧内预测（planar/DC/33 角度，4..32）
    ├── hevc_scan.mbt         # HEVC 系数扫描（对角/水平/垂直，3 组尺寸）
    ├── hevc_transform.mbt    # HEVC IDCT/DST/反量化/residual 叠加
    ├── hevc_intra_ref.mbt    # HEVC 参考样本可用性/边界填充/邻域表
    ├── hevc_picture.mbt      # HEVC 图像缓冲（单色/4:2:0/4:2:2/4:4:4 的 y/cb/cr 平面与 clone）
    ├── hevc_residual.mbt     # HEVC 残差解码（含 transform skip 与 scaling list；RDPCM 拒绝）
    ├── hevc_deblock.mbt      # HEVC 去块滤波（weak/strong、亮度+色度、bS 驱动）
    ├── hevc_sao.mbt          # HEVC SAO（merge/band/edge、CTB 边界恢复）
    ├── hevc_intra_decode.mbt # HEVC I 片整帧解码器（CTU/CU/PU/TU 编排）
    ├── hevc_refs.mbt         # HEVC 参考帧管理（MvField、DPB 槽位、RPS/L0/L1）
    ├── hevc_mc.mbt           # HEVC 运动补偿（luma qpel/chroma epel、uni/bi/加权）
    ├── hevc_inter.mbt        # HEVC inter 语法（merge/AMVP/mvd/ref_idx）
    ├── hevc_inter_decode.mbt # HEVC P/B 帧级驱动（DPB+RPL+MC 编排+bS）
    ├── audio_headers.mbt     # AAC ADTS 头 + MPEG 音频帧头（MP1/MP2/MP3）
    ├── mp3_tables.mbt        # Layer III 码表/Huffman 表（C 源批量提取）
    ├── mp3_dsp.mbt           # 定点 IMDCT/DCT32/合成窗（mpegaudiodsp fixed 路径）
    ├── mp3_decode.mbt        # Layer III 定点解码器 + 三层共用的合成滤波（PCM 输出）
    ├── mpa_layer12.mbt       # Layer I/II 定点解码器（bit allocation/scalefactor/组量化）
    ├── flac_decode.mbt       # FLAC 解码器（frame/subframe、Rice、fixed/LPC、声道去相关）
    ├── mpa_layer12_tables.mbt# Layer I/II 分配表/量化表/定点缩放表
    ├── idct.mbt              # SIMPLE-C IDCT（int16 8/10/12 bit）+ idctdsp 像素/扫描表辅助
    ├── dctref.mbt            # 双精度参考 DCT/IDCT（dctref.c），供 IDCT 测试作期望值
    ├── huffman.mbt           # Huffman 树/码长构建（huffman.c）+ 规范化码编解码
    ├── mpeg4audio.mbt        # AudioSpecificConfig 解析（mpeg4audio.c，esds/AAC 用）
    ├── h264_slice_wbtest.mbt # slice header 逐字段对照 trace_headers
    ├── h264_slice_realdata_wbtest.mbt # slice header 真实码流 base64 素材
    ├── h264_decode_wbtest.mbt# H.264 真实码流逐帧 YUV MD5 + 未支持输入负例
    ├── h264_decode_realdata_wbtest.mbt # H.264 码流 base64 素材与期望 MD5
    ├── h264_weightp_wbtest.mbt # H.264 加权 P + 4 参考去块真实码流逐帧 MD5
    ├── h264_weightp_realdata_wbtest.mbt # 加权 P/多参考码流 base64 素材与期望 MD5
    ├── h264_p8x8_deblock_realdata_wbtest.mbt # CABAC P_8x8 多参考去块真实码流逐帧 MD5
    ├── h264_8x8dct_realdata_wbtest.mbt # High profile 8x8 变换（inter/intra）真实码流逐帧 MD5
    ├── mp3_decode_wbtest.mbt # MP3 定点解码 PCM MD5（mono/stereo/LSF）
    ├── mpa_layer12_wbtest.mbt# MP2 四种分配表 + 手工 Layer I 帧的 PCM MD5
    ├── mpa_layer12_realdata_wbtest.mbt # MP2/MP1 码流 base64 素材
    ├── mp3_decode_realdata_wbtest.mbt # MP3 码流 base64 素材与期望 MD5
    ├── bitstream_wbtest.mbt  # H.264/H.265 表 9-1 Exp-Golomb + 位读取边界
    ├── h2645_parse_wbtest.mbt# RBSP/NAL 拆分合成与真实参数集用例
    ├── h264_ps_wbtest.mbt    # 真实 avcC + trace_headers 字段逐项断言
    ├── hevc_ps_wbtest.mbt    # 真实 hvcC/Annex B（8-bit/Main10）+ RPS/POC 用例
    ├── hevc_slice_wbtest.mbt # slice header 逐字段对照 trace_headers
    ├── hevc_sei_wbtest.mbt   # 真实 x265 SEI 逐字段对照 trace_headers
    ├── hevc_access_unit_wbtest.mbt # 真实 B 帧 GOP 的 POC/解码序
    ├── hevc_cabac_wbtest.mbt # CABAC init 状态 + 真实 slice bin/位对 C
    ├── hevc_intra_wbtest.mbt # 33 种帧内模式对自编译 C pred.c 逐值
    ├── hevc_transform_wbtest.mbt # IDCT/DST/反量化对 C DSP 逐值
    ├── hevc_intra_decode_wbtest.mbt # I 片真实码流逐帧 YUV MD5
    ├── hevc_inter_wbtest.mbt # P/B 真实码流逐帧 YUV MD5 + 拒绝路径负例
    ├── hevc_inter_realdata_wbtest.mbt # P/B 码流 base64 素材与期望 MD5
    ├── hevc_bitdepth_wbtest.mbt # Main10/Main12 真实码流逐帧 YUV MD5（滤波/未滤波）
    ├── hevc_bitdepth_realdata_wbtest.mbt # 10/12-bit 码流 base64 素材与期望 MD5
    ├── hevc_wpp_wbtest.mbt   # 单 slice WPP entry point 真实码流逐帧 MD5
    ├── hevc_wpp_realdata_wbtest.mbt # WPP 码流 base64 素材与期望 MD5
    ├── hevc_tiles_wbtest.mbt # 2x2 tiles 真实码流逐帧 MD5（滤波/未滤波）
    ├── hevc_tiles_realdata_wbtest.mbt # kvazaar tiles 码流 base64 素材与期望 MD5
    ├── hevc_tskip_wbtest.mbt # transform skip 真实码流逐帧 MD5（滤波/未滤波）
    ├── hevc_tskip_realdata_wbtest.mbt # transform skip 码流 base64 素材与期望 MD5
    ├── hevc_444_wbtest.mbt   # 8/10-bit 4:4:4 真实码流逐帧 MD5 + 独立 intra 接口
    ├── hevc_444_realdata_wbtest.mbt # 4:4:4 码流 base64 素材与期望 MD5
    ├── hevc_422_wbtest.mbt   # 8/10-bit 4:2:2 真实码流逐帧 MD5 + 独立 intra 接口
    ├── hevc_422_realdata_wbtest.mbt # 4:2:2 码流 base64 素材与期望 MD5
    ├── hevc_scaling_wbtest.mbt # 默认 scaling list 真实码流逐帧 MD5（滤波/未滤波）
    ├── hevc_scaling_realdata_wbtest.mbt # kvazaar scaling list 码流 base64 素材与期望 MD5
    ├── hevc_monochrome_wbtest.mbt # 8/10-bit 单色真实码流逐帧 MD5 + 独立 intra
    ├── hevc_monochrome_realdata_wbtest.mbt # 单色码流 base64 素材与期望 MD5
    ├── audio_headers_wbtest.mbt # 真实 ADTS/MP3 帧头 + LSF/自由格式
    ├── flac_decode_wbtest.mbt # FLAC 真实文件 PCM MD5（constant/verbatim/fixed/LPC、四种立体声模式）
    ├── flac_decode_realdata_wbtest.mbt # FLAC 文件 base64 素材与期望 PCM MD5
    ├── idct_wbtest.mbt       # 对照 dctref 参考 + FFmpeg dct.c 判据
    ├── huffman_wbtest.mbt    # Kraft/最优性/两实现互证/往返
    ├── h264_sei_wbtest.mbt   # SEI 真实码流逐字段对照 trace_headers + 负例
    ├── h264_sei_realdata_wbtest.mbt # SEI 码流 base64 素材（x264 CBR/bsf 注入/合成）
    ├── h264_inter_wbtest.mbt # MC 整数/半像素/四分像素 + mvpred 中值/回退单元测试
    ├── h264_psynth_wbtest.mbt # 合成 CABAC P 帧端到端（I_PCM 参考 + MV 平移断言）
    ├── aac_wbtest.mbt        # ICS/TNS 逐字段 + MDCT/TDAC/窗口性质 + 静音帧 PCM
    ├── aac_spectrum_wbtest.mbt # 真实 libfdk_aac 码流 46 帧 PCM 差分（≤1 LSB）
    ├── aac_codebook_wbtest.mbt # 全码书元组/符号/位消费 + 长短窗逆变换缩放回归
    ├── aac_pulse_wbtest.mbt   # pulse 语法、采样率相关边界与非法输入回归
    ├── aac_transient_wbtest.mbt # 真实短窗/窗口转换/TNS/PNS 与 pulse 的 PCM 差分
    ├── aac_transient_realdata_wbtest.mbt # native/FDK 突发信号与合成 pulse 参考素材
    ├── aac_stereo_wbtest.mbt  # CPE 逐样本差分、工具覆盖与畸形码流回归
    ├── aac_stereo_realdata_wbtest.mbt # native/FDK 立体声与独立/联合立体声合成素材
    ├── aac_layout_wbtest.mbt  # 真实 5.1/7.1、PCE、标签和元素乱序 PCM 差分
    ├── aac_layout_realdata_wbtest.mbt # 多声道与非连续标签/乱序元素参考素材
    ├── aac_pce_wbtest.mbt     # 默认布局、PCE 截断/重复、DSE 与失败帧状态回归
    ├── aac_height_wbtest.mbt   # 三层 PCE、indexed 11..14、22.2/960，含 codec/format 参考向量
    ├── aac_coupling_wbtest.mbt # CCE 三位置/增益/标签与截断回归，含 codec/format 参考向量
    ├── aac_960_wbtest.mbt     # 960/120 MP4 到 PCM，配套 aac_960_realdata_wbtest.mbt
    ├── aac_config_wbtest.mbt  # ASC 初始化、PCE 对齐、延迟/扩展字段及不支持配置回归
    ├── aac_spectrum_realdata_wbtest.mbt # 真实 AAC 码流 + 参考 PCM base64 素材
    └── mpeg4audio_wbtest.mbt # 位串手推的 AAC-LC/SBR/PS/USAC/ALS 头部用例
```

## 3. 当前进度

**总体**：全量 FFmpeg 约 160 万行（实测 1,595,275 行），本次完成 MoonBit 源文件
（`moonbit/**/*.mbt`，不含 `_build` 生成文件）共 **149453 行**（libavutil 6483 行 +
libavformat 的 aviobuf 流式 AVIO 层、MP4 解复用、Matroska EBML/Segment/Tracks/Cluster、
裸 AAC/MP3 分帧、容器码流参数桥与 HLS/DASH 分片回调 33319 行 + libavcodec 的位流/H.264/HEVC/AAC/MP3 码流头、
IDCT/Huffman/MPEG-4 音频切片、**H.264 I 帧重建 + 环路去块滤波（CAVLC 全通 / CABAC 部分
向量 / MP4 `avcC` 长度前缀入口）**、**H.264 P 帧解码与滑窗 DPB（运动补偿/MV 预测/inter
宏块层，含 CABAC P 路径的合成码流端到端向量、显式加权预测、参考列表重排与多参考去块）**、
**H.264 High profile 8x8 变换（`transform_8x8_mode`：CABAC cat5 残差/`dequant8_coeff`/`pred8x8l`
帧内预测含 DC 边缘改判/去块跳过 8x8 内部奇边，真实 x264 码流逐帧 YUV MD5 与 ffmpeg 逐字节一致）**、**H.264 MBAFF 隔行帧图（I/P/B、CAVLC/CABAC、8/10-bit、4:0:0/4:2:0/4:2:2/4:4:4，真实 x264 码流滤波/未滤波逐字节一致）**、**H.264 High 4:4:4 lossless transform bypass（qpprime_y_zero_transform_bypass_flag / pred_add 累加语义）**、**H.264 SEI 解析**、**AAC 频谱解码
（Huffman 码书 1..11 + scalefactor + PNS/TNS + IMDCT/加窗全管线，真实 libfdk_aac
码流逐帧解码）**、**FLAC 无损解码（frame/subframe、Rice 分区、fixed/LPC、wasted bits、四种立体声去相关，9 个真实文件 PCM 逐字节一致）**、**MPEG 音频 Layer I/II/III 定点解码**、**HEVC I 片整帧解码与完整
环路滤波（SAO+去块，真实 x265 码流逐帧 YUV MD5 与 ffmpeg 逐字节一致）** 与
**HEVC P/B 帧间预测（merge/AMVP、luma qpel/chroma epel 的 uni/bi/加权 MC、DPB+RPL、
MV 去块边界强度，真实 x265 码流 21/21 帧逐字节一致）**、**HEVC Main10/Main12 位深参数化
（10-bit 加权 B 帧、12-bit 32×32 TU 负移位反量化，去块 beta/tc 位深缩放）**、**HEVC
多 slice 与 WPP（AU 聚合、slice 起点/逐 slice CABAC/QP/RPL、跨 slice bS/SAO 边界、
entry point 与行间状态同步，320×240 四 slice 与单 slice WPP 流逐字节一致）**、**HEVC
tiles（uniform 2×2 tile 布局、CTB raster↔tile-scan 映射、tile 边界 CABAC 重置、tile
感知预测可用性、跨 tile 去块/SAO 边界，kvazaar 码流逐字节一致）**、**HEVC transform
skip（transform_skip_flag/上下文、仅反量化不做反变换，x265 真实码流逐字节一致）**、**HEVC
4:4:4（全尺寸色度平面、逐 PU 色度帧内模式、4×4 色度 TU、色度 QP 截断到 51、hshift=vshift=0
的色度 MC 与去块/SAO）**、**HEVC 4:2:2（hshift=1/vshift=0、每个亮度 TU 两个纵向堆叠色度块、
`tab_mode_idx` 色度模式重映射，8/10-bit I/P/B 真实码流滤波与未滤波两条路径均逐字节一致）**、**HEVC
scaling list（默认/SPS/PPS 矩阵、逐系数 scale_m 与 DC 缩放，kvazaar 默认矩阵
I/P/B+WPP 码流逐字节一致）**、**HEVC 单色（chroma_format_idc=0：仅亮度平面，跳过色度帧内模式/
残差/MC/去块/SAO，8/10-bit I/P/B 真实码流逐字节一致）**
及 **VP9 profile-0 关键帧解码（五流 12 帧 YUV MD5 一致）**、**Opus 共享熵解码、
CELT 连续帧频域/PCM 解码与 OpusHead 多流映射**，109651 行，含测试）——约占整体 **9.4%**。

### 已完成模块（阶段 0 + 阶段 1 开端 + 阶段 4/5 码流层）

| 模块 | 对应 C 源码 | 移植内容 | 测试 |
|---|---|---|---|
| [mathematics.mbt](libavutil/mathematics.mbt) | `libavutil/mathematics.c`、`intmath.h` | `av_gcd`(Stein 二进制 GCD)、`av_rescale_rnd`(128 位拆分乘法 + 位长除法)、`av_rescale`/`av_rescale_q`/`av_rescale_q_rnd`、`av_log2`、`ff_ctzll`、`av_compare_ts`、`av_compare_mod` | ✅ |
| [display.mbt](libavutil/display.mbt) | `libavutil/display.c` | `av_display_rotation_set`（16.16 定点、C 截断语义）、`av_display_matrix_flip` | ✅ |
| [rational.mbt](libavutil/rational.mbt) | `libavutil/rational.c` | `AVRational`、`av_make_q`、`av_cmp_q`、`av_q2d`、`av_inv_q`、`av_reduce`、`av_mul_q`/`av_div_q`/`av_add_q`/`av_sub_q`、`av_d2q`(自实现 `frexp`)、`av_gcd_q`、`av_q2intfloat` | ✅ |
| [crc.mbt](libavutil/crc.mbt) | `libavutil/crc.c` | `AVCRCId`、`av_crc_init`(任意多项式表生成)、`av_crc_get_table`、`av_crc`、`av_crc_ids` | ✅ |
| [adler32.mbt](libavutil/adler32.mbt) | `libavutil/adler32.c` | `av_adler32_update`（标量，`DO1`/`DO4`） | ✅ |
| [adler32_v128.mbt](libavutil/adler32_v128.mbt) | 同上 + FFmpeg 的 SSE2/AVX2 路径 | `av_adler32_update_v128`：16 字节块内用 `i32x4_dot_i16x8_s`（`pmaddwd` 等价）一次算出 `Σb` 与 `Σ(16-i)·b`，`NMAX=5552` 模数纪律；`#cfg(any(target="native", target="wasm"))`，其余目标回退标量。测试断言与标量逐值一致 | ✅ |
| [base64.mbt](libavutil/base64.mbt) | `libavutil/base64.c` | `av_base64_encode`、`av_base64_decode`、`av_base64_decode_syntax`、`av_base64_size` | ✅ |
| [base64_v128.mbt](libavutil/base64_v128.mbt) | 同上 + FFmpeg `base64.asm` | `av_base64_encode_v128`：12 字节 → 16 字符/轮；`i8x16_swizzle` 展开成 `[b0,b1,b2,0,…]`，四个 6-bit 字段由 `i32x4` 移位/掩码取得，`i32x4→i16x8→i8x16` 窄化 + `i8x16_shuffle` 重排，字母表无分支比较映射；尾部回退标量 | ✅ |
| [md5.mbt](libavutil/md5.mbt) | `libavutil/md5.c` | `AVMD5`、`av_md5_init/update/final/sum` | ✅ |
| [md5_v128.mbt](libavutil/md5_v128.mbt) | 同上（多缓冲 4 路并行） | `av_md5_sum_x4`：`i32x4` 的 4 个 lane 各承载一条**等长**消息，`T[i]` 常量、`F`/`G`/`H`/`I` 混合与 `rotl(S[i])` 全部逐 lane 施加，一轮同时推进 4 条摘要（单流 MD5 无 SIMD 收益，故新增多缓冲 API）；每路按各自长度填充到 `nblocks*64` 平面缓冲，块调度跨 lane 一致。`#cfg(any(target="native", target="wasm"))`；测试与标量 `av_md5_sum` 逐 lane 逐值一致 | ✅ |
| [sha.mbt](libavutil/sha.mbt) | `libavutil/sha.c` | `AVSHA`、`av_sha_init/update/final/sum`（SHA-1/224/256） | ✅ |
| [avstring.mbt](libavutil/avstring.mbt) | `libavutil/avstring.c`、`bprint.c`(`av_bprint_escape`) | `av_strstart`/`av_stristart`/`av_stristr`/`av_strnstr`、`av_strlcpy`/`av_strlcat`、`av_strcasecmp`/`av_strncasecmp`、`av_get_token`、`av_strireplace`、`av_basename`/`av_dirname`/`av_append_path_component`、`av_match_name`/`av_match_list`、`av_escape`(QUOTE/XML/BACKSLASH 三模式 + flag)、`av_utf8_decode` | ✅ |
| [avstring_v128.mbt](libavutil/avstring_v128.mbt) | 同上（扫描加速） | `av_strnstr_v128`：以 8 个 UTF-16 码元为步长用 `v128_load_i16x8` + `i16x8_eq` + `i16x8_bitmask` 先扫首字符，命中再标量核对；语义与 `av_strnstr` 完全一致，仅 BMP（码元==码点）走 SIMD | ✅ |
| [rc4.mbt](libavutil/rc4.mbt) | `libavutil/rc4.c` | `AVRC4`、`av_rc4_alloc`、`av_rc4_init`(KSA，key_bits 非 8 倍数报错)、`av_rc4_crypt`(密钥流/加解密) | ✅ |
| [aes.mbt](libavutil/aes.mbt) | `libavutil/aes.c` | `AVAES`、`av_aes_init`(128/192/256 密钥扩展 + S-box 表生成)、`av_aes_crypt`(ECB / CBC，含 iv 链式更新)。**轮函数按 FIPS-197 规范实现**，见下方说明 | ✅ |
| [xtea.mbt](libavutil/xtea.mbt) | `libavutil/xtea.c` | `AVXTEA`、`av_xtea_alloc`、`av_xtea_init`(大端密钥装载)、`av_xtea_le_init`(小端)、`av_xtea_crypt`/`av_xtea_le_crypt`(ECB / CBC，含 iv 链式更新)。32 轮用 `CONFIG_SMALL` 循环形式（与 C 展开版等价） | ✅ |
| [tea.mbt](libavutil/tea.mbt) | `libavutil/tea.c` | `AVTEA`、`av_tea_alloc`、`av_tea_init`(大端密钥 + `rounds` 参数，标准 64)、`av_tea_crypt`(ECB / CBC，含 iv 链式更新)。Feistel 网络 `rounds/2` 个双轮，delta `0x9E3779B9` | ✅ |
| [blowfish.mbt](libavutil/blowfish.mbt) | `libavutil/blowfish.c` | `AVBlowfish`、`av_blowfish_alloc`、`av_blowfish_init`(P-array 密钥异或 + 用 (0,0) 反复加密重导出 P/S)、`av_blowfish_crypt_ecb`(16 轮 Feistel，返回两半)、`av_blowfish_crypt`(ECB / CBC，含 iv 链式更新)。`orig_p`/`orig_s` 表逐值移植 | ✅ |
| [cast5.mbt](libavutil/cast5.mbt) | `libavutil/cast5.c` | `AVCAST5`、`av_cast5_alloc`、`av_cast5_init`(40/80/128 位密钥 → `Km`/`Kr` 轮密钥，`Kr` 掩到 5 位)、`av_cast5_crypt`(ECB)、`av_cast5_crypt2`(CBC)。`F1/F2/F3` 轮函数与 `COMPUTE_Z/COMPUTE_X` 密钥调度；`S1`..`S8` 表由 C 源码逐值提取 | ✅ |
| [aes_ctr.mbt](libavutil/aes_ctr.mbt) | `libavutil/aes_ctr.c` | `AVAESCTR`、`av_aes_ctr_alloc/init`、`av_aes_ctr_set_iv`(8 字节 IV+零计数器)、`av_aes_ctr_set_full_iv`/`get_iv`(16 字节)、`av_aes_ctr_increment_iv`(BE64 高位加一)、`av_aes_ctr_crypt`(keystream 缓存 + `block_offset` 跨调用状态，支持任意切分)。`set_random_iv` 依赖平台随机源，留待 FFI | ✅ |
| [aviobuf.mbt](libavformat/aviobuf.mbt) | `libavformat/aviobuf.c`（1484 行） | `AvioContext`：回调驱动的缓冲 IO（`read_packet`/`write_packet`/`seek` 闭包）。**读侧**：`fill_buffer` 的追加或回绕选择、探测后收缩回 `orig_buffer_size`、`avio_r8`/`rb16/24/32/64`/`rl16/24/32/64`、`avio_read`（超过缓冲的大请求直通回调）、`avio_read_partial`、`ffio_read_size/indirect`、`avio_get_str`/`get_str16le/be`（GET_UTF16 代理对）、`ff_get_line/chomp_line`、`ffio_read_varlen`/`leb`；**seek**：`avio_seek` 四路径（缓冲内快路 / short_seek 阈内读进 / 半缓冲回填 / 协议回调）+ `avio_skip/tell/size(AVSEEK_SIZE→SEEK_END 回退)/feof(清标志重测)`；**写侧**：`w8`/`write`/`flush`（回退位恢复）与 `wl/wb 16/24/32/64`、`ffio_fill`、`put_str`/`put_str16le/be`、`ffio_wfourcc`、`print_string_array`、`ffio_write_leb/lines`；**dyn buf**：`avio_open_dyn_buf`（×1.5+1 增长）/`get`/`reset`/`close`（64 字节 padding）/packet buf/null buf；**校验和**：`ff_crc04C11DB7/EDB88320/A001_update` + `ffio_init/get_checksum`（fill/flush 两侧的范围折算）；`ffio_ensure_seekback`/`ffio_realloc_buf`。未移植（无消费者）：`avio_printf` 族（需 AVBPrint）、`write_marker`、`avio_pause/seek_time`、probe/limit 辅助 | ✅ |
| [mov.mbt](libavformat/mov.mbt) | `libavformat/mov.c` | box 遍历 + `ftyp`（含 compatible brands）/`mvhd`/`tkhd`/`mdhd`/`hdlr`（仅认识 vide/soun/subp/clcp，数据 hdlr 如 `url ` 不覆盖）/`elst`/`stsd`(视频/音频 sample entry，音频含 QT v1/v2 打包字段 + `mov_int2double`)/`stts`（含 `FFMIN` 时长上限与 `track_end` 播种）/`ctts`/`stsc`/`stsz`/`stz2`/`stco`/`co64`/`stss`/`stps` 解析；`mov_merge_tts_data` 合并 + `mov_build_index` 采样索引 → 每样本 offset/size/DTS/PTS/duration/关键帧/`min_distance`/`discard`，含**未压缩音频分块路径**（1024 样本/包，MACE/ADPCM/GSM/PCM 打包默认）。**碎片化 MP4**：`moof`/`traf`/`tfhd`/`tfdt`/`trun`/`trex` 按 `default-base-is-moof` + `tfdt`/`sidx`/`track_end` 追加每样本索引，按 `trun` 采样标志判定关键帧，重叠片段置 `AVINDEX_DISCARD_FRAME`。**cenc 加密**：`sinf`/`schm`/`tenc`/`senc`（流级 + 碎片内）/`saiz`/`saio`（辅助信息块）解析 + `mov_decrypt_sample`（`cenc`/`cbc1`/`cens`/`cbcs`，基于 `aes_ctr`）；**sample group**：`sgpd`('sync' NAL 类型)/`sbgp`('rap '/'sync' run) 解析 + `build_open_gop_key_points`（逐样本 ctts offset 展开、CRA 开放 GOP 样本表、最小 stts 时长）→ seek 的 `can_seek_to_key_sample`（`pts = dts + offset + dts_shift > 请求点` 即拒绝 + 回退步进）；**多 sample description**：`ff_mov_read_stsd_entries` 逐 entry 走查（`mov_skip_multiple_stsd` 同 fourcc 才续析）、extradata 槽位（仅 slot 0 会被写入 —— 后续 entry 的 `pseudo_stream_id` 为 -1，C 对 `sc->extradata[-1]` 的越界写按无操作处理并记录）、`pseudo_stream_id` 索引过滤（-1 全收，否则只收 `stsc.id-1` 匹配的 chunk）、tfdt/trun 的 `stsd_id` 门（tfhd 旗标或 trex 默认）、首 chunk 引用 entry 2 时的 `last_stsd` 主 extradata 切换；**advanced edit list**（`mov_fix_index` 全量）：`get_edit_list_entry`（时长换算到轨道时标，u64 回绕判据照抄）、`find_prev_closest_index`（关键帧回退 + ctts 精化）、按 edit 时间轴重建索引（前导弃帧经 frame-duration 缓冲重写为严格递增时间戳、空 edit 平移时间轴并成为 `start_time`、音频部分帧修剪 `skip_samples`、正 `min_corrected_pts` 将首个视频 PTS 归零）、tts 重建 + PTS 重导出、时长钳制、`mov_estimate_video_delay`（H264 重排估计）、mov_read_trak 的 stsc 清理 quirk 与碎片化自动禁用（顺序敏感）；`av_mov_open_with(buf, false)` 走非 advanced 路径 | ✅ |
| [extradata.mbt](libavformat/extradata.mbt) | `mov.c`(`mov_read_glbl`)、`mov_esds.c`/`isom.c`(`ff_mov_read_esds`、`ff_mp4_read_descr*`、`ff_mp4_read_dec_config_descr`、`ff_mp4_obj_type`) | `avcC`/`hvcC`/`av1C`/`glbl`/`vvcC`(跳 4 字节)/`evcC`/`lvcC`/`apvC` 载荷原样入 extradata（含 `fiel` 包裹的破损文件回退）；`esds` 走 ES→DecoderConfig→DecoderSpecificInfo 描述符链（`ff_mp4_read_descr_len` 7 位长度、ES flags 的 URL/OCR 字段），对象类型表 → codec-id 字符串，MPEG 音频无 DSI 特例；AAC 时解析 AudioSpecificConfig 回填声道/采样率/帧长，`mp3on4`/`mp4als` 覆盖 | ✅ |
| [frag.mbt](libavformat/frag.mbt) | `mov.c`(`mov_read_sidx`、`mov_read_mfra`、`read_tfra`) | `sidx` v0/v1：参考 ID/时间戳/earliest_presentation_time/first_offset（锚点为入口 position+payload，`av_rescale_q` 换算到轨道时间戳）+ 每参考 offset/pts/size/duration，累计 pts 即 `track_end` 与时长；`mfra`：从文件尾 4 字节（`mfro`）探测大小、校验后读 `tfra`（v0/v1 时间+offset，field_length 定长的三个尺寸字段） | ✅ |
| [seek.mbt](libavformat/seek.mbt) | `mov.c`(`mov_seek_stream`、`can_seek_to_key_sample`)、`seek.c`(`ff_index_search_timestamp`) | 二分查找（丢弃帧跳过、BACKWARD/ANY 语义、关键帧回退）+ 时间戳 seek：`dts_shift` 折算、越界钳到首样本、`min_sample_duration` 回退步进、HEVC open-GOP 门（无 sgpd 时的无操作形式）；`mov_seek_track` 返回样本序号与 DTS；seek 入口按 C 减去 `min_corrected_pts + dts_shift`（advanced_editlist 产出） | ✅ |
| [ebml.mbt](libavformat/ebml.mbt) | `libavformat/matroskadec.c`（EBML 读取层；9.0 无独立 ebml.c） | 变长整数原语：`ebml_read_num`（首字节前导零定宽 + 标志位清除）、`ebml_read_length`（全一宽度 → 未知长度）、`ebml_read_uint/sint`（首字节符号扩展）/`float`（binary32/64 重构含 inf/NaN）、`ascii`（lenient UTF-8）、`binary`、`ebml_read_master`（16 层深度栈）、`matroska_ebmlnum_sint`（lace 尺寸再偏置）、`is_ebml_id_valid`（标志位与字节长度一致）；**通用元素游走器** `ebml_parse`/`ebml_parse_nest`：`current_id` 预读状态、level_check（元素越界即错 / 末端相等 → LEVEL_ENDED 多层连弹）、`max_lengths[]` 每类型上限、VOID/CRC32/未知元素跳过（非 seekable 的 unknown_count × UNKNOWN_EQUIV > SKIP_THRESHOLD 启发式）、NEEDS_CHECKING → EOF→EIO 转换、未知长度 master 遇上层元素终止（C 的 CHILD_OF 哨兵链 → 祖先栈）、live 模式 EOF 即止。**表驱动模型**：C 的 `EbmlSyntax{type,def,data_offset}` → `EbmlKind` + sink 闭包 + 子表 thunk（无环引用） | ✅ |
| [matroska.mbt](libavformat/matroska.mbt) | `matroskadec.c`（`ebml_header[]`/`ebml_syntax[]` 表 + `matroska_read_header` 头部校验链） | `EbmlHeader`（ReadVersion/MaxIDLength/MaxSizeLength/DocType/DocTypeReadVersion，默认值 = C 的 zero-init + nest 预置遍历）；`matroska_read_ebml_header`：解析头元素（缺 Segment 在此阶段不算失败，与 C 一致）、版本越界 → `PATCHWELCOME`、DocTypeReadVersion 3 → 警示、未知 doctype → 警示标志 | ✅ |
| [ebml.mbt](libavformat/ebml.mbt) + [matroska.mbt](libavformat/matroska.mbt)（切片 2） | `matroskadec.c`（`matroska_segment[]`/`matroska_segments[]`/`matroska_info[]`/`matroska_seekhead[]` 表、`matroska_read_header` 段走查、`matroska_execute_seekhead`/`matroska_parse_seekhead_entry`、`matroska_resync`/`matroska_reset_status`、`matroska_find_level1_elem`） | Segment 走查：首个 Cluster 即 `EBML_STOP`（data_offset = 其 id 起点），损坏输入经 `matroska_resync`（32 位滑窗找 level-1 id + 状态重置 + 未知长度级别）恢复；**Info**（TimecodeScale 默认 1e6/Duration 浮点/Title/MuxingApp/DateUTC，WritingApp/SegmentUID 按 C 跳过）；**SeekHead**（SeekID/SeekPosition，条目在 master 开始时按 C 的 EbmlList 语义追加）；**execute_seekhead**（仅 seekable：按条目回跳解析未过的 level-1 元素——CUES 恒延迟、失败标记 cues broken——dummy level + 位置恢复的 crude hack 照抄）；**level-1 注册表**（容量 64、SeekHead/Tags 按 id+pos 去重、Cluster/非法 id 排除、segment_start/cues_parsing_deferred 簿记并入游走器）；入口 `av_matroska_open`（Attachments/Chapters/Cues/Tags 识别为跳过，待后续切片） | ✅ |
| [matroska_track.mbt](libavformat/matroska_track.mbt) | `matroskadec.c`（`matroska_tracks[]`/`matroska_track[]`/`matroska_track_video[]`/`matroska_track_audio[]`/`matroska_track_encodings[]`/`matroska_block_addition_mapping[]`） | `MkvTrack` 全字段（TrackNumber/UID/Type/CodecID/CodecPrivate/CodecDelay/语言默认 eng/DefaultDuration/TimecodeScale 默认 1.0/五组旗标默认值/SeekPreRoll/MaxBlockAdditionID）+ `MkvTrackVideo`（Pixel/Display 尺寸默认 -1 未设、四向 crop、DisplayUnit、Interlaced/FieldOrder/StereoMode/AlphaMode、ColourSpace 二进制）+ `MkvTrackAudio`（采样率默认 8000、输出采样率、位深、声道默认 1）+ ContentEncodings（Scope 默认 1、Type、CompAlgo 默认 zlib、CompSettings；加密行识别跳过）+ BlockAdditionMapping。TrackOperation（3D）与 colour/projection masters 识别跳过待后续。轨道经 TRACKS Level1 行按 Entry 追加 | ✅ |
| [matroska_block.mbt](libavformat/matroska_block.mbt) | `matroskadec.c`（`matroska_parse_laces`/`matroska_parse_block`/`matroska_cluster_parsing[]`/`matroska_blockgroup[]`/`matroska_blockmore[]`） | `MkvPacket`（stream_index/data/pts/duration/pos/keyframe/discard_padding）；**三种 lacing**：Xiph（0xFF 链，越界即错）、fixed-size（整除校验）、EBML（首帧**无符号** vint + `laces-2` 个 sint 增量，按 `2^(7w-1)-1` 再偏置，负尺寸/溢出拒绝，末帧取余量）；`matroska_parse_block`：VINT 轨道号（`matroska_find_track_by_num`）、BE16 有符号时间偏移、flags（keyframe bit7 + lacing bits1-2）、`timecode = cluster_time/track.time_scale + block_time - codec_delay_in_track_tb`（`av_rescale_q` 四舍五入到 track timebase）、lace duration 由 BlockDuration 或 DefaultDuration×laces/time_scale 分摊、`pkt->pos` 恒为 **Block payload 起点**（9.0.2 的 `pkt->pos = pos`，各 lace 相同）；Cluster 表（Timecode/SimpleBlock/BlockGroup/BlockAdditions/Position/PrevSize）→ **多 Cluster 循环走查** + 尾部截断容忍（EOF 保留已入队 packet）；BlockGroup 在 master 结束时才解码，保证 BlockDuration/ReferenceBlock 已到齐（`!reference_count` 定关键帧） | ✅ |
| [mpeg4audio.mbt](libavcodec/mpeg4audio.mbt) | `libavcodec/mpeg4audio.c`、`mpeg4audio_sample_rates.h` | `ff_mpeg4audio_get_config`：对象类型（32+ escape）、采样率表/24 位显式、声道配置表、头部隐式 SBR/PS、ALS 头嗅探与重载、USAC/LD 帧长、`0x2b7`/`0x548` 隐式 SBR/PS 同步扩展扫描、PS 修正（含 `channels & ~1`）。返回 GASpecificConfig 位偏移与 C 一致 | ✅ |
| [idct.mbt](libavcodec/idct.mbt) | `libavcodec/simple_idct.c`、`simple_idct_template.c`（`BIT_DEPTH` 8/10/12 + `IN_IDCT_DEPTH` 16）、`idctdsp.c` | `ff_simple_idct{,_put,_add}_int16_{8,10,12}bit`（行 DC 快速路径 + 列蝶形，`SUINT`→`UInt` 32 位回绕算术）；`ff_put_pixels_clamped_c`/`ff_add_pixels_clamped_c`/`put_signed_pixels_clamped_c`、`ff_init_scantable_permutation`、`ff_permute_scantable` | ✅ |
| [idct_v128.mbt](libavcodec/idct_v128.mbt) | 同上（表示改造 + `libavcodec/x86/simple_idct.asm`、`simple_idct10.asm`） | `ff_simple_idct{,_put,_add}_v128`（8 位）与 `..._v128_10bit`/`..._v128_12bit`：块改用 **128 字节 LE-int16 缓冲**；`i8x16_shuffle` 三级 unpack 做 8x8 转置，`i32x4` 逐 lane 蝶形 + `i16x8_narrow_i32x4_s` 收窄（含 int16 截断语义）；`put`/`add` 用饱和窄化（8 位）或 `i16x8_min/max_s`（10/12 位）实现 `av_clip_uintp2` | ✅ |
| [huffman.mbt](libavcodec/huffman.mbt) | `libavcodec/huffman.c`、`huffman.h`（+ `vlc.c`/`get_bits.h` 的位读取） | `ff_huff_build_tree`（两列表树构建 + `get_tree_codes` DFS → `lens`/`xlat`）、`ff_huff_gen_len_table`（堆法码长生成，含 `offset <<= 1` 重试）；另附规范化 Huffman 码的建立与 MSB-first 编/解码（对应 `ff_vlc_init_from_lengths` + `put_bits`/`get_bits`） | ✅ |
| [bitstream.mbt](libavcodec/bitstream.mbt) | `libavcodec/get_bits.h`、`golomb.h` | MSB-first `BitReader`（`get_bits`/`show_bits`/`skip_bits`/越界归零/`get_bits_left`）+ `get_ue_golomb{,_long,_31}`、`get_se_golomb{,_long}`、`bytesize`、`align`；H.264 表 9-1 的 Exp-Golomb 向量 | ✅ |
| [h2645_parse.mbt](libavcodec/h2645_parse.mbt) | `libavcodec/h2645_parse.c`、`h2645_parse.h` | `ff_h2645_extract_rbsp`（防竞争字节去除 + 起始码截断）、`ff_h2645_packet_split`（Annex B / 长度前缀，H.264 与 HEVC NAL 头）、`ff_h2645_unit_requires_zero_byte`；NAL 头解析（`nal_ref_idc`/`nuh_layer_id`/`temporal_id`） | ✅ |
| [h2645_vui.mbt](libavcodec/h2645_vui.mbt) | `libavcodec/h2645_vui.c`、`h2645data.c` | 公共 VUI：SAR 表/扩展 SAR、overscan、video signal + 颜色描述（保留值映射 unspecified）、chroma location；`ff_h2645_pixel_aspect` 全表 | ✅ |
| [h264_ps.mbt](libavcodec/h264_ps.mbt) | `libavcodec/h264_ps.c`、`h264_ps.h`、`h264_parse.c`(`ff_h264_decode_extradata`) | SPS 全字段（高 profile 的 chroma/位深/缩放矩阵、POC、裁剪、VUI/HRD/bitstream restriction、帧率与 `num_reorder_frames` 推导）、PPS（CABAC/ref/SliceGroup 拒绝/加权/QP/缩放列表）、`avcC` 与 Annex B extradata 解析。未建解码端 dequant/chroma-QP 派生表 | ✅ |
| [hevc_ps.mbt](libavcodec/hevc_ps.mbt) | `libavcodec/hevc/ps.c`、`ps.h`、`hevc/parse.c`(`ff_hevc_decode_extradata`) | PTL（profile tier level + 兼容标志）、VPS、SPS（chroma/位深/裁剪/CTB 派生/RPS/PCM/VUI/RExt/SCC）、PPS（tiles/deblocking/缩放/RExt/SCC）、`hvcC`/Annex B extradata、`ff_hevc_compute_poc2`。多层/3D 扩展返回 `PATCHWELCOME`；HRD 按位消费但未存储 | ✅ |
| [h264_slice.mbt](libavcodec/h264_slice.mbt) | `libavcodec/h264_slice.c`、`h264_parse.c` | I/P/B/SP/SI slice header：POC 0/1/2、MMCO、L0/L1 参考计数覆盖/重排、显式亮度/色度权重、B direct 标志、SP/SI QS、CABAC 初值与去块字段；B 像素重建仍拒绝 | ✅ |
| [h264_cavlc.mbt](libavcodec/h264_cavlc.mbt) | `libavcodec/h264_cavlc.c`(`decode_residual`)、`h264_ps.c`(`ff_h264_dequant4_coeff_init`/`ff_h264_chroma_qp`) | CAVLC 残差：coeff_token/total_zeros/run_before/level_prefix-suffix 三级码表（C 的 VLC 子系统改为同表构建的二叉 trie，逐位等价）、PPS 派生的去量化表与 chroma QP 表；系数按 C 的转置布局存放 | ✅ |
| [h264_idct.mbt](libavcodec/h264_idct.mbt) | `libavcodec/h264idct_template.c` | `ff_h264_idct_add`、`ff_h264_idct8_add`（High profile 8x8DCT，含 `idct8_add4` 的 nnz==1 DC 捷径）、`ff_h264_idct_dc_add`、`ff_h264_luma_dc_dequant_idct`、`ff_h264_chroma_dc_dequant_idct`（8-bit）；`SUINT` 用 32 位 `UInt` 复刻回绕语义，`dctcoef` 写回按 `int16` 截断 | ✅ |
| [h264_pred8x8l.mbt](libavcodec/h264_pred8x8l.mbt) | `libavcodec/h264pred_template.c`(`pred8x8l_*`) | High profile 8x8 帧内预测：参考行/列 3 抽头滤波（含 topleft/topright 可用性），九种 4x4 风格模式 + `ff_h264_check_intra4x4_pred_mode` 的 DC 边缘改判（LEFT_DC/TOP_DC/DC_128） | ✅ |
| [h264_pred.mbt](libavcodec/h264_pred.mbt) | `libavcodec/h264pred_template.c`、`h264_parse.c`(`ff_h264_check_intra4x4_pred_mode`/`ff_h264_check_intra_pred_mode`) | 4x4 九模式（含 DC 边缘变体）、16x16 四模式、chroma 8x8 四模式的 8-bit 帧内预测，以及「参考样本可用性 → 模式改判」逻辑；邻居直接读重建平面（FFmpeg 读每 MB 的边界缓冲，样本值相同） | ✅ |
| [h264_decode.mbt](libavcodec/h264_decode.mbt) | `h264_cavlc.c`(`ff_h264_decode_mb_cavlc`)、`h264_mb_template.c`(`hl_decode_mb`)、`h264_slice.c`(`init_dimensions`) | **H.264 Baseline I 帧重建**：CAVLC 宏块解析 → dequant → 4x4 IDCT / luma & chroma DC Hadamard → 帧内预测 → 写回平面；`I_4x4`/`I_16x16`/`I_PCM` 三条宏块路径、8x8 z-order 块序与 top-right 可用性掩码、SPS 裁剪与 `H264Frame::yuv420p` 输出；入口 `h264_decode_annexb_i_frames`（Annex B，就地解析 SPS/PPS），默认出厂即走环路滤波，`..._ex(data, true)` 跳过 | ✅ |
| [h264_deblock.mbt](libavcodec/h264_deblock.mbt) | `h264dsp_template.c`(`h264_loop_filter_luma{,_intra}`、`h264_loop_filter_chroma{,_intra}`)、`h264_loopfilter.c`(`filter_mb_dir`/`ff_h264_filter_mb_fast`) | **环路去块滤波**：弱/强（intra）luma 与 chroma 内核（`xstep`/`ystep` 参数化后一套代码同时覆盖横向/纵向边）；bS 按 8.7.2.3 逐 4x4：intra 宏块边=4/内部边=3，非零系数=2，ref 或 MV（|dx|/|dy|≥4）差异=1；参考索引经 `ref2frm` 的 POC 身份比较（重排列表中的重复图片视为同一参考）；色度边沿用 luma 的 bS[4]，每段 2 样本各取 `tc0_table[..][bS[i]]+1`；`alpha_table`/`beta_table`/`tc0_table` 由 C 源批量提取；左右/上下 QP 取均值、chroma QP 按 `chroma_qp_index_offset` 派生、slice 偏移按 C 的 `div2 * 2` 缩放 | ✅ |
| [h264_sei.mbt](libavcodec/h264_sei.mbt) | `libavcodec/h264_sei.c`、`h2645_sei.c`、`sei.h` | **H.264 SEI 解析**：消息框架（type/size 的 0xFF 扩展、双零字节截尾）；buffering period（缺失 SPS 返回 `AVERROR_PS_NOT_FOUND` 非致命）、picture timing（payload 暂存 + `ff_h264_sei_process_picture_timing` 对 SPS 延迟解析，含时钟时间戳全语法）、recovery point、green metadata；公共消息集（h2645_sei.c）：user data unregistered（含 `x264 - core N` 版本嗅探）、display orientation、frame packing arrangement（H.264 分支）、film grain characteristics、alternative transfer、ambient viewing environment、mastering display（`present=2` 语义）、content light。ITU-T T.35（type 4）经 `itut35.mbt` 解析，帧 side-data 导出在 `h264_side_data.mbt`（均见下两行）；HEVC 侧入口未接 | ✅ |
| [itut35.mbt](libavcodec/itut35.mbt) | `libavcodec/itut35.c`、`atsc_a53.c`、`dynamic_hdr_vivid.c`、`aom_film_grain.c` | **ITU-T T.35 注册用户数据**：`ff_itut_t35_parse_buffer` 全 provider 分发（US 的 ATSC GA94/DTG1、AOM、Samsung、Dolby、SMPTE；UK V-Nova；CN HDR Vivid，含 0xFF 国家码扩展字节与截断 EINVAL）；载荷解析：A53 CC（`ff_parse_a53_cc` 双场合并、尾 marker 字节边界）、AFD（`&0xf`）、LCEVC 原样保存、HDR Vivid 全字段（T/UWA 005.1-2022）、AOM AFGS1 颗粒集（含 scaling 预测/参考集、CICP 合法性、错误清空）。Dolby 与 C 一致忽略（H.264 路径无 DOVIContext）；HDR10+/SMPTE 2094-40 App5 保留原始载荷（结构级解析未做） | ✅ |
| [h264_side_data.mbt](libavcodec/h264_side_data.mbt) | `h2645_sei.c`(`ff_h2645_sei_to_frame`/`h2645_sei_to_side_data`)、`libavutil/display.c`、`h264dec.c`(`decode_nal_units` 的 SEI/边界)、`h264_slice.c`(`h264_export_frame_props`/`h264_frame_start`) | **帧 side-data 与 NAL 边界语义**：`H264FrameSideData` 标签列表（stereo3d、displaymatrix、A53/AFD/LCEVC、SEI 未注册、ambient、mastering display（g,b,r→r,g,b 重排与合法性检查）、content light、H.274 颗粒、AOM 颗粒集）；`H264Decoder.decode_nals` 按 C 语义在 AU 入口 `ff_h264_sei_uninit`（待配场跳过）、首 slice 前 SEI 归属本图、slice 后 SEI 只更新状态；导出时机=帧图或场对第二场；场对 A53 合并/AFD 取后场/LCEVC 先场挂载；`sei_recovery_frame_cnt` 取首场值。HDR10+/2094-App5/HDR Vivid 与 C 一致留在 SEI 上下文不导出 | ✅ |
| [audio_headers.mbt](libavcodec/audio_headers.mbt) | `libavcodec/adts_header.c`、`adts_parser.c`、`mpegaudiodecheader.c`、`mpegaudiotabs.h` | `ff_adts_header_parse{,_buf}`（AAC-LC/Main/SSR/LTP、采样率/声道/帧长/码率）、`ff_mpa_check_header`、`avpriv_mpegaudio_decode_header`（MPEG1/2/2.5、Layer I/II/III、LSF、自由格式）、`ff_mpa_decode_header`；码率/采样率全表 | ✅ |
| [mp3_tables.mbt](libavcodec/mp3_tables.mbt) | `libavcodec/mpegaudiodec_common.c`、`mpegaudiodsp_data.c` | `ff_band_size_long/short`、`ff_slen_table`、`ff_lsf_nsf_table`、`ff_mpa_pretab`、`ff_mpa_huff_data` 与 Layer III 的 15 张 Huffman 码长/符号表，全部由 C 源批量提取（不手工转录） | ✅ |
| [mp3_dsp.mbt](libavcodec/mp3_dsp.mbt) | `mpegaudiodsp_template.c`(fixed, `USE_FLOATS=0`)、`mpegaudiodsp.c`、`dct32_template.c` | 定点 IMDCT/多相合成滤波：`ff_imdct36` 与 36 点/12 点窗表生成、`ff_dct32`、合成窗与频率反转；`INTFLOAT`=`int` → MoonBit 32 位回绕 `Int`，`mulh`/`mull` 走 64 位中间积 | ✅ |
| [mp3_decode.mbt](libavcodec/mp3_decode.mbt) | `mpegaudiodec_template.c`(fixed) | **MPEG-1/2/2.5 Layer III 定点解码**：side info、scalefactor（slen 表）、Huffman 解码、反量化（`ff_mpa_pretab` 预增益）、立体声处理（MS/intensity）、短块重排、bit reservoir、输出 S16 交织 PCM；三层共用的合成滤波 `mpa_synthesize`；入口 `decode_mp3_stream` / `Mp3Decoder`（`decode_frame` 按帧头 `layer` 分派） | ✅ |
| [flac_decode.mbt](libavcodec/flac_decode.mbt) | `libavcodec/flacdec.c`、`flac.c`(`ff_flac_decode_frame_header`)、`flacdsp.c`/`flacdsp_template.c` | **FLAC 无损解码**：`fLaC` 元数据块（STREAMINFO）、frame header（UTF-8 帧号/块大小/采样率/声道模式）、constant/verbatim/fixed(order 0-4)/LPC(order 1-32) 子帧、Rice 分区与 escape 编码、wasted bits、lpc16（C 的有符号 int 累加+算术右移）/lpc32 预测、independent/left-side/right-side/mid-side 去相关，输出 S16/S32 交织 PCM；支持 8/16/20/24-bit（夹具覆盖 16/24-bit）、1-8 声道、44.1/48 kHz，9 个真实文件 PCM MD5 与 ffmpeg 逐字节一致 | ✅ |
| [mpa_layer12.mbt](libavcodec/mpa_layer12.mbt) | `mpegaudiodec_template.c`(`mp_decode_layer1`/`mp_decode_layer2`)、`mpegaudio.c`(`ff_mpa_l2_select_table`) | **Layer I / Layer II 定点解码**：Layer I 的 4 位分配 + 6 位 scalefactor + `n+1` 位尾数；Layer II 的五张分配表选择（含 `ff_mpa_l2_select_table` 的码率/采样率判据）、`scale_code` 四种 scalefactor 共享模式、分组量化（3/5/9 步，走 `ff_division_tabs`）与线性量化、joint stereo 的 `bound` 共享区；`l1_unscale`/`l2_unscale_group` 用与 C 相同的 `FRAC_BITS` 整数乘子；无 IMDCT/混叠/bit reservoir | ✅ |
| [mpa_layer12_tables.mbt](libavcodec/mpa_layer12_tables.mbt) | `mpegaudiodata.c`、`mpegaudiodec_common.c` | `ff_mpa_sblimit_table`、`alloc_table_{1,3,4}`、`ff_mpa_quant_{steps,bits}` 由 C 源脚本提取；`ff_scale_factor_modshift`、`scale_factor_mult`、`scale_factor_mult2`、`ff_division_tabs` 按 C 的初始化逻辑**派生**（不硬编码 2368 个魔数） | ✅ |
| [rawaudio.mbt](libavformat/rawaudio.mbt) | `libavformat/aacdec.c`、`mp3dec.c`、`id3v2.c`（分帧部分） | 裸 ADTS AAC 与 MPEG 音频分帧：ID3v2 跳过、ADTS 逐帧、MP3 帧扫描、首帧 Xing/Info/VBRI 识别并剔除、ID3v1 尾部跳过；输出与 `ffprobe` 一致的每帧 `pos`/`size`/`pts`/`duration` | ✅ |
| [codec_tags.mbt](libavformat/codec_tags.mbt) | `libavformat/matroska.c`(`ff_mkv_codec_tags`)、`isom_tags.c` | Matroska CodecID 与 MP4 sample-entry fourcc → FFmpeg 编解码器名（H.264/HEVC/AAC/MP3 及常用邻居） | ✅ |
| [codec_parameters.mbt](libavformat/codec_parameters.mbt) | `mov.c`/`matroskadec.c` 的 `codecpar` 汇聚 | 容器 → `CodecParameters` 桥：H.264/HEVC `avcC`/`hvcC` 解出分辨率/裁剪/位深/profile/level/VUI 帧率/SAR；AAC AudioSpecificConfig 解出采样率/声道/帧长；MP4/MKV 两套入口（`mp4_track_codec_parameters`/`mkv_track_codec_parameters`） | ✅ |

| [h264_mb.mbt](libavcodec/h264_mb.mbt) | `h264_mb_template.c`(`hl_decode_mb_predict_luma`/`hl_decode_mb_idct_luma` + chroma 路径) | 宏块**重建**半段：帧内预测 → IDCT/Hadamard 叠加 → 平面写回。CAVLC 与 CABAC 的解析层共用这一个函数，像素路径只有一份实现 | ✅ |
| [cabac.mbt](libavcodec/cabac.mbt) | `libavcodec/cabac.c`、`cabac_functions.h`、`h264_cabac.c`(`ff_h264_init_cabac_states`) | **CABAC 算术解码核心**：`ff_init_cabac_decoder`、`refill`/`refill2`/`renorm_cabac_decoder_once`、`get_cabac`、`get_cabac_bypass`、`get_cabac_bypass_sign`、`get_cabac_terminate`，以及按 slice QP 派生 1024 个上下文状态的 `ff_h264_init_cabac_states`。`low` 为 32 位回绕 `Int`，`ff_ctz` 用循环实现 | ✅ |
| [h264_cabac_tables.mbt](libavcodec/h264_cabac_tables.mbt) | `cabac.c`(`ff_h264_cabac_tables`)、`h264_cabac.c`(`cabac_context_init_I`/`_PB`) | 1343 项算术表（`norm_shift`/`lps_range`/`mlps_state`/`last_coeff_flag_offset_8x8`）+ 1024×2 与 3×1024×2 的上下文初值表，由一次性提取脚本从 C 源批量提取（`uint8_t` 负字面量按回绕存储） | ✅ |
| [h264_cabac_mb.mbt](libavcodec/h264_cabac_mb.mbt) | `h264_cabac.c`(`decode_cabac_intra_mb_type`/`_mb_intra4x4_pred_mode`/`_mb_chroma_pre_mode`/`_mb_cbp_luma`/`_mb_cbp_chroma`、`get_cabac_cbf_ctx`、`decode_cabac_residual_internal`、`ff_h264_decode_mb_cabac` 的 intra 分支) | **CABAC 宏块层（I 宏块）**：mb_type/I16x16 预测模式/chroma 预模式/两级 CBP/`mb_qp_delta`/残差（significance map + 逆序 level，含 `>15` 逃逸与 `bypass_sign`）；邻居上下文由每宏块的 `nz`/`cbp_all`/`mb_type_bits` 直接推导（复刻 `fill_decode_caches` 的 `left_cbp`/`top_cbp` 打包与 `0x7CF` 不可用默认值） | ✅（部分向量，见下） |
| [h264_decode.mbt](libavcodec/h264_decode.mbt)（`h264_decode_avc_frames`） | `h264dec.c`(`decode_nal_units` 的 extradata 路径) | **MP4 `avcC` + 长度前缀样本入口**：`ff_h264_decode_extradata` 解参数集并给出 `nal_length_size`，逐样本按长度前缀拆 NAL（`is_nalff=true`），SEI 跳过，切片 NAL 走同一解码路径 | ✅ |
| [h264_mc.mbt](libavcodec/h264_mc.mbt) | `h264_mb.c`/`mc.h`(`mc_dir_part`、`qpel_mc`/`chroma_mc` 标量内核) | **运动补偿**：6 抽头半像素 luma 滤波（qpel 四分插值取全/半平均）、chroma 1/8 双线性；越界取样按 `ff_emulated_edge_mc` 的边界复制钳制。块基准位置显式传入（luma 与 chroma 一致） | ✅ |
| [h264_mvpred.mbt](libavcodec/h264_mvpred.mbt) | `h264_mvpred.h`(`pred_motion`/`pred_16x8_motion`/`pred_8x16_motion`/`pred_pskip_motion`) | **MV 预测**：A/B/C 邻块提取（topright 缺席回退 topleft）、`match_count` 分支（多数取中值 `mid_pred`、单一匹配取该邻块、全不匹配且仅有左邻取 A）、`mvd_cache` 语义（不可用/intra 邻居贡献 0） | ✅ |
| [h264_inter.mbt](libavcodec/h264_inter.mbt) | `h264_cabac.c` 的 P 分支(`decode_cabac_mb_skip`/`decode_cabac_mb_type`/`decode_cabac_p_mb_sub_type`/`decode_cabac_mb_ref`/`decode_cabac_mb_mvd`)、`h264_cavlc.c` 的 P 分支、`h264_mb.c`(`mc_part_std`/`mc_part_weighted`) | **P 宏块层**：skip flag、P_L0_16x16/16x8/8x16/8x8 四种划分与子划分、ref_idx（`ref_count>1` 才解码，且每个分区的 ref 立即写回邻域表供后续分区的 CABAC 上下文使用）、mvd 读取与写回、inter CBP 与残差路径、分块运动补偿编排（CAVLC/CABAC 共用）、显式加权预测（luma/chroma 权重与偏移，`(v*w + off<<denom + round) >> denom`，含 skip 与所有划分） | ✅ |
| [h264_dpb.mbt](libavcodec/h264_dpb.mbt)、[h264_ref_list.mbt](libavcodec/h264_ref_list.mbt)、[h264_reference_marking.mbt](libavcodec/h264_reference_marking.mbt) | `h264dec.c`、`h264_parse.c`、`h264_refs.c` | I/P/B 的 POC 0/1/2、frame_num 回绕、MMCO 1–6/IDR 长参考；帧/场短长参考表与重排、缺失参考替换、B 同表场交换；PAFF 互补场配对、共享平面/运动历史、逐场参考掩码与 DPB 超限回收；frame_num 间隙参考合成、灰图替换与 invalid_gap 过期；Annex B/avcC 多 slice 入口，画面完整后提交参考与 POC | ✅ |
| [aac_ics.mbt](libavcodec/aac_ics.mbt) | `libavcodec/aac/aacdec.c`(`decode_ics_info`/`decode_band_types`/`ff_aac_decode_tns`/`decode_pulses`、gain-control) | **AAC ICS 语法层**：窗口序列/`max_sfb`/分组、section escape、scalefactor 差分、TNS 压缩系数、1..4 个 pulse 的起始带/offset/amp、gain-control；短窗 pulse 与截断语法拒绝 | ✅ |
| [aac_mdct.mbt](libavcodec/aac_mdct.mbt) | `aacdec_dsp_template.c`(`imdct_and_windowing`)、`aacdec.c` 的 raw_data_block/SCE/CPE/LFE 框架、`kbdwin.c` | **AAC IMDCT/加窗 + 多元素帧框架**：直接法 MDCT/IMDCT（O(N²)，适配 FFmpeg 符号与浮点缩放）、四种窗口序列、正弦/KBD 窗；循环解码 SCE/CPE/LFE/FIL/PCE/DSE，按布局输出 PCM，以类型/标签/子声道绑定历史；校验 END 和元素完整性后才提交布局与窗状态，错误时恢复 PNS 随机状态 | ✅ |
| [aac_stereo.mbt](libavcodec/aac_stereo.mbt) | `aacdec.c`(`decode_cpe`/`decode_mid_side_stereo`)、`aacdec_dsp_template.c`(`apply_mid_side_stereo`/`apply_intensity_stereo`) | **AAC-LC CPE**：共享或独立 ICS、M/S 模式 0/1/2 与逐组掩码、噪声带排除、强度立体声 14/15 的极性与 M/S 反转；左右频谱/pulse→M/S→强度立体声→各声道 TNS/IMDCT，左右历史窗口独立 | ✅ |
| [aac_coupling.mbt](libavcodec/aac_coupling.mbt) | `aacdec_proc_template.c`(`decode_cce`)、`aacdec.c`(`apply_channel_coupling`)、`aacdec_float_coupling.h` | AAC-LC CCE：8 个目标/16 组增益、四增益尺度、公共或逐带有符号差分增益、CPE 四种声道选择、TNS 前/后频域耦合与 IMDCT 后时域耦合；独立标签历史、PCE/ASC 声明及成功布局更新时清理移除的 CCE；支持 1024/960 帧 | ✅ |
| [aac_layout.mbt](libavcodec/aac_layout.mbt) | `aacdec.c`(`decode_pce`/`count_paired_channels`/`assign_channels`/`sniff_channel_order`/`skip_data_stream_element`)、`aacdec_tab.c` 的默认布局/声道表 | 默认配置 1..7/11..14、PCE base/top/bottom 三层前/侧/后/LFE 标签映射，元素掩码排序与 22.2 特殊重排、mixdown/associated-data/comment 语法、DSE 对齐和扩展长度；PCE 耦合标签列表及独立声明；拒绝歧义布局、重复标签、越界与无法命名的 height+CCE 组合；配置 7 采用 FFmpeg 默认非 strict 的 7.1 解释 | ✅ |
| [aac_config.mbt](libavcodec/aac_config.mbt) | `aacdec.c`(`decode_audio_specific_config_gb`/`decode_ga_specific_config`) | AAC-LC 专用 ASC 解析：GA 标志、14 位 coreCoderDelay、ASC-only PCE、extensionFlag3 与 consumed bits；`new_aac_decoder_from_asc` 自动建立布局与声道容量；MP4 esds / CodecParameters 解析 LC PCE 声道数；支持 1024/128 与 960/120 帧长，其他 profile/SBR/PS 初始化显式拒绝 | ✅ |
| [aac_spectrum.mbt](libavcodec/aac_spectrum.mbt) | `aacdec_proc_template.c`(`decode_spectrum_and_dequant` 浮点路径)、`aacdec.c`(`decode_scalefactors`/`decode_pulses`)、`aacdec_dsp_template.c`(`dequant_scalefactors`/`apply_tns`、Levinson 展开) | **AAC 频谱解码**：scalefactor 差分、码书 1..11 Huffman + 打包索引/符号位、book 11 escape、PNS 单位能量噪声、intensity 零填充、采样率相关 pulse 位置校验与重建、TNS Levinson + 就地 AR 滤波 | ✅ |
| [aac_tables.mbt](libavcodec/aac_tables.mbt) | `aactab.c` 全部解码表 + `ff_aac_pow2sf_tab`/`ff_cbrt_tab` 生成算法 | 批量提取表：scalefactor VLC（121 项）、码书 1..11 的 codes/bits、5 张 `codebook_vector_*_idx` 打包索引表、3 张 vals 表、`pow2sf` 428 项 float32、`pow43` 8192 项、swb 偏移表（1024/128 与 960/120 全采样率）与 `tns_max_bands`；提取脚本 `scripts/extract_aac_tables.py`（与 C 源逐值 diff 校验） | ✅ |

**当前状态**：`moon check` 0 error / 0 warning，`moon test` **699/699 全绿**。各格式的能力边界见本节末尾的迁移矩阵。

### CABAC 的验证范围

CABAC 算术核心（`cabac.mbt`）**已用 FFmpeg 自己的 `libavcodec/tests/cabac.c` 向量验证**：把该测试里的参考编码器一并移植（`cabac_wbtest.mbt`），10240 个 bypass 比特 + 10240 个上下文比特全部逐比特还原，终止位也命中。`av_lfg_get`（`libavutil/lfg.c`，MD5 播种）在测试里复刻，向量完全可复现。

宏块层已对上 FFmpeg 输出的组合包括

* 单宏块（`testsrc2` 16x16）、一行 2 / 3 / 4 个宏块（左邻上下文，`testsrc2` 稠密残差逐像素等于 FFmpeg `-skip_loop_filter all`）；
* 平坦 I16x16 内容的 2 行 / 3 行（上邻上下文 + luma DC 的 coded-block 标志）；
* 3x3 宏块（48x48 稠密）；
* **单行 I + 全 skip CABAC P 帧经滑窗 DPB 端到端**（`libavformat/h264_cabac_p_realdata_wbtest.mbt`：MP4 `avcC` + 长度前缀样本入口与 Annex B 入口各一条，逐帧 YUV MD5 等于 FFmpeg 默认解码）。

**2026-10-22 复核**：旧文档所列的 16×48 / 32×48 / 64×48 稠密多行 I 流和静态编码型 P 流已通过。新增 `h264_cabac_regression_wbtest.mbt`：上述三条 I 流、64×16 I+3P（关闭 fast-pskip）和 96×64 运动 I+5P，共 **13 帧**，默认滤波与 `-skip_loop_filter all` 两套 YUV MD5 均完全一致。16×48 的 2403 条、64×48 的 8233 条 CABAC 上下文/状态/解码位与禁用汇编的原 C 参考逐条一致。该证据覆盖这些流；B/SP/SI、场编码、高位深与多 slice 等边界仍按入口限制处理。

已修掉并沉淀进 `agent.md` 的坑（都会被这三个向量立刻抓住）：`cbp_all` 在残差后不能被重置（DC 标志会被抹掉）、CABAC 上方邻居的 `nz` 要取上宏块**最后一行**（luma `nz[12+x]`、chroma `nz[2+x]`）、`write_back_intra_pred_mode` 发生在 `check_intra4x4_pred_mode` **之前**（邻居上下文用原始模式而非改判后的 DC 变体）、`cbp==0` 且非 I16x16 的宏块必须复位 `last_qscale_diff`（否则下一宏块的 `mb_qp_delta` 上下文选错）。

**CABAC P 路径**另有一组合成码流端到端向量（`h264_psynth_wbtest.mbt`）：手写 SPS/双
PPS（CAVLC+CABAC）、全 `I_PCM` 的 IDR 帧（非平坦图案，也是 `I_PCM` 路径的首个真实
覆盖）与逐宏块 `P_L0_16x16` + 整数 MV (-8,-8) 的 CABAC P 帧——编码器是
`cabac_wbtest.mbt` 参考编码器的镜像，skip/type/mvd/CBP 的每个上下文推导都按解码端
逐 bin 复刻（含 `amvd` 邻域、inter CBP 的逐 bit 邻域与不可用邻域 `0x00F`）。P 帧必须
等于参考帧按 MV 平移（边界按 `ff_emulated_edge_mc` 复制）。该向量曾抓出两个潜伏
bug：**chroma 运动补偿漏掉块在帧内的基准位置**（此前只有基准 0 的单元测试，列 0
以外的宏块全部取样错位）与 **DPB 参考图的 `height` 字段误取 `cstride*2`**（真实
P 码流的子像素插值一旦触底即越界 panic）。

### AAC 的差分验证与剩余范围

**既有真实向量的逐样本差分已收敛**：libfdk_aac 生成的 44100 Hz 单声道正弦淡入流
（46 帧、全 ONLY_LONG、KBD 窗），全部 **47,104 样本最大误差 1 LSB**，其中
**47,091 样本完全一致**。参考 PCM 已由本机 Docker 的 FFmpeg 9.0.2 重新解码核对，
与内嵌素材逐字节相同。回归测试强制完整解码、最大误差 ≤1 LSB、精确匹配率 ≥99.9%。

此前的大幅度饱和与符号位疑点已定位并修复：

* 索引表的逐项值虽正确，**码书到表的指针映射抄错**：books 7/8 均应使用
  `codebook_vector6_idx`，9/10 均使用 `codebook_vector8_idx`，仅 11 使用
  `codebook_vector10_idx`。提取脚本现直接读取 C 的指针表并校验各表长度；
  `python scripts/extract_aac_tables.py --check` 可核对全部 55 个数组而不改格式。
* FFmpeg 半逆变换是本移植教科书 IMDCT **中间一半的负值**；AAC 浮点路径的
  `init_dsp` 缩放为 `1/(N*32768)`，相对于本移植的 `2/N` 还需乘 **-1/65536**。
  长短窗均已修正，窗表长度分别为 1024/128。
* 参考 PCM 已包含编码器延迟，差分时应对齐同一帧；原探针额外偏移 1024 样本。
* 新增全 11 本码书的**所有元组与非零值符号组合**回归，独立按笛卡尔积顺序构造
  期望系数，并断言位消费数量及尾随哨兵；长短窗综合另对照
  `ff_tx_mdct_naive_inv` 的余弦公式、符号与 PCM 缩放。

**新增真实短窗与窗口转换对拍**：48 kHz 单声道突发信号分别由 FFmpeg native AAC
与 libfdk_aac 编码，参考 PCM 由 FFmpeg 9.0.2 解码取得。native 的 13 帧覆盖
ONLY_LONG/LONG_START/EIGHT_SHORT/LONG_STOP = 1/2/9/1、24 个多窗分组、70 个 PNS
频带；FDK 的 14 帧对应 3/2/7/2、16 个多窗分组、13 帧 TNS。两者均包含正弦/KBD
窗，合计 **27,648 样本最大误差 1 LSB，27,644 样本完全一致**，测试强制窗口/工具
覆盖计数和精确匹配率 ≥99.9%。生成脚本：`scripts/generate_aac_transient_vectors.py`。

**pulse 工具已移植**：解析 1..4 个 pulse、按实际采样率解析 SWB 起点与累计位置、
在谱系数反量化后/TNS 前执行 pulse 重建；噪声与零缩放频带保持原值。语法解析器保留
原始 band/offset，帧解码器在读取频谱前检查位置（与 C 把位置转换放在语法解析中的
组织方式不同，计算与边界相同）。六帧合成码流由 FFmpeg 解码取得参考，覆盖零系数、
正负系数、跨带、重复位置以及 ZERO_BT/NOISE_BT 跳过：**6,144 样本最大误差 1 LSB，
6,136 样本完全一致**；pulse 多步浮点运算采用 Double，精确匹配率要求 ≥99.8%。
另有短窗 pulse、截断语法、非法起始带和越界位置的负例，以及多采样率边界回归。

**新增 AAC-LC CPE 双声道解码**：支持共享 ICS 窗口和左右独立窗口、M/S 的 0/1/2
模式、强度立体声及其 M/S 极性翻转。先解左右频谱/pulse，再做 M/S、强度立体声，
之后各自执行 TNS/IMDCT；共享当前窗口信息不会覆盖右声道的历史窗口形状。

立体声参考由 `scripts/generate_aac_stereo_vectors.py` 生成：native 10 帧（M/S 全带、
20 个强度立体声频带、241 个 PNS 频带），FDK 11 帧（M/S 显式掩码、86 个强度频带、
18 个声道帧 TNS），两者均有短窗；另有 5 帧左右独立窗口与 9 帧显式 M/S/强度/PNS
组合的合成码流。四组参考 PCM 均由 FFmpeg 9.0.2 解码同一码流取得，双声道合计
**71,680 样本最大误差 1 LSB，71,655 样本完全一致**。测试强制左右声道、帧数、
工具覆盖计数与精确匹配率 ≥99.9%。负例覆盖保留的 M/S 模式、掩码截断、越界频带、
声道状态不足和非法尾元素；尾部校验失败不会推进左右窗口历史。

**新增多元素声道布局**：默认 channel_config 1..7 与带内 PCE 已接入，支持同块多个
SCE/CPE/LFE，按 AVChannel 顺序返回 PCM；PCE 支持前/侧/后/LFE 分组、非连续标签、
mixdown/associated-data/comment 语法。PCE 的四位 instance tag 已正确消费，布局跨帧
保留；历史缓冲按元素类型/标签/子声道绑定，元素顺序改变不会交换声道历史。输出顺序
可读 `AacDecoder.layout`，`channels` 保存历史槽位，槽位次序不等同于输出顺序。
`new_aac_decoder(config, n)` 的 n 是容量，必须足以容纳实际布局。带内 PCE 成功提交后
更新 config 的声道数并将 channel_config 置 0。双 SCE 的 indexed stereo 也可解码。

`scripts/generate_aac_layout_vectors.py` 新增五组参考：真实默认 5.1、PCE 5.1/PCE 7.1
各五帧，非连续标签双 SCE 和乱序 PCE 5.1 各四帧，共 **135,168 个声道样本最大误差
1 LSB，135,137 个完全一致**，精确匹配率要求 ≥99.9%。7.1 编码强制 PCE，因为 native
编码器的 indexed config 12 超出 ADTS 三位字段。合成码流每帧交换音频元素顺序，
并覆盖 DSE 普通/扩展长度与对齐；期望 PCM 全部由 FFmpeg 解码同一码流取得。
负例覆盖默认布局范围、PCE 的逐位截断/重复/空布局/不支持排列、缺失/重复/未声明
元素和 DSE 截断。失败帧不提交新布局、不推进窗口历史，并恢复 PNS 随机状态。

**新增 ASC-only PCE 初始化与容器桥接**：`aac_parse_audio_specific_config` 在通用
MPEG 音频头解析之后读取 AAC-LC GASpecificConfig，包括 coreCoderDelay、PCE 和
extensionFlag3，返回配置、布局和 GA 消费位数；PCE 对齐仍相对于 ASC 起点，不因
14 位延迟字段或 PCE 起始位置改变。`new_aac_decoder_from_asc` 据此分配声道容量并
保存布局，MP4 原始音频包无需重复带内 PCE。MP4 esds 与容器通用 CodecParameters
桥接（也用于 Matroska）现能从 LC ASC 的 PCE 得到声道数，避免 config=0 被当作 0 声道。
通用 `ff_mpeg4audio_get_config` 的返回契约及其他音频类型路径保持原有行为。

`scripts/generate_aac_asc_vectors.py` 生成三个真实 MP4：indexed stereo、仅 ASC
携带 PCE 的 5.1/7.1。ffprobe 提供 ASC 和逐包 offset/size，参考 PCM 由 FFmpeg
`-flags2 +skip_manual` 解码，保留 priming 和完整编码帧。三组共 12 包、**65,536 个
声道样本最大误差 1 LSB，65,528 个完全一致**，精确匹配率要求 ≥99.9%；测试同时核对
轨道、参数桥接和初始化后的声道数/布局。新增 GA/PCE/延迟/extensionFlag3 截断、
不支持 profile/采样率与 SBR/PS 的负例；非法 PCE 参数桥接保留容器回退值。
**新增 960/120 帧长综合**：移植 FFmpeg 的 960/120 频带表、正弦/KBD 窗及
420/60 过渡窗重叠，ASC 工厂按 frameLengthFlag 选择几何。短窗频谱仍保持 128 点
步长，IMDCT 结果按 120 点排列；MP4 与参数桥接的 frame_size 同步为 960。
`scripts/generate_aac_960_vectors.py` 合成单/双声道各 10 帧，覆盖四窗口序列、
两种窗形、独立 CPE 窗形及首尾频带；FFmpeg 解码同一 MP4 的 **28,800 个声道样本
最大误差 1 LSB，精确匹配率 ≥99.9%**。本机 FDK 编码器拒绝 960 granule 配置，
因此尚未验证真实编码器产生的 960 码流。合成 MP4 由等长 ADTS 占位包重封装后
替换包内容并设置 ASC 标志；测试容器时间戳保留重封装器的 1024 tick，逐帧 PCM
参考保留完整编码帧，每帧输出 960 样本。

**新增 AAC-LC CCE 声道耦合**：`aac_coupling.mbt` 移植 CCE 语法、目标选择及增益链，
支持最多 8 个目标、16 组增益、四个尺度、公共和逐带差分增益（含符号与零增量缓存）。
源频谱先完成 TNS，目标分别在 TNS 前、TNS 后或 IMDCT 后接收耦合；多个源按标签
顺序累加，独立 CCE 的窗口历史不占输出声道容量。PCE/ASC 保存耦合标签列表，
带内 PCE 成功移除标签后清除对应历史。重复源、未声明源、缺失目标及截断帧在提交
历史前报错；失败帧保持源/目标窗口和 PNS 状态。截断回归同时修复 section data
读尽后持续读取补零而无法退出的问题；空 section 仍遵循 FFmpeg 行为继续消费输入。

`scripts/generate_aac_coupling_vectors.py` 合成 7 组 1024 帧 ADTS 和 2 组 960 帧 MP4：
覆盖四窗口序列、交替窗形、分组短窗、源/目标 TNS、全部增益尺度与 CPE 选择，
包括 16 组增益上限、乱序多个 CCE 和 ASC/PCE 引导。FFmpeg 9.0.2 解码同一码流
产生参考，共 **151,040 个声道样本最大差分 1 LSB，精确匹配率 ≥99.9%**。
这是合成语法覆盖，尚未取得真实编码器产生的 CCE 样本；960 测试容器的时间戳
仍保留 ADTS 重封装的 1024 tick，PCM 对比使用每包完整 960 样本。

**新增高度 PCE 与 indexed 11..14**：按 `decode_pce` 读取 comment 中的 0xAC
高度扩展，先按 base/top/bottom 重排，再移植 `sniff_channel_order` 的三层前/侧/后/LFE
推导。高度值 3 按 C 行为保留原始顺序；短 0xAC comment 仍视作普通注释，高度字段
长度不包含 LFE。普通命名布局按整个元素的声道掩码排序，CPE 左右始终相邻；22.2
执行 C 的九次元素交换，输出顺序有意保留 FFmpeg 的特例，并非逐个 AVChannel 排序。
默认配置 11/12/13/14 及 ASC 工厂、MP4 参数桥接、24 声道容量现在全部接通。

`scripts/generate_aac_height_vectors.py` 合成 4 组 indexed MP4、4 组带高度注释的 ADTS
和一组 ASC-only PCE 的 960 帧 22.2 MP4，由 ffprobe 提供元数据、FFmpeg 9.0.2 解码
同一字节流提供参考 PCM。共 **473,088 个声道样本最大差分 1 LSB，精确匹配率
≥99.9%**，覆盖四窗口序列、交替窗形、PCE 元素乱序、顶层中置/CPE 邻接、保留高度值
回退及 22.2 的 LFE2、顶层、底层。另有 PCE 注释截断与失败帧不推进历史的回归。
这些高度与 indexed 扩展向量为合成语法，真实编码器产生的高度样本尚待补充；960
测试容器时间戳仍保留重封装的 1024 tick，参考比较每包完整 960 样本。

剩余范围与偏差：只支持能映射为命名声道的三层 PCE；C 可回退为 unspecified
顺序的歧义布局在本移植中显式拒绝，高度与 CCE 并用时暂拒绝无法命名的布局；
SBR/PS、USAC 未接入。默认配置 8..10、15 保留/不支持，配置 7 跟随 FFmpeg 默认
非 strict 的 7.1 解释；
未复刻错误 element tag 的位置重映射等兼容 quirk。无布局配置时保留单音频元素的
语法测试入口，多元素必须有 indexed/PCE 布局。更多采样率和工具组合仍需扩充。

### AES 的实现偏差（已记录）

`aes.mbt` 是 **libavutil 内唯一一处**按规范而非按 C 源码逐行实现的地方（libavcodec 的
Huffman 规范化码另有等效性说明，见下文）：

- **逐行移植**的部分：S-box / 逆 S-box 的生成（`aes_init_static` 的 log8/alog8 + 仿射变换）、
  `rcon` 表、密钥扩展的 Rcon/SubWord 结构（含 AES-256 的 `KC == 8 && j == KC >> 1` 特例）。
- **按 FIPS-197 规范实现**的部分：轮函数（SubBytes / ShiftRows / MixColumns / AddRoundKey
  及其逆）。FFmpeg 的 `subshift` / `mix` / `multbl` 表驱动内核是同一算法的等价优化，
  其字节布局依赖小端 `u32`/`u64` 类型双关与 `sizeof(av_aes_block)` 指针运算；复刻风险高、
  零行为收益，而 FIPS-197 形式可用官方标准向量直接验证。
- **后果**：轮密钥按 FIPS-197 顺序存放，而非 FFmpeg 内部对解密预变换的形式；
  `av_aes_init` / `av_aes_crypt` 的签名与 ECB/CBC 行为保持一致。

### libavcodec 切片（IDCT + Huffman）的范围边界与说明

- **已做（IDCT）**：`simple_idct_template.c` 的 `int16` 输入三档（`BIT_DEPTH` 8/10/12，
  均 `IN_IDCT_DEPTH == 16`）的 `put`/`add`/原地变换，以及 `idctdsp.c` 的钳位像素与扫描表
  辅助。C 宏（`W1..W7`、`ROW_SHIFT`/`COL_SHIFT`/`DC_SHIFT`、`MUL16`/`MAC16`）化为
  MoonBit 常量与内联算术；`SUINT` 用 `UInt`（32 位回绕）复刻生产构建的溢出语义，写回
  块时按 `int16` 截断（`i16`）。
- **未做（IDCT）**：`int32` 输入变体 `ff_simple_idct_*_int32_10bit`（MPEG-4 Simple Studio
  Profile）、2x4x8/8x4/4x8 的部分变换（DV/WMV2 的 `idct248`/`84`/`48`/`44`）、整数
  `FF_IDCT_INT`（`jrevdct.c`）、FAAN、Xvid 的**标量 C** 实现；其 x86 SIMD 变体
  （`libavcodec/x86/simple_idct.asm`、`simple_idct10.asm`、`idctdsp.asm`）按新边界原则
  列入下方「`@v128` 重写」清单，而非"留在 C"。
- **已做（Huffman）**：`ff_huff_build_tree`（排序 + 两列表原地合并 + `get_tree_codes` DFS）
  与 `ff_huff_gen_len_table`（堆 + `offset <<= 1` 长度上限重试）。二者返回 `(lens, xlat)`，
  即 `ff_vlc_init_from_lengths` 需要的输入。
- **说明 / 偏差（Huffman）**：不移植整个 VLC 子系统（`vlc.c` 的多级查表）；为使结果可
  解码验证，另按规范化 Huffman 规则由 `(lens, xlat)` 直接生成码字并做 MSB-first 编/解码
  （与 `ff_vlc_init_from_lengths` 的「按发射序累加码字」在码长非递减时等价）。FFmpeg 的
  `AV_QSORT` 非稳定排序，等频次叶子的次序可能与 FFmpeg 不同，但仍是同一最优码（测试以
  Kraft 等和 + 独立教科书法的最优代价互证）。
- **未做（libavcodec）**：LZO、`vlc.c` 表构建、AC-3 等音频解析、其余编解码器
  （见阶段 5）。

### libavcodec 码流层切片（H.264 / H.265 / AAC / MP3）的范围边界与说明

- **H.264**：`h2645_parse.mbt` 完成 RBSP 去防竞争字节、Annex B 与长度前缀两种 NAL 拆分、
  H.264/HEVC NAL 头；`h264_ps.mbt` 完成 SPS/PPS 全语法（含高 profile 缩放矩阵、VUI、
  HRD、bitstream restriction、裁剪尺寸、`num_reorder_frames` 的水平表推导）与
  `avcC`/Annex B extradata。**像素解码已开始**：Baseline I 帧重建通路（slice header、
  CAVLC、宏块重建、环路去块滤波）见下一小节；**SEI 已完成**（`h264_sei.mbt` + `itut35.mbt` 的 T.35 与
  `h264_side_data.mbt` 的帧 side-data/NAL 边界语义）；**P/B 帧与滑窗 DPB 已完成**（`h264_inter/mvpred/mc/dpb` + `h264_b_*`：
  B 全子分区、空间/时间 direct、显式/隐式双向加权与显示重排；SP/SI 重建仍拒绝；
  单色 4:0:0 已支持，按 FFmpeg 的 yuv420p 中性色度输出逐字节对齐；
  MBAFF 帧图（I/P/B、CAVLC/CABAC、混合 field/frame 宏块对、多参考/B-pyramid、
  空间/时间 direct、CQM、8/10-bit 4:2:0/4:2:2/4:4:4、多 slice）已逐字节对齐，
  High 4:4:4 lossless transform bypass（4:0:0/4:2:0/4:2:2/4:4:4、8/10-bit、CABAC/CAVLC、含 MBAFF）
  也已逐字节对齐；PAFF 场图与帧场交叉引用已验证（I/P/B、CAVLC/CABAC、空间/时间 direct，
  8/9/10/12/14-bit、4:0:0/4:2:0/4:2:2/4:4:4、POC 0/1/2、bottom-first、
  多 slice 与禁止跨 slice 去块），具体组合见下方 PAFF 验收矩阵；
  CABAC 的多行与运动 I/P 验证范围见 §3。
- **H.265**：`hevc_ps.mbt` 完成 VPS/SPS/PPS 与 PTL、short-term RPS、VUI、缩放列表、
  Range Extension 与 SCC 语法、`hvcC`/Annex B extradata、POC 计算。**未做**：slice
  segment header、CABAC、预测/变换/滤波、SEI；多层扩展（VPS multilayer、PPS
  multilayer/3D）不建模，遇到即返回 `PATCHWELCOME`；HRD 字段按位消费但不存储。
- **AAC**：`audio_headers.mbt` 完成 ADTS 头；`rawaudio.mbt` 完成裸 ADTS 流逐帧分帧
  （含 ID3v2 跳过与重同步）。**频谱解码已完成**（`aac_spectrum.mbt` + `aac_tables.mbt`：
  Huffman 码书 1..11 含 escape、scalefactor 差分/noise 前导、pulse 重建、PNS、TNS 应用、gain
  control 语法解析；`aac_mdct.mbt`：全四窗口序列 + KBD/正弦窗的 IMDCT→TDAC 叠接），
  CPE 的共享/独立窗口、M/S 和强度立体声已接入，真实长短窗单/双/多声道码流可逐帧
  解码出 PCM，**现有单/双/多声道向量的逐样本差分
  最大为 1 LSB**，含 TNS/PNS/pulse（见 §3「AAC 的差分验证与剩余范围」）；
  默认配置 1..7/11..14、多元素 SCE/CPE/LFE、三层 PCE 及 CCE 已接入；
  SBR/PS、USAC 未做；960 综合已用合成码流对拍。
- **MP3/MP2/MP1**：`audio_headers.mbt` 完成 MPEG 音频头（版本/层/码率/采样率/帧长/
  自由格式）；`rawaudio.mbt` 完成裸流分帧、ID3v2/ID3v1 跳过与首帧 Xing/Info/VBRI
  识别剔除。**三层定点解码均已完成**（见下一小节）。
- **容器接入**：`codec_tags.mbt` 负责 Matroska CodecID / MP4 fourcc 到统一编解码器名的
  映射，`codec_parameters.mbt` 把容器的尺寸/采样率与 `avcC`/`hvcC`/AudioSpecificConfig
  解析结果合并成 `CodecParameters`（profile/level/位深/色度格式/像素格式/帧率/SAR/
  AAC 帧长），MKV 与 MP4 共用同一结构。
- **偏差**：H.264 的 `AV_CODEC_FLAG2_IGNORE_CROP`、`SMV2` codec tag、严格合规级别选项
  未建模；HEVC 的 `setup_pps` 瓦片扫描表未建；像素格式名采用本机小端名
  （如 `yuv420p10le`），与 FFmpeg 的 `AV_PIX_FMT_YUV420P10` 在 x86 上的取值一致。

### libavcodec 解码切片（H.264 Baseline I 帧 / MPEG 音频 Layer I·II·III）的范围边界与说明

这是本项目第一次产出**像素与 PCM**，不再是纯解析层。

**H.264 I 帧重建 + 去块滤波**（`h264_slice.mbt` + `h264_cavlc.mbt` + `h264_idct.mbt` +
`h264_pred.mbt` + `h264_decode.mbt` + `h264_deblock.mbt`）：

- **已做**：I/SI slice header；CAVLC 残差（`coeff_token`/`total_zeros`/`run_before`/
  `level_prefix`+`suffix`，含 chroma DC 的 4 项特殊码表）；PPS 派生的 `dequant4_coeff`
  （`dequant8_coeff` 未建，因为 8x8DCT 直接拒绝）与 `chroma_qp` 表；4x4 IDCT（行/列两遍）
  与 luma/chroma DC Hadamard；4x4 九模式、16x16 四模式、chroma 8x8 四模式的帧内预测与
  可用性改判；`I_4x4`/`I_16x16`（含 `cbp` 六类）/`I_PCM` 三条宏块路径；8x8 z-order 的
  块重建次序与 top-right 可用性掩码；SPS 帧裁剪与 `yuv420p` 打包；**环路去块滤波**
  （宏块边 bS=4 走强核、内部边 bS=3 走弱核，左右/上下 QP 取均值，`alpha`/`beta`/`tc0`
  三张表 + slice 级 `alpha_c0`/`beta` 偏移，`disable_deblocking_filter_idc` 0/1/2）。
- **验证**：默认入口 `h264_decode_annexb_i_frames` **本身就做去块滤波**，因此参考输出是
  FFmpeg 的**默认解码**；需要未滤波像素时用 `h264_decode_annexb_i_frames_ex(data, true)`
  对照 `ffmpeg -skip_loop_filter all`（该选项必须在 `-i` 之前）。两种模式都有对应向量。
- **滤波时序（踩过的坑，务必保持）**：帧内预测必须看到**未滤波**的重建像素，而滤波必须
  按光栅序、且下一行的边要看到**已滤波**的上一行。FFmpeg 用两个机制同时满足：逐行
  `loop_filter()` + 预测读滤波前备份的 `sl->top_borders`。本移植的等价做法是
  **先把整帧重建完，再按宏块光栅序整帧滤波**——「每解一个宏块就立刻滤波」会让后续宏块
  的帧内预测吃到已滤波的左邻，整帧像素会偏。
- 该切片当时的边界是 Progressive 8-bit 4:2:0 单 slice；后续切片已解除大部分限制：
  CABAC、P/B slice、多 slice、8x8DCT、8–14 bit 与 4:2:2/4:4:4 均已支持
  （见 2026-10-22/23 增量）。**仍显式拒绝**（返回 `Err`，不 panic）：场/MBAFF、
  separate colour plane、单色、SP/SI 重建；参考帧间隙已补齐，见下方验收。
- **代码已写但无真实向量**：`I_PCM` 宏块路径（x264 的 baseline profile 不接受
  `-qp 0` lossless，且 lossless 走变换旁路而非 I_PCM，需手工构造码流才能覆盖；该路径在
  滤波里按 C 的 `qscale_table = 0` 处理）。
- **未接**：SEI 已在后续切片完成（`h264_sei.mbt`）；`chroma_qp_diff`
  （`second_chroma_qp_index_offset` 与第一个不等）时的非 fast 路径
  ——当前按 fast 路径用 plane 0 的偏移派生出 Cb/Cr 各自的 QP。

**H.264 High profile 8x8 变换**（`h264_cavlc.mbt` 的 dequant8 表 + `h264_cabac_mb.mbt` 的 cat5 残差 + `h264_idct.mbt` 的 `idct8_add` + `h264_pred8x8l.mbt` + 去块/重建接线）：

- **已做**：PPS `transform_8x8_mode` 下的完整 8x8DCT 路径——CABAC 残差 category 5（luma cbp 位隐含 cbf、`significant_coeff_flag_offset_8x8` 上下文与压缩的 `last_coeff_flag_offset_8x8` 表）、`dequant8_coeff`（注意 C 的 8x8 移位是 `quant_div6[q]`，**没有** 4x4 表的 +2）、`ff_h264_idct8_add`/`idct8_dc_add`（`idct8_add4` 按 `nnz==1 && DC!=0` 走 DC 捷径）、`get_dct8x8_allowed`（P_8x8 四个子块都是 8x8 且 `direct_8x8_inference` 才允许）、`neighbor_transform_size` 上下文（399+左/上 8x8DCT 邻居数）、`pred8x8l_*` 九模式（参考行/列 3 抽头滤波 + topleft/topright 可用性）与 `ff_h264_check_intra4x4_pred_mode` 的 DC 边缘改判（LEFT_DC/TOP_DC/DC_128 对应 `pred8x8l_left_dc`/`_top_dc`/`_128_dc`）、去块滤波跳过 8x8DCT 宏块的内部奇数 luma 边。CAVLC 路径仍显式拒绝 8x8DCT（与 FFmpeg 的 CAVLC 8x8 支持面一致取舍）。
- **验证**：4 条真实 libx264 High profile 码流（inter 8x8DCT 单参考/双参考、噪声内容 + 去块、CRF36 all-intra 含 intra 8x8DCT 宏块），滤波与 `-skip_loop_filter all` 两种模式逐帧 YUV MD5 与 ffmpeg 一致；测试 **535/535**。
- **踩过的坑**：`h264_zigzag_scan8x8` 是 `ff_zigzag_direct` 的**转置**（`h264_slice.c` 的 `TRANSPOSE`），系数块本身按转置布局存储；`idct8_add` 第二遍按行内列偏移 0/4/2/6 读系数（不是第一遍的 +8 步长）；`pred8x8l` 的 DDR/VR/HD 用**滤波后的** top/topleft 样本，且 `ff_h264_check_intra4x4_pred_mode` 可能把模式改判到 9/10/11（`pred8x8l[9+3]` 表）。

- **已做**：MPEG-1/2/2.5 Layer III、mono/stereo/joint stereo（MS 与 intensity）、短块
  重排、scalefactor（slen 表 + `ff_lsf_nsf_table`）、15 张 Huffman 表（符号按 C 的
  `transform_huff_symbol` 变换）、反量化与 `ff_mpa_pretab` 预增益、定点 IMDCT/DCT32/
  合成窗、bit reservoir、输出 S16 交织 PCM。数值路径完全走 C 的 `USE_FLOATS=0`。
- **偏差（bit reservoir）**：C 用 `last_buf` 滚动缓冲 + `gb`/`in_gb` 双读取器切换；本
  移植改为「保留每帧 main-data 区拼接 + 从 `8*main_data_begin` 位前起读」，字节流与 C
  重建的一致，良性码流下逐位相同；reservoir 超过 8 KiB 时截断到最近 4 KiB。
- **未做**：浮点路径、自由格式码流的解码、合成后处理（如 LAME 的 `iTunSMPB` 相关裁剪）。
- **验证**：与 FFmpeg **定点**解码器（`-c:a mp3`，非 `mp3float`）的 PCM MD5 逐字节
  一致——这正是区分定点/浮点两条实现的关键点（浮点解码器给出不同 MD5）。

**MPEG 音频 Layer I / Layer II 定点解码**（`mpa_layer12.mbt` + `mpa_layer12_tables.mbt`）：

- **已做**：Layer I 的 4 位 bit allocation（joint stereo 时按 `bound = (mode_ext+1)*4`
  切分）、6 位 scalefactor、`n+1` 位尾数；Layer II 的分配表选择（`ff_mpa_l2_select_table`
  按每声道码率/采样率/LSF 选 5 张表之一，`sblimit` 分别为 27/30/8/12/30）、`scale_code`
  的四种 scalefactor 共享模式、分组量化（3/5/9 步，用 `ff_division_tabs` 一次取三个样本）
  与线性量化、joint stereo 的 `bound` 共享区（一份尾数、两个 scalefactor）。
  两个层都不用 IMDCT、混叠抑制和 bit reservoir，量化后的子带样本直接进三层共用的
  多相合成滤波（`mpa_synthesize`）。
- **数值路径**：`l1_unscale` / `l2_unscale_group` 与 C 完全相同——`ff_scale_factor_modshift`
  拆出 `shift`/`mod`，乘子是 `MULLx(norm, FIXR(k*2), FRAC_BITS)` 与 `SCALE_GEN`，全部
  按 C 的初始化表达式在 MoonBit 里算出来（含 `4/3`、`4/5`、`4/9` 三组组量化乘子）。
- **验证**：Layer II 用 FFmpeg 原生 `mp2` 编码器生成 4 条码流（stereo 44.1k/192k、
  mono 44.1k/64k、joint stereo 32k/64k、MPEG-2 LSF stereo 24k/64k），覆盖分配表
  1/0/3/4、`bound` 共享区、分组与线性量化；参考 PCM 取 FFmpeg **定点**解码器
  `-c:a mp2`。**Layer I 在 FFmpeg 里没有编码器**，所以两条 Layer I 向量是按规范手工
  拼的帧（3 帧/流，让 mp2 demuxer 能探测），但期望值仍是 FFmpeg 解码同一段码流的
  PCM MD5——手工的是输入，不是期望值。

**FLAC 无损解码**（`flac_decode.mbt`）：

- **已做**：`fLaC` + 元数据块（STREAMINFO 的采样率/声道/位深/总样本数）、frame
  header（同步码、块大小/采样率码表、UTF-8 帧号、声道模式、位深）、constant/
  verbatim/fixed(order 0-4)/LPC(order 1-32) 子帧、Rice 分区残差（含 escape 原始
  样本）、wasted bits、`lpc16`/`lpc32` 预测（`lpc16` 按 C 的 **有符号 int** 累加与
  算术右移，这是与 `lpc32` 的 int64 路径唯一不同处）、independent/left-side/
  right-side/mid-side 去相关。
- **验证**：9 条真实 FLAC（mono/stereo LPC、24-bit、静音 constant、低压缩 fixed、
  `lpc_type=none` verbatim、left/right/mid-side 强制模式），解码后 S16/S32 交织 PCM
  的 MD5 与 `ffmpeg -i x.flac -f s16le/s32le` 逐字节一致；测试 **535/535**。
- **未做**：CRC 校验（与 Layer III 一致：只跳过 16 位 CRC）、自由格式码流、Layer II 的
  ISO 13818-3 低采样率扩展表以外的私有编码器约定（FFmpeg 也只有这 5 张表）。

### SIMD（`@v128`）化候选（已移植代码）

按新边界原则，FFmpeg 原用 NASM 写的向量化内核改用 MoonBit `@v128` 重写。下表为**已移植
代码**中可 SIMD 化的位置。「FFmpeg x86 对应」列是 FFmpeg 侧同功能的 `*.asm`（仅作参照与
可行性依据，**不是**要保留的黑盒）；「—」表示该功能在 FFmpeg 中本就是标量 C（仍可选做
并行化，但无现成参照）。

| 已移植模块 | 可 SIMD 化的内核 | FFmpeg x86 对应 | 说明 / 优先级 |
|---|---|---|---|
| [adler32.mbt](libavutil/adler32.mbt) | ✅ **已完成**：`av_adler32_update_v128`（`i32x4_dot_i16x8_s` 算 Σb 与 Σ(16-i)b） | —（FFmpeg 亦为标量 C） | 高；已与标量逐值一致 |
| [crc.mbt](libavutil/crc.mbt) | —（FFmpeg 的 `crc.asm` 用 **PCLMULQDQ**，Wasm SIMD128 无法表达） | `crc.asm` | **例外 → FFI**；纯软件只能做「多路并行查表」（ILP），并非向量化 |
| [base64.mbt](libavutil/base64.mbt) | ✅ **已完成**：`av_base64_encode_v128`（`i8x16_swizzle` 展开 + `i32x4` 取字段 + `i8x16_shuffle` 重排，字母表无分支映射） | `base64.asm`（libavcodec） | 高；与标量/RFC 4648 逐值一致 |
| [idct.mbt](libavcodec/idct.mbt) | ✅ **已完成（8/10/12 位）**：`idct_v128.mbt` 的 `ff_simple_idct{,_put,_add}_v128{,_10bit,_12bit}`（块改 128 字节 LE-int16 缓冲） | `simple_idct.asm`、`simple_idct10.asm`、`idctdsp.asm` | 高；8 位与标量逐值一致；10 位满足 dct.c 判据；12 位在 dct.c 中标 `nonspec=1`（其 omse 判据不适用），实测 profile 与标量一致 |
| 同上：`ff_put/add_pixels_clamped_c` | 已由 `put`/`add` 的 SIMD 路径覆盖（`i8x16_narrow_i16x8_u` 饱和钳位） | `pixblockdsp.asm`、`blockdsp.asm` | 高；独立的 8/16 路版本待做 |
| [h264_idct.mbt](libavcodec/h264_idct.mbt) | 4x4 IDCT 与 DC Hadamard（块级，天然 8/16 位并行） | `h264_idct.asm`、`h264_idct_10bit.asm` | 中；需先把系数的转置布局与 `v128` 载入对齐 |
| [h264_pred.mbt](libavcodec/h264_pred.mbt) | 帧内预测（16x16/chroma 的整块填充与 4x4 的多次 4 行搬移） | `h264_intrapred.asm`、`h264_intrapred_10bit.asm` | 中；DC/plane 的横向/纵向求和可用 `i16x8` 归约 |
| [h264_deblock.mbt](libavcodec/h264_deblock.mbt) | 去块滤波四个核（luma/chroma × 弱/强）：16 或 8 条边沿样本天然是一条 `i16x8`/`i8x16`，`p0/p1/p2/q0/q1/q2` 六个向量载入后按掩码混合 | `h264_deblock.asm`、`h264_deblock_10bit.asm` | 中高；`|p0-q0| < alpha` 等判据可转成 `i8x16_lt_u` + `bitmask` 生成的逐通道掩码，避免分支 |
| [mp3_dsp.mbt](libavcodec/mp3_dsp.mbt) | IMDCT36 / DCT32 / 合成窗（多相滤波的乘加链） | `x86/mpegaudiodsp.c`（SSE 内联） | 中；32 位定点乘加需 `i32x4` 双倍宽技巧，先保证与标量逐样本一致 |
| [aes.mbt](libavutil/aes.mbt) / [aes_ctr.mbt](libavutil/aes_ctr.mbt) | —（FFmpeg 的 `aes.asm` 用 **AES-NI**，Wasm SIMD128 无法表达） | `aes.asm` | **例外 → FFI**；纯 SIMD 只能位切片重写（成本高、收益低），列低 |
| [md5.mbt](libavutil/md5.mbt) | 多缓冲 4 路并行 | —（FFmpeg 亦为标量 C） | ✅ **已完成**：`av_md5_sum_x4`（`i32x4` 承载 4 条等长消息，逐 lane 施加轮函数）；与标量逐 lane 逐值一致 |
| [sha.mbt](libavutil/sha.mbt) | 多缓冲 4 路并行（SHA-1/256 轮函数 + 消息扩展） | —（SHA-NI 属 x86 专有） | 中；**需新增多缓冲 API**（单流无收益），FFmpeg 亦为标量 |
| [rc4.mbt](libavutil/rc4.mbt) | 4/8 路并行密钥流 | — | 低 |
| [blowfish](libavutil/blowfish.mbt) / [cast5](libavutil/cast5.mbt) / [tea](libavutil/tea.mbt) / [xtea](libavutil/xtea.mbt) | 4/8 块并行 ECB/CBC | —（FFmpeg 亦为标量 C） | 低 |
| [avstring.mbt](libavutil/avstring.mbt) | ✅ **已完成**：`av_strnstr_v128`（`v128_load_i16x8` + `i16x8_eq` + `i16x8_bitmask` 扫首字符，命中再标量核对；仅 BMP 走 SIMD） | —（FFmpeg 为标量 `strstr`） | 低；已与标量逐值一致 |
| [huffman.mbt](libavcodec/huffman.mbt) | 位串 MSB-first 解码难以向量化；表驱动多符号解码可局部并行 | —（`huffyuvdsp.asm` 属另一回事） | 低；收益有限 |
| [mathematics.mbt](libavutil/mathematics.mbt) / [rational.mbt](libavutil/rational.mbt) | —（标量整数运算） | — | 排除 |
| [aviobuf.mbt](libavformat/aviobuf.mbt) / [mov.mbt](libavformat/mov.mbt) | —（缓冲管理/box 解析、采样表展开，非数据并行） | — | 排除 |

**SIMD 进度**：已完成 5 个（`adler32`、`base64` 编码、`avstring` 的 `av_strnstr`、IDCT
8/10/12 位 + 其 `put`/`add`、MD5 多缓冲 4 路）。表达性受限的说明：

- **`crc` / `aes`**：FFmpeg 对应 asm 依赖 x86 专有指令（`PCLMULQDQ` / `AES-NI`），
  Wasm SIMD128 **无法表达** → 按边界原则属 **FFI 例外**，不进 `@v128` 清单。
- **IDCT 表示改造**（已完成）：块/像素改用字节缓冲（`FixedArray[Byte]`，128 字节 LE-int16
  / 像素面），转置用 `i8x16_shuffle` 三级 unpack，蝶形 `i32x4` 逐 lane；`put` 的钳位由
  `i8x16_narrow_i16x8_u`（8 位）或 `i16x8_min/max_s`（10/12 位）完成。
  `int32` 输入变体（MPEG-4 studio）仍走标量。
- **`md5` / `sha`**：可用 `i32x4` 做 4 路并行（不依赖专有指令），但要把状态机重写为多路。
  MD5 已完成（`av_md5_sum_x4`，4 条等长消息共享同一块调度）；SHA 的多缓冲版待做。
  注意这类内核**只对多路输入有收益**，单流仍需标量路径。

**未移植库**中同样存在向量化内核，待对应模块移植时一并按 `@v128` 重写：
`libavfilter/x86`（42 个，如 `vf_*` 滤镜内核）、`libswscale/x86`（13 个，缩放/像素转换）、
`libswresample/x86`（3 个，重采样/混音）。

### MP4 demuxer 切片的范围边界

- **已做**：从内存 `Bytes` 解析整文件（调用方读文件），因此无需 AVIO/AVPacket/AVFormatContext；
  采样表全部展开为每样本索引，DTS/CTS 与 FFmpeg 的 `pkt->dts`/`pkt->pts`/`pkt->duration` 计算一致
  （含 `max_stts_delta` 裁剪、DTS 漂移修正、负 `ctts` 触发的 `dts_shift`、edit list 的
  `time_offset`、`stsc` 校验修复、`stz2` 的 MSB-first 4/8/16/32 位打包、64 位 box 长度与 `co64`）。
- **已做（碎片化 MP4）**：`moof`/`traf`/`tfhd`/`tfdt`/`trun`/`trex`；按
  `default-base-is-moof`（或显式 `base-data-offset`/`implicit_offset`）+ `tfdt` 驱动 DTS，
  无 `tfdt` 时回退 `sidx` 最早呈现时间（按 C 原样不减 `time_offset`）再回退 `track_end`；
  按 `trun` 采样标志判定关键帧、按 `dts_shift` 计算 PTS；时间重叠的片段按
  `prev_dts >= dts` 置 `discard`（`AVINDEX_DISCARD_FRAME`）。
- **已做（片段索引）**：`sidx` v0/v1 参考表（offset/pts/size/duration，锚点与 `av_rescale_q`
  换算照 C）与 `st->duration = track_end = pts` 赋值；`mfra`/`tfra` 从文件尾 `mfro` 字探测。
- **已做（extradata）**：`avcC`/`hvcC`/`av1C`/`glbl` 等载荷原样；`esds` 描述符链 +
  AAC AudioSpecificConfig（libavcodec 的 `mpeg4audio.mbt`）回填声道/采样率/帧长/codec-id。
- **已做（seek）**：`ff_index_search_timestamp` + `mov_seek_stream`（BACKWARD/ANY、关键帧
  回退、`min_sample_duration` 步进；HEVC open-GOP 门取无 sgpd 的空集形式）。
- **已做（未压缩音频分块路径）**：`stts` 单条目 duration==1 的音频轨道走 C 的 chunk 索引
  （≤1024 样本/包或按 samples_per_frame 打包；`MAC3`/`MAC6`/`ima4`/`agsm` 与
  `sowt`/`twos`/`raw`/`in24`/`in32`/`fl32`/`fl64` 的 `av_get_bits_per_sample` 映射），
  原先置 `unsupported_audio_chunk_demux` 标志后产出空表的行为已替换为真实现（标志保留作路径指示）。
- **已做（cenc）**：`sinf`/`schm`/`tenc`（方案、pattern、每样本 IV 大小与常量 IV）、
  `senc`（流级 + 碎片内，每样本 IV + 子样本表，位置对齐追加）与 `saiz`/`saio`
  （默认/逐样本尺寸 + 绝对偏移，碎片内加 `base_data_offset`，`mov_parse_auxiliary_info`
  读辅助块后恢复读位置）；`mov_decrypt_sample` 对 `cenc`/`cbc1`/`cens`/`cbcs`
  四种方案解密单个样本（密钥由调用方提供）。
- **已做（码流参数桥）**：`codec_tags.mbt`/`codec_parameters.mbt` 为 MP4 与 MKV 提供统一的
  `CodecParameters`（H.264/HEVC 分辨率/profile/level/位深/VUI 帧率/SAR，AAC 采样率/声道/
  帧长）；MP4 的 `esds` 与 MKV 的 CodecPrivate 都走同一套 libavcodec 解析器。
- **未做**：`dvcC`/`dvvC` 整 atom extradata 变体、强制 `use_mfra` 模式（AUTO 下 mfra 仅作
  数据源）、muxer；MP4 中的 MP3 样本仍按通用 `mp4a` 路径处理（`ff_mov_read_esds` 已能给出
  "mp3" codec-id）。

### 未完成（本次切片外）

- `mathematics.c` 中：`av_rescale_delta`、`av_add_stable`、`av_bessel_i0`
- `libavutil/sha512.c`（SHA-384/512）
- libavutil 其余部分：`dict`、`opt`、`pixdesc`、`samplefmt`、`intreadwrite`、
  `des/twofish/sm4`、`lzo`、`eval`、`timecode`、`parseutils` 等
- libavformat 其余部分：`avio.c` 的 URL/协议层、其余 demuxer/muxer
  （AVI、MPEG-TS 优先）、AC-3/DTS 等其余音频流解析、字幕（`tx3g`/TTML）、
  AVIF/HEIF 图像、MP4 muxer；裸 AAC/MP3 分帧与 Matroska 已完成
- libavcodec 已**开始**：IDCT SIMPLE-C、Huffman 树/码长、位流读取、H.264/HEVC 参数集与
  NAL 拆分、AAC ADTS 与 MPEG 音频帧头，以及**三条真正出数据的解码通路**——H.264
  I 帧重建 + 环路去块滤波（CAVLC 全通、CABAC 部分向量、MP4 `avcC` 长度前缀入口）与
  **H.264 P 帧解码 + 滑窗 DPB**（MC/MV 预测/inter 宏块层，Annex B 与 avcC 两入口）、
  MP3 Layer III 定点解码（IMDCT/合成滤波/PCM）+ MPEG 音频 Layer I/II（分配/scalefactor/
  组量化）、+ **H.264 SEI**（`h264_sei.mbt`，含公共消息集）与 **AAC 频谱解码**
  （码书 1..11/scalefactor/PNS/TNS + 四窗口序列 IMDCT/KBD 窗，真实码流逐帧出 PCM）；
  其余未做（H.264 的 B 帧与 4:2:2/4:4:4 已在 2026-10-22/23 增量完成）：SP/SI 重建、HEVC 的 tiles+WPP 组合与
  dependent slice（I/P/B、10/12-bit、多 slice、WPP、tiles 与 transform skip 主链路已出数据）、
  AAC SBR/PS 与无法命名的 height+CCE 布局、**SEI 的 ITU-T T.35 与 AVFrame side-data 层**、
  VLC 表构建、LZO 与其余编解码器
- libswscale / libswresample / libavdevice / fftools：**未开始**

---

### 2026-10-21 增量：WAV/TS/HLS/DASH 容器 + PNG/MJPEG 解码 + VP9/AV1/Opus 码流头

本轮在既有 MP4/MKV/H.264/AAC/MP3 切片之上新增：

| 切片 | 文件 | 范围与验证 |
|---|---|---|
| WAV/RIFF | [wav.mbt](libavformat/wav.mbt) | RIFF/RIFX/RF64/BW64、`fmt `/`fact`/`data` 走查、WAVE_FORMAT_EXTENSIBLE、`ff_pcm_default_packet_size` 的 10 fps 包长与块对齐；真实 ffmpeg s16/f32/s24 文件的 pos/size/pts/duration 逐值对齐 ffprobe，另含 RIFX/RF64/奇数块填充/缺失块负例 |
| MPEG-TS | [mpegts.mbt](libavformat/mpegts.mbt) | 188/192(M2TS)/204 布局、PAT/PMT（PSI 分片累积）、PES 头部/PTS/DTS、无长度视频 PES 在下一 PUSI/EOF 冲刷、已知长度 PES 即时输出、PCR、ISO/HDMV/registration 流类型表；真实 H.264+AAC 与 B 帧 TS 的 5 个/3 个 PES 的 pos/pts/dts/size 与 `ffprobe -fflags +noparse` 逐值一致；真实 x265 HEVC+AAC TS 的两个流映射（0x24/0x0F）与 5 个 PES 交错的 pos/pts/dts/size 逐值一致；合成 HEVC+Opus 注册描述符与 M2TS 用例 |
| HLS | [hls.mbt](libavformat/hls.mbt)、[hls_crypto.mbt](libavformat/hls_crypto.mbt) | 主/媒体 M3U8（VERSION/TARGETDURATION/MEDIA-SEQUENCE/ENDLIST/PLAYLIST-TYPE）、EXTINF、BYTERANGE 续接、KEY/MAP/DISCONTINUITY/GAP/PROGRAM-DATE-TIME、STREAM-INF/I-FRAME-STREAM-INF/EXT-X-MEDIA、`$...$` 之外的 URI 相对解析；真实 ffmpeg TS/fMP4 播放列表与手工边界用例 |
| DASH | [xml.mbt](libavformat/xml.mbt)、[dash.mbt](libavformat/dash.mbt) | 最小 XML（元素/属性/CDATA/注释/实体/命名空间后缀匹配）+ MPD Period/AdaptationSet/Representation 继承、BaseURL 链、SegmentTemplate `$Number%05d$`/`$Time$`/`$RepresentationID$`/`$Bandwidth$`、SegmentTimeline（含 `r="-1"`）、duration 枚举、SegmentList/SegmentBase；真实 ffmpeg MPD 的 3+3 个分片编号/时间/时长/URL 逐值断言 |
| Matroska Cues | [matroska_cues.mbt](libavformat/matroska_cues.mbt) | CuePoint/CueTime/CueTrackPositions/CueClusterPosition/RelativePosition/BlockNumber 解析与按时间二分 seek；真实 x264 MKV（关键帧 0/1/2s）3 条 cue 与 ffprobe 关键帧位置对应 |
| MP4/WebM 参数桥 | [codec_parameters.mbt](libavformat/codec_parameters.mbt)、[extradata.mbt](libavformat/extradata.mbt) | VP9 `vpcC`、AV1 `av1C`、Opus`OpusHead` 解析；MP4 `dOps` → OpusHead、`av1C`/`vpcC` 路径；真实 VP9/AV1/Opus WebM 与 MP4 AV1/VP9/Opus 的 codec/尺寸/位深/声道/pre-skip 对齐 ffprobe |
| PNG | [inflate.mbt](libavcodec/inflate.mbt)、[png.mbt](libavcodec/png.mbt)、[png_apng.mbt](libavcodec/png_apng.mbt) | 纯 MoonBit zlib/DEFLATE（stored/fixed/dynamic Huffman、LZ77、Adler32）+ PNG IHDR/PLTE/tRNS/IDAT、五种行滤波、Adam7 隔行、1/2/4/8/16-bit 与调色板/灰度/RGB/RGBA/GA、tRNS 提升；8 种原生 pix_fmt 的原始输出 MD5 与 `ffmpeg -f rawvideo` 完全一致（含 pal8 的 256 项 BGRA 调色板）。另含分块 CRC-32 校验（`av_png_decode_ex(data, true)`，对齐 `-err_detect crccheck+explode`）与 APNG：`acTL`/`fcTL`/`fdAT`、默认图排除、帧矩形、delay/loop、NONE/BACKGROUND/PREVIOUS 处置与 SOURCE/OVER 混合，输出规范化 RGBA；FFmpeg 生成的 3 帧 APNG 逐帧 MD5 一致 |
| MJPEG | [mjpeg.mbt](libavcodec/mjpeg.mbt)、[mjpeg_scan.mbt](libavcodec/mjpeg_scan.mbt) | 基线 SOF0/SOF1 与渐进式 SOF2、DQT/DHT/DRI/SOS、重启标记、DC 预测（`4<<8` 偏置）、AC 游程、多扫描累积系数、渐进 DC first/refine 与 AC first/refine（含 EOBRUN/ZRL/last_nnz）、Simple-C IDCT、4:2:0/4:2:2/4:4:4/灰度采样与原生 yuvj 输出；真实 ffmpeg 顺序与渐进 JPEG 三平面 MD5 逐字节一致 |
| HEVC slice header | [hevc_slice.mbt](libavcodec/hevc_slice.mbt) | 首片/独立非首片的 slice segment header：IRAP no_output、PPS/SPS 查找、slice address、slice_type、POC LSB、short/long-term RPS、temporal-MVP、SAO、P/B 参考计数与权重表、collocated/merge、I/P/B 的 QP/chroma-QP/去块字段；真实 x265 IDR 与 B 帧 GOP 的字段和 `trace_headers` 逐值一致，依赖片段与列表重排返回 PATCHWELCOME |
| H.264 去块修复 | [h264_deblock.mbt](libavcodec/h264_deblock.mbt) | bS 派生修正：内部边非零系数=2、MB 边=3，MV/ref 差异只产生 bS=1（|dx|/|dy|≥4）；真实 CABAC P_8x8 refs=2 流在默认与 `-skip_loop_filter all` 两种 framemd5 下均与 ffmpeg 逐字节一致 |
| HEVC CABAC | [hevc_cabac.mbt](libavcodec/hevc_cabac.mbt) | 3×199 init_values 与 cabac_init_state 公式；真实 x265 IDR slice 的上下文状态、64 个 ctx=7 bin 与 16 个 bypass 位和自编译 FFmpeg C 逐值一致 |
| HEVC intra pred | [hevc_intra.mbt](libavcodec/hevc_intra.mbt) | planar/DC（含 corner 平滑）/33 个角度模式，4/8/16/32 全部模式；用自编译 FFmpeg C pred.c 生成参考，和值+首末样本逐值一致（参考样本平滑/可用性由调用层后续接入） |
| HEVC transform | [hevc_transform.mbt](libavcodec/hevc_transform.mbt) | 8/10/12-bit IDCT 4/8/16/32（含 col_limit 逐列收缩）、DC-only IDCT、4x4 DST、反量化（`15 - bit_depth - log2_size`，12-bit 大块负移位按 C 的 uint16 回绕）与 residual 叠加；4x4/8x8 全量系数、16/32 和值与抽样点均与 FFmpeg C DSP 逐值一致 |
| HEVC SEI | [hevc_sei.mbt](libavcodec/hevc_sei.mbt) | 前缀/后缀 SEI、0xFF type/size 续接；mastering display、content light、user_data_unregistered、recovery point、alternative transfer、ambient viewing；x265 真实 SEI 的 CLL/色域/亮度与 trace_headers 逐值一致 |
| HEVC access unit | [hevc_access_unit.mbt](libavcodec/hevc_access_unit.mbt) | 按 `first_slice_segment_in_pic_flag` 组装访问单元、IDR/IRAP 关键帧标记、按 temporal-id-0 更新 POC；真实 x265 B 帧 GOP（I/P/B/B）的 POC 0,3,2,1 与解码顺序逐值一致 |
| 码流头 | [opus_packet.mbt](libavcodec/opus_packet.mbt)、[av1.mbt](libavcodec/av1.mbt)、[vp9.mbt](libavcodec/vp9.mbt) | Opus TOC/四种包码/Xiph lacing/120 ms 限制；AV1 OBU LEB128 + `av1C`/序列头（含 order-hint/screen-content/superres 状态）与关键/帧内/inter frame header（类型/show/尺寸/render/superres/intrabc、primary_ref/refresh、7 个参考槽、统一 tile 布局与 base_q_idx/量化 delta；关键帧与 inter 字段均对齐 `trace_headers`）；VP9 帧头（profile、关键帧/帧内、色彩配置、尺寸、刷新掩码）与 superframe index 拆分；真实 WebM 包与 ffprobe 对齐 |
| VP9 压缩头/分片 | [vp9_rac.mbt](libavcodec/vp9_rac.mbt)、[vp9.mbt](libavcodec/vp9.mbt) | FFmpeg `VPXRangeCoder` 的 MoonBit 移植（norm_shift/renorm/概率与等概率比特/`update_prob`）；压缩头 marker、tx_mode 0–4（lossless 强制 0，switchable 的 tx8/16/32 概率更新）、skip 概率、系数/模式/MV 更新语法的精确消费；tile 布局 tile_cols/rows log2、多行多列、除末片外的 4 字节大端长度、末片剩余字节。libvpx-vp9 真实向量的 tx_mode 由 FFmpeg 解码器 `vp9.c` 插桩逐帧核对（0/1/2/3/4 均覆盖）；512×256 双列与 512×512 双行双列分片的 offset/size/首 marker 均逐值断言 |
| HEVC 整帧解码（I 片） | [hevc_intra_decode.mbt](libavcodec/hevc_intra_decode.mbt)、[hevc_cabac_syntax.mbt](libavcodec/hevc_cabac_syntax.mbt)、[hevc_residual.mbt](libavcodec/hevc_residual.mbt)、[hevc_intra_ref.mbt](libavcodec/hevc_intra_ref.mbt)、[hevc_scan.mbt](libavcodec/hevc_scan.mbt)、[hevc_picture.mbt](libavcodec/hevc_picture.mbt)、[hevc_deblock.mbt](libavcodec/hevc_deblock.mbt)、[hevc_sao.mbt](libavcodec/hevc_sao.mbt) | 打通 I 片主链路与完整环路滤波：CTU/CU 四叉树、split/part_mode、帧内 MPM（含 CTB 边界规则）、变换树与 cbf、cu_qp_delta/qP 预测、残差语法（sign hiding）、反量化、IDCT/DST、参考样本可用性/z-scan/3-tap 与 strong 平滑、4:2:0 重建、TU 边界 bS=2 的去块（亮度 weak/strong、色度）、SAO（merge、band/edge offset、图片边界恢复）。真实 x265 流 32×32、64×48 四 CTU（默认与未滤波）、64×48 SAO+去块（默认解码）、64×64 强平滑、80×64 部分 CTU 的 yuv420p MD5 均与 ffmpeg 逐字节一致；长度前缀 NAL 输入一致；另有 8×8 真实块 IDCT 对 C DSP 的逐值回归。范围：8/10/12-bit 单色/4:2:0/4:2:2/4:4:4 单 I 片（Main10 IDR 已逐字节验证），支持 SPS/PPS scaling list（默认矩阵与显式数据），无 PCM/transform skip/tiles/WPP；`ff_hevc_decode_intra_frame_unfiltered` 返回滤波前像素 |
| HEVC P/B 帧间预测 | [hevc_refs.mbt](libavcodec/hevc_refs.mbt)、[hevc_mc.mbt](libavcodec/hevc_mc.mbt)、[hevc_inter.mbt](libavcodec/hevc_inter.mbt)、[hevc_inter_decode.mbt](libavcodec/hevc_inter_decode.mbt) | 在 I 片链路上补齐 P/B：MvField/DPB 槽位池、RPS 候选列表与 `ff_hevc_slice_rpl` L0/L1 拼接、`mv_scale`/`check_mvset` 时域 MV 缩放；运动补偿全套（luma 8-tap qpel / chroma 4-tap epel 的 uni/bi 与加权变体、`ff_emulated_edge_mc` 语义的边界复制）；inter 语法（skip/merge/merge_idx/inter_pred_idc/ref_idx/mvd_coding/mvp_lx/part_mode 全模式含 AMP）；merge 候选推导（空间 A1/B1/B0/A0/B2 + 时域 + 组合 BI + 零 MV 填充）、AMVP（`ff_hevc_luma_mv_mvp_mode` 含 `mp_mx`/`mp_mx_lt`/`dist_scale`）；prediction_unit 的 MC 编排（PF_L0/L1/BI 分派 + 加权分支）；inter 去块 bS 推导（`ff_hevc_deblocking_boundary_strengths` + `boundary_strength`）。三条真实 x265 流（gray I+6P、moving I+6P、I/P/B bframes=2）共 21 帧逐帧 YUV MD5 与 FFmpeg 9.0.2 默认解码逐字节一致；slice header 补 POC 全推导/short-term RPS/long-term RPS/collocated/完整 pred_weight_table/alignment_bit_equal_to_one。范围：8/10/12-bit 4:2:0，支持单/多 slice、WPP（单线程语义：entry point 解析、行边界 terminate+reinit、第二 CTB 后状态同步、跨 slice bS/SAO 边界恢复）与 tiles（PPS 尺寸派生、CTB raster↔tile-scan、tile 边界 CABAC 重置、tile 感知邻居/预测可用性、跨 tile bS/SAO 边界；kvazaar 2×2 uniform tiles 已验证，tiles+WPP 暂拒绝）；closed GOP、无 dependent slice/reference-list modification/PCM/scaling list/RDPCM（均有具体错误串的负例）。transform skip 支持 transform-skip significance 上下文（x265 `tskip=1` 真实流中 4×4 亮度/色度 skip 块逐字节一致）。Main10 加权 I/P/B、Main12 32×32 TU、320×240 四 slice、单 slice WPP、2×2 tiles 与 transform skip 流共 27 帧，滤波后与 `-skip_loop_filter all` 未滤波两条路径均与 FFmpeg 9.0.2 逐字节一致；去块 `beta`/`tc` 按 `bit_depth - 8` 缩放并按 CTB 取 slice 偏移。4:4:4（hshift = vshift = 0）复用同一驱动：全尺寸色度平面、逐 PU 色度模式、4×4 色度 TU、色度 QP 截断 51、4:4:4 色度 MC/去块/SAO，8-bit I/P、8-bit 加权 B、10-bit 加权 B 三流共 17 帧同样逐字节一致。4:2:2（hshift = 1, vshift = 0）：半宽全高色度平面、每亮度 TU 两个纵向堆叠色度块、`tab_mode_idx` 重映射、色度 QP 截断 51 与通用 `mv >> (2+shift)` 色度 MC，8-bit I/P、8-bit 加权 B、10-bit 加权 B 三流共 17 帧同样逐字节一致。单色（chroma_format_idc=0）仅分配亮度平面，跳过色度模式/cbf/MC/去块/SAO，8/10-bit I/P/B 两流共 14 帧同样逐字节一致。scaling list 在反量化时按 `sl[log2-2][3*inter+c_idx]` 取系数矩阵、`sl_dc` 取 DC 缩放（transform skip 且 log2>2 时禁用），kvazaar 默认矩阵流（I/P/B+WPP）3 帧逐字节一致 |

测试总数 627/627（`moon test --target wasm`）。仍未完成的重点：HEVC 的 dependent slice、
tiles+WPP 组合、PCM 与 RDPCM，VP9 帧间解码与 AV1 像素解码、
Opus SILK/混合模式、CELT 包级 PCM 编排与 pre-skip/gain、HLS/DASH 网络 adapter/live reload、MJPEG 无损/12-bit/算术编码。

### 2026-10-22 增量：VP9 profile-0 关键帧像素解码

入口为 `vp9_decoder_new()` → `decode_frame(payload)` → `frame_bytes()`，输入单帧 VP9
payload，输出紧密排列的 8-bit Y/Cb/Cr。支持 4:2:0 关键帧、单/多 tile、压缩头真实概率
更新、分区/帧内模式/系数上下文、4/8/16/32 DCT 与 ADST、lossless 4×4 WHT、去块滤波。
分段量化与滤波特征已接入；真实向量未单独覆盖所有 segmentation 组合。

| 验证流 | 尺寸 | 关键帧数 | 参考来源 |
|---|---|---|---|
| basic | 160×144 | 3 | docker `linuxserver/ffmpeg:9.0.2` 解码 YUV MD5 |
| small | 70×36 | 3 | 同上，非超块对齐边界 |
| qfull | 64×48 | 2 | 同上，最低量化器与密集小 TU |
| lossless | 64×48 | 2 | 同上，无损 WHT |
| tiles | 512×128 | 2 | 同上，2×2 tiles |

五流共 **12 帧逐帧 MD5 完全一致**；qfull 第 0 帧 1411 条、small 第 0 帧 1287 条
系数 STEP 与插桩 C 参考逐项一致。生成与嵌入方法保留在
`scripts/_gen_vp9_kf_vectors.py`、`scripts/_emit_vp9_kf_realdata.py`；33 张常量表由
`scripts/extract_vp9_tables.py --check` 与原 C 源核对。

本切片拒绝 inter/intra-only 与非 profile-0 输入；后续已接入隐藏帧与 show-existing 显示。不实现帧间预测、
自适应概率统计回写或其他 profile。负例断言具体错误串。调试插桩、临时调试测试与
轨迹补丁脚本已清理。

### 2026-10-22 继续迁移：解码正确性、AV1/Opus 基础与分片读取

本轮沿用纯 MoonBit 算法实现，自动化新增为 `.mbtx`。C 文件仅作为独立黄金向量生成器，
不进入运行时 FFI。新增实现及验证：

- **H.264**：修复 P_SKIP 左/上邻不可用时遗漏零运动矢量捷径的问题。
  HLS TS、HLS fMP4、DASH fMP4 三条真实链路共 12 帧全部通过 YUV MD5；
  另补齐上文五条 CABAC 流的 13 帧双路径回归，撤销旧文档中过时的多行失步结论。
- **AV1**：修复交错 tile size/payload 被读成连续长度表的问题，补全每 tile 的
  offset/size，新增 `av1_extract_tiles` 检查 group 连续覆盖、越界、对齐及 LEB128 溢出。
  真实 256×128、2×2 tile libaom 流的四个 payload 偏移/长度/MD5 与 C 参考一致。
  **这仍是 tile 输入准备，尚未实现 AV1 像素重建。**
- **VP9**：新增 `decode_packet`，支持 superframe、隐藏关键帧与八槽 show-existing；
  保留每个输出自身的尺寸和像素快照。FFmpeg 对隐藏关键帧后八次显示的 MD5 均为
  `7476553695d6397f4ab595fa8fb2b68c`，与本实现一致。inter/intra-only 仍拒绝。
- **Opus**：新增 `opus_rc.mbt`，移植 SILK/CELT 共用的 CDF/log/uniform/step/triangular/
  Laplace 熵解码、倒序 raw bits 与 tell/tell_frac；32×14 = **448 条符号和完整状态**
  与原 `opus/rc.c` 一致。包层新增 self-delimiting、frame offsets 与 consumed size，
  拒绝超长帧和 padding 导致的负长度。**尚未产出 Opus PCM。**
- **HLS/DASH**：新增 [segment_io.mbt](libavformat/segment_io.mbt)，通过 `MediaFetch`
  回调获取初始化、媒体和密钥；支持字节范围、初始化缓存、失败重试。HLS 还支持
  AES-128/显式或序号 IV、密钥轮换、GAP、discontinuity；初始化段保留声明时的密钥。
  DASH SegmentList 补齐继承、timescale/startNumber、时间递增和媒体/初始化范围。
  真实 TS/fMP4 分片经现有 demux → H.264 解码，三流各 4 帧 MD5 全部一致。

| 请求范围 | 当前可用切片 | 主要剩余项 |
|---|---|---|
| H.264 | progressive + MBAFF（含混合 field/frame 宏块对）8–14 bit 4:0:0/4:2:0/4:2:2/4:4:4 I/P/B；CAVLC/CABAC 4×4/8×8、I_PCM、多 slice/受限帧内预测、POC 0/1/2、MMCO/短长参考、空间/时间 direct、显式/隐式双向加权、显示重排、High 4:4:4 lossless transform bypass；PAFF 与 frame_num 间隙参考合成；渐进帧与 MBAFF 帧的错误补偿（多轮 MV 猜测 + 恢复区滤波） | SP/SI 重建、FMO、数据分区；场图片按上游边界报错；P 帧大缺口/高位深恢复未逐像素对齐 |
| HEVC | I/P/B，8/10/12-bit，多色度/单色，WPP/tiles/多 slice | dependent slice、tiles+WPP、PCM/RDPCM 等 |
| AV1 | OBU/序列与帧头、tile payload 提取；8-bit 4:2:0/4:2:2/4:4:4/单色 多 tile 无损关键帧像素输出 | 有损帧的环路滤波、帧间帧、10/12-bit 等 |
| VP9 | profile 0/1/2/3 关键帧与帧间帧，8/10/12-bit，4:2:0/4:2:2/4:4:0/4:4:4；隐藏/show-existing、superframe、多 tile、概率自适应 | 复合预测（B 帧）、参考缩放 |
| MJPEG | 8-bit baseline/progressive，重启标记与多采样 | 无损、12-bit、算术编码 |
| PNG | 灰度/RGB/RGBA/调色板、1–16-bit、Adam7、APNG | 更多扩展元数据/色彩管理 |
| AAC | LC，多声道/PCE、CCE、960/1024、短窗/TNS/PNS/pulse | SBR/PS、USAC 与更多真实 CCE/960 向量 |
| MP3 | Layer I/II/III 定点 PCM | 更多损坏流恢复/边界向量 |
| Opus | OpusHead 四类映射、多流包拆分、共享熵解码、CELT 连续帧完整频域及 48 kHz 单/双声道 Float PCM（IMDCT/加窗/后滤波/去加重） | SILK/混合模式、包级 PCM 编排/多流声道重映射、pre-skip/gain、声道数切换/下混、PLC |
| MP4 | 普通/碎片化、索引/seek、avcC/hvcC/av1C/vpcC、cenc | 公共统一播放入口及更多扩展 |
| MKV / WebM | EBML、轨道、Cluster/Block/lacing、Cues | 更多压缩/侧数据与流式特化 |
| TS | 188/192/204、PAT/PMT、PES、PTS/DTS/PCR | CRC、加扰、33-bit 时间回绕等 |
| HLS | 播放列表 + 回调读取 + TS/fMP4 + AES-128 | 实际 HTTP adapter、live reload/LL-HLS、SAMPLE-AES |
| DASH | MPD/template/timeline/list + 回调范围读取 + fMP4 | SegmentBase index 播放、动态刷新/时钟、DRM |
| WAV | RIFF/RIFX/RF64/BW64、PCM/扩展格式与分块 | 更多 WAV 编码器及播放桥接 |

新增黄金向量与复现方式见 `scripts/emit_*_vectors.mbtx`、
`scripts/emit_h264_cabac_regression.mbtx`、`scripts/opus_rc_reference.c`、
`scripts/av1_tiles_reference.c` 及对应 `*_realdata_wbtest.mbt` 文件头。

### 2026-10-22 继续迁移：Opus CELT PVQ 与多声道容器链路

- [opus_pvq.mbt](libavcodec/opus_pvq.mbt) 移植 `opus/pvq.c` 的 `celt_cwrsi`、
  `celt_decode_pulses`、`celt_alg_unquant`：有符号脉冲向量、平方范数、频带归一化、
  四种 spread 与 collapse mask。支持原 C 查表域、2..176 维，拒绝越界参数/索引；
  码本大小用 UInt64 求和，超过 UInt32 时拒绝，不能用有符号 Int 存储大码本。
  零脉冲向量不消费熵状态；频带归一化要求正脉冲数。
- 1,272 项 U 表由 `scripts/extract_opus_pvq_tables.mbtx` 从 `opus/tab.c` 提取，
  `--check` 可逐值核对。`scripts/opus_pvq_reference.c` 直接调用原 C 核，验证
  **2,000 个码本大小、14,181 个整数向量**；向量整体 MD5 为
  `b6bac12c1663ec747dbd52166094338e`（LE N/K/index/norm64/pulses32）。
  同一 forward/raw coder 连续解码的 **416 组频带**覆盖四种 spread、长短块和高脉冲数：
  collapse mask、range/value/total_bits/tell_frac 完全一致，Float32 系数绝对误差 ≤1e-6。
- [opus_head.mbt](libavcodec/opus_head.mbt) 移植 OpusHead 版本、pre-skip、
  有符号 Q8 增益、原输入采样率和 0/1/2/255 四种映射族；包括 Vorbis→原 C 输出顺序、
  coupled/mono 流定位、重复通道复制与静音通道。`OpusHead::parse_packet` 拆分整个多流包，
  仅末流使用普通 framing，检查各流总样本数一致，允许帧数不同、padding 与零字节 DTX。
  使用 BytesView 解析，避免对每条流复制剩余容器包。
- 真实 libopus **5.1 WebM 与无损重封装 MP4**：各 4 包、每包 640 字节，4 条逻辑流、
  2 条 coupled stream、pre-skip=312；OpusHead/dOps、6 个输出通道映射及全部 16 条逻辑包
  的 offset/size/config/stereo/frame spans 与原 `opus/parse.c` 一致。MP4 与 WebM 的
  原始 packet bytes 逐字节一致。畸形 OpusHead 不再覆盖容器中已有的采样率/声道元数据。

此 PVQ 切片本身不产出 Opus PCM；后续章节继续完成 CELT 频域编排。
C harness 仅生成黄金参考，运行时保持纯 MoonBit。
上述数值回归在默认 Wasm 目标验收；native 补测在运行测试前因本机 MSVC/Windows SDK
工具链不可用而未能构建，不能据此宣称 native 已通过。

### 2026-10-22 继续迁移：CELT 比特分配与 PVQ 前置能量状态

- [opus_celt_alloc.mbt](libavcodec/opus_celt_alloc.mbt) 移植 `ff_celt_bitalloc` 解码路径：
  spread、动态 boost、trim、anti-collapse 预留、两层二分分配、跳带、intensity/dual stereo
  标志与逐带 fine-energy/PVQ 预算。`pvq_bits`/`remaining` 的单位为 1/8 bit，不能直接当作
  脉冲数。模型/频带/静态分配/caps/TF 表从 `opus/tab.c` 提取（后续已扩为 17 张），可用
  `moon run scripts/extract_opus_celt_tables.mbtx --check` 核对。
- [opus_celt_energy.mbt](libavcodec/opus_celt_energy.mbt) 新增 `decode_celt_setup`：
  解析静音、postfilter 参数与 transient，做粗能量的时域/频域预测、TF 切换与 fine-energy
  raw bits 解码，输出 PVQ 语法起点的完整状态。支持 120/240/480/960 样本、单/双声道、
  指定频带和 42 项调用方能量历史；mono 按原 C 合并两行历史。参数错误不消费熵，
  调用方历史不原地修改；中途畸形帧错误可能消费该帧熵状态，应丢弃该 range decoder。
- `scripts/opus_celt_reference.c` 直接调用原 C 的 `celt.c`/`dec_celt.c`：
  **896 组比特分配 + 896 组 setup + 8 组标准静音 setup** 全字段一致。
  覆盖两种声道数、四种时长、七种频带区间、1..1275 字节、预消费熵和零/非零能量历史；
  包含 transient、postfilter、正负 TF、dual stereo 与跳带。逐带 caps/PVQ/fine/priority、
  range/value/total_bits/tell_frac 及 Float32 能量/postfilter 增益的位模式均精确一致。
- 上轮真实 5.1 WebM/MP4 所含 **16 个 CELT 逻辑帧**，分别以 flush 能量历史进入 setup，
  至 PVQ 前的上述状态同样与 C 精确一致；这验证真实字节输入的前置阶段，尚未完成连续帧
  音频解码。标准静音的 setup 输出也是中间能量，最终置为 -28/清频谱位于完整帧后段。

此 setup 切片的后续频域处理见下节；当前仍未输出 Opus PCM。

### 2026-10-22 继续迁移：CELT 递归频带与连续频域帧

- [opus_celt_bands.mbt](libavcodec/opus_celt_bands.mbt) 移植 `quant_band_template` 的
  解码路径：theta 的 step/uniform/triangular 模型、N=2 特殊立体声、mid/side 预算重分配、
  递归分带、PVQ、零脉冲噪声/折叠、相位反转与 TF/Hadamard/Haar 重排。
  [opus_celt_spectrum.mbt](libavcodec/opus_celt_spectrum.mbt) 串起整帧频带预算平衡、
  跨带 folding 和 dual→intensity stereo 切换，返回归一化频谱、collapse masks 与噪声种子。
- [opus_celt_coefficients.mbt](libavcodec/opus_celt_coefficients.mbt) 新增
  `OpusCeltHistory` 与 `decode_celt_coefficients`：最终 fine-energy、anti-collapse、
  能量反归一化、静音清谱、mono 能量复制以及两代历史提交。每条逻辑流独立持有历史，
  输入历史不原地修改，失败时不会提交半帧状态。帧尾 seed 取当前 `rc.range`，不是最终 LCG 值。
- 新增真实 libopus 120/240/480/960 样本的 mono/stereo 低延迟流，连同已有 5.1 流，
  **九条流共 84 个逻辑帧 × 相位反转关/开**，得到 **168 组归一化与 168 组连续频域参考**。
  原 `celt.c`/`pvq.c`/`dec_celt.c` 直接生成期望值；熵状态、种子、collapse masks、
  最终能量与两代历史的 Float32 位模式精确一致。归一化系数绝对误差 ≤1e-6；
  缩放后系数误差 ≤`1e-5 + abs(reference)*3e-6`。覆盖 transient、anti-collapse 与后滤波参数。
  另有 **8 组标准静音**的完整频域/历史与 C 逐项精确对拍。
- 真流暴露并修复旧 PVQ 核仅允许 8 个块的限制：TF subdivision 可产生 **16 个内部块**。
  安全检查按实际 TF 表限制调用方可变数组，防止伪造 transient TF 令 Hadamard stride 越界。
  静音填满帧预算后，C 的 1/8-bit safety margin 会合法地产生 -1 的带预算，不能一概拒绝。
  17 张整数表支持十六进制项与 `--check`，C 向量生成方法见
  `scripts/opus_celt_spectrum_reference.c` 与 `scripts/emit_opus_celt_spectrum_vectors.mbtx`。

此频域切片的 PCM 合成见下节；验证目标仍为默认 Wasm。

### 2026-10-22 继续迁移：CELT 连续帧 PCM 合成

- [opus_celt_synthesis.mbt](libavcodec/opus_celt_synthesis.mbt) 移植 FFmpeg
  半逆 MDCT、120 样本 overlap/add、两阶段 pitch postfilter 过渡与稳态滤波，
  最后应用 0.8500061035 去加重并清除非 normal 的末尾状态。逆变换采用 Double
  累加的 O(N²) 直接计算，缩放为 -1/32768，输出回到 Float；尚未做 FFT 优化。
- [opus_celt_decode.mbt](libavcodec/opus_celt_decode.mbt) 提供
  `OpusCeltDecoder::new`、`decode_frame` 与 `reset`。每条逻辑流独立维护频域、
  重叠区、前后两代后滤波参数和去加重历史；错误帧不会提交这些状态。
  输入为去掉 TOC 的 CELT 帧，支持 120/240/480/960 样本和 end band 13/17/19/21，
  输出为 48 kHz、channel-major 的单/双声道 Float PCM。
- [opus_celt_pcm_reference.c](scripts/opus_celt_pcm_reference.c) 直接调用原始
  `ff_celt_init` / `ff_celt_decode_frame`，用原始 AVTX 与 scalar DSP 生成参考。
  九条已有真实流的 **168 组连续逻辑帧**，以及音频后接静音并切换帧长的
  **144 组尾音帧**，逐样本绝对误差均 ≤`3e-6`。覆盖 transient 短块、相位反转开关、
  后滤波延续和残余重叠；复位、非法参数与错误回滚另有回归。
  此处验证每条 5.1 逻辑流的 PCM，尚未实现组合后的 5.1 PCM 输出与声道重映射。
- `moon run scripts/extract_opus_celt_windows.mbtx --check` 校验两张原 C Float 窗口；
  `scripts/emit_opus_celt_pcm_vectors.mbtx` 从 `.scratch/NAME_pcm_reference.txt`
  内嵌上述参考。C harness 沿用频域参考的编译方法，额外链接 `opus/dsp.c`，保持
  `-ffp-contract=off`；运行输入为同名 `NAME_packets.bin`。

本切片固定每个 decoder 的编码/输出声道数，不处理 TOC、多帧包、混合模式、PLC、
容器 pre-skip、输出 gain 或声道切换/下混。下一步接包级/多流 PCM 编排与容器裁剪，
随后继续 SILK/混合模式。

### 2026-10-22 H.264 完整迁移推进（尚未全部完成）

- CAVLC 8×8：四路交错 scan、8×8 反量化和帧内预测；保留原始模式供邻块预测，
  边缘修正模式只送重建。修正 `P_8x8ref0` 不应读取参考索引、8×8 变换允许条件，
  以及 CAVLC 熵计数与去块 8×8 存在标志的差异。
- CABAC I_PCM：退回算术 lookahead，读原始样本后重新初始化 range/low，保留概率状态。
  原 `tests/cabac.c` 编码器生成连续两次 PCM escape，原 FFmpeg 解码取 MD5；
  [h264_pcm_reference.c](scripts/h264_pcm_reference.c) 仅用于独立测试素材，无运行时 FFI。
- 多 slice：共享画面平面，按 slice 重置熵/QP/MV-difference 上下文；帧内与运动预测
  不跨 slice，`constrained_intra_pred` 只约束像素/模式邻居。去块保存各 MB 的 slice、
  参数与参考图 POC 身份；idc=2 保留片内滤波并跳过跨片边。两入口均拒绝缺失/重叠
  slice，完整画面才提交 DPB/POC；AVC 样本失败后可重试。
- POC 0/1/2、frame_num 回绕、POC1 周期与 delta、MMCO 1–6、IDR/普通长参考、
  短/长 L0 重排和滑窗淘汰。修正 MMCO6 仅有一个操作数，以及 POC 字段条件应取 PPS。
  默认 P L0 的短参考按 FrameNumWrap 排序；MMCO5 后的 POC 历史考虑已回绕的 MSB。
- B 前置 header：L0/L1 活跃计数、两套修改命令、direct spatial 标志与双列表显式权重；
  同时保留 SP/SI 的 QS 字段。真实七帧 B-GOP 与显式加权规范流的 header 值/位位置
  由原 FFmpeg `trace_headers` 核验；此处不表示已完成 B/SP/SI 像素重建。

新增十组像素向量 **92 帧 × 滤波/未滤波两套 MD5** 全部对齐 FFmpeg 9.0.2：
真实 CAVLC High 8×8、多参考与加权 P、CABAC 高熵、规则三片/行中间起片及受限帧内预测，
再加 MMCO/POC 三类规范流和连续 I_PCM escape。生成方法在
`scripts/generate_h264_marking_vectors.mbtx`、`scripts/emit_h264_completion_vectors.mbtx`
及向量文件头。全量 Wasm **627/627**；`moon check` 零错误/零警告。

2026-10-23 增量完成后，原清单 1、2 两项中的 B 帧全套、高位深（8–14 bit）与
4:2:2/4:4:4 均已完成（见下节）；仍开放：

1. SP/SI 重建；separate colour plane 与 FMO 继续按原 C 的拒绝边界处理。
2. 单色、transform bypass、MBAFF、PAFF 场图与帧场交叉引用已完成实流验证；
   PAFF 位深/色度、POC 与场顺序的验收矩阵见下节。
3. frame_num gap、SEI T.35 与帧 side-data/边界语义均已完成（见后续小节）；数据分区/冗余 slice、错误恢复仍待补齐。
   原 C 本身拒绝的 FMO 等功能继续保留对应拒绝边界。

### 2026-10-23 H.264 frame_num 间隙参考合成

`h264_gap.mbt` 对照 `h264_slice.c:h264_field_start` 与 `h264_refs.c`：在构建参考表前
补齐缺失 frame_num；大间隙先截短，每次最多合成 SPS 参考容量数量的图片，支持编号回绕。
合成图只参与 DPB/滑窗，不作为输出帧；仅推进 prev_frame_num，不虚构逐帧 POC 历史。
兼容前短参考时复制像素并继承其 POC+2/gray；没有可用前参考时填位深中点灰图。
保留合成图独立的编号/运动身份，同时在去块时按原 C 共享像素缓冲的身份判断参考相等。
禁止间隙的流沿用默认 FFmpeg 容错语义，并按实际输入 frame_num 淘汰过期 invalid_gap。
非灰参考建立后按 noref_gray 替换灰参考；PAFF 待配对场抑制合成，IDR/MMCO5 保持复位语义。
解码失败会回滚本次新增的 DPB 参考和编号/POC 历史，完整图片重试可取得相同结果。

验证：**19 条流、86 帧，默认滤波与跳过滤波两套逐帧 MD5 均对齐 Docker FFmpeg 9.0.2**；
包含独立生成的 CAVLC I_PCM/P_SKIP 流，以及移除完整图片后的真实 CAVLC/CABAC、
多 slice、MBAFF 和 JM PAFF 流。覆盖 POC0/1/2、大间隙/回绕、允许/禁止间隙、
8/10/12-bit 灰图、4:4:4、参考重排、非参考图、IDR/MMCO5；另有 7 项状态回归。
这不代表任意损坏流的错误恢复已完成；无兼容参考时使用确定的中性像素，不仿真原 C
在已恢复但缓冲不兼容等错误路径中的未初始化分配器内容。

复现：准备 `.scratch/h264_baseline.264`、`h264_slices1.264`、
`h264_mbaff_p_cabac.264` 和 `.scratch/JM/bin/jm_paff_p_long.264` 的原始生成流，运行
`moon run scripts/gen_h264_gap_vectors.mbtx`，再运行
`moon run scripts/run_h264_gap_reference.mbtx <.scratch/h264_gaps绝对路径>` 与
`moon run scripts/emit_h264_gap_vectors.mbtx`。参考生成器只挂载样本目录并禁用网络；
显式指定 H.264 输入格式，`-flags2 +showall` 用于观察故意从非 IDR 开始的灰图样本。
提交的回归内嵌码流和固定黄金 MD5，运行测试无需 Docker 或原始生成文件。

全量 Wasm **758/758**；`moon check --target wasm --deny-warn --no-render` 与
`moon info --target wasm` 通过。H.264 剩余重点仍有 SP/SI、数据分区/冗余 slice、错误恢复（SEI 输出元数据已在后续切片完成）；
FMO 与 separate colour plane 保持原 C 的拒绝边界。

### 2026-10-23 H.264 SEI T.35、帧 side-data 与 NAL 边界语义

`itut35.mbt` 移植 `itut35.c`/`atsc_a53.c`/`dynamic_hdr_vivid.c` 与 `aom_film_grain.c` 的
解析侧：T.35 country/provider 全分发（ATSC GA94 字幕/AFD、AOM 颗粒、Samsung、Dolby、SMPTE、V-Nova LCEVC、HDR Vivid）。
与 C 一致的关键语义：Dolby 0x800 在 H.264 路径因无 DOVIContext 忽略；HDR10+/SMPTE 2094-40 App5/HDR Vivid 解析后留在 SEI 上下文不导出到帧（原 C 中 `ff_itut_t35_parse_payload_to_frame` 仅 AV1 解码器调用）；0xFF 国家码消费扩展字节后仍落 default 忽略。
`h264_side_data.mbt` 移植 `ff_h2645_sei_to_frame`：帧标签列表覆盖 stereo3d、displaymatrix、A53/AFD/LCEVC、未注册用户数据、ambient、
mastering display（g,b,r→r,g,b 重排、合法性检查）、content light、H.274 与 AOM 颗粒参数；携带即消费语义与 C 一致。
NAL 边界语义接入 `H264Decoder.decode_nals`：AU 入口在非待配场时 `ff_h264_sei_uninit`（孤场残留带给下一图，同 `!h->first_field`）；
首 slice 前的 SEI 归属本图、slice 后的 SEI 只更新状态；导出时机=帧图或场对第二场（`!FIELD_PICTURE || !first_field`）。
场对细节：A53 双场合并、AFD 取第二场值、LCEVC 第一场在 picture start 挂到待配帧并入对帧输出、
`sei_recovery_frame_cnt` 取首场值（`h264_frame_start` 仅帧图/首场运行）；帧图无恢复点 SEI 时保持 -1。
H.274 颗粒 seed 用图片 POC（本移植无 poc_offset）；另移植 `libavutil/display.c` 的旋转/翻转矩阵。

验证：T.35 分发表/A53 合并/HDR Vivid 位流/AOM 颗粒集单元测试；合成 SEI 流的帧 side-data 逐字段对 trace_headers 转写值断言；
真实 x264 流验证未注册数据挂载与 per-AU 排空；JM PAFF 流注入 SEI 验证场对边界与合并语义。

全量 Wasm **767/767**；`moon check --target wasm --deny-warn --no-render` 与
`moon info --target wasm` 通过。H.264 剩余重点：SP/SI、数据分区/冗余 slice、错误恢复、
HDR10+/SMPTE 2094-40 App5 结构级 T.35 解析与颗粒合成应用。

### 2026-10-23 H.264 PAFF 场图

支持 `field_pic_flag=1` 的 I/P/B 场图与帧场交叉引用：互补场共享平面与运动历史，
按 frame_num、场奇偶及 droppable 状态配对，第二场完成后输出。未解码场 POC 保持
INT_MAX，成对输出取两场最小 POC；第二场接续 slice 编号。场参考表按同/异场交错，
最多 32 项；MMCO 按场掩码更新参考并还原 PicNum 差值；DPB 超限按原 C 回收参考。

`h264_colocated.mbt` 对照 `h264_direct.c` 补齐 col_parity、异场行偏移、帧→场半宏块
抽取与场→帧双宏块抽取；时域 direct 统一参考身份为 `4*identity+structure`，映射
MBAFF/PAFF 场奇偶并使用对应场 POC 缩放，纵向 MV 按帧/场单位换算（除法向零截断）。
4:4:4 场图的水平宏块边使用 bS=3；禁止跨 slice 滤波时定位同场上邻，避免误读另一场。

验证：`h264_paff_realdata_wbtest.mbt` 共 **24 流、276 帧 × 滤波/未滤波两套
framemd5**，全部逐字节对齐 Docker `linuxserver/ffmpeg:9.0.2`
（镜像 ID `sha256:742e17516dd4d7b76b939d774bd08f0f6b7cab8bce1c014b95ab2fb992fd0a1f`）。
每条流同时断言固定帧数，另有 9 项场状态/真实特性覆盖测试。

| 实流组合 | 验收内容 |
| --- | --- |
| 8-bit 4:2:0 | I/P/B、CAVLC/CABAC、空间/时间 direct、12 帧参考标记、多 slice |
| 自适应 frame/field | 空间 direct 流含 7 帧图+10 场图，时间 direct 流含 6 帧图+12 场图；断言帧→场、场→帧 direct 均实际使用 |
| 场顺序/POC | bottom-first 互补场、POC type 0/1/2 |
| 8-bit 4:0:0/4:2:2/4:4:4 | B 帧与完整滤波；单色按 yuv420p 中性色度输出 |
| 高位深 | 4:2:0 的 9/10/12-bit；4:2:2 的 10-bit；4:4:4 的 10/14-bit，含时间 direct |
| slice 去块边界 | 每场多个 slice，`disable_deblocking_filter_idc=2`，同时覆盖同 slice 相邻场行与真实边界 |

底层均保持纯 MoonBit。实流基础来自 JM 19.0；bottom-first 是对真实 JM 时域 direct
流逐 slice 翻转 bottom_field_flag 后的派生样本，其新像素仍由独立 FFmpeg 严格解码取得。
JM 单色编码完成后存在空色度释放错误；生成器仅对完成标记后的该特例继续，并强制
FFmpeg `-err_detect explode` 与回归的固定 12 帧断言。未逐一穷举上表以外的工具组合。

复现：构建 JM 19.0 到 `.scratch/JM/`，保留原版 `bin/encoder.cfg` 与
`foreman_part_qcif.yuv`，依次执行 `moon run scripts/gen_jm_paff_input.mbtx`、
`moon run scripts/gen_jm_paff_cfgs.mbtx`、
`moon run scripts/run_h264_paff_reference.mbtx <JM/bin绝对路径>`、
`moon run scripts/emit_h264_paff_vectors.mbtx`。参考生成器仅挂载 JM/bin，容器禁用网络；
混编配置使用 Full Search，避开 JM EPZS 在跨结构参考时的崩溃。大型码流按需加载，
避免完整 Wasm 测试的包初始化函数超过局部变量上限。

全量 `moon test --target wasm`：**732/732**；`moon check --target wasm --deny-warn`
零错误/零警告；`moon info --target wasm` 通过。剩余 H.264 重点为 SP/SI、
data partitioning 与错误恢复；frame_num gap 合成已在后续完成；FMO、separate colour plane 保持原 C 拒绝边界。

原两条 P 流曾请求 6 帧却只有 3 帧输入，旧“8 流 84 帧”统计应为 78 帧；本轮已补足
输入并重建为各 6 帧，以上 24 流 276 帧由固定帧数断言核验。

### 2026-10-23 H.264 B 帧与 4:2:2/4:4:4 解码

在 I/P 链路上补齐 B 帧全套与全部非单色色度格式，入口与门禁不变：

- **B 帧**（`h264_b_mb/b_mc/b_refs/b_direct`、`h264_output`）：B_Skip/B_Direct 与
  B_16x16/16x8/8x16/8x8 全子分区，L0/L1 参考、mvd 与残差（CAVLC/CABAC 共用）；
  空间/时间 direct（colocated MV 缩放、`direct_8x8_inference`）；显式双向加权与
  隐式权重（POC 距离缩放）；默认 B 列表（FrameNumWrap + LongTermPicNum 两套）。
  输出按 POC 显示重排，IDR/MMCO5 开新 epoch，drain 延迟 B 帧。
- **高位深**（`h264_depth.mbt`）：8/9/10/12/14-bit 样本截断（`h264_store_coef`/`clip8`
  按 bit_depth 缩放）、QP 范围 51+6*(depth-8)、去块 alpha/beta/tc 移位。
- **4:2:2**：chroma422 DC 专用 coeff_token/total_zeros 码表与 8 系数扫描、2×4 DC
  Hadamard（dequant 表取 chroma_qp+3）、`pred8x16_*` 全模式（DC 四行象限、plane 的
  `V=(5v+32)>>6`）、色度 MC `my>>2` + `(my<<1)&7` 相位、CABAC DC `sig_coeff_offset_dc`
  三上下文与 `coeff_abs_levelgt1_ctx` 的 422 变体、`scan8` 缓存纵向堆叠的 nC/cbf
  邻居（偶数块读左宏块 b1/b3/b5/b7、首行块读上宏块 b6/b7）、去块垂直边 16 行
  （每个 bS 管 4 行）与水平边全滤。
- **4:4:4**：色度平面复用 luma 机器（I4x4/I16x16 预测、4×4/8×8 残差、逐平面 DC），
  `ctx_cat` 扩展的 CABAC 类别 6–13 与 cbf 上下文（cat 5 在 444 也读 cbf）、灰度
  cbp 映射表、`fill_decode_caches` 的 444 邻居填充与 8x8DCT/4x4 邻接 quirk
  （非 8x8 邻居按 0/PCM 64 参与上下文）、cbf=0 时整象限 nz 清零、色度 MC 直接用
  luma qpel 滤波器、PCM 768 样本/宏块、去块色度用 luma 核与色度 QP。

验证：`h264_b_realdata_wbtest.mbt` 21 条真实流（加权、CQM、多片、多 GOP、参考切换、
9–14 bit、连续 PCM、稠密残差）共 **142 帧**、`h264_chroma_realdata_wbtest.mbt` 6 条
4:2:2/4:4:4 真实流（CAVLC/CABAC × 8/10 bit）共 **42 帧**，均含默认滤波与
`-skip_loop_filter all` 两套 framemd5，与 FFmpeg 9.0.2 逐字节一致；生成命令在
`scripts/emit_h264_b_vectors.mbtx`、`scripts/emit_h264_chroma_vectors.mbtx` 与文件头。
全量 Wasm **699/699**；`moon check` 零错误/零警告。

## 4. 验证方式

所有期望值都取自 FFmpeg 仓库内的**一手资料**，不使用自造数据：

| 被测模块 | 向量来源 |
|---|---|
| crc | `tests/ref/fate/crc`（7 组原始输出）+ `libavutil/crc.c` 内硬编码表前缀（直接验证表生成器） |
| adler32 | `libavutil/tests/adler32.c` 内断言 `0x50E6E508` + Adler-32 规范示例；SIMD 版（`adler32_v128.mbt`）另用公开向量 Adler-32("Wikipedia")=`0x11E60398`，并对生成缓冲区与标量版**逐值相等**（含 `NMAX=5552` 边界） |
| base64 | `libavutil/tests/base64.c` 的参考编码表；SIMD 版（`base64_v128.mbt`）另用 **RFC 4648 §10** 向量，并与标量版在长度 0..129、长缓冲、12 字节边界及全 256 字节值上**逐值相等** |
| avstring | `tests/ref/fate/avstring`（`av_get_token` 的 26 组 input→token→rest）+ 该测试中的 `av_append_path_component`(7 组) / `av_strireplace`(3 组) / `av_strnstr` 偏移期望；其余用例由 `avstring.c`、`bprint.c` 的算法手推。SIMD 版 `av_strnstr_v128` 与标量版在生成输入（含每个 `hay_length`、非 ASCII BMP 串）上**逐值相等** |
| md5 | `tests/ref/fate/md5`（5 组摘要：1000 / 63 / 64 / 65 / 999 字节）+ RFC 1321 向量；SIMD 多缓冲版（`md5_v128.mbt`）的 `av_md5_sum_x4` 以四路相同消息复现全部上述摘要，并与标量 `av_md5_sum` 在**每 lane、跨 0/55/56/63/64/65/119/120/128/999/1000/4096 长度的四路不同缓冲**上**逐值相等** |
| sha | `libavutil/tests/sha.c` 引用的 FIPS 180 向量（含百万个 `'a'`） |
| mathematics | `libavutil/mathematics.h` 语义 + 边界用例 |
| rational | `libavutil/tests/rational.c` 的 `unit_mul_q` / `unit_add_q` 饱和表；`av_cmp_q` 与 `double` 比较的全枚举对拍 |
| avstring | `tests/ref/fate/avstring`（`av_get_token` 的 26 组 input→token→rest）+ 该测试中的 `av_append_path_component`(7 组) / `av_strireplace`(3 组) / `av_strnstr` 偏移期望；其余用例由 `avstring.c`、`bprint.c` 的算法手推 |
| rc4 | `libavutil/tests/rc4.c` 内转录的 **RFC 6229** 密钥流向量（40/56/64/128 位密钥，各 8 字节） |
| aes | **FIPS-197** 附录 C.1/C.2/C.3 的 ECB 向量（128/192/256）与 S-box 公开值；**NIST SP 800-38A** F.2.1 的 CBC-AES128 两分组向量（含 iv 链式值）——均为独立于 FFmpeg 的官方标准 |
| xtea | `libavutil/tests/xtea.c` 内嵌的 6 组 key/明文/密文三元组（大端，另按 C 测试的字节序变换覆盖小端路径）；CBC 分支复用该测试的 `"HALLO123"` IV 与 4 分组往返。`tests/ref/fate/xtea` 仅打印成功行，不含数值 |
| tea | `libavutil/tests/tea.c` 内嵌的 4 组 key/明文/密文三元组（64 轮）；CBC 分支复用该测试的 `"HALLO123"` IV 与 4 分组往返。`tests/ref/fate/tea` 仅打印成功行 |
| blowfish | `libavutil/tests/blowfish.c` 内嵌的 **34 组可变密钥 ECB 三元组** + 字符串 ECB(`"BLOWFISH"→324ed0fe…`) / CBC(`"BLOWFISHBLOWFISH"`，IV `"blowfish"`) 向量；均为该测试自带、可直接校验大 S-box 表的权威数据 |
| cast5 | `libavutil/tests/cast5.c` 内嵌的 **RFC 2144** 向量（128/80/40 位密钥，明文 `0123456789abcdef`）；CBC 分支复刻该测试的 `av_cast5_crypt2` 往返。该测试另含 100 万次迭代向量 `rct2`，因耗时未纳入 |
| aes_ctr | `libavutil/tests/aes_ctr.c` 的固定密钥/IV 40 字节向量（`fixed_key`=`"0123456789abcdef"`、`fixed_iv`=`deadbeefdeadbeef`）；另按 CTR 性质校验「任意切分 == 一次性」以覆盖分部块状态；`increment_iv`/`set_iv` 的计数器语义按 `aes_ctr.c` 手推 |
| MP4 demuxer | 测试内**逐字节合成**的 MP4/MOV（`ftyp`+`moov`+`trak`+`stbl`+`mdat`），期望值由 box 布局与 `mov.c` 语义手算：chunk→样本 offset 累积、`stts` DTS 序列、`ctts` 重排后的 PTS、负 `ctts` 的 `dts_shift`、`stss` 关键帧、`stz2` 位打包、`co64` 与 64 位 box、`elst` 的 `time_offset`、`stsc` 校验修复；碎片化：空 `stbl` 初始化段 + 两个 `moof` 片段（`tfdt` 驱动首段、无 `tfdt` 由 `track_end` 续接），校验 offset/DTS/PTS/size/duration/关键帧；cenc：合成的加密 MP4（`sinf`+`senc`），以同密钥/IV 的 AES-CTR 生成密文，验证 `mov_decrypt_sample` 还原明文（全样本与子样本各一）；**真实文件交叉验证**：由 `linuxserver/ffmpeg:9.0.2` 生成普通/碎片化/cenc 三种真实 MP4，以 `ffprobe` 的 `packet` 输出（`pos/size/dts/pts/duration/flags`）为期望值逐 packet 比对（普通与碎片化完全一致），cenc 用已知密钥解密后与明文样本逐字节一致 |
| aviobuf | 期望值全部由 `aviobuf.c` 的缓冲/seek 路径语义手推（跨 1 字节分块的 BE/LE 读写、EOF 语义与 seek 恢复、`avio_read` 直通/短读、四条 seek 路径各自不经/必经回调、`avio_size` 双回退、写侧 flush 时机与 seekback 恢复、dyn buf 增长/packet 前缀/padding、`get_str`/`get_str16` 代理对的消费计数、leb/varlen）；CRC 三助手与已验证的 libavutil `av_crc` 同表互证，流式 checksum 与一次性计算逐位相等；**集成回归**：既有 MP4 全部测试改经 `init_read_context` 跑在 AVIO 上，另以 3 字节分块 + 7 字节缓冲的 seekable / 不可 seek 源对同一合成 MP4 复算采样表，与整文件解析逐值一致 |
| Matroska Tracks | 合成轨道手推（空 Entry 的全表默认值——语言 eng/flag_default 1/采样率 8000/声道 1/Display 尺寸 -1；视频全字段（含 crop/AlphaMode/ColourSpace）；音频（48000 双声道 16 位、CodecDelay/SeekPreRoll/TimecodeScale）；header-strip 压缩的三层 ContentEncodings→ContentEncoding→ContentCompression 结构（Scope 默认 1）；双轨字段互不串扰）；**真实文件**：webm 双轨（V_VP8 128x96 无 Display 字段 → 默认 -1、Interlaced=2、DefaultDuration 1e8=10fps、A_OPUS 48000 单声道 + OpusHead CodecPrivate、CodecDelay 6.5e6/SeekPreRoll 8e7）与 mkv 单视频轨（V_MPEG4/ISO/AVC + avcC，首字节 1）逐值断言 |
| Matroska Segment/Info/SeekHead | 合成布局手推（已知/未知长度 Segment、空 Info 默认值、SeekHead 回跳——cluster-first 布局验证 execute_seekhead 真跳回解析未过的 Info、resync 跨 0x00 垃圾恢复）；**真实文件**：docker 生成的 webm/mkv 各取 1024 字节前缀（头 + 双 SeekHead×4 条目 + Info + Tracks + 首 Cluster），TimecodeScale/Duration（1008/1000 = ffprobe 的 1.01s/1.00s）/MuxingApp=Lavf63.1.102/segment_start/first_cluster_pos/8 条 SeekHead 的 id 与位置逐值断言 |
| Matroska Cluster/Block | 合成手推：lacing 四模式（无/Xiph/fixed/EBML）各有正例，三个反例（Xiph 长度链越界、fixed 不整除、EBML 负尺寸与尺寸超量）；SimpleBlock 单包（stream_index/data/keyframe/pts）、cluster timecode + 负 block 偏移（tc 1000、offset -250 → pts 750）、Xiph 双帧逐帧数据与 lace duration 分摊、BlockGroup（ReferenceBlock → 非关键帧、BlockDuration 覆盖默认）、双 Cluster 按 pos 排序、未知轨道号 → `EINVAL`；**真实文件**：docker `linuxserver/ffmpeg:9.0.2` 生成 32x32/5fps 的 webm（VP8+Opus）与 mkv（H.264+AAC），按 `pos + size + 4` 精确截到第二个包（正好落在第三个 Block 头前，顺带验证尾部截断容忍），前两个包的 stream_index/pos/size/pts/keyframe 与 ffprobe 逐值一致——负 pts 来自 `av_rescale_q(codec_delay, …)` 的舍入（Opus 6.5ms → 7 tick = -7、AAC 23.22ms → -23） |
| EBML 层 + EBML 头 | VINT 取 EBML 规范（RFC 8796 §4）宽度示例（含全一宽度的未知长度映射与 2/3/8 字节最大值；两处易错点由测试反推确认：0x3F 是 3 字节前缀而非 2 字节全一，最大已知 1 字节长度是 0xFE）；`is_ebml_id_valid` 用 matroska.h 的真实 ID（Header/Segment/Info/Void/CRC32）+ 标志位错位反例；游走器各路径（VOID 跳过、未知长度 master 遇 Segment 终止 = CHILD_OF 语义、零长 master、live 模式、元素越界、空流/截断）合成字节手推；**真实文件**：docker `linuxserver/ffmpeg:9.0.2` 生成的 webm（libvpx+libopus）与 mkv（libx264+aac），头部 64 字节 base64 内嵌，doctype/版本/长度上限与 ffprobe 的 EBML 头输出一致，头后紧跟的 Segment（未知长度）触发 EBML_STOP |
| advanced_editlist | 全部由 mov_fix_index 的步进语义手推：中部 media_time 编辑（前导弃帧保留但时间戳经 duration 缓冲重写为 -20/-10，保留帧从 0 起，含关键帧回退）；空首 edit（时间轴 +500、`start_time=500`、`min_corrected_pts` 净 0）；音频部分帧（`skip_samples=512`、首帧行至 -512）；正 `min_corrected_pts`（视频 DTS 整体 -50 使首 PTS 归零，seek 落在修正后时间轴）；`mov_estimate_video_delay`（ctts +20/0/+10 → 1，非 avc1 不估）；碎片化 trak 按 mov_read_trak 顺序禁用（先建 trak 仍被 fix、后建 trak 直通、movie 旗标翻转）；**真实文件回归**：docker AAC 文件 14 个包仍与 ffprobe 逐值一致，并新增 `discard`（首帧 AVINDEX_DISCARD_FRAME）与 `skip_samples=1024`（编码器延迟）断言 |
| sgpd/sbgp/open-GOP/多 stsd | 期望值由 mov.c 语义手推：sgpd NAL 类型（6 位掩码）与 sbgp run 表（含零计数 run 保留）；HEVC 无 CRA entry 时 offsets 仍展开但 key 表/min 时长不建（`cra_index==0` 早退）；open-GOP seek 为**差分测试**——同一文件 `hvc1`（CRA 样本 pts `dts+offset+dts_shift` 超过请求点即拒、按 `FFMAX(min_sample_duration,1)` 回退到闭合关键帧）与 `avc1`（无门直达 CRA）各求一次；`dts_shift` 为 0 的成因（`mov_update_dts_shift` 只跑非最后两条 entry）一并断言；多 stsd 三态（同 fourcc → `pseudo_stream_id=-1` 全索引且仅 slot 0 有 extradata；异 fourcc → 跳过且 `stsc.id=2` 的 chunk 剔除；首 chunk 引用 entry 2 → `last_stsd` 切到空槽清空主 extradata）；碎片 `stsd_id` 门走 trex 默认与 tfhd 旗标两条路径（匹配 1 收样、2 全拒） |
| mpeg4audio | 位串按 ISO/IEC 14496-3 手推：AAC-LC 44100 单/多声道、显式 24 位采样率、escape 对象类型、头部隐式 SBR、`0x2b7` 同步扩展的 SBR/PS、AAC-LD 帧长；`12 08 56 e5 00` 五字节直接取自 docker 生成的 `aac.mp4`（与真实文件互证） |
| extradata/esds | 合成 ES→DCD→DSI 描述符链（AAC 0x40 带 `12 10` 配置、MPEG-1 0x6b 无 DSI 特例）手推期望；**真实文件**：docker `ffmpeg:9.0.2` 生成的 `aac.mp4`，codec-id/声道/采样率/帧长/码率/5 字节 extradata 与 ffprobe 及 python 直读 esds 的结果逐项一致，14 个 packet 的 pos/size/pts/dts/duration 逐值比对（首包 pts -1024 的 edit-list 偏移在内） |
| sidx/mfra/碎片化 | 合成 box 手推（参考偏移、`FFMIN` 无关的 track_end/duration、无 tfdt 时的 sidx 回退、重叠片段 discard、mfro 探测）；**真实文件**：`-movflags +frag_keyframe+dash` 生成的 `sidx.mp4`，两条 sidx 参考（offset 891/2315、size 1372/1272）与 tfra 表（(0,891)/(10240,2315)）和 python box 直读一致，10 个 packet 与 ffprobe 逐值比对 |
| saiz/saio/senc | 合成 box：流级 saiz+saio 指向 free box 内的辅助块、`moof` 内 senc 的逐样本 IV，手推期望 |
| seek | 合成 10 样本索引（关键帧 1/7）：BACKWARD 回退到关键帧、ANY 精确命中、越界钳制、碎片化索引跨片段 seek；真实 `sidx.mp4` 上 seek 12000→样本 5、5000→样本 0 |
| 未压缩音频分块 | 合成 QT `sowt` 文件（2500 样本两 chunk → 1024+476+1000 样本三包）手推；**真实文件**：docker 生成的 `pcm_short.mov`，7 个 packet（6×1024 样本 + 471）的 pos/size/dts/duration 与 ffprobe 逐值比对 |
| idct | 期望值来自 FFmpeg **自带的双精度参考 IDCT** `libavcodec/dctref.c`（`libavcodec/tests/dct.c` 亦以此为准）；随机系数由 FFmpeg 自己的 `av_lfg`（`libavutil/lfg.c`，seed 1）与 `init_block()` 生成，**复刻 FATE `fate-idct8x8-0/1/2` 的方法**；判据逐字取自 `dct.c`（`err_inf ≤ 1`、`omse ≤ 0.02`、`|ome| ≤ 0.0015`），SIMPLE-C 8/10 两档完全满足；**12 位**在 `dct.c` 中标记 `nonspec = 1`（其 omse/bias 判据本就不适用），实测 SIMD 与标量 profile 完全一致（如 mode 0 两者均为 `err_inf=1, omse=0.1351, bias=-0.00259`）。SIMD 版（`idct_v128.mbt`，8/10/12 位）另在全 64 个单系数块上与标量版**逐值一致**，转置额外做「转置两次 == 原块」校验 |
| huffman | `tests/ref/fate/` 无 `huffman.c` 的数值向量（`fate-libavcodec-huffman` 实测的是另一套 `mjpegenc_huffman.c`）。采用：① Kraft 等和 `Σ2^-len == 1`；② 与**独立教科书两队列 Huffman** 的加权路径长相等（最优性）；③ FFmpeg 自带两套独立实现 `ff_huff_build_tree` 与 `ff_huff_gen_len_table` 互证；④ 编→解往返；⑤ 手推教科书样例（频次 1,1,2,4 → 码长 {1,2,3,3}）；⑥ `sum >> 31` 的过高频次拒绝 |
| bitstream | Exp-Golomb 码表直接取 **ITU-T H.264 / ISO 14496-10 表 9-1**（`1/010/011/00100/...` → 0..7，se 映射 0/1/-1/2/-2/3）；位读取的 MSB-first/`show_bits`/overread 语义按 `get_bits.h` 手推，含零窗口 `get_ue_golomb_long` 的 C 特有跳过行为 |
| h2645 NAL | 合成字节手推：00 00 03 去除、遇到 00 00 01 截断、尾零/停位 `get_bit_length`、Annex B 双 NAL（含前导垃圾）、无起始码报错、长度前缀双 NAL 与尺寸越界、HEVC NAL 头层号/时域号（含 temporal_id_plus1=0 拒绝）。**真实数据**：`avcC`/`hvcC` 的 VPS/SPS/PPS NAL 经长度前缀路径解析（见下） |
| h264_ps | **真实文件**：docker `linuxserver/ffmpeg:9.0.2` 生成的 `h264.mp4`（libx264，64x48，High@3.0），avcC 44 字节内嵌；期望值逐项取自 FFmpeg 自己的 `trace_headers` 输出（profile 100/level 30/chroma 1/8-bit/POC 2/ref 3/4x3 MB/VUI SAR 1:1 + timing 1/10 + bitstream restriction 0/3；PPS CABAC、ref 3/1、QP 23/26、chroma offsets -2、transform_8x8、second offset -2）；同一参数集另走 Annex B extradata 路径复核；截断 SPS 与缺失 SPS 的 PPS 均被拒绝 |
| hevc_ps | **真实文件**：docker 生成的 `hevc.mp4`（libx265 Main@3.0，64x48）与 `hevc10.mp4`（Main10），hvcC 内嵌（仅保留 VPS/SPS/PPS 数组，头字节/NAL 原样）；期望值取自 `trace_headers`（Main profile 1/level 30/64x48/chroma 1/8-bit、POC LSB 8、CTB 32x32、VUI 1:1 + timing 1/5；PPS sign hiding/ref 1/1/init QP 0/CU-QP-delta/weighted；10 位档 profile 2、`yuv420p10le`、qp_bd_offset 12）；另走 Annex B extradata 路径。short-term RPS 用规范位串手推（-1/+1、used 位图），`compute_poc2` 覆盖回绕与 BLA 类型复位 |
| AAC/MP3 帧头与裸流 | `audio_headers`：真实 ADTS 首帧 `FF F1 50 40 1B 1F FC`（LC/44100/mono/216 字节，与 ffprobe packet size 一致）与 LAME 首帧 `FF FB 90 C0`（MPEG-1 L3/128k/44100/mono/417 字节）；MPEG-2 LSF（22050/64k/208 字节）与自由格式（bitrate_index 0）手推；ADTS 同步/采样率/帧长错误与头部检查负例。`rawaudio`：内嵌 docker 生成的完整 1517 字节 ADTS AAC 与 2506 字节 MP3（LAME 128k CBR + Xing/Info），逐帧 `pos`/`size` 与 `ffprobe -show_packets` 全部一致（AAC 10 帧 216/210/127/…；MP3 剔除 417 字节 Info 帧后 417,418×4，首包 pos 417）；另加 44 字节合成 ID3v2 前缀验证整体偏移 +44，以及截断输入拒绝 |
| 码流参数桥 | H.264/HEVC 用上述真实 avcC/hvcC 断言 `CodecParameters`（64x48、profile/level、chroma/位深、pix_fmt、VUI 帧率 5/1 或 10/2、SAR 1:1）；AAC 用 `12 08 56 e5 00`（LC/44100/mono/1024）；**真实容器**：MP4 `aac.mp4`（`esds` → AudioSpecificConfig）与 Matroska x264 文件（avcC → Baseline@1.0、128x96）分别经 `mp4_track_codec_parameters` / `mkv_track_codec_parameters` 解出同一结构 |

| H.264 I 帧解码 | **真实码流 + 逐帧 YUV MD5**：docker `linuxserver/ffmpeg:9.0.2` 生成 6 组 Baseline 两帧 IDR 码流（`testsrc2`，`keyint=1:bframes=0:cabac=0:8x8dct=0:slices=1`），参考输出取 **`-skip_loop_filter all`** 的 rawvideo yuv420p，逐帧 MD5 即期望值：64x48 qp26（I_4x4 为主）、64x48 `gradients`（I_16x16 luma DC）、64x48 纯色（I_16x16 DC/VERT、无残差）、**88x56 裁剪流**（6x4 宏块裁到 88x56）、80x64 qp40（长零游程）、64x48 qp10（稠密残差）。另有「重建结果不是平坦预测」护栏（luma 动态范围 > 32），以及 5 条**负例**断言具体错误串：P slice→`PATCHWELCOME`、多 slice→`multi-slice pictures are not supported`、CABAC→（已支持，见下两行）、4:2:2→`only 4:2:0 is supported`、10-bit→`only 8-bit decoding is supported` |
| H.264 去块滤波 | **同一批 6 条码流的第二套期望值**：参考输出改取 FFmpeg **默认解码**（不传 `-skip_loop_filter`）的 rawvideo yuv420p，逐帧 YUV MD5 必须与默认入口 `h264_decode_annexb_i_frames` 一致；原先的 6 组向量继续以 `..._ex(data, true)` 钉住未滤波重建。两组向量合起来同时钉死「重建是否正确」与「滤波是否只改了该改的样本」——本切片最初就是靠这组对照发现「逐宏块滤波破坏了帧内预测输入」的。这 6 条里 `gradients` 的第 2 帧与纯色流两帧的滤波前后 MD5 相同（边沿低于 `alpha`/`beta`，滤波器合法地不改任何样本） |
| H.264 CABAC 核心 | FFmpeg 自带 `libavcodec/tests/cabac.c`：测试内的参考编码器（`put_cabac`/`put_cabac_bypass`/`put_cabac_terminate`）与 `av_lfg_get`（`libavutil/lfg.c`，MD5 播种、种子 1）一并移植，10240 bypass + 10240 上下文比特逐比特还原；`get_cabac_terminate` 必须非零。注意 `init_cabac_encoder` 的 `pb.bit_left++` 会吞掉首个比特，打包时要丢掉流的第一位 |
| H.264 CABAC 宏块层 | 原五条单行/平坦 IDR 向量；新增 16×48/32×48/64×48 稠密 I、64×16 I+3P、96×64 运动 I+5P，共 13 帧滤波/未滤波 MD5。16×48 的 2403 条、64×48 的 8233 条 ctx/state/bin 与原 C 一致，见 §3「CABAC 的验证范围」 |
| H.264 frame_num 间隙 | 19 流/86 帧，独立合成与真实丢帧流，滤波/未滤波两套 MD5 对齐 Docker FFmpeg 9.0.2；另有 7 项状态与失败重试测试 |
| H.264 PAFF / 帧场交叉引用 | JM 19.0 及 bottom-first 派生实流，24 流 276 帧，默认滤波与跳过滤波两套 framemd5 对齐 Docker FFmpeg 9.0.2；覆盖位深/色度、POC、场顺序与多 slice 的具体矩阵见 PAFF 小节 |
| H.264 P 帧 / DPB | 真实码流：`keyint=10` 的 Baseline I+P+P 流经 `h264_decode_annexb_frames` 解出 3 帧（滑窗 DPB 出参考）；单元测试覆盖 MC 的整数/半像素/四分像素与 chroma 1/8 双线性（含负位移的边界复制）、`mid_pred` 与 A/B/C 邻块回退、P_SKIP 零捷径。**合成码流**：全 `I_PCM` IDR（非平坦图案 = `I_PCM` 路径首个向量）+ CABAC P 帧（`P_L0_16x16`、整数 MV、零残差），P 帧逐像素等于参考帧平移（左/上边界按 `ff_emulated_edge_mc` 复制）；编码器镜像复刻解码端每个 CABAC 上下文推导 |
| AAC ICS/TNS/IMDCT | 合成位串手推：`ics_info` 各窗口序列与 `max_sfb`、section 数据（含 escape 长度）、TNS 逐字段、EIGHT_SHORT 分组展开、codebook 0 静音帧、TNS 一阶 AR 滤波、predictor 数据拒绝；全部 11 本码书的元组/符号组合与位消费；MDCT/加窗的 Princen-Bradley 互补性、核行逐点、`vector_fmul_window` 2×len、KBD 窗和窗口转换结构；长短窗综合对照 `ff_tx_mdct_naive_inv` 的符号与 PCM 缩放；**真实码流**：libfdk_aac 46 帧正弦淡入流的 47,104 样本对照 FFmpeg PCM，最大误差 1 LSB、精确匹配率 ≥99.9%（见 §3） |
| AAC 真实窗口切换 / pulse | `generate_aac_transient_vectors.py` 调用 Docker FFmpeg 9.0.2：native/FDK 分别编码同一 48 kHz 突发信号，覆盖四种序列、两种 window shape、短窗分组、PNS/TNS，27 帧共 27,648 样本最大误差 1 LSB；六帧合成 pulse 码流由 FFmpeg 解码取得参考 PCM，6,144 样本最大误差 1 LSB；另有 pulse 语法、多采样率边界及短窗/截断/越界负例 |
| AAC CPE / M/S / 强度立体声 | `generate_aac_stereo_vectors.py` 用 FFmpeg native/FDK 生成真实双声道码流，另构造独立左右窗口与 M/S 0/1/2、强度 14/15、噪声频带排除的合成码流；四组共 35 帧、71,680 个声道样本对照 FFmpeg 9.0.2 PCM 最大误差 1 LSB、精确匹配率 ≥99.9%；另有掩码分组/消费位数和非法模式/频带/声道状态/尾元素回归 |
| AAC 多元素 / PCE / LFE | `generate_aac_layout_vectors.py` 生成真实 indexed 5.1、PCE 5.1/7.1 与非连续标签双 SCE、每帧乱序 PCE 5.1；五组 23 帧共 135,168 个声道样本对照 FFmpeg PCM 最大误差 1 LSB、精确匹配率 ≥99.9%；默认 1..7 声道表对照、PCE 逐位截断/重复/布局边界、DSE 对齐和扩展长度、失败帧窗历史/布局/PNS 状态回归 |
| AAC ASC / MP4 到 PCM | `generate_aac_asc_vectors.py` 生成 stereo 与 ASC-only PCE 5.1/7.1 的真实 MP4；ffprobe 的 ASC/逐包位置与大小为元数据期望，FFmpeg `-flags2 +skip_manual` PCM 为音频期望；MP4 demux→参数桥接→ASC 初始化→raw packet 解码共 65,536 样本最大差分 1 LSB、精确匹配率 ≥99.9%，另含 ASC 延迟/对齐/截断/不支持配置及畸形 PCE 的回退行为 |
| AAC-LC 960/120 | `generate_aac_960_vectors.py` 合成 mono/CPE 的四窗口序列、正弦/KBD、首尾频带；FFmpeg 9.0.2 解码 PCM 为期望，28,800 样本最大误差 1 LSB、精确匹配率 ≥99.9%；真实编码器样本尚待验证 |
| AAC-LC CCE | `generate_aac_coupling_vectors.py` 合成 7 组 ADTS（1024）及 2 组 MP4（960）；FFmpeg 9.0.2 PCM 为期望；三耦合位置、四增益尺度、有符号差分、CPE 选择、TNS/分组短窗、16 增益列表和多源乱序，共 151,040 样本最大差分 1 LSB；另含截断/错误目标/PCE 标签清理与历史回滚 |
| AAC 高度 / indexed 11..14 | `generate_aac_height_vectors.py` 的 9 组合成 ADTS/MP4；ffprobe 元数据与 FFmpeg PCM 为期望；三层 PCE、22.2 特殊元素顺序、普通 CPE 掩码排序、24 声道、四窗序列、保留高度回退、960 ASC-only PCE，共 473,088 样本最大差分 1 LSB，另有截断与历史回滚回归 |
| H.264 `avcC` 入口 | 真实 `avcc_baseline.mp4`（5042 字节，baseline/CAVLC）：`av_mov_open` → `track.extradata`（`avcC`）+ 按 `stco`/`stsz` 切样本 → `h264_decode_avc_frames`，逐帧 YUV MD5 对齐 FFmpeg 默认解码 |
| H.264 SEI | **真实码流 + `trace_headers` 逐字段**：docker `linuxserver/ffmpeg:9.0.2` 生成三条码流——① x264 `nal-hrd=cbr:vbv-maxrate=200:vbv-bufsize=200`（buffering period ×2 + picture timing ×2 + x264 user-data，VUI HRD 全字段，BP/PT 的值延迟对 SPS 解析）；② 同一流经 `h264_metadata` bsf 注入 display orientation（旋转 16384 + 双翻转）；③ 手工构造的 11 类合成消息流（恢复点、green metadata、film grain、frame packing、alternative transfer、ambient viewing、mastering display、content light、双 unregistered、cancel 的 display orientation、带时钟时间戳的 picture timing——后者靠 SPS VUI 的 `pic_struct_present_flag` 原位翻转启用，不移动任何后续字段）。期望值逐字段转录自 `ffmpeg -c copy -bsf:v trace_headers` 输出（green metadata 无 CBS dumper，期望值取 trace 的 payload 字节）；负例按 C 的错误路径断言具体错误串（<16 字节 unregistered、size 越界、`recovery_frame_cnt ≥ 1<<16`、film grain `num_model_values>6`、缺失 SPS 的 buffering period 返回 `AVERROR_PS_NOT_FOUND` 非致命）。码流以 base64 内嵌于 `h264_sei_realdata_wbtest.mbt` |
| MPEG 音频 Layer I/II 解码 | **真实码流 + PCM MD5（Layer II）**：docker 生成 4 条 `mp2`（stereo 44.1k/192k、mono 44.1k/64k、joint stereo 32k/64k、MPEG-2 LSF stereo 24k/64k），期望值为 FFmpeg 定点解码器 `-c:a mp2 -f s16le` 的 MD5（`818bac4a…` / `5c6c8e47…` / `941cb133…` / `f9bf9037…`），覆盖分配表 1/0/3/4 与 3/5/9 步组量化。**Layer I 无编码器**，改用按规范手工拼帧、由 FFmpeg 解码同一段码流取得期望值（`cb2824e3…` mono 32k、`a0ed4a92…` joint stereo 44.1k bound=4），输入是手工的、期望值仍是参考实现的 |
| MP3 Layer III 解码 | **真实码流 + PCM MD5**：docker 生成 stereo（440Hz L / 660Hz R，44100Hz，128kbps，无 Xing 帧）、mono（5 帧共 5760 样本）与 MPEG-2 LSF（22050Hz，64kbps）三条码流，期望值为 FFmpeg **定点**解码器 `-c:a mp3 -f s16le` 的 MD5（`c6f9d146…` / `7c931d35…` / `97d37691…`）；浮点解码器给出不同 MD5，故这组向量专门钉住定点路径 |

补充交叉校验：分块更新与一次性计算等价（CRC / Adler-32 / MD5 / SHA）、增量 MD5、
Base64 截断输出行为与非法输入检测。

**真实文件交叉验证（已完成，两批）**：由 Docker 镜像 `linuxserver/ffmpeg:9.0.2` 生成真实
MP4/MOV，用其 `ffprobe` 的 `packet` 输出作为期望值，与本文档解析结果逐 packet 比对
（`pos`/`size`/`dts`/`pts`/`duration`/关键帧）。第一批覆盖普通、碎片化（`frag_keyframe`）
与 cenc 加密三种文件（cenc 以已知密钥解密后与明文样本逐字节一致）；第二批新增
AAC `esds`（extradata/声道/采样率/帧长/码率 + 首包 edit-list 偏移 -1024）、PCM `sowt`
（未压缩音频分块路径）与 `+dash` 碎片（`sidx`/`mfra` + 跨片段 seek）。文件以 base64
内嵌于 `libavformat/mp4_real_wbtest.mbt`（附生成命令便于复现）。第二批还暴露并修复了
一个保真 bug：QuickTime 在 `minf` 里放置的数据 `hdlr`（subtype `url `）不应覆盖
`mdia`/`hdlr` 已识别的媒体类型（C 只对 vide/soun/subp/clcp 赋值）。

**解码器向量（第三批）**：H.264 的 6 条 Baseline 码流与 MP3 的 3 条码流同样由该镜像生成，
期望值分别是 `-skip_loop_filter all` 的**逐帧 YUV MD5** 与定点解码器的 **PCM MD5**；
码流与期望值以 base64 内嵌于 `libavcodec/h264_decode_realdata_wbtest.mbt` 和
`libavcodec/mp3_decode_realdata_wbtest.mbt`（文件头附完整生成命令）。这一批暴露并修复了
两个 bug：SPS 裁剪被**缩放两次**（88x56 解出 80 宽），以及多 slice 码流被「当整帧解」
后才报错（现改为解码前预扫 `first_mb_in_slice`）。

**音频 Layer I/II 向量（第五批）**：Layer II 用 FFmpeg 原生 `mp2` 编码器现场生成 4 条码流；
**Layer I 没有任何编码器可用**，因此两条 Layer I 向量是按规范手工拼出的帧（3 帧/流以便
mp2 demuxer 探测），期望值取 FFmpeg 解码同一段码流的定点 PCM MD5——手工的只是输入。
这一批暴露的坑记在 §7 的 31–33。

**去块滤波向量（第四批）**：同 6 条码流再取一套 FFmpeg **默认解码**（启用环路滤波）的
逐帧 YUV MD5，作为 `h264_decode_annexb_i_frames` 的期望值。这一批暴露并修复了三个 bug：
`slice_alpha_c0_offset`/`slice_beta_offset` 漏乘 2（C 存的是 `div2 * 2`）；内部边误走强
核而非 bS=3 弱核；以及最关键的**滤波时序**——逐宏块滤波会让后续宏块的帧内预测读到已滤波
像素，必须「整帧重建后再按光栅序滤波」才能同时满足预测读未滤波、滤波读已滤波。

复现命令：

```bash
cd moonbit
moon check     # 0 error / 0 warning
moon test --target wasm  # 758/758 passed
```

---

## 5. 后续重构计划

### 阶段 0 —— 工程骨架 + libavutil 纯算法首批（已完成）
见第 3 节。

### 阶段 1 —— 补齐 libavutil 纯算法模块
优先级从高到低（1 已完成）：

1. ~~`avstring.c`~~ **已完成**（见第 3 节；varargs 三件套 `av_strlcatf`/`av_asprintf`/`av_bprintf`
   与依赖就地改写字符串的 `av_strtok` 未移植）
2. `intreadwrite.h` 语义的字节读写工具（BE/LE 定点读写，作为后续所有解析器的基础）
3. `aes.c`、`rc4.c`、`xtea.c`、`tea.c`、`blowfish.c`、`cast5.c`、`aes_ctr.c`
   **已完成**（见第 3 节）；其余分组密码 `des.c`、`twofish.c`、`sm4.c` 待做 —— 其
   `libavutil/tests/{des,twofish}.c` 自带向量可作权威参考，`sm4` 用标准向量。
4. `lzo.c`（解压）、`eval.c`（表达式求值）、`timecode.c`、`parseutils.c`
5. `pixdesc.c`、`samplefmt.c`（大量纯数据表 + 少量逻辑）
6. `dict.c`、`opt.c`（依赖内存分配，见阶段 2 的约束）

### 阶段 2 —— 内存与平台相关部分
`mem.c`、`av_malloc`/`av_free`、`pthread`/`thread` 抽象。这些依赖宿主内存模型，
需要先确定 MoonBit 侧的策略（MoonBit 原生内存 + FFI，还是保留 C 侧实现）。

### 阶段 3 —— libswresample / libswscale
重采样与缩放中的纯算法部分（有理数重采样比、滤波器系数计算）；
向量化内核（`libswscale/x86` 13 个、`libswresample/x86` 3 个）按新边界原则用 `@v128` 重写。

### 阶段 4 —— libavformat（**MP4/MOV 切片 + 流式 AVIO 层基本完备**）
已完成 MP4/MOV 容器解析、采样表索引、**碎片化 MP4**（含 `sidx`/`mfra` 与重叠片段
`discard`）、**extradata**（`avcC`/`hvcC`/`av1C` 原样 + `esds`/AAC 配置）、**seek**
（`mov_seek_stream`）、**未压缩音频分块路径**与 **cenc 全家**（流级 + 碎片内 `senc`、
`saiz`/`saio`；见第 3 节范围边界与第 4 节验证方式），以及 **`aviobuf.c` 的流式 AVIO
层**（读+写+dyn buf+校验和，回调驱动）——MP4 解复用已改跑在 `AvioContext` 上
（`av_mov_open_avio`），"整文件入内存"仅剩 `init_read_context` 便捷封装——以及
**sample group 与多 sample description**：`sgpd`/`sbgp` 解析、
`build_open_gop_key_points` → `can_seek_to_key_sample` 的 HEVC 开放 GOP seek、
`ff_mov_read_stsd_entries` 多 entry 走查 + `pseudo_stream_id` 索引过滤 +
tfdt/trun `stsd_id` 门，以及 **`advanced_editlist`**（`mov_fix_index` 全量：
edit 时间轴索引重建/discard 时间戳重写/`skip_samples`/`min_corrected_pts` 归零/
`start_time`/tts 重建/`mov_estimate_video_delay`/碎片化自动禁用）。MP4/MOV 解复用
至此覆盖 mov.c 的全部索引与时间戳路径。余下：

1. 其他容器：**Matroska/WebM 已到可出包阶段**（EBML 层 + EBML 头 + Segment/Info/SeekHead/
   resync + Tracks + **Cluster/Block 与三种 lacing** 完成，`av_matroska_open` 已产出带
   pos/size/pts/keyframe 的 `MkvPacket` 队列，多 Cluster 走查并对尾部截断容忍；余下 Cues
   索引与 `skip_to_keyframe` 懒解复用、BlockAdditions 侧数据、header-strip 解压、RM 音频 /
   WebVTT 特化）。**WAV/RIFF、MPEG-TS（188/192/204、PAT/PMT/PES/PCR）、HLS（主/媒体
   播放列表与分片元数据）、DASH（MPD + SegmentTemplate/Timeline/BaseURL）已在
   2026-10-21 切片中落地**；余 AVI、HLS/DASH 的实际网络/分片 IO 与 AES-128。
2. **裸 AAC/MP3 流**：`rawaudio.mbt` 完成 ADTS 与 MPEG 音频分帧（ID3v2/v1、Xing/Info/VBRI、
   `ffprobe` 对齐的 pos/size），后续可接公共 `avformat_open_input` 风格入口
3. **码流参数桥**：`codec_tags.mbt`/`codec_parameters.mbt` 让 MP4/MKV 共用
   `CodecParameters`（H.264/H.265/AAC/MP3 的 profile/level/位深/帧率/SAR/帧长）
4. `avio.c` 的 URL/协议层与 `avio_open`（文件/网络 IO，属平台边界，或留 FFI）

### 阶段 5 —— libavcodec（**已开始**：IDCT + Huffman + 码流层 + H.264 I 帧+去块 / MPEG 音频三层）
只做纯 C 的编解码器（Huffman/VLC 表、LZO、部分无损编解码）；其向量化内核
（`libavcodec/x86`，共 113 个 `*.asm`）按新边界原则用 `@v128` 重写，而非保留 C/FFI。

已完成 SIMPLE-C IDCT（`int16` 8/10/12 bit）、`huffman.c` 的树/码长构建、`get_bits`/Golomb
位流层、H.264/HEVC NAL 拆分与参数集、AAC ADTS 与 MPEG 音频帧头、**H.264 SEI**
（消息框架 + buffering period/picture timing/恢复点/green metadata + h2645 公共消息集），
以及 **H.264 Baseline
I 帧重建 + 环路去块滤波**（slice header + CAVLC + 4x4 IDCT + 帧内预测 + `h264_loopfilter`，
逐帧 YUV MD5 同时对齐 FFmpeg 默认解码与 `-skip_loop_filter all` 两套输出）与
**MPEG 音频 Layer I/II/III 定点解码**（逐样本 PCM MD5 对齐 FFmpeg 定点解码器）——见第 3 节。仍是「部分解码器 + 完整解析层」的能力边界。后续：

1. H.264 SEI 的 ITU-T T.35（`itut35.c`/`atsc_a53.c`）与 AVFrame side-data 层；
   HEVC SEI 入口（`hevc/sei.c`）
2. `vlc.c` 的 `ff_vlc_init_from_lengths` 表构建（当前只产出它需要的 `lens`/`xlat`）
3. ~~接 MP4 的 avcC + 长度前缀入口 → CABAC（`h264_cabac.c`）→ P 帧与 DPB/参考帧管理~~
   **已完成**（`h264_inter/mvpred/mc/dpb` + `h264_b_*` 全套 B 帧；SP/SI 重建仍开放）
4. VP9 profile-0 关键帧像素解码已完成；后续为 VP9 帧间参考/概率适配、AV1 像素与
   Opus 音频解码。HEVC 剩余边界见上方范围清单。
5. AAC 原始帧解析：ICS/TNS/MDCT/**频谱 Huffman**/scalefactor/pulse/PNS/gain-control
   语法与**全四窗口序列 + KBD 窗**已完成（`aac_spectrum.mbt`/`aac_tables.mbt`，
   真实长窗、短窗/窗口切换及 pulse 合成码流 PCM 对照 FFmpeg 最大误差 1 LSB）；
   CPE 双声道与共享/独立窗口、M/S/强度立体声已完成；余下更多采样率/工具组合
   向量、SBR/PS、height+CCE 命名布局与 CCE/960/高度真实编码器样本；默认 1..7/11..14、多元素
   SCE/CPE/LFE、带内与 ASC PCE 已完成，960 综合已有合成参考验证
6. `jrevdct.c`（`FF_IDCT_INT`）、`faanidct.c`、`xvididct.c` 与 `idct248/84/48/44` 部分变换；
   `lzo.c` 与其余编解码器（无损优先）

### 阶段 6 —— 公共 API 层与 fftools
为已移植部分提供稳定的 MoonBit 公共 API，并提供 `extern "C"` 绑定层，
让未移植的 C 代码与 MoonBit 代码互通。

---

## 6. 移植约定（C → MoonBit）

为保证可对照、可 review，逐行移植时统一遵循以下约定：

| C 惯用法 | MoonBit 约定 |
|---|---|
| 输出参数 `int *dst_num, *dst_den` | 改为返回值，例如 `av_reduce` 返回 `(num, den, exact)` 元组 |
| 返回 `<0` 表示错误（`AVERROR(EINVAL)`） | 返回 `Result[T, String]`，错误信息为 `"EINVAL"` |
| C 字符串 `const char *` + 长度 | 输入用 `Bytes`；文本输出返回 `String` |
| 可变输出缓冲区 | `FixedArray[Byte]` / `FixedArray[UInt]` |
| `goto`（base64 解码状态机） | 结构化改写为显式状态标志 |
| 函数指针（`ctx->transform`） | 布尔选择器（`is_sha1`） |
| `~x`、`>>>`（本工具链不可用） | `x.lnot()`；逻辑右移改用 `UInt`/`UInt64` |
| `CONFIG_SMALL` 分支 | 统一实现 small 分支（等价结果，可读性优先） |
| `av_mallocz` 零初始化上下文 | 构造器 `*_alloc()` 显式置零 |
| 大常量表（Blowfish `orig_s`、CAST5 `S1..S8`） | 从对应 C 源文件批量提取十六进制常量后生成表字面量，避免手工转录出错；再用模块自带的权威向量校验 |
| FFmpeg x86 SIMD 内核（NASM `*.asm`） | 用 `@v128`（`moonbitlang/core/v128`）重写，`#cfg(any(target="native", target="wasm"))` 门控 + 标量回退；不可表达的 x86 专有指令（AES-NI / SHA-NI / PCLMULQDQ）才留 C/FFI |
| SIMD 实现的可验证性 | SIMD 路径必须与标量路径、以及既有权威向量**逐值一致**（同一测试同时跑两条路径，或断言 SIMD == 标量） |

**注意**：MoonBit 的 `String` 是 UTF-16。`Bytes::to_string()` 返回的是 `b"..."`
字面量形式、`to_unchecked_string()` 按 UTF-16 解释字节 —— 二者都不能把 ASCII 字节
转成正确字符串，需要按码位构造 `String`（见 `base64.mbt` 的 `ascii_to_string`）。

---

## 7. 踩坑记录

1. **`av_rescale_rnd` 无限递归（已修复）**：负数分支 `-FFMAX(a, -INT64_MAX)` 有两层取反，
   初次移植漏掉内层取反，导致 `rnd` 在 2↔3 之间振荡、无限递归、测试进程栈溢出
   （退出码 `0xc00000fd`）。测试立即暴露。
2. **`Bytes` ↔ `String` 转换语义**：见第 6 节末尾，曾导致 base64 测试"两边一起错"的假通过。
3. **工具链语法差异**：`~`（按位取反）与 `>>>`（逻辑右移）在此工具链不存在；
   `in` 是保留字；`for i = 0; ...` 与 `for i in 0..<n` 均可用。
4. **方法定义需带类型前缀**：`fn position(self : Mp4Reader)` 已弃用，须写成
   `fn Mp4Reader::position(self : Mp4Reader)`。
5. **转换函数命名**：`UInt::to_int` / `Int::to_uint` 等按位转换已更名为 `reinterpret_as_int` /
   `reinterpret_as_uint`；真实的加宽转换（`to_int64`/`to_uint64`）仍可用。注意
   `Int`（32 位）没有 `reinterpret_as_uint64`，需要先 `to_int64()`。
6. **`to_uint()`/`to_int()` 的返回值**：丢弃非 `Unit` 返回值必须显式写出（`let _ = ...`），
   否则报 `4139`；结构体字段上多余的 `mut` 会当作错误（`unused_mut`）。
7. **枚举比较需要 `Eq`**：`derive(Eq, Show)` 会触发 `implicit_impl_as_method` 与
   `deprecated_syntax` 告警；本切片改为 `match` 辅助函数（`media_is_video`/`media_is_audio`）
   避免派生的连锁告警。
8. **绝对寻址 peek 不能模拟顺序位读（流式 AVIO 教训）**：`stz2`/变长 `stsz` 的
   `get_bits_long` 初版用「save 游标 → 绝对 seek → 读 1 字节 → 回退」的 peek 实现，
   在不可 seek 的流式源上内部 seek 失败被忽略，游标漂移导致后续 box 全部错位（整文件
   与 seekable 源都测不出来）。正确做法与 C 一致：把位域字节**顺序读入**再解包。
9. **`mov.c` 9.0 的 `tts` 合并**：`mov_build_index` 已不再直接消费 `stts_data`/`ctts_data`，
   而是先经 `mov_merge_tts_data` 展开为每样本一条的 `MovTts`；PTS 由
   `mov_finalize_packet` 的 `dts + dts_shift + tts.offset` 得到（`mov.c:11686`）。
   按旧版（直接读 stts/ctts）移植会与 9.0 行为不一致。
9. **大写标识符不能作参数名**：MoonBit 的值标识符须以小写或 `_` 开头；`cast5.mbt` 的
   密钥调度参数 `K` 触发 `E4074/E3002`，改名 `k` 即可。
10. **批量提取常量表时的两个陷阱**：把 C 源码里的 `0x…` 常量转大写时，`0x` 前缀会
   一并变成 `0X`（MoonBit 不识别，须只大写十六进制数位）；此外不要对提取结果做
   「行重排」式的二次处理 —— 曾因此把 8 张 S-box 表错位，靠「与 C 源码逐值 diff == 0」
   才定位。生成后务必用模块自带向量 + 与 C 源的逐值比对双重校验。
11. **ISO VisualSampleEntry 末尾的 `pre_defined = -1`（2 字节）**：`parse_stsd_video`
   起初漏读这 2 字节，使其后子 box（`esds`/`sinf`）整体错位；普通非加密文件因
   「跳过 entry 其余部分」而无感，但 cenc 的 `sinf` 永远读不到（`cenc_scheme` 为 0）。
   该问题由**真实文件交叉验证**（docker `ffmpeg:9.0.2` + `ffprobe`）暴露并修复。
12. **`test` 是 MoonBit 保留字**：与其同名的参数/变量（如 `dct.c` 的 `init_block(..., int test, ...)`）
   会触发大面积 `E3002` 解析错误并连带出无关告警；移植时改名为 `mode` 即可。
13. **白盒测试专用依赖**：`moon.pkg` 里 `import { "…/libavutil", } for "wbtest"` 只对
   `*_wbtest.mbt` 生效，避免非测试文件不使用依赖而触发 `unused_package` 告警。
14. **行数统计口径**：`Get-Content $f | Measure-Object -Line` 会比 `(Get-Content $f).Count`
   少计（末行无换行符时不计入）。本 README 统一用后者。
15. **合成 MP4 的 SampleEntry 头是 8 字节**（6 保留 + 2 字节 `data_reference_index`），
   视频入口后还有 16 字节 pre_defined、`compressorname` 是 1 长度字节 + 31 字节；音频入口
   后是 QT 的 version/revision/vendor 8 字节。第一版合成器各少写/漏写导致 entry 内整体
   错位、extradata 与声道全错 —— 对照 `mp4_wbtest.mbt` 的既有构造器修正。
16. **`minf` 里的数据 `hdlr`（subtype `url `）**：QuickTime/ffmpeg 的 PCM MOV 会在 `minf`
   下放一个 `hdlr`（数据引用），C 的 `mov_read_hdlr` 只对 vide/soun/subp/clcp 赋值、
   其余类型不覆盖已识别的媒体类型；第一版把它也当媒体 hdlr 处理，把 `soun` 覆盖成
   Unknown，`stsd` 采样入口不再解析（声道为 0）。真实文件交叉验证暴露。
17. **`mov_read_sidx` 的锚点在函数入口取**：`offset = avio_tell(pb) + atom.size` 是进入
   函数时的 position；若在读完 28 字节头部字段后再取 position，参考偏移会整体后移
   28 字节，`trun` 的 sidx 回退随之失配（表现为 DTS 落到 `track_end`）。
18. **`mfra` 必须带 `mfro` 尾**：demuxer 从文件最后 4 字节读 mfra 大小 —— 那是 `mfro`
   box 的最后一个字段；合成不带 `mfro` 的 `mfra` 永远探测不到。
19. **H.264 4x4 IDCT 的第二遍索引**：行变换读 `b/k*4` 系列，列变换若沿用第一遍的
   `b+8`/`b+4`/`b+12` 就会拿到错位的系数（正确是 `b+2`/`b+1`/`b+3`）。这是最初
   「整帧像素全错、但 MD5 差距均匀」的主因。
20. **4x4 top-right 可用性不能按几何位置判断**：块是**按 8x8 z-order** 重建的，同一行
   的 `bx`/`by` 与视觉位置并不对应。最终规则（`h264_decode.mbt`）：
   `by == 0` → 上方 MB 存在且（`bx < 3` 或右方 MB 存在）；`by > 0` →
   `bx == 0 || bx == 2 || (bx == 1 && by % 2 == 0)`，与 FFmpeg 的
   `topright_samples_available` 掩码 `0xEEEA`/`0x26EA` 一致。
21. **16x16 / chroma 8x8 平面预测的梯度 off-by-one**：C 的 `8+k`/`8-k` 应为 `7+k`/`7-k`、
   `4+k`/`4-k` 应为 `3+k`/`3-k`；写错不只是梯度偏移，底边会直接越界 panic。
22. **SPS 裁剪被缩放两次**：`h264_ps.mbt` 已按 `step_x`/`step_y` 把裁剪偏移折成**亮度
   像素**（`h264_ps.c:510-513`），`h264_slice.c` 的 `init_dimensions` 直接
   `width - (cr+cl)`；解码端再乘一次 2 就把 88x56 解成 80 宽。**只有非宏块对齐的分辨率
   能暴露**——64x48 之类整数倍分辨率的向量永远测不出来。
23. **多 slice 码流会「当整帧解」**：宏块循环按 `mb_count` 读完第一个 slice 后越界读零，
   于是静默产出垃圾像素（常在 `chroma prediction mode out of range` 之类的下游错误上
   才炸）。正确做法是解码前**预扫每个 slice NAL 的 `first_mb_in_slice`**，非 0 即拒绝。
24. **MoonBit 表达式优先级的两个坑（解码切片踩到）**：`cbp & 15 != 0` 会按
   `cbp & (15 != 0)` 解析，必须写 `(cbp & 15) != 0`；`Array::make(n, inner)` 会让 n 个
   元素**别名同一个内部数组**，逐宏块信息必须用循环逐个新建。
25. **调试解码器最省时的办法**：按宏块（或每行）分别算 luma MD5，与参考 YUV 的同区域
   比对，能一次把问题定位到单个宏块/单条预测路径，而不是对着整帧 MD5 猜。
26. **去块滤波的时序比算法本身更容易错**：FFmpeg 是「解完一行 → 滤这一行」，并且帧内
   预测读的是**滤波前**备份的上边界（`sl->top_borders`）。等价实现是「先整帧重建，再按
   宏块光栅序整帧滤波」。写成「每解一个宏块就立刻滤波」时，同一行里后面的宏块会拿已滤波
   的左邻做预测，重建值就变了——症状是整帧只差一两百个样本、且集中在 MB 边界附近。
27. **内部边是 bS=3（弱核），不是 bS=4**：`h264_loopfilter.c` 的 `filter_mb_dir` 只把
   **宏块边**的 bS 抬到 4，内部边保持 3 并用 `intra=0` 走弱滤波；误走强核后，凡是
   `|p0-q0| < (alpha>>2)+2` 的平坦边都会多改一排像素。
28. **`slice_alpha_c0_offset` 存的是 `div2 * 2`**：语法元素是 `slice_alpha_c0_offset_div2`
   （se(v)，-6..6），C 在 `h264_slice.c:1909` 乘 2 后存进 `sl->slice_alpha_c0_offset`。
   漏乘 2 时 `a`/`b` 偏移只有应有值的一半，`alpha`/`beta` 会偏小（漏滤），且只在这两个
   值非 0 的码流上暴露。
29. **`get_chroma_qp` 的偏移来自 PPS 而不是 0**：本次向量集的 PPS 里
   `chroma_qp_index_offset = -2`、`pic_init_qp_minus26 = -3`（手工解析 PPS 确认）。写参考
   模型时若默认偏移为 0，`alpha_table`/`beta_table` 下标会差 2，表现为 V 平面个别样本
   相差 2。审计这类常量时直接从码流解 PPS 比猜默认值可靠。
30. **验证脚本里最难发现的一类错**：拿「未滤波 YUV + 逐宏块滤波」当参考模型时，它**恰好**
   与 FFmpeg 默认解码逐字节一致——因为 FFmpeg 的预测也是读滤波前像素。这类模型能验出
   滤波核/强度推导的错，但验不出时序错，所以时序必须另有对照（整帧 MD5 + 逐 MB 区域哈希）。
31. **`fn f(self : T, ...)` 会被当成方法定义**：MoonBit 把首参名为 `self` 的 `fn` 解释成
   `T::f`（还给一条 `deprecated_syntax` 警告），于是从别的文件、甚至同文件的调用点都
   报 `unbound identifier`。**自由函数的第一参数不要叫 `self`**（本项目改用 `dec`/`ctx`）。
32. **Layer I 没有参考编码器**：FFmpeg 只有 `mp2`/`mp3` 编码器，Layer I 只能手工拼帧。
   做法是按规范写 header + 4 位分配 + 6 位 scalefactor + `n+1` 位尾数，**把期望值交给
   FFmpeg 解码**（`-c:a mp2`）；手工的必须是输入，绝不能是期望值。另外 mp2 demuxer 要求
   至少两个连续帧才能探测，所以一条向量至少拼 3 帧。
33. **Layer II 分配表是「行首字节 = 位宽，其余 = 该分配值对应的量化索引」**：遍历时
   `j += 1 << bit_alloc_bits`，`qindex = alloc_table[j + b]`；把 `j` 当成「子带序号」或
   漏掉 `1 << bits` 会从第二个子带起整体错位（症状：只有最低几个子带对，后面全错）。

---

## 8. 许可证

本目录是对 FFmpeg 的衍生移植，遵循 FFmpeg 的 **LGPL-2.1-or-later**（`moon.mod` 中已声明）。
各源文件头部保留了原始 C 文件的版权与作者信息（如 Michael Niedermayer、Ryan Martell 等）。

许可证正文随仓库提供两份，均为 GNU 官方文本（与 FFmpeg 仓库根的同名文件逐字节一致）：

| 文件 | 内容 | MD5 |
|---|---|---|
| [COPYING.LGPLv2.1](COPYING.LGPLv2.1) | GNU Lesser General Public License, Version 2.1, February 1999（502 行） | `eed22b3456132611e3d4aa7a7ec64dac` |
| [COPYING.LGPLv3](COPYING.LGPLv3) | GNU Lesser General Public License, Version 3, 29 June 2007（165 行） | `e6a600fd5e1d9cbde2d983680233ad02` |

之所以两份都放：FFmpeg 是 **LGPL-2.1-or-later**，被许可人可以按 v2.1 或**任一更晚版本**
（含 v3）使用。注意 LGPLv3 **不是完整条款**——它是在 GNU GPLv3 之上追加许可的短文本，
引用前须一并提供 GPLv3 正文（FFmpeg 发行包根目录的 `COPYING.GPLv3`，本项目未内置）。
本项目自身的选则是 **v2.1**（见 `moon.mod` 的 `license` 字段），保留 v3 文本只为让下游
可以按 v3 条款再分发。

## 9. 2026-10-23 H.264 冗余 slice 与范围取舍

`h264_slice.mbt` 保存 `redundant_pic_cnt`；`h264_dpb.mbt` 在读完 slice header 后丢弃冗余 slice，不让它替代主图或更新 DPB。合成 I_PCM 主图验证冗余 slice 前置、后置时都只输出主图。

SP/SI 像素重建明确不纳入项目范围；真实 SP switching 流与合成 SI 流现在都要求返回未支持错误，避免通用 P/I 路径偶然输出错误像素。FFmpeg 9.0.2 自身未解码的数据分区和 FMO 维持未支持边界；本移植对数据分区 NAL 明确报错。

损坏码流的错误恢复当时仍未移植（随后在第 10、11 节补齐渐进帧和 MBAFF）。此次变更后 `moon check --target wasm --deny-warn --no-render` 通过，`moon test --target wasm --no-render` 为 **770/770**。

## 10. 2026-10-23 H.264 渐进帧错误补偿

持久解码器新增可选的 `set_error_concealment(true)`。渐进帧允许缺失首段、中段或尾段 slice；切片内部解码失败时保留已完成的宏块并补偿剩余区域。帧的 `damaged_macroblocks` 记录补偿数量，恢复后的参考帧照常进入 DPB。严格模式仍可在补齐 slice 后重试同一帧。

空间补偿从四个方向的有效宏块估计每个 8×8 luma 和色度子块的 DC；时间补偿从首个 L0 参考帧选取零矢量和相邻宏块矢量，以 16×16 边界差选择候选。实现位于 `h264_error_resilience.mbt`，时序保持“正常 slice 去块滤波之后补偿”，与 FFmpeg 的 `ff_er_frame_end` 一致。回归覆盖缺失中段、末段、首段和截断 CABAC P slice；完整 Wasm 测试为 **772/772**。

这一版仍不是逐像素移植：多轮运动矢量猜测、恢复区专用去块滤波和损坏参考帧的输出策略在下一节补齐；损坏的 MBAFF 帧及场图片当时仍报错。

## 11. 2026-10-23 H.264 MBAFF 错误补偿与场图片边界

MBAFF 损坏帧现在可以补偿。关键在于 `h264_slice_init` 对 `first_mb_in_slice` 做了 `<< FIELD_OR_MBAFF_PICTURE`：MBAFF 帧按宏块**对**编址，换算出 raster 起点后，`ff_er_frame_end` 全程在普通的 `mb_width × mb_height` 网格上工作——上游 `error_resilience.c` 没有任何 MBAFF 分支，`er->mb_height` 仍是宏块行数。因此只需在 `h264_er_raster_start` 里把 pair 地址展开，其余沿用渐进帧逻辑。补偿出的宏块统一按帧宏块重建（`h264_er_decode_mb` 把 `mb_field_decoding_flag` 清 0），`h264_conceal_missing_mbs` 同步清 `field_mb`。

MBAFF 回归用 96×64、6×2 宏块对、每帧两 slice 的流，丢弃 12 个变体（覆盖 I/P/B、前半/后半 slice、损坏参考帧传播），**12/12 逐帧 YUV MD5 与 Docker FFmpeg 9.0.2 一致**。丢 IDR 后半 slice 时上游记录 `concealing 12 DC, 12 AC, 12 MV`，移植侧的 `damaged_macroblocks` 同样是 12。

**场图片不做恢复，这是上游边界而非移植缺失**：`h264_slice.c` 设置 `pic->field_picture = picture_structure != PICT_FRAME`，`er_supported()` 见到它就返回 0，`ff_er_frame_end()` 开头即 `return`，源码里还留着 "We do not support ER of field pictures yet"。实测 PAFF 多 slice 流丢弃一个 slice 后，上游既没有 `concealing` 也没有 `corrupt decoded frame` 日志，输出的是缺失宏块保持缓冲旧值的帧，无法复现。移植侧因此继续明确报 `incomplete H.264 picture`。

同时修掉一个 Annex B 分组缺陷：原先用 `first_mb_in_slice == 0` 判断新图，丢失某帧首 slice 后该帧剩余 slice 会被粘到下一帧，触发 `inconsistent H.264 picture slices`。现在按 slice header 的图标识（`frame_num`/`poc_lsb`/`idr_pic_id`/`field` 标志/`nal_type`/`ref_idc` 等，与 `h264_decode_picture_ex` 的一致性检查同一组字段）比较，`h264_same_picture` 负责判定，解析不出 header 时退回原判据。

新增回归：渐进流缺失首段/中段/连续两段/截断末段 4 个变体逐帧对齐上游；10-bit 4:2:0、4:4:4、4:2:2 的多 slice 流在开关补偿两种模式下均逐帧一致。`put_dc` 的 DC 钳位补上了上游的 `dc > 2040 → 2040`（不随位深缩放，10-bit 因此封顶 255）。

**已知偏差**：P 帧大缺口场景仍未逐像素对齐。`trunc_p_mid`（P 帧中间 slice 截断）最终只剩 V 平面 2 个像素差 1–2，Y/U 整帧一致；`two_split`（参考帧本身被补偿过）与丢整段 slice 的 10-bit/4:4:4/4:2:2 场景差异更大，Y 平面即不同，源于多轮 `guess_mv` 与 DC 估计的取值分歧。这些场景不纳入断言，避免把未对齐的行为固化。

对 `trunc_p_mid` 做过四轮定位，已确认与已排除如下（供后续接手，避免重走）：

- **恢复本身完全正确**。关掉两侧滤波后，恢复宏块 mb16 的 V 区域（x32..39, y16..31）与上游 `-ec 1 -skip_loop_filter all` 逐字节相同；色度 MC 手算校验一致（`mv=(8,-15)` → `dx=0, dy=1` → `(7a+c+4)>>3`）。
- **FFmpeg 的 `-ec` 是位掩码**（`FF_EC_DEBLOCK = 2`）。`-ec 1` 配合 `-skip_loop_filter all` 可以取到"恢复后 / 两种滤波前"的四种中间状态，与移植侧 `skip_lf × er_deblock` 四组合逐一对比——这是这类问题最有效的手段。
- **已排除**：weighted prediction（补上 `h264_weight_rect` 后输出零变化，这些帧 `use_weight == 0`）、bS 取值（1→2 无变化）、QP（恢复宏块 QP 强制 1..40 全无变化）、slice 的 alpha/beta offset（都是 0）、恢复宏块 `deblock_idc`（1→0）、`cstride` 与 `cbase` 坐标（实测 `p=1186 stride=48 len=1920` 正确）、`tc0` 表越界（表长 624，索引有效）。
- **实测参数**：mb22 上边界的判决为 `bs=[2,2,2,2]`、`cq0=20` → `index_a=72` → `alpha=7 beta=3 tc=1`。这条边界上 V 平面 x=34 的 `|p0-q0|=6 < 7` 本应通过，但日志里 `p0` 全是 225~228 这类高值，**V 平面那次调用没有出现在日志中**，说明它与 U 平面共用 `coff`、无法从 `start` 区分，且 V 侧可能提前返回。
- **遗留线索**：在 `h264_filter_chroma_edge` 开头的 `if alpha == 0 || beta == 0 { return }` 插桩，并在 `h264_filter_mb` 调用处用 `data == ctx.v` 区分平面，可以判定 V 是否提前返回。

清理：删除 `scripts/compare_h264_er.mbtx`（脚本模式解析不了本地包导入）、`scripts/inspect_h264_er_reference.mbtx`、`h264_picture_wbtest.mbt` 里的 `moon sums` 调试块，以及已成死代码的单次 `h264_er_guess_mv`（B 帧恒用零矢量，P 帧在 `!picture_mv_done` 分支不可达）。`filter181` 的 `value * 10923` 经核算在 14-bit 最坏值 1.4e9，仍在 Int32 内，与上游 32 位求值一致，加了说明注释。

`moon check --target wasm --deny-warn --no-render` 通过，`moon test --target wasm --no-render` 为 **783/783**。

## 12. 2026-10-23 VP9 帧间帧（inter）解码

补齐 profile-0 帧间路径：`decode_mode` 的 inter 分支（is_inter/comp/参考选择/四种 MV 模式/switchable filter，`vp9_inter_block.mbt`）、`find_ref_mvs` 候选与 sub8x8 直接 MV（`vp9_mvs.mbt`）、非缩放运动补偿与 8tap/sixtap/bilinear 亚像素滤波（`vp9_inter_recon.mbt`）、概率模型 `ff_vp9_adapt_probs` 与 parallelmode 提交（`vp9_probs.mbt`）、inter 概率常量表（`vp9_inter_tables.mbt`）。

验收向量：`testsrc2` 160×120、libvpx-vp9、单参考无 altref 的 IVF（关键帧 + 14 个 P 帧，`-g 6`），**15 帧逐帧 YUV MD5 与 Docker `linuxserver/ffmpeg:9.0.2` 一致**（`vp9_inter_realdata_wbtest.mbt`）。另与插桩 C 参考解码器做了全轨迹对拍：15 帧的块入口（RAC 状态）、skip、系数 STEP 与块级 IMD（模式/MV/滤波器）逐行一致后才比像素。

本轮修掉两个真 bug，定位方法值得复用：

- **commit 的槽步长**：工作表 `prob_coef` 每槽 11 个值（3 真概率 + 8 pareto 尾），上下文表每槽 3 个；parallelmode 下从工作表拷回上下文表时源偏移按 3 计算，读到了别的槽（144 恰为 band0/ctx2/pos2 的默认值），导致帧 1 起系数概率整体错位、约半数像素 ±1~2 偏差。首个分歧 STEP 的 `tp0`（C=14 / MBT=144）配合默认表比对直接锁定。
- **帧级 filter 顺序**：帧头 `interp_filter` 原码是 **0=smooth,1=regular,2=sharp,3=bilinear**（C 直接当 FilterMode 枚举消费），switchable 树码才是 0=regular、1=smooth（C 经 `ff_vp9_filter_lut` 转换）；non-switchable 帧直接透传原码会选错 tap。

对拍中的两个假分歧也记录在案：C 的 `VP9mv` 是 int16 结构体，printf 传整个 struct 会打出打包的 32 位值（如 -1048576 实为 (0,-16)），必须取 `.x/.y`；两侧 filter 枚举顺序不同（C 0=smooth/1=regular，本移植 0=regular/1=smooth）时 diff 脚本要先做 0↔1 归一化。

边界：复合预测（B 帧）、多参考与参考缩放（缩放明确报错）、profile 1/2 未覆盖；`ff_vp9_adapt_probs` 的计数驱动路径已移植，但现有真实流全部是 parallelmode（走 commit 路径），该分支尚未被真实向量验证。调试插桩（IMD/BLK/SKIP/HDR/ADAPT 打印、`vp9_dbg` 字段、临时 trace 测试与脚本）已全部清理，`scripts/extract_vp9_tables.py --check` 复核通过。

`moon check --no-render` 0 error/0 warning，`moon test` **784/784**。

### 2026-10-23 补：VP9 计数驱动概率自适应与多参考验证

用 `-frame-parallel 0` 生成的 160×120 流（关键帧 + 20 个 inter 帧）补齐上一节的验证缺口：21 帧全部 `parallelmode=0`（走 `ff_vp9_adapt_probs` 计数驱动路径而非 parallelmode commit），且三路参考（last/gold/altref）都有块使用。21 帧逐帧 YUV MD5 与 Docker FFmpeg 9.0.2 一致（`vp9_np_realdata_wbtest.mbt`，生成脚本 `scripts/_emit_vp9_np_vectors.py`）。

对拍又抓到一个真 bug：`adapt_prob` 的合成公式 C 侧是 `p1 + (((p2 - p1) * uf + 128) >> 8)`，算术右移对负调整量向负无穷取整；移植初版用 `/ 256`（向零截断），负值时该退位的概率不退位，帧 1 起自适应概率逐槽 +1 漂移（首个分歧 NZ 行的 pareto 尾概率差 1 直接可见）。改为 `>> 8` 后 21 帧语法轨迹（含每系数 NZ 行）与 C 完全一致。

FFmpeg 的 libvpx-vp9 封装不支持编码 B 帧（`libvpxenc.c` 无 max_b_frames 传递），复合预测无法用该编码器生成向量，维持未验证；参考缩放与 profile 1/2 仍是明确边界。

`moon check --no-render` 0 error/0 warning，`moon test` **785/785**。

### 2026-10-23 补：VP9 profile 2（10/12-bit）

在 8-bit inter 之上补齐 profile 2 的 10/12-bit 路径。C 侧 16bpp 与 8bpp 的差异经逐项核对为：`dctcoef`/`dctint` 变宽（int16→int32 存储、int64 蝶形）、q 表按 bpp 行选（`ff_vp9_{dc,ac}_qlookup[bpp_index]`，表早已提取只是索引漏了行偏移）、cat6 系数在 12-bit 多读 4 个高位概率位、LF 的 E/I/H/F 按 `<<(bpp-8)` 缩放且 clip_intp2 位宽为 bpp-1、DC 边缘常量 127/129/128 按 `<<(bpp-8)` 缩放、MC 的 clip 上限放开、输出按 yuv420p10le/12le 打包 LE16。实现方式是把全部 1D 变换核统一到 Int64 蝶形（对 8-bit 有界输入逐位等价，已由 785 帧回归确认）、clip/LF/预测入口带 bpp/pixel_max 参数。

本轮对拍修掉四个真 bug，前两个是移植错、后两个是一次脚本化重写引入又靠真值对拍抓回：

- **av_clip_intp2 的通带**：定义是裁到 `[-2^p, 2^p-1]`（p=BIT_DEPTH-1），首版误写成 `1<<(bits-1)`，8-bit 回归恰好只差 19 像素、由旧新核随机 A/B（2000 组 1 组分歧）定位。
- **iadst16 两处正则重写损伤**：`-(t2a+t3a)*11585` 被改写成 `-(t2a3 + t3a3*11585)`（乘法结合错对象）、`-((8192+sum)>>14)` 的负号被移进移位里。8-bit 向量覆盖不到这些分支（单系数稀疏 TU 才触发），靠「C 模板编成 ktest.c 真值发生器 + MBT 同 LCG 序列 A/B」逐核抓出：七个核各 3000 组随机输入全对齐后，三条 10/12-bit 真实流（45 帧）MD5 全部收敛。
- **系数存储截断**：`decode_coeffs_b` 的存入在 16bpp 下不得做 int16 截断。
- **inter 帧 header 继承**：C 的帧头是持久结构体，inter 帧不重读 coloursspace；本实现的逐帧默认 header 需要把 profile/bit_depth/subsampling 从上一帧带过来。

新增三条真实向量（`vp9_10p/10np/12p_realdata_wbtest.mbt`，生成脚本 `scripts/_emit_vp9_p2_vectors.py`）：10-bit 关键帧+P、10-bit 计数驱动自适应、12-bit，共 45 帧逐帧 MD5 与 Docker FFmpeg 一致。核 A/B 固化为常驻测试 `vp9_kernels_wbtest.mbt`（每核 3000 组 LCG 输入的输出 MD5 对 C 真值），覆盖 8-bit 流触不到的分支。

对拍技巧沉淀：LF 探针按 (E,I,wd) 值组合过滤即可在 C/MBT 两侧对齐同一批调用而不必打印坐标；`String::to_bytes()` 是 **UTF-16LE** 不是 UTF-8，喂 MD5 前须逐 code unit 取低字节。
## 13. 2026-10-24 Opus SILK/混合模式与包级解码

**范围**：Opus 解码器补全 LP 层——`opus_silk.mbt`（silk.c 全量移植：NLSF 稳定化、RFC 8251
LPC 稳定性检查、LSF→LPC 带宽扩展、激励 PVQ/LSB 展开/符号建模、LTP/LPC 合成、mid-only
边声道、立体声 unmix）、`opus_silk_tables.mbt`（tab.c 63 张表，`scripts/extract_opus_silk_tables.mbtx`
批量提取 + `--check` 核对）、`opus_resample.mbt`（libswresample 浮点多相重采样器在
dec.c 配置下的专用移植：filter_size 16、Kaiser β9、exact_rational 精确有理比、
invert_initial_buffer/resample_flush/padless=7 全套状态机；SILK 8/12/16 kHz→48 kHz 全部
走 frac≡0 的 common 核）、`opus_decode.mbt`（dec.c 包级编排：三模式分派、SILK 流的
CELT redundancy 交叉淡化、hybrid 的 celt_delay FIFO、多流 sync FIFO、增益、flush/drain）。
CELT 侧为此增加 `decode_frame_rc`（复用外部 rc + start_band 17 的 hybrid 入口）与
`coefficients_log` 诊断钩子。

**验证**（两层标准，均为 asm 禁用构建的 FFmpeg 9.0.2——x86 swr/float_dsp 内核带 FMA，
位模式与标量不同）：
- SILK-only 七条真实流（NB/MB/WB × 10/20/40/60 ms、CBR code-3 lacing、FEC/LBRR、立体声、
  mid-only）与原生解码器 **framemd5 逐帧位级一致**（含 pre-skip、Ogg granule 末端裁剪、
  resampler drain 帧），向量内嵌于 `opus_silk_realdata_wbtest.mbt`。
- hybrid 两条流（mono/stereo FB）：去归一化 CELT 系数经插桩 dec_celt.c 逐帧 dump，
  **位级 MD5 一致**（语法/能量/分配/PVQ 全链路）；合成阶段（naive IMDCT vs AVTX）沿用
  既有 CELT 3e-6 容差标准。排障路径：熵消耗 tell 全帧一致 → 系数一致 → IMDCT 输入一致
  输出差 ulp → 定位为合成层。

**排障沉淀**：`silk_lsf2lpc` 的 maxabs 扫描读 `lpc32[k]`（上一 argmax）而非 `lpc32[j]`，
maxabs 实际只跟踪 |lpc32[0]|；Q12 赋值的 int16 截断在病态 NLSF 下依赖回绕；编排层
cur_out 必须整帧完成后推进。向量生成与 C 参考构建命令见 `scripts/emit_opus_silk_vectors.py`
文件头与 agent.md §7。


`moon check --no-render` 0 error/0 warning，`moon test` **789/789**。

## AV1 无损关键帧像素解码

以 dav1d 1.5.1 的 `decode.c`、`recon_tmpl.c`、`ipred_tmpl.c` 和 `itx_tmpl.c` 为对照，
完成 8-bit 4:2:0、单 tile、无损关键帧的分区、熵解码、palette、预测、WHT 逆变换和
YUV 输出。参考源码仅用于独立对拍，运行时不依赖 C FFI。

`av1_kf_realdata_wbtest.mbt` 内嵌 libaom 编码的 128×96 单帧及 160×120 六帧码流；
七帧的逐帧 YUV MD5 与 libdav1d 一致。定位时逐块比对 dav1d 的符号解码 range，并
逐行比对原始 YUV。关键修正包括系数 CDF 步长与邻居上下文地址、零 DC 时继续读取
AC 符号、palette 色度左侧缓存的行索引、partition 上下文覆盖长度，以及 8-bit
路径的 32767 系数上限。

当前像素入口对有损帧、帧间帧、多 tile、segmentation、delta Q、qmatrix 和 superres
明确报错；环路滤波、帧间预测及其他像素格式仍待移植。本轮验证：
`moon check --deny-warn --no-render` 0 error/0 warning，`moon test --no-render` **792/792**。

## 14. 2026-10-24 VP9 profile 1/3（4:2:2 / 4:4:4）

在既有 4:2:0 路径上补齐 profile 1（8-bit 4:2:2/4:4:4）与 profile 3（10/12-bit
4:2:2/4:4:4）。解码头解析早已读取 `subsampling_x/y`，缺口集中在像素路径：

- **色度运动补偿**：C 的 `mc_chroma_unscaled` 先把 1/8-pel 亮度 MV 缩放为
  `mv * (1 << (1 - ss_h/v))`，再按 1/16-pel 取整/取相位；移植此前直接使用亮度 MV，
  只对 4:2:0 成立。同步把边界判断的参考色度尺寸改为 `(w + ss_h) >> ss_h`。
- **4:4:4 色度系数缓冲**：C 按 `64*64 >> (ss_h + ss_v)` 分配色度块与 EOB 缓冲；
  4:4:4 的 64×64 块可有四个 32×32 色度 TU，需要 4096 系数与 256 个 EOB。移植的
  1024/64 仅够 4:2:0，12-bit 4:4:4 在 `uveob[n]` 越界，按最大尺寸统一分配。
- **输出打包**：`frame_bytes` 按当前帧的 ss 计算色度宽高，覆盖 yuv422p 与 yuv444p
  （含 10/12-bit LE16），不再硬编码 4:2:0。
- **inter 帧头继承顺序**：profile 1/3 的 inter 帧不重读色彩配置，必须先把上一帧的
  profile/bit-depth/subsampling 带入当前帧，再做“profile 0/2 必须 4:2:0、profile
  1/3 必须 4:2:2/4:4:4”的合法性检查。

验证向量位于独立测试包 `libavcodec/vp9_vectors`（避免主包 wasm `__init` 的局部变量
上限），由 `scripts/_emit_vp9_444_vectors.py` 生成：8-bit 4:4:4、8-bit 4:2:2、
10-bit 4:4:4、12-bit 4:4:4 四条 libvpx-vp9 流，各 6 帧（两个关键帧 + P 帧），
24 帧逐帧 YUV MD5 与 Docker `linuxserver/ffmpeg:9.0.2` 一致。仍余：复合预测（B 帧）、
参考缩放，以及 profile 3 的 4:2:2 组合未单独出向量。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **796/796**。

## 15. 2026-10-24 AV1 无损关键帧 4:2:2 / 4:4:4

AV1 像素路径原先按 8-bit 4:2:0 写死。序列头解析早已读取
`chroma_subsampling_x/y`，本轮把像素侧也参数化：

- `Av1FrameCtx` 保存 `ss_h/ss_v`，`decode_b` 与 `recon_b_intra` 的
  `cbx4/cby4/cbw4/cbh4/has_chroma` 全部改用实际采样；色度边缘可用性标志按
  `I444_FLAG << (ss_h + ss_v)` 选择（与 dav1d 的
  `EDGE_I420_* >> (layout - 1)` 等价）。
- 修正**色度 skip 上下文**的 `not_one_blk`：C 用
  `b_dim[2] - (!!b_dim[2] && ss_hor)`，移植此前把两个采样位都写死为 1。
  这个分歧会让 4:4:4/4:2:2 的 `coef.skip` CDF 上下文错位，轻则像素偏差、
  重则熵解码越界；修正后基础 4:4:4 流即逐帧对齐。
- `Av1FrameOutput::packed()` 按 `ss_h/ss_v` 打包 Y/Cb/Cr，4:2:2 为半宽全高、
  4:4:4 为全尺寸色度；`Av1Tile::data()` 暴露 tile 熵负载供跨包测试使用。
- 新增独立回归包 `libavcodec/av1_vectors`（避免主包 wasm `__init` 局部变量上限），
  由 `scripts/_emit_av1_444_vectors.py` 生成两条 libaom `-crf 0 -g 1` 流：
  64×48 yuv444p 与 yuv422p，各 9 帧，**18 帧逐帧 MD5 与 libdav1d 一致**。

**已知边界**：`enable_intra_edge_filter=0` 的流当时仍与上游不一致；用同参数生成的
4:2:0 流同样复现，确认是本轮之前就存在的 AV1 预测边滤波缺口，不是 4:4:4 引入
（已在 §17 修复）。有损帧、帧间帧与 10/12-bit 仍明确报错（单色见 §18）。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **798/798**。

## 16. 2026-10-24 AV1 多 tile 无损关键帧

多 tile 解码的关键是让每个 tile 成为独立的熵解码单元，同时共享帧级像素平面：

- `av1_decode_keyframe_tiles()` 接收 `av1_extract_tiles` 的全部 tile；核心驱动按
  `tile_cols/tile_rows` 推导每个 tile 的 SB 行列范围，并为每个 tile 新建
  `Av1Msac` 与 `av1_cdf_copy_defaults()`，避免 tile 间概率状态串联。
- above 上下文按 tile row 分 bank（`sb128w * tile_rows`），起始索引加上
  `col_start >> 5`；left 上下文在每行 SB 起始处重置。这样 tile 边界两侧不会互相
  读取预测/系数上下文。
- palette 的 above/left 缓存与 `pal_sz_uv` 从帧级共享改为**每 tile 独立**，并在
  每个 SB 行重置左侧缓存；此前多 tile 会在 tile 边界串用上一 tile 的 palette
  上下文，导致 `pal_sz`/`use_pal` 语法错位并最终熵解码越界。
- 像素写入仍共用整帧平面；tile 边界只影响上下文与预测可用性，不影响最终拼图。

验证：`libavcodec/av1_tiles_vectors` 内嵌两条 libaom 无损流（128×64 双 tile
列、128×128 2×2 tile），各 3 帧，**6 帧逐帧 MD5 与 libdav1d 一致**（生成脚本
`scripts/_emit_av1_tiles_vectors.py`）。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **800/800**。

## 17. 2026-10-24 AV1 无帧内边滤波（Z2）修复

上一节登记的 `enable_intra_edge_filter=0` 缺口定位到 `ipred_z2` 的无滤波左侧
边缘拷贝：dav1d 的 `pixel_copy(&topleft[-height], &topleft_in[-height], height)`
要求写入 `topleft-height .. topleft-1`，移植写成了 `topleft .. topleft-height+1`，
整体偏移一位且丢失 `topleft-height`。滤波/上采样路径不经过该分支，因此此前所有
默认流都能通过；关闭 edge filter 后 Z2 预测块开始出现偏差。

修正为按 `topleft - height + i` / `topleft_in - height + i` 顺序拷贝。新增
`av1_420_noedge` 真实流（64×48、关闭 palette/CFL/filter-intra/edge-filter，3 帧），
连同原有 4:4:4/4:2:2 两流共 **21 帧**逐帧 MD5 与 libdav1d 一致。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **801/801**。

## 18. 2026-10-24 AV1 单色无损关键帧

AV1 monochrome 不再在入口拒绝：

- `decode_b` 与 `recon_b_intra` 的 `has_chroma` 增加 `!seq.monochrome` 条件，
  所有 UV 模式、palette、色度 CBF/变换与色度预测语法按规范跳过。
- `Av1FrameOutput` 记录 `mono`，`packed()` 在单色时只输出 Y 平面，匹配
  ffmpeg 的 `gray` 帧布局。
- 新增 64×48 8-bit 灰度 libaom 无损流（3 帧），与 4:4:4/4:2:2/noedge 三条流
  合计 **24 帧逐帧 MD5 与 libdav1d 一致**。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **802/802**。

## 19. 2026-10-24 VP9 profile 3 4:2:2 与 4:4:0 覆盖

VP9 的色度路径在第 14/15 节已按 `ss_h/ss_v` 通用化，但此前只验证过
4:2:0、8-bit 4:2:2 与 10/12-bit 4:4:4。本轮补齐四个真实流组合：

- profile 3：10-bit 4:2:2、12-bit 4:2:2；
- profile 1：8-bit 4:4:0；
- profile 3：10-bit 4:4:0。

四条流均为 64×48、6 帧（关键帧 + P 帧），色度 MC、4:4:0 的半高色度平面、
输出打包与循环滤波均直接复用现有通用实现，**无需改动解码代码**；
24 帧逐帧 YUV MD5 与 Docker FFmpeg 9.0.2 一致。VP9 4:4:4/4:2:2/4:4:0
回归现共 8 条流、48 帧。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **806/806**。

## 20. 2026-10-24 WAV PCM 写入器

新增 `WavMuxer`（`libavformat/wav_mux.mbt`），按 FFmpeg `wavenc.c` 的
RIFF/WAVE 布局写出 16/24/32-bit 有符号 PCM：

- `wav_muxer_new` 校验采样率、声道数与位深；8-bit（WAV 的无符号 PCM）明确拒绝。
- `write_samples` 接收交错 `Int32` 样本并按小端写入低 `bits` 位；
  `finish` 生成 `RIFF`/`fmt `/`data`、计算 `block_align`/`byte_rate`，
  奇数长度 data 自动补 pad 字节。
- 回归将写出的字节重新交给现有 `av_wav_open`，验证格式字段、数据偏移/长度、
  样本计数、16-bit 立体声与 24-bit 单声道奇数长度补位。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **809/809**。

## 21. 2026-10-24 MPEG-TS 写入器

新增 `TsMuxer`（`libavformat/ts_mux.mbt`），按 `mpegtsenc.c` 的静态表与
PES 布局写出单节目 TS：

- PAT（PID 0）与 PMT（PID 0x1000）用 MPEG-2 CRC32（poly `0x04C11DB7`、
  init `0xFFFFFFFF`）生成，PMT 声明 H.264 `0x1B` 与 AAC ADTS `0x0F`。
- PES 支持 PTS-only 头；视频 PES 使用无界长度、音频使用有界长度。
- 视频 PES 首包写入 33-bit PCR（90 kHz，扩展位 0），采用
  adaptation field + PCR_flag，并相应把该包净荷缩到 176 字节。
- 188 字节打包：首包 PUSI、按 PID 维护 4-bit continuity counter；不足 184
  字节时用 adaptation field stuffing 补齐。
- 大负载自动跨多个 TS 包；测试用现有 `av_mpegts_open` 回读，验证
  stream PID/codec、PES 负载、PTS、PCR 与多包拆分。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **811/811**。

## 22. 2026-10-24 ADTS AAC 写入器

`libavcodec/audio_headers.mbt` 新增 `aac_adts_header_write`：按 ADTS 54-bit
fixed/variable header 生成 7 字节无 CRC 头（MPEG-4、AAC object type、
sampling_frequency_index、channel_configuration、13-bit frame_length、
buffer_fullness 全 1）。

`libavformat/adts_mux.mbt` 的 `AdtsMuxer` 将每个 AAC 原始帧加头后累积；
`adts_muxer_new(rate, channels, object_type=2)` 校验参数，`write_frame`
限制帧长不超过 8191，`finish` 返回完整 ADTS 流。回归把输出交给现有
`av_adts_open`，验证 codec/profile/sample rate/channels、两帧长度与
`total_samples == 2048`，并覆盖不支持采样率与超长帧的负例。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **813/813**。

## 23. 2026-10-24 Ogg/Opus demuxer

新增 `libavformat/ogg.mbt`，支持单个 Opus 逻辑流的常见 `.opus`/`.ogg` 文件：

- Ogg 页解析：`OggS` 捕获、granule、serial、segment table 与 lacing
  重组（含跨页 packet）；按页 CRC-32/MPEG-2（非反射、init 0）校验。
- 识别 `OpusHead`/`OpusTags`，保存 OpusHead 为 extradata，输出 48 kHz、
  pre-skip 与声道数。
- 数据包用 `opus_parse_packet` 得到 TOC 时长；PTS 按
  `cumulative - pre_skip` 计算，并用页 granule 修正末包 duration。
- 真实 libopus 流（48 kHz 单声道、0.2 s、64 kb/s）回归：11 个包的大小、
  PTS、duration 与 ffprobe 一致，包负载 MD5 全部对齐；损坏页 CRC 负例通过。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **815/815**。

## 24. 2026-10-24 Ogg/Opus 写入器

`libavformat/ogg_mux.mbt` 新增 `OggOpusMuxer`：

- `ogg_opus_muxer_new` 支持 1/2 声道、pre-skip、输入采样率与 serial。
- 首包前写 BOS 的 `OpusHead` 与 `OpusTags`；随后每个 Opus 包一页，
  granule 为累计 48 kHz 样本数，末页置 EOS。
- 每页按 Ogg lacing（255 续段）编码，并用非反射 MPEG-2 CRC32 回填校验和。
- 回归将写出的 Ogg 交给 `av_ogg_opus_open`：3 个真实 Opus 包的
  payload、PTS `[-312, 648, 1608]`、duration 960、pre-skip 与 extradata
  全部一致；空包负例通过。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **817/817**。

## 25. 2026-10-24 WAV 8-bit 与 WAVE_FORMAT_EXTENSIBLE

`WavMuxer` 扩展：

- 支持 8-bit PCM，按 WAV 约定把有符号输入偏移到无符号字节（`pcm_u8`）。
- `wav_muxer_new` 新增 `channel_mask`；声道数 >2 或 mask 非零时写
  `WAVE_FORMAT_EXTENSIBLE`（fmt chunk 40 字节、valid_bits、channel mask 与
  KSDATAFORMAT_SUBTYPE_PCM GUID）。
- 回归覆盖 8-bit 单声道字节值、6 声道/mask 0x3F 的 extensible 头与
  `av_wav_open` 回读（codec、声道、位深、channel_mask、data 大小）。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **818/818**。

## 26. 2026-10-24 AAC-in-MP4 写入器

新增 `libavformat/mp4_mux.mbt`，写出最小非分片 MP4：

- `ftyp` + `mdat` + `moov`；`mvhd`/`tkhd`/`mdhd`/`hdlr`/`smhd`/`dinf`
  与音频 `stbl`（`stsd`/`stts`/`stsc`/`stsz`/`stco`）。
- `mp4a` AudioSampleEntry + `esds`（ES/DecoderConfig/DecoderSpecific/SL
  descriptors），DecoderSpecificInfo 为 AAC-LC 的 2 字节 AudioSpecificConfig。
- 支持任意 AAC 采样率表中的采样率与 1–7 声道、逐包 duration；`stts`
  自动做 run-length 压缩，单个 chunk 记录全部样本。
- `libavformat/mp4_mux_wbtest.mbt` 用 `av_mov_open` 回读：track
  `mp4a`/`aac`、48 kHz/双声道、ASC、3 个样本的 offset/size/dts/duration 与
  payload 全部一致；非法采样率与空包负例通过。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **820/820**。

## 27. 2026-10-24 H.264-in-MP4 写入器

新增 `libavformat/mp4_video_mux.mbt`：

- 调用方提供 `avcC` 与长度前缀访问单元；写 `avc1` VisualSampleEntry、
  `avcC`、`vmhd`/`dinf` 与视频 `stbl`。
- 支持时间基、逐包 duration、关键帧标记；`stss` 只列出关键帧，
  `stts` 做 run-length，`tkhd` 宽高按 16.16 写入。
- 回归用真实 44 字节 High-profile avcC，`av_mov_open` 回读验证
  `avc1`、64×48、时间基、extradata、3 个样本的 size/dts/duration/
  keyframe 全部一致；非法参数与空包负例通过。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **822/822**。

## 28. 2026-10-24 Opus-in-MP4 写入器

新增 `libavformat/mp4_opus_mux.mbt`：写 `Opus` AudioSampleEntry + `dOps`
（version/channels/pre-skip/input rate/gain/mapping family 按大端存放），
复用 MP4 的 `mvhd`/`trak`/`stbl` 样本表布局。逐包 duration 从
`opus_parse_packet` 的 TOC 得到，`stts` 自动 run-length。

回归用真实 20 ms Opus 包写 3 帧，经 `av_mov_open` 回读：track
`Opus`、48 kHz、单声道、19 字节 OpusHead extradata，3 个样本的
size/dts/duration 与 payload 全部一致。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **823/823**。

## 29. 2026-10-24 AV1-in-MP4 写入器

新增 `libavformat/mp4_av1_mux.mbt`：写 `av01` VisualSampleEntry + `av1C`
（调用方提供配置记录）、`vmhd`/`dinf` 与视频样本表。逐包 duration、
关键帧 `stss`、`stts` run-length 与 H.264 写入器一致。

回归用 4 字节 av1C 写 3 个样本，`av_mov_open` 回读验证 `av01`、64×48、
extradata、size/dts/duration/keyframe 全部一致；非法参数与空包负例通过。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **825/825**。

## 30. 2026-10-24 WAV IEEE float32 写入

`WavMuxer` 新增 `float=true` 模式：

- 仅允许 32-bit；`write_float_samples` 按 IEEE-754 位模式写小端。
- `fmt` 使用 format tag 3（IEEE float）；extensible 时 GUID 首 4 字节
  改为 0x00000003。
- 回归写 `[0.5, -0.5]` 双声道，`av_wav_open` 回读为 `pcm_f32le`，
  位模式 `00 00 00 3F / 00 00 00 BF` 与 data 大小一致；16-bit float 负例报错。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **826/826**。

## 31. 2026-10-24 HEVC-in-MP4 写入器

新增 `libavformat/mp4_hevc_mux.mbt`：写 `hvc1` VisualSampleEntry + `hvcC`
与视频样本表。回归使用真实 108 字节 Main-profile hvcC，`av_mov_open`
验证 `hvc1`、64×48、extradata、两个样本的 dts/duration/keyframe。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **827/827**。

## 32. 2026-10-24 VP9-in-MP4 写入器

新增 `libavformat/mp4_vp9_mux.mbt`：写 `vp09` VisualSampleEntry + `vpcC`
FullBox 与视频样本表，支持关键帧、逐包 duration、`stts` run-length。

回归写 12 字节 vpcC（version 1、8-bit 4:2:0）与 2 个样本，`av_mov_open`
回读验证 `vp09`、64×48、样本 dts/duration/keyframe。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **828/828**。

## 33. 2026-10-24 MP4 `vpcC` 解析

`mov_read_vpcc` 之前未移植，VP9-in-MP4 的 `vpcC` 被当作未知 box 跳过。新增
`parse_vpcc`：从 FullBox 读取 version/flags、profile、level、bit-depth/
chroma/range 打包字节，保存为 4 字节 `vpcC` 记录供 `vp9_parse_vpcc` 使用。
既有 `mp4_vp9_b64` fixture 实际含 12 字节 vpcC（profile 0、level 10、
8-bit 4:2:0），测试由“未解析”更新为校验 profile/level/pix_fmt；新写的
VP9 MP4 也能回读出 extradata。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **828/828**。

## 34. 2026-10-24 WebM/Opus 写入器

新增 `libavformat/webm_mux.mbt`：写 EBML Header（DocType `webm`）、Segment
Info、单个 `A_OPUS` Track（CodecPrivate=OpusHead）与一个 Cluster。SimpleBlock
按 Opus TOC 累计时长写入毫秒相对时间戳，所有音频帧标记为 keyframe。

EBML 元素使用最小长度 VINT；Tracks/TrackEntry 尾部插入空 Void，避免当前
demux 在多层元素同一偏移结束时重复解析 TrackEntry。输出经
`av_matroska_open` 回读：`is_webm`、1 条 `A_OPUS` 轨、19 字节 OpusHead、
3 个包 payload 与 PTS 0/20/40 ms 全部一致；ffprobe 亦识别为 Opus/48 kHz/mono。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **830/830**。

## 35. 2026-10-24 WebM/VP9 写入器

新增 `libavformat/webm_video_mux.mbt`：复用 WebM EBML 辅助写 `V_VP9`
视频轨（Video PixelWidth/PixelHeight）与 Cluster/SimpleBlock。逐包 duration
以毫秒写入相对时间戳，关键帧置 SimpleBlock keyframe 位。

回归写 64×48、3 个 VP9 包，`av_matroska_open` 回读验证 `V_VP9`、像素尺寸、
payload 长度、PTS 0/33/66 ms 与 keyframe 标记；非法参数/空包负例通过。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **832/832**。

## 36. 2026-10-24 WebM VP9+Opus 多轨写入器

新增 `libavformat/webm_av_mux.mbt`：单文件写入 `V_VP9` 视频轨与 `A_OPUS`
音频轨。调用方按毫秒提供每包 PTS/duration，`finish` 按 PTS 稳定排序后在
一个 Cluster 中交错输出 SimpleBlock；视频保留 keyframe 位，音频恒为同步帧。

回归写 3 个视频包 + 3 个音频包，`av_matroska_open` 回读验证两条轨道的
CodecID/Video/Audio 参数、6 个包的交错顺序、stream_index、PTS 与 payload。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **834/834**。

## 37. 2026-10-24 WebM/AV1 写入器

新增 `libavformat/webm_av1_mux.mbt`：写 `V_AV1` 视频轨、Video
PixelWidth/PixelHeight 与 `CodecPrivate`（av1C），复用 WebM EBML 与
SimpleBlock 写入器。回归用 4 字节 av1C、2 个样本回读验证 CodecID/
CodecPrivate、尺寸、PTS 与 keyframe。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **835/835**。

## 38. 2026-10-24 H.264 + AAC 双轨 MP4 写入器

新增 `libavformat/mp4_av_mux.mbt`：一个文件内写 video (`avc1`/`avcC`)
与 audio (`mp4a`/`esds`) 两条轨，共用一个 `mdat`；视频/音频样本分别成 chunk，
`stco` 记录各自偏移，`stbl` 的 `stts`/`stsc`/`stsz` 与视频 `stss` 逐轨生成。
movie timescale 固定 1000，tkhd/mvhd 时长按各自媒体时长换算。

回归写 2 个 H.264 样本 + 3 个 AAC 样本，`av_mov_open` 回读验证两轨
codec/尺寸/采样率/声道、样本 dts/duration/keyframe、chunk payload 与
AAC esds。非法参数与空包负例通过。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **837/837**。

## 39. 2026-10-24 MKV/H.264 写入器

新增 `libavformat/mkv_h264_mux.mbt`：EBML DocType `matroska`、`V_MPEG4/ISO/AVC`
视频轨与 `CodecPrivate`（avcC）、Video 尺寸、Cluster/SimpleBlock。回归用真实
44 字节 avcC 写 2 个样本，`av_matroska_open` 回读验证 CodecID/CodecPrivate、
64×48、PTS 与 keyframe。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **838/838**。

## 40. 2026-10-24 MKV/H.264+AAC 双轨写入器

新增 `libavformat/mkv_av_mux.mbt`：DocType `matroska`，同时写
`V_MPEG4/ISO/AVC`（CodecPrivate=avcC）与 `A_AAC`（CodecPrivate=
AudioSpecificConfig）两轨。按毫秒 PTS 稳定排序后交错写 SimpleBlock；
两轨 TrackNumber/UID 分别为 1/2。

回归写 2 个 H.264 + 2 个 AAC 包，`av_matroska_open` 回读验证两条轨
CodecID/CodecPrivate、4 个包的交错顺序、stream_index、PTS 与 payload。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **840/840**。

## 41. 2026-10-24 HLS 媒体播放列表写入

新增 `libavformat/hls_mux.mbt` 的 `hls_write_media_playlist`：输出
`#EXTM3U`/`EXT-X-VERSION`/`EXT-X-TARGETDURATION`/`EXT-X-MEDIA-SEQUENCE`、
逐段 `#EXTINF` 与可选 `#EXT-X-ENDLIST`。target duration 可显式给出，否则
取最大段时长上取整。

回归把输出重新交给 `av_hls_parse`，验证版本、目标时长、media sequence、
3 个 segment 的 URI/时长与 ENDLIST；空列表/无 ENDLIST 也覆盖。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **842/842**。

## 42. 2026-10-24 DASH SegmentTemplate MPD 写入

新增 `libavformat/dash_mux.mbt` 的 `dash_write_segment_template_mpd`：
生成单 Period/单 AdaptationSet/单 Representation 的静态 MPD，含
`SegmentTemplate` 的 timescale/duration/startNumber/initialization/media
模板。回归交给 `av_dash_parse`，验证 `$RepresentationID$` 与
`$Number%03d$` 展开、5 个 segment、编号 5..9 与 URI。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **843/843**。

## 43. 2026-10-24 HLS 主播放列表写入

新增 `hls_write_master_playlist`：输出 `#EXTM3U`/VERSION 与
`#EXT-X-STREAM-INF:BANDWIDTH,RESOLUTION,CODECS` + variant URI。回归用
`av_hls_parse` 回读验证 `is_master`、两条 variant 的带宽/分辨率/codecs/URI。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **844/844**。

## 44. 2026-10-24 fMP4 初始化段与媒体段写入器

新增 `libavformat/fmp4_mux.mbt`：面向 DASH/HLS 分片，单轨一段。初始化段写
`ftyp`（isom/iso6/mp41/dash）与 `moov`（`mvhd`/`trak`/`mvex`/`trex`，`stbl`
只保留 `stsd` 和空样本表）；媒体段写 `styp`（msdh/msix/cmfs）、`moof`
（`mfhd`/`tfhd` default-base-is-moof/`tfdt` v1/`trun` 带 per-sample
duration/size/flags）与紧随其后的 `mdat`，`trun` 的 data_offset 指向 mdat
负载。支持 H.264（`avc1`/`avcC`）与 AAC（`mp4a`/`esds`）两种初始化段；
抽出 `mp4_h264_entry`/`mp4_aac_entry` 供普通与分片写入器共用。

回归把初始化段与两段媒体段拼接后交给 `av_mov_open`，验证编解码器、尺寸、
extradata、样本 DTS/时长/keyframe 与负载偏移；负例覆盖空段与零时长。另用
FFmpeg 9.0.2 外部校验：生成的 H.264 分片文件逐帧 framemd5 与源 MP4 完全一致，
AAC 分片解码 PCM 去掉首帧 priming 后与源文件逐样本一致。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **848/848**。

## 45. 2026-10-24 fMP4 扩展到 HEVC/AV1/VP9/Opus

把 fMP4 写入器从 H.264/AAC 扩展到 HEVC（`hvc1`/`hvcC`）、AV1
（`av01`/`av1C`）、VP9（`vp09`/`vpcC`）与 Opus（`Opus`/`dOps`）。抽出
`mp4_visual_entry` 统一生成 VisualSampleEntry，HEVC/AV1/VP9 与 H.264
写入器共用；Opus 抽出 `mp4_opus_entry`/`mp4_opus_dops`，普通 MP4 与分片
写入器复用。

回归新增四组初始化段+媒体段回读（编解码器、尺寸、extradata、DTS/时长）
与参数负例。外部用 FFmpeg 9.0.2 校验：四种分片文件均被正确识别为
hevc/av1/vp9/opus，前 3 帧（或 Opus 前 3 包）解码结果与源 MP4 完全一致。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **853/853**。

## 46. 2026-10-24 MPEG-TS 写入器支持自定义 stream type

`ts_muxer_new` 新增 `video_stream_type`/`audio_stream_type` 命名参数（默认
0x1B H.264 与 0x0F AAC，保持既有调用兼容），PMT 中的流类型改由字段驱动，
由此支持 HEVC（0x24）、MP3（0x03/0x04）等解复用器已能识别的组合。

回归内嵌 docker FFmpeg 9.0.2 生成的真实向量：libx265（AUD 拆分的 3 个
access unit）与 libmp3lame（4 帧 MPEG-1 Layer III），以 HEVC+MP3 组合
写入并经 `av_mpegts_open` 回读，验证 codec/pid/PTS/payload/PCR 全部一致。
外部校验：同参数产物 `.ts` 被 ffprobe 正确识别为 hevc+mp3，视频 3 帧与
音频 4 帧的 framemd5 与源码流解码完全一致，ffprobe 的 pts_time 序列
（视频 0/0.1/0.2 s，音频 1152 样本步进）符合预期。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **854/854**。

## 47. 2026-10-24 Matroska DocType 下的 VP9/AV1/Opus 写入器

`webm_vp9_muxer_new`/`webm_av1_muxer_new`/`webm_opus_muxer_new` 新增
`doctype` 命名参数（默认 `webm`，可选 `matroska`，其余值报 EINVAL），
EBML 头的 DocType 改由参数驱动，VP9/AV1/Opus 由此可直接产出独立 .mkv。

同轮对齐 FFmpeg `matroskaenc.c` 的 Opus 语义：pre-skip 非零时写
`CodecDelay`（0x56AA，pre-skip 按 48 kHz 换算成 ns）与 `SeekPreRoll`
（0x56BB，80 ms），并把 SimpleBlock 时间戳加上 ms 时间基下的 ts_offset
（半远离零舍入），使解复用器的 `- codec_delay_in_track_tb`（已有逻辑）
恢复原始时间戳。此前 webm/matroska 产物缺这三者，ffmpeg 解码时 pre-skip
时间戳语义与 mp4 路径不一致。

回归新增三种编码的 matroska DocType 回读（`is_webm` 为假、CodecID、
extradata/OpusHead、`codec_delay`/`seek_preroll`、包与 PTS round-trip）
与非法 DocType 负例。外部用 FFmpeg 9.0.2 校验：三个 .mkv 均被 ffprobe
识别为 vp9/av1/opus；VP9、AV1 各 3 帧 framemd5 与源完全一致；Opus 前
3 帧解码 PCM（跳过 pre-skip 312 样本后 2568 样本）与源 mp4 解码输出
逐字节一致。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **860/860**。

## 48. 2026-10-24 VP9 复合预测触发确认与 alt-ref 真实向量测试

`Vp9Decoder` 新增诊断计数器 `comp_inter_blocks`：`decode_mode_inter` 进入
复合参考分支（`b.comp != 0`，含 `comp_pred_mode` 1/2 两种来源）时累加，
用于确认复合预测是否真的被码流触发。

用它检查 libvpx `-auto-alt-ref 1` 生成的 21 包码流：所有 inter 帧的
`comp_pred_mode` 均为 0（单参考），复合块计数为 0——即 libvpx 在该模式下
确实不产生复合预测块，README 的"编码器无法生成"表述经计数探针证实。
顺带发现旧探针向量（`.trash` 内 base64）是截断的（4159/17953 字节），
已从 `.scratch/vp9_alt.ivf` 重新提取。

这组码流转正为 `vp9_vectors/vp9_altref_test.mbt`：21 帧逐帧 MD5 与
FFmpeg 9.0.2 framemd5 全部一致，并断言 `comp_inter_blocks == 0`。
大 base64 字面量仍需放独立向量包（libavcodec 主包会撞 Wasm `__init`
局部变量上限）。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **861/861**。

## 49. 2026-10-24 AV1 无损关键帧支持 10/12-bit

把 AV1 像素路径从 8-bit 泛化到 8/10/12-bit。核心是像素平面
`FixedArray[Byte]` → `FixedArray[Int32]` 的一次性重构：`planes`/
`ipred_edge`/`edge_buf`/`pal`/`al_pal` 换为分量值存储，`Av1FrameCtx` 增加
`bpc`/`pixel_max`/`mid`，`Av1FrameOutput::packed` 对高位深按小端 uint16
打包（对应 ffmpeg 的 yuv420p10le/p12le）。

位深相关语义逐点对齐 dav1d（BITDEPTH 模板分支）：
- ipred 常量参数化——edge 填充 129/127/128 → `mid±1`/`mid`；DC_128 →
  `(1<<bpc)>>1`；`dc_gen` 非方形乘数 8-bit (0x5556/0x3334,>>16) 与
  10/12-bit (0xAAAB/0x6667,>>17) 分支；`iclip_pixel` 上限 `pixel_max`；
  ipred/itx 系列函数加 `bd_max`/`mid` 可选命名参数（默认 255/128 兼容）。
- palette 解析 `bpc` 来自序列头（`decode_bools(bpc)`、delta bits、max 环绕）。
- dequant 表按 `dav1d_dq_tbl[hbd]` 三组选取（init_quant 原已就绪）。
- **decode_coefs 的 cf_max 修正**：原硬编码 32767（8-bit），改为
  `~(~127<<bpc)`（10→131071、12→524287）。这是 12-bit 不收敛的根因——
  大系数被错误钳制；10-bit 流恰未触发但同属错误，一并修复。
- CFL AC 模拟 dav1d `int16_t` 存储的环绕语义（12-bit AC 和可达 131040）。

回归：既有 8-bit 无损向量 MD5 不变（11/11）；新增 10/12-bit yuv420p
libaom `-crf 0` 三帧向量（128x96），与 libdav1d framemd5 逐帧一致。
调试过程沉淀：先按行/平面 MD5 定位到 U 平面 16x8 区域，再用块参数
插桩（uv_mode/pal_sz/cfl_alpha）区分 CFL 与非 CFL 块，最终锁定 cf_max。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **862/862**。

## 50. 2026-10-24 AV1 有损帧开工：非方形变换 levels 布局修正

有损帧（lossless=0）支持的前置调研与首个正确性修复。侦察结论：
tx_mode=2 的逐块 tx size 符号、txtp_intra1/2 符号、eob_bin 全表
（16..1024）、dequant/cf_max、tx_type_class 等解码层组件此前均已就位；
用 `-enable-cdef 0 -enable-restoration 0` 的 crf30 8 帧向量试探，6 帧
可完整解码、2 帧在帧尾附近块符号分歧导致 overread（分歧点已用逐
tx 子块的 msac cnt 插桩定位到 (18,23) 的 4x4 IDTX 块附近，root cause
尚需全帧符号仲裁器对拍）。ffmpeg `-skip_loop_filter` 对 libdav1d 无效，
未滤波参考不可得，LF 必须实现后才能整帧验收。

修复 `decode_coefs` 的 levels 布局：dav1d 的 DECODE_COEFS_CLASS 宏中
`if (TX_CLASS_2D)` 以枚举常量 0 参与判断恒为假，levels 对**所有**
tx class 都取转置布局 `x*stride+y`；原实现 2D 类用 `rc` 索引，仅在
方形变换（含全部无损 4x4 路径）时数值巧合相等，非方形变换的
lo_ctx 会错。入口对 `!coded_lossless` 保持拒绝（待 LF 移植与符号
分歧收敛后放开）。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **862/862**（既有无损向量不受影响）。

## 51. 2026-10-25 AV1 有损对拍基础设施：全帧 C 仲裁器（进行中）

有损符号分歧（见 §50）的对拍工具建设。apt 网络持续不可用使
meson 构建 dav1d-dbg 不可行；改走 alpine:3.20 + `apk add gcc`
（可用），参照 trace_arb.c 的 include 真源码模式写全帧仲裁器
`.scratch/av1_hbd/kfarb.c`（decode_sb 递归含 T-split 三子块/H4/V4、
decode_b 的 skip/ymode/uvmode/CFL/palette/txsize、decode_coefs 全
token 链），符号 tag 打印用于与 MoonBit 侧逐行 diff。

已修通的 kfarb 自身 bug：partition 的 n_symbols 应为
`partition_type_count[bl]`（不减 1，MoonBit 侧本来就对）；txsz CDF
三维索引 `[max-1][ctx]`；CDF qcat 按 qidx 分档（>120/>60/>20）；
palette 的 sz_ctx=`b_dim[2]+b_dim[3]-2` 与 pal_ctx 双比特、
`allow_screen_content_tools` + `bw4+bh4>=4` 门控；CFL 的 joint
sign（`cfl_sign` 8 值拆 sign_u/sign_v）+ `cfl_alpha[ctx]` 两段式。

当前卡点：trace 在 f2 的 (4,0) bl=2 bp=8（T_LEFT_SPLIT）块
bs=9 处 segfault，uvmode 符号已解出、后续 pre-palette 探针未达，
崩因待查（疑 read_pal_plane 的 al_pal 索引或 T-split 边界坐标）。
向量与现场保留在 `.scratch/av1_hbd/`（kfarb.c、run_kfarb.py、
lossy8.ivf、crf30/crf55 向量）；下一步修通后与 MoonBit 侧 trace
diff 定位首个分歧符号，再开 LF 移植。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **862/862**。

## 52. 2026-10-25 AV1 有损关键帧解码 + 环路去块滤波

接续 §50/§51：放弃手写 C 仲裁器，改为**直接编译 dav1d 1.5.1 源码**
（alpine `gcc`，`HAVE_ASM=0`，非模板文件不定义 `BITDEPTH`，模板文件
`-DBITDEPTH=8`；用 `config.h` + 空 `vcs_version.h`）并打开
`DEBUG_BLOCK_INFO`、`decode_coefs` 的 `dbg`，得到权威逐符号轨迹；
同时把 `Dav1dSettings.inloop_filters` 置 0 取得未滤波参考帧，便于分离
「重建」与「滤波」两类差异。工具落在 `.scratch/dav1d_dbg/`（不入库）。

符号层对比（8 帧全部一致）后定位并修复五个真 bug：
1. `cfl_allowed_mask` 位集错误：按自然色度尺寸写的，dav1d 的
   `enum BlockSize` 顺序下 CFL 允许集是 7,8,9,11..21（含 `BS_8x8=17`）。
   有损路径首个块即分歧；无损走 `cbw4==cbh4==1` 分支所以从未暴露。
2. `decode_coefs` 非 2D（H/V）变换的 DC hi-token 上下文：必须用 DC 处
   `get_lo_ctx` 返回的 `hi_mag`，原实现丢弃后沿用了 AC 循环残留值。
3. palette 索引与 tx_size 的**符号顺序**：dav1d 是
   palette → filter_intra → palette 索引 → tx_size；原实现在 tx_size 之后
   读索引。无损时 tx_size 不读符号，顺序错也通过。
4. 逆变换 1D 类型表方向：dav1d 的 DSP 赋值对非对称类型做了行列转置
   （`[ADST_DCT] = inv_txfm_add_dct_adst`、`[V_DCT] = identity_dct`），
   原表按名字序，导致所有混合类型（DCT+ADST/FLIPADST/IDENTITY）错，
   同类型组合因对称而通过。
5. 逆变换核自身：`inv_adst16_1d` 的第二级旋转读到了被覆盖的中间量；
   `inv_dct64_1d` 多处系数转录错误（-2824/-1474/-2191）与 SSA 名混淆，
   改为脚本从 C 源机械生成（357 条语句的 SSA 版本）。

新增 `libavcodec/av1_lf.mbt`：`lf_mask.c` 的 intra 掩码/level cache /
`calc_eih` / `calc_lf_values`，`loopfilter_tmpl.c` 的 4 个 C 核，以及
`lf_apply_tmpl.c` 的列/行驱动（单 tile；多 tile 有损显式拒绝）。
整帧解码完成后按超级块行依次做「列→行」滤波，对 intra 帧与 dav1d 的
逐行时序等价（预测已消费未滤波像素，行间只读上一行）。修复了
chroma 掩码 `sidx` 的移位（应为 `sidx << vbits/hbits`，此前写成
`sidx * vbits`）、行滤波 level 的重复行偏移、行分量漏加 `+1`、
64x64 行级别指针基准等错误。

契约收紧：有损帧在 CDEF 强度非零、任一平面环路恢复类型非零、
`delta_lf_present` 时仍显式拒绝（`cdef` 强度位已完整解析保存）。

验证：
- 符号轨迹：8 帧逐符号与 dav1d 完全一致（含 palette/CFL/所有 token）。
- 逆变换：155 个 (RectTxfmSize, TxfmType) 组合的 `inv_txfm_add` 校验和
  与 dav1d DSP 逐值一致，固化在 `libavcodec/itx_probe/itx_probe_test.mbt`。
- 像素：未滤波重建与 dav1d（`inloop_filters=0`）逐字节一致；
  带环路滤波的 8 帧 framemd5 与 `linuxserver/ffmpeg:9.0.2`（libdav1d）
  完全一致，向量见 `libavcodec/av1_lossy_vectors/av1_lossy_test.mbt`
  （libaom crf30、`-enable-cdef 0 -enable-restoration 0`、128x96）。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **864/864**。

## 53. 2026-10-25 AV1 CDEF（约束方向增强滤波）

新增 `libavcodec/av1_cdef.mbt`：dav1d `cdef_tmpl.c` 的
`cdef_find_dir`（八方向代价 + 方差）与 `cdef_filter_block`（主/次抽头、
`constrain`、主+次分支的 min/max 钳制），以及 `cdef_apply_tmpl.c` 的
单线程逐块调度（cdef_idx/强度/方向/边缘标志）。

实现要点：
- 每 128x128 区域的 `cdef_idx[4]` 与 `noskip_mask[16][2]` 挂在
  `Av1Filter` 上：前者在 `decode_b` 读 cdef 索引时按 64-SB 槽位
  （`((bx&16)>>4)+((by&16)>>3)`）写入（此前所有 64-SB 共用槽 0，
  导致 3/4 的块被漏滤）；后者按 dav1d 的 4x8 位布局在 `!skip` 时填充。
- 滤波在 LF 之后进行，但先对整帧做一次**未 CDEF 快照**：CDEF 每个
  8x8 块只读快照、写当前帧。对 intra 帧这与 dav1d 的逐 SB 行+2 行滞后
  调度等价（deblock 不能消费 CDEF 输出），实现上更简单。
- `cdef_find_dir` 只在 luma 主强度或 chroma 主强度非零时计算；luma
  次强度单开时 dav1d 传 `dir=0`（这是本轮最后一个差异点）。
- 4:2:0 色度用 4x4 核、damping-1；4:2:2 用 4x8 核与 uv 方向映射。

验证：libaom crf30（`-enable-restoration 0`，CDEF 开启）128x96 8 帧，
逐块 (bx,by,cdef_idx,yl,uvl,dir,var,edges) 与 dav1d 完全一致，
framemd5 8/8 与 FFmpeg 9.0.2（libdav1d）一致；向量见
`libavcodec/av1_lossy_vectors/cdef_test.mbt`。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **865/865**。

## 54. 2026-10-25 AV1 逐 SB delta-q 与 tile 边界修正；LR 语法层

为接入 SVT-AV1 的环路恢复向量（本机 libaom 在 `-enable-restoration 1`
下仍选择 NONE，只有 SVT/rav1e 实际发出 LR；SVT 同时启用 delta-q），实现
了 `decode_b` 的逐超级块 delta_q：

- `Av1TileCtx` 增加 `last_qidx` 与 `dq_cur[8*6]`；在 SB 原点（`bx/by`
  按 64/128 对齐）于 skip/cdef 索引之后解析 `delta_q` 符号（含
  `3` → n_bits 扩展、符号位、`<< delta_q_res`、clip 到 1..255），并按
  当前 qidx 重建 dequant 表（新增 `fill_dq`，与 `init_quant_tables` 的
  非分割路径一致）。`decode_coefs` 改用 tile 的 `dq_cur`。
- delta_lf 仍未实现（该向量 dlf=false），入口在 `delta_lf_present` 时
  继续拒绝。

顺带修复一个**帧边界裁剪** bug：tile 的 `col_start/row_start/col_end/
row_end` 此前按 SB 对齐（如 160x120 得到 48/32），未按 `bw/bh` 裁剪；
dav1d 的 `setup_tile` 用 `imin(..., f->bw/f->bh)`。该差异使
`prepare_intra_edges` 的 `px_have = imin(sz,(h-y)<<2)` 多读越界行，
导致帧底部 CFL 块的 AC 用到错误的 luma 填充值（同帧可见像素仍完全
正确，只有 CFL 色度错）。修正后 SVT delta-q 向量 6/6 帧 framemd5 与
libdav1d 一致；向量见 `av1_lossy_vectors/deltaq_test.mbt`。

另外把去块滤波核的对拍固化：用 dav1d 的 `loop_filter` C 参考在
4/6/8/16 四种宽度上跑 800 组随机输入，FNV-1a 校验和作为期望值，
新增 `libavcodec/lf_kernel` 回归包（为排查本轮 CFL 伪影时建立）。

LR（环路恢复）本轮完成了**语法层**：帧头 `unit_size[2]` 解析、
`read_restoration_info`（switchable/wiener/sgrproj + subexp + lr_ref
初值）与逐 SB 的 unit 槽位存放（`Av1Filter.lr[3*4]`），8 帧 SVT SGR
向量的 `Post-lr_sgrproj` 轨迹与 dav1d 完全一致。滤波核（Wiener/SGR）
尚未移植，入口仍对非零 `lr_type` 显式拒绝；`av1_lr.mbt` 已含
`sgr_params`/`sgr_x_by_x` 表（脚本自 c->src 提取）。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **867/867**。

## 55. 2026-10-25 AV1 ipred edge 缓冲尺寸修正（高帧)

在尝试接入 LR 向量（libaom + mandelbrot 内容可产出 Wiener 类型）时发现：
`ipred_edge` 按 `sb128w * 128 * (sb128h + 1)` 分配，而 dav1d 按
`sbh * sb128w * 128`（`sbh` = 64/128 行超级块行数）。对高度 >128 的帧，
`backup_ipred_edge` 的 `sby = by >> sb_shift` 可超过 `sb128h`，写入越界
（320x240 直接 trap）。改为按实际 SB 行数分配。

新增 320x240 的 SVT-AV1 delta-q 关键帧向量（6 帧，framemd5 与 libdav1d
一致），覆盖多 SB 行、`ipred_edge` 跨行备份与 LF/CDEF 的多行驱动。

LR 部分留待下一增量：本轮实现了 Wiener unit 滤波原型（快照 + 逐 unit），
帧内绝大多数像素正确，但 dav1d 的 LR 按 8 行滞后的 **stripe** 处理，
stripe 顶部（56/120/184 行）使用 `lr_lpf_line` 的两行 + 特殊重复窗口，
与「整 unit 按帧边界钳制」相差 ±1。SGR（3x3/5x5/mix）尚未实现。
入口对任何非零 `lr_type` 仍显式拒绝。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **868/868**。

## 56. 2026-10-25 AV1 有损 4:4:4 / 4:2:2 / 10-bit

此前有损路径只在 8-bit 4:2:0 上验收。补上 libaom 生成的 128x96 关键帧
向量（4 帧，crf30，关闭 CDEF/LR，仅去块滤波）后发现 **4:4:4 未滤波
重建即错**：首块 luma 正确，随后整帧 Y/U/V 全错。

根因是 `decode_b` 里色度变换尺寸取了 `dav1d_max_txfm_size_for_bs`
的**固定 4:2:0 列**（`bs*4+1`）。dav1d 用的是
`...[bs][f->cur.p.layout]`（列 0=y、1=420、2=422、3=444）；4:4:4 时
色度 tx 被取小（如 16x16 块应 16x16 却按 8x8），`decode_coefs` 读错的
系数个数导致**熵解码分叉**，此后连 luma 也错。无损路径强制 4x4 tx，
所以此前的 4:4:4 无损向量发现不了。

修正：按 `monochrome/420/422/444` 选择列。修正后未滤波 4:4:4 首帧
与 dav1d 逐字节一致，带 LF 的 framemd5 也与 libdav1d 完全一致。

新增 `libavcodec/av1_lossy_extra/`：4:4:4、4:2:2（均为 yuv444p/
yuv422p 8-bit）与 10-bit 4:2:0 三条流，各 4 帧 framemd5 对拍。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **869/869**。

## 57. 2026-10-25 AV1 环路恢复（Wiener / SGR）

补齐 AV1 有损关键帧最后一个环内滤波：环路恢复（LR）。照 dav1d 1.5.1
`looprestoration_tmpl.c` / `lr_apply_tmpl.c` / `lf_apply_tmpl.c` 移植：

- **滤波核**（`av1_lr_filter.mbt`）：wiener_filter_h/v/hv + wiener_c 的
  流式滑窗；SGR 的 3x3 / 5x5 / mix 三套（行聚合、A/B 反演、finish 与
  加权）。行缓冲按 dav1d 的 400/390/384 步长组织。
- **应用驱动**（`av1_lr_apply.mbt`）：逐 SB 行、逐 unit 的 stripe 调度，
  8 行滞后 + 每 stripe 56/64 行；unit 左右边界的 4 列备份
  （`backup4xU`）与 `lr_lpf_line` 12 行备份（`backup_lpf`，含跨 SB 行
  顶部 4 行复制与帧末 3 行复制规则）。

对拍方式：先做**核级回归** `libavcodec/lr_kernel`——用 dav1d C 参考
（`looprestoration_tmpl.c` 直接编入）在 w=4/8/13/64/128/256、h=1/2/3/7/
56/64、11 种 edge 组合上跑 1980 组随机输入，FNV 哈希逐例一致。期间
修掉两个移植错误：Wiener `hv` 的中间行必须写临时缓冲（`ptrs[6]` 在
稳态会等于 `ptrs[0]`，先写会破坏仍要被读的窗口）；SGR 的 A/B 目标行
必须用旋转后的指针（`A_ptrs[2]`/`A_ptrs[1]`/`A3_ptrs[3]`），不能写死；
mix 的第二行 3x3 邻居需从 `&A3_ptrs[1]` 起算。

整帧验收（`libavcodec/av1_lossy_lr`）：
- `wt1.ivf` 320x240 ×4（libaom + mandelbrot，Wiener）
- `t5.ivf` 160x120 ×6（SVT-AV1，SGR）
- `lr10.ivf` / `lr10nc.ivf` / `lr12.ivf` 128x96 ×4（10/12-bit 关键帧）
全部 framemd5 与 libdav1d 一致。

整帧测试还暴露出一个**高位深逆变换** bug：`av1_inv_txfm_add` 的
行/列中间裁剪此前固定为 int16（dav1d 的 8-bit 分支），10-bit 时
dav1d 用 `~bitdepth_max << 7` / `<< 5` 的更大范围。固定 int16 会让
大系数块（本例 x=80 的 16x16 H 分块）产生 ±1..47 偏差；改为按
`bd_max` 选择后 lr10/lr12 逐帧一致（8-bit 不变）。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **871/871**。

## 58. 2026-10-25 AV1 高位深 4:4:4 / 4:2:2 有损覆盖

在 LR 收敛后补齐高位深非 4:2:0 的有损关键帧向量（libaom，128x96，4 帧，
crf30，LF+CDEF 默认开启）：10-bit 4:4:4、10-bit 4:2:2、12-bit 4:4:4，
framemd5 均与 libdav1d 一致，加入 `av1_lossy_extra`。这同时回归了 §57
的逆变换裁剪修正与高位深 CDEF/LF 在 444/422 布局下的路径。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **871/871**。

## 59. 2026-10-25 AV1 多 tile 有损环内滤波（LF tile 边界强度修正）

补上多 tile 有损帧的最后缺口：去块滤波在 tile 边界处的**变换尺寸档位
修正**（dav1d 1.5.1 `src/lf_apply_tmpl.c` `dav1d_loopfilter_sbrow_cols`
前半，约 331–401 行）。此前 `av1_decode6.mbt` 对 LF/CDEF/LR 开启且
`num_tiles > 1` 的帧显式拒绝；CDEF/LR 本身无 tile 专用逻辑（消费 LF
输出），拒绝解除后即可整帧通过。

实现分三块：

- **右边缘备份**（`src/decode.c` 2734–2742 行的移植）：`decode_tile_sbrow`
  返回后把左上下文 `f.l.tx_lpf_y[by&16 .. +sb_step)` / `tx_lpf_uv` 存入
  `Av1FrameCtx` 新增的 `tile_lpf_y` / `tile_lpf_uv`（每 tile 列行距
  `align_h=(bh+31)&~31`，chroma 为 `align_h>>ss_v`——dav1d 源码里
  chroma 行距在保存处有 `align_h >>= ss_ver` 一句，易漏）；同时每 tile
  行首个 sbrow 在 `start_of_tile_row[by/sb_step]` 记下 tile 行号。
- **tile 列边界修正**（`av1_lf.mbt` `lf_sbrow_cols` 开头）：对每个
  `tile_col >= 1`，取该列首个 SB 的 4x4 起点 `tile_col_start4[tc]`，
  用左邻 tile 的右边缘值钳制列方向掩码档位：`idx = 2*!!(lvl2)+!!(lvl1)`，
  清 0/1/2 三档后置 `min(idx, lpf_y[y-starty4])`。luma 位图 16 位半区
  （`sidx = bit>=0x10000`），chroma 半区 `16>>ss_v` 位、基址
  `(tc-1)*(align_h>>ss_v) + (sby<<sbl2)>>ss_v`。
- **tile 行边界修正**：`start_of_tile_row[sby] != 0` 时用**上一 tile 行
  残留的 above 上下文** `f.a[sb128w*(r-1)+x].tx_lpf_y/uv` 钳制行方向
  掩码（`f.a` 按 tile 行分段，`a_idx = tile_row*sb128w + col_start>>5`，
  与 dav1d 布局一致）；uv 只有 0/1 两档。

新向量（libaom 320x240 crf30，`-g 1`，`tile-columns=1:tile-rows=1`，
均 4 帧、framemd5 与 libdav1d 一致，入 `av1_lossy_extra`）：

- `mt8`：64x64 SB、4:2:0 8-bit 2x2 tile（上一会话备好）；
- `mt444`：64x64 SB、4:4:4 10-bit 2x2 tile——覆盖 `ss_h=ss_v=0` 的
  chroma 半区/基址路径；
- `mtsb128`：**128x128 SB**、4:2:0 8-bit 2x2 tile。扫描发现此前全部
  有损向量（§52–§58）都是 64x64 SB——libaom 的
  `AOM_SUPERBLOCK_SIZE_DYNAMIC` 恒选 64（720p 也是），ffmpeg 9.0.2 又
  没透传 `--sb-size`；该向量改用 debian `aom-tools` 的 aomenc
  `--sb-size=128 --tile-columns=1 --tile-rows=1` 生成（ffmpeg 出
  rawvideo 管道进容器）。这是 128x128 SB 有损链路（LF 的
  `is_sb64=0` 分支）首次被整帧验证。

三条向量解码与 dav1d 逐帧 md5 全部一致。`moon check --target wasm
--deny-warn --no-render` 0 error/0 warning，`moon test --target wasm
--no-render` **871/871**（向量并入既有 test 块，总数不变）。

## 60. 2026-10-25 AV1 逐 SB delta-lf

补上最后一个显式拒绝的帧头特性：`delta_lf_present`。实现完全对齐
dav1d `decode.c` 960–1027 行的符号读取与 `ts->lflvl` 切换：

- `Av1TileCtx` 新增 `last_delta_lf[4]`（每 tile 重置为 0，对应
  `setup_tile` 的 `ts->last_delta_lf.u32 = 0`）与 `lf_lvl_cur[8*64]`
  （SB 级 LF level 表副本，对应 `ts->lflvl`/`ts->lflvlmem` 的指针切换）。
- `decode_b` 在 delta_q 符号之后、同一 SB 原点门控内读取 delta_lf：
  `n_lfs = multi ? (单色 ? 2 : 4) : 1`，CDF 行 `i + multi`
  （`av1_cdf_delta_lf[5][4]`）；`3` → n_bits 扩展、符号位、
  `<< delta_lf_res`、clip ±63。
- 表切换与 dav1d 一比一（用 prev 快照替代指针状态）：全 0 → 回帧级
  `f.lf_lvl`；非 0 且（从 0 变来或值变化）→ `av1_calc_lf_values` 重算
  副本；非 0 未变 → 不动。`create_lf_mask_intra` 改用 tile 的
  `lf_lvl_cur`。
- `av1_calc_lf_values` 补 `lf_delta[4]` 参数：dir0 用 `lf_delta[0]`，
  dir1/2/3 用 `lf_delta[multi ? i : 0]`（chroma 的 level==0 早退在加
  delta 之前判断，与 dav1d `calc_lf_value_chroma` 一致）。

**向量生成的坑**：libaom 的全帧内路径不发 delta_q（`-g 1` 时
`--deltaq-mode=1` 无效），SVT 任意 preset 都只发 deltaq 不发 delta_lf，
SVT 也没有 `enable-deltalf` 参数键。可行组合是 aomenc
`--enable-tpl-model=1 --deltaq-mode=3 --delta-lf-mode=1`
（mode 3 = key frame visual quality，关键帧专用 deltaq）+ `--kf-max-dist=1`。
bookworm 与 trixie 的 aom-tools 各出一条（`dlfm3`/`dlft3`，320x240
crf30，LF 配置不同），均 `multi=false`、4 帧 framemd5 与 libdav1d 一致，
入 `av1_lossy_extra`。`multi=true` 的码流主流编码器产不出
（libaom 硬编码单模式），multi 分支（n_lfs=4/2 + CDF 行 +1）已实现但
暂无向量，同 VP9 复合预测的情况留待手搓码流。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **871/871**。

## 61. 2026-10-25 AV1 环路恢复的 4:2:2 / 4:4:4 / 10-bit 覆盖

LR 的整帧向量此前全是 4:2:0（§57）。本轮补齐格式矩阵，代码零改动——
五条向量全部直接通过，验证的是既有实现：

- `svt42010`：SVT-AV1 420 **10-bit**（320x240 crf35），`lr_type=[SGR,
  Wiener, Wiener]` 的混合 + 逐 SB delta-q——首个高位深 SGR 整帧向量；
- `ts4448` / `ts44410`：libaom 4:4:4 8/10-bit，chroma Wiener（y=NONE）；
- `ts422` / `ts42210`：libaom 4:2:2 8/10-bit，chroma Wiener。

**向量生成的关键经验**：libaom 对 mandelbrot 内容永不启用 restoration，
但换成 `testsrc2`（`-f lavfi -i testsrc2=...`）+ `-aom-params
enable-restoration=1` 后 chroma Wiener 稳定启用；SVT 走 ffmpeg 包装时
只支持 yuv420p/420p10（444/422 输入被静默转成 420，三条探测流 md5
完全相同才发现），SVT CLI（alpine v2.0）也只编 420。444/422 的 LR
只能靠 libaom+testsrc2。rav1e 虽能编 444 10-bit 且 `lr_type=SWITCHABLE`
（每 unit 动态选核，尚无向量覆盖），但其码流带 segmentation（入口仍
拒绝），暂不可用。向量入新包 `libavcodec/av1_lossy_lr_fmt/`。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning，
`moon test --target wasm --no-render` **872/872**。

## 62. 2026-10-25 AV1 关键帧 segmentation 与 SWITCHABLE 环路恢复

在关键帧路径接入 segmentation：帧头保存每段 ALT_Q/ALT_LF、preskip 与
last_active；量化和 lossless 状态按段计算；块级 seg_id 依 dav1d 的空间
预测、neg_deinterleave 和 preskip/postskip 顺序解码，并写回当前帧 segmap。
SKIP feature 直接设置块 skip；LF 等级按段叠加 ALT_LF。关键帧没有上一参考帧，
temporal/update_map=false 路径仍属于后续 inter 帧工作。

复核 dav1d `get_sbits(gb, 7)` 后确认 ALT_LF 是 **7 位补码**，不是额外
读取符号位；修正了交接草稿中的错误判断。同时修复 LR 多 restoration unit
时四列边界备份的行偏移单位错误。新包 `libavcodec/av1_seg_vectors/` 使用
rav1e 4:4:4 10-bit 的 4 帧关键帧（含 segmentation 与 SWITCHABLE LR），
逐帧 packed MD5 与 libdav1d framemd5 相同：
`6d2e878462c1662a0d512bdfe777939f`、
`409f3b3dba66f0ad7d11d913c562ed9d`、
`4697af2a0fe6608f8d665fb11c1265a9`、
`82650ec88b7303f28d4d9e90bbfc5a56`。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning；
`moon test --target wasm --no-render` **873/873**。

## 63. 2026-10-25 AV1 帧间帧基础：非缩放单参考运动补偿

新增 `libavcodec/av1_mc_core/`：移植 dav1d 1.5.1 `mc_tmpl.c` 的
`put_8tap_c` 与 `put_bilin_c`，支持 8/10/12-bit、九种 8-tap 水平/垂直
组合、bilinear、单轴/双轴十六分之一像素相位，以及宽或高不超过 4 时的
短滤波器选择。边界抽样按 dav1d 的 edge emulation 重复最近像素。
滤波系数由 dav1d `tables.c` 的 6×15×8 表机械提取，逐行和均为 64。

独立 C 参考程序按 dav1d 的整数舍入与滤波顺序生成 14 组固定像素结果；
新包 `libavcodec/av1_mc_vectors/` 逐像素对拍，覆盖各滤波家族、
8/10/12-bit、帧边界和窄块。大型系数表放进独立 `av1_mc_core` 包，
避免主 `libavcodec` Wasm 白盒测试模块的初始化局部变量超限。
这一步只实现单参考 MC 核；参考帧管理、帧间块语法、复合预测、
缩放/warp 与整帧入口仍需继续。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning；
`moon test --target wasm --no-render` **874/874**。

## 64. 2026-10-25 AV1 复合预测中间样本与平均合成

在 `av1_mc_core/compound.mbt` 接入 dav1d 1.5.1 的 `prep_8tap_c`、
`prep_bilin_c`、`avg_c` 和 `w_avg_c` 整数路径。`prep` 保留滤波后的
中间精度与高位深偏置，等权/距离加权合成再完成舍入和像素钳制；
复合预测的 mask、wedge、OBMC、warp 和缩放路径仍待后续实现。

先修正 §63 的向量生成位置：当时独立测试包实际只收录了 10 组，
文档误写成 14 组；本轮已把余下 4 组写入并通过。新增两个测试，
用独立 C 参考程序的固定输出逐像素验证 14 组 `prep` 和 3 组
8/10/12-bit 双参考的等权/加权合成（各 2 种合成结果）。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning；
`moon test --target wasm --no-render` **876/876**。

## 65. 2026-10-25 AV1 复合预测逐像素 mask

新增 `av1_mc_core/mask.mbt`，移植 dav1d 1.5.1 `mc_tmpl.c` 的
`mask_c`、`blend_c` 与 `w_mask_c`。支持外部 0..64 mask 对两个
`prep` 中间块加权、已重建像素块混合，以及按两个中间块差值生成
compound mask 和对应像素。差值 mask 的 4:4:4/4:2:2/4:2:0 缩采样
与 `sign` 舍入按 dav1d 的行/列顺序实现。

独立 C 参考程序固定 4 组 8/10/12-bit、三种布局和两种 sign 的输出；
`av1_mc_vectors/mask_test.mbt` 同时对拍像素与缩采样 mask。
OBMC 专用空间掩码、wedge 掩码、warp/缩放和帧间块语法仍待实现。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning；
`moon test --target wasm --no-render` **877/877**。

## 66. 2026-10-25 AV1 OBMC 水平与垂直重叠混合

新增 `av1_mc_core/obmc.mbt`，按 dav1d 1.5.1 `tables.c` 的
`dav1d_obmc_masks` 与 `mc_tmpl.c` 的 `blend_v_c`、`blend_h_c`
实现 OBMC。垂直路径只混合左侧 3/4 宽度，水平路径只混合顶部
3/4 高度，剩余像素保留原目标块；2/4/8/16/32 像素掩码按
宽度或高度索引，使用整数舍入公式 `(a*(64-m)+b*m+32)>>6`。

独立 C 参考程序固定 5 组 8/10/12-bit 块，覆盖全部掩码尺寸及
两个混合方向；`av1_mc_vectors/obmc_test.mbt` 对拍完整块摘要并
检查输入块不被修改。wedge、warp/缩放、refmvs、帧间块语法与整帧
解码入口仍需后续实现。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning；
`moon test --target wasm --no-render` **878/878**。

## 67. 2026-10-25 AV1 wedge 复合预测掩码与 inter-intra 掩码表

新增 `av1_mc_core/wedge.mbt`：完整移植 dav1d 1.5.1 `src/wedge.c`
的一次性初始化 `dav1d_init_ii_wedge_masks`——六个 64×64 主模板
（insert_border/transposed/hflip）、三套 16 项 wedge codebook
（heqw/hltw/hgtw，逐尺寸 signs 位掩码）、`fill2d_16x2` 的 444 掩码
裁剪与 422/420 色度下采样（含两种极性的舍入差），以及 inter-intra
的 ii_dc 常量掩码与九个共享 nondc 缓冲（vert/hor/smooth 三平面）。
对外提供 `av1_wedge_mask(layout, bs, sign, wedge_idx)` 与
`av1_ii_mask(layout, bs, mode)`，按 dav1d `enum BlockSize` 值索引，
444 两个 sign 槽位同值（亮度路径极性由操作数顺序处理）。

验证走 docker（`vpxdev-img` 自带 gcc，绕开 apt 网络不稳）：C dump
程序包含真实 `wedge.c`，dump 全部 864 条 wedge 掩码与 84 条
inter-intra 掩码字节，并按 8bpc/16bpc 两份 TU 实例化 `mc_tmpl.c`
用真 `mask_c` 生成 7 组固定 blend 向量；`av1_mc_vectors/wedge_test.mbt`
（`scripts/_emit_av1_wedge_vectors.py` 生成）逐条 FNV-32 对拍掩码、
4 组逐像素 + 3 组校验和比对 blend。期间修掉三处移植坑：MoonBit
`FixedArray::make(n, ref_init)` 会让嵌套引用数组互相别名（主模板
六方向共板、ii 槽位三布局共板，须 `makei` 逐个新建）；dav1d 1.5.1
`wedge_codebook_16_hgtw` 的 idx5/idx6 是 (H,4,4)、(H,4,6)（初抄时
二者颠倒，靠 C-dump 反向暴力搜索裁剪参数定位）；16bpc `mc_tmpl.c`
的 `PXSTRIDE` 是字节跨距（`w*sizeof(pixel)`，误传像素数导致 10-bit
fixture 行错位）。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning；
`moon test --target wasm --no-render` **883/883**。warp/缩放 MC、
refmvs、帧间块语法与整帧解码入口仍需后续实现。

## 68. 2026-10-25 AV1 缩放参考 MC 与 superres resize

新增 `av1_mc_core/scaled.mbt`：移植 dav1d 1.5.1 `mc_tmpl.c` 的
`put_8tap_scaled_c`、`prep_8tap_scaled_c`、`put_bilin_scaled_c`、
`prep_bilin_scaled_c` 与 `resize_c`。环形缓冲实现改写为等价的直接
二维求值：输出像素 (x,y) 的源位置为 `(mx+x·dx, my+y·dy)`（1/1024
单位），8-tap 相位 `(pos>>6)&15` 查 `av1_mc_subpel_filters`（含
w/h≤4 的短滤波行与 phase-0 直通路径）；prep 保留中间精度。resize
按 1/16384 步进查新表 `av1_mc_resize_filter`（64 相位 × 8 抽头，
机械提取进 `tables.mbt`），每个抽头围绕未钳位 `src_x` 独立钳位，
整个抽头和取反（dav1d 全抽头取反存储）。

C 参考按 §67 的 docker 管道（vpxdev-img gcc）实例化真
`mc_tmpl.c`（8bpc/16bpc 两份 TU）：源平面 96×64 加 8px 边距、
LCG 生成，25 组向量覆盖上/下采样步进、双相位、窄块、phase-0
直通、bilinear 与 resize 的 8/10/12-bit；测试侧用同一 LCG 重建
平面并以 FNV-32 锚定（`av1_mc_vectors/scaled_test.mbt`，生成器
`scripts/_emit_av1_scaled_vectors.py`）。修复两处移植错误：resize
的 F[3] 抽头漏了 `src_x=-1` 起点的独立钳位（越界 trap），以及把
C 的 `-(F[0]*…+F[7]*…)` 误读成仅取反首抽头——负号作用于整个和。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning；
`moon test --target wasm --no-render` **884/884**。warp MC、refmvs、
帧间块语法与整帧解码入口仍需后续实现。

## 69. 2026-10-25 AV1 warp 仿射运动补偿

新增 `av1_mc_core/warp.mbt`：移植 dav1d 1.5.1 `mc_tmpl.c` 的
`warp_affine_8x8_c`（像素）与 `warp_affine_8x8t_c`（prep 中间样本）。
两核共享 15 行 × 8 列水平中间层：每行按仿射模型推进
`mx += abcd[1]`、每列 `tmx += abcd[0]`，相位
`64 + ((tmx+512)>>10)` 查 193 相位 warp 滤波表（机械提取为
`av1_mc_warp_filter`，1544 个系数）；垂直pass 以
`my += abcd[3]`、`tmy += abcd[2]` 查同一表，put 走 `7+ib` 舍入并
钳位，prep 走 `7` 舍入保留中间精度再减 bias。块锚点以显式
`(ox, oy)` 传入，对应 dav1d 传入的源指针。

C 参考沿用 §67/§68 的 docker 管道：8bpc/16bpc 两份 TU 实例化真
`warp_affine_8x8*_c`，112×80 填充平面（块锚 (8,8)），10 组向量覆盖
单位矩阵（相位 64 恒等抽头）、正/负混合 abcd 与 8/10/12-bit 的
put/prep；测试侧同一 LCG 重建平面并以 FNV-32 锚定
（`scripts/_emit_av1_warp_vectors.py`）。本轮一次对拍通过。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning；
`moon test --target wasm --no-render` **885/885**。MC 核全部就绪；
refmvs、帧间块语法、参考帧生命周期与整帧解码入口仍需后续实现。

## 70. 2026-10-25 AV1 帧间帧头：全局运动、skip_mode 与参考状态

补齐 `av1.mbt` 帧间/switch 帧头的全部缺口，为此前跳过的位与桩：

- **序列头六工具位**：interintra/masked_compound/warped_motion/dual_filter
  与 jnt_comp/ref_frame_mvs 此前 `skip` 丢弃，现读入
  `Av1SequenceParameters`（warp/use_ref_frame_mvs 的帧级位以它们为门）。
- **全局运动**：替换 `PATCHWELCOME` 桩为完整解析（类型三连位 +
  ROT_ZOOM/AFFINE 的 mat[2..5] 与 TRANSLATION 的 mat[0..1] subexp 增量，
  基准取 primary ref 的矩阵或默认）。为此给帧头 BitReader 移植了
  dav1d getbits.c 的 `get_uniform`/`get_bits_subexp`（复用已有
  `inv_recenter`）。
- **skip_mode**：按 dav1d 的 off_before/off_after → before2 两段推导
  计算 `skip_mode_allowed` 与 refs 对，需要 8 slot 的 order hint。
- **frame_size_with_refs**：inter/switch 且非 error-resilient、
  frame_size_override 时逐 ref 读 found_ref 位，命中则从该 slot 拷贝
  尺寸（superres 位仍读）；未命中走显式尺寸。superres 下采样宽度
  补上 dav1d 的 `imax(ds, imin(16, w))` 钳位。
- **短信令 refidx 派生**：`frame_ref_short_signaling` 的完整移植
  （refidx0/3 显式读，其余按 8 slot 的 wrapped order hint 推导，
  含全局最早兜底），此前只 skip(6) 且不填 ref_frame_idx。
- switch 帧的 frame_size_override 强制为 1；reference_select/
  allow_warped_motion 扩展到 switch 帧。
- 新增 `Av1RefSlotState`（order hint/尺寸/GM）与可选参数
  `av1_parse_frame_header(data, seq, ref_slots?=...)`。

向量 `libavcodec/av1_inter_hdr_vectors/`（docker libaom）：testsrc2
-crf 30 的普通 P 帧（identity GM，检验全字段 + tile group 分裂的位
精确性）与 mandelbrot -crf 20 的 P 帧（**LAST 带 ROT_ZOOM 矩阵
[73728,67584,64458,72,-72,64458]，subexp 路径实测**；后续 P 帧
**skip_mode 开启 refs=[0,6]**，覆盖 skip 推导分支）。两向量共用
8-slot 参考池模拟 key 0xFF 刷新。libavcodec 主测试包的 Wasm init
已到局部变量上限，向量放独立包（同 av1_tiles_vectors 惯例）。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning；
`moon test --target wasm --no-render` **887/887**。剩余：refmvs、
帧间块语法、参考帧生命周期与整帧解码入口。

## 71. 2026-10-26 AV1 NEWMV 残差解码原语

新增 `libavcodec/av1_inter.mbt`：移植 dav1d 1.5.1 `decode.c` 的
`read_mv_component_diff` 与 `read_mv_residual`——joint（3 符号）+
每分量的 sign/classes(10)/class0/class0_fp/class0_hp 或
classN[cl]/classN_fp/classN_hp 链，`mv_prec < 0`（整像素）跳过
fp/hp 位；CDF 用 `Av1CdfCtx` 的 mv0/mv1 平铺表（comp[1] 与 comp[0]
同源，dav1d copy 规则）。MVJoint 映射：H=1→x 分量、V=2→y 分量。

C 对拍（docker vpxdev-img）：`dump_mvres.c` 逐字拷贝 dav1d 的两个
函数 + `msac.c`/`cdf.c` 直编（mem pool 链接桩），经公开的
`dav1d_cdf_thread_init_static/copy` 取默认 MV CDF（default 表是
file-static 不能直接引）；LCG 字节流 × 4 组（seed/精度组合）×
400 步，逐步对拍 y/x/rng/dif。`av1_mvres_vectors/`（emit：
`scripts/_emit_av1_mvres_vectors.py`）全部一致。

调试记录（教训）：(1) dav1d 的 `class0_hp` 读取**不带 up 索引**
（永远用行 0，classN_hp 同理）——初版按直觉传了 up 导致第 2 步
rng 分叉；(2) C dump 的 `dav1d_msac_init` 第 4 参是
**disable_cdf_update_flag**，误传 1 得到"永不更新的 CDF"，
与 MoonBit 的允许更新在首步后表值分叉——用"在 i=1 前打印两侧
joint 表"定位；(3) CDF 宏 `CDF1(x)=(32768-x)`，dav1d 内存即镜像
表示，与 MoonBit 提取表一致，勿按 cdf.c 字面值对照。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning；
`moon test --target wasm --no-render` **892/892**。帧间块语法主体
（skip_mode/is_comp/refidx/comp 类型/interintra/motion_mode）与
refmvs 仍待移植。

## 72. 2026-10-26 AV1 refmvs 候选查找（空间/时域 + gmv + 上下文）

新增 `libavcodec/av1_refmvs.mbt`：移植 dav1d 1.5.1 `src/refmvs.c` 的
`dav1d_refmvs_find` 主体——空间候选（scan_row/scan_col 的单块直扫与
多块步进两种权重形态、top-right/top-left、8x8 分辨率的 secondary
行列）、时域候选（rp_proj 8x8 网格投影 + 右下角补点，首点探
globalmv_ctx）、`mv_projection`（7.9.3 舍入/钳位）、`get_gmv_2d`
（translation/rot-zoom/affine 的块中心投影）与 `fix_mv_precision`；
nearest/secondary 两段稳定冒泡排序、compound 的 same/diff 扩展候选
合并（**dav1d 的 `same` 是 mvstack 尾部别名**，cnt=1 时槽位偏移）、
single 扩展候选、±(block+4)*32 边界钳位与 ctx 输出（单参考
`(refmv<<4)|(gmv<<3)|newmv`，compound 的三分支映射）。

C 对拍（docker vpxdev-img）：`dump_refmvs.c` 直接 include 真
`refmvs.c`，LCG 构造 37×48 空间块阵 + 16×40 时域块（24 seed ×
14 尺寸 × 3 ref 组合 = 1008 例，覆盖单/双参考、bs 1..32、
ROT_ZOOM/TRANSLATION GM），外加 12 组 gmv_2d 与 16 组 projection
单元向量；`av1_refmvs_vectors/`（emit：
`scripts/_emit_av1_refmvs_vectors.py` + 模板 `_av1_refmvs_test_body.tpl`）
全部一致。调试记录：(1) C dump 初版漏设 `use_ref_frame_mvs`，
时域路径被静默跳过（首例 cnt 8 vs 3 暴露）；(2) compound 扩展候选
初用独立数组，cnt=1 时错覆 stack[0]——dav1d 的 same 指针别名语义
是关键；(3) 测试 bs 表把 BS_64x32 误写为 5（枚举值是 4）；
(4) 单参考时 C dump 的 mv1 是未初始化栈内存，emit 侧归零对拍。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning；
`moon test --target wasm --no-render` **894/894**。剩余：
refmvs 的 init_frame/load-save_tmvs/splat（状态机接线）、帧间块
语法主体与整帧解码入口。

## 73. 2026-10-26 AV1 refmvs 帧初始化（sign 偏置与 mfmv 选择）

`av1_refmvs.mbt` 新增 `av1_refmvs_init_frame`（dav1d 同名函数的
标量部分，单 tile/帧线程路径）：7 个参考的 sign_bias/mfmv_sign/
pocdiff（wrapped order-hint 差与 ±31 钳位），以及时域 MV 的参考
选择——last（slot0，除非 alt-of-last==gold）、bwd(4)/altref2(5)/
altref(6)（须为过去帧）、last2(1) 兜底，共 total 个；ref2cur 取
±diff1（INT_MIN 表示超界）并填 ref2ref 的 21 个投影距离
（unsigned >31 归零）。

C 对拍扩展进 `dump_refmvs.c`：20 组随机 ref_poc/rp_ref/
ref_ref_poc（7-bit order hint 环绕），逐字段对拍 n_mfmvs、三组
偏置、pocdiff、mfmv_ref/ref2cur 与 21 个 ref2ref。调试记录：
(1) **rp_ref 的 has 语义**——dav1d `rnd()%3==0` 是 NULL（无时域），
初版条件写反；(2) F 行的 `ref:cur` 对与 ref2ref 列表间需显式
分隔符，否则解析粘连；(3) 期望表去掉行首标记后 token 下标整体
-1。测试基建：**rnd 状态从全局改为 build 返回尾状态**（MoonBit
测试并行，全局可变状态会数据竞争）。

`moon check --target wasm --deny-warn --no-render` 0 error/0 warning；
`moon test --target wasm --no-render` **895/895**。剩余：
load/save_tmvs、splat_mv、sbrow_init（与 decode 接线）、帧间块
语法主体与整帧解码入口。

## 74. 2026-10-26 AV1 refmvs 行状态与帧间参考上下文

新增 `av1_refmvs_state.mbt`：单线程路径的 37 槽行指针环（MoonBit
用共享行数组表示）、`splat_mv_c`、`save_tmvs_c`、`load_tmvs_c`。
保存遵循 dav1d 的第二参考优先级与 4096 MV 范围检查；加载按
ref2cur/ref2ref 投影，并保持相邻相同 MV 块的 x 快进语义。
`Av1RefmvsFrameState` 串联 sbrow begin/load、find/splat、end/save，
并持有可供后续参考帧使用的 rp 时域图。
另在 `av1_inter_ctx.mbt` 增加帧间参考树、复合方向、滤波器、
mask、jnt 与 DRL 上下文；`av1_inter.mbt` 增加单/双参考索引的
算术解码树，以及单/双参考模式、DRL 与 MV 解码原语。

直接用 dav1d 1.5.1 `refmvs.c` 编译的 C 探针
（`.scratch/av1/state_ref.c`，不入库）核对 64x64 superblock 的
sby=0/1 行映射、双参考保存和时域投影。C 输出 `save -30 40 2
11 22 1`、`load 0 0 1`，与 MoonBit 独立测试一致。
参考树另用 `dump_mvres.c` 的真 MSAC/CDF 构建 C 探针，四种
64 字节输入 × 单/双参考共 8 例，参考索引均与 MoonBit 相同。
核对 `refmvs.h` 发现早期交接文档对 `save_tmvs` 实参的描述有误：
真实调用传 `rt->r + 6`，函数内部再用 `(y & 15) * 2` 选行。

**尚未完成**：状态与 `decode_b` 接线、inter 块语法和重建、参考帧
生命周期、整帧入口、P/B 帧 framemd5 对拍。此阶段不能声称 AV1
迁移完成。`moon check --target wasm --deny-warn --no-render` 无警告，
`moon test --target wasm --no-render` **904/904**。

## 75. 2026-10-26 AV1 refmvs 缩放投影与跨帧传播验证

新增 refmvs 测试覆盖非零 MV 的 LAST/BWD 双向时域投影、2:1 参考帧距离
缩放、8×8 时域 superblock 边界、128×128 superblock 的空间行环，以及
“当前帧 splat/save → 下一帧 load/find”完整传播链。直接编译 dav1d 1.5.1
`refmvs.c` 的 C 探针确认两种方向的投影坐标与保存的原始 MV 一致。
`mv_projection` 的倍率表改为模块级常量，避免每次候选查询重复创建数组。

`moon check --target wasm --deny-warn --no-render` 无警告，
`av1_refmvs_vectors` **16/16**，全量测试 **907/907**。`moon fmt --check` 在当前 Windows 路径下
因 formatter 生成文件路径不可读而失败；`moon info -p libavcodec` 通过。

## 76. 2026-10-26 AV1 帧间块 MV 入口与参考上下文修正

`av1_inter_ctx.mbt` 修正两个对照 dav1d `env.h` 发现的参考上下文偏差：
只有一个帧内邻块时，`get_comp_ctx` 要把参考索引 `-1` 按 C 的无符号
比较视为后向；`av1_get_bwd_ref_ctx` 的计数顺序是 ALTREF 对 BWD +
ALTREF2，原实现反向。增加七棵参考树对单个参考索引 0..6 的完整
49 格期望值测试。

`av1_refmvs_state.mbt` 增加与 dav1d `splat_intraref`、
`splat_oneref_mv`、`splat_tworef_mv` 对应的发布方法，保存参考索引、
NEWMV/GLOBALMV 标记与 MV。新增 `av1_inter_motion.mbt`，串联复合标志、
单/双参考选择、refmvs 候选、skip mode 及单/双 MV 模式读取；块内其余
语法读取与重建仍需接入 `decode_b`。定向测试覆盖发布标记、skip mode
不消耗算术码以及 segmentation 强制参考的全局 MV。
`moon check --target wasm --deny-warn --no-render` 无警告，
`av1_refmvs_vectors` **22/22**，全量测试 **913/913**；
`moon info -p libavcodec` 通过。

## 77. 2026-10-26 AV1 帧间 MC 坐标桥接

已有 `av1_mc_core` 具备经 dav1d C 向量对拍的 8-tap、bilinear、
复合、缩放、OBMC 和 warp 内核；本节补上 `predict.mbt`，从块 4×4 坐标、
luma 1/8 像素 MV、色度 subsampling 和参考帧尺寸推导单参考 MC 的源像素
位置、1/16 相位，以及缩放路径的 1/1024 起点和步长。提供单参考像素块
预测和复合预测 prep 两个入口，边界由现有内核执行最近边缘像素扩展。
定向测试涵盖负 MV 的 luma/chroma 相位、80×48 参考帧对 64×64 当前帧的
缩放坐标，以及预测/prep 与内核的逐像素一致性。
`av1_mc_vectors` **15/15**，全量测试 **916/916**；
`moon check --target wasm --deny-warn --no-render` 与
`moon info -p av1_mc_core` 通过。

尚需在帧间块解码入口接入参考帧图像、剩余 inter 语法、复合模式与残差
重建，才能解出 P/B 帧。

## 78. 2026-10-26 AV1 refmvs 参考槽位与跨帧投影

新增 `av1_refmvs_refs.mbt`，管理 AV1 的 8 个参考槽位。完成帧的
时域 MV 图及其 7 个参考 order hint 随 refresh mask 保存；下一帧从帧头
的 `ref_frame_idx` 取槽位、检查参考图尺寸，并构造 refmvs 初始化参数。
尺寸判断按 dav1d 的 8 像素向上取整规则。`prepare_header` 将已解析的
inter/switch 帧头直接接入这一流程，包含全局运动、MV 精度与
superblock 尺寸。

测试验证 key 帧槽位刷新、前帧 MV 图保存到后帧 LAST 投影、2:1 距离
缩放、尺寸不匹配时禁用时域图、四个连续 superblock 行的空间环与时域
保存，以及 LAST/BWD 投影相撞时后者覆盖前者。帧头向量测试也调用
`prepare_header`。参考槽位生命周期目前是可供帧间解码器调用的独立
模块；`decode_b` 尚未接入帧间块语法和重建，P/B 帧解码仍未完成。
`moon check --target wasm --deny-warn --no-render`、`moon info -p libavcodec`
通过；`av1_refmvs_vectors` **26/26**，帧头向量 **2/2**，全量测试
**920/920**。

## 79. 2026-10-26 AV1 refmvs 状态机随机对拍与上采样宽度语义

新增 `libavcodec/av1_refmvs_vectors/refmvs_state_parity_test.mbt`（生成器
`scripts/_emit_av1_refmvs_state_vectors.py`）。探针
`.scratch/av1/state_dump.c`（不入库）直接 `#include` dav1d 1.5.1
`refmvs.c`，取 `dump_refmvs.c` 的 static 符号，覆盖五组几何：
64×80/64×56/128×80/128×96/64×72 等（各含奇数尾行与 sb64/sb128），
每个 sbrow 依次 `tile_sbrow_init`、8 次随机 `splat_mv_c`（随机
ref/bs/mf、±4200 MV 触发 4096 保存门限）、`save_tmvs_c`，对
37 槽行环（含奇偶 sby 交换，跳过 dav1d 的 NULL 槽 1/3）、当前 rp
和 16 行 rp_proj 做 31 进制模 1e9+7 哈希；随后每组几何再用三组
`load_tmvs_c`：整幅、部分列 + SB 对齐的次行带、超宽列（内部截断），
源时域图按 3 列 × 2 行的等值游程构造，MV 为 ±64 的倍数，覆盖
正/负投影、ref2cur/ref2ref 缩放、跨 8×8 时域块边界与游程快进。
5 例空间哈希与 15 例投影哈希全部与 C 一致。

对拍陷阱：`load_tmvs` 的 `row_start8` 必须是 superblock 行对齐。
dav1d 清行时用 `&rf->rp_proj[(row_start8 & 15) * stride]` 再线性前进
`row_end8 - row_start8` 行；行带若从 10 起且长 10 行会越过 16 行
环形缓冲（C 侧越界写）。MoonBit 按 `(y & 15)` 逐行取模，只在合法
SB 行带上与 C 等价，因此探针改用对齐行带；这不是移植偏差。

修正 refmvs 宽度语义：dav1d `dav1d_refmvs_init_frame` 的
`iw8/rp_stride` 与参考尺寸门都用 `frame_hdr->width[0]`（superres
上采样后的宽度），不是编码宽度 `width[1]`。`Av1RefmvsReference` /
`Av1RefmvsPreparedFrame` 的 `coded_width` 更名 `upscaled_width`，
`prepare_header` 改传 `hdr.upscaled_width`；尺寸门仍先按 8 像素
向上取整。新增测试验证两个上采样宽度同为 128 的 superres 帧可共享
时域 MV 图，而编码宽度相同、上采样宽度 119 的参考被排除。

`moon check --target wasm --deny-warn --no-render` 无警告，
`av1_refmvs_vectors` **32/32**，帧头向量 **2/2**，全量测试 **926/926**。

## 80. 2026-10-26 AV1 帧间块复合/inter-intra/motion/filter 语法

新增 `av1_inter_syntax.mbt`，补齐 `decode_b` 帧间分支里 MV 之后的语法：

- 复合 mask：`mask_comp` 判定 seg/wedge，`jnt_comp`（weighted/avg），
  `wedge_comp` + `wedge_idx`（16 符号），以及等概率 `mask_sign`；
- inter-intra：`interintra` 门、4 值 `interintra_mode`、
  `interintra_wedge` 与楔形索引；
- motion variation：可 warp 时三值 `motion_mode`，否则 `obmc` 布尔；
- subpel 滤波器：可切换时按 `filter[dir][ctx]` 读水平/垂直滤波器
  （dual_filter 才读第二个），输出 dav1d `Filter2d` 组合索引。

同时移植 `dav1d_wedge_ctx_lut`、`dav1d_filter_2d` 与
`wedge_allowed`/`interintra_allowed` 尺寸门（32x8、8x32 不参与
inter-intra）。CDF 行宽与 dav1d 一致：interintra_mode/motion_mode/
filter 为 3 符号、wedge_idx 为 15 符号。

对拍探针 `.scratch/av1/inter_syntax_dump.c`（不入库）把 dav1d 1.5.1
`decode_b` 的对应片段原样拷出，用 `dump_mvres.c` 的 MSAC/CDF 设施跑
4 组 LCG 字节流 × 256 次随机上下文调用，逐例比较输出并在每组末尾
校验 `msac.rng`，生成器
`scripts/_emit_av1_inter_syntax_vectors.py` → 独立测试包
`av1_inter_syntax_vectors`（4/4）。

坑：`Dav1dFilterMode` 的顺序是 REGULAR=0、**SMOOTH=1、SHARP=2**
（不是 regular/sharp/smooth），因此 `dav1d_filter_2d` 的三行是
`{0,1,2}`、`{6,7,8}`、`{3,4,5}`。最初按字面初始器猜成
`{0,2,1}`，C 对拍在 subpel_mode=2 时立即暴露；另外
`interintra_wedge` 表只有 7 行，靠尺寸门排除 wedge_ctx 7/8 才不越界。

`moon check --target wasm --deny-warn --no-render` 无警告，
`av1_inter_syntax_vectors` **4/4**，全量测试 **930/930**。

## 81. 2026-10-26 AV1 warp 邻居匹配与仿射模型推导

新增 `av1_warpmv.mbt`（dav1d `src/warpmv.c` 完整移植）：257 项
`div_lut`、`resolve_divisor_32/64`、`iclip_wmp`、
`get_shear_params`（alpha/beta/gamma/delta 与发散判定）、
`find_affine_int`（int64 最小二乘、对角/非对角权重裁剪）以及
`set_affine_mv2d`（帧级 GM 的 warp 锚定）。

新增 `av1_inter_warp.mbt`：`find_matching_ref` 在 refmvs 行环上沿
块尺寸步进扫描上/左邻域，输出两段 32 位游程掩码（bit 32 表示
top-left / top-right），最多 8 个候选；`derive_warpmv` 按掩码取样
（含 `-off` 左对齐特例）、以 `4 * clip(max(bw4,bh4),4,28)` 为阈值
剔除离群样本、压缩后做仿射拟合，拟合或 shear 检查失败时回退
identity。至此 `motion_mode == MM_WARP` 的矩阵来源齐备（warp MC
核已在 §69 就绪）。

对拍探针 `.scratch/av1/warp_dump.c`（不入库）**直接链接 dav1d
`warpmv.c`**，并原样拷贝 `decode.c` 的 `find_matching_ref` /
`derive_warpmv`（仅把 `t->rt.r`/`t->bx`/`t->by`/`t->ts->tiling.col_end`
换成探针全局）；覆盖 64 个随机矩阵的 shear、64 组点集的最小二乘、
32 组 `set_affine_mv2d`、96 组合成行环的掩码+推导（矩阵/type/
shear 全比较）。生成器 `scripts/_emit_av1_warpmv_vectors.py` →
独立测试包 `av1_inter_warp_vectors`（4/4）。

坑：①拷贝 `derive_warpmv` 时 `bs()` 宏定义在 `find_matching_ref`
尾部被 `#undef`，推导函数内要重新 `#define`；②`find_matching_ref`
假定 `have_top/have_left` 与块位置一致，随机测试若在 `bx=0` 置
`have_left` 会读 `[-1]` 列（C 越界、MoonBit 越界 panic）；
③合成行环的 `by` 需满足 `(by&31)+h4 <= 32`（真实解码中块不跨
superblock 行），否则行环槽位越界。

`moon check --target wasm --deny-warn --no-render` 无警告，
`av1_inter_warp_vectors` **4/4**，全量测试 **934/934**。

## 82. 2026-10-26 AV1 帧间环路滤波掩码（decomp_tx / mask_edges_inter）

补齐 `lf_mask.c` 的 inter 分支：`av1_decomp_tx` 按 `tx_split` 递归
展开每个 4x4 的 tx 宽度（log2 截到 2）与步长上下文；
`av1_mask_edges_inter` 写块左/上边与内部 tx 边（skip 时跳过内部边），
并回写 a/l 的右/下 tx 宽度；`av1_mask_edges_chroma` 增加 `skip_inter`
参数（intra 调用传 false），从而与 dav1d 的 chroma skip 语义一致。
新增自由函数 `av1_create_lf_mask_inter`（显式传入 4x4 级缓存、
b4_stride、帧宽高、色度移位、mask 数组与 a/l），供后续帧间块解码
调用；intra 路径保持不变。

对拍探针 `.scratch/av1/lfmask_dump.c`（不入库）直接 `#include`
dav1d 1.5.1 `lf_mask.c`，调用真实 `dav1d_create_lf_mask_inter`，
覆盖 8 组合法 block/tx、48 个随机位置、skip、`tx_split`、四种
pixel layout；对 `filter_y`/`filter_uv`/级缓存/a/l 做哈希比较。
生成器 `scripts/_emit_av1_lf_mask_vectors.py` → 独立测试包
`av1_lf_mask_vectors`（1/1，48 组）。

坑：①C 的 chroma 分支只看 `auv != NULL`，不看 layout，I400 调用者
必须传 NULL；探针起初无条件传数组，导致 I400 的 chroma 哈希与
MoonBit 不符；②`case_set_upto16` 的写入个数是 `1<<t_dim->lw`
（未截断），而写入值是 `min(2, lw)`；③`dav1d_memset_pow2[lw]`
写 `1<<lw` 个字节的 `t_dim->h`，两者都按未截断的 lw 取个数；
④最初把 inter 包装改成公开方法时误伤了 intra 函数（同名片段被
全局替换），已按函数切片修复并由关键帧真实码流测试回归。

`moon check --target wasm --deny-warn --no-render` 无警告，
`av1_lf_mask_vectors` **1/1**，全量测试 **935/935**。

## 83. 2026-10-26 AV1 帧间块语法驱动接线

新增 `av1_inter_block.mbt`：`Av1InterTileState` 维护逐 4x4 的
上/左邻域（dav1d `BlockContext` 的 inter 子集，含 `set_block` 与
`findoddzero` 等价的重叠邻块判定）；`av1_inter_read_block` 按
dav1d `decode_b` 顺序串联参考/MV（`av1_inter_read_block_motion`）、
复合 mask、inter-intra、motion variation（含 `find_matching_ref`/
`derive_warpmv` 与 `allow_warp`）、subpel 滤波器，并把最终块发布到
refmvs。返回的 `Av1InterBlock` 含 refs/mode/DRL/MV/复合类型/wedge/
mask 符号/inter-intra/motion mode/warp 矩阵/滤波器/掩码与 refmvs
find 上下文，可直接供重建使用。

C 源码对照发现一处关键语义：dav1d 1.5.1 的 inter-intra 与 motion
variation **位于单参考 `else` 分支内部**，复合（含 skip mode）块不读
这两个语法；驱动据此只在 `ref1 < 0` 时读取。另修正
`av1_inter_comp_ctx` 单邻块分支：dav1d 对 `ref[0] >= 4` 使用有符号
比较，帧内邻块（-1）不算后向（此前 §76 的测试假设相反，已更新）。

对拍进展（探针 `.scratch/av1/interblock_dump.c` + 生成器，均不入库）：
直接拷贝 dav1d `decode_b` 的 inter 分支（1383–1872 行）并复用真实
`env.h` 上下文函数、refmvs 与默认 CDF，96 组随机码流/邻居/行环下，
**已解码语法字段、refmvs find 上下文、warp 掩码与 `allow_warp` 全部
与 C 一致**；但算术解码器的 `msac` 最终状态仍有差异，尚未定位到
具体 CDF 选择，故本轮不提交该对拍测试，仅提交确定性单元测试
（skip mode 零符号消费、复合块不读 inter-intra/motion、发布结果与
邻域更新）。该差异意味着驱动尚不能宣称位级对拍完成，接线整帧入口
前需继续收敛。

`moon check --target wasm --deny-warn --no-render` 无警告，
`av1_refmvs_vectors` **34/34**，全量测试 **937/937**。

## 84. 2026-10-26 AV1 帧间块驱动 C 位级对拍收敛

上一节遗留的 `msac` 状态差异定位到参考树上下文，并顺带修正两处
真实偏差：

1. `av1_inter_ref_tree_ctx` 的 **bwd 变体（tree 4）比较方向写反**
   （应等价于 `cnt[0]+cnt[1]` 与 `cnt[2]` 比较，0 表示前者更小）；
   `ref_2_ctx` 即 bwd 上下文，`ref_3/4/5/6` 分别复用
   fwd/fwd_1/fwd_2/bwd_1 变体（与 env.h 的宏别名一致）。
2. dav1d 的 `get_mask_comp_ctx` / `get_jnt_comp_ctx` / `get_filter_ctx`
   **不使用 have_top/have_left**，直接读 a/l 数组（tile 边界预置为
   intra）；驱动此前对缺失邻居传默认 intra 邻块，随机上下文中会选错
   上下文。改为始终传数组值后，msac 状态与 C 完全一致。

修正后，完整对拍（96 组随机码流/邻居/refmvs 行环）在
`scripts/_emit_av1_inter_block_vectors.py` 生成、测试包
`av1_inter_block_vectors` 中比较：refs、复合类型/wedge/mask 符号、
mode/DRL/MV、inter-intra、motion mode、warp type/矩阵、滤波器、
refmvs find 上下文、`msac.rng` 与 `dif>>32`、warp 掩码与
`allow_warp` —— **全部与 dav1d 一致**。生成器复用
`scripts/_av1_inter_block_test_body.tpl`（测试体与数据分离）；探针
`.scratch/av1/interblock_dump.c`/`interblock_gen2.py` 不入库。

`moon check --target wasm --deny-warn --no-render` 无警告，
`av1_inter_block_vectors` **1/1**（96 组），
`av1_refmvs_vectors` **34/34**，全量测试 **938/938**。

## 85. 2026-10-26 AV1 warp 平面预测驱动

`av1_mc_core/warp.mbt` 新增 `av1_mc_warp_predict_plane`：按 dav1d
`recon_tmpl.c` 的 `warp_affine` 逐 8x8 子块计算变换中心
（`src_x/src_y` 含色度移位）、`abcd`（矩阵 2/3/4/5 减 0x10000）与
相位 `mx/my`（含 alpha/beta/gamma/delta 修正并 `& ~0x3f`），再调用
已对拍的 `warp_affine_8x8` 核；边界由核内最近像素扩展等价于
dav1d `emu_edge`。支持 4:2:0/4:2:2/4:4:4 与 8/10/12-bit。

新增 `av1_mc_vectors/warp_plane_test.mbt`：恒等矩阵逐像素等于源块、
整像素平移（含负向平移的边界钳制）、4:2:0 色度按 2 倍子采样相位
取样。该驱动补齐了 `MM_WARP` 从语法/矩阵推导到像素预测的最后一环；
帧间重建仍缺 OBMC 驱动、inter-intra 混合、复合 mask 合成与残差叠加。

`moon check --target wasm --deny-warn --no-render` 无警告，
`av1_mc_vectors` **18/18**，全量测试 **941/941**。

## 86. 2026-10-26 AV1 复合预测合成与 warp prep 平面驱动

`av1_mc_core/compound.mbt` 新增 `av1_mc_compound_blend`：按 dav1d
`recon_inter_block` 的分派把两块 `prep` 合成——AVG（`avg`）、
WEIGHTED_AVG（`w_avg`，权重来自 `jnt_weights`）、SEG（`w_mask`，
`mask_sign` 交换输入并参与掩码）与 WEDGE（`mask`，掩码由调用方按
亮度/色度选择，输入按 `mask_sign` 交换）。WEDGE/SEG 的输入顺序与
dav1d `tmp[mask_sign]` / `tmp[!mask_sign]` 一致。

`av1_mc_core/warp.mbt` 新增 `av1_mc_warp_prepare_plane`：与
`av1_mc_warp_predict_plane` 相同的 8x8 相位/ABCD 遍历，改用
`warp_affine_8x8t` 输出复合预测精度的 `prep` 平面（全局 warp 复合的
两参考准备）。

测试（`av1_mc_vectors/compound_blend_test.mbt`）：AVG 与直接 `avg` 核一致；
WEIGHTED_AVG 使用传入权重且忽略 `mask_sign`；WEDGE 在 `mask_sign=0/1`
下分别等价于 `mask(a,b)` / `mask(b,a)`；SEG 等价于 `w_mask` 的对应
符号与输入顺序；warp prep 8x8 与直接 `warp_affine_8x8t` 一致。

`moon check --target wasm --deny-warn --no-render` 无警告，
`av1_mc_vectors` **21/21**，全量测试 **944/944**。

## 87. 2026-10-26 AV1 帧间块预测装配（单/复合/warp）

新增 `av1_recon_inter.mbt`（libavcodec 现依赖 `av1_mc_core`）：
- `Av1InterBlock` 增加 `bs` 与派生 warp 的 alpha/beta/gamma/delta；
- `av1_jnt_comp_weight`：dav1d `jnt_weights` 的距离量化权重
  （`quant_dist_weight`/`lookup` 搜索）；
- `av1_recon_predict_block`：单参考按 `filter2d` 走 put 平面预测，
  `motion_mode=MM_WARP` 且仿射有效时走 warp 平面预测（8x8 相位 +
  派生剪切参数）；复合参考先对两帧做 `prep`，再按
  `av1_mc_compound_blend` 合成（AVG/W_AVG/WEDGE/SEG，WEDGE 掩码
  亮度用 sign 0、色度用 `mask_sign`）；亮度/色度分别按 4:2:0/4:2:2/
  4:4:4 取样。inter-intra、OBMC 与复合 GLOBALMV warp 暂显式返回错误，
  色度 sub8x8 派生也未接入。

新增测试包 `av1_recon_vectors`（3/3）：单参考逐平面与 MC 核一致、
复合 AVG 与 `prep+avg` 一致、WEDGE(sign=1) 与交换输入的 `mask` 一致、
未支持路径返回错误，以及两组 jnt 权重（5/11）核对。

`moon check --target wasm --deny-warn --no-render` 无警告，
全量测试 **947/947**。

## 88. 2026-10-26 AV1 inter-intra 混合与 OBMC 驱动

`av1_recon_inter.mbt` 扩展：
- **inter-intra**：`interintra_type` 为 BLEND 时用 `av1_ii_mask`
  （按 interintra_mode），WEDGE 时用 `av1_wedge_mask(layout, bs, 0,
  wedge_idx)`（II_MASK 语义：亮度 sign 0、色度同样 sign 0），再用
  `blend_pixels` 以掩码加权 intra 样本；由 `av1_recon_predict_block`
  的 `intra` 参数接入，缺失时返回错误。
- **OBMC**：按 dav1d `obmc()` 遍历上/左邻块（refmvs 行环、邻块
  `bs` 尺寸 clip 2..16、每侧最多 `min(ulog2(w4/h4),4)` 个重叠、
  top 高度 `min(bh4,16)>>1` 且预测高度 `(oh4*3+3)>>2`、left 宽度
  `min(bw4,16)>>1`），邻块滤波器由 `Av1InterTileState` 的 filter0/1
  合成 `Filter2d`；重叠块经单参考预测后用 `blend_h`/`blend_v` 写回
  目标子块。色度要求边界像素和 ≥16（dav1d 条件）。`obmc` 参数缺失
  且 `motion_mode=MM_OBMC` 时返回错误。

新增测试（`av1_recon_vectors` 5/5）：II BLEND 逐平面等于
`blend_pixels(inter, intra, ii_mask)`；OBMC 顶部邻块的重叠预测与
直接 `predict + obmc_horizontal` 一致，且 8x8 4:2:0 块按 dav1d 条件
跳过色度 OBMC。

已知未接：色度 sub8x8 派生（bw4==ss_hor/ bh4==ss_v 时邻块 MV 推导）、
复合 GLOBALMV warp、残差系数与加法、`decode_b`/整帧入口。

`moon check --target wasm --deny-warn --no-render` 无警告，
全量测试 **949/949**。

## 89. 2026-10-26 AV1 色度 sub8x8 预测派生

`av1_recon_inter.mbt` 新增 `av1_recon_sub8x8_chroma`：单参考下
4x4/4x8/8x4 luma 块（`bw4==ss_h` 或 `bh4==ss_v`，且仅 4:2:0/4:2:2）
的色度预测按 dav1d `recon_b_inter` 的 sub8x8 规则从邻块运动派生：
- 检查左/上/左上邻块 `ref0>0`，缺任一所需邻居则回退普通块级色度预测；
- 4x4 块由左上、左、上、自身 MV 各预测 2x2 象限拼成 4x4 色度单元；
  4x8/8x4 分别由左/上邻块与自身各占一半；
- 邻块滤波器由 `Av1InterTileState` 的 filter0/1 合成（左上取 `top[bx4-1]`，
  左取 `left[by4]`，上取 `top[bx4]`）；
- 输出为覆盖该 8x8 luma 单元的色度尺寸（4x4 或 4x8），
  `Av1ReconPlanes.cw/ch` 同步修正；
- 若需要 sub8x8 但调用方未提供邻域状态则返回错误（避免静默错像素）。

测试（`av1_recon_vectors` 7/7）：4x4 块的四个象限逐像素等于
左上/左/上/自身 MV 的直接色度预测；缺少邻域时返回错误；既有 II/OBMC
与单/复合用例保持通过。

`moon check --target wasm --deny-warn --no-render` 无警告，
全量测试 **951/951**。

## 90. 2026-10-26 AV1 帧间变换类型选择（txtp）

`av1_decode2.mbt` 把 `decode_coefs` 里的变换类型选择抽成
`av1_decode_txtp`，并补齐帧间路径（此前只实现帧内分支）：
- 早退：`t_dim->max + intra >= TX_64X64` 直接 DCT_DCT；色度先走
  `get_uv_inter_txtp`（32x32 上限只允许 IDTX，16x16 下限剔除
  H/V_FLIPADST 与 H/V_ADST），帧内色度仍用 `dav1d_txtp_from_uvmode`；
- 帧间亮度 `qidx==0` 隐式 DCT_DCT；
- `reduced_txtp_set || max==TX_32X32`：`txtp_inter3[min]` 单 bool
  （`idx ? DCT_DCT : IDTX`）；
- `min==TX_16X16`：`txtp_inter2`（11 符号、基址 12）；
- 其余：`txtp_inter1[min]`（15 符号、基址 24）；
- `decode_coefs` 增加 `luma_txtp` 参数，色度帧间由亮度类型派生；
  无损块仍直接 WHT_WHT，全跳过返回 DCT_DCT。

对拍：`.scratch/av1/txtp_dump.c` 直接链接 dav1d 的 msac/cdf 表，按
2 个 LCG seed × 256 组随机 `(intra, plane, tx, qidx_zero, reduced,
y_mode, y_angle, uv_mode, luma_txtp)` 调用 `txtp_select`，dump
返回值、`msac.rng`、`dif>>32` 与该步实际 CDF 概率；生成器
`scripts/_emit_av1_txtp_vectors.py` 产出
`av1_inter_syntax_vectors/txtp_test.mbt`（逐例校验 txtp/rng/dif）。

踩坑：C 探针的 `CdfContext` 在两个 seed 之间**不重置**（只在最外层
`dav1d_cdf_thread_copy` 一次），最初生成的测试每个 seed 新建 CDF，
seed 1 的 k=281 首次复用 `txtp_inter3[2]` 时概率仍为默认 30770，
而 C 已被 seed 0 自适应到 29321，rng 随即分叉（48964 vs 46720）。
把 CDF 提出 seed 循环后完全一致——这不是实现错误，而是对拍夹具的
状态生命周期写错。

`moon check --target wasm --deny-warn --no-render` 无警告，
全量测试 **952/952**。

## 91. 2026-10-26 AV1 帧间 var-tx 分区树

`av1_tx_tree.mbt` 移植 dav1d `read_vartx_tree`/`read_tx_tree`：
- 帧间块不做逐块 `txsz` 读取，而是从 `max_txfm_size_for_bs[bs][0]`
  开始按 `txpart` 递归二分（深度 < 2、且当前 tx 大于 8x8 时继续），
  记录 `tx_split0/tx_split1` 供重建与 LF 掩码使用；
- `txpart` 上下文取 `cat = 2*(TX_64X64 - t_dim->max) - depth` 与左右邻
  `a->tx`/`l.tx` 是否小于 tx 宽高（0..2）；
- 叶节点按 `1 << lw` 写 `a->tx`/`l.tx`（split 到 4x4 时写 TX_4X4），
  skip 或非 SWITCHABLE 时按块尺寸一次性填上下文；无损/coded-4x4 走
  TX_4X4 分支；
- `max_ytx`/`uvtx` 与 dav1d 一致（色度取 `max_txfm_size_for_bs[bs][layout]`，
  layout 0=I400/1=I420/2=I422/3=I444）。

对拍：`.scratch/av1/txtree_dump.c` 逐行拷贝上述两个函数（仅替换
tile context 为测试结构），2 个 LCG seed × 128 组随机
`(bs, skip, lossless, tx_mode, layout, bx/by/bw/bh)` 用例，dump
`max_ytx/uvtx/tx_split/rng/dif` 及调用后的 32+32 个 `tx` 上下文；
生成器 `scripts/_emit_av1_txtree_vectors.py` 产出
`av1_tx_tree_vectors/txtree_test.mbt`，256 组全部位级一致。
探针的初始上下文由 `(j*3+bs)%5` / `(j*5+seg)%5` 确定性生成，
避免额外转储。踩坑：dav1d `RectTxfmSize` 枚举顺序是
`RTX_4X16=13, RTX_16X4=14, RTX_8X32=15, RTX_32X8=16, RTX_16X64=17,
RTX_64X16=18`，与按 16x64/64x16/8x32/32x8 的直觉顺序不同（表已按此
转储，勿按名字重排）。

`moon check --target wasm --deny-warn --no-render` 无警告，
全量测试 **953/953**。

## 92. 2026-10-26 AV1 帧间残差遍历（系数树 + 色度）

`av1_inter_residual.mbt` 移植 dav1d `recon_b_inter` 的残差半段：
- `av1_read_coef_tree`：按 `tx_split0/1` 递归下探（深度 <2、父 tx >8x8），
  每叶读一个系数块（`Av1CoefReader` 注入，生产端接 `decode_coefs`），
  按 `imin(txw, bw-bx)` / `imin(txh, bh-by)` 写 `a->lcoef`/`l->lcoef`，
  并把 txtp 填满 `txtp_map` 的 `txh × txw` 区域；
- `av1_read_inter_residual`：luma 以 **块尺寸** 划分 16x16 初始化窗口、
  内层止于可见 `w4/h4`（`x_off/y_off = !!init`）；色度按 `uvtx`
  逐块循环，`luma_txtp` 从 `txtp_map[(by+(y<<ss_v))*32 + bx+(x<<ss_h)]`
  取，上下文写长度用 `((bw-bx+ss)>>ss)`；`has_chroma=false` 跳过色度。

对拍：`.scratch/av1/coeftree_dump.c` 逐行拷贝上述两段（仅把
`decode_coefs` 换成确定性 1 位读取器，并把 itxfm 换成叶子记录），
2 个 LCG seed × 64 组随机 `(bs, abx/aby, bw/bh, max_ytx, split0/1,
uvtx, layout)` 用例；比较叶子顺序哈希（相对坐标/tx/txtp/eob）、
`lcoef`/`ccoef` 上下文、`txtp_map` 区域哈希与 msac 终态。
生成器 `scripts/_emit_av1_inter_residual_vectors.py` 产出
`av1_inter_residual_vectors/residual_test.mbt`，128 组全部一致。

注意点：luma 外层窗口按 **块** 尺寸（含不可见部分）推进、内层才按可见
尺寸停止；色度上下文索引用块内 SB 偏移 + 色度相对坐标；`txtp_map`
以 4x4 luma 单元为粒度。真实 `decode_coefs` 已由关键帧整帧位精确覆盖，
本夹具只验证遍历与上下文，两者互补。

`moon check --target wasm --deny-warn --no-render` 无警告，
全量测试 **954/954**。

## 93. 2026-10-26 AV1 帧间块重建装配（预测 + 残差）

- `decode_coefs` 从 `Av1TileCtx` 方法抽成 `Av1CoefState` + 公开
  `av1_decode_coefs`：状态只含 msac/cdf/levels/dq_cur/seg_lossless/
  seg_qidx/reduced_tx_set/bpc/ss_h/ss_v，块信息改传
  `seg_id/y_mode/y_angle/uv_mode`（不再依赖私有 `Av1IntraBlock`）；
  tile 侧保留 `coef_state()` + 薄包装，帧内调用点不变。
- `av1_recon_predict_block` 新增 `mono` 可选参数：单色时跳过色度
  预测/子 8x8 派生/II 色度混合，避免访问空参考数组。
- 新增 `av1_recon_b_inter`（`av1_recon_inter.mbt`）：先经
  `av1_recon_predict_block` 预测并把整块（含边距）写回 `ctx.y/u/v`
  帧平面；skip 时按块尺寸把 lcoef/ccoef 置 0x40；否则用
  `Av1CoefReader` 包装 `av1_decode_coefs`，交给
  `av1_read_inter_residual` 遍历，apply 回调按
  `4*((by+ry)*stride + bx+rx)`（色度按 `(by>>ss_v)+cy` 等）直接在
  帧平面上执行 `av1_itxfm_add`。

测试（`av1_recon_vectors` 9/9，新增 2 个）：
- 8x8 单参考 skip 块：输出等于 `av1_recon_predict_block` 在相同偏移的
  拷贝，且 lcoef/ccoef 上下文被清为 0x40；
- 非 skip 块：用独立 msac/CDF 与显式坐标重建「预测 + 残差」期望，
  与 `av1_recon_b_inter` 的输出逐像素一致，且两路 msac 终态、lcoef/
  ccoef 上下文完全一致（并断言至少解码出一个系数块）。

`moon check --target wasm --deny-warn --no-render` 无警告，
全量测试 **956/956**。

## 94. 2026-10-26 AV1 分段 inter 特征、skip_mode 前置与邻域坐标修正

- **分段特征**（`av1.mbt`）：`Av1FrameHeader` 新增 `seg_ref`（原始 3-bit，
  -1 = 未编码）、`seg_skip`、`seg_globalmv`；解析 feature 5/6/7 时分别
  保存。`decode_b` 的 preskip 强制跳过改用 `seg_skip[seg_id]`——此前
  误用了 feature 5（REF_FRAME）的掩码位，是潜伏 bug（关键帧向量未触发）。
  `update_data=false` 的「从 primary ref 复制分段」尚未实现（inter 整帧
  入口时补）。
- **skip_mode 前置**（`av1_decode4.mbt`）：按 dav1d 顺序在 preskip seg_id
  之后、skip 之前读 `skip_mode`：仅当没有强制 ref/GMV/SKIP 分段、
  `skip_mode_enabled` 且 `min(bw4,bh4)>1` 时读取；
  `skip = skip_mode || seg_skip || cdf skip`。非 preskip 时 dav1d 的
  `seg == NULL`，因此不参与 skip_mode/skip 判定。inter 主体分支仍未接入
  （入口继续显式拒绝）。
- **邻域坐标语义修正**（`av1_inter_block.mbt` / `av1_recon_inter.mbt`）：
  `Av1InterBlockParams` 增加 `bx4_ctx/by4_ctx`——refmvs find/splat、
  `find_matching_ref` 与 OBMC 行仍用**绝对** 4x4 坐标，而
  `a/l` 邻域（mask/jnt/filter ctx、has_overlappable_neighbor、OBMC 邻块
  滤波器、色度 sub8x8 派生）用 `& 31` 的 SB 内坐标，与 dav1d
  `bx4 = t->bx & 31` 一致。既有 96 组块驱动对拍（坐标 <32 时两者相同）
  保持通过。

`moon check --target wasm --deny-warn --no-render` 无警告，
全量测试 **956/956**。

## 95. AV1 整帧前置状态：分段继承与 temporal 语法、CDF 快照、邻域坐标

- `av1_segment.mbt` 统一 decode_b 的空间/temporal/旧 map 分段读取：
  postskip 的 skip 块不消耗 seg_pred/seg_id 符号；旧 map 取可见块最小 id；
  真实 seg_pred 回写 a/l，cur_segmap 仅在 update_map 时更新。
  帧入口的 prev_segmap 仍为 None，会话选择和尺寸匹配还未接入。
- 帧头保留三个 segmentation 更新标志；新增 reference-header 快照，
  update_data=false 通过 ref_frame_idx[primary_ref] 继承全部 feature 数据，
  qidx/lossless 按新帧重新推导。使用真实 libaom P2 header 插入 feature
  syntax 验证三种更新组合、slot 1 间接引用和数组隔离；缺旧 header 明确报错。
- `Av1CdfCtx::copy` 深拷贝 82 个表；逐元素验证源、reference、tile 三者隔离。
  `set_block` 写入改为 &31，增加仅清 left 的 reset；跨 bank 回归保证 top 保留。
- 独立 `av1_segment_vectors` 包加入 96 组 dav1d 原文 C 分支对拍，比较
  决策、rng/dif 和两组 CDF 全表。新自动化均为 `.mbtx`，C 源来自
  dav1d 1.5.1 decode.c:169、494、816–944，未自造期望值。

这一步补齐状态原语；inter 主体、会话与端到端 MD5 仍未完成。接续说明见
[FRAME_STATE](AGENT_HANDOFF_2026-10-26_FRAME_STATE.md)。改动未提交、未推送。

`moon check --target wasm --deny-warn --no-render` 零警告，Wasm 全量 **960/960**。

## 96. AV1 帧间整帧与持久会话、真实多帧 MD5

`Av1Decoder::decode_packet` 现在维护 sequence、8 个参考槽位及图像/header/
CDF/segmap/refmvs 状态，支持隐藏帧与 show-existing。decode_b 接入 inter
语法、inter 帧内 intra、var-tx、预测/残差、LF、邻域与 segmap；通用帧驱动
按 sbrow × tile 共享时域投影，保留独立 tile CDF 与 above bank，继续使用
完整 deblock/CDEF/LR。原关键帧 API 保留。

参考 CDF 清真实 symbol-count 位置的计数，保留 padding，80 表与 dav1d
原始函数对拍；primary ref 同时用于 segmap 与 LF delta 继承。有效 frame
GM warp 接入单/双参考，按 plane 最小尺寸和参考缩放条件选择。

真实流修正了 refmvs header temporal bit 与 globalmv_ctx、时域坐标清低三位、
tile 左边界的左上候选、warp unsigned mask 比较、DSP rounded shear、
小色度 plane 的 warp 门控、sub8x8 fallback 与 segment compound 的亮度掩码
复用。新独立测试包用 5 组 libaom IVF 的 **35 个显示帧** 对拍 libdav1d
framemd5，覆盖普通 P 帧、2×2 tiles、10-bit、GM、隐藏/重排序帧，全部一致。

主包 Wasm 测试 init 达到 local 上限，合并既有 AAC height fixture 的相邻
base64 Bytes 字面量以减少运行时拼接，编码字节不变；再生后可运行
`scripts/compact_test_bytes.mbtx`。新增 AV1 大向量保持独立包。

下一步为整帧 superres、qmatrix 及更广 inter 格式/尺寸变化。film grain、
intrabc 按原范围不做。完整参数、入口约定与再生步骤见
[INTER_SESSION](AGENT_HANDOFF_2026-10-26_INTER_SESSION.md)。本轮按用户要求提交并推送。

`moon check --target wasm --deny-warn --no-render` 零警告，Wasm 全量
**968/968**；`moon info --target wasm` 已运行并检查新增接口。

## 97. AV1 整帧 superres 与 sb128 LR 左边界修复

`av1_decode_frame_payloads` 去掉 superres guard，按 dav1d 双平面模型接通：
coded 宽度完成解码/deblock/CDEF 后新增 resize 级（`av1_scale_fac` =
`scale_fac(coded, upscaled)` 14bit 步长、`av1_get_upscale_x0` 相位，
逐行调用 `av1_mc_resize`，该原语新增 `src_off` 参数），随后把
`f.planes/y_stride/uv_stride` 重绑到上采样平面；LR 在上采样宽度上运行
（`Av1FrameCtx.sr_sb128w` 网格、`av1_lr_sbrow` 用 `hdr.upscaled_width`），
输出 picture 宽度 = upscaled。帧头新增 `superres_denom` 字段。

`read_restoration_sb` 补 superres 分支：单元网格跨上采样宽度，coded SB
区间经 `x = ((bx*4*d >> ss_h) + unit_size*8 - 1) >> (log2+3)` 映射到单元
索引，一个 SB 可读多个单元；`lr_frame` 的 12 行边界备份按 dav1d
`backup_lpf` resize 分支逐行上采样（src 推进用固定 64 行条带步长，尾行
复制），buffer 以重绑后的上采样 stride 分配、源平面用 coded stride 传入。
会话入口参考帧保持上采样宽度（scaled MC svc 与 2×/16× 限制基准），但
`Av1RefmvsReference` 改存 **coded** 宽度：dav1d 的时域 MV 同尺寸门比较
ref `frame_hdr->width[0]`（coded）与当前 `bw`。

顺带修复 LR 条带循环两处与 dav1d 的偏差：左边界指针需按「刚滤波完的
条带高度」推进（此前完全没推进；sb64 每调用恰一条带从未暴露，sb128 或
superres 单次调用多条带即触发），推进时机在下一条带高度推导之前。

真实流验证：aomenc（apt `aom-tools`，FFmpeg 封装不暴露 superres 参数）
四组 yuv420p 流——`superres_inter`（denom13 fixed，8 帧）、`superres_kf`
（kf-max-dist=1 全关键帧对照，10 帧）、`superres_10bit`（yuv420p10le
denom12，8 帧）、`superres_mixed`（superres-mode=2 逐帧随机 denom 9–15
且含非 superres inter 帧，8 帧）——共 **34 个显示帧**与 libdav1d framemd5
逐帧一致；scaled-reference MC 路径首次被真实流覆盖。调试方法：vpxdev-img
gcc 直编 trace_src dav1d 树并插桩逐级 dump（coded 级 0 差异定位到 resize
参数序错误 `scale_fac(upscaled, coded)`；resize 级 0 差异定位到 LR 左边界
指针推进），`run_sr_stage.py` 用 base64 通道回传二进制（Windows 管道吃
NUL 字节）。

aomenc superres fixed 模式只对 inter 帧启用（关键帧恒 denom16），
superres-mode=2 才会出现 superres 关键帧。qmatrix 仍拒绝；film grain、
intrabc 不做。再生步骤与编码参数见
[SUPERRES](AGENT_HANDOFF_2026-10-26_SUPERRES.md)。

`moon check --target wasm --deny-warn --no-render` 零警告，Wasm 全量
**972/972**；`moon fmt`/`moon info --target wasm` 已运行。

## 98. AAC SBR（HE-AAC 频谱带复制）

参照 FFmpeg 9.0.2 `libavcodec/aacsbr_template.c`（float 实例，经 `aacsbr.c`
include）、`aacsbr.c`（dequant/chirp/hf_inverse_filter/gain_calc/hf_assemble）、
`sbrdsp_template.c`（QMF kernel 与噪声表）、`aacsbrdata.h`、`aac/aacdec_tab.c`
（SBR Huffman 表）。新增 `aac_sbr.mbt`（状态与语法层）、`aac_sbr_dsp.mbt`
（DSP 层）、`aac_sbr_entry.mbt`（FIL 扩展分发与 per-element 应用）、
`aac_sbr_tables.mbt`（脚本提取的常量表）。

**语法层**：`read_sbr_header/grid/dtdf/invf/envelope/noise/data/extension`、
`sbr_make_f_master/f_derived/hf_calc_npatches/f_tablelim`；`make_bands` 的
连乘保留在 f32（`lrintf` 舍入须与 C 位级一致）；`env_facs_q` 的 uint8
回绕与 `>127/>30` 判定逐条对齐。**对接**：`decode_raw_block` 记录
`che_prev`，TYPE_FIL 分发到 `decode_fill_element`（EXT_SBR_DATA_CRC 门控：
sbr=0 拒绝、隐式 sbr=-1 首帧后拒绝、单声道隐式 PS 升格），输出通道数
按元素翻倍（2048 样本/通道）。

**DSP 层**：QMF 分析/合成（naive IMDCT64 直译 `ff_tx_mdct_naive_inv`，
scale −2·32768 / 1/(64·32768)）、sum64x5/pre/post_shuffle/deint/bfly/
autocorrelate/hf_gen/hf_g_filt/hf_apply_noise、dequant（exp2fi）、chirp、
mapping/env_estimate/gain_calc/hf_assemble/x_gen/lf_gen。

**对拍方法**：docker vpxdev-img 最小化构建插桩 ffmpeg
（`_build/sbr_work/instrument_sbr.py` 锚点式插桩，产出逐帧语法文本与
in/W/XLOW/XHIGH/ALPHA/ECURR/GAIN/QM/SM/YF/X0/OUT 逐级 f32 dump），
MoonBit 测试以 f32 位模式序列化同序状态全量比对。修复链（每条都由
分级 dump 定位）：①表提取正则漏掉 aacsbrdata.h 的 4 处 `-Q31(...)`
负号；②`vector_fmul_reverse` 方向（实为 `dst[i]=win[i]*x[319-i]`）；
③`env_estimate` 的 `+ilb` 与 `sum_square` 计数是复元素单位；④`hf_gen`
的 `X_high[k]+2` 指针偏移（+2 复元素）；⑤`hf_g_filt` 的 `X_high+kx`
行偏移；⑥`indexnoise` 双重递增（kernel 内递增按值传递不回传，帧内每槽
仅在尾部 +m_max，e_a 槽也递增）。

**向量**：libfdk_aac `-profile:a aac_he` 生成 4 组 ADTS 流（48 kHz 单声道
64k、44.1 kHz 单声道 32k、32 kHz 三声道 96k、48 kHz 立体声 128k），参考
PCM 由原生 `aac` 解码器输出。语法层 24+17 帧全字段位级一致（含 coupling=1
立体声耦合）；端到端 PCM 单声道/三声道 max_diff=1 LSB、精确率 99%
（Double vs C float32 舍入，98234/98304 逐样本精确）。PS 载荷出现时
显式报错（PS 合成未移植）。

`moon check --target wasm --deny-warn --no-render` 零警告，Wasm 全量
**976/976**；`moon fmt`/`moon info --target wasm` 已运行并检查新增接口。

## 99. AV1 整帧 qmatrix 与更广 inter 格式回归

保存帧头的 Y/U/V 矩阵级别，按 separate_uv_delta_q 解析/推断 V；
普通反量化接通 dav1d 的 `(dq * weight + 16) >> 5`，保持 Golomb
截断、乘积掩码和正负饱和边界。IDTX/方向 identity/WHT 不使用矩阵；
级别 15 为 identity。关键帧与 inter 共用 coefficient state，帧级分配
矩阵级别数组；移除整帧入口对 qmatrix 的拒绝。

原始 dav1d 1.5.1 qm.c 的两张紧凑表共 31,200 字节，由 `.mbtx`
脚本提取并保留 BSD 许可证。MoonBit 直接定位转置/降采样/三角矩阵，
64 点变换复用 32 点权重；原始 C 的完整初始化代码独立产生 608 个矩阵
摘要（16 级别 × 两平面类别 × 19 变换尺寸），全部与 MoonBit 一致。

13 组码流共 57 个显示帧与 FFmpeg 9.0.2/libdav1d MD5 一致，涵盖
key/inter/reorder、8/10/12-bit、420/422/444/mono、色度 delta-q、
矩阵级别极值及独立的 (Y,U,V)=(3,8,12)/(0,15,7)。独立平面流由
真实关键帧改写帧头生成，FFmpeg trace_headers 确认字段，libdav1d
独立解码给出期望值。inter 首次加入 12-bit、422/444/mono 的代表性
端到端验证；尺寸变化和更广组合仍待补。

`Av1FrameHeader` 新增 qm_y/qm_u/qm_v；公开 `Av1CoefState` 新增
qm_levels，手动构造时 `[15,15,15]` 保留原行为。另仅拆分两份超过
编译器列限制的旧 AAC SBR base64 字面量，编码内容不变。

`moon check --target wasm --deny-warn --no-render` 零警告；Wasm 全量
**987/987**；`moon fmt`、脚本格式化和 `moon info --target wasm` 已运行。
详细再生步骤及后续边界见 `AGENT_HANDOFF_2026-10-27_QMATRIX.md`。

## 100. HEVC dependent slice segments 与 B 帧纠错

依照 `hls_slice_header`/`decode_slice_data`/`ff_hevc_cabac_init` 接通依赖
segment：帧头继承前段未编码的字段，重新解析 entry points/扩展和
alignment；AU 探测不读取不存在的 slice type/POC。公共单头解析器新增
可选 `previous` 参数，原独立 slice 的调用保持兼容。

帧驱动区分 segment 起点与独立 slice 起点。依赖段重启算术解码器，
继承 CABAC 概率与 QP 历史，不新增独立 slice ID 或重建 RPL；验证
tile-scan 顺序的前一 CTB 已解码，缺段、零地址、截断头明确报错。
WPP 的第二 CTB 状态跨段保存，行首装入；tile 首 CTB 初始化状态。
统一 tile-scan CTB 循环取代三套分支，覆盖行中起始 segment。

真实码流同时暴露并修复三个既有问题：L1 参考索引错误选择专用 CABAC
contexts（FFmpeg 对两列表均使用 REF_IDX_L0）；inter 4×4 亮度误用
intra DST；1080 行图像最后半个色度滤波块无条件读取图外 QP。
原生 C 插桩的前三帧共 4341 个 prediction units 活跃 MV/参考索引
与 MoonBit 一致；插桩 C 的 YUV 也与镜像默认实现一致。

FFmpeg FATE `DSLICE_A/B/C_HHI_5.bit` 各前九个解码包，共 27 个显示帧，
默认滤波及 `-skip_loop_filter all` 的 54 份逐帧 MD5 全部一致；覆盖
8-bit 4:2:0、多个独立 slice 的依赖链、WPP、tiles 和 I/B 重排序。
新增六个回归测试；生成脚本为 `.mbtx`，夹具不依赖外部文件。

严格 Wasm check 零警告，全量 **993/993**；`moon fmt`、脚本格式化、
`moon info --target wasm` 已完成。tiles 与 WPP 同时启用、dependent
segment 的高位深/其他色度组合已在后续轮次补齐（见 §113/§114）。详见
`AGENT_HANDOFF_2026-10-27_HEVC_DEPENDENT.md`。

## 101. HEVC PCM、transquant bypass 与高位深 dependent segment 回归

PCM CU 按合法类型/尺寸读取 terminate bin，使用 CABAC skip_bytes 对齐、
取原始负载、在末端重启算术解码器，保持上下文概率；Y/Cb/Cr 按独立
PCM 精度读取并左移到输出位深。普通预测和变换树跳过，QP、深度和
边界强度继续更新。原来的“PCM-capable SPS 必须拒绝”测试改为普通
编码块正例，FFmpeg -xerror 证实其七帧灰色输出保持相同 MD5。

新增 min-PU 保护图，PCM 禁用滤波及 transquant bypass CU 均标记。
去块滤波只修改非保护侧：亮度每个四行组立即还原保护侧的三个样本，
色度按侧门控，避免后续边读取被改写的 PCM。SAO 使用不可变源平面，
因此最后还原保护单元。公开去块/SAO API 仅新增可选 pcm 参数。

混合 PCM 流暴露 PPS transquant bypass：本轮实现 CU 标志与空间域
原始残差，跳过量化矩阵/反量化/逆变换、transform_skip_flag 和 sign
hiding。残差 API 新增可选 transquant_bypass 参数，默认行为兼容。
RDPCM 继续拒绝（旧 intra 专用入口也增加对应门控）。

七个原始 FATE PCM 流共七帧，覆盖 8-bit 420、10-bit 422、12-bit 444、
6/6、6/8、8/8 PCM 精度、禁用滤波及混合 CU；四组 x265 lossless=1
I/P/B 流共二十帧，覆盖上述位深/色度与单色。两种滤波模式合计 54
份逐帧 MD5 均与 FFmpeg 9.0.2 一致，无损流默认/跳过滤波 MD5 相同。

另外修改 DSLICE A/B/C 的 VPS/SPS profile、兼容/约束与位深 Golomb
字段，保留 slice bytes，重建 RBSP 对齐及防竞争字节，派生六组
10/12-bit 420 流。新增 54 帧双模式逐帧 MD5 对齐，参考解码开启
-xerror；这些是派生测试流，不能当作原始高位深编码器一致性样本。

新增十八项回归，含 PCM 原始负载越界与 CABAC 状态不变检查。
严格 Wasm check 零警告，全量 **1011/1011**；fmt、脚本格式化、
fmt --check 和 moon info 均完成。详细再生步骤与剩余边界见
`AGENT_HANDOFF_2026-10-27_HEVC_PCM.md`。

## 102. HEVC implicit / explicit RDPCM

接通 explicit CABAC 标志/方向（亮度与色度独立上下文）及 implicit
方向推导；空间累加逐项保持 int16 回绕。残差入口新增可选 intra_mode，
亮度/色度 TU 分别传入实际方向；保留旋转、反量化、累加的执行顺序。
修正 bypass 的 transform-skip significance 上下文，按上游规则关闭
RDPCM 的 sign hiding；完整帧及旧 intra 入口移除 RDPCM 拒绝。

每 CU 的亮度 PU 方向重置为 DC，色度方向按 C local context 保持跨帧
状态。上游 bypass 分支对 implicit 的条件不限制 intra，且 implicit
开启时方向优先取残留 mode；因此 FFmpeg 帧线程数影响这类流的结果。
本轮保留上游**单线程**行为，参考解码固定 -threads 1 -xerror，不能
据此声称输出等同于默认多线程或 HM 编码器重建。

HM 16.8 including RExt 固定提交 382e0a040c7f0189670c4aad9716d45db977b319
生成六组五帧流，覆盖 8/10/12-bit、420/422/444/mono、implicit/explicit、
transform skip/bypass、rotation/sigmap。独立 C 插桩确认实际工具激活，
包括 explicit skip 的水平 56 块、垂直 24 块，避免只测到 SPS 标志。
30 帧两种滤波模式的 60 份 MD5 全部对齐 FFmpeg 单线程。原始 C 内核
另产生 128 份随机输入摘要，覆盖 4/8/16/32 方块、两方向及有符号回绕。

再生脚本、参考行为边界及后续范围见
`AGENT_HANDOFF_2026-10-27_HEVC_RDPCM.md`。

新增七项回归，严格 Wasm check 零警告，全量 **1018/1018**；
`moon fmt`、脚本格式化、`moon fmt --check`、`moon info --target wasm`
和 `git diff --check` 完成。全量日志为 `.scratch/hevc_rdpcm/full_tests.log`。

## 103. HEVC 跨分量预测与 DPB 按需分配

接通 HEVC cross-component prediction：Cb/Cr 各自解码四步截断一元
绝对值（contexts 167..174）与符号（175..176），得到 0/±1/±2/±4/±8；
色度残差按 `chroma += (scale * luma) >> 3` 逐项以 int16 回绕合成，
之后才裁剪输出像素。亮度 TU 的空间域残差（裁剪前）由残差内核返回，
供同 TU 的色度复用，不能用解码图片与预测图片之差替代。

intra 侧新增原始 chroma syntax mode 表，NxN PU 与 TU 分裂时正确继承；
只有原始模式为 DM（4）时才允许 CCP，避免与已映射的
`pu_mode_c`/`tu_mode_c` 混淆。CU/TU 按 `PPS cross flag && cbf_luma &&
(inter || 原始色度模式 == 4)` 门控；色度 CBF=0 的块仍按亮度预测生成
残差。原 intra 入口与完整帧入口的 CCP 拒绝已移除。

HM 16.8 including RExt 生成六组 64x64、五帧 4:4:4 流（8/10/12-bit、
intra/inter、skip、bypass、零色度、NxN split）。30 帧在默认滤波与
skip_loop_filter 两模式下共 60 份 MD5 全部对齐 FFmpeg 9.0.2
`-threads 1 -xerror`；C 插桩确认各工具实际激活，插桩二进制的原始
YUV 与未修改 FFmpeg 逐字节一致。原始 C 合成循环另生成 576 组
4/8/16/32 方块 × 九种 scale × 16 seeds 的摘要。

验证全量时发现 333 项 `moonbit.alloc_externref` trap。定位为 DPB 在
建库时预留 40 个槽位、每槽都分配整帧画面与 min-PU 运动场；1080p
dependent 流单测峰值约 1.2 GB，整包累积越过 Wasm 线性内存上限。
改为槽位被真正占用时才分配画面与运动场（对齐上游 `alloc_frame`，
复用槽位保留缓冲），dependent 单测峰值降至约 0.45 GB，全量峰值
从约 3.5 GB 降到约 2.4 GB，全量恢复。

新增七项回归（六组整流 + 一组 576 组内核对照）与六份嵌入码流；
严格 Wasm check 零警告，全量 **1025/1025**。依赖补丁、再生脚本与
后续范围见 `AGENT_HANDOFF_2026-10-27_HEVC_CCP.md`。

## 104. HEVC 参考列表修改（ref_pic_list_modification）

切片头按上游顺序解析：先由 short/long-term RPS 的 `used` 标志算出
候选参考数 `nb_refs`（当前图片引用计入但本移植仍拒绝该工具），
P/B 且 `nb_refs == 0` 报错；`lists_modification_present_flag` 且
`nb_refs > 1` 时读 L0/L1 标志与 `ceil_log2(nb_refs)` 位条目。

RPL 构造在 RPS 候选串接完成后按条目下标重排；越界下标返回错误，
不修改标志时保持原截断行为。依赖 slice segment 继承独立段的列表。
权重表仍按重排后的下标解释，语义与上游一致。

参考码流由 HM 16.8（固定提交）生成：scratch 树在 `HM_RPLM=1` 下置位
`lists_modification_present_flag`，反转每条活动列表并写入对应条目，
编码器自身也按反转后的列表做预测。覆盖 P 低延迟 420 8-bit、层次 B
420 8-bit、B 444 10-bit、B 422 12-bit 共 33 帧，默认/跳过滤波共 66
份 MD5 对齐 FFmpeg 9.0.2 `-threads 1 -xerror`；`trace_headers`
确认 L0/L1 标志与反转条目真实出现。新增五项回归，其中一项注入越界
条目验证拒绝路径。

RExt 随机接入配置默认启用 persistent Rice 参数自适应，本移植尚未
实现；生成器显式关闭该工具，避免未覆盖工具污染 RPLM 参考。该缺口
与 tiles+WPP、当前图片引用一并列入后续范围。

严格 Wasm check 零警告，全量 **1030/1030**。再生与补丁细节见
`AGENT_HANDOFF_2026-10-27_HEVC_RPLM.md`。

## 105. HEVC persistent Rice 参数自适应与切片内联 RPS 修正

接通 `persistent_rice_adaptation_enabled`：`stat_coeff[4]` 以
luma/chroma ×（transform skip 或 transquant bypass）分四类，残差子集
起始 `c_rice_param = stat_coeff[sb_type] / 4`，解码出首个 remaining
后按 `remaining >= 3 << init` 增、`2 * remaining < 1 << init` 减（不小于
0）。开启时 `c_rice_param` 的递增不再截断到 4。独立 slice 与 tile 起点
清零；dependent 段沿用前段，WPP 行与列边界按上游 `save_states`/
`load_states` 一并保存、恢复概率状态与 `stat_coeff`。实现位置
`hevc_cabac_syntax.mbt`（状态）、`hevc_residual.mbt`（取值与更新）、
`hevc_inter_decode.mbt`（段/行/瓦片边界）。

HM 16.8 生成四组流：4:4:4 10-bit 层次 B、4:2:2 12-bit 层次 B + WPP、
4:2:0 8-bit，以及 4:4:4 10-bit dependent slice segment 组合，共 29 帧；
默认/跳过环内滤波共 58 份 MD5 对齐 FFmpeg 9.0.2 `-threads 1 -xerror`。`trace_headers` 确认 persistent rice 与 WPP 标志
真实置位。新增四项回归（三组整流 + stat_coeff 增减单元测试）。

dependent segment 组合确认 `stat_coeff` 跨段继承（独立段清零、后续段沿用）
与上游一致。另外验证了 tiles 与 WPP 同时启用的可行性：HM 在非 High
Throughput profile 下直接拒绝该组合，FFmpeg 的 WPP 路径按入口点子流
逐 CTB 行解码，需引入 skipped-bytes 感知的入口点偏移；本移植继续明确
拒绝该组合，等待独立一轮实现。

验证过程中发现并修正一个潜伏缺陷：切片头内联 short-term RPS 时，
`inter_ref_pic_set_prediction_flag` 只在 `rps_index > 0` 时读取，而
上游条件对切片头恒成立（`rps != sps->st_rps`），导致存在 SPS RPS 的
码流解析错位。此前夹具都走 `short_term_ref_pic_set_sps_flag=1`，未覆盖。

严格 Wasm check 零警告，全量 **1034/1034**。再生见
`AGENT_HANDOFF_2026-10-27_HEVC_PERSISTENT_RICE.md`。

## 106. HEVC dependent segment 的 RExt 色度组合验证

此前 dependent slice segment 只在 4:2:0 上验证（高位深流由改写 VPS/SPS
深度得到）。本轮用 HM 16.8 原生编码 `SliceSegmentMode=1`
（`SliceSegmentArgument=1`，每 CTB 一个依赖段）生成 4:4:4 8-bit、
4:2:2 10-bit、4:4:4 12-bit 三组 64×64 五帧流，共 15 帧；默认/跳过环内
滤波共 30 份 MD5 对齐 FFmpeg 9.0.2 `-threads 1 -xerror`。
`trace_headers` 确认 `dependent_slice_segment_flag=1` 与非零段地址。

新增三项回归（复用 dependent 校验助手并断言 SPS 色度格式/位深）。
严格 Wasm check 零警告，全量 **1038/1038**。再生脚本
`scripts/generate_hevc_dependent_rext_vectors.mbtx`。

## 107. AAC 参数化立体声与 SBR 接线

移植 FFmpeg 9.0.2 PS 语法、混合滤波、去相关和立体声矩阵，并接入 SBR。
两条 libfdk_aac HE-AACv2 流的全部 51/47 帧与 FFmpeg s16 PCM 对拍，
最大差均为 1。修正参数网格重映射、矩阵插值、右声道综合和相位混合符号。

`scripts/ps_dsp_harness.c` 构造六帧参数，覆盖 34/20 频带切换、HA/HB、
IPD/OPD 开关和 1–4 个包络；7680 个输出采样点与 C 的最大差约 1.15e-6。
另以固定种子生成八帧连续编码负载，覆盖 34/20 频带切换、HA/HB、IPD/OPD、
1–4 个包络、帧内及跨帧频率/时间差分和不同 `top`：两端均从负载解析。
另覆盖 10→20/34 与 ICC 20→34 频带映射、相位关闭、固定包络网格及自动补包络。
C 插桩的完整混合分析、去相关缓冲和四组矩阵共 106240 个值，最大差约
6.21e-7；65536 个 QMF 输出值最大差约 9.00e-7。随机负载使用固定种子。
另以 native 目标对拍 16 帧完整中间缓冲和输出，共 212480 个中间值与
131072 个输出值，最大差分别约 6.72e-7 和 9.74e-7；wasm 全量测试保留
8 帧以避开当前工具链 externref 链表的 2 GiB 地址问题。

## 108. AV1 帧尺寸变化（resize-mode）与非均匀 tile

用 aomenc resize-mode 码流首次覆盖 AV1 帧级尺寸切换。此前所有真实流
（inter/superres/qmatrix）参考帧与当前帧尺寸恒一致，尺寸切换语法与
scaled MC 从未被真实码流触发；本轮连修三个由此暴露的缺口：

- **非均匀 tile 间距**（`uniform_tile_spacing_flag=0`）：此前直接
  `PATCHWELCOME` 拒绝。按 dav1d `parse_tile_info` 移植逐 tile 的
  反均匀码宽度/高度读取与面积上限，`Av1FrameHeader` 新增
  `tile_col_start_sb`/`tile_row_start_sb` 显式边界数组（长度
  cols+1/rows+1，均匀分支同样填充），解码主循环、LF 的
  `tile_col_start4` 与 tile 任务起点全部改为查表，不再假设等宽。
- **显式帧尺寸位宽**：帧内显式宽高此前误读 4+4 位计数位——规范中
  `frame_width_bits_minus_1` 在序列头，dav1d `read_frame_size` 直接用
  `seq_hdr->frame_width_bits`。序列头结构新增两个字段，帧级按其读取。
  该路径仅尺寸切换流首次触发。
- **resize 流参考基准**：FFmpeg framemd5 对尺寸变化的流会自动缩放到
  首帧尺寸（filter graph 协商固定输出尺寸），参考完全失真。改用
  trace_src 的 dav1d 1.5.1 CLI（容器内 meson 构建）以 `--muxer` 扩展名
  探测 + `%n`/`%w`/`%h` 逐帧文件输出原生尺寸 YUV，容器内 md5sum 后由
  `scripts/assemble_resize_md5.mbtx` 组装成 framemd5 格式。注意 `%n`
  替换仅在未强制 `--muxer` 时生效；逐帧循环需 `while set --`（dash 的
  for 词表只展开一次）；帧序必须按帧号 glob 而非文件名排序。

七组流（`scripts/generate_av1_resize_vectors.mbtx`，Ubuntu aom-tools
的 aomenc，`--resize-mode=1/2`）逐帧 MD5 与 dav1d 1.5.1 一致：

| stem | 覆盖 | 帧数 |
| --- | --- | --- |
| resize_up | KF 192x128 ↔ inter 96x64 交替（2× 缩放 MC、非均匀 tile、显式尺寸+render 补偿） | 24 |
| resize_10bit | KF 192x128 ↔ inter 128x85（10-bit、2/3 缩放、奇数高度 85） | 24 |
| resize_qm | 同 up 的尺寸切换 × qmatrix（qm 8） | 24 |
| resize_random | mandelbrot 源 resize-mode=2 逐帧随机尺寸 96~192（升降双向 scaled MC、跨尺寸 refmvs 门控） | 24 |
| resize_down / resize_444 / resize_lag | 固定模式单 denominator 未触发缩放的对照流（420/444/隐藏帧） | 24 |

其余修复：tile 边界数组长度校验、非均匀分支 tile 计数器从 0 起（dash
遗留错误曾致多计一个 tile）、`context_update_tile_id` 越界拒绝。
resize×superres×qm 组合使 dav1d 1.5.1 首帧后段错误，无参考可用，不纳入。

同日补根因：testsrc2 源 random 流（补回为 `resize_random_t2` 夹具）
140x93 帧内恰差 1 像素，逐级对拍（MC 参数/预测块、系数、itx 输出、
level 网格双侧 dump）收敛到帧对齐填充列：dav1d 的 `f->w4`/`f->h4`
是**可见** 4x4 网格（`(dim+3)>>2`），level 缓存写入与去块边缘循环
（cols/rows 及 tile 行修正）都以它为界，对齐填充列/行从不写入、
从不滤波；本移植此前用 8 像素对齐的 `bw`/`bh`，帧缘 8x8 块的
填充列参与去块并读到 stride 填充值，经 CDEF 方差改变传播到可见
像素。修复后 `Av1FrameCtx` 新增 `w4`/`h4`，去块循环、level 缓存
写入与 tile 行修正全部改用可见网格（CDEF/重建仍用对齐域，与
dav1d 一致）。该差异自 inter 整帧接入以来即潜伏，此前所有流的
可见区域恰好未被触及。8 组流（含两组 random）168+24 帧全绿，
全量 wasm 1049/1049。

## 109. VP9 参考缩放（scaled reference MC）

移植 FFmpeg 9.0.2 `vp9recon.c` 的 `mc_luma_scaled`/`mc_chroma_scaled` 与
`vp9dsp_template.c` 的 `do_scaled_8tap_c`/`do_scaled_bilin_c`：帧间帧引用
尺寸不同的参考缓冲时，按 14 位分数比例（`(ref_dim << 14) / cur_dim`，
步长 `16 * scale >> 14`）采样，复刻 libvpx 的两个已知舍入怪癖——MV 两
分量分别缩放、4:2:0 色度 MV 的 `webm/issues/820` 舍入
（`scale_mv(x*16) & ~15 + scale_mv(x*32) & 15` 组合）。缩放系数在
`mc_luma`/`mc_chroma` 分派时按参考尺寸现算，参考尺寸非法（超 2×/1⁄16×
范围）沿用 C 的 REF_INVALID_SCALE 继续。

验证两级：内核级以独立 C harness（`do_scaled_8tap_c`/`do_scaled_bilin_c`
直编）12 个确定性用例 FNV-1a 逐像素一致；整帧级用 libvpx 官方
`vp90-2-05-resize.ivf`（FFmpeg FATE `fate-vp9-05-resize` 的同一向量），
10 帧 352x288 ↔ 282x173 交替，双向 scaled MC（inter 282 引 352 与
inter 352 引 282）逐帧 MD5 与 vpxdec 原生输出一致。参考基准用
`vpxdec --i420 -o 'f%2n_%w_%h.yuv'` 逐帧原生尺寸输出（FFmpeg framemd5
对尺寸变化流会自动缩放到首帧尺寸，同 AV1 resize 轮的陷阱）。调试中
顺带修正的中间错误：色度坐标域混淆（dispatcher 传入的已是色度坐标，
内部不得再下采样）与 `& ~15` 掩码写反。

## 110. AV1 inter 工具组合真实流验证

用 aomenc 的工具开关（`--enable-interintra-comp/-wedge/-smooth`、
`--enable-masked-comp`、`--enable-obmc`、`--enable-warped-motion`、
`--enable-dual-filter`、`--enable-ref-frame-mvs`）生成五组 14 帧
192x128 真实码流：interintra 家族、masked compound、OBMC+local warp、
dual filter+ref-frame-mvs 各自隔离（其余工具显式关闭以强制差异化
编码决策）与全开组合，逐帧 MD5 与 dav1d 1.5.1 CLI 原生输出一致
（`scripts/generate_av1_toolcombo_vectors.mbtx`，参考链同 resize 轮）。
回应 README 中"更广工具组合待验证"：§83-96 逐工具移植的 inter
路径在组合开启的真实码流上全部收敛。注意 aomenc 默认全开这些工具，
`=1` 单独开启不改变编码决策（五流同大小），需互斥关闭其他工具才
能隔离验证。

## 111. H.264 P 帧错误恢复第二源矩阵与色度几何边界确认

新增第二条独立 8-bit 4:2:0 源（testsrc2，此前矩阵用 h264_slices1.264）
的 P 帧损坏矩阵：丢 IDR 首 slice、丢 P 首 slice、丢两个 P slice、截断
尾 P slice 四种损坏 × 6 帧逐帧 MD5 与 FFmpeg 9.0.2（`-flags2 +showall`
默认 EC）一致（`h264_er_420p_test.mbt`）。

同轮对 4:4:4/4:2:2/10-bit 的 EC 对齐做了可行性勘察并确认边界：
FFmpeg 的 `error_resilience.c` 对色度恢复硬编码 4:2:0 几何
（`dest_cb = data[1] + mb_x*8 + mb_y*8*linesize` 每 MB 一个 DC、
`put_dc` 写 `dc/8` 的 uint8 语义），4:4:4/4:2:2 下写入位置只覆盖每 MB
左上 8x8、10-bit 下平面按低字节截断——这些是与未定义行为区域耦合的
实现细节，不做逐像素复刻，README 明确记为不在对齐范围。勘察中确认
MoonBit 侧 conceal 的色度 DC 网格应为 MB 几何（此前每 8x8 块），
该修正仅影响非 4:2:0 格式的恢复输出（4:2:0 下两种网格重合），
故不在主线提交。

## 112. HEVC 跨分量预测与 dependent/PCM 组合验证

回应 README 中"CCP 与 dependent/PCM 组合待验证"：用 HM 16.8（同一
pinned commit `382e0a0`）在 4:4:4、`CrossComponentPrediction=1` 下生成
四组组合流，逐帧 MD5 与 FFmpeg 9.0.2 **单线程**在默认滤波与
`-skip_loop_filter all` 两种模式下均一致（每组 5 帧，共 20 帧次 × 2）：

| 流 | 组合 | 配置 | 验证点 |
| --- | --- | --- | --- |
| `ccp_dep8` | CCP × dependent slice segments | 8-bit 444，低延迟 GOP，QP 22 | 每帧 1 独立 + 3 依赖段 |
| `ccp_dep10` | 同上 | 10-bit 444，QP 20 | 同上 |
| `ccp_pcm8` | CCP × PCM | 8-bit 444，QP 4，强制 transquant bypass | 每流 20 个 PCM CU |
| `ccp_dep_pcm10` | CCP × PCM × dependent | 10-bit 444，QP 4，强制 bypass | 20 个 PCM CU 与每帧 3 依赖段同时出现 |

两条再生陷阱：HM 的 PCM 只在 `uiBestBits > uiRawBits` 时进入候选，
普通 QP 的结构化源不会选中，必须用确定性满量程噪声源并强制
transquant bypass（无损），本组每帧 4 个 32×32 CTU 全部走 PCM；
`SliceMode` 生成的是独立 slice，依赖段要用 `SliceSegmentMode`
（HM 在 `sliceSegmentMode != NO_SLICES` 时置 PPS 的
`dependent_slice_segments_enabled_flag`）。流内标志已用 FFmpeg
`trace_headers` 核对：`ccp_dep8/10` 为 dependent=1 + PCM=0，
`ccp_pcm8` 为 dependent=0 + PCM=1，`ccp_dep_pcm10` 全开，四组 PPS
的 `cross_component_prediction_enabled_flag` 均为 1。

夹具为黑盒（`hevc_ccp_combo_test.mbt` 与生成的
`hevc_ccp_combo_vectors_test.mbt`，规避白盒局部数上限），再生脚本
`scripts/generate_hevc_ccp_combo_vectors.mbtx`（参数：`.scratch/hevc_ccp`
与 HM 源码目录）。为此新增 pub 访问器 `HevcDecodedFrame::poc` 与
`HevcDecodedFrame::yuvp`（黑盒测试取 POC 排序与打包平面，先例
`H264Frame::yuvp`）。全量 wasm **1061/1061**。

## 113. HEVC tiles + WPP 同时启用与 cabac_bypass_alignment

补齐 README 最后一个 HEVC 外部受阻项之外的缺口：tiles 与 WPP 同时启用。
该组合仅 High Throughput（Intra 4:4:4）profile 允许，HM 16.8 三组流
（64×64、CTU 32、2×2 tiles 8/10-bit；192×128、CTU 64、2×2 tiles
得 2+1 列 × 1+1 行非方形 tile 8-bit）共 15 帧，与 HM 解码输出逐帧
MD5 一致。

**FFmpeg 9.0.2 无法解码该组合**：单线程 `hls_decode_entry` 的
`ff_hevc_cabac_init` 用 tile-scan 索引对 picture CTB 宽度取模来决定
WPP 行起点，tiles 下该条件与真实 tile 行边界无关；实测 FFmpeg 解码
HM 重建 PSNR 约 41 dB 的流只得到约 10 dB 的垃圾。多线程 WPP 路径也
只在 `num_tile_rows == 1 && num_tile_columns == 1` 时启用。因此本组夹具
以 HM 解码输出为参考（先验证 HM 解码 == HM 重建），FFmpeg 仅作为原始
YUV 的 MD5 哈希工具。

实现要点：

1. **子流定位**：按 `TDecSlice::decompressSlice` 用 slice header 的
   entry point 偏移定位每个子流，不做 terminate/字节推算。entry point
   长度按 EBSP 字节计数，而 `H2645NAL.data` 是去防竞争字节后的 RBSP；
   `RbspExtract`/`H2645Nal` 新增 `skipped_pos`，照搬 FFmpeg
   `hls_slice_data_wpp` 的 `cmpt` 调整把偏移映射回 RBSP 位置。此改动
   同时把 WPP-only/tiles-only/dependent 的旧逐字节路径替换为偏移定位，
   全量回归通过。
2. **上下文**：每个 tile 起点重置概率与 persistent Rice 统计；tile 内
   后续 CTU 行仅在上右 CTU 与当前 CTU 同 slice 且同 tile 时装载上一行
   第二个 CTU 保存的上下文，否则保持新初始化（HM `TDecSlice` 同款条件）。
   首行 CTU 行（`x == tile_left`）置 `first_qp_group`。
3. **cabac_bypass_alignment**（RExt SPS 标志，HT profile 规范要求为 1）：
   `TDecBinCABAC::align` 把 range 置 256，使符号与
   `coeff_abs_level_remaining` 退化为原始字节读取；escape 判定按 HM
   补齐为「第二个及以上 greater1 命中、gt2 为 1、nCoeff>8」三者之一。
   FFmpeg 9.0.2 对该标志只打 "not yet implemented" 警告，未实现语义。
4. 邻域可用性沿用按 tile 的 `end_of_tiles_x`（`hls_decode_neighbour` 在
   WPP 分支把上限设成 picture 宽度是 FFmpeg 该组合失效的一部分，不复刻）。

HT cfg 默认 `CrossComponentPrediction: 1`，本组团流内共命中 4262 次
`cross_component_scale` 语法，CCP 与 tiles+WPP 的组合也随本组流覆盖
（仅 intra；HT profile 要求全 intra，inter CCP 无法用该 profile 生成）。

另加两组 8-bit dependent 组合流（64×64 与 128×128；后者 2×2 tiles、
每 tile 2×2 CTU，依赖段起点包含 tile 内 WPP 行首）：依赖段 NAL 不带
entry point（`num_entry_point_offsets=0`），走已有的 slice-start
继承逻辑——tile 起点重置、tile 内其余段沿用上一段结束状态，50 帧次
仍与 HM 解码逐帧一致。另加一组"每 2 CTU 一个独立 slice"的
tiles+WPP 流（128×128，每帧 8 slice，含 tile 内 WPP 行首起 slice）：
独立 slice 起点重置概率，slice 内 WPP 行首装载同步上下文，5 帧
同样与 HM 一致。

夹具为黑盒：`hevc_tiles_wpp_test.mbt` + 生成的
`hevc_tiles_wpp_vectors_test.mbt`；再生脚本
`scripts/generate_hevc_tiles_wpp_vectors.mbtx`（参数：`.scratch/hevc_ccp`
与 HM 源码目录）。全量 wasm **1067/1067**。

## 114. HEVC dependent RExt 色度/位深矩阵补全

补齐 §100 遗留的 dependent segment 色度/位深组合：HM 16.8 在
4:4:4/4:2:2 × 8/10/12-bit 六种组合下各生成 5 帧（64×64、CTU 32、
`SliceSegmentMode=1/SliceSegmentArgument=1`，每 CTU 一个依赖段），
默认滤波与 `-skip_loop_filter all` 两种模式下逐帧 MD5 均与 FFmpeg
9.0.2 **单线程**一致（30 帧 × 2）。此前只有 444 8/12-bit 与 422
10-bit 三组，README 的 "8/10/12-bit" 表述因此名不副实；本轮补齐后
矩阵完整（6 组流）。

夹具分配：444 8/12-bit 与 422 10-bit 保留原白盒
`hevc_*_realdata_wbtest.mbt`；新增的 444 10-bit 与 422 8/12-bit 放
黑盒 `hevc_dependent_rext_test.mbt` + 生成的
`hevc_dependent_rext_vectors_test.mbt`（白盒包局部数已贴上限）。
生成器 `scripts/generate_hevc_dependent_rext_vectors.mbtx` 现在一次
输出六组（前三组仍写各自白盒夹具，后三组汇总黑盒文件），并修正旧
夹具头部误写的 "persistent_rice 生成器" 署名。全量 wasm **1070/1070**。

## 115. MKV/Matroska HEVC 写入器

补齐容器写入对称性：MP4/TS/fMP4 早已支持 HEVC，MKV 此前只有
H.264/VP9/AV1/Opus/AAC 写入。新增 `libavformat/mkv_hevc_mux.mbt`
（镜像 mkv_h264_mux）：CodecID `V_MPEGH/ISO/HEVC`，CodecPrivate 为
调用方提供的 hvcC，单 Cluster SimpleBlock，DocType 固定 matroska
（WebM 不允许 HEVC）。块负载按调用方提供的字节原样写入，约定为与
hvcC `lengthSizeMinusOne` 一致的长度前缀 NAL。

验证两层：

- 白盒 `mkv_hevc_mux_wbtest.mbt`：HM 16.8 4:4:4 5 帧流按 access unit
  切分并改写为 4 字节长度前缀作为块负载，hvcC 取自 ffmpeg 对该流的
  MP4 封装；写出的 MKV 用本项目 `av_matroska_open` 读回，校验
  codec/CodecPrivate/尺寸/PTS/keyframe 后，把块负载拼回 nalff 流用
  `ff_hevc_decode_frames_ex` 解码，5 帧在两种滤波模式下 MD5 与原流
  参考一致。
- 外部验证：同一 muxer 输出交 `ffprobe`（hevc 64x64）与 ffmpeg 9.0.2
  单线程解码，5 帧 MD5 与参考完全一致。

全量 wasm **1071/1071**。

## 116. FLAC 32-bit 流与宽定点预测

解禁 32-bit FLAC：STREAMINFO bps 上限 24 → 32；`decode_subframe_fixed`
按 FFmpeg `DECODER_SUBFRAME_FIXED_WIDE` 增加宽路径（bps+order>32 时用
Int64 组合式重建，再截回 32 位样本）；33-bps side channel（32-bit
stereo 的 left/right/mid-side 装饰模式）仍明确报错，独立声道与
≤32 bps 子帧全部支持。

顺带修掉一个真 bug：`flac_rice` 的 zigzag 折叠值原用 Int32 计算，
32-bit 残差编码值 ≥ 2^31 时会先回绕（INT_MAX 的残差被解成 -1）；
改为 UInt32 运算并补上 C 的 `0x80000000` 拒绝，命中后 24-bit 及更低位深
行为不变（全量回归为证）。

验证：libFLAC 1.4.3 `flac -0` 编码 44.1 kHz 单声道满幅近直流正弦
（0.5 s，20 个 FIXED 子帧、bps=32）；FFmpeg 9.0.2 的 s32le 解码
MD5 与源 raw 完全一致，本项目 `pcm_bytes` 亦逐字节一致。夹具
`flac_32bit_vectors_test.mbt` + 黑盒 `flac_32bit_test.mbt`。LPC 在
bps>16 时与既有 24-bit 路径相同（Int64 累加），无需新代码。
全量 wasm **1072/1072**。

## 117. MKV HEVC + AAC 真实读取验证

补一条容器读取的真实验证：ffmpeg（x265 + native AAC）生成的
64×64/5 帧 HEVC + 44.1 kHz 单声道 AAC 的 MKV（14,860 字节）嵌入
白盒测试，`av_matroska_open` 读出两条轨：

- 视频：`V_MPEGH/ISO/HEVC`，64×64，CodecPrivate 为 2408 字节 hvcC
  （`lengthSizeMinusOne=3`），5 个包、PTS 0/200/400/600/800 ms、
  首包 keyframe；
- 音频：`A_AAC`，44.1 kHz 单声道，CodecPrivate 为 5 字节
  AudioSpecificConfig，45 个包。

解码侧：把 hvcC 的 VPS/SPS/PPS 数组改写成 4 字节长度前缀拼到视频包
之前，`ff_hevc_decode_frames_ex(nalff=4)` 在两种滤波模式下解出 5 帧，
逐帧 MD5 与 ffmpeg 对该 MKV 的 framemd5 一致；AAC 的 45 个解复用包
逐个喂给 `new_aac_decoder_from_asc` + `decode_frame` 均成功（单声道
单元素；AAC 内容精度已由 libavcodec 既有套件覆盖）。

顺带确认了 MKV 解复用的时间基/负 PTS（音频首包 -23 ms）与
CodecPrivate 大 hvcC（x265 写入了完整参数集数组）都能正确读出。
全量 wasm **1073/1073**。

## 118. Ogg FLAC（RFC 5334）读写

补齐 Ogg 容器除 Opus 外的 FLAC 映射：

- **demux**（`libavformat/ogg_flac.mbt`）：校验 `0x7F "FLAC"` 映射头
  （版本 1.0、big-endian 头部包计数、`fLaC`+STREAMINFO），按计数分离
  元数据包与音频包，解析 STREAMINFO 的采样率/声道/位深/总样本数
  （总数为 0 时取末页 granule），并组装出一个可直接交给
  `ff_flac_decode` 的原生 `fLaC` 流。
- **mux**：按映射头 + 元数据页 + 单音频页（granule = 总样本数）输出；
  头计数按 big-endian 写入（Ogg 页字段是 little-endian，容易写错）。
- 页工具复用 `ogg_mux_page_multi`（新增多包页，把 Opus 写入器的单包页
  改为其包装），Ogg CRC/页解析/续页处理沿用既有实现。

验证：ffmpeg `-f ogg` 的 24-bit 44.1 kHz 单声道 FLAC（9 帧、总样本
8820）嵌入白盒测试，demux → 解码 PCM 与 ffmpeg s32le 参考 MD5 一致；
demux 出的 STREAMINFO/元数据/帧再经本 muxer 写出，重新 demux 后解码
仍一致。外部验证：本 muxer 输出的 `.oga` 交 ffprobe（flac 24-bit/
44.1 kHz/mono）与 ffmpeg 解码，PCM MD5 与参考完全相同。
全量 wasm **1074/1074**。

## 119. MKV 单轨 AAC / FLAC 写入器

补齐 Matroska 音频写入：`libavformat/mkv_audio_mux.mbt` 提供
`MkvAacMuxer`（`A_AAC`，CodecPrivate=AudioSpecificConfig）与
`MkvFlacMuxer`（`A_FLAC`，CodecPrivate=`fLaC`+STREAMINFO/元数据），
单 Cluster、时间戳毫秒。

两个实现细节：

- **Cluster Timecode 是 EBML_UINT**（FFmpeg `matroskadec` 同语义），
  不能写负值；AAC priming 的负 PTS 用 **CodecDelay（0x56AA）** 表达：
  remux 时容器时间戳整体 +delay，再把 `codec_delay_ns` 写进
  TrackEntry，解复用端减回去。ffmpeg 解码本 muxer 的输出因此与源
  PCM 完全一致（s16le MD5 相同）。
- **EBML 多字节 id 必须写对 id 长度**：BitDepth 的 id `0x6264` 初版
  按 1 字节写，尾部只剩 `0x64`，解析端把后续 size 字节并进 id 得到
  `0x6481`，Audio 元素越界触发 EBML level-check EINVAL；段遍历 resync
  后重读 Tracks，表现为“同一条轨出现两次”。修正为 2 字节 id。

验证：真实 MKV（HEVC+AAC）的 45 个 AAC 包 remux 成单轨 MKV 后，
解复用 PTS 恢复为 -23/0/23…，逐包解码成功，ffmpeg 解码 PCM 与源一致；
Ogg-FLAC 夹具的 9 帧 remux 成 `A_FLAC` MKV 后，解复用 + 组装原生
`fLaC` 解码 PCM MD5 与参考一致，ffprobe 识别 flac/44.1k/mono/24-bit，
ffmpeg 解码 MD5 相同。全量 wasm **1076/1076**。

## 120. MP4 单轨 MP3 写入器

`libavformat/mp4_audio_mux.mbt` 新增 `Mp4Mp3Muxer`：`mp4a` 采样条目 +
`esds`（objectTypeIndication 0x6B MPEG-1 / 0x69 MPEG-2，无
DecoderSpecificInfo），复用 Opus/AAC 写入器的非分片样本表布局
（`ftyp`/`mdat`/`moov`、单 `trak`、`stts/stsc/stsz/stco`）。
`libavcodec` 侧为 `Mp3Pcm` 增加 `pcm_bytes()`（s16le）。

验证：ffmpeg libmp3lame 的 13 帧 CBR 44.1 kHz 单声道 MP3 逐帧喂入，
`av_mov_open` 读回 `codec_id=mp3`、13 个样本、`sample_rate=44100`、
`channels=1`，样本按偏移拼回的字节与原帧流完全一致；解码 PCM 与
FFmpeg **定点** `mp3` 解码器一致。注意 FFmpeg CLI 默认的 `mp3float`
在首个非零样本即差 1 LSB（本仓库既有 MP3 白盒套件同样以定点解码器
为基准）。外部验证：ffprobe 识别 mp3/44.1k/mono，ffmpeg `-c:a mp3`
解码本 muxer 输出 MD5 与参考相同。全量 wasm **1077/1077**。

## 121. MP4 FLAC 写入与 `fLaC`/`dfLa` 解复用

补 FLAC-in-MP4 双向：

- **写入**（`mp4_audio_mux.mbt` 新增 `Mp4FlacMuxer`）：`fLaC` 音频
  采样条目 + `dfLa` FullBox（version 0 + STREAMINFO 元数据块，可带
  后续元数据块）；STREAMINFO 解析复用 Ogg-FLAC 的位域读取，采样率/
  声道/位深写入采样条目；样本表布局同 Opus/AAC 写入器。
- **读取**（`mov.mbt`/`extradata.mbt`）：新增 `dfLa` 子 box 解析，
  跳过 FullBox 头后要求首块为 type 0/长度 34 的 STREAMINFO，只把
  34 字节 payload 存为 extradata（与 FFmpeg `mov_read_dfla` 一致，
  后续元数据块忽略）；fourcc `fLaC` 经 `mov_codec_name` 解析为
  `flac`。

验证：Ogg-FLAC 夹具的 STREAMINFO/元数据/9 帧写入 MP4 后，
`av_mov_open` 读回 `codec=fLaC`、extradata 与 STREAMINFO 逐字节相同、
9 个样本、采样率/声道正确；样本按偏移拼回后与源帧总字节相等，组装
原生 `fLaC` 流解码 PCM MD5 与参考一致。外部：ffprobe 识别
flac/44.1k/mono，ffmpeg 解码本 muxer 输出 MD5 相同。全量 wasm
**1078/1078**。

## 122. TS 写入器 HEVC/MP3 外部解码验证

`ts_mux` 早已支持 PMT stream type 0x24（HEVC）与 0x03（MP3），但此前
只有本项目 demux 的结构回环。本轮把 muxer 输出交给 ffmpeg 9.0.2：
ffprobe 识别 hevc+mp3 两路，`-threads 1` 解码的 3 帧视频 MD5 与
MP3 定点解码 PCM MD5 都与直接解码原始基本流完全一致（AUD 分隔的
x265 HEVC AU 与 CBR MP3 帧）。

新增白盒 `ts_mux_decode_wbtest.mbt`：把 muxer 输出经 `av_mpegts_open`
取回 payload，拼接后分别用 `ff_hevc_decode_frames` 与
`decode_mp3_stream` 解码，断言与 FFmpeg 参考 MD5 一致（视频三帧 +
PCM），把外部验证固化为回归。全量 wasm **1079/1079**。

## 123. fMP4/HLS 真实码流互操作验证与 EXT-X-MAP 写入

此前 fMP4 与 HLS 写入器只有合成负载回环。本轮用真实 x264 流
（64×48、5 帧、avcC + 长度前缀样本取自 ffmpeg 的 MP4）验证：

- `fmp4_h264_init_segment` + `fmp4_media_segment` 写出的初始化/媒体段
  经 `av_mov_open` 读回（avcC 逐字节一致、5 个样本），把样本与 avcC
  的 SPS/PPS 转回 Annex B 后用本移植的 H.264 解码器解码，5 帧 MD5
  与 FFmpeg 9.0.2 单线程一致（新增
  `fmp4_h264_realdata_wbtest.mbt`）。
- HLS 媒体播放列表写入器此前不输出 `EXT-X-MAP`，fMP4 HLS 无法被播放；
  为其增加 `map_uri` 参数并输出 `#EXT-X-MAP:URI="..."`，`av_hls_parse`
  回读 `maps`/`map_index` 有测试覆盖。
- 外部验证：init + seg + 本写入器生成的 `media.m3u8` 交 ffmpeg 9.0.2
  播放，5 帧 framemd5 与参考完全一致。

全量 wasm **1081/1081**。

## 124. DASH 真实 fMP4 链路验证

用与 HLS 轮相同的 x264 fMP4 夹具验证 DASH 全链路：本项目
`dash_write_segment_template_mpd` 生成 SegmentTemplate MPD（timescale
90000、duration 45000、startNumber 1、`seg-$Number$.m4s`），
`av_dash_parse` 展开出 `init.mp4` + `seg-1.m4s`，`DashSegmentReader`
经 `MediaFetch` 回调取回初始化+媒体段并拼接，`av_mov_open` 读回 5 个
样本，再转 Annex B 用本移植 H.264 解码，5 帧 MD5 与 FFmpeg 单线程
一致（`dash_h264_realdata_wbtest.mbt`）。

外部验证：init/seg-1 与 MPD 交 ffmpeg 9.0.2 播放，5 个不同帧的
framemd5 与参考完全一致。注意 ffmpeg CLI 对本地 DASH 会把探测与
播放各读一遍（ffmpeg 自己 `-f dash` 生成的 MPD 同样解出 10 帧），
比对时按去重后的 5 帧看。全量 wasm **1082/1082**。

## 125. MP4 Opus 写入端到端验证

`mp4_opus_muxer` 用真实 Opus 包（307 字节，单声道、pre-skip 312）写
三帧 MP4：ffprobe 识别 `opus/48000/mono`，ffmpeg 9.0.2 解码输出
2568 个样本（3×960 减去首个 pre-skip）。`av_mov_open` 读回的
`dOps` 经 `parse_dops` 转成 OpusHead，喂给本移植的
`OpusDecoder`，同样得到 2880 个样本；按容器 pre-skip 去掉前 312 个
后与 ffmpeg 的 s16le 逐样本比较，2568 个样本中仅 4 个差 1 LSB
（浮点/定点舍入差异），其余完全一致。新增
`mp4_opus_realdata_wbtest.mbt` 固化该对比。全量 wasm **1083/1083**。

## 126. WebM VP9+Opus 真实写入验证

`webm_av_muxer` 用真实码流验证：libvpx 编码的 5 帧 VP9（64×48，首帧
关键帧，IVF 抽取）+ 3 个真实单声道 Opus 包（pre-skip 312），
`av_matroska_open` 读回 `V_VP9`/`A_OPUS` 两轨、5+3 个包；视频包喂给
本移植 `Vp9Decoder`，5 帧 YUV MD5 与 FFmpeg 解码源 IVF 一致；
`OpusHead`（来自 CodecPrivate）构造 `OpusDecoder`，裁掉 312 pre-skip
后与 FFmpeg 参考 PCM 最大差 1 LSB（与 MP4 Opus 轮同一参考）。

外部验证：ffprobe 识别 vp9 64×48 + opus mono，ffmpeg 解码的视频
framemd5 与 IVF 参考完全一致，音频 s16le MD5 与 MP4 Opus 参考完全
相同。新增 `webm_av_realdata_wbtest.mbt`。全量 wasm **1084/1084**。

## 127. ADTS AAC 与 Ogg Opus 写入真实数据验证

两条音频写入器此前只有本项目 demux 回环：

- **ADTS**：取真实 MKV 前 5 个 AAC 访问单元（44.1 kHz 单声道），
  `adts_muxer` 写出后 `av_adts_open` 读回 5 帧，按 7 字节头剥离后用
  `new_aac_decoder_from_asc`（LC 44100/mono 的 ASC `12 08`）解码，
  5120 个样本与 FFmpeg 解码本 ADTS 的 s16le 参考最大差 1 LSB。
  ffprobe 识别 aac/44100/mono。
- **Ogg Opus**：3 个真实 Opus 包（pre-skip 312）写入 Ogg，
  `av_ogg_opus_open` 读回、OpusHead→`OpusDecoder` 解码，裁 312 后与
  MP4 Opus 轮同一 FFmpeg 参考 PCM 最大差 1 LSB。外部 ffmpeg 解码
  本 Ogg 的 s16le MD5 与 MP4 参考完全相同。

新增 `adts_ogg_realdata_wbtest.mbt`。全量 wasm **1086/1086**。

## 128. HLS AES-128 真实加密流验证

用 ffmpeg 9.0.2 `-f hls -hls_key_info_file` 生成真实 AES-128 加密的
HLS 集（单 TS 分片、`EXT-X-KEY:METHOD=AES-128,URI="key.bin",IV=0x0`、
key 16 字节），走本项目完整读链路：`av_hls_parse` 解析 KEY →
`HlsSegmentReader` 经 `MediaFetch` 取 key/分片 → AES-128 解密 →
`av_mpegts_open` 解复用 → H.264 解码，5 帧 MD5 与 ffmpeg 解码同一
加密 HLS 集一致（外部参考需 `-allowed_extensions ALL`，ffmpeg 默认
拦截 .bin 扩展名）。

新增 `hls_aes_realdata_wbtest.mbt`（嵌入 playlist/key/加密分片）。
全量 wasm **1087/1087**。

## 129. TS 写入器 AAC ADTS 封装与 H.264/AAC 验证

发现并修复 TS 写入器的 AAC 互操作缺口：stream type 0x0F 的 PES 直接
携带原始 AAC AU 时，ffprobe 将其识别为 unknown（TS 里的 AAC 需要
ADTS 同步头）。新增 `TsMuxer::enable_adts(sample_rate, channels)`，
启用后 `write_audio` 自动为每个 AU 加 7 字节 ADTS 头（超 13-bit
长度的帧回退为原始写入）。

用真实 x264 AU（avcC 参数前置）与真实 AAC 帧（44.1 kHz 单声道）
验证：ffprobe 识别 h264+aac，ffmpeg 解码视频 5 帧 framemd5 与源
一致、音频 s16le MD5 与本项目 ADTS 输出参考完全一致。新增
`ts_h264_aac_decode_wbtest.mbt`：demux 后视频解码对齐
`fmp4_h264_refs`，音频剥 ADTS 头后 AAC 解码与 ADTS 参考最大差
1 LSB。全量 wasm **1088/1088**。

## 130. WAV 写入器 extensible float 修正与 ffmpeg 外部验证

对照 FFmpeg 9.0.2 `riffenc.c`/`wavenc.c` 修正两处写入器语义：
- 多声道（或显式 channel mask）float WAV 原先写 `format_tag=3` 且
  fmt 块长 40，属于自相矛盾；现按 `ff_put_wav_header` 写
  `WAVE_FORMAT_EXTENSIBLE`(0xFFFE)，IEEE float 由子格式 GUID 表达。
- 非 PCM（`codec_tag != 1`）补 `fact` 块，负载为帧数（样本数/声道），
  RIFF 大小同步计入，与 `wav_write_header`/`wav_write_trailer` 一致。

外部验证：用写入器生成 8-bit mono 8 kHz、16-bit stereo 48/96 kHz、
24-bit stereo 44.1 kHz、32-bit stereo、float32 stereo、16-bit 5.1、
float32 5.1 共 8 个 WAV，经 docker ffmpeg 9.0.2 解码为对应原始
PCM（u8/s16le/s24le/s32le/f32le），全部与源样本逐字节一致；
ffprobe 正确识别 codec、采样率、声道数，5.1 输出报
`channel_layout=5.1`（extensible float 修正前会丢失该信息）。

新增回归测试：extensible float 头的 tag/cbSize/valid bits/mask/GUID
与 fact 偏移、24/32-bit PCM 载荷字节、96 kHz 纯 WAVEFORMATEX。
全量 wasm **1091/1091**。

## 131. 音频容器写入器的 ffmpeg 外部解码验证

对已有音频写入器做外部互通抽查：用共享真实素材生成 MP4 MP3（13 帧
CBR mono）、MP4 FLAC / MKV FLAC / Ogg FLAC（Ogg-FLAC fixture 9 帧
重新封装）、MKV AAC（真实 MKV 的 45 个 AAC 包重封装），分别经
docker ffmpeg 9.0.2 解码：
- MP4 FLAC / MKV FLAC / Ogg FLAC 与源 Ogg 的 s32le PCM MD5 全部等于
  `2d5bb7ccf66e68c7045c4032f947c881`（即本项目参考值），容器封装的
  时间戳不同不影响音频内容。
- MP4 MP3 用固定点 `mp3` 解码器输出 s16le MD5
  `7399f7601b7f5f4652329db70d3ddbfb`，与写入器测试参考一致。
- MKV AAC 输出与源 MKV 音轨解码 PCM 完全一致；在回归测试中补充
  “重封装后每个包字节与源相同”的断言（比解码一致更强）。

本轮未发现写入器缺陷。全量 wasm **1091/1091**。

## 132. MP4 单轨 H.264/HEVC 写入器的真实样本验证

此前 `mp4_h264_muxer`/`mp4_hevc_muxer` 只用伪样本做回读测试。现复用
fMP4 H.264 fixture（x264 64x48 High，5 帧 avcC + 长度前缀样本）与
MKV HEVC fixture（64x64 Rext 4:4:4，5 个 AU + hvcC）：
- 写入非分片 MP4 后由 `av_mov_open` 读回：avc1/hvc1、hvcC/avcC
  extradata、5 个样本与时长均正确；H.264 转 Annex B 解码 5 帧 MD5
  等于 `fmp4_h264_refs`，HEVC 直接解码（按 POC 排序）等于
  `mkv_hevc_refs`。
- 外部验证：docker ffmpeg 9.0.2 的 `-f framemd5` 输出 10 个帧 MD5
  与上述两组参考完全一致（HEVC 报 yuv444p/Rext，符合 fixture）。

新增测试并入 `mp4_video_mux_wbtest.mbt` / `mp4_hevc_mux_wbtest.mbt`。
全量 wasm **1093/1093**。

## 133. MP4 单轨 AV1/VP9 写入器的真实样本验证

补齐 `mp4_av1_muxer`/`mp4_vp9_muxer` 的真实码流验证（此前只有伪样本）：
- AV1：用 docker ffmpeg 9.0.2 `libaom-av1` 编码 64x48 5 帧（Main，
  yuv420p）生成源 MP4；`av_mov_open` 读出 av1C（16 字节）与 5 个
  temporal unit 作为夹具；写入我们的 MP4 后回读样本字节一致，端口
  `Av1Decoder` 解码 `packed()` MD5 与源帧一致。
- VP9：复用 WebM VP9 夹具（64x48 5 帧）写入 MP4（vpcC profile 0、
  8-bit 4:2:0），回读样本字节一致，`vp9_decoder_new` 解码 MD5 对齐
  `webm_av_vp9_refs`。
- 外部验证：两个输出经 ffmpeg `-f framemd5` 解码，5+5 帧 MD5 与上述
  参考全部一致（AV1 报 Main/yuv420p，VP9 报 Profile 0）。
- 期间确认 `av_mov_open` 可直接解析 ffmpeg 的 av01 MP4 并恢复 av1C。

新增测试并入 `mp4_av1_mux_wbtest.mbt` / `mp4_vp9_mux_wbtest.mbt`。
全量 wasm **1095/1095**。

## 134. WebM/Matroska AV1 写入器的真实样本验证

复用 §133 的 libaom-av1 夹具（av1C 16 字节 + 5 个 temporal unit）写入
WebM AV1：回读 `V_AV1`、CodecPrivate、5 个包字节一致，端口 AV1 解码
`packed()` MD5 与参考一致。外部 ffmpeg 9.0.2 解码该 WebM 得到 5 帧
framemd5，与同一组参考完全相同（av1/yuv420p）。

新增测试并入 `webm_av1_mux_wbtest.mbt`。全量 wasm **1096/1096**。

## 135. MP4 双轨 H.264+AAC 写入器真实样本验证

`mp4_av_muxer` 此前只有伪包测试。现用真实数据（fMP4 x264 5 帧 +
源 MKV 的 45 个 AAC-LC 44.1 kHz 单声道包）写入双轨非分片 MP4：
- 回读两条轨的样本表与载荷：视频转 Annex B 解码 5 帧 MD5 等于
  `fmp4_h264_refs`；AAC 45 包字节与源相同并逐帧解码成功。
- 外部 ffmpeg 9.0.2：视频 framemd5 与参考一致；音频解码 PCM
  MD5 `cfe6223331361877aef4f7e88a2a50bc` 与把同一组 AU 写成 ADTS
  后 ffmpeg 解码结果完全一致（92160 字节）。源 MKV 因 CodecDelay 会
  裁掉一帧 priming（90112 字节），本 MP4 写入器不建模初始 padding/
  edit list，与既有 MP4 Opus pre-skip 说明一致，此项已记录。

新增测试并入 `mp4_av_mux_wbtest.mbt`。全量 wasm **1097/1097**。

## 136. MKV/Matroska H.264 与 H.264+AAC 写入器的真实样本验证

`mkv_h264_muxer` 与 `mkv_av_muxer` 此前只有伪包测试。现用真实数据
（x264 5 帧 + 源 MKV 的 45 个 AAC-LC 44.1 kHz 单声道包）：
- 单轨写 5 帧、双轨交错写 5 视频帧 + 45 音频包；回读视频包字节一致、
  双轨音频包与源逐包相同；视频转 Annex B 解码 5 帧 MD5 等于
  `fmp4_h264_refs`，AAC 45 帧逐帧解码成功。
- 外部 ffmpeg 9.0.2：单轨/双轨视频 framemd5（需 `-fps_mode
  passthrough`，否则 CLI 默认 CFR 会按时间戳丢帧）5+5 帧与参考一致；
  双轨音频 s16le MD5 `cfe6223331361877aef4f7e88a2a50bc` 与同 AU 的
  ADTS 解码逐字节一致。

新增测试并入 `mkv_h264_mux_wbtest.mbt` / `mkv_av_mux_wbtest.mbt`。
全量 wasm **1099/1099**。

## 137. MP4 AAC priming 编辑列表（edts/elst）

§135 记录的 priming 缺口补齐：`mp4_aac_muxer_new` 与 `mp4_av_muxer_new`
新增可选 `priming`/`aac_priming`（采样数）。>0 时按 `movenc` 的方式写
单条 `edts`/`elst`（media_time=priming，segment_duration 按电影时基
向上取整），mvhd/tkhd 用显示时长、mdhd 保留完整媒体时长；普通
WAVEFORMAT/样本表不变。
- 回归：单轨 3 包与双轨（1 视频 + 3 音频）回读 `skip_samples` 分别为
  1024，视频轨不受影响；负 priming 报错。
- 外部：45 个 AAC 包 + priming=1024 的单轨/双轨 MP4 经 ffmpeg 9.0.2
  解码为 90112 字节 s16le，MD5 `8cc619542aaa74e3769b295fb26c0866`
  与源 MKV（CodecDelay 裁掉一帧后）解码完全一致。

全量 wasm **1102/1102**。

## 138. WAV 写入器 RF64 大文件头

`wav_muxer_new` 新增 `rf64` 开关：置位后按 `wavenc.c` 的 RF64 布局写
`RF64`/`0xFFFFFFFF`/`WAVE`/`ds64`（riffSize、dataSize、sampleCount、
0 表项），`data` 块大小同样写 0xFFFFFFFF；PCM/float/extensible 载荷
不变，可与既有选项组合。
- 回归：16-bit stereo 与 extensible float 5.1 两种 RF64 文件回读
  codec/data_size/sample_count/data_offset 与载荷正确。
- 外部：ffmpeg 9.0.2 正确识别（含 5.1 布局）并解码，两个文件样本
  与源逐字节一致。

全量 wasm **1104/1104**。

## 139. WAV 写入器 RIFX（大端）支持与读端 BE 映射

`wav_muxer_new` 新增 `big_endian` 开关：写 `RIFX` 容器，块大小/格式
字段与 16/24/32-bit/float 载荷均为大端，8-bit 载荷不变；RIFX 与
RF64/extensible 组合在构造时报错（FFmpeg 的 RIFX 读端同样拒绝
WAVEFORMATEX）。
- 读端补充 `ff_get_wav_header` 的 BE 映射：RIFX 的 `pcm_s16/s24/s32/
  f32/f64` 映射为对应 `pcm_*be`。注意这是**有意偏离 FFmpeg 9.0.2**：
  其 wavdemux 对 RIFX 仍报 LE codec id，会把 BE 载荷按 LE 解码（实测
  ffmpeg 的 16-bit RIFX 输出恰为源样本逐字字节交换）。
- 回归：16-bit stereo / 24-bit mono / float32 stereo / 8-bit mono 的
  RIFX 头字段、载荷字节与回读（BE codec 名、data offset/size）断言。
- 外部：ffprobe 可解析四种 RIFX；8-bit（字节序无关）经 ffmpeg 解码
  与源逐字节一致；16-bit 解码结果等于源字节交换，印证上述 ffmpeg
  限制而非写入错误。

全量 wasm **1108/1108**。

## 140. AV1 film grain 合成（show-existing 输出）

按 dav1d 1.5.1 `src/fg_apply_tmpl.c`、`src/filmgrain_tmpl.c` 与
`src/tables.c`（BSD-2-Clause）把 AV1 film grain 合成移植到纯 MoonBit：
- 帧头解析由 `br.skip` 改为完整读取：缩放的 (value, scaling) 点表、
  AR 系数（`raw-128`）、uv mult/luma_mult（`raw-128`）、uv offset
  （9-bit `raw-256`），与 dav1d 的符号语义一致。
- 新增 `libavcodec/av1_grain.mbt`：缩放 LUT 线性插值（8/10/12-bit）、
  LFSR + 高斯序列 grain 模板、AR 滤波、32×32 块 offset/overlap 混合、
  色度 luma 混合与 clip_to_restricted；输出为 grain 后副本，参考帧保持
  未加 grain 的样本。
- show-existing 输出按其参考帧保存的 grain 参数与 seed 应用。
- 调试过程：用独立 C harness（dav1d 同款生成/应用函数）逐级对拍，
  发现色度按 luma 32×32 块行偏移写到了 `row*32` 而非
  `(row*32)>>ss_y`，修正后 Y/U 平面与 C 逐像素一致。
- 验证：libaom `-aom-params film-grain-test=1` 64x48 5 帧流（含
  show-existing 帧），端口输出 5 帧 MD5 与 dav1d 加 grain 的
  framemd5 完全一致（2081ffab…、4777434c…、3a2f0e8a…、10f104e4…、
  b77fbad1…）。libavformat 的 grain 参数测试同步改为有符号期望值。

grain-from-reference（inter 帧 update_grain=0）仍显式报错。
全量 wasm **1109/1109**。

## 141. fMP4 HEVC/AV1/VP9 写入器的真实样本验证

`fmp4_hevc_init_segment` / `fmp4_av1_init_segment` / `fmp4_vp9_init_segment`
+ `fmp4_media_segment` 此前只有伪样本回读测试。现用真实夹具（MKV HEVC
5 AU、libaom AV1 5 TU、WebM VP9 5 帧）生成 fMP4：
- 回读 `av_mov_open` 校验 hvc1/av01/vp09、extradata、5 个样本；HEVC
  载荷等于按 AU 重打包后的长度前缀流，AV1/VP9 载荷与输入帧逐字节一致；
  本地解码 MD5 分别对齐 `mkv_hevc_refs`（POC 排序）、
  `mp4_av1_real_refs`、`webm_av_vp9_refs`。
- 外部：三个 init+media 文件经 ffmpeg 9.0.2 `-fps_mode passthrough`
  `-f framemd5` 解码，5+5+5 帧 MD5 与上述参考完全一致。

新增测试 `fmp4_video_realdata_wbtest.mbt`。全量 wasm **1112/1112**。

## 142. AV1 film grain from reference（update_grain=0）

补齐 AV1 帧头 `update_grain=0` 路径：inter 帧读取 3-bit `refidx`，
校验其出现在当前帧 `ref_frame_idx` 列表中，并从 `Av1RefSlotState.header`
（此前仅供 segmentation 继承的完整参考帧头）复制缩放点表/AR 系数/
色度系数等 grain 参数，seed 保留当前帧读到的 16-bit 值，与 dav1d
`obu.c` 的 `load_grain_params` 语义一致。

验证矢量由 `grain.ivf` 手工构造：用 `trace_headers` 定位最后一帧的
`update_grain` 位（payload 145）与 128 字节帧头边界，把该位改为 0、
插入 refidx=2、按字节对齐后拼回原 tile 数据并重建 OBU/IVF 大小。因
libaom 测试流的各帧 grain 参数相同，引用继承后的输出与原 update=1
文件一致，但若未复制参数则会退化为无 grain 画面。dav1d 与端口在该
变体上 5 帧 MD5 完全相同（2081ffab…b77fbad1…）。

新增回归 `av1: film grain from reference matches dav1d`。全量 wasm
**1113/1113**。

## 143. HLS AES-128 写入（EXT-X-KEY + 加密分片）

补齐 HLS 写入侧加密：
- `hls_encrypt_aes128(key, iv, data)`：AES-128-CBC + PKCS#7（与 FFmpeg
  `crypto` 协议的 `crypto_write`/`close` 行为一致；整块时补 16 字节），
  复用 `av_aes_crypt` 的加密方向。
- `hls_write_media_playlist(..., key_uri=, key_iv=)` 在
  MEDIA-SEQUENCE 后输出 `#EXT-X-KEY:METHOD=AES-128,URI="...",IV=0x`
  大写十六进制。
- 回归：0..100 字节各长度加密/解密往返、短 key/IV 报错；EXT-X-KEY
  文本与 `av_hls_parse` 的 key/iv 解析；真实 H.264+AAC TS 分片加密后
  经 `av_hls_parse`+`HlsSegmentReader` + 内存 fetch 解密、解复用并
  解码，视频 5 帧 MD5 对齐 `fmp4_h264_refs`。
- 外部：ffmpeg 9.0.2 以 `-allowed_extensions ALL` 播放本项目生成的
  加密 HLS，ffprobe 识别 h264+aac，视频 framemd5 与参考一致，音频
  s16le MD5 `0389e90a…` 与明文 HLS 完全相同。

全量 wasm **1116/1116**。

## 144. FLAC 32-bit stereo 的 33-bps side channel

此前 32-bit 立体声的 decorrelation 模式直接报错。现按 FFmpeg
`flacdec.c` 的 `decoded_33bps` 路径补齐：
- 新增 `flac_sbits64`（33-bit 有符号读取）与
  `flac_decode_subframe_{fixed,lpc}_33`：33-bit warm-up 写 Int64 缓冲，
  32-bit Rice 残差经 `DECODER_SUBFRAME_FIXED_WIDE` 的 64 位组合式重建；
  constant/verbatim 33-bit 同样支持。
- wasted bits：原 bps 为 33 且 `bps - wasted <= 32` 时按 FFmpeg
  `flac_wasted_33_c` 左移到 Int64 缓冲。
- decorrelate：left/right/mid-side 的 side 取 Int64 缓冲，按 FFmpeg
  `decorrelate_33bps` 的 64 位补码运算后截断为 32-bit 输出。
- 验证：ffmpeg 9.0.2 以 `-sample_fmt s32 -bits_per_raw_sample 32
  -strict experimental` 生成 4 个 0.05 s 32-bit stereo FLAC（噪声 LPC、
  相关 mid-side、单边强 side、compression_level 0 fixed），端口解码
  s32le MD5 与 ffmpeg 逐一相同（eac0717f…、8d5e77ce…、2883edaa…）。

新增 `libavcodec/flac_33bps_test.mbt`。全量 wasm **1117/1117**。

## 145. AAC PS 编码负载组合扩充

重写 `scripts/generate_aac_ps_tests.py` 的 `payload_cases`（保持 8 帧，
夹具体积不变），新增此前未覆盖的语法组合：
- header 复用（payload 0 位），沿用上一帧的 IID/ICC/扩展开关；
- `enable_iid=0` / `enable_icc=0`（FFmpeg 会清零对应参数数组）；
- fixed grid 0/1/2/4 包络（含 0 包络 + 跨帧 fake envelope 补齐）；
- 变量网格 4 包络且末边界 29 触发补包络；
- 扩展块开关切换（header 关扩展、扩展 id 0 关闭 IPD/OPD、开启 IPD/OPD）；
- 10/20/34 频带与量化/非量化 IID、ICC mode 交叉。

用 docker `vpxdev-img` 重建插桩 aacps + payload harness，生成新的
`payload_psbin` 与 HOUT/ROUT/LBUF/DEC/H 参考；驱动断言改为按 case
表校验 num_env/is34bands/nr_* 频带数。端口无需改码即全部通过：
LBUF/DEC/H 最大差 5.4e-7，65536 个 QMF 输出最大差 1.5e-6（与既有容差
同量级）。syntax/pcm 测试同步按生成器格式重排（数据不变）。

全量 wasm **1117/1117**。

## 146. HLS 多 rendition 主播放列表与单流 TS

- `hls_write_master_playlist` 改为结构化 API：`HlsMasterVariant`
  （uri/bandwidth/分辨率/codecs/可选 AUDIO 组）与 `HlsMasterRendition`
  （EXT-X-MEDIA：TYPE/GROUP-ID/NAME/LANGUAGE/DEFAULT/AUTOSELECT/
  CHANNELS/URI）。输出 `#EXT-X-MEDIA` 行与
  `EXT-X-STREAM-INF...,AUDIO="group"`；`av_hls_parse` 可回读
  renditions 全部属性。
- `TsMuxer` 新增 `video~`/`audio~` 开关：PMT 只列出启用的 ES，
  PCR_PID 取第一个启用的流（音频单轨 TS 的 PMT 只含 AAC 条目）。
- 回归：主播放列表/rendition 文本与解析、音频单流与视频单流 TS 的
  PMT 流数。
- 外部：视频 TS + 音轨 rendition（audio-only TS，ADTS）+ 主播放列表，
  ffmpeg 9.0.2 `-allowed_extensions ALL` 播放：识别 h264+aac，视频
  framemd5 对齐 `fmp4_h264_refs`，音频 MD5 `0389e90a…` 与既有 ADTS
  参考相同。

全量 wasm **1119/1119**。

## 147. DASH 多 AdaptationSet（视频 + 音频）

`dash_write_segment_template_mpd` 改为结构化 API：`DashAdaptation`
（contentType/mimeType/codecs/id/bandwidth/timescale/duration/init/media
模板，视频带 width/height，音频带 audioSamplingRate，可选 lang），一个
Period 内可输出多个 AdaptationSet/Representation。
- 回归：视频+音频 MPD 的文本与 `av_dash_parse` 结构（content_type、
  lang、audio_sampling_rate、codec/segment 展开）。
- 新增 `dash_audio_realdata_wbtest.mbt`：真实 AAC 5 帧写入 fMP4
  init/media，作为音频 AdaptationSet；`DashSegmentReader` 取回拼接后
  `av_mov_open` 解复用、AAC 解码与 ADTS 参考最大差 1。
- 外部：双轨 MPD（H.264 视频 + AAC 音频）经 ffmpeg 9.0.2
  `-allowed_extensions ALL` 播放；视频 framemd5 为参考序列（CLI 本地
  DASH 的探测+播放双读已知行为，每帧出现两次）；音频前 10240 字节
  （5 帧）MD5 `0389e90a…` 与 ADTS 参考一致。
- wasm_probe 的 DASH 签名同步更新。

全量 wasm **1121/1121**。

## 148. HLS fMP4 分离音轨 rendition 验证

在 §146 的 rendition 主播放列表与 §141 的 fMP4 写入器基础上补齐组合
验证：视频/音频各自 fMP4 init+media（H.264 5 帧、AAC 5 帧），媒体
播放列表带 `EXT-X-MAP`（version=6），主播放列表用 `EXT-X-MEDIA`
音频组关联。
- 回归 `hls_fmp4_renditions_wbtest.mbt`：解析主/媒体播放列表的
  rendition 与 map，`HlsSegmentReader` 取回 init+media，`av_mov_open`
  解复用后 H.264 5 帧 MD5 对齐 `fmp4_h264_refs`，AAC 5 帧解码成功。
- 外部：ffmpeg 9.0.2 `-allowed_extensions ALL` 播放主播放列表，识别
  h264+aac，视频 framemd5 与参考一致，音频 s16le MD5
  `0389e90a81fbef4945c3bbe0b11c048c` 与 ADTS 参考相同（fMP4 HLS 无
  DASH CLI 的双读现象）。

全量 wasm **1122/1122**。

## 149. AV1 intra block copy（intrabc）

补齐 AV1 intrabc（此前显式 PATCHWELCOME）：
- 关键帧 `decode_b` 在 `allow_intrabc` 时按 CDF 读 intrabc 位；intrabc
  块跳过普通帧内模式，改走新的 `decode_intrabc_block`：
  refmvs 以 ref {INTRA,-1} 取空间 DV 候选（无候选时按 SB 行给默认
  DV）、`av1_read_mv_residual(..., -1)` 整数残差，按 tile 边界/当前 SB
  裁剪并重算像素 DV；随后 `read_vartx_tree` 读变换树。
- 预测/残差复用 inter 重建：把当前帧 planes 包成 `Av1ReconRefPlane`
  （参考=当前帧）、`filter2d=9`（bilinear）构造 `Av1InterBlock`，
  调 `av1_recon_b_inter` 完成预测拷贝与残差/逆变换；随后按 dav1d
  set_ctx 更新 a/l 上下文（DC_PRED、intra=0、pal=0、skip 等）并把
  intrabc DV splat 进 refmvs；普通帧内块在 allow_intrabc 帧也会
  splat INTRA 参考供 DV 预测。
- 关键帧的 refmvs：新增 `av1_refmvs_intrabc_state`（无时域参考），
  `av1_decode6` 在 `inter=None && allow_intrabc` 时为每个 tile 建立
  refmvs 状态。
- 验证：SVT-AV1 v4.2 `-svtav1-params enable-intrabc=1`（testsrc2/
  smptebars，各含 screen-content-mode=2 变体）4 组 16 帧，端口解码
  MD5 与 dav1d 1.5.1 framemd5 完全一致。

新增 `libavcodec/av1_intrabc_realdata_test.mbt`。全量 wasm **1123/1123**。

## 150. AV1 intrabc 的 10-bit 与多 tile 验证

在 §149 基础上用 SVT-AV1 生成更多组合：10-bit 4:2:0（64x48）、
128x96（4 SB、默认单 tile）、128x96 + `tile-columns=1:tile-rows=1`
（2×2 tiles）、以及 10-bit + 2×2 tiles。端口解码 4 组 16 帧 MD5 与
dav1d framemd5 全部一致，确认 DV 裁剪的 tile 边界处理、
refmvs 在 10-bit 下的 MV 精度与当前帧 bilinear 预测在色度/高位深下
均正确。永久回归扩展到 8 组 32 帧（8/10-bit、64x48/128x96、tiles）。

全量 wasm **1123/1123**。

## 151. DASH SegmentList MPD 写入器

在 §147 的 SegmentTemplate 之外补齐 SegmentList：新增
`DashListSegment`/`DashListAdaptation` 与 `dash_write_segment_list_mpd`，
每个 AdaptationSet 一个 Representation，支持两种布局：
- 分离文件：`<Initialization sourceURL=.../>` +
  `<SegmentURL media=.../>`；
- 单文件：`BaseURL` + `Initialization range` + `SegmentURL
  mediaRange`（reader 按偏移/长度抓取）。

`startNumber` 仅在非默认值（≠1）时写出。FFmpeg 9.0.2 的 dashdec
对显式 SegmentURL 列表按 `fragments[cur_seq_no]` 索引，而
`cur_seq_no` 会被 `startNumber` 平移：写 `startNumber="1"` 时单分片
列表 `1 < n_fragments` 为假，回退到模板路径并报
"Cannot get fragment, missing template URL"。DASH 规范缺省即 1，
省略后语义不变且 ffmpeg 可播放。

- 回归 `dash_segment_list_realdata_wbtest.mbt`：
  - 单文件布局：H.264 init+media 与 AAC init+media 拼成两个文件，
    MPD 用 BaseURL+range 描述；`av_dash_parse` 回读后
    `DashSegmentReader` 按范围取回、`av_mov_open` 解复用，H.264
    5 帧 MD5 对齐 `fmp4_h264_refs`，AAC 5 帧解码成功；断言默认
    `startNumber` 不写出。
  - 非默认 `startNumber="5"` 写出并回读 SegmentURL。
- 外部：分离文件 MPD 经 ffmpeg 9.0.2 播放（ffprobe 识别
  h264+aac；视频 framemd5 前 5 帧与参考一致，CLI 本地 DASH 双读
  已知行为导致随后重放；音频 s16le 整文件 MD5
  `fce8e95d1489dd5c53b4b9a22adaa2c7`，前 10240 字节（5 帧）
  `0389e90a…` 与 ADTS 参考一致）。单文件 mediaRange 变体在
  ffmpeg 本地文件上不可播放（探测阻塞），由内部 reader 覆盖。

全量 wasm **1125/1125**。

## 152. ALAC 解码与 MP4 `alac` 读取

新增 `libavcodec/alac.mbt`，移植 `alac.c` / `alac_data.c` / `alacdsp.c`：
- magic cookie 为 36 字节 QuickTime atom（size/tag/version +
  ALACSpecificConfig），解析 `max_samples_per_frame`、位深、Rice 参数、
  声道数与采样率；位深限 16/20/24/32，声道数 1-8。
- `decode_scalar`（最多 9 个 1 的 unary 前缀 + k 低位，或 bps 位逃逸）、
  `rice_decompress`（自适应 history、零块游程、`sign_modifier`）、
  `lpc_prediction`（自适应 FIR、prediction type 15 的 31 阶预处理、
  int16 系数回绕）、stereo decorrelation 与 extra bits 拼接。
- 元素按 SCE/CPE/LFE 解析；`ff_alac_channel_layout_offsets` 把 Apple
  元素声道序映射到标准声道序。输出交织 S16/S32（20-bit <<12、
  24-bit <<8），与 FFmpeg 默认输出一致。

MOV 解复用：样本条目内的 `alac` 子 box 按 `mov_read_extradata` 语义
重建完整 36 字节 atom（8 字节 box 头 + payload）作为 extradata，
`mov_codec_name` 的 `alac` 映射直接生效。

验证：
- ffmpeg 9.0.2 从确定性 WAV（正弦/静音/噪声/冲激）编码的真实夹具：
  16-bit 单声道、16-bit 双声道、24-bit 双声道（8 extra bits）、
  5.1 16-bit，各 6 帧（4096×5+1570），解码 PCM MD5 与 ffmpeg
  s16le/s32le 逐字节一致。`alac_realdata_test.mbt` 内嵌 cookie 与
  原始包；`mp4_alac_demux_realdata_wbtest.mbt` 内嵌真实 m4a，经
  `av_mov_open` 取包后解码结果相同。
- 手工构造未压缩元素（SCE/CPE、显式/隐式帧长、20/32-bit 缩放、
  无 END、通道越界、坏 cookie）覆盖参考编码器不产出的分支。

FFmpeg 的 ALAC 编码器只产出 16/24-bit，20/32-bit 路径无外部参考；
`extra_bits_bug` 非标准解码模式未复刻。

全量 wasm **1131/1131**。

## 153. MP4 ALAC 写入器

`mp4_audio_mux.mbt` 新增 `Mp4AlacMuxer`：`alac` 音频样本条目，36 字节
magic cookie 原样作为子 box（对应 movenc 的 `mov_write_extradata_tag`），
样本条目写声道数、`sample_size = cookie 位深` 与采样率；复用
`mp4_audio_document` 的 `ftyp`+`mdat`+`moov` 单轨布局。

- 回归 `mp4_alac_mux_wbtest.mbt`：真实 m4a 经 `av_mov_open` 逐包解码
  得到帧长后重封装；回读的 cookie、采样率、声道、样本数与源一致，
  重解码 PCM 与源逐字节相同（MD5 `38fb3080…`）。
- 外部：重封装文件经 ffmpeg 9.0.2 解码 s16le，MD5
  `38fb308079b63baf40e49bd051d22bc8` 与源参考一致；ffprobe 识别
  alac / 44100 / 2ch / 16-bit / 0.5s。

全量 wasm **1132/1132**。

## 154. TTA（The Lossless True Audio）解码

新增 `libavcodec/tta.mbt`：解析完整 native `.tta` 文件（`TTA1` 头、
seek 表与帧），实现 C 的小端位流读取器（`BITSTREAM_READER_LE`）、
自适应 Rice（k0/k1、sum0/sum1 与 shift 表）、8 阶混合滤波器
（qm/dx/dl/error 自适应）、固定 4/5 阶预测与多声道反相关；输出
8/16/24-bit PCM（24-bit 左移 8 位，8-bit 为 U8 偏置形式）。

- 夹具：ffmpeg 9.0.2 编码自 8 kHz 确定性 WAV（正弦/静音/噪声/冲激）。
  16-bit 单/双声道、24-bit 双声道、8-bit 双声道各 2.5 s（两整帧 +
  残帧），单声道恰为两整帧（残帧长度 0 路径）。解码 PCM 与 ffmpeg
  s16le/s32le/u8 逐字节一致（MD5 `a7e2b296…`、`793c16db…`、
  `322c025b…`、`b53c7498…`）。
- 构造测试拒绝坏 magic、加密格式、非法位深/采样率与截断 seek 表。

说明：header/seek/帧 CRC 不校验（与 FLAC 端口一致）；加密格式 2
明确拒绝。

全量 wasm **1134/1134**。

## 155. MP4 faststart 重排

新增 `mp4_faststart.mbt`：对非分片写入器产出的 `ftyp`+`mdat`+`moov`
文件，重排为 `ftyp`+`moov`+`mdat`，并把 `moov` 内 `stco`/`co64`
的每个 chunk 偏移加上 `moov` 大小（对应 ffmpeg 的
`-movflags +faststart`）。仅接受恰好这三个顶层 box；分片
（`moof`/`mdat`）文件、非法 box 大小与 stco 溢出均报错。

- 回归 `mp4_faststart_wbtest.mbt`：真实 H.264 5 帧经
  `mp4_h264_muxer` 写出后重排，断言 ftyp/moov/mdat 顺序、mdat
  字节不变、长度不变，回读解码 5 帧 MD5 与 `fmp4_h264_refs` 一致。
- 外部：重排文件经 ffmpeg 9.0.2 `ffprobe` 识别 h264 64x48/5 帧，
  framemd5 与参考逐帧一致。

全量 wasm **1135/1135**。

## 156. HLS EXT-X-BYTERANGE 写入与 MAP 范围

- `hls_mux.mbt` 新增 `HlsMediaSegmentSpec` 与
  `hls_write_media_playlist_byterange`：媒体播放列表条目可带
  `EXT-X-BYTERANGE:<len>[@<off>]`，`offset < 0` 省略 `@offset`
  （由解析端按前一子范围续接）；`EXT-X-MAP` 支持
  `BYTERANGE="<len>[@<off>]"` 属性。版本自动不低于 4。原
  `hls_write_media_playlist` 改为共享实现的薄包装，行为不变。
- `hls.mbt` 新增公开构造 `HlsByteRange::new(length, offset~)`；
  `EXT-X-MAP` 省略偏移时按 FFmpeg 语义取 0（此前会留下 -1 被
  `MediaFetch` 拒绝）。
- 回归 `hls_byterange_wbtest.mbt`：5 个真实 AAC 帧编码为 3 个
  音频 TS 分片，拼成单文件 `media.ts`；列表仅首段写偏移、后两段
  省略，解析后偏移按长度续接；`HlsSegmentReader` 按范围取回 3 段，
  TS 解复用 + AAC 解码共 5 包/5120 样本。另有 MAP BYTERANGE
  属性写读断言。
- 外部：ffmpeg 9.0.2 `-allowed_extensions ALL` 播放该单文件
  byterange 播放列表，输出 s16le MD5
  `0389e90a81fbef4945c3bbe0b11c048c` 与 ADTS 参考一致。

全量 wasm **1137/1137**。

## 157. DASH SegmentTimeline 写入器

新增 `dash_timeline_mux.mbt`：`DashTimelineSegment`（`d`/可选 `t`/`r`）
与 `DashTimelineAdaptation`，`dash_write_timeline_mpd` 输出带
`<SegmentTimeline>` 的 `SegmentTemplate`，媒体模板可含 `$Time$`
或 `$Number$`（与解析端 `dash_segments_from_timeline` 对齐）。

- 回归 `dash_timeline_realdata_wbtest.mbt`：真实 H.264 5 帧各写为
  一个单样本 fMP4 媒体分片（`video-<time>.m4s`），MPD 用
  `$Time$` + 5 个 `<S>`（仅首个带 t）；解析出的 time 为
  0/9000/.../36000，`DashSegmentReader` 取回后重组 Annex B 解码，
  5 帧 MD5 与 `fmp4_h264_refs` 一致。
- 外部：ffmpeg 9.0.2 播放该 MPD，视频 framemd5 前 5 帧与参考一致
  （本地 DASH CLI 双读的已知行为产生第 6 个重复/失参考帧）。

全量 wasm **1138/1138**。

## 158. ADPCM 解码（IMA WAV / MS / Yamaha）

新增 `libavcodec/adpcm.mbt`，移植 `adpcm.c` 与 `adpcm_data.c` 中的
三种块格式：
- IMA WAV 4-bit：每声道 4 字节头（预测值 le16、step index、保留），
  之后按 8 样本组读取 4 字节/声道；使用 `ff_adpcm_ima_qt_expand_nibble`
  的逐位步长分解（与直接乘法的通用 IMA 公式在截断上不同）。
- Microsoft ADPCM：1-2 声道交织布局（每块 predictor 索引 0..6、
  idelta/sample1/sample2，自适应系数表，输出首 2 个样本来自块头）。
- Yamaha ADPCM：无块头，步长/预测值状态跨块保持；低/高半字节分别
  给第 0/1 声道（单声道则同一状态产生两个样本）。
解码器按 `block_align` 切块并跨块保留状态，输出交织 S16。

验证：ffmpeg 9.0.2 编码的 8 kHz 确定性 WAV 夹具（IMA/MS/Yamaha ×
单/双声道，各 1 秒、8 个 1024 字节块）经 `av_wav_open` 解复用后逐块
解码，PCM 与 ffmpeg s16le 参考逐字节一致（MD5 `f9bdd690…`、
`5d822f73…`、`4bc43286…`、`e79bcc0e…`、`cbee695f…`、`c742c7fb…`）。
构造测试拒绝非法 step index/预测器索引、过短块与不支持的参数。

说明：MS/Yamaha 仅支持 1-2 声道交织布局；IMA 仅 4-bit（其余位深
由 C 的位读取分支处理，当前夹具不可得）。

全量 wasm **1141/1141**。

## 159. AIFF/AIFC PCM 读写

新增 `aiff.mbt`（解复用）与 `aiff_mux.mbt`（写入）：
- 读取 `FORM`/`AIFF|AIFC`、`COMM`、`SSND`，解析 80-bit IEEE 扩展
  采样率，按块对齐切分 PCM 包；支持 AIFF 大端 PCM 8/16/24/32-bit
  与 AIFC `NONE`/`sowt`（小端 PCM）。压缩 AIFC 拒绝。
- 写入 `FORM`/`AIFF` + `COMM` + `SSND`，大端交错有符号 PCM
  （AIFF 的 8-bit 是有符号，与 WAV 不同），采样率编码为 80-bit
  扩展浮点，chunk/FORM 长度与偶数字节填充由 `finish` 计算。

验证：
- ffmpeg 9.0.2 生成的 8 kHz 夹具（s16 单/双声道、s8、s24、s32
  双声道）经端口读取并转换为参考布局后，PCM MD5 与 ffmpeg
  s8le/s16le/s32le 参考一致；这些夹具再用 `aiff_muxer` 重写，
  本地回读与参考一致。
- 外部：端口写出的 s16/s24/s32 AIFF 由 ffmpeg 识别为
  pcm_s16be/pcm_s24be/pcm_s32be、8000 Hz、2 声道，解码 PCM MD5
  与参考相同（`c35b7be8…`、`322c025b…`）。
- 损坏的 FORM/RIFF 头被拒绝。

全量 wasm **1143/1143**。

## 160. CAF（Core Audio Format）ALAC/LPCM 读写

新增 `caf.mbt`（解复用）与 `caf_mux.mbt`（写入）：
- 读取 `caff` 的 `desc`/`kuki`/`pakt`/`data` 块；ALAC 支持旧式
  `frma`+`alac` cookie 与 24 字节新式 cookie（补出 36 字节 atom）；
  `pakt` 中包大小用 MPEG-4 descriptor varint，末包时长按
  validFrames 截断；LPCM 支持 CAF 标志位（bit1 = 小端，与 MOV
  相反）与 8/16/24/32-bit。
- 写入 ALAC：`frma`+`alac` kuki、`pakt`（帧数/priming/remainder 与
  包大小）、`data`；写入 LPCM：可选大小端（flag 2 = 小端）。
- 修正点：CAF 的 LPCM 标志 bit1 表示小端（`(flags ^ 2)` 后才对应
  MOV 的 big-endian 位），最初按大端解析导致 PCM 不匹配。

验证：
- ffmpeg 9.0.2 生成的 5 个 CAF 夹具（ALAC 单/双声道、LPCM
  s16le/s16be/s24be）读取后解码/转换 PCM 与参考 MD5 一致
  （`38fb3080…`、`509cc1b0…`、`c35b7be8…`、`998ae6d7…`、
  `322c025b…`）；用端口的 ALAC/LPCM 写入器重封装后回读仍一致。
- 外部：端口写出的 ALAC、s16le、s24be CAF 由 ffmpeg 识别并解码，
  PCM MD5 与同源参考一致。

全量 wasm **1145/1145**。

## 161. Matroska ALAC 读写

- `mkv_alac_mux.mbt` 新增 `MkvAlacMuxer`：`A_ALAC` 轨、CodecPrivate
  只写 cookie 的 24 字节 ALACSpecificConfig（与 matroskaenc 一致），
  音频设置写采样率/声道/位深，复用 `mkv_audio_document`。
- `matroska_track.mbt` 在打开文件后对 `A_ALAC` 的 CodecPrivate 补出
  12 字节 atom 头（size/tag/version），复刻 matroskadec 的兼容逻辑。
- 回归 `mkv_alac_wbtest.mbt`：ffmpeg 生成的 `alac.mka` 读取解码
  PCM MD5 `38fb3080…` 与参考一致；用 `MkvAlacMuxer` 重封装后回读
  CodecPrivate（36 字节）与包字节透明，重解码仍一致。
- 外部：端口写出的 mka 经 ffmpeg 9.0.2 解码为 s16le，MD5 与参考
  相同。

全量 wasm **1146/1146**。

## 162. WavPack 无损解码

新增 `libavcodec/wavpack.mbt`，移植 `wavpack.c` / `wavpackdata.c` /
`wavpack.h` 的无损整数路径：
- 小端熵位流（`BITSTREAM_READER_LE`）、`get_unary_0_33`/`get_tail`、
  三阶自适应中值（GET/DEC/INC_MED）、zero/one/zeroes 游程状态。
- 元数据块：DECTERMS、DECWEIGHTS、DECSAMPLES（`wp_exp2`）、ENTROPY、
  INT32INFO、DATA、EXTRABITS、SAMPLE_RATE；16-bit 输出使用 32 位
  模算术反相关权重路径，24/32-bit 使用 64 位乘积路径；joint stereo
  与 `wv_get_value_integer`（extra bits、and/or/shift、post_shift）。
- 输出：16-bit S16；24-bit 左移 8 位写入 S32（`post_shift`）；
  32-bit 原样。整文件按 `wvpk` 块拼接解码，校验块数与总采样数。

验证：
- ffmpeg 9.0.2 编码的 8 kHz 夹具（16-bit 单/双声道、24-bit 源被
  编码器提升为 32-bit 的 32-bit 流）与 44.1 kHz 32-bit 流，解码
  PCM MD5 与 ffmpeg s16le/s32le 参考一致（`a7e2b296…`、
  `793c16db…`、`322c025b…`、`4f632c4c…`）。
- 构造测试拒绝 hybrid/float/DSD、多块（>2 声道）、8-bit、无元数据
  与截断文件。

范围：无损整数 16/24/32-bit、1-2 声道、单块包；CRC 不校验。
hybrid（有损）、float、DSD 与多声道多块流明确报错。

全量 wasm **1148/1148**。

## 163. G.711 / G.722 / G.726 电话编解码解码

新增三个小解码器：
- `g711.mbt`：A-law 与 µ-law 到 s16 的转换（`alaw2linear`/
  `ulaw2linear` 公式），WAV 8-bit A/µ-law（tag 0x0006/0x0007）按字节
  交织解码。
- `g722.mbt`：移植 ITU-T G.722 子带 ADPCM 解码（g722dec.c/g722.c/
  g722dsp.c）：64 kbit/s 两个高带比特 + 6 个低带比特、`s_zero`/
  自适应二阶极点预测、对数量化因子、12 抽头 QMF、跨包状态。
  解码端量化使用 64 项 `low_inv_quant6`，而预测更新使用 16 项
  `low_inv_quant4`（两者不可混用）。
- `g726.mbt`：移植 g726.c 自适应量化/六阶预测/11-bit 浮点乘法，
  支持 2/3/4/5 bit 码字（16/24/32/40 kbit/s）与跨包状态；码字位数
  取自 WAV fmt 的 bits_per_sample。
- `wav.mbt` 增加 tag 0x028F → `adpcm_g722` 映射（ffmpeg 写出的
  G.722 WAV 使用该 tag）。

验证：ffmpeg 9.0.2 编码的 8 kHz/16 kHz 夹具（A-law/µ-law 单/双
声道、G.722 16 kHz 单声道、G.726 16/24/32/40 kbit/s）经 WAV 块
对齐解复用后解码，PCM MD5 与 ffmpeg s16le 参考一致
（`defddc47…`、`cc1562a6…`、`4d9e1303…`、`ce3a9326…`、
`fa866b54…`、`3c4dd4ef…`、`859fd29a…`、`a0536288…`、`75802c10…`）。

排障要点：`av_clip_intp2(v, p)` 的有效范围是 `[-2^p, 2^p-1]`
（不是 p 位有符号的 `[-2^(p-1), 2^(p-1)-1]`）；G.722 的 rlow/
rhigh 依赖这一点，最初实现导致样本 20 起出现 ±1 漂移。

全量 wasm **1150/1150**。

## 164. aptX / aptX HD 解码

新增 `libavcodec/aptx.mbt` 与自动提取的 `aptx_tables.mbt`（量化区间/
反量化抖动因子/factor_select 偏移 × 经典与 HD × 4 子带，QMF 内外
系数，`quantization_factors`）：
- 每块经典 4 字节（2 声道 × 16-bit 码字）或 HD 6 字节（2 × 24-bit），
  码字按大端读取后拆成 7/4/2/3（HD 9/6/4/5）位子带量化样本，并按
  抖动奇偶校验修正最低位。
- 两级 QMF 合成树（16 抽头内/外多相 FIR，`rshift64_clip24`）、
  `invert_quantization`（dither + factor_select/quantization_factor
  自适应）、二阶符号权重与 24/12/6 阶差值预测；输出 S32（`sample*256`）。
- 修正点：预测器 `(MUL64+MUL64) >> 22` 必须先做 64 位右移再截断到
  int32；码字拆包后须先写入新的 qs3 再计算抖动奇偶（奇偶本身使用
  新 qs3）。

验证：ffmpeg 9.0.2 编码的 aptX 与 aptX HD 原始流（由确定性立体声源
编码），解码 PCM MD5 与 ffmpeg s32le 参考一致（`94f1435c…`、
`a436a744…`）；构造测试覆盖不足一块的尾部字节被忽略。

全量 wasm **1152/1152**。

## 165. IMA QuickTime（MOV `ima4`）解码

- `adpcm.mbt` 新增 `AdpcmImaQtDecoder`：每声道 34 字节块（大端 16-bit
  头：9-bit 预测值 + 7-bit step index，随后 64 个 4-bit nibble），
  复用 IMA QT nibble 展开；头中 step 相同且预测值变化 ≤0x7F 时保留
  旧预测状态（与 adpcm.c 的 IMA_QT 分支一致），状态跨块保持。
- `codec_tags.mbt` 增加 `ima4` → `adpcm_ima_qt` 映射；MOV 解复用器
  已有 ima4 的 64 样本/34×声道字节默认打包，无需其它改动。
- 回归 `ima_qt_realdata_wbtest.mbt`：ffmpeg 9.0.2 编码的 8 kHz
  IMA QT 单/双声道 MOV，经 `av_mov_open` 逐块解码，PCM MD5 与 ffmpeg
  s16le 参考一致（`609afae1…`、`2db50b2d…`）。

全量 wasm **1153/1153**。

## 166. Sun AU 读写与 G.726LE 解码

- 新增 `au.mbt`（解复用）与 `au_mux.mbt`（写入）：`.snd` 24 字节
  头（header size/data size/codec tag/采样率/声道，未知大小 0xFFFFFFFF
  按剩余长度处理、跳过 annotation），映射 µ-law(1)、s8(2)、
  s16be(3)、s24be(4)、s32be(5)、f32be(6)、f64be(7)、G.726LE
  (23/25/26/'7262' 分别 4/3/5/2-bit) 与 A-law(27)；写入 8/16/24/32-bit
  大端 PCM。
- `g726.mbt` 新增 `little_endian` 选项：AU/`g726le` 使用
  `get_bits_le` 的 LSB-first 码字组装（WAV 仍为 MSB-first）。
- 回归 `au_realdata_wbtest.mbt`：ffmpeg 生成的 s16/s32/µ-law/A-law/
  G.726LE AU 夹具解码 PCM MD5 与参考一致（`998ae6d7…`、`b7fc09c5…`、
  `ce3a9326…`、`4d9e1303…`、`a0536288…`）；PCM 写回后回读字节与
  原数据一致。
- 外部：端口写出的 s16/s32 AU 经 ffmpeg 9.0.2 解码，MD5 与参考一致。

全量 wasm **1155/1155**。

## 167. G.711 编码与 AU µ-law/A-law 写入

- `g711.mbt` 增加 `pcm_alaw_encode`/`pcm_mulaw_encode`：按
  `build_xlaw_table` 构造 16384 项 `linear_to_xlaw` 表（mask 0xd5 /
  0xff，`(sample + 32768) >> 2` 索引），编码结果与 ffmpeg 逐字节一致。
- `au_mux.mbt` 的 `au_muxer_new` 增加 `codec~` 选项（`pcm` /
  `pcm_alaw` / `pcm_mulaw`），G.711 编码后写 tag 27/1；解码端已有。
- 回归：用 AU s16 夹具的样本重编码 µ-law/A-law，与 ffmpeg 生成的 AU
  夹具**数据区逐字节相同**，端口回读解码 MD5 与参考一致。
- 外部：端口写出的 µ-law/A-law AU 经 ffmpeg 9.0.2 解码，MD5 与
  ffmpeg 自身编码后解码一致（`ce3a9326…`、`4d9e1303…`）。

全量 wasm **1156/1156**。

## 168. DASH SegmentBase 单文件（indexRange/sidx）读取

- `dash.mbt` 在 `SegmentBase` 下解析 `<Initialization>` 的
  `range`/`sourceURL`：无 sourceURL 时初始化段就是同一 BaseURL 的
  字节范围。
- `segment_io.mbt` 的 `DashSegmentReader` 支持 `indexRange` 播放：
  按范围抓取 `sidx`（支持 version 0/1、first_offset 与引用列表），
  计算 `media_start = index_end + 1 + first_offset`，随后按每个
  reference 的 size 逐个抓取子分片；初始化段仍只抓取一次。
- 新增 `sidx` 解析（box size、version、reference_count、第 31 位
  引用类型与 31 位 size）。
- 回归 `dash_segment_base_realdata_wbtest.mbt`：ffmpeg
  `-global_sidx 1 -single_file 1` 生成的 64x48 H.264 MP4（init 0-816、
  global sidx 817-880、两个媒体范围），MPD 改写成 SegmentBase；
  内部解析 MPD、按 sidx 抓取两段并解码 10 帧，MD5 与 ffmpeg
  framemd5 一致。
- 外部：同一 SegmentBase MPD 由 ffmpeg 9.0.2 播放，framemd5 前 10
  帧与整文件解码参考一致（本地 DASH CLI 双读的已知重复行为除外）。

全量 wasm **1157/1157**。

## 169. 双轨 H.264+AAC fMP4 写入

- `fmp4_mux.mbt` 抽出 `fmp4_ftyp`/`fmp4_mvhd`/`fmp4_trak`/`fmp4_trex`、
  `fmp4_styp`/`fmp4_mfhd`/`fmp4_traf`/`fmp4_payload` 复用初始化段与媒体段
  构件；单轨输出字节不变。
- 新增 `fmp4_av_init_segment`：同一 `moov` 内 video（`avc1`/`avcC`，视频
  timescale）与 audio（`mp4a`/`esds`，timescale=采样率）两个 `trak`，
  `mvex` 带两个 `trex`；track id 可自定义且必须互异。
- 新增 `fmp4_av_media_segment`：一个 `moof` 内两个 `traf`（各自
  `tfhd`/`tfdt`/`trun`），`mdat` 先放视频样本再放音频样本；两个
  `trun` 的 data_offset 按 `moof` 长度 +8、再累加视频负载计算。
- 回归 `fmp4_av_realdata_wbtest.mbt`：真实 64x48 x264 5 帧与
  `mkv_av_b64` 的 45 个 AAC-LC 44.1 kHz 单声道包，按 3+2 视频帧、
  23+22 音频包写成两个媒体段（第二段 `tfdt`=27000/23552）；
  `av_mov_open` 回读两轨样本数/DTS/时长/负载逐一匹配，视频帧 MD5 等于
  ffmpeg 参考（`67209b5b…` 等 5 帧），AAC 45 帧解码共 46080 样本；
  另测自定义 track id（3/7）与非法参数拒绝。
- 外部：init+两段拼接为 `av.mp4`，ffmpeg 9.0.2 识别为 10 fps H.264
  + AAC 1.04 s；视频 framemd5 与参考逐帧一致，音频 s16le PCM MD5
  `cfe6223331361877aef4f7e88a2a50bc` 与同 AU 生成的 ADTS 解码完全一致。

全量 wasm **1160/1160**。

## 170. AC-3 解码（含裸流切分）

- 新增 `ac3_tables.mbt`（由 `scripts/_gen_ac3_tables.py` 从 FFmpeg
  9.0.2 `ac3.c`/`ac3tab.c`/`ac3dec_data.c` 提取：帧长/声道/采样率/
  码率表、临界带与 logadd、听觉阈值、bap/量化表、mantissa 分组表等；
  `ff_ac3_floor_tab` 按 `int16_t` 语义把 0xf800 转成 -2048）。
- 新增 `ac3.mbt`：`ac3_parse_header`（同步/BSI，bsid≤10；E-AC-3 明确
  报错）与 `Ac3Decoder`。覆盖双单声道、单声道、立体声（rematrixing）、
  3F/2F1R/3F1R/2F2R/3F2R+LFE；耦合策略/坐标/相位标志、delta bit
  allocation、零尾数抖动（MD5 种子的 LFG，与 C 的 `av_lfg_get` 一致）、
  `dynrng` 动态范围（`drc_scale` 固定 1.0，同 ffmpeg 默认）、
  比特分配三阶段（PSD/掩蔽曲线/bap）、Q24 尾数解码、KBD α=5 窗与
  256/128 点 IMDCT（预计算余弦矩阵直接求值，等价
  `ff_tx_mdct_naive_inv`）；输出按 SMPTE 声道顺序（`channel_map` 求逆）。
- 新增 `av_ac3_open`（rawaudio.mbt）：按每帧头部 `frame_size` 切分，
  覆盖 44.1 kHz 交替帧长；返回码率/采样率/声道/1536 样本帧。
- 回归 `ac3_realdata_test.mbt`：ffmpeg 9.0.2 编码的 48 kHz 单声道/
  立体声（含瞬态与 rematrixing）/5.1/强制耦合立体声、44.1 kHz 与
  32 kHz 单声道共 6 组夹具，s16 量化输出与 ffmpeg 参考最大差 1 LSB
  （单声道逐字节一致），另测同步搜索偏移、字节交换同步拒绝与
  E-AC-3 拒绝。
- 回归 `ac3_realdata_wbtest.mbt`：裸流切分的帧数/偏移/时长、
  44.1 kHz 变长帧（416/418/418/418）与 5.1 帧长；单声道解码
  s16 MD5 `0b4d53cb6c20a3642b1b06b01546afae` 与 ffmpeg 一致。
- 外部：全部参考 PCM 由 ffmpeg 9.0.2 `-f s16le` 解码生成；5.1 声道
  映射修正前 LFE/环绕声道错位，按 C 的 `outptr[channel_map[ch]]`
  求逆后各声道一致。

全量 wasm **1164/1164**。

## 171. AMR-NB 解码与裸流切分

- 新增 `amr_tables.mbt`（`scripts/_gen_amr_tables.py` 从 FFmpeg 9.0.2
  `amrnbdata.h`/`acelp_vectors.c` 提取：8 种模式的 RFC 4867 位重排表、
  LSF 码本（3/5 分裂）、增益码本、IR 滤波、b60 sinc、pow 表、高通系数；
  重排表中的 `AMR_BIT`/`AMR_OF` 宏先展开成字节偏移再取数）。
- 新增 `amr.mbt`：移植 `amrnbdec.c` 浮点路径及 `acelp_filters.c`/
  `acelp_vectors.c`/`acelp_pitch_delay.c`/`celp_filters.c`/`lsp.c`
  所需函数——位重排、LSF→LSP→LPC（12.2k 5 分裂与其余模式 3 分裂、
  最小间距约束）、1/3 与 1/6 基音滞后、基音矢量 b60 插值、10 位/8 位/
  4 位代数码本、固定增益平滑、抗稀疏 IR 滤波、LPC 合成、倾斜/AGC
  后滤波与二阶高通；输出 8 kHz 单声道 160 样本。DTX/no-data 明确报错。
- 新增 `av_amr_open`（rawaudio.mbt）：识别 `#!AMR\n`（AMR-WB 报错），
  按 TOC 帧类型切分（4.75k 13 字节 … 12.2k 32 字节）。
- 回归 `amr_realdata_test.mbt`：libopencore_amrnb 编码的 8 种模式
  （`-b:a 4750…12200`）各 21 帧，s16 输出与 ffmpeg 参考最大差 1 LSB；
  另测 DTX、no-data 与保留模式拒绝。
- 回归 `amr_realdata_wbtest.mbt`：`#!AMR` 头/TOC 切分的帧数、偏移、
  PTS 与 13/32 字节帧长，4.75k 解码逐样本对比；AMR-WB 头拒绝。
- 外部：全部参考 PCM 由 ffmpeg 9.0.2 `-f s16le` 解码生成。FFmpeg 自身
  文档说明该浮点解码器与 3GPP 定点参考不逐位一致，故按 1 LSB 容差验证。

全量 wasm **1168/1168**。

## 172. E-AC-3 基线解码

- `ac3_parse_header` 支持 bsid 11–16：解析 stream type/substream、11 位
  帧长、2/3/6 块码、缩减采样率拒绝、音量控制、依赖流声道映射、mixing/
  informational metadata、转换器同步位与附加比特流信息。
- `ac3.mbt` 新增 `parse_eac3_frame_header`：帧头语法标志（AC-3 式/LUT
  指数策略、block switch/dither/比特分配/fast gain/DBA/skip 语法开关）、
  每块耦合策略标志、AHT 检测（有 AHT 即报错）、每帧 SNR 偏移、瞬态
  预处理/SPX 衰减码跳过与 block-start 信息；`decode_audio_block` 按
  E-AC-3 分支读取耦合/rematrix/指数/快速增益/转换器 SNR 位，并维护
  跨块耦合帯结构；SPX、增强耦合、AHT、依赖帧与缩减采样率均明确报错。
- `av_ac3_open` 按 bsid 返回 `ac3`/`eac3`，支持 2/3/6 块帧长与 44.1 kHz
  变长 AC-3 帧。
- 回归 `ac3_eac3_realdata_test.mbt`：ffmpeg 9.0.2 原生 eac3 编码的
  48 kHz 单/立体声/5.1、44.1/32 kHz 单声道及 1.5M/3M 高码率 3 块/2 块
  夹具，s16 输出与 ffmpeg（`-f eac3` 强制解复用）参考最大差 1 LSB。
- 回归 `eac3_realdata_wbtest.mbt`：码率/帧长/块数切分（384、4096 字节
  帧；512/1536 样本）与单声道 PCM 对拍。
- 已知限制：FFmpeg 9.0.2 的 eac3 编码器不产出 SPX/AHT/增强耦合，故这些
  路径未实现并以错误返回；依赖帧（多子流）暂不支持。

全量 wasm **1171/1171**。

## 173. AC-3/E-AC-3 写入 MP4、MPEG-TS 与 Matroska

- `mp4_mux.mbt` 抽出通用 `mp4_audio_muxer_new(entry, ...)`（原 AAC 写入器
  改为构造 `mp4_aac_entry` 后复用），新增 `mp4_ac3_entry`
  （`ac-3` + `dac3`：fscod/bsid/bsmod/acmod/lfeon/bit_rate_code）与
  `mp4_eac3_entry`（`ec-3` + `dec3`：data_rate/单独立子流/无依赖子流）；
  `mp4_ac3_muxer_new`/`mp4_eac3_muxer_new` 由首帧头自动填参数。
  MOV 读取端已有 `ac-3`/`ec-3` → `ac3`/`eac3` 映射，无需改动。
- `mkv_audio_mux.mbt` 新增 `MkvAc3Muxer`（`mkv_ac3_muxer_new`/
  `mkv_eac3_muxer_new`，CodecID `A_AC3`/`A_EAC3`，无 CodecPrivate；
  `mkv_audio_document` 现在仅在 CodecPrivate 非空时写该元素）。
- MPEG-TS 写入器本就支持任意 audio stream type；新增 0x81/0x87 用例
  （解复用端映射已存在）。
- 回归 `ac3_containers_realdata_wbtest.mbt`：同一组真实 AC-3 单声道与
  E-AC-3 立体声帧分别写 MP4/TS/MKV，回读校验 codec/采样率/时长/负载；
  外部 ffmpeg 9.0.2 解码六种输出与裸流参考 PCM 的 MD5 完全一致
  （`0b4d53cb…`、`596feb82…`）。

全量 wasm **1174/1174**。

## 174. AC-3/E-AC-3 fMP4 分片

- `fmp4_mux.mbt` 新增 `fmp4_ac3_init_segment`/`fmp4_eac3_init_segment`：
  由首帧头生成 `ac-3`+`dac3` / `ec-3`+`dec3` 初始化段，媒体 timescale
  取采样率；媒体段沿用通用 `fmp4_media_segment`（整帧 1536 样本）。
- 回归 `fmp4_ac3_realdata_wbtest.mbt`：真实 AC-3 单声道与 E-AC-3 立体声
  各写 init + 两段媒体段（第二段 `tfdt`=3072），`av_mov_open` 回读
  codec/采样率/时长/负载一致；外部 ffmpeg 9.0.2 解码两个分片序列与
  裸流参考 PCM MD5 完全相同。

全量 wasm **1175/1175**。

## 175. WMA v1/v2 解码

- 新增 `wma_tables.mbt`（`scripts/_gen_wma_tables.py` 从 `wmadata.h`/
  `wma_freqs.c` 提取：六张系数 VLC 表（huffcodes/huffbits/levels）、
  LSP 码本、hgain 码表、指数带表与临界频率）。
- 新增 `wma.mbt`/`wma_decode.mbt`：移植 `wma.c` 的 `ff_wma_init`
  （frame_len/噪声编码判定/指数带与高频带划分/噪声表/系数表选择/LSP
  pow 表）与 `wmadec.c`（指数 VLC/LSP 解码、run-level 系数、噪声编码、
  MS stereo、正弦窗与全 IMDCT、比特储备/可变块语法、EOF 冲刷帧）。
  C 的 float FFT MDCT 改为按行余弦递推的直接全 IMDCT（等价
  `ff_tx_mdct_inv_full`，含 1/32768 缩放）。
- 回归 `wma_realdata_test.mbt`：ffmpeg 9.0.2 wmav1/wmav2 编码后以
  `-c copy -f data` 提取的包（44.1 kHz 单声道 96/32 kb/s、立体声
  128 kb/s、32 kHz 单声道 64 kb/s、WMA v1），按 API 语义跳过 2 帧
  解码延迟并附 EOF 重叠帧，s16 输出与 ffmpeg 参考最大差 1 LSB；
  另测非法版本/声道/block_align 拒绝。
- 已知限制：ASF 解复用尚未实现（夹具以包级 API 提供），比特储备/
  可变块路径已移植但无编码器可产出真实夹具。

全量 wasm **1177/1177**。

## 176. ASF/WMA 解复用

- 新增 `asf.mbt`：解析 ASF Header Object 的 File Properties（packet size、
  preroll）与音频 Stream Properties（WAVEFORMATEX：0x160 WMAv1 /
  0x161 WMAv2、采样率、声道、avg bytes、block_align、CodecPrivate），
  并在 Header Object 之后定位 Data Object；按
  `asf_get_packet`/`asf_read_frame_header`/`asf_parse_packet` 解析包级
  ECC、PPI/填充/时间戳、每负载的 stream/property 字段、replicated data
  （媒体对象大小与时间戳）及分片，拼装为每帧一个 `AsfPacket`；
  另支持 bit-reservoir 的多子负载包。
- 回归 `asf_realdata_wbtest.mbt`：ffmpeg wmav2 生成的 44.1 kHz 单/立体声
  ASF，校验 packet size 3200、preroll 3100、流参数、6 个 557 字节负载及
  PTS（0/46/92/139/185/232 ms）；`wma_decoder_new` + 包级解码（跳过
  2 帧延迟 + EOF 冲刷）与 ffmpeg 参考 PCM 最大差 1 LSB。
- 已知限制：仅音频流；视频/扩展流属性/DRM/负载扩展系统跳过。

全量 wasm **1180/1180**。

## 177. AC-3/E-AC-3 双轨 A/V 容器

- MP4：`mp4_av_muxer_new` 增加 `audio_entry`（自定义 sample entry）与
  `aac_priming`；新增 `mp4_av_ac3_muxer_new`/`mp4_av_eac3_muxer_new`，
  由首帧 AC-3/E-AC-3 头生成 `ac-3`+`dac3` / `ec-3`+`dec3` 条目，
  音轨 timescale 取采样率。
- Matroska：`MkvAvMuxer` 的音轨改为可配置 `audio_codec`/CodecPrivate；
  新增 `mkv_av_ac3_muxer_new`/`mkv_av_eac3_muxer_new`（A_AC3/A_EAC3）。
- MPEG-TS 修复：PAT/PMT 的 CRC 需按 `mpegtsenc.c` 写
  `av_bswap32(av_crc(...))`，此前直接写大端值导致 FFmpeg 丢弃整张表并
  退回按 PES 负载探测（H.264 被误判为 mp3）。回归测试锁定 PAT/PMT 的
  CRC 字节；视频 PES 仍要求 Annex B，用例把 AVCC 样本转换为 Annex B
  并在首帧前置 SPS/PPS。
- 回归 `ac3_av_containers_realdata_test.mbt`：真实 5 帧 H.264 与 4 帧
  AC-3/E-AC-3 分别写 MP4、TS（0x81/0x87）与 Matroska，本地解复用
  codec/负载/帧 MD5 一致；外部 ffmpeg 9.0.2 解码六种输出，视频
  framemd5 五项与 `67209b5b…`/`4c68e9b9…`/`bb1a50b7…`/`0b3917a8…`/
  `5f67aea8…` 完全相同，AC-3 音频 PCM MD5 为 `0b4d53cb…`、E-AC-3 为
  `4f289b44…`，均与裸流参考一致。

全量 wasm **1186/1186**。

## 178. Vorbis I 解码与 Ogg 解复用

- 新增 `libavcodec/vorbis_tables.mbt`（`scripts/_gen_vorbis_tables.py`
  从 `vorbis_data.c` 提取 8 组窗口斜率与 floor1 逆 dB 表）与
  `libavcodec/vorbis.mbt`：LSB-first 位读器、`ff_vorbis_len2vlc`
  规范 Huffman、码本（VQ lookup type 1、稀疏/有序长度码）、floor 0/1
  （`ff_vorbis_floor1_render_list` 整数光栅化）、residue 0/1/2
  （含 libvorbis 尾包兼容）、平方极坐标耦合、submap/mux、全部模式与
  重叠相加；输出按 FFmpeg 的 Vorbis→native 声道偏移表重排。
- IMDCT：C 侧用 `AV_TX_FLOAT_MDCT` 半逆变换（scale=-1）；本移植实测
  其基函数为 `out[k]=Σ c[j]·(-1)^(j+1)·sin(π/(4n)(2k+1)(2j+1))`
  （用 gcc 探针逐点核对 L=8/16/32），以正弦递推 O(N²) 求值。
  `vector_fmul_window` 的索引按 C 语义使用 `win[k]/win[2len-1-k]` 与
  `dst[k]/dst[2len-1-k]`（与 AAC 的窗口布局不同）。
- 新增 `libavformat/ogg_vorbis.mbt`：Ogg 页/CRC/lacing 解析、三头校验、
  音频包与 granule 收集；`decode_stream` 跳过首帧（对齐解码器
  `skip_samples`），`decode_stream_trimmed` 再按 granule 裁尾
  （等价 oggparsevorbis 的 `end_trimming`；granule 短于末块的流按
  FFmpeg 行为保留整帧，测试用未裁剪路径）。
- 回归 `vorbis_realdata_test.mbt`：FFmpeg 9.0.2 libvorbis 编码的
  mono/stereo（44.1 kHz，耦合+residue 2）、5.1（48 kHz，2 submap、
  4 耦合步、residue 2/2/1）与 pink 噪声（短块切换）四组真实流，
  解复用参数、包数、granule 校验后 s16 输出与 FFmpeg 参考最大差
  1 LSB 且长度一致；另测 CRC 损坏与错误容器被拒绝。
- 已知限制：floor 0 与码本 lookup type 2 未由 libvorbis 产出，floor 0
  仅按 C 实现、无真实夹具；解码为 O(N²) IMDCT（测试规模可接受）。

全量 wasm **1191/1191**。

## 179. 清单内编解码 x 容器交叉补齐

本轮核对：用户列出的解码器（H.264/HEVC/AV1/VP9/MJPEG/PNG/AAC/MP3/Opus）与容器
（MP4/MKV/TS/HLS/DASH/WebM/WAV）此前均已落地；本轮实际做交叉矩阵检查
后补齐 4 个集成缺口：

- MOV 静像：`mov_codec_name` 增加 `jpeg`/`mjpa`/`mjpg`→`mjpeg`、
  `mjpb`→`mjpegb`、`png `→`png`，MOV 内的 MJPEG/PNG 样本可直接交给
  现有解码器。
- Matroska 静像：`V_MS/VFW/FOURCC` 轨按 FFmpeg `matroskadec.c` 读取
  BITMAPINFOHEADER 的 `biCompression`（偏移 16），`MPNG`/`PNG1`/`png `
  →`png`，`MJPG`/`jpeg`/`mjpg`→`mjpeg`；FFmpeg 自身 Matroska muxer
  写 PNG 用的正是 VFW+MPNG（它没有 `V_PNG` 标签）。
- 新增 `mkv_mp3_muxer_new`（`A_MPEG/L3`，无 CodecPrivate）与
  `mkv_still_mux.mbt`：`mkv_mjpeg_muxer_new`（`V_MJPEG`）、
  `mkv_png_muxer_new`（`V_MS/VFW/FOURCC` + 40 字节 BITMAPINFOHEADER
  `MPNG`/24bpp），单 Cluster SimpleBlocks。
- WAV：实测 0x0055 MP3 与 0x00FF/0x1610 AAC 载荷均可解复用；MP3 用
  `av_mp3_open` 切帧后逐帧解码；AAC 原始块按 `block_align` 分块而非
  按帧，只有首块落在帧边界，剩余块需要 AAC parser（未移植，已在测试
  注释与 README 标注）。

新回归 `container_codec_matrix_realdata_test.mbt`（FFmpeg 9.0.2 夹具）：
MOV/MKV 的 MJPEG/PNG 帧 MD5 与 `ffmpeg -f framemd5` 完全一致
（`165c0e08…`/`9cc1f45c…`、`84cf7e4f…`/`87a8e6c7…`）；MKV MJPEG/PNG
与 MP3 写入器回读一致，外部 ffmpeg 解码我们写出的三个 MKV：静像
framemd5 与源帧一致，MP3 PCM 与 ffmpeg 重封装参考一致
（`5910ef2b…`）。全量 wasm **1196/1196**。

## 180. MP4/MOV MJPEG/PNG 静像写入

- `mp4_video_mux.mbt` 将 `finish` 的文档构建抽成 `mp4_video_document`
  （H.264 写入器保留薄包装，行为不变），`mp4_visual_entry` 支持无解码
  配置子盒的样本条目（`config_tag == ""` 时省略）。
- 新增 `mp4_still_mux.mbt`：`mp4_mjpeg_muxer_new`（`jpeg` 样本条目）与
  `mp4_png_muxer_new`（`png ` 样本条目），单视频轨、单 chunk、stts/
  stsc/stsz/stco/stss，与 MOV 解复用器和 FFmpeg 的 MOV muxer 四字码一致。
- 回归 `container_codec_matrix_realdata_test.mbt` 新增 MP4/MOV 静像写入
  用例：回读样本逐字节一致、帧 MD5 与 `ffmpeg -f framemd5` 相同；外部
  ffmpeg 9.0.2 可直接解码我们写出的 MP4，codec_tag 分别为 `jpeg`/`png `，
  两帧 framemd5 与源一致（`165c0e08…`/`9cc1f45c…`、`84cf7e4f…`/`87a8e6c7…`）。

全量 wasm **1197/1197**。

## 181. MJPEG 无损 JPEG（SOF3）解码

- `mjpeg.mbt` 接受 SOF3，8-bit 平面路径；解析 SOS 的 Ss（预测器 0..7）、
  Al（point transform），按 `ljpeg_decode_yuv_scan` 逐样本解码：
  MCU 尺寸 `ceil(width/h_max) × ceil(height/v_max)`，每分量
  `h_count*v_count` 个样本，左/上/左上邻域 + PREDICT，重启间隔按
  `ff_mjpeg_should_restart` 语义复位 resync 坐标。
- 新增 `mjpeg_finalize_lossless`：按分量采样平面裁出 yuvj420p/422p/
  440p/444p/gray（1 分量），跳过 IDCT；1x1 三分量“无损 RGB”和
  8-bit 以上精度明确 PATCHWELCOME。
- 回归 `mjpeg_lossless_realdata_test.mbt`：FFmpeg 9.0.2 `ljpeg` 夹具
  （yuvj420p 预测器 1/2、yuvj422p、yuvj444p），解码平面与
  `ffmpeg -f rawvideo` 字节一致（MD5 `ef117420…`／`7e9feadf…`／
  `4cbccb31…`）；9-bit 与 1x1 三分量变体被拒绝。原白盒用例中的
  SOF3“不支持”断言改为 SOF5，并补 SOF3 无 scan 用例。
- 全量 wasm **1203/1203**。

## 182. HLS/DASH × HEVC/AV1/VP9 fMP4 验证

- HLS 媒体播放列表（`EXT-X-MAP`）与 DASH SegmentTemplate MPD 本身与编码
  格式无关；本轮用现有 `fmp4_hevc/av1/vp9_init_segment` + 媒体段构造
  清单，经 `hls_segment_reader_new` / `dash_segment_reader_new` 取回
  初始化段与分片，`av_mov_open` 解复用后分别用 HEVC、AV1、VP9 解码器
  与既有帧 MD5 对拍。
- 新增 `hls_dash_video_codecs_realdata_wbtest.mbt`（3 个用例，复用
  fMP4 真实样本与参考 MD5）。外部 ffmpeg 可播放同构 HLS/DASH 清单
  （此前 H.264/AAC 已做过文件级播放验证）。
- 全量 wasm **1203/1203**。

## 183. 原始 AAC 帧切分（WAV `WAVE_FORMAT_AAC`）

- `AacDecoder::decode_frame_with_consumed` 返回本帧消费的字节数，规则与
  FFmpeg `aac_decode_frame` 一致（`(bits+7)>>3`，尾部全零时吞到包尾）。
- 新增 `aac_raw.mbt` 的 `aac_decode_raw_stream(asc, data)`：按消费字节
  循环解码拼接的 raw_data_block，返回每帧数据与 PCM，解决 WAV AAC 数据块
  按 `block_align` 切分、不落在帧边界的问题。
- 回归 `aac_raw_realdata_test.mbt`：0.2 s WAV AAC 夹具切出 10 帧 ×1024
  样本，PCM 与 ffmpeg 9.0.2 解码最大差 1 LSB；`container_codec_matrix`
  的 WAV AAC 用例改为整段切帧解码（10 帧）。0.1 s 夹具被确认是 ffmpeg
  WAV AAC muxer 的截断产物（解码报 “Input buffer exhausted”），未使用。
- 全量 wasm **1204/1204**。

## 184. FLAC 编码器

- 新增 `flac_encode.mbt`：固定预测 0..4 阶逐声道选优、Rice/Rice2
  残差（分区阶 0，参数穷举最小化位数）、常量/verbatim 回退；
  帧头（固定块策略、UTF-8 帧号、显式块长、CRC-8）与帧尾 CRC-16，
  STREAMINFO（块长/帧长/采样率/声道/位深/总样本/MD5）。
- API：`flac_encode(samples, channels, sample_rate, bits, block_size~)`
  返回 `FlacEncoded { streaminfo, frames, file() }`，支持 8/12/16/20/24-bit、
  1-8 声道（独立声道）。
- 回归 `flac_encode_realdata_test.mbt`：16-bit stereo（8820 样本/3 帧）、
  24-bit mono（7200/2 帧）、8-bit mono（1600/1 帧）三组 PCM WAV 夹具，
  经 `ff_flac_decode` 回转逐样本一致；外部 ffmpeg 9.0.2 解码 PCM 与源
  WAV 逐字节一致（`f3acc310…`、`741aa137…`、`a440ba76…`）。非法参数
  拒绝用例同时覆盖。
- 已知限制：未实现 LPC、分区阶 >0、stereo decorrelation、wasted bits
  与 32-bit 样本；压缩率低于 ffmpeg 的默认编码器，但输出为标准 FLAC。
- 全量 wasm **1208/1208**。

## 185. ALAC 编码器

- 新增 `alac_encode.mbt`：单声道 SCE / 立体声 CPE，预测类型 0（无 LPC）、
  Rice 熵编码（history_mult 40 / initial_history 10 / k_modifier 14 /
  rice_modifier 4）、24-bit 的 extra bits 交织、TYPE_END 与字节对齐；
  36 字节 ALACSpecificConfig cookie（帧长、位深、声道、Rice 参数、
  最大帧长、平均码率、采样率）。
- API：`alac_encode(samples, channels, sample_rate, bits, frame_size~=4096)`
  返回 `AlacEncoded { cookie, frames }`；支持 16/24-bit、1-2 声道。
- 回归 `alac_encode_realdata_test.mbt`：16-bit stereo（8820 样本/3 帧）与
  24-bit mono（7200/2 帧）WAV 夹具；`alac_decoder_new` 回转逐样本一致；
  MP4 ALAC 写入回读帧一致；外部 ffmpeg 9.0.2 解码 MP4 的 PCM 与源 WAV
  逐字节一致（`f3acc310…`、`741aa137…`）。非法参数（3 声道、8/20-bit、
  0 采样率、非交织）拒绝。
- 已知限制：无 LPC、stereo decorrelation、20/32-bit 与 >2 声道；压缩率
  低于 ffmpeg 默认编码器，但输出为标准 ALAC。
- 全量 wasm **1211/1211**。

## 186. FLAC 编码器：立体声去相关与 wasted bits

- `flac_encode` 对双声道逐帧比较 independent / left-side / right-side /
  mid-side 四种模式的子帧估计位数，选择最小者；side 声道位深 +1，
  帧头写 8/9/10 通道分配码（解码器已有对应重建路径）。
- 每个子帧检测公共尾零（`remove_wasted_bits` 语义），写 wasted flag +
  unary，按缩减后的位深编码；Rice 参数由求和公式取近似最优并比较
  邻近 k，避免逐 k 扫描。
- 回归：原有 16-bit stereo / 24-bit mono / 8-bit mono 用例不变；新增
  相关立体声夹具（左右完全相同）断言去相关生效（帧长 < 3000，
  通道码 8..10），输出 1646 字节且 ffmpeg 解码 PCM MD5
  `ef9ca189…` 与源 WAV 一致。
- 全量 wasm **1212/1212**。

## 187. FLAC 编码器：LPC 子帧

- 按 FFmpeg 9.0.2 `lpc.c` 移植 LPC 分析：Welch 窗加权的自相关
  （含 C 实现的 +1.0 种子）、Levinson-Durbin 递推（1..12 阶）、
  15-bit 误差反馈量化（shift 0..15，系数超界时缩放而非负移位）。
- 子帧选择扩展为 constant / verbatim / fixed 0..4 / LPC 1..12 全量
  位数比较（含 4+5+15*order 系数开销）；写 LPC 子帧类型码
  `32|(order-1)`、精度-1、5-bit shift 与 15-bit 系数，残差仍走
  Rice/Rice2 分区阶 0。编码器用 64 位计算预测，与解码器
  `bps+15+log2(order) > 32` 时的 64 位路径一致（16-bit 及以下
  不溢出场景下 32 位路径等价）。
- 回归：新增 AR(2) 共振信号用例，断言首子帧类型为 LPC 且
  16-bit mono / 24-bit stereo（经去相关）回转逐样本一致；原三组
  真实夹具 MD5 不变。
- 外部验证：同一 AR 信号与真实夹具（16-bit stereo、24-bit mono、
  8-bit mono）编码后经 ffmpeg 9.0.2 解码，PCM 与源逐字节一致；
  16-bit stereo 夹具由 7361 字节降到 4903 字节（首帧 LPC 10 阶）。
- 全量 wasm **1214/1214**。

## 188. FLAC 编码器：Rice 分区阶搜索

- 按 FFmpeg 9.0.2 `calc_rice_params` / `get_max_p_order` 实现分区搜索：
  分区阶上限取 `min(8, blocksize 的尾零数, log2(blocksize/order))`，
  逐阶计算「4-bit 分区阶 + 每分区参数 + Rice 码」位数并取最小；
  首分区不编码 warm-up 样本（与解码器 `decode_residuals` 一致）。
- 子帧选优改为两段：各候选预测阶先用单分区成本排序，最便宜者再做
  完整分区搜索，避免 Rice 搜索按阶数倍增；写端按阶输出每分区参数与
  残差。
- 回归：新增非平稳块（前半低幅、后半高幅噪声）用例，断言首子帧
  分区阶 > 0 且回转逐样本一致；相关立体声夹具 1646→1266 字节。
- 外部验证：非平稳夹具与 16-bit stereo / 24-bit mono / 8-bit mono /
  相关立体声夹具经 ffmpeg 9.0.2 解码，PCM 与源逐字节一致
  （非平稳文件为分区阶 1 的真实码流）。
- 全量 wasm **1215/1215**。

## 189. ALAC 编码器：自适应 LPC 与立体声去相关

- 按 FFmpeg 9.0.2 `alacenc.c` 移植：`estimate_stereo_mode` 用二阶残差
  绝对值之和在 left/right、left/side、right/side、mid/side 四种模式中
  选优并写 interlacing shift/leftweight；`calc_predictor_params` 复用
  FLAC 的 Welch 自相关/Levinson-Durbin，按估计阶（反射系数 > 0.1，
  4..6 阶）取 9-bit 系数与 shift 0..9（shift 为 0 时退回无预测，
  解码器拒绝 lpc_quant 0）。
- `alac_linear_predictor` 逐样本自适应 FIR（含解码端一致的系数
  更新与 16-bit 符号扩展），残差写 prediction type 0 + 系数表 +
  Rice 熵编码；24-bit 的 extra bits 在去相关前拆分、头部之后写入。
- 回归：新增 16-bit stereo 断言 LPC 6 阶与 24-bit stereo（相关
  声道，right/side 模式）回转逐样本一致；相关立体声夹具断言
  leftweight != 0。原 16/24-bit 夹具 MD5 不变。
- 外部验证：16-bit stereo、相关立体声、24-bit mono 与构造的
  24-bit stereo 编码成 MP4 后经 ffmpeg 9.0.2 解码，PCM 与源逐字节
  一致（`f3acc310…`、`ef9ca189…`、`3c32cd35…`、`52bc0b75…`）。
- 全量 wasm **1217/1217**。

## 190. MP4 元数据：udta/meta/ilst 读写

- 解复用：`Mp4Movie` 新增 `metadata : Array[(String, String)]`，
  按 `mov_read_udta_string` 的映射解析 `udta/meta/ilst` 的
  `data` 子盒（UTF-8/UTF-16BE，index 表覆盖 title/artist/album/
  album_artist/composer/date/encoder/comment/genre/copyright/
  grouping/lyrics/description/synopsis/show/episode_id/network/
  keywords 等）；`meta` 的 FullBox 版本/标志字按是否紧跟 `hdlr`
  判断。
- 写入：新增 `mp4_metadata_udta`（`mov_write_ilst_tag` 的 iTunes
  映射 + `mdir` handler）与 `mp4_add_metadata(file, tags)`，对已完成
  的 MP4 重建 moov：删除旧 `udta`（等价 ffmpeg `-metadata` 替换语义）
  并追加新 udta；要求 moov 不在 mdat 之前（本仓库 muxer 均如此，
  faststart 在之后调用），chunk 偏移因此保持有效。
- 回归 `mp4_metadata_realdata_test.mbt`：ffmpeg 9.0.2 生成的带标签
  AAC MP4（3261 字节）读回 7 个标签；`mp4_add_metadata` 改写后
  旧标签被替换、AAC 轨样本数不变；无 moov 文件拒绝。
- 外部验证：改写后的文件经 ffprobe 读到 title/artist/copyright/
  genre，音频解码无错误。
- 全量 wasm **1220/1220**。

## 191. Matroska Tags 元数据读写

- 解复用：`MatroskaHeader.metadata` 解析 `Tags/Tag/SimpleTag` 的
  TagName/TagString（两种出现顺序都能配对），按
  `matroska_convert_tags` 只收容器级（`Targets` 无
  TrackUID/ChapterUID/AttachmentUID）的标签，并应用
  `ff_mkv_metadata_conv`（LEAD_PERFORMER→performer、PART_NUMBER→track）。
- 追加字节级 `Tags` 扫描（`matroska_scan_tags`）：流式解析在首个
  Cluster 处停止，簇之后的 `Tags`（合法位置）也能读到；扫描失败时保留
  流式结果。
- 写入：`mkv_tags_element` 按 `matroskaenc` 规则把键名大写、
  空格转下划线；`mkv_add_tags` 把 `Tags` 插入首个 Cluster 之前，并把
  `SeekHead` 的 SeekPosition 与 `Cues` 的 CueClusterPosition 按插入
  位移重写（uint 宽度按需加宽，循环到稳定）。
- 回归 `mkv_tags_realdata_test.mbt`：ffmpeg 9.0.2 生成带
  容器级/轨道级 Tags 的 Opus MKV（2706 字节）读回 6 个容器标签、
  Info Title 与轨道标签过滤；`mkv_add_tags` 追加 3 个标签后标签数
  +3、音视频包数不变、旧标签保留。
- 外部验证：改写后的 MKV 经 ffprobe 读到 TITLE/ARTIST/CUSTOM_KEY
  等全部标签；`ffmpeg -ss 0.05` 解码输出与原始 ffmpeg 文件逐字节
  一致（Cues 位移正确）。
- 全量 wasm **1223/1223**。

## 192. Matroska Chapters 章节读写

- 读取：`MatroskaHeader.chapters`（`MkvChapter`：纳秒起点、可选终点、
  首个 `ChapterDisplay` 标题），字节级扫描解析
  `Chapters/EditionEntry/ChapterAtom`，簇后章节同样覆盖；`mkv_chapter_new`
  供包外构造。
- 写入：`mkv_chapters_element` 生成单 EditionEntry 的 ChapterAtom 列表
  （UID 递增、`und` 语言）；`mkv_add_chapters` 走共享的
  `mkv_write_level1`：已存在 Chapters 时在原位置整体替换（避免 FFmpeg
  对重复 Chapters 元素的告警与合并），否则插到首个 Cluster 前；两种
  情况都按尺寸差重写 SeekHead/Cues 位置。
- 回归 `mkv_chapters_realdata_test.mbt`：ffmpeg 9.0.2 生成的三章节
  Opus MKV 读回 Intro/Middle/Outro 及 0/40/80 ms 起止；写入两章后
  旧章节被替换、包数不变；无章节文件插入一章后包数不变、
  标签不受影响；非 EBML 输入拒绝。
- 外部验证：替换与插入两种结果经 ffprobe 读到正确章节（无重复元素
  告警），`ffmpeg -ss` 输出与源逐字节一致。
- 全量 wasm **1227/1227**。

## 193. MP4 章节：chpl 读写

- 读取：`Mp4Movie.chapters`（`Mp4Chapter`：1/10,000,000 s 起点 +
  标题），`mov_parse_chpl` 解析 udta 下的 Nero `chpl` 盒（version 1
  时跳过未知字，逐章 8 字节起点 + 1 字节标题长 + UTF-8 标题）；
  `mp4_chapter_new` 供包外构造。
- 写入：`mp4_chpl_box` 按 `mov_write_chpl_tag` 生成；`udta` 构建改为
  `meta/ilst` + `chpl` 组合，`mp4_add_metadata` / `mp4_add_chapters`
  共用 `mp4_rewrite_udta`——传入的那一部分被替换、另一部分保留
  （读回现有 udta 后再拼装）。
- 回归 `mp4_chapters_realdata_test.mbt`：ffmpeg 生成的带标签 + 章节
  （音频轨 + 章节文本轨）MP4 读回 2 章与 3 个标签；`mp4_add_chapters`
  替换为 3 章且标签/样本数不变；`mp4_add_metadata` 改写标签且章节
  保留。
- 外部验证：对无章节轨的 ffmpeg MP4 添加 3 章后，ffprobe 读到
  Intro/Middle/Outro 与正确起点，解码无错误，`-ss` 输出与源逐字节
  一致。（带章节文本轨的文件里 FFmpeg 读取时以文本轨为准，chpl
  仅为兼容副本。）
- 全量 wasm **1230/1230**。

## 194. Matroska 字幕（S_TEXT/UTF8）

- 读取：现有轨道/块解析已按 TrackType 17 与 CodecID 收包；新增回归
  锁定 ffmpeg 9.0.2 生成的 SRT-in-MKV（BlockGroup + BlockDuration）
  读回 2 条字幕（pts 0/1200、时长 1000/1300）与原文。
- 写入：新增 `mkv_subtitle_mux.mbt`（`MkvSubtitleMuxer` /
  `mkv_subtitle_cue_new` / `write_cue` / `finish`）：单 `S_TEXT/UTF8`
  轨（TrackType 17、FlagLacing 0、Language und），每簇 BlockGroup
  （Block + BlockDuration），相对时间码超过 16 位范围时自动开新簇。
- 边界：解复用器的层结束弹出逻辑会在「簇的最后一个子元素是嵌套
  master 且与簇同界」时连带弹出簇层，从而吞掉后一簇；FFmpeg 自身
  布局（以 SimpleBlock 结尾）不会触发，本字幕写入器因此在簇尾追加
  零长 Void 规避，并在文件内注明。
- 验证：三簇（0/1.2/40 s）文件经 ffmpeg 提取 SRT 与源文本/时间
  逐条一致，ffprobe 识别为 subrip 字幕轨；全量 wasm **1233/1233**。

## 195. MP4 mov_text（tx3g）字幕

- 读取：容器层已按 `tx3g` 样本条目收轨（timescale 1,000,000）；
  新增 `mp4_tx3g_text`（u16 长度 + UTF-8，忽略后续样式盒）与
  ffmpeg 9.0.2 生成的字幕 MP4 夹具回归：3 个有效样本
  （"Hello"/空/"Second line"，时长 1e6/2e5/1.3e6）；FFmpeg 追加的
  零时长收尾样本被 edit list 裁剪。
- 写入：新增 `mp4_subtitle_mux.mbt`（`Mp4SubtitleMuxer` /
  `mp4_subtitle_cue_new` / `write_cue` / `finish`）：单 `tx3g` 轨
  （`sbtl` handler、Arial ftab 样本条目、nmhd/dinf/stbl），每个 cue
  一个文本样本，间隙写空样本，结尾补零时长空样本；movie/track
  timescale 均为 1,000,000，重叠 cue 与 >65535 字节文本拒绝。
- 外部验证：两 cue 文件经 ffmpeg 识别为 mov_text 字幕轨，提取 SRT
  与源逐行一致；本地回转样本数/时长/文本一致。
- 全量 wasm **1236/1236**。

## 196. FLAC 编码器：32-bit 样本与 33-bit 侧声道

- 编码器内部样本/预测改为 64 位：`flac_fixed_residual` / `flac_lpc_residual`
  在 64 位计算并检查 32 位残差范围（超界返回 None，回退其它阶或
  verbatim），`flac_wasted_bits`、立体声 side/mid 与声道收集全部用
  Int64；双层 Rice 选择与子帧成本不变。
- `FlacBits::write_signed_wide` 支持最多 33 位的常数/verbatim/warm-up
  写入（分块，避免 UInt32 左移 32）；帧头 bps 码新增 32→7；
  STREAMINFO 位深 32（bits-1=31）与 PCM MD5 的 4 字节打包同步补齐。
- 回归：32-bit mono、32-bit stereo（相关声道走 side 模式）与
  L/R 极值（L-R 达 33 位，侧声道只能 verbatim）三组回转逐样本一致；
  16-bit stereo / 相关立体声夹具编码尺寸保持 4903 / 1266 字节。
- 外部验证：三组 32-bit 文件经 ffmpeg 9.0.2 解码 s32le 与源 PCM
  逐字节一致，ffprobe 报告 flac/s32/32-bit。
- 全量 wasm **1239/1239**。

## 197. Matroska A/V 写入器：字幕轨

- `MkvAvMuxer`（H.264 + AAC/AC-3/E-AC-3）新增
  `write_subtitle(start_ms, end_ms, text)`：有 cue 时追加一条
  `S_TEXT/UTF8` 轨（TrackType 17、FlagLacing 0、und），簇内按时间戳
  把字幕 BlockGroup（Block + BlockDuration）与音视频 SimpleBlock
  合并排序，簇尾补 Void 规避层结束问题；Info Duration 计入字幕结尾。
- 回归：白盒夹具验证 3 条轨、字幕包时间/时长/文本与错误拒绝；
  真实 H.264 + AC-3 样本加两条字幕，读回视频/音频包与无字幕版本
  逐字节相同，字幕包正确。
- 外部验证：ffprobe 识别 h264/ac3/subrip 三轨，ffmpeg 提取 SRT 与
  源逐行一致，视频 framemd5 与音频 PCM MD5 与无字幕版本相同。
- 全量 wasm **1241/1241**。

## 198. MP4 A/V 写入器：mov_text 字幕轨

- `Mp4AvMuxer`（H.264 + AAC/AC-3/E-AC-3）新增
  `write_subtitle(start_ms, end_ms, text)`：有 cue 时在 `mdat` 追加
  mov-text 样本并写出第三条 `tx3g` 轨（`sbtl` handler、Arial ftab、
  nmhd、1000 timescale、TrackID 3）；间隙/起始空样本与零时长收尾
  样本复用抽出的 `mp4_subtitle_samples`（微秒），单轨字幕写入器同步
  复用该 helper。mvhd next_track_id 与 movie duration 计入字幕。
- 回归：真实 H.264 + AC-3 样本加两条字幕 → 3 轨、tx3g 轨道
  5 个样本（起始空/文本/间隙空/文本/收尾空）时长与文本正确，
  音视频样本与无字幕版本逐字节相同。
- 外部验证：ffprobe 识别 h264/ac3/mov_text，ffmpeg 提取 SRT 与源
  逐行一致，视频 framemd5 与音频 PCM 与无字幕版本完全相同。
- 全量 wasm **1242/1242**。

## 199. FLAC Vorbis comment 标签读写

- 读：`flac_read_tags` 遍历 metadata block（按 last 标志停止），解析
  type 4 的 vendor/entry 列表，`KEY=value` 在首个 `=` 处切分，并按
  `ff_vorbiscomment_metadata_conv` 把 ALBUMARTIST/TRACKNUMBER/
  DISCNUMBER/DESCRIPTION 映射为 album_artist/track/disc/comment。
- 写：`flac_add_tags(file, tags)` 删除已有 type 4 块、在 STREAMINFO
  之后插入新块（vendor "moonbit"），其余元数据块与全部帧原样保留，
  重写 last 标志；空列表即删除注释块；键含 `=` 或 NUL 拒绝。
- 回归 `flac_tags_realdata_test.mbt`：ffmpeg 生成的带标签/ PADDING
  FLAC（9941 字节）读回 4 个标签；替换为 3 个标签后旧标签消失、
  PCM MD5 不变；删除标签后仍可解码；非 FLAC/非法键拒绝。
- 外部验证：自编码 FLAC 写标签后 ffprobe 读到 title/ARTIST/album/
  track，ffmpeg 解码 PCM 与源逐字节一致（`f3acc310…`）。
- 全量 wasm **1245/1245**。

## 200. WAV LIST/INFO 标签读写

- 读：`wav_read_tags` 遍历 RIFF/RF64 块，解析 `LIST`/`INFO`
  子块（NUL 结尾、偶数对齐），按 `ff_riff_info_conv` 把
  IART/ICMT/ICOP/ICRD/IGNR/ILNG/INAM/IPRD/IPRT/ITRK/ISFT/ISMP/ITCH
  映射为通用键。
- 写：`wav_add_tags(file, tags)` 删除旧 INFO 列表、在 `fmt ` 后插入
  新 `LIST`（ffmpeg 的 IPRT 轨道号、NUL 结尾、偶数填充），保留
  fmt/data 等全部块；RIFF 尺寸重算，RF64 则回填 `ds64` 的 riffSize
  并保持 data 的 0xFFFFFFFF 哨兵（读取时用 ds64 dataSize）。
- 回归 `wav_tags_realdata_test.mbt`：ffmpeg 生成的带标签 WAV 读回
  5 个标签；替换后旧标签消失、data 块逐字节不变；删除/再加标签、
  自写 WAV 与 RF64 变体均可读写且数据不变；非 RIFF 输入拒绝。
- 外部验证：改写后的 WAV 经 ffprobe 读到 title/artist/track，
  ffmpeg 解码 PCM 与源 data 块逐字节一致（`b359e7ed…`）。
- 全量 wasm **1249/1249**。

## 201. ID3v2 标签读写

- 读：`id3v2_read_tags` 解析 ID3v2.3/2.4 头（synchsafe 尺寸、
  unsynchronisation 还原、扩展头跳过），遍历帧：`T***` 文本帧
  （Latin-1/UTF-16 BOM/UTF-16BE/UTF-8）、`TXXX`（description\0value）
  与 `COMM`（语言+描述+文本），按 `ff_id3v2_metadata_conv` 映射
  TIT2/TPE1/TPE2/TALB/TDRC/TCON/TRCK/TCOM/TSSE/TCOP/TPOS；无标签
  返回空列表。
- 写：`id3v2_write_tag` 生成 ID3v2.4（UTF-8 编码字节 3、synchsafe
  帧长），已知键写原生帧、其余写 `TXXX`；`id3v2_add_tags` 替换文件
  开头的旧标签（空列表即删除、含 footer 时一并跳过），音频字节原样
  保留。
- 回归 `id3v2_tags_realdata_test.mbt`：ffmpeg 生成的 ID3v2.4 MP3
  读回 7 个标签（含 TXXX comment/TSSE encoder）；替换为 4 个标签后
  旧标签消失、音频负载逐字节不变；删除标签得到裸 MP3 且读回空。
- 外部验证：改写后的 MP3 经 ffprobe 读到 title/artist/comment/
  custom key，ffmpeg 解码 PCM 与源文件一致。
- 全量 wasm **1251/1251**。

## 202. Ogg Vorbis comment 标签

- 抽出共享的 `vorbis_comment.mbt`（vendor/entry 解析、`KEY=value`
  切分、ALBUMARTIST/TRACKNUMBER/DISCNUMBER/DESCRIPTION 映射、payload
  序列化），FLAC 标签模块改为复用之。
- 读取：Opus 解复用解析 OpusTags（偏移 8）、Vorbis 解复用解析
  comment 头（偏移 7）、Ogg FLAC 解复用解析 type-4 元数据块，三者
  都新增 `tags` 字段。
- 写入：`ogg_opus_muxer_new(tags~)` 把自定义注释写进 OpusTags，
  vendor 仍为 "moonbit"。
- 回归 `ogg_tags_realdata_test.mbt`：ffmpeg 生成的 libvorbis 与
  Ogg FLAC 夹具读回 title/artist/encoder；Opus 写入器带 3 个标签
  回转正确（TRACKNUMBER→track）、无标签时为空。
- 外部验证：自写带标签 Opus 经 ffprobe 读到 title/artist/track，
  ffmpeg 解码 PCM 与无标签版本一致。
- 全量 wasm **1254/1254**。

## 203. A/V 写入器：标签与章节

- `Mp4AvMuxer` 新增 `set_tags` / `set_chapters`（`Mp4Chapter`），
  `finish()` 把 `udta`（meta/ilst + chpl）挂到 moov 末尾，与可选
  mov_text 轨并存。
- `MkvAvMuxer` 新增 `set_tags` / `set_chapters`（`MkvChapter`），
  `finish()` 在 Tracks 之后、Cluster 之前写 Chapters 与 Tags 元素。
- 修复：`mkv_tags_element` 在 Tag/Tags 末尾各补一个零长 Void。解复用
  器的层结束弹出会把「与父层同界」的内层 master 连同父层一起弹出，
  导致 Tag 恰好结束于 Tags 结尾时流式解析越过元素、触发重同步并
  重复解析 Tracks（表现为轨道数翻倍）。Void 使各层边界唯一；带标签
  的 A/V MKV 现能正确读出 3 轨 + 标签 + 章节。
- 回归：真实 H.264 + AC-3 的 MP4/MKV 同时带字幕、标签与章节，读回
  轨道/标签/章节计数正确，音视频样本与无附加轨版本逐字节相同。
- 外部验证：ffprobe 两个文件均显示 h264/ac3/字幕（mov_text 或
  subrip）、TAG 与章节（含结束时间），ffmpeg 全量解码无错误并正确
  提取 SRT。
- 全量 wasm **1254/1254**。

## 204. HLS 字幕 rendition 与 WebVTT 分片

- `HlsMasterVariant` 新增 `subtitle_group`，`hls_write_master_playlist`
  输出 `SUBTITLES="group"` 属性；`HlsMasterRendition` 已有 TYPE 字段
  可写 `EXT-X-MEDIA:TYPE=SUBTITLES`。
- 新增 `hls_subtitles.mbt`：`HlsVttCue`（绝对毫秒）与
  `hls_write_vtt_segment`，按 FFmpeg `webvttenc.c` 输出 `WEBVTT` 头 +
  绝对时间戳 cue（`HH:MM:SS.mmm`），不写 X-TIMESTAMP-MAP（FFmpeg 的
  HLS demuxer 以 playlist 时间轴读取 cue）。字幕媒体播放列表复用
  `hls_write_media_playlist`。
- 回归：VTT 文本逐字节断言（含多行 cue 与时间格式）、master 播放列表
  含 TYPE/GROUP-ID/URI/SUBTITLES/LANGUAGE 属性。
- 外部验证：生成的 sub0.vtt + subs.m3u8 + master.m3u8 经 ffprobe
  识别 webvtt 字幕流，ffmpeg 从裸 VTT 与 master 均提取出时间/文本
  完全一致的 SRT。
- 全量 wasm **1256/1256**。

## 205. DASH 字幕 AdaptationSet（WebVTT）

- `dash_write_segment_template_mpd` 适配文本轨：`codecs` 为空时省略
  属性、`initialization` 为空时省略（WebVTT 分片自包含）、
  `bandwidth<=0` 时省略；其余属性不变，字幕分片复用
  `hls_write_vtt_segment`（同格式 WebVTT）。
- 回归：`text` AdaptationSet 输出 `contentType="text"`、
  `mimeType="text/vtt"`、`lang`、`media="sub$Number$.vtt"`，且不含
  codecs/initialization。
- 外部验证：自写 manifest.mpd + sub1/sub2.vtt 经 `ffprobe
  -allowed_extensions ALL` 识别 webvtt 字幕流，ffmpeg 提取两段 cue
  的时间/文本与源一致。
- 边界：空的 WebVTT 分片（仅 `WEBVTT` 头）会让 ffmpeg dashdec 报
  “Error when loading first fragment”，属其空片段处理限制；有 cue
  的分片无告警。
- 全量 wasm **1257/1257**。

## 206. Sony Wave64（.w64）读写

- 读取：`av_w64_open` 按 GUID 分块遍历（块长 24 字节头 + 8 字节
  对齐），支持 `fmt `（WAVEFORMATEX）、`fact`（采样数）与 `data`；
  复用抽出的 `wav_build_demux` 完成 PCM 封包（WAV/RF64 路径不变）。
- 写入：`w64_muxer_new`/`write_samples`/`finish`：riff/wave GUID、
  对齐后的 fmt/data 块、非 PCM（IEEE float32）追加 fact 块；采样
  打包与 WAV 写入器一致。
- 回归 `w64_realdata_test.mbt`：ffmpeg 生成的 16-bit mono 与
  24-bit stereo W64 与其等源 WAV 的 PCM 载荷逐字节一致（格式/声道/
  采样率/采样数断言）；自写 PCM 与 float（tag 3 + fact）回转正确。
- 外部验证：自写 16-bit 与 float32 W64 经 ffmpeg 解码 s16le/f32le
  与源字节完全一致。
- 全量 wasm **1259/1259**。

## 207. Wave64 summarylist 标签

- 读：`w64_read_tags` 解析 `summarylist` 块（u32 条目数，每条
  4CC + u32 字节长 + UTF-16LE 字符串），按 `ff_riff_info_conv`
  映射为通用键。
- 写：`W64Muxer::set_tags` 在 data 之后追加 summarylist 块
  （UTF-16LE + NUL、8 字节对齐、riff 总长同步）；未知键与空值跳过。
- 回归：带 title/artist/track（含中文标题）与无标签两组回转断言，
  audio 载荷不变。
- 外部验证：自写带标签 W64 经 ffprobe 读到 title/artist，ffmpeg
  解码 PCM 与无标签版本完全相同。
- 全量 wasm **1260/1260**。

## 208. DASH 字幕 SegmentList/SegmentTimeline

- `dash_write_segment_list_mpd` 与 `dash_write_timeline_mpd` 适配文本轨：
  `codecs` 为空时省略属性、`bandwidth<=0` 时省略、SegmentList 的
  `<Initialization>` 仅在 sourceURL/range 非空时输出、SegmentTimeline
  的 `initialization` 属性为空时省略，其余输出不变。
- 回归：两种 MPD 的 `text/vtt` AdaptationSet 均断言无 codecs/
  initialization/bandwidth，解析回读得到正确分片 URI。
- 回读：WebVTT 分片经 `dash_segment_reader_new` 逐段取出，字节与
  `hls_write_vtt_segment` 输出完全一致，且 initialization 为空；
  SegmentList 与 `$Time$` 时间线各一组。
- 外部验证：自写 list.mpd / timeline.mpd（各两个 VTT 分片，含中文
  cue）经 ffmpeg 9.0.2 `-allowed_extensions ALL` 识别为 webvtt 字幕流，
  提取 SRT 的时间与文本与源一致。
- 全量 wasm **1264/1264**。

## 209. DASH SegmentBase 单文件写入

- 新增 `fmp4_sidx`：version 1 全局 `sidx`（reference_ID/timescale/
  earliest_presentation_time/first_offset=0、每条 media 引用 12 字节），
  与 `movenc.c` 的 fragmented 输出布局一致；非法参数返回 EINVAL。
- 新增 `dash_write_segment_base_mpd`/`DashBaseAdaptation`：
  `BaseURL` + `SegmentBase indexRange` + `Initialization range`，
  文本/音频/视频属性按空值省略。
- 回归：用现有 ffmpeg 夹具的 `sidx`（文件 817-880）逐字节比对通过；
  自建 init+sidx+两段 media 的单文件经 `av_dash_parse` 与
  `DashSegmentReader` 回读，5 帧 H.264 MD5 与参考一致。
- 外部验证：ffmpeg 9.0.2 生成 64x48/25fps/10 帧 H.264 MKV，经
  Matroska 解复用→fMP4 写入→sidx/MPD 生成自写单文件，HTTP 服务下
  ffmpeg `-allowed_extensions ALL` 解码 MPD 的 framemd5 与源 MKV
  逐帧一致（尾部重复帧为 dashdec 对官方 SegmentBase 夹具同样存在的
  行为）。
- 全量 wasm **1266/1266**。

## 210. MJPEG 无损 RGB（1x1 三分量，8/9-bit）

- 新增 `mjpeg_decode_scan_rgb`（`ljpeg_decode_rgb_scan` 移植）：
  逐像素三分量 DC + 空间预测，支持单/多 scan 与 restart interval；
  `mjpeg_finalize_rgb` 输出 FFmpeg 原生 `bgr24`（8-bit 直接映射，
  9-bit 走 BGR 编码器的可逆颜色变换 `rct`）。
- SOF3 解析允许 3 分量 1x1 的 9-bit 精度；其他 >8-bit 仍报
  PATCHWELCOME。无损 YUV 平面路径不变。
- `jpeg_marker_start` 按 FFmpeg `find_marker` 语义向后扫描未消费的
  scan 尾部数据（部分编码器会留下填充字节），避免误报 marker 错误。
- 回归：原有 64x48 夹具（8-bit 与 9-bit 各一）按 ffmpeg rawvideo
  bgr24 MD5 断言；新增 17x13 / 预测器 1、2 的 libjpeg 夹具，BGR24
  MD5 与 ffmpeg 一致。失效的“RGB/9-bit 必须拒绝”旧断言已改为正向。
- 全量 wasm **1268/1268**。

## 211. AV1 resize×superres 组合（libaom 参考）

- dav1d 1.5.1 在 resize×superres 流上段错误；改用 libaom `aomdec`
  逐帧原生 I420 参考（已先在 superres_inter 上与 dav1d MD5 对齐验证
  方法本身）。
- 新增 aomenc 流 `resize_sr`：fixed resize（denom 16 / kf 8）+
  superres mode 2（denom 13），24 帧、输出尺寸 192x128/96x64 交替；
  本移植逐帧 MD5 24/24 与 aomdec 一致。
- 生成脚本加入该 case，`resize_sr` 的参考步骤改用 aomdec；夹具
  `resize_sr_fixture_test.mbt` 与 `resize_test.mbt` session 入库。
- 另外手工验证 resize×superres×qm 流同样 24/24 一致（该流无编码
  命令记录，未固化）。
- 全量 wasm **1269/1269**。

## 212. MJPEG 4 分量 CMYK/YCCK（1x1）

- 解析 Adobe APP14，`transform` 0 = CMYK、2 = YCCK；4 分量且组件
  ID 为 C/M/Y/K 时按 FFmpeg 强制 transform 0。
- `mjpeg_finalize_image` 增加 4 分量 1x1 分支：按 FFmpeg 的
  `adobe_transform == 0 -> gbrap`、否则 `yuva444p`，并逐像素应用
  原位转换（GBRAP 的 `C*K/M*K/Y*K` 与 YUVA 的 YCCK 反变换，
  alpha 置 255）。子采样 4 分量返回 PATCHWELCOME。
- 夹具：ImageMagick 生成的 17x13 CMYK JPEG（APP14 transform 2）及
  同文件 APP14 字节改为 transform 0 的变体；两者 concat 四平面后
  与 `ffmpeg -f rawvideo`（yuva444p/gbrap）MD5 逐字节一致。
- 全量 wasm **1270/1270**。

## 213. WAV A-law/µ-law 写入

- `wav_muxer_new` 新增 `companding="alaw"|"mulaw"`：调用现有
  `pcm_alaw_encode`/`pcm_mulaw_encode` 写每样本 1 字节；仅允许
  8-bit 小端，float/RIFX/16-bit 组合报错。
- 头部按 `ff_put_wav_header`：非 extensible 用 WAVEFORMATEX
  （fmt 18 字节、cbSize=0、tag 6/7）、`fact` 帧数、byte rate=采样率×
  声道；>2 声道或 mask 走 0xFFFE，valid bits=8，子格式 6/7。
- 回归 `wav_companding_wbtest.mbt`：A-law/µ-law 74 字节文件逐字段
  断言（与 ffmpeg 输出去掉 LIST/INFO 后逐字节一致）、WAV 解复用
  回读、6 声道 extensible 子格式与 round-trip、非法参数拒绝。
- 外部验证：ffmpeg 9.0.2 将自写单声道与 5.1 文件识别为
  pcm_alaw/pcm_mulaw 并解码为与 ffmpeg 自产文件相同的 PCM。
- 全量 wasm **1274/1274**。

## 214. MJPEG 3 分量 Adobe 变换（GBRP）

- `mjpeg_finalize_image` 3 分量分支：Adobe APP14 transform 0 或组件
  ID 为 R/G/B 时按 FFmpeg 选择 `gbrp`，并将解码后的 R,G,B 平面交换为
  G,B,R；子采样组合返回 PATCHWELCOME，其余仍走 yuvj 选择。
- 夹具：`cjpeg -rgb` 生成的真实 17x13 RGB JPEG（transform 0 + RGB
  ID），另造两个变体分别只保留 transform 0（ID 改为 1/2/3，SOF/SOS
  同步）和只保留 RGB ID（transform 改为 1）；三者 GBRP 三平面拼接后
  与 `ffmpeg -f rawvideo -pix_fmt gbrp` MD5 一致。
- 全量 wasm **1275/1275**。

## 215. MJPEG 子采样 YCCK 上采样

- 新增 `mjpeg_upscale2`：复刻 FFmpeg 的 2× chroma 上采样（水平
  `(src[x/2]+src[(x+1)/2])>>1`、垂直半行平均/边缘复制、底向上原地
  等价）。
- 4 分量分支支持 `2x2 Y + 1x1 Cb/Cr + 2x2 K` 布局：transform 0/2
  时先把 chroma 两平面升到全分辨率，再走原有 gbrap/yuva444p 转换；
  其他子采样布局仍 PATCHWELCOME。
- 顺带修复奇数尺寸子采样 JPEG 的 MCU 边界：右/下填充 MCU 块照常
  解码以维持熵流与 DC 预测，但不写入组件块表（此前会报
  “MCU block outside component”）。
- 夹具：ImageMagick `-sampling-factor 2x2,1x1,1x1,2x2` 生成的 17x13
  CMYK JPEG（transform 2）与 APP14 改为 0 的变体；四平面与 ffmpeg
  rawvideo（yuva444p/gbrap）MD5 一致。
- 全量 wasm **1276/1276**。

## 216. MP4 H.264 B 帧（ctts）

- `Mp4H264Muxer::write_packet` 新增 `cts`（PTS-DTS），finish 在
  `stts` 后写 `ctts`：连续相同偏移合并为 run，全 0 时省略；存在负
  偏移时用 version 1（signed），否则 version 0。
- `mp4_video_document` 增加 cts 参数（静态图写入器传空数组），样本
  表顺序 stsd/stts/ctts/stss/stsc/stsz/stco。
- 回归 `mp4_bframes_wbtest.mbt`：真实 x264 B 帧 MP4（bf=3，8 帧）
  经解复用→重封装→再解复用，逐样本 cts 与 DTS 相对差值、时长、
  关键帧标志一致，解码帧 MD5 序列一致；另测负 ctts 走 version 1
  且 PTS/DTS 正确。
- 外部验证：宿主机生成的 B 帧 MP4 用本写入器重封装后，ffmpeg
  9.0.2 的 framemd5 8/8 与源一致，`-c copy -f framecrc` 的 packet
  DTS/PTS 逐行与源相同（含负 DTS）。
- 全量 wasm **1278/1278**。

## 217. MP4 A/V 写入器 B 帧 ctts

- `Mp4AvMuxer::write_video` 新增 `cts`；`mp4_av_stbl` 在 `stts` 后写
  `ctts`（run 合并、全 0 省略、负偏移 version 1），音频/字幕轨传空。
- 回归 `mp4_av_bframes_wbtest.mbt`：真实 H.264 B 帧 + AAC-LC MP4
  （5 视频帧 + 音频包）解复用→重封装→再解复用，视频 CTS/DTS 按
  90000 timescale 换算后逐样本一致、帧 MD5 序列一致；音频样本数与
  时长一致；`skip_samples` 作为 `aac_priming` 写入 elst。
- 外部验证：ffmpeg 9.0.2 对自写 A/V 文件 video framemd5 与源一致，
  AAC 解码 PCM 88200 字节逐字节一致（priming 生效）。
- 全量 wasm **1279/1279**。

## 218. MP4 HEVC/AV1/VP9 ctts

- 抽出共享 `mp4_ctts_box`（run 合并、全 0 返回 None、负偏移
  version 1），H.264 单轨与 A/V 写入器改用该 helper。
- `Mp4HevcMuxer`/`Mp4Av1Muxer`/`Mp4Vp9Muxer::write_packet` 新增
  `cts`，stbl 在 `stts` 后按需插入 `ctts`。
- 回归：三个写入器的合成样本加入正/负 cts，PTS 经解复用回读正确；
  另加真实 x265 B 帧 HEVC MP4（6 帧）重封装测试，CTS/DTS 与解码
  帧 MD5 一致。
- 外部验证：ffmpeg 9.0.2 对自写 HEVC 文件 framemd5 6/6 一致，
  `-c copy -f framecrc` 的 packet DTS/PTS 逐行与源相同。
- 全量 wasm **1280/1280**。

## 219. MPEG-TS PES PTS+DTS（B 帧）

- `TsMuxer::write_video` 新增可选 `dts`：PES 头部 flags 0xC0、
  header_length 10，按规范先写 PTS 再写 DTS；PCR 使用 DTS（未给
  时仍用 PTS）。音频 PES 保持 PTS-only。
- 路由/有界 PES 的长度计算同步使用 13 字节头（PTS+DTS）。
- 回归 `ts_bframes_wbtest.mbt`：真实 x264 B 帧 TS（6 帧）解复用→
  重封装→再解复用，逐包 CTS/DTS 相对值与 payload 完全一致，解码帧
  MD5 一致。
- 外部验证：ffmpeg 9.0.2 对自写 TS 的 framemd5 6/6 一致，
  `-c copy -f framecrc` 的 DTS/PTS（含负 DTS）逐行与源相同。
- 全量 wasm **1281/1281**。

## 220. fMP4 trun composition time offset（B 帧）

- `Fmp4Sample` 新增 `cts`（PTS-DTS，默认 0）；`fmp4_sample` 增加
  可选参数；`fmp4_traf` 在任一样本 cts 非 0 时给 trun 加 0x0800
  标志并逐样本写 composition offset，负值用 trun version 1（否则 0）。
- 现有 H.264/HEVC/AV1/VP9/AAC/Opus 初始化段与 HLS/DASH 分片写入器
  自动受益；MOV 解复用器已有 trun CTS 解析，无需改动。
- 回归 `fmp4_bframes_wbtest.mbt`：真实 x264 B 帧 MP4 解复用后拆成
  两段 fMP4（init+2 media），`av_mov_open` 回读逐样本 CTS/DTS 相对
  值一致，解码帧 MD5 一致。
- 外部验证：自写 fMP4（init+2 moof）经 ffmpeg 9.0.2 解码
  framemd5 8/8 与源一致，`-c copy -f framecrc` 的 DTS/PTS（含负
  DTS）逐行相同。
- 全量 wasm **1282/1282**。

## 221. MP4 HEVC+AAC 双轨写入

- `Mp4AvMuxer::new` 新增 `hevc~=false`：为 true 时把 avcc 参数解释为
  hvcC 并写 `hvc1`/`hvcC`（复用 `mp4_hevc_entry`），否则维持
  `avc1`/`avcC`；视频轨 ctts/DTS 与音频 elst priming 逻辑不变。
- 回归 `mp4_av_hevc_wbtest.mbt`：真实 x265 B 帧 + AAC-LC MP4
  解复用→重封装→再解复用，视频 CTS/DTS（timescale 换算后）逐样本
  一致、HEVC 解码帧 MD5 一致，音频样本数/时长一致。
- 外部验证：ffmpeg 9.0.2 识别 hvc1+AAC，video framemd5 5/5 与源
  一致，AAC 解码 PCM 88200 字节逐字节一致。
- 全量 wasm **1283/1283**。

## 222. MP4 双轨视频 edit list（B 帧起点对齐）

- 新增 `mp4_edit_list_at(media_time, movie_duration)`：单条 elst，
  duration 为完整影片时长、media_time 为首帧呈现时间。
- `Mp4AvMuxer::finish` 计算视频最小 PTS（累计 DTS + cts）；大于 0
  时写视频 `edts`，把 B 帧重排延迟从呈现时间轴裁掉。此前重封装
  x264 B 帧文件会出现视频比音频晚 0.4s 的 A/V 偏移。
- 回归：H.264+AAC 与 HEVC+AAC 重封装测试新增断言——视频首样本
  PTS=0、DTS<0（edit list 生效），原有 CTS/DTS 与解码帧校验不变。
- 外部验证：ffmpeg 9.0.2 对两个自写文件的 video framemd5 5/5 与源
  一致、AAC PCM 逐字节一致，`framecrc` 视频首包恢复为
  PTS 0 / DTS −36000（90000 timescale），与源音视频起点对齐。
- 全量 wasm **1283/1283**。

## 223. MP4 单轨视频 edit list

- 新增共享 `mp4_video_edts`：由 durations+cts 求最小 PTS，正偏移时
  用 `mp4_edit_list_at` 生成 edts（单轨写入器的 movie/media
  timescale 相同）。
- `mp4_video_document`（H.264/静像）与 HEVC/AV1/VP9 finish 在 trak
  中按需插入 edts；无 cts 或首帧 PTS=0 时行为不变。
- 回归：H.264 与 HEVC B 帧重封装测试断言首样本 PTS=0、DTS<0；
  AV1/VP9 合成 cts 测试不受影响。外部 ffmpeg framecrc 首包恢复
  PTS 0 / DTS −4096，framemd5 8/8（H.264）与 6/6（HEVC）与源一致。
- 全量 wasm **1283/1283**。

## 224. TS 多节目解复用回归

- 解复用器本身已支持 PAT/PMT 多节目；新增真实双节目 TS 夹具
  （两个独立 H.264 视频，PAT 中 program 1/2，PMT/PID 0x100/0x101）。
- 回归 `ts_multiprogram_wbtest.mbt`：programs=2、streams=2，
  各流 4 个 PES、PID 映射正确、PCR 存在；两路 Annex-B 拼接后分别
  解码 4 帧，MD5 与 ffmpeg 9.0.2 按节目 `-map 0:p:N` 的 framemd5
  完全一致。
- 代码无改动（覆盖已有实现）。
- 全量 wasm **1284/1284**。

## 225. MP4 双轨 AV1/VP9 视频

- `Mp4AvMuxer::new` 的 `hevc~` 替换为 `video_codec~`（h264/hevc/
  av1/vp9）：按 codec 选择 avc1/hvc1/av01/vp09 采样条目；avcC 仍
  要求 ≥7 字节，hvcC/av1C/vpcC 要求 ≥4 字节。
- 回归 `mp4_av_av1_wbtest.mbt`：真实 AV1+AAC MP4 重封装后 av01
  条目/extradata、逐样本 payload、CTS/DTS（timescale 换算后）、
  音频时长一致；VP9 vpcC 条目与 AAC 合成 round-trip。
- 外部验证：ffmpeg 9.0.2 识别 av01+AAC，video framemd5 5/5 与源
  一致，AAC PCM 88200 字节逐字节一致。
- 全量 wasm **1286/1286**。

## 226. HLS/DASH B 帧分片回归

- 新增 HLS 与 DASH 端到端测试：真实 x264 B 帧 MP4 拆成两段带
  trun CTS 的 fMP4 分片；HLS 用 `EXT-X-MAP` 媒体列表、DASH 用
  SegmentTemplate MPD。
- 两个 reader 回读 init+media 后逐样本 payload、CTS/DTS（相对值）
  与源一致，解码帧 MD5 序列一致。
- 无代码改动（覆盖已有 fMP4 CTS + HLS/DASH reader 组合）。
- 全量 wasm **1288/1288**。

## 227. MP4 双轨 Opus 音频

- `Mp4AvMuxer::new` 新增 `audio_codec~`（aac/opus）：opus 时用
  `mp4_opus_entry` 写 `Opus`/`dOps`（pre-skip 取 `aac_priming`），
  elst priming 逻辑复用；Opus 限制 1/2 声道。
- 回归 `mp4_av_opus_wbtest.mbt`：真实 AV1+Opus MP4 重封装后
  av01/Opus 条目与 extradata、视频 payload、音频包数/时长、
  `skip_samples` 均与源一致。
- 外部验证：ffmpeg 9.0.2 识别 av01+Opus，video framemd5 5/5 与源
  一致，Opus 解码 PCM 97296 字节逐字节一致。
- 全量 wasm **1289/1289**。

## 228. MJPEG 渐进 4 分量 CMYK 回归

- ImageMagick `-interlace JPEG` 生成 17x13 渐进 CMYK（SOF2，
  transform 2）及 APP14 改为 0 的变体；解码后四平面与 ffmpeg
  rawvideo（yuva444p/gbrap）MD5 与同源顺序式文件一致。
- 覆盖现有渐进扫描 + 4 分量 finalize 组合，代码无改动。
- 尝试渐进 3 分量 RGB ID 夹具时 FFmpeg 自身报
  `decode_sos: index(3) out of components`，不纳入。
- 全量 wasm **1290/1290**。

## 229. TS H.264 B 帧 + AAC 双轨回归

- 真实 x264 B 帧 + AAC MPEG-TS（5 视频 PES + 多个 ADTS PES）经
  `TsMuxer` 解复用→重封装→再解复用：按 PID 比较每个 PES 的
  PTS/DTS/payload 完全一致，视频解码帧 MD5 一致。
- 外部验证：ffmpeg 9.0.2 对源与自写 TS 的 video framemd5 5/5、
  AAC PCM 92160 字节一致，`-c copy -f framecrc` 逐包相同。
- 覆盖 PES 完成顺序与文件顺序不同的情况：测试按 PID 分组比较。
- 全量 wasm **1291/1291**。

## 230. WebM SeekHead/Cues 写入

- 新增共享 `matroska_index_mux.mbt`：固定 8 字节 SeekPosition 的
  SeekHead（Info/Tracks/Cues）与单簇 Cues（每个视频关键帧一条
  CuePoint，CueClusterPosition 指向 Cluster 元素）。
- 接入 WebM VP9/AV1 单轨、VP9+Opus 双轨写入器（音频轨不进 Cues）；
  Opus 单轨写入 SeekHead（无 Cues）。
- 回归 `webm_cues_wbtest.mbt`：CueTime 0/200（A/V 0/200），
  CueClusterPosition 与文件内实际 Cluster 偏移一致，
  `mkv_seek_cue` 返回正确条目。
- 外部验证：真实 libvpx VP9 WebM 经重封装后 ffmpeg 全片 framemd5
  5/5 与源一致，`-ss 0.4` 输出的帧哈希与源相同。
- 全量 wasm **1293/1293**。

## 231. MKV 单轨 PTS + SeekHead/Cues

- `MkvH264Muxer`/`MkvHevcMuxer::write_packet` 新增可选 `pts_ms`
  （-1 表示沿用累计时长），SimpleBlock 时间码与 Cues 的 CueTime 使用
  该 PTS，从而支持 B 帧解码顺序 ≠ 呈现顺序。
- 两个写入器接入共享 `mkv_seekhead_cues`（Info/Tracks/Cues +
  关键帧 CuePoint）。
- 回归 `mkv_index_wbtest.mbt`：CueTime/CueClusterPosition 与
  Cluster 偏移一致，显式 PTS 0/200/100 经解复用按序保留。
- 外部验证：真实 x264 B 帧 MP4 重封装为 MKV 后，ffmpeg 全片
  framemd5 8/8 与源一致，`-c copy -f framecrc` 的 PTS 重排序列
  相同，`-ss 0.4` 定位帧哈希与源一致。
- 全量 wasm **1296/1296**。

## 232. MKV A/V SeekHead/Cues 与 B 帧解码顺序

- `mkv_seekhead_cues_mid` 支持 Tags/Chapters 位于 Tracks 与 Cluster
  之间；MKV A/V 写入器输出 SeekHead/Cues（关键帧 CueTime）。
- 修复 A/V 事件按 PTS 全排序导致 B 帧块被重排、破坏解码顺序的问题：
  现在先按各轨累计 DTS 稳定排序交错，SimpleBlock 时间码仍写 PTS。
- 回归：A/V Cues 时间与 Cluster 偏移；`mkv_add_tags` 插入后 Cues
  位置前移且可解析；显式乱序 PTS（0/200/100）经解复用保持解码顺序。
- 外部验证：真实 x264 B 帧 + AAC MP4 重封装为 MKV 后，视频
  framemd5 5/5 与源一致。
- 全量 wasm **1298/1298**。

## 233. MKV A/V AAC priming（CodecDelay/DiscardPadding）

- `MkvAvMuxer::new` 新增 `audio_priming` 与 `audio_discard_padding`
  （采样数）：前者写 TrackEntry CodecDelay（ns，独立四舍五入）并把
  音频块时间码前移 offset（ms），后者把最后一个音频块写成
  BlockGroup + DiscardPadding（正值表示尾丢弃，与 FFmpeg 一致）。
- 音频 PTS 允许到 -offset；`mkv_sint_elem` 输出最小宽度有符号 EBML。
- 回归：CodecDelay>0、首音频包 PTS 回到 0、尾块 DiscardPadding
  非 0。
- 外部验证：真实 x264 B 帧 + AAC MP4 重封装为 MKV 后，ffmpeg 解码
  AAC PCM 88200 字节与源逐字节一致（初版尾块符号/舍入错误已修正）。
- 全量 wasm **1299/1299**。

## 234. WebM A/V Opus delay/padding

- `WebmAvMuxer` 新增 `audio_discard_padding`（采样数）；音频轨写
  TrackEntry CodecDelay（pre-skip，48 kHz 四舍五入）与 SeekPreRoll
  80 ms；音频块时间码前移 offset（ms），最后一个音频块用
  BlockGroup + DiscardPadding（正值）表达尾丢弃。
- 回归：codec_delay/seek_preroll 字段、首音频 PTS=0、尾块
  discard_padding 非 0，Cues 仍为视频关键帧。
- 外部验证：真实 libvpx VP9 + libopus WebM 重封装后视频 framemd5
  5/5 与源一致，Opus PCM 96000 字节逐字节一致，`-ss 0.4` 输出序列
  与源相同。
- 全量 wasm **1300/1300**。

## 235. HLS 分段级 DISCONTINUITY/GAP/PDT 标签

- `HlsMediaSegmentSpec` 增加 `discontinuity`、`gap`、
  `program_date_time`；新增 `hls_write_media_playlist_specs`（按请求
  version 输出），`hls_write_media_playlist_byterange` 改为其薄包装
  （BYTERANGE 仍自动抬到 version 4）。
- 标签顺序对齐 FFmpeg：`#EXT-X-DISCONTINUITY`/`#EXT-X-GAP` 在
  `#EXTINF` 前，`#EXT-X-PROGRAM-DATE-TIME` 在 EXTINF/BYTERANGE 之后、
  URI 之前（hlsplaylist.c `ff_hls_write_file_entry`）；
  `#EXT-X-DISCONTINUITY-SEQUENCE` 紧跟 VERSION、位于首个分段之前
  （按 RFC 8216；ffmpeg 9.0.2 hlsenc 实际不写该标签，属本移植扩展）。
- 回归：写出文本的标签顺序 + `av_hls_parse` 回读
  `discontinuity`/`gap`/`program_date_time`/`discontinuity_sequence`；
  无标签分段不产生多余行。
- 外部验证：ffmpeg 9.0.2 hlsenc `-hls_flags program_date_time` 的输出
  确认 PDT 位于 EXTINF 之后；该版本 HLS demuxer 源码不解析
  DISCONTINUITY/GAP/PDT，故标签语义以本项目解析器回读为准。
- 全量 wasm **1301/1301**。

## 236. HLS 播放列表头标签

- 媒体播放列表写入器新增 `allow_cache`（0/1 写
  `#EXT-X-ALLOW-CACHE:NO/YES`，其余省略）、`playlist_type`（非空时
  原样写 `#EXT-X-PLAYLIST-TYPE`，如 VOD/EVENT）、`i_frames_only`
  （`#EXT-X-I-FRAMES-ONLY`）与 `independent_segments`
  （`#EXT-X-INDEPENDENT-SEGMENTS`）；三个公开写入器均透传。
- 顺序对齐 hlsplaylist.c：VERSION → ALLOW-CACHE → TARGETDURATION →
  MEDIA-SEQUENCE → PLAYLIST-TYPE → I-FRAMES-ONLY；hlsenc 在头部之后、
  分段之前写 INDEPENDENT-SEGMENTS。
- 回归：标签文本与顺序、`av_hls_parse` 回读 `playlist_type`/
  `i_frames_only`/`independent_segments`、默认值不产生任何头标签。
- 外部验证：ffmpeg 9.0.2 hlsenc
  `-hls_playlist_type vod -hls_flags independent_segments -hls_allow_cache 0`
  生成的播放列表顺序与本写入器一致；用 ffmpeg 生成的真实 TS 分片，
  本写入器输出的播放列表经 ffmpeg 解码 framemd5 与 ffmpeg 自身播放
  列表完全相同。
- 全量 wasm **1302/1302**。

## 237. FLAC 编码器 Rice escape 分区

- `flac_choose_rice` 增加 escape 候选：分区阶 > 0 时可选用全 1 Rice
  参数，把分区存成定宽有符号样本（5 位位宽字段）；RFC 9639 限定
  escape 仅用于分区阶 > 0，单分区选择器传 false。
  `flac_escape_width` 按分区内二补码所需位宽计算，超过 31 位不可
  escape。仅当 escape 比特数低于 Rice 时才采用。
- 写入端在参数等于 `(1<<rice_bits)-1` 时写 5 位位宽和原始样本
  （Rice 参数 15、Rice2 参数 31）。
- 回归：24-bit 密集噪声（值域 ±2^15，4096 样本）帧长 8205 字节
  （<8500；Rice2 约 8700、verbatim 约 12290），`ff_flac_decode`
  回读 PCM 逐样本一致（解码器把 24-bit 左对齐到 32 位）。
- 外部验证：ffmpeg 9.0.2 解码该 FLAC 的 s32le 输出 MD5 与独立计算
  的移位 PCM MD5 相同，证明含 escape 分区的码流符合规范。
- 注：ffmpeg 的 flacenc 本身不产出 escape 分区（无 escape 代码），
  故无法做编码字节对拍；其 flacdec 的 `decode_residuals` 支持 escape。
- 全量 wasm **1303/1303**。

## 238. Opus output_gain 增益验证

- 新增 `libavcodec/opus_gain_test.mbt`：同一段 libopus CELT 码流分别用
  `output_gain` 0、+5 dB（1280）、-5 dB（-1280）的 OpusHead 解码，
  断言增益流的每个样本恰为无增益样本乘 FFmpeg 的 float 增益常量
  （`ff_exp10(gain_i/(20*256))` = `exp2(M_LOG2_10 * x)`，glibc 结果
  转 float：+5 dB = 1071881897、-5 dB = 1058010522）。
- 增益常量对比：在 -10..+16 dB 的 18 个取值上，MoonBit 的
  `pow(10, x)`、`pow(2, M_LOG2_10*x)`、`exp(M_LN2 * M_LOG2_10*x)`
  转 float 后与 glibc `exp2` 的 float 位完全相同，因此解码器的
  `pow(10, x)` 实现无需改动；注释已更新。
- 注：ffmpeg 的 opusdec 用 `vector_fmul_scalar(..., FFALIGN(n,8))`
  逐样本乘同一 float，对齐部分不影响有效样本。白盒测试会触发
  wasm `__init` 局部变量上限，故测试放在 blackbox 二进制。
- 全量 wasm **1304/1304**。

## 239. LOAS/LATM AAC 读取

- 新增 `libavcodec/latm.mbt` 的 `latm_parse_stream`：按 0x2B7/13-bit
  长度切 LOAS 帧，解析 AudioMuxElement/StreamMuxConfig（audioMuxVersion
  0/1、单 program/单 layer、frameLengthType 0/1），用
  `aac_parse_audio_specific_config` 求出内嵌 ASC 的比特长度并取出 ASC，
  PayloadLengthInfo 之后按比特重打包为字节对齐的原始 AAC AU。
  `audioMuxVersionA`、多 program/layer 与 AAC-LC 以外的 ASC 明确报错。
- 回归：`libavcodec/latm_test.mbt` 用 ffmpeg 把 AAC-LC 转封装为 LATM
  （`-c copy -f latm`），解析得到 2 字节 ASC（0x15 0x88）与 3 个 AU，
  解码 PCM 与 ffmpeg `-f loas` 解码差 ≤1 LSB。
- 集成：`libavformat/ts_latm_test.mbt` 用 `-c:a aac -f mpegts
  -mpegts_flags +latm` 生成真实 TS（stream type 0x11），把该 PID 的
  PES 负载拼接后交给 `latm_parse_stream`，解码 PCM 与 ffmpeg 解码 TS
  差 ≤1 LSB。注意：`-c copy` 从 ADTS 转 TS 即使带 +latm 也只改 PMT
  类型、负载仍是 ADTS，必须转码生成，测试因此用 lavfi 正弦 + 原生
  AAC 编码器。
- 全量 wasm **1306/1306**。

## 240. MPEG-TS SDT 服务描述

- `ts_muxer_new` 新增 `service_name`/`service_provider`（空串用
  mpegtsenc 默认 Service01/FFmpeg）、`service_type`（默认 0x01）、
  `transport_stream_id`（默认 1）与 `original_network_id`
  （默认 0xFF01）；PAT 的 tsid 改用该字段。
- 表输出顺序对齐 mpegtsenc：SDT(0x11) → PAT → PMT → PES；SDT 含
  表头、原始网络 ID、service_id=program_number、0xFC 标志、
  service_descriptor(0x48：service_type + 长度前缀 provider/name)
  与字节交换 CRC。
- 解复用：PID 0x11 的 PSI 段先缓存，包循环结束后应用到 program
  （service_id == program_number），因此 ffmpeg 那种 SDT 先于 PAT
  的顺序也能回读；`TsProgram` 新增 `service_name`/`service_provider`/
  `service_type`。
- 回归：`-metadata service_name=TestSvc -metadata
  service_provider=TestProv` 的 ffmpeg TS 中 SDT 段作为期望字节，
  本写入器输出逐字节一致；回读服务字段；默认值路径 Service01/FFmpeg。
  另修正 AC-3 A/V 测试里 PAT/PMT CRC 的绝对偏移（SDT 占首包）。
- 全量 wasm **1307/1307**。

## 241. HLS 主播放列表属性扩充

- `HlsMasterVariant` 新增 `average_bandwidth`、`closed_captions_group`：
  `#EXT-X-STREAM-INF` 按 mpegtsenc/hlsplaylist.c 顺序输出
  BANDWIDTH、AVERAGE-BANDWIDTH、RESOLUTION、CODECS、AUDIO、
  CLOSED-CAPTIONS、SUBTITLES；RESOLUTION 在宽高任一为 0 时省略，
  CODECS 为空时省略（与 `ff_hls_write_stream_info` 一致）。
- `HlsMasterRendition` 新增 `forced`/`instream_id`：SUBTITLES 渲染可写
  `FORCED=YES`；`TYPE=CLOSED-CAPTIONS` 按 hlsenc 写 GROUP-ID/NAME/
  LANGUAGE/INSTREAM-ID 且不带 URI（也不写 DEFAULT/AUTOSELECT）。
- 回归：文本属性顺序 + `av_hls_parse` 回读 `average_bandwidth`/
  `closed_captions`/`instream_id`/`forced`；0x0 分辨率与空 CODECS 的
  省略路径。
- 外部对照：ffmpeg 9.0.2 hlsenc 的双路 master 播放列表为
  `BANDWIDTH=...,AVERAGE-BANDWIDTH=...,RESOLUTION=...`，属性顺序与
  本写入器一致。
- 全量 wasm **1308/1308**。

## 242. MPEG-TS NIT 网络描述

- `ts_muxer_new` 新增 `nit`（默认关闭，对应 ffmpeg
  `-mpegts_flags +nit`）：表顺序变为 SDT → PAT → PMT → NIT；NIT
  （table_id 0x40）在 PID 0x10，network_name_descriptor(0x40) 用
  provider 名，transport_stream_loop 内 service_list_descriptor(0x41)
  列出 service_id=program_number 与 service_type，CRC 与 PAT/SDT 相同。
- 解复用：PID 0x10 的 PSI 段解析 network_name_descriptor 到
  `TsDemux.network_name`；SDT/PAT/NIT 共用段缓冲。
- 回归：ffmpeg `-mpegts_flags +nit -metadata service_provider=TestProv`
  产生的 37 字节 NIT 段作为期望字节，本写入器逐字节一致；回读
  `network_name` 与服务字段；未开 `nit` 时不存在 PID 0x10 包。
- 全量 wasm **1309/1309**。

## 243. MPEG-TS PMT 语言/HEVC registration 描述符

- `ts_muxer_new` 新增 `audio_language`（3 字母 ISO 639，非 3 字母或空
  不写）：音频 ES 写 ISO_639_LANGUAGE_DESCRIPTOR（0x0A，len 4，
  语言 + audio_type 0），PMT section_length/ES_info_length 动态计算。
- 视频流类型 0x24 时自动写 registration_descriptor（0x05 len 4
  "HEVC"），对应 mpegtsenc 在默认 strict compliance 下的行为。
- 回归：ffmpeg 9.0.2 用
  `-c:v libx264 -c:a aac -metadata:s:a:0 language=eng` 生成的 PMT
  （32 字节）和 libx265 单轨 TS 的 PMT（27 字节）作为期望字节，
  本写入器逐字节一致；解复用回读 `streams[1].language == "eng"`、
  `streams[0].registration == "HEVC"`；未设语言时仍为旧 23 字节 PMT。
- 全量 wasm **1310/1310**。

## 244. HLS EXT-X-START 与 FRAME-RATE/VIDEO 属性

- 媒体播放列表三个写入器新增 `start`：非空时在 PLAYLIST-TYPE 之后写
  `#EXT-X-START:<原样值>`（如 `TIME-OFFSET=6.0`），解析器回读
  `HlsPlaylist.start`。
- `HlsMasterVariant` 新增 `frame_rate`/`video_group`：
  `#EXT-X-STREAM-INF` 在 RESOLUTION 之后写 `FRAME-RATE=`，在 AUDIO
  之后写 `VIDEO="..."`；两者为空时省略。
- 回归：文本属性 + `av_hls_parse` 回读 `start`/`frame_rate`/`video`；
  默认值不产生多余标签。
- 全量 wasm **1311/1311**。

## 245. Matroska 字幕轨道元数据

- `mkv_subtitle_muxer_new` 新增 `language`（默认 "und"）、`name`、
  `default`、`forced`；TrackEntry 按 matroskaenc 写 Name(0x536E)、
  Language(0x22B59C)、FlagDefault(0x88，仅在 false 时写 0)、
  FlagForced(0x55AA，仅在 true 时写 1)。
- 回归：写入 `language="ger" name="Forced signs" default=false
  forced=true` 后由 `av_matroska_open` 回读四个字段；默认路径回到
  "und"/无 Name/FlagDefault=1/FlagForced=0。
- 全量 wasm **1312/1312**。

## 246. MP4 mdhd 轨道语言

- 读取端解析 `mdhd` 的 16 位 language（3×5-bit ISO 639-2/T，bit15 为
  pad，0 组回落 "und"），写入 `Mp4Track.language`（默认 "und"）。
- `mp4_av_muxer_new` 新增 `video_language`/`audio_language`：
  `mp4_language_code` 按 movenc 方式打包（"eng" → 0x15C7），
  非 3 字母/非 a-z 回落 "und"（0x55C4）；`mp4_av_trak` 新增 language
  参数，字幕轨固定 0x55C4。
- 回归：双轨写入 `video_language="und" audio_language="eng"` 后
  `av_mov_open` 回读 und/eng，并直接检查两个 `mdhd` 的 language 字节
  为 `55 C4` / `15 C7`（与 ffmpeg movenc 生成的 MP4 一致）；非法码
  回落 "und"。
- 全量 wasm **1313/1313**。

## 247. Matroska A/V 轨道语言

- `mkv_av_muxer_new` 新增 `video_language`/`audio_language`/
  `subtitle_language`（默认 "und"）与 `subtitle_forced`；TrackEntry
  按 matroskaenc 为三条轨都写 Language(0x22B59C)，字幕轨在
  `subtitle_forced` 时写 FlagForced(0x55AA, 1)。
- 回归：`video_language="und" audio_language="eng"
  subtitle_language="fre" subtitle_forced=true` 写入后
  `av_matroska_open` 回读三条轨语言与字幕 flag_forced=1。
- 全量 wasm **1314/1314**。

## 248. WebM A/V 轨道语言

- `webm_av_muxer_new` 新增 `video_language`/`audio_language`
  （空串回落 "und"）；VP9/Opus 两条 TrackEntry 在 TrackType 之后写
  Language(0x22B59C)，与 ffmpeg matroskaenc 一致。
- 回归：`audio_language="spa"` 写入后 `av_matroska_open` 回读
  und/spa；空串回落 "und"。
- 全量 wasm **1315/1315**。

## 249. DASH AudioChannelConfiguration

- 三种 MPD 写入器（SegmentTemplate/SegmentList/SegmentBase）的
  Adaptation 新增 `channels`；音频且 channels>0 时在 Representation
  内写
  `<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="N" />`
  （scheme 与 ffmpeg dashenc 一致；SegmentTemplate 的 Representation
  由自闭合改为带子元素）。
- 回归：6 声道音频在三种写入器中都产出该元素，默认 0 时省略；
  MPD 仍可被 `av_dash_parse` 解析。
- 全量 wasm **1316/1316**。

## 250. MJPEG 12-bit 无损灰度

- SOF3 单分量允许 10..16-bit 精度（9-bit 仍仅限 1x1 RGB→bgr24）。
  `mjpeg_decode_scan_lossless` 增加 `bits_per_sample`：初始预测改为
  `1 << (bits-1)`（JPEG 无损规范的首样本预测 2^(P-1)），掩码/回写
  使用 `((1<<P)-1)`；`mjpeg_finalize_lossless` 在 >8-bit 时输出
  MSB 对齐的 16-bit 小端平面，pix_fmt 为 gray16le。
- 夹具：手工构造 12-bit SOF3 单分量灰度 JPEG（16x8，categories 0..12
  的 4-bit DC 表，predictor 1），ffmpeg 9.0.2 `-pix_fmt gray16le`
  解出的 256 字节作为参考；本解码器输出逐字节一致。
- 回归：`libavcodec/mjpeg_gray12_realdata_test.mbt`。
- 说明：12-bit 的基线/渐进（SOF0/2）与多分量无损仍不支持（前者需要
  按位深缩放的 IDCT 与不同的输出格式），residual 的 9-bit 单分量路径
  也仍按旧行为拒绝（已有测试锁定）。
- 全量 wasm **1317/1317**。

## 251. HLS EXT-X-I-FRAME-STREAM-INF

- `HlsMasterVariant` 新增 `is_i_frame`：为 true 时写单行
  `#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=...,[AVERAGE-BANDWIDTH,]
  [RESOLUTION,][FRAME-RATE,][CODECS,][VIDEO,]URI="..."`，不写
  AUDIO/SUBTITLES/CLOSED-CAPTIONS，也不再输出 URI 单独一行。
- 回归：文本单行 + `av_hls_parse` 回读 `is_i_frame`/bandwidth/uri/
  frame_rate；普通变体仍走两行 `EXT-X-STREAM-INF`。
- 全量 wasm **1318/1318**。

## 252. MPEG-TS 纯音频 PCR

- `write_audio` 在 `has_video == false` 时把该 PES 的 PTS 作为 PCR
  写入音频 PID（有视频时仍只在视频 PES 写 PCR，对应 mpegtsenc 的
  单一 PCR PID）；每个音频 PES 都带 PCR，远高于每 100 ms 一次的
  最低要求。
- 回归：audio-only TS 的 `demux.pcrs` 两条记录分别对应 90000/
  90930，PID 为音频 0x101；A/V 路径的 PCR 行为不变。
- 全量 wasm **1319/1319**。

## 253. fMP4 初始化段轨道语言

- `fmp4_trak`/`fmp4_init_segment` 增加 language 参数；
  8 个单轨 `fmp4_*_init_segment` 新增 `language~`（默认 "und"），
  `fmp4_av_init_segment` 新增 `video_language`/`audio_language`，
  均经 `mp4_language_code` 打包进 mdhd。
- 回归：H.264 init `language="eng"` 经 `av_mov_open` 回读 "eng"；
  A/V init（und + spa）回读两轨语言。
- 全量 wasm **1320/1320**。

## 254. 单轨 MP4 写入器轨道语言

- `mp4_audio_muxer_new`（AAC/AC-3/E-AC-3 共用）、
  `mp4_aac_muxer_new`、`mp4_mp3_muxer_new`、`mp4_flac_muxer_new`、
  `mp4_alac_muxer_new`、`mp4_opus_muxer_new`、
  `mp4_h264_muxer_new`/`mp4_hevc_muxer_new`/`mp4_av1_muxer_new`/
  `mp4_vp9_muxer_new`、`mp4_subtitle_muxer_new` 均新增
  `language~ : String = "und"`，经 `mp4_language_code` 写入 mdhd；
  共享的 `mp4_audio_document`/`mp4_video_document`/
  `mp4_subtitle_document` 增加 language 参数（仍在 mp4_still_mux 中固定
  0x55C4）。
- 回归：AAC/MP3/Opus/mov_text/H.264 单轨写入 eng/fra/deu/spa/jpn 后
  `av_mov_open` 回读正确。
- 全量 wasm **1325/1325**。

## 255. DASH 多 Period SegmentTemplate

- 抽取 `dash_write_template_adaptations` 复用 AdaptationSet 输出；
  新增 `DashPeriodSpec { id, start_seconds, duration_seconds, adaptations }`
  与 `dash_write_segment_template_mpd_periods`：每个 Period 输出
  `<Period id start [duration]>`（duration>0 时显式写），
  SegmentTemplate 各 Period 独立展开。
- 回归：两个各 2 s 的 Period（第二个 startNumber=3），
  `av_dash_parse` 得到 2 个 period、duration 2/2、每 period 两个
  分段，第二 period URI 从 p1-00003 递增。
- 全量 wasm **1326/1326**。

## 256. FLAC 编码器按真实位数选阶

- `flac_plan_subframe` 不再"单分区代价预选 + 仅对胜者做分区搜索"：
  固定预测 0-4 阶与 LPC 1-12 阶的每个候选都用
  `flac_choose_partitioned_rice`（分区阶 0-8 + escape）计算真实子帧
  位数，与 verbatim 一起取最小。
- 全部既有 FLAC 编码回归（8/12/16/20/24/32-bit、1-8 声道、33-bit
  侧声道、分区非平稳夹具、Rice escape）解码逐字节一致；非平稳夹具
  的首分区阶仍 >0。
- 全量 wasm **1326/1326**。

## 257. HLS 分段级 EXT-X-MAP 覆盖

- `HlsMediaSegmentSpec` 新增 `map_uri`/`map_byterange`：分段可指定新的
  初始化段；写入器在 discontinuity/gap 之后、EXTINF 之前按需插入
  `EXT-X-MAP`，并与当前生效的 map（URI+BYTERANGE）比较，相同则只写
  一次。全局 map 由 `map_uri`/`map_byterange` 参数提供。
- 解析器本就把 MAP 作为 maps 数组并记录 segment.map_index；回归验证
  两个 init 段、后两分段共享第二个 map、BYTERANGE 20@0 回读。
- 全量 wasm **1327/1327**。

## 258. WAV Broadcast Wave `bext` 读写

- 新增 `libavformat/wav_bext.mbt`：`WavBext`（description/originator/
  originator_reference/origination_date/origination_time/
  time_reference/umid/coding_history）、`wav_read_bext` 扫描
  RIFF/RF64 块，以及按 `bwf_write_bext_chunk` 生成 chunk（version 1、
  UMID 64 字节、190 保留字节、coding history 以 NUL 结尾）。
- `wav_muxer_new` 新增 `bext` 参数，chunk 插在 fmt 之后并计入 RIFF/
  RF64 尺寸；RIFX 与 bext 互斥（bext 为小端）。
- 回归：以 ffmpeg 9.0.2 `-write_bext 1` + 同一组元数据（description/
  originator/originator_reference/origination_date/origination_time/
  time_reference=123456789/coding_history）写出的 636 字节 bext 段为
  期望，本写入器输出逐字节一致；`wav_read_bext` 回读全部字段；无 bext
  文件返回 None。
- 全量 wasm **1328/1328**。

## 259. HLS 分段级 EXT-X-KEY 轮换

- `HlsMediaSegmentSpec` 新增 `key_uri`/`key_iv`/`key_none`：
  分段可切换 AES-128 密钥（IV 变化也会触发），或用 `key_none` 写
  `#EXT-X-KEY:METHOD=NONE` 清空当前密钥；与当前生效密钥比较后去重。
  全局密钥仍由 `key_uri`/`key_iv` 参数提供。
- 抽取 `hls_mux_key_tag`/`hls_mux_same_iv`，全局与分段共用。
- 回归：全局 key0 → 分段切 key1（带 IV）→ 继承 → METHOD=NONE →
  重新选 key0；`av_hls_parse` 得到 keys.length=3、segment.key_index
  [0,1,1,-1,2]。
- 全量 wasm **1329/1329**。

## 260. MJPEG 12-bit 基线/渐进灰度

- SOF0/1/2 单分量允许 10/12-bit（多分量 >9-bit 仍拒绝；9-bit 仅限
  lossless RGB）。`mjpeg_idct_put` 按精度选择已有的
  `ff_simple_idct_put_int16_{8,10,12}bit`；`mjpeg_finalize_image`
  >8-bit 时输出 MSB 对齐 16-bit 小端平面（gray16le），并据此选择
  pix_fmt。
- DC 预测初值改为按精度 `4 << bits`（存为 `dc_init`，restart 复位
  也用它）；之前硬编码 `4 << 8` 会让 12-bit 图像整体偏移
  (2048-128)<<4。
- 夹具：手工构造 12-bit SOF0 单分量灰度 JPEG（16x8，DQT 全 1、标准
  DC 表 + 仅 EOB 的 AC 表），ffmpeg `-pix_fmt gray16le` 的 256 字节
  为参考，本解码器逐字节一致。
- 全量 wasm **1330/1330**。

## 261. MJPEG 10/12-bit YCbCr

- 基线/渐进 3 分量在 10/12-bit、无 Adobe RGB 变换、且采样为
  4:2:0/4:2:2/4:4:0/4:4:4（亮度取 max_h/max_v、两路色度 1x1，无需
  上采样）时放行；finalize 输出 MSB 对齐 16-bit LE 平面，pix_fmt 为
  yuv420p16le/yuv422p16le/yuv440p16le/yuv444p16le。
- 夹具：手工 12-bit SOF0 4:2:0 JPEG（16x16、一个 MCU、Y 四块偏移
  0/8/4/12、色度中性），ffmpeg `-pix_fmt yuv420p16le` 的 768 字节
  参考；Y/Cb/Cr 三平面逐字节一致。
- 不支持：需要色度上采样的 4:2:2/4:1:1 变体、4 分量 >8-bit、
  Adobe RGB（transform 0）10/12-bit。
- 全量 wasm **1331/1331**。

## 262. 单轨 Matroska/WebM 轨道语言

- `webm_vp9_muxer_new`、`webm_av1_muxer_new`、`webm_opus_muxer_new`、
  `mkv_h264_muxer_new`、`mkv_hevc_muxer_new` 新增
  `language~ : String = "und"`（空串回落 und），TrackEntry 在
  TrackType 后写 Language(0x22B59C)。
- 回归：VP9/AV1/Opus/H.264/HEVC 单轨分别写 spa/fra/deu/jpn/ita 后
  `av_matroska_open` 回读正确；未指定时为 "und"。
- 全量 wasm **1336/1336**。

## 263. MKV 音频单轨轨道语言

- `mkv_audio_document` 增加 `language` 参数；`mkv_aac_muxer_new`、
  `mkv_flac_muxer_new`、`mkv_ac3_muxer_new`/`mkv_eac3_muxer_new`/
  `mkv_mp3_muxer_new` 与 `mkv_alac_muxer_new` 新增
  `language~ : String = "und"`，TrackEntry 写 Language(0x22B59C)。
- 回归：AAC/FLAC/ALAC 既有回读测试分别用 eng/fra/deu 并断言
  `track.language`（AC-3/E-AC-3/MP3 走同一 document 路径）。
- 全量 wasm **1336/1336**。

## 264. MJPEG 9-bit 基线灰度

- 基线/渐进单分量精度放宽到 9-bit；9 与 10-bit 都使用已有的
  `ff_simple_idct_put_int16_10bit`（FFmpeg 的 idctdsp 对 9/10-bit
  做同样选择），输出仍为 MSB 对齐 gray16le（shift 7）。
- 夹具：手工 9-bit SOF0 灰度 JPEG（16x16、DQT 全 1、DC-only 块），
  ffmpeg `-pix_fmt gray16le` 的 512 字节参考逐字节一致。
- 9-bit lossless 单分量仍按旧测试拒绝（仅 9-bit 1x1 RGB 无损支持）。
- 全量 wasm **1337/1337**。

## 265. VP9 复合预测确定性对拍

- 解码器代码无改动（复合预测 fix/var 双参考选择、comp 上下文树与计数
  此前已实现）；本轮为上次遗留的「P 帧像素不一致」定位根因，结果在
  最小确定性样本上不复现，偏差为测试向量构造错误而非实现缺陷。
- 新增 `libavcodec/vp9_realdata_vectors/`（独立子包，规避 libavcodec
  主包 wasm `__init` 局部变量上限）：手工构造 3 帧 64x64 确定性流
  （f0 全 128 关键帧；f1 四个 32x32 intra，V 无上→DC_127、H 无左→
  DC_129，刷新 GOLD；f2 64x64 ZEROMV compound，fix=LAST=f0、
  var=GOLD=f1，输出上 (128+127+1)>>1=128、下 129，与任一单参考拷贝
  均不同）逐帧 MD5 对齐 ffmpeg 9.0.2；另固化 libvpx 真实三会话
  （128x96 crf30 / 64x48 crf8 / 96x64 crf40+alt-ref 隐藏帧，共 34
  显示帧，含非 frame-parallel 反向适配）framemd5 逐帧一致。向量由
  `scripts/emit_vp9_realdata_vectors.py` 生成。
- 构造流的两个根因（记录供后续向量参考）：compressed header 的
  comp_pred_mode 两位必须紧跟 intra 4 个 update flag 之后（FFmpeg
  vp9.c:1013-1016 读取顺序），写在全部 252-prob flag 之后会被当作
  single-ref；禁用概率反向适配须用 refreshctx=0 + parallelmode=1
  （vp9.c:1792 门控 `refreshctx && !parallelmode`），errorres=1 会把
  sign_bias 强制清零（vp9.c:601-605）导致 compound 不可用。
- 全量 wasm **1341/1341**（含新增 4 个测试）。

## 266. Opus hybrid 端到端 PCM 容差对拍

- 此前 hybrid 只有 CELT 系数 MD5（语法/能量/分配/PVQ/反归一化），
  最终 PCM 缺端到端证据。CELT 合成按 `opus_celt_pcm_reference.c`
  约定只保证 3e-6 容差，因此不能用 framemd5 逐位对拍。
- 用 `--disable-asm` 的 FFmpeg 9.0.2 解码 hyb_fb20 / hyb_st20，导出
  f32le 参考帧（含 312 样本 pre-skip、Ogg 末端裁剪与 resampler
  drain），重建后的 framemd5 与仓库既有 `hyb_*.md5` 逐行一致（151/151、
  101/101），确认参考同源。
- 新增 `libavcodec/opus_hybrid_pcm_vectors/` 独立子包（规避 libavcodec
  主包 wasm `__init` 局部变量上限）：保存两条流、OpusHead 与所选参考
  帧，黑盒测试 `OpusDecoder`→`decode_packet`→drain→末端裁剪后逐样本
  比较。实测最大误差 hyb_fb20 1.49e-8、hyb_st20 7.45e-9，均在 3e-6
  容差内。
- `scripts/emit_opus_silk_vectors.py` 增加 assembly-free 参考目录参数与
  hybrid PCM 输出；新增数据为 `pub let`，重新生成后既有
  `opus_silk_realdata_wbtest.mbt` 零 diff。
- 清理 `opus_decode.mbt` 遗留的 hybrid CELT PCM `println` 调试输出。
- 全量 wasm **1342/1342**。

## 267. 2026-11-02 剩余缺口收尾：Opus PLC 判定、AAC SBR 边界、HEVC 交叉验证、容器边界

- **Opus PLC：判定不做，与上游一致**。FFmpeg 9.0.2 的原生 Opus 解码器
  `libavcodec/opus/dec.c` 不包含包丢失隐藏：NULL packet 只在
  `flush_needed` 时刷新 resampler，否则直接 `return 0`，不生成补偿帧；
  PLC 仅存在于 `libavcodec/libopusdec.c` 的 libopus 封装路径，不属于本移植
  范围。因此本移植的丢包行为与原生 FFmpeg 对齐（不合成 fade-in 补偿帧），
  不另造 PLC 实现。README 已同步更新该判定。
- **AAC SBR/PS 边界**：新增 `libavcodec/aac_sbr_extra_vectors/` 独立子包
  （规避 libavcodec 主包 wasm `__init` 局部变量上限），由
  `_build/gen_sbr_extra.py`（临时脚本，未提交）用 libfdk_aac 生成三条真实
  ADTS 流：32 kHz stereo HE-AAC、44.1 kHz stereo HE-AAC、44.1 kHz
  HE-AACv2（PS）。黑盒测试经新增的
  `new_aac_decoder_from_adts` 逐帧解码，与 FFmpeg 9.0.2 `s16le` 参考比较，
  判据 `max_diff ≤ 1`、逐位相等样本 ≥99.9%，3/3 通过。
  SBR 的 `bs_sbr_crc_bits` 与上游 `aacsbr_template.c` 的 TODO 一致地跳过
  校验，已在 README 标注，不额外实现 CRC。发现并记录 5.1 HE-AAC（6 声道）
  的 SBR 未生效（每帧 1024 而非 2048 样本），本轮未硬造参考、不列为通过项。
- **HEVC tiles+WPP 第二参考源**：libde265 1.1.3（master 构建，
  `/tmp/libde265-master/build/dec265/dec265`）独立解码 6 组夹具。
  `htw_slices8` 的 5 帧 MD5 与 HM 参考逐帧完全一致（5/5，如首帧
  `edd6e2b9b0a84018a188cbb0b167bd92`），可作为该组合的第二参考；
  其余 5 组 libde265 不报错但输出与 HM 不同（htw8 首帧
  `1797123e6242face9761ac21b0d78e92` vs HM
  `017ebde8e1d4351a4edaffa083ccfbec`），说明其 HT 扩展解码路径与 HM
  不一致。FFmpeg 9.0.2 对这些流同样不报错，htw8 首帧为
  `82f46e8be06b529e60fb805c01a65d3c`，属静默错误解码；因此这 5 组仍只能
  以 HM 输出为准。结论与命令已写入 README。
- **容器读取层边界**：
  - HLS：两条 `EXT-X-KEY` 的 AES-128 密钥轮换（首段隐式 IV 由
    `EXT-X-MEDIA-SEQUENCE` 推出、次段显式 `IV`，密钥各取一次并缓存）；
    无 `EXT-X-ENDLIST` 的活动列表首次 fetch 失败后游标不回退、重试返回
    同一段，耗尽返回 `None`。两个用例在 `segment_io_wbtest.mbt`。
  - DASH：多 Period（各自 BaseURL、`start`/`duration`、`startNumber` 编号）；
    `SegmentBase indexRange` 的 `index_range`/`initialization_range` 解析；
    显式重叠 Period 只解析不拒绝（与文件头声明的范围一致）。
  - fMP4：单 `traf` 多 `trun`。修复 `mov.mbt` 缺失的
    `frag_stream_info->next_trun_dts` 语义（每个 `tfhd` 重置，上一 `trun` 结束
    后 `next_trun_dts = dts + time_offset`），第二 `trun` 的 DTS 从上一段续接
    而不是重启于 `tfdt`；用例同时校验按 `trun` 串联的显式数据偏移。
  - WAV：零长度未知 chunk（`JUNK` size=0，无 pad）不移动 `data_offset`；
    声明长度为 0 的 `data` chunk 按上游语义视为“未知长度”，修复 `wav.mbt`
    在此处停止 chunk 扫描（对应 `wavdec.c` 的 `goto break_loop`），后续
    `data` 头并入 payload。
- 验证：`moon test --target wasm --no-render -q` 全量 **1350/1350** 通过；
  新增用例覆盖 `libavformat` 的 HLS×2、DASH×3、fMP4×1、WAV×2，以及
  `aac_sbr_extra_vectors`×1（内含 3 条流）。

## 268. 多声道 HE-AAC：每个元素的 SBR 上下文与逐声道低带

- **根因（两个叠加）**：
  1. `aacdec.c` 的 `che_configure` 会为**每个** channel element
     （SCE/CPE/LFE）调用 `ff_aac_sbr_ctx_alloc_init`，此后
     `ff_aac_sbr_apply` 对所有元素执行 QMF 分析/合成；即使 LFE 在 HE-AAC
     里没有 SBR 负载（`start == 0`），也会被纯上采样到 2048。本移植的
     `apply_sbr` 只对“已有 SBR payload”的元素建 state，缺 state 的元素
     直接 `continue`，于是 5.1/7.1 的 LFE 停留在 1024 样本。
  2. `sbr_lf_gen` 在 C 里按声道传入 `sbr->data[ch].W`；本移植的
     `AacSbr::lf_gen` 硬编码 `data[0].w_buf`，导致 CPE 右声道的低带
     `X_low` 取自左声道。单声道与 PS 流看不出问题，多声道下右声道几乎
     退化为左声道（左/右差异被压到 ~1e-4 均方量级）。
- **修复**：
  - `aac_sbr_entry.mbt` 的 `apply_sbr`：缺失 state 时按
    `aac_new_sbr(layout[i].element_type)` 建立上下文（`kx=32, m=0`），
    LFE 因此走 `AacSbr::apply` 的 `start == false` 纯上采样路径。
  - `aac_sbr_dsp.mbt`：`lf_gen` 增加 `ch` 参数并使用
    `self.data[ch].w_buf`，`apply` 按 `self.lf_gen(ch, d.ypos, ...)` 调用，
    与 C 的逐声道 `W` 语义一致。
- **验证**：新增 `aac_sbr_extra_vectors/multichannel_vectors.mbt` 与
  `multichannel_test.mbt`，三组 libfdk_aac（`-profile:a aac_he`，48 kHz）
  真实 ADTS 流各 3 帧，参考为 FFmpeg 9.0.2 解码的 s16le：
  - 5.1（6ch，含 LFE）：frames=3，6144 样本/声道，`max_diff=1`，
    逐位相等 99.9485%；
  - 5.0（5ch，无 LFE）：frames=3，6144 样本/声道，`max_diff=1`，
    99.9544%；
  - 7.1 wide（8ch）：frames=3，6144 样本/声道，`max_diff=1`，
    99.9878%。
  libfdk_aac 不支持 2.1 布局（支持列表为 mono/stereo/3.0/4.0/5.0/5.1/
  6.1(back)/7.1(wide)/7.1/5.1.2(back)），因此 2.1 无法生成真实流；6.1 与
  5.1.2 未加用例。修复按 element 类型与 `data[ch]` 通用，不依赖声道数。
- 验证：`moon test --target wasm --no-render -q` 全量 **1351/1351** 通过。

## 269. 6.1(back)/5.1.2(back) HE-AAC 用例与全清单残余扫描

### 6.1(back)/5.1.2(back)：ADTS 不可达，改用 MP4 ASC + 原始 AU

- libfdk_aac 本身能编码这两种布局（`aformat=channel_layouts=6.1(back)`
  / `5.1.2(back)`、`-profile:a aac_he`、48 kHz，编码成功），但 **ADTS 无法
  表达**：
  - ADTS 固定头只有 3 bit `channel_configuration`，而 ASC 里是 4 bit；
    `libavformat/adtsenc.c` 用 `put_bits(&pb, 3, ctx->channel_conf)` 写头，
    ASC 的 11/14 被截断成 3/6，且仅当 `channel_conf == 0` 才写 PCE。
  - 实测 `ffprobe -f aac` 把 6.1(back) 报成 `3.0`、5.1.2(back) 报成
    `5.1`，并报 `channel element 1.1/1.2 is not allocated`；FFmpeg 解码
    该 ADTS 输出 0 字节（`Could not open encoder before EOF`）。原生
    `aac` 编码器与 `-flags +global_header` 同样失败。
- 替代验证：以 MP4 封装（ASC 保留 4-bit 11/14）编码同源流，用
  `ffmpeg -map 0:a -c copy -f data` 抽出原始 AU，按 ffprobe 包大小切成
  3 帧；参考 PCM 用 FFmpeg 原生解码器 `-ignore_editlist 1` 输出 s16le
  的前 3 帧（该模式输出正好是逐帧 2048 样本的拼接，不含 priming 裁剪）。
- 解码端新增 `libavcodec/aac_config.mbt` 的
  `new_aac_decoder_from_asc_channels`：接受完整 4-bit ASC 通道配置
  （1–7、11–14），核心采样率/索引由调用方给出，SBR/PS 仍走隐式检测，
  与 ADTS 路径语义一致。
- 新增 `aac_sbr_extra_vectors/multichannel_asc_vectors.mbt` 与
  `multichannel_asc_test.mbt`（沿用 `sbr_extra_match` 的 `max_diff ≤ 1`、
  逐位相等 ≥99.9%、每帧每声道 2048 样本口径）：
  - 6.1(back)：7ch，3 帧 597/846/671 字节，6144 样本/声道，
    `max_diff=1`，逐位相等 **99.99535%**；
  - 5.1.2(back)：8ch，3 帧 682/899/721 字节，6144 样本/声道，
    `max_diff=1`，逐位相等 **99.99797%**。
- libfdk_aac 不支持 2.1（其布局列表为 mono/stereo/3.0/4.0/5.0/5.1/
  6.1(back)/7.1(wide)/7.1/5.1.2(back)），2.1 仍无真实夹具。

### 全清单残余扫描：无同类“缺 ctx 元素 / 按声道硬编码”残留

- 扫描模式（非测试源码全量 grep）：`data[0]`、`layout[0]`、
  `channels[0]`、`ch_layout[0]`、`ch == 0`、`element_channel == 0`，
  并复核 `stub`/`TODO`/`FIXME`/`unimplemented`/`panic`/`abort`/
  `identity`/`placeholder`/`ignore(...)`。
- 音频：
  - AAC SBR（`aac_sbr.mbt`/`aac_sbr_dsp.mbt`/`aac_sbr_entry.mbt`）：
    余下的 `data[0]` 仅用于 CPE 共享元数据（grid、`bs_amp_res`、
    coupling、`bs_num_env` 等），与 `aacsbr_template.c` 一致；逐声道
    路径均用 `data[ch]` 或显式处理 `data[0]/data[1]`。`dequant` 同时写
    `data[0]` 与 `data[1]` 的 `env_facs_q/hi`。
  - `aac_mdct.mbt` 的 `layout[0].element_type == 1` 是 dual-mono SCE
    判定的上游语义；`aac_layout.mbt` 的 `element_channel == 0` 是元素
    内声道掩码排序；`aac_ps.mbt`/`aac_coupling.mbt` 无声道 0 硬编码。
  - MPEG Layer I/II：`mpa_layer12.mbt` 按 `ch` 循环，未假设单声道。
  - Opus：CELT 的 1/2 声道守卫对应 C 的 per-stream 限制；多流由
    `opus_head` 拆分后逐流解码，无 `channels[0]` 假设。
  - FLAC/ALAC/AC-3/E-AC-3/TTA/WavPack/G.71x/aptX/WMA/Vorbis：声道循环
    均按 `frame.channels`/`nb_channels`；AC-3 的 `ch == 0` 是耦合通道
    内部槽位，与 `ac3dec.c` 一致；aptX 的 `channels[0/1]` 是其规范固定
    双声道；未发现单声道或单元素硬编码。
- 视频：
  - H.264 的 `plane_count` 仅 4:4:4 用 3 平面，`h264_deblock` 的
    `ignore(bit_depth)` 与上游一致（阈值已含位深）；多 slice 由
    `h264_decode_picture_ex` 的 `slices` 循环处理，`first_mb_in_slice`
    检查仅用于“首 slice 非 0 且未开错误掩盖”的场景。
  - HEVC 的 `hevc_intra_decoder_new` 是逐 slice 子解码器，tiles/WPP/
    dependent slice 在 `hevc_inter_decode.mbt` 的 `HevcDecoder` 路径处理；
    子解码器自身对多 slice/非 I slice 的 PATCHWELCOME 是刻意边界。
  - AV1/VP9 的关键帧/帧间路径无 `data[0]`/`layout[0]`/`ch == 0` 硬编码；
    `av1_decode.mbt` 的 `ignore(av1_ymode_size_context[0])` 只是表引用，
    该表在 `av1_decode4.mbt`/`av1_inter_syntax.mbt` 中使用。
  - MJPEG/PNG 按组件数生成平面（灰度 1 平面、CMYK 4 平面），
    `png_apng.mbt` 的 `ignore(previous)` 只是丢弃已用完的变量。
- 容器：`codec_parameters.mbt`/`extradata.mbt` 的 AAC 通道配置走 4-bit
  ASC（11/12/13/14 已在 `aac_height_realdata_wbtest.mbt` 覆盖）；
  `wav.mbt` 从 fmt 的 `channels` 读取，无固定 2 声道假设；
  MP4/MKV/TS/HLS/DASH/WebM 仅转发 codecpar，无声道硬编码。
- 结论：除已修复的两个 SBR 缺陷外，未发现同类“缺失 ctx 元素”或
  “按声道/元素 0 硬编码”的残留；扫描中出现的 `panic`/`abort` 均为
  初始化不变量，`TODO` 仅为与上游一致的 SBR CRC 跳过。
- 验证：`moon check --target wasm --deny-warn` 通过；全量
  `moon test --target wasm --no-render -q` **1352/1352** 通过。

## 270. 统一格式探测层（format.c 的 av_probe_input_format）

- **缺口**：清单里的 MP4/MKV/TS/HLS/DASH/WebM/WAV 与裸 ADTS/MP3/Ogg/FLAC/
  AIFF/AU/CAF/ASF/W64/AC-3 此前各有独立入口（`av_mov_open`、
  `av_matroska_open`、`av_mpegts_open` …），调用方必须自己判断字节是什么
  格式。README 的“容器统一播放 API”因此列为待立项项。本轮补上它的第一层：
  FFmpeg `libavformat/format.c` 的探测入口。
- **移植内容**（新文件 `probe.mbt` / `probe_container.mbt` /
  `probe_elementary.mbt`）：
  - `av_probe_input_format3` 的完整语义：ID3v2 跳过与 `nodat` 四态、
    `read_probe` 打分、扩展名提分（`1` / `EXTENSION/2-1` / `EXTENSION`）、
    MIME bonus（+30 封顶 100）、同分时清空结果（`fmt = NULL`）。
  - `av_probe_input_format2`（`score <= AVPROBE_SCORE_RETRY` 丢弃）与
    `av_probe_input_buffer2`（从 `PROBE_BUF_MIN`=2048 倍增到
    `PROBE_BUF_MAX`=1 MB，EOF 时不报错、探测后回退游标）。
  - 20 个 `read_probe`：`mov_probe`（含 moov→mhlr/MPEG 的 MPEG-PS 降级）、
    `matroska_probe`（DocType 表 + 未知长度头）、`analyze` +
    `mpegts_probe`（188/192/204 三种布局与 `CHECK_COUNT` 组合公式）、
    `wav_probe`/`w64_probe`、`hls_probe`（MIME/扩展名/URL query 三重门）、
    `dash_probe`、`adts_aac_probe`、`ac3_eac3_probe`、`aiff_probe`、
    `amr_probe`、`png_probe`、`asf_probe`、`au_probe`、`caf_probe`、
    `flac_probe` + `raw_flac_probe`、`mjpeg_probe`、`mp3_read_probe`、
    `ogg_probe`。
  - 注册表顺序按 `allformats.c` 的声明顺序（aac→ac3→aiff→amr→png→asf→au→
    caf→dash→eac3→flac→hls→matroska→mjpeg→mov→mp3→mpegts→ogg→w64→wav）。
- **关键坑**：
  1. **AC-3 的 CRC 是 `AV_CRC_16_ANSI` = (le=0, poly=0x8005)**，不是
     `AV_CRC_16_ANSI_LE`（le=1, 0xA001）。用后者算出的 CRC 永不为 0，
     `ac3_eac3_probe` 恒返回 0，raw AC-3 只能靠扩展名认出来。
  2. **`mp3_read_probe` 的 free-format 帧 `frame_size == 0`**：C 用
     `avpriv_mpegaudio_decode_header` 的返回值 `!= 0` 直接 break；本移植的
     `Ok(free_format: true, frame_size: 0)` 若不判断，`buf2 += 0` 会让探测
     死循环（实测 wasm 测试卡死 5 分钟无输出）。
  3. **AIFF 的 `FORM` 是 `AV_RL32`**：直接比 `0x4D524F46`（M,R,O,F 的 LE
     拼法），写成 `0x4D46524F` 会永不匹配；用 `pb_match(buf, 0, "FORM")`
     更不容易错。
  4. **单个 JPEG 文件 ffprobe 报的是 `jpeg_pipe` 而不是 `mjpeg`**：两个
     demuxer 共用 `mjpeg_probe`，实测胜出的是 `jpeg_pipe`，`FormatId::name`
     按实测结果命名。
  5. **ID3 超过探测窗时 C 不移动 `buf`**：`av_probe_input_format3` 在
     `ID3_GREATER_PROBE` 分支只 break、不前进指针，所以 `mp3_read_probe`
     仍然看到 ID3 头，返回 `EXTENSION/4`=12（不是 0），扩展名再抬到 24。
  6. **越界读按 0**：C 依赖 `AVPROBE_PADDING_SIZE` 的零填充，本移植的
     `pb_u8`/`pb_byte` 对越界下标返回 0，语义等价且不会越界。
  7. **注册表是 FFmpeg 的子集**：仅在 FFmpeg 有、本移植没有的 demuxer 参与
     同分时结果才会不同；已实现的格式两两之间没有观察到的分歧（17 组真实
     文件实测）。
- **验证**：`scripts/gen_probe_fixtures.sh` 用 `linuxserver/ffmpeg:9.0.2`
  生成 17 个真实文件（mp4/mkv/webm/ts/wav/flac/mp3/adts/ogg/ac3/aiff/au/
  caf/asf/w64/png/mjpeg），期望值取 `ffprobe -show_entries
  format=format_name`；`scripts/gen_probe_vectors.py` 把它们截成探测窗口并
  写成 `libavformat/probe_vectors/fixtures.mbt` 的 base64 常量。测试 12 项：
  17 组格式名、常量路径分数（mp4 100 / wav 99 / png 99 / ogg 100 …）、
  帧计数分数（aac 51 / mp3 52 / ac3 52 / mjpeg 12 / ts 100）、HLS 三重门、
  DASH profile、ID3 降级、MIME bonus、空/随机/扩展名单项、以及
  `av_probe_input_buffer` 的正常、短源（EOF）、空源三种路径。
- 验证：`moon check --target wasm --deny-warn` 通过；全量
  `moon test --target wasm --no-render -q` **1364/1364** 通过。

## 271. 音频编码第一步：可行性盘点与 AAC-LC 编码（第 2 阶段）

- **盘点结论**（C 侧规模 / MoonBit 依赖 / 可复用度）：
  - AAC-LC：`libavcodec/aacenc.c` 1201 行、`aacenc_tns.c` 521、
    `aacenc_is.c` 153、`aaccoder.c` 804、`aaccoder_twoloop.h` 718、
    `aacpsy.c` 904、`psymodel.c` 74、`aacenctab.c` 391。依赖 MDCT（已有）、
    量化与熵编码（BitWriter + 码本表已有）、心理声学模型（尚无）与两遍
    码率控制（尚无）；ADTS 封装和 swb 表可直接复用解码侧。是三者的最短
    路径，故排第一。
  - MP3：`mpegaudioenc.c` 约 739 行 + `mpegaudioenc_template.c` /
    `_float.c` / `_fixed.c`，另需 `mpegaudiodata.c`（码本/表）与
    `mpegaudiodsp.c`（32 点子带合成滤波）。走 `libmp3lame` 需引入外部 C
    依赖，不符合本仓库纯移植约定，应改用 FFmpeg 原生 mpegaudioenc。主要
    新增是 32 点分析与心理声学（部分表可与 AAC 共享），工作量中等。
  - Opus：`libavcodec/opus/enc.c` 646 行、`enc_psy.c` 546 行，另需编码侧的
    CELT/PVQ 搜索、SILK LSF 量化与模式决策（解码侧虽已大量移植，编码侧
    几乎都要重写）。三者中最大，放最后。
- **可复用基建**：`huffman.mbt` 的 MSB-first `BitWriter`；AAC 的
  `aac_swb_tables` / `aac_spectrum` 码本表 / ADTS 头；`aac_mdct.mbt` 的
  正反变换；MP4/ADTS/TS 容器写入器可直接承载输出码流。QMF 仅有解码侧
  （SBR），Opus 的 CELT 另需 480/960 点 MDCT 与 PVQ。
- **第 1 阶段实现**（`libavcodec/aac_encode.mbt`）：
  - 仅 AAC-LC（object type 2），1024 点长窗 + sine 窗（ONLY_LONG），
    1/2 声道（SCE / 独立双 ICS 的 CPE），无 TNS/PNS/预测/M-S。
  - 与 FFmpeg `aacenc` 相同的约定：分析系数
    `X[j] = Σ x[t]·cos(π/1024·(t+512.5)·(j+0.5))`，编码器系数
    `c = 65536·X`；量化
    `q = floor((|c|·2^(-(sf-100)/4))^(3/4) + 0.405)`。
  - ADTS：7 字节头 + raw_data_block + ID_END(3 bit) + 字节对齐。
- **第 2 阶段实现**（本轮）：
  - **codebook 11 escape**：取消 `q ≤ 15` 上限，量化幅度上限 8191；
    幅度 ≥ 16 时按上游 `aaccoder.c` 写 `len-4` 个 1 + 终止 0 +
    `len` 位余数（`len = floor(log2(v))`），符号位在 escape 序列之前
    写完（与解码端 `VMUL4S` 的读取顺序一致）。
  - **逐带码本选择**：对带内每个 2/4 元组，按码本 1..11 的真实 Huffman
    位耗（cb11 计入 escape 位）取最小；反向查表以
    `codebook_vector02/4/6/8/10_idx` 的打包键（低 8 位数值 + bits 8..15
    的符号槽位/nnz）索引，避免线性搜索。
  - **码率填满**：以 `sf = ceil(100 + 4·log2(peak/35.66))` 为搜索中心，
    在整数 delta ∈ [-255, 255] 上二分，取仍装得下
    `bit_rate·1024/sample_rate/8` 的最小 delta（可为负，最细量化）；
    量化先夹紧再转 Int，避免极粗 sf 时 `a^(3/4)` 溢出。
  - 声道内仍共享一个标度因子：在“每系数约 log(q) 比特”的代价模型下，
    均匀量化步长对 MSE 已最优，逐带 sf 只对感知掩蔽有益。
- **量化指标**（`libavcodec/aac_encode_vectors/`，44.1 kHz 立体声、8 帧
  多音 + LCG 抖动、目标 128 kbps，帧预算 371 字节）：
  - 9 帧（含 1 个 flush 帧）ADTS 共 3128 字节，平均 347 字节/帧，实测
    **119.7 kbps**（目标的 93.5%）。
  - 谱域往返 SNR：mono 33.81–41.30 dB；stereo 23.70–31.35 dB。
  - 时域往返（本仓库解码器，延迟 1024 样本）：**SNR 29.97 dB**、
    PSNR（峰值 32768）**39.8 dB**、MSE 112317。
  - 外部验证：`linuxserver/ffmpeg:9.0.2` 解码该 ADTS 流输出
    `aac (LC), 44100 Hz, stereo, 119 kb/s`、9 帧、无告警；其 s16 输出与本
    仓库解码器输出差 0.0008 MSE（亚 LSB 舍入），故上述 SNR/PSNR 与真实
    ffmpeg 解码一致。
  - 断言已写入测试：理想分析/合成 SNR≈254 dB 且 gain=1；mono 谱 SNR >
    32 dB；stereo 谱 SNR > 22 dB；往返 SNR > 28 dB、PSNR > 38 dB，
    平均帧长 ≤ 目标字节数且 ≥ 目标的 75%。
- **验证**：`moon check --target wasm --deny-warn` 通过；全量
  `moon test --target wasm --no-render` **1368/1368** 通过。
- **下一阶段**：AAC 补短窗/窗口切换、TNS 与简化心理声学模型（届时才引入
  逐带标度因子）；MP3 走原生 mpegaudioenc 的 32 点滤波器组；Opus 最后做，
  重点是 CELT/PVQ 编码搜索。
- **风险**：无心理声学时 39.8 dB PSNR 只代表波形相似，不代表听感；flush
  帧仍按预算填满，纯零输入也会花掉整帧比特；MP3 子带滤波器组与 Opus PVQ
  搜索是本仓库尚无的算法模块，周期与调试成本最高。

## 274. MP2 编码：移植 mpegaudioenc 的定点分析滤波器组

- **范围决定**：FFmpeg 9.0.2 只有 Layer I/II 编码器（`mpegaudioenc.c`，float
  `mp2` 与定点 `mp2fixed` 两条路径），没有 Layer III 编码器；上一节设想的
  “MP3 走原生 mpegaudioenc 的 32 点滤波器组”实际可交付的是 **MP2**。本阶段
  新增 `libavcodec/mpa_encode.mbt`，按 `USE_FLOATS=0` 的 `mp2fixed` 路径移植，
  Layer III 编码需从零实现，明确不在本阶段。
- **分析滤波器组**：每声道 4096 样本历史缓冲（`samples_offset` 从 0 起，每
  块写 `offset + (31-i)` 后 `offset -= 32`，降到 0 以下时把前 480 样本搬
  到 `4096-480` 并令 `offset = 3584`）；64 点窗化用
  `ff_mpa_enwindow` 的 `WFRAC_BITS=14` 缩放 `(v+2)>>2` 生成 512 项
  `filter_bank`（`(i & 63) != 0` 时取反后写入 `filter_bank[512-i]`）；8 段
  `p[k*64+i]·q[k*64+i]` 求和后按 `WSHIFT=14` 折叠成 32 点 `tmp1`，再过
  `idct32`（`costab32[30]`、`bitinv32[32]`、`FRAC_BITS=15` 的
  `MUL(a,b)=((int64)a*b)>>15`，四段蝶形 + 负号段 + 4/2 分组 + 16 级旋转）。
- **帧长与填充**：`a=(float)(bitrate*1000*1152)/(freq*8.0)`，
  `frame_size=((int)a)*8`，`frame_frac_incr=(int)((a-floor(a))*65536.0)`；
  每帧 `frame_frac += incr`，`>= 65536` 时减回、`do_padding=1`、位预算 `+8`。
  44.1 kHz/128 kbps 得 417 字节基准帧、每 12 帧有 11 帧填充。
- **Layer II 语法**：位分配表由 `mpa_l2_select_table` 选（44.1 kHz 立体声
  128 kbps → table 0、`sblimit=27`）；逐带 3 个标度因子用
  `scale_factor_table`（`exp2((3-i)/3)*2^20`）做上界搜索，再按
  `scale_diff_table` 的 `d1*5+d2` 落到 25 种合并分支的 4 种 scale code；
  位分配用固定 `fixed_smr*10` 表与 `quant_snr[17]`、`total_quant_bits[17]`
  的贪心填充，状态机 `NOTALLOCATED/ALLOCATED/NOMORE` 与上游一致。
- **量化与写位**：`shift<0` 时左移、否则右移；`(q1*mult)>>15`（Int64 中转）
  后 `+2^15`、钳零，再 `(q1*steps)>>16` 并钳到 `steps-1`；`bits<0` 时把
  3 个样本打成一个码字，否则逐样本写。
- **关键修正**：标度因子码与标度因子必须**分成两趟**写：C 先写所有带宽的
  2-bit `scale_code`，再写所有标度因子；此前把两者按“带内 ch0/ch1”交错
  写出，导致整帧从标度因子码起点（bit 208）开始错位，解码得到噪声。
- **定位方法**：用本机 gcc 编一个独立的 C harness（原样复制
  `idct32`/`filter`/`compute_scale_factors`/位分配/写帧，并链接上游
  `ff_mpa_enwindow` 与 `alloc_table_1`），其输出与 ffmpeg `mp2fixed`
  **逐字节一致**；再把 MoonBit 侧逐帧的 `sb_samples`（32 项 + 多项式哈希）、
  `scale_code`、`scale_factors` dump 出来对拍，确认滤波器组与标度因子完全
  一致后，把差异收敛到写位顺序。
- **验证**：
  - 与 ffmpeg 同素材（12 帧 44.1 kHz 立体声 128 kbps）输出 **5015 字节**
    （417 + 11×418），与 `ffmpeg -c:a mp2fixed` 的同名输出 MD5 完全相同
    （`049A748995807E25F624AA0268049C2E`），即逐字节一致。
  - `linuxserver/ffmpeg:9.0.2` 的 ffprobe 识别 `mp2, 44100 Hz, stereo,
    128 kb/s`；解码无 codec 告警（仅在 `-v warning` 下有 mp3 demuxer 的
    duration 估算提示）。补偿 962 样本（481 编码器 + 481 合成滤波器组）
    延迟后对源 **SNR 21.77 dB / PSNR 29.66 dB**（信号 RMS 9401.3、
    噪声 RMS 766.7）。
  - 单元测试 `libavcodec/mpa_encode_vectors`：
    `mp2_stereo_header_and_frame_size`（头 `FF FD`、mode stereo、总长
    5015、往返 SNR 24.26 dB、延迟 481）与
    `mp2_mono_lsf_header_and_round_trip`（22050 Hz mono 64 kbps、
    头 `FF F5`、mode mono、往返 SNR 24.85 dB）。
- **检查**：`moon check --target wasm --deny-warn --no-render` 通过；全量
  `moon test --target wasm --no-render` **1372/1372** 通过（上一阶段为
  1370，新增 2 个 MP2 用例）。
- **边界**：只覆盖 `mp2fixed` 的定点路径，float `mp2` 编码器不移植；编码器
  不做 flush（上游同样不带 flush，末帧之后无补偿帧）；心理声学模型就是
  上游的固定 SMR 表，不是自适应掩蔽模型；Layer III（MP3）编码器不存在于
  FFmpeg 9.0.2，未交付。
- **下一阶段**：Opus（CELT/PVQ）。




## 275. MP3 编码：移植 shine 3.1.1 的 Layer III 定点编码器

- **范围决定**：FFmpeg 9.0.2 没有 Layer III 编码器（`mpegaudioenc.c` 只有
  Layer I/II），官方支持路径是外部库（`libmp3lame` / `libshine`）。按仓库
  "纯移植、无外部 C 依赖"的约定，本阶段移植 FFmpeg `libshine.c` 所封装的
  shine 3.1.1（ISO 11172 dist10 血统的定点 Layer III 编码器，LGPL-2.1），
  交付真正的 MP3 编码：`libavcodec/mp3_encode.mbt`（layer3.c + reservoir.c
  的编排与码率储备）+ `mp3_enc_subband.mbt`（32 带多相分析滤波器组）+
  `mp3_enc_mdct.mbt`（36 点 MDCT + 混叠蝶形）+ `mp3_enc_loop.mbt`（量化、
  内外迭代循环、Huffman 表选择、位计数）+ `mp3_enc_bitstream.mbt`（32 位
  缓存的 MSB-first 位写入器 + 帧封装）。
- **数值语义**：shine 的 `mul` 家族按 noarch 路径复刻——`mul(a,b) =
  (a*b)>>32`、`mulr(a,b) = (a*b+0x80000000)>>32`、`mulsr(a,a) =
  (a*a+0x40000000)>>31`，全部 Int64 中转后截断 Int32；乘积逐项截断后整数
  求和（模 2^32 与顺序无关）。`steptab`（libm `pow` 的产物）以 IEEE-754
  位模式零转录嵌入；`subband.fl`、`mdct.cos_l`、`enwindow`、34 张
  Huffman 码表/码长表与 `int2idx` 由 `scripts/gen_mp3_enc_tables.sh` 的
  C dumper 从编译后的参考二进制直接 dump（`scripts/extract_mp3_enc_tables.py`
  转成 `mp3_enc_tables.mbt`），无手工转录。
- **关键坑**：
  1. **子带缓冲是倒序填充**：C 的 `for (i=32;i--;)` 是后缀自减，第一个
     PCM 样本写入 `x[off+31]`（整个 32 样本块反转）；写成正序会让所有
     频谱错位、量化循环跑偏（首帧 mdct_in 哈希即分歧）。
  2. **位流缓存跨帧连续**：`shine_putbits` 只在缓存填满 32 位时落盘，
     `shine_encode_buffer_internal` 每帧只把 `data_position` 归零——
     帧尾不足一个字的位留在 cache 里，成为下一帧第一个字的高位。逐帧
     输出拼接后才是连续码流；若每帧清缓存会丢尾部填充位。
  3. **`calc_scfsi` 的 libm log 不可跨平台**：`en = log(x)/LN2` 截断成
     Int 时，MSVCRT `log` 与 wasm `Math.log2` 的 ulp 差会在整数边界翻转
     scfsi 判定（实测 v1 第 5 帧带 0 翻转）。双端（C harness 与 MoonBit）
     改用同一个纯 IEEE 运算的确定性 `ln`（`mp3enc_ln` / `shine_ref_log`：
     精确 2 的幂归一化 + 25 项 `2·atanh((m-1)/(m+1))` 级数 + `e·ln2`），
     除这一点外其余全部上游一致；对拍素材上该值与 libm 截断结果一致。
  4. **MPEG-2/2.5 每帧只消费 576 样本**（`granules_per_frame×576`），
     参考 harness 最初按 1152 跳帧喂样本导致 v3 分歧——MoonBit 侧按
     `shine_samples_per_pass` 消费是正确的。
  5. `shine_huffman_table` 的元数据提取最初只写了 code/hlen 偏移、漏掉
     `xlen/ylen/linbits/linmax`，导致 `new_choose_table` 读表越界；
     `huff_desc` 每项 6 个字段（4 元数据 + 2 偏移）。
- **对拍方法**：`_ref/shine/harness.c` 复刻 `shine_encode_buffer_internal`
  的三段流水（`shine_mdct_sub` → `shine_iteration_loop` →
  `shine_format_bitstream`），每帧 dump `l3_sb_sample`（含逐槽位）、
  `mdct_freq`/`l3_enc` FNV-1a 哈希与全部 granule 侧信息、scfsi、ResvSize
  与输出字节哈希；MoonBit 侧用临时 debug 钩子输出同格式逐项 diff。收敛
  后删掉 debug 钩子，只留端到端字节级断言。harness 输出与未改动
  `shineenc`（同一 WAV）MD5 一致，确保参考本身成立。
- **验证**（`libavcodec/mp3_encode_vectors/`，夹具由
  `scripts/gen_mp3_enc_vectors.py` 生成）：
  - **三组向量与 C 参考逐字节一致**：v1 = 44.1 kHz 立体声 128 kbps
    （MPEG-1 双 granule、scfsi、padding 序列）、v2 = 48 kHz 单声道
    192 kbps、v3 = 22050 Hz 单声道 64 kbps（MPEG-2 LSF 单 granule）。
  - `linuxserver/ffmpeg:9.0.2` ffprobe 识别三组输出为 `mp3`（码率/声道/
    采样率正确），`ffmpeg -v error` 解码零告警，PCM 长度精确匹配。
  - 本仓库 `decode_mp3_stream` 往返（对齐 lag 1057 后）SNR：v1 17.4 dB、
    v2 18.1 dB、v3 18.1 dB（与外部解码一致；shine 的固定 SMR 心理声学
    + 零 scalefactor 决定了这个量级，与 LAME 不可比）。
  - 配置校验：非法采样率/码率/声道数/样本数均拒绝。
- **检查**：`moon check --target wasm --deny-warn` 通过；全量
  `moon test --target wasm --no-render` **1376/1376** 通过（上一阶段为
  1372，新增 4 个 MP3 用例）。
- **边界**：只覆盖 shine 支持的模式（STEREO/MONO，无 joint stereo、无
  短窗/窗口切换——shine 本身不做 blocksplit）；`scalefac_compress` 恒 0
  （不写 scalefactor 位），reservoir 恒空（`ResvMax=0`、`main_data_begin=0`），
  这是上游 shine 的行为而非移植缺口；native 目标未验证（本机无 MSVC，
  与仓库 wasm-first 约定一致）。至此"音频编码：AAC + MP3 + Opus"清单中
  AAC（§270-273）与 MP3（本节）已交付，剩 Opus。
- **下一阶段**：Opus（FFmpeg 原生 `opus/enc.c` 的 CELT/PVQ 编码路径）。

## 276. Opus 编码：移植 FFmpeg 原生 `opus/enc.c` 的 CELT/PVQ 编码路径

- **范围决定**：FFmpeg 9.0.2 的原生 Opus 编码器只有 CELT（`enc.c` +
  `enc_psy.c` + `pvq.c` + `celt.c`），SILK/hybrid 不在编码侧范围内。
  按仓库"纯移植、无外部 C 依赖"的约定，交付 CELT-only 编码器：
  `libavcodec/opus_enc_rc.mbt`（range coder：前向 range 字节 + 反向
  raw-bit 区、`tell/tell_frac`、checkpoint/rollback）+ `opus_enc_math.mbt`
  （确定性 `ln/log2f/expf/atan2f/sinf/cosf`）+ `opus_enc_tables.mbt`
  （band 表、窗、Bessel 系数、静态 caps 等）+ `opus_enc_celt.mbt`
  （CeltFrame/Block、preemph、frame MDCT、coarse/fine/final 能量量化、tf）+
  `opus_enc_bands.mbt`（`quant_band` 全模板：split/折叠/LCG、`alg_quant`
  PVQ 搜索、`exp_rotation`、Haar/Hadamard、collapse mask）+
  `opus_enc_alloc.mbt`（`ff_celt_bitalloc` + `ff_celt_quant_bands`）+
  `opus_enc_psy.mbt`/`opus_enc_psy2.mbt`（step 采集、bessel 激励、
  gauge/强度/双声道/tf 搜索、帧预算分配、postencode）+ `opus_encode.mbt`
  （`OpusEncoder` 编排、lookahead 缓冲、TOC/包组装、OpusHead extradata）。
- **验证**（`libavcodec/opus_encode_vectors/`，夹具由
  `scripts/gen_opus_enc_vectors.py` 从 C 参考重新生成）：
  - **v1 = 48 kHz 立体声 96 kbps，v2 = 48 kHz 单声道 48 kbps，各 12 帧
    （13 个包，含尾部 1 字节静音包）与 C 参考逐字节一致（26/26 包）**。
    参考链路见 `_ref/opus/build_ref.sh`：给 `enc.c`/`enc_psy.c`/`pvq.c`/
    `celt.c`/`rc.c` 打探针后从静态库摘掉原对象重新链接，`OPUS_DBG=1`
    输出逐阶段 dump（precoarse/coarse/tf/alloc/bands/bq/sp/aq/rx/pv/nf/
    gauge/isearch/bc/tf/pr/end 等），`scripts/make_ogg_opus.py` 把包流
    封装成 Ogg Opus 供外部解码验证。
  - `linuxserver/ffmpeg:9.0.2` 对 Ogg Opus 输出 `-v error` 解码零告警；
    同流参考解码器对源 SNR 16.6 dB（v1）/13.5 dB（v2），本仓库 Opus
    解码器往返 15.7/13.0 dB（差 <1 dB）。
  - 正式测试：byte-exact v1/v2、配置校验（声道数/样本不足拒绝）、
    19 字节 OpusHead、两组解码往返 SNR 阈值 14.0/11.5 dB。
- **关键坑**（全部为 C 语义与直译的偏差，均已双端验证）：
  1. **`ff_opus_rc_enc_end` 的末位舍入必须无符号右移**：`end >> 23` 若先
     `reinterpret_as_int()` 再做算术移位，`end` 的 bit31 置位时得到 -256
     而不是 +256，carry 少 1（包内单个字节 0x44→0x42）。这个位在整帧
     末尾，且不改变 `tell`/`end` 状态，最难从阶段 dump 上看出来。
  2. **`bands_dist` 的 `tdist` 在 C 里是 `int`**（`int tdist = 0.0f;`）：
     每条带的 float 距离先截断再加总。改成 Float 累加会让强度搜索的
     距离曲线整体偏移 ~10，`is` 选择从第 6 帧起分叉。
  3. **`alloc_boost` 是截断不是四舍五入**：`(int)((score/max)*3.0f)`，
     用 `rintf` 会让部分带多 1 个 boost，直接改分配。
  4. **非立体声 split 的 collapse mask 合并**：C 是
     `cm |= cmt << ((B0 >> 1) & (stereo - 1))`，`stereo=0` 时
     `stereo-1 == -1` 表示"不移位掩码"；写成 `& 1` 只移 0/1 位。
  5. **level 0 收尾的 `av_zero_extend(cm, blocks)` 用的是时间分割后的
     `B0 << recombine`**，不是 split 折半后的 `blocks`；否则高块被裁掉。
  6. **折叠路径的 `effective_lowband` 必须按 C 的赋值条件填写**
     （`lowband_offset != 0 && (spread != AGGRESSIVE || blocks > 1 ||
     tf_change < 0)`），写死 -1 会让整条 folding 退化成 LCG 噪声。
  7. **观测者效应**：探针循环变量一律用独立名字（`qi/gi/fj`），
     复用 `i` 会改掉 C 后续循环的起点（曾让 fine 循环整个不执行）。
- **边界**：只支持 FFmpeg 原生编码器默认配置——48 kHz、CELT-only、
  `frame_size=960`（20 ms）、MONO/STEREO、FULLBAND；SILK/hybrid 不在
  FFmpeg 原生编码器范围。与上游的显式偏差（双端一致）：确定性 libm
  替换、直通 MDCT 替代 `av_tx` FFT（与真 `av_tx` 有 ~1 ulp 差异）、
  Bessel 系数用 dump 而非 `tanf` 重算。native 目标未验证（本机无 MSVC，
  与仓库 wasm-first 约定一致）。
- **检查**：`moon check --target wasm --deny-warn` 干净；全量
  `moon test --target wasm` **1382/1382** 通过（临时 dump/dev 测试已删除）。
  至此"音频编码：AAC + MP2 + MP3 + Opus"清单全部交付。

## 277. AAC 编码器：3GPP 心理声学模型（`aacpsy.c`）与算法参考工具链

- **动机**：§271 的 AAC-LC 编码器用的是简化心理声学（带能量 + 上扩 + 固定 SNR aim）
  与"整帧单标度因子 + 全局 delta 二分"，和 FFmpeg `aacenc` 的
  `psy_3gpp_analyze` + `search_for_quantizers_twoloop` 两遍码率控制差距最大。
  本片先把 `aacpsy.c` 的 3GPP 模型原样移植、独立对拍，作为后续接入的可验证层；
  不改动 `aac_encode.mbt` 的任何输出。
- **C 参考工具链** `_ref/aacenc/`（纯参考，gitignored）：不经过 avcodec 公共 API，
  直接以 MinGW-W64 gcc `-O1` 编译 FFmpeg 9.0.2 的 `aacpsy.c` / `aaccoder.c` /
  `aaccoder_twoloop.h` / `aactab.c`（`patch.py` 用精确字符串替换 + assert 锚点
  注入导出变量，`build.sh` 摘除可能冲突的静态库对象后链接），`aac_ref.exe`
  对 5 帧 × 1024 点确定性合成谱输出 510 行 `vectors.txt`：
  `PSYALLOC` / 49 行 `PSYBAND` / 49 行 `TWOBAND`。复现：
  `bash _ref/aacenc/build.sh && ./aac_ref.exe > vectors.txt`。
  夹具由 `scripts/gen_aac_psy_vectors.py` 转成 `fixtures.mbt`。
  工具链的简化与字段出处逐条记在 `_ref/aacenc/README.md`。
- **MoonBit 侧** `libavcodec/aac_psy.mbt`：`psy_3gpp_init` 的长/短窗静态系数
  （Bark、spread 系数、min_snr、ATH）、`calc_thr_3gpp`（能量 / form factor /
  nz_lines）、双向阈值与能量扩散、预回声控制（`prev_band.thr_quiet`）、
  `calc_bit_demand`（比特池 + 自适应的 `pe.min/max`）、`calc_pe_3gpp`、
  reduction 两轮估计、`calc_reduced_thr_3gpp`、avoid-holes 选择、
  linearization 与高频 PE 削减，最后发布 `FFPsyBand` 并提交跨帧状态。
  上下文由 `aac_psy_new(sampling_index, sample_rate, bit_rate, channels)` 构造，
  每帧调 `analyze(coefs, prev_bits)`；`prev_bits` 对应 `bitres.bits`，
  显式留参以便日后接比特流写入器。
- **验证**（`libavcodec/aac_psy_vectors/`，黑盒 3 项）：44100 Hz / 128 kbps /
  2 声道、ONLY_LONG 49 带、5 帧连续分析，`alloc` / `fill_level` / 每带 `bits`
  **逐位一致**，energy / spread / entropy 相对误差 ≤1e-5，threshold ≤5e-4；
  另有跨帧预回声状态与有限阈值测试。C 全程 `float`、本移植用 `Double` +
  libm 边界 f32 舍入，frame 4 band 0 恰好落在 reduction/linearization 边界，
  1 ulp 被放大到约 1e-4 相对，故 threshold 单独放宽到 5e-4。
- **关键坑**（都是 C 语义与直译的偏差）：
  1. `psy_3gpp_init` 的 `coeffs[g+1].barks - coeffs->barks` 里 `coeffs` 是
     **数组指针**，减的是 `coeffs[0].barks`（从 0 号带起的累积 Bark 宽度），
     不是相邻带差。写成相邻差会让所有 `min_snr` 掉到 -25 dB 下限，
     reduction 整段失效。
  2. `calc_reduced_thr_3gpp` 是 `(sqrt(sqrt(thr)) + reduction)^4`
     （C 连做两次 `sqrtf`、两次自乘），不是混合根相乘。
  3. `en_spread_hi` 的上游判据把 bits/s 的 `chan_bitrate` 与 `22`（kbps）比较，
     对任何真实 ABR 码率恒选 `HI_L1`；照抄上游，不"修正"单位。
  4. 谱输入必须用 f64 的 `sin/cos` 算完再**强转 f32**；psy 内部每个
     `log2f/exp2f/exp10f` 结果都存进 `float`，移植侧须在 libm 边界舍入一次。
  5. 参考 harness 的 `bitres.bits = 0`（每帧），`fill_level` 被
     `av_clip(..., 0, bitres.size)` 钳成恒 4656；真正演化的跨帧状态是
     `bitres.alloc`、`entropy`、`pe.min/max` 与 `prev_band`。
- **边界**：只覆盖单长窗（`num_windows = 1`、`ONLY_LONG_SEQUENCE`、49 带）、
  ABR（无 QSCALE）、PNS/TNS/IS/M-S 全关、无块切换；短窗系数表虽在 `init`
  里构建，但 `analyze` 不走。合成谱输入，未跑 PCM→MDCT。
  **本片是独立可验证层，尚未接入 `aac_encode.mbt`**；夹具里已备好的
  245 条 `TWOBAND` 参考（twoloop 量化器的 sf_idx / band_type / zeroes /
  maxval）是下一片的对拍目标。
- **检查**：`moon check --target wasm --deny-warn` 干净；全量
  `moon test --target wasm` **1385/1385** 通过。

## 278. AAC 编码器：twoloop 量化器（`aaccoder_twoloop.h`）

- **动机**：§271 的 AAC-LC 编码器仍是"逐带在码本 1..11 中选最小位耗 + 整帧
  单标度因子 + 全局 delta 二分"；§277 交付了 3GPP 心理声学模型，同时把
  `aaccoder.c` 的 `search_for_quantizers_twoloop` 参考向量（245 条 `TWOBAND`）
  留作下一片的对拍目标。本片把 twoloop（FFmpeg 的两遍码率控制：内层按帧
  预算收敛标度因子、外层逐带按失真/比特代价调整）原样移植并独立对拍，
  让"psy 分析 → 两遍码率控制 → 逐带量化"链路的两段都可验证。
  仍不改动 `aac_encode.mbt` 的任何输出。
- **C 参考工具链** `_ref/aacenc/`（纯参考，gitignored）：在 §277 的基础上新增
  独立的 `dump_tables.c`（链接时不带 `harness.o`，避免 `main()` 冲突；内含
  `aac_tableinit()` 的逐字复制，用于生成运行期构造的
  `ff_aac_pow2sf_tab` / `ff_aac_pow34sf_tab`），`build.sh` 追加
  `dump_tables.o` 与 `== 5/5 link table dumper ==` 段。dump 标签：
  `POW2SF` / `POW34SF` / `CBRANGE` / `CBMAXVAL` / `MAXVALCB` / `VEC <k>` /
  `BITS <k>`。`patch.py` 追加 6 段 stderr 阶段探针（`TWOPARM`、`TWOINIT`、
  `TWOWEIGHT`、`TWOLOOP`、`TWODIST`、`TWOOUT`），因为 stdout 与 stderr 分离，
  探针不影响 `vectors.txt`；重建后重新生成 §277 的夹具零 diff，证明参考输出
  未被污染。夹具由 `scripts/gen_aac_twoloop_tables.py` 转成
  `aac_twoloop_tables.mbt`（生成期断言：`vecs[10][-2:] == [64.0, 64.0]` 的
  BT_ESC 哨兵、`vecs[0][:4] == [-1.0]*4`、`bits[10][0] == 4`）。
- **MoonBit 侧**：
  - `libavcodec/aac_twoloop_tables.mbt`（761 行，自动生成）：`pow2sf` /
    `pow34sf`、`cb_range`（`ff_aac_spectral_bits` 的区间起点）、`cb_maxval`、
    `maxval_cb`、`vec1..vec11` 与 `bits1..bits11` 逐码本码字表，另有汇总
    `aac_twoloop_vectors` / `aac_twoloop_spectral_bits`。索引顺序与 C 一致。
  - `libavcodec/aac_twoloop.mbt`（973 行）：f32 助手族
    （`add/sub/mul/div/sqrtf/log2f/expf/logf/cbrtf/powf/fast_powf` 与
    `clipf`/`av_log2`）、`aac_two_abs_pow34`、`find_max_val`、`coef2minsf`、
    `find_min_book`、`find_form_factor`、`quant`、`band_cost`
    （`quantize_and_encode_band_cost_template`，含 codebook 0 的零带短路与
    codebook 11 的 escape 计数）、`init_nextband_map` / `can_remove_band`，
    以及 `priv struct AacTwoLoop`（`collect()` 复刻 C 里重复写了两遍的量化
    循环，填 `sf`/`zeroes`/`dists`/`maxvals`/`qenergies`）。
    对外只暴露 `aac_twoloop_search(coefs, band_sizes, bands, alloc,
    bit_rate, sample_rate, channels, lambda) -> AacTwoResult`，
    `AacTwoResult { sf, cb, zeroes, maxvals, tbits, iters }`。
  - 带计划不重复推导：`aac_psy.mbt` 追加 `AacPsyContext::band_sizes()` 访问器，
    twoloop 直接取 `psy_3gpp_init` 得到的长窗 49 带尺寸，保证两者同源。
- **验证**（`libavcodec/aac_twoloop_vectors/`，黑盒 3 项）：44100 Hz / 128 kbps /
  2 声道、ONLY_LONG 49 带、5 帧连续谱，与 `vectors.txt` 的 245 条 `TWOBAND`
  逐带比对——`sf_idx` / `band_type` / `zeroes` **逐位一致**，`maxval` 相对误差
  ≤1e-5；5 帧 `alloc` = 2229 / 1966 / 1943 / 1910 / 1971，最终 `tbits` =
  2352 / 1929 / 2052 / 2009 / 1957（帧 0/2/3/4 高于 `alloc`，是上游 twoloop
  外层逐带调整后**不做全局回退**的原样行为，移植逐位复现），`iters` 恒 30。
  另两项：全零谱返回全零带 + `sf == 140` + `tbits == 0`；结构不变量
  （`cb ∈ 1..11`、`sf ∈ 60..219`、相邻非零带 delta ≤60）。
- **关键坑**：
  1. `quantize_band_cost_cached(..., 1.0f, INFINITY, ...)` 里这个 **1.0f 是固定的
     成本 lambda，不是搜索用的 `lambda=120`**。最初把 120 传进成本函数，
     `dists` 整体放大 120 倍 → 逐带 `sf` 立刻分叉
     （`TWODIST its=0 g=0`：C 6663840 / MB 799660800），而 `sf`/`maxval` 的
     前半段看起来还算"接近"，很容易误判成量化器精度问题。定位靠
     `TWOOUT` 显示 `its=0` 的**前 4 带**就已不同（C 136/153/153/153 vs
     MB 136/152/147/147）——分歧在内层循环之后、逐带调整之前，即 `dists` 本身。
  2. `vecs[10][0]` 不是 `(64,64)`：codebook 11（BT_ESC）的哨兵在表的**末尾**，
     首项是 `(0,0)`；codebook 0 的前 4 项是 `-1.0`（不是 `-1,0,1`）；
     `bits[10][0] == 4`。生成器最初的这三条断言全错，是表读取方向问题而非
     表导出问题。
  3. 内层循环的 `iters` 上限 30 是**必须跑满**的语义：参考侧 5 帧都恰好用满
     30 次，`sf0` 在 127/131/132/136/137 之间来回振荡不收敛。任何"提前收敛
     就跳出"的优化都会让结果偏离。
  4. 调试期教训沿用 §277 的约定：新增 stderr 探针的循环变量一律用独立命名
     （`aac_dbg_pg`），绝不复用 `i`/`j`/`g`/`w`，否则会把参考实现自身改坏。
- **边界**：只覆盖单长窗（`num_windows = 1`、`ONLY_LONG_SEQUENCE`、49 带）、
  ABR（`AV_CODEC_FLAG_QSCALE` 分支不走）、PNS/TNS/IS/M-S 全关、无块切换、
  无 `-q:a` 直通；合成谱输入，未跑 PCM→MDCT。
  **本片与 §277 同样是独立可验证层，尚未接入 `aac_encode.mbt`**；
  下一步是把 `aac_psy.mbt` + `aac_twoloop.mbt` 一起接进编码器主线，
  替换 §271 的简化模型与全局 delta 二分。
- **检查**：`moon check --target wasm --deny-warn` 干净；全量
  `moon test --target wasm` **1388/1388** 通过。

## 279. AAC 编码器：接入 3GPP 模型 + twoloop（可选码率控制）

- **动机**：§277（`aacpsy.c`）与 §278（`aaccoder_twoloop.h`）两层都独立对拍过，
  本片把它们接进 `aac_encode.mbt`，替换 §271 的简化心理声学与 ±255 全局 delta
  二分。接通后量化噪声仍未落到模型自身阈值以内（见"测量"），所以**默认不启用**：
  新增 `aac_encode_config_perceptual`，默认的 `aac_encode_config` 仍走 §271 的
  路径，编码质量不回退。
- **接线**（`aac_encode.mbt`）：
  - `AacEncoder` 新增 `psy : Array[AacPsyContext]`、`mut last_frame_bits`、
    `mut lambda`。
  - 每帧顺序：M/S → `psy.analyze`（每声道一次，`prev_bits =
    last_frame_bits / channels`）→ TNS → twoloop → 写帧。psy 看的是**实际被编码
    的谱**（M/S 之后、TNS 之前）。
  - 只对 `aac_only_long` 生效；LONG_START / EIGHT_SHORT / LONG_STOP 仍走简化模型
    + delta 二分（psy 移植只做长窗，twoloop 只做单窗）。
  - 预算：`alloc = psy.alloc / channels`（`aacenc.c` 在累加 `target_bits` 之后就是
    这个除法）。
  - 标度因子映射：twoloop 内部 `Q = 2^((104-sf)/4)`，本模块 bitstream 侧
    `q = floor((|c|·2^(-(s-100)/4))^(3/4)+0.405)`，两者相等当且仅当 **`s = sf-4`**。
  - `aac_twoloop_quantize_band`（`aac_twoloop.mbt` 新增）：按 twoloop 的 f32 路径
    重新量化（`abs_pow34` + `quantize_bands`，cb 11 的 escape 走 `quant()` 并 clip
    到 8191），使写进码流的系数就是被成本函数定价的那批。逐带核对过：成本与写入
    位数一致（个别 escape 带差 2–6 位，来源是 C 用 `av_log2(c)*2-4+1` 估算）。
  - 码率控制：`aac_encode_frame` 的**严格分支**（`bit_rate_tolerance == 0`）——
    帧超出 `bit_rate*1024/sample_rate` 位就把 λ 乘 `min(0.9, rate/frame)` 重来，
    装得下就把 λ 复位 120；迭代上限 10 次（上游靠 λ 收敛，这里加了保险）。
- **测量**（44100 Hz / 128 kbps / 2 声道，8 帧四音 + LCG 抖动 + 静音 flush）：
  - 默认路径（不变）：117.1 kbps、往返 SNR 29.05 dB、PSNR 38.88 dB。
  - 感知路径：122.7 kbps、SNR 21.48 dB、PSNR 31.31 dB；逐 1024 样本块
    27.9 / 28.3 / 28.4 / 23.0 / 23.1 / 20.4 / 18.7 / 17.0 dB（前 3 块是过渡帧，
    仍走旧路径）。
  - 单声道长帧基本持平（33.50 vs 33.53 dB，546 vs 560 字节），差距集中在立体声。
  - 观察：稳定长帧上逐带量化噪声是 psy 自身阈值的 5–20 倍（头几帧只有 0.6–1.3
    倍）。**已交叉验证这不是移植或接线问题**，见下一条。
- **交叉验证（决定性）**：`_ref/aacenc/harness.c` 新增 `--in <dump>` 模式，把编码器
  交给 `aac_twoloop_search` 的**同一份输入**（M/S 与 TNS 之后的 1024 点谱、49 条
  `FFPsyBand`、`alloc`）喂回**未修改**的 C `search_for_quantizers_twoloop`，只让它
  输出 `sf/cb/zeroes`。结果：第 5/7 帧共 **98 带逐位一致（0 差异）**，两套解的谱
  SNR 也完全相同（18.586 / 19.254 dB）。也就是说 §271 路径与本路径 8 dB 的 SNR
  差距**与移植无关**：3GPP 模型在这个码率上给出的 alloc 本身就只支撑到约 19 dB 谱
  SNR，阈值是理想目标而不是可达约束；§271 的启发式把整个字节预算烧在"更细的
  量化"上，SNR 自然更高。
  复现：给 `aac_encode.mbt` 加 dump 探针（见交接文档 §4）后
  `python _ref/aacenc/compare_encoder_input.py`（退出码 0 = 全一致）。
- **已知偏差**（相对 `aacenc.c`，按影响排序）：
  1. psy 分析的是 M/S 之后的谱；FFmpeg 分析 L/R，量化之后才做 M/S。
     **已实测过**把分析挪到 M/S 之前：21.42 dB vs 21.48 dB（帧长 353 vs 356
     字节），没有差别，因此保留"看 M/S"的顺序——它自洽（sf 按实际编码的谱选），
     而且单独前移只会让 twoloop 拿 L/R 的阈值去量化 M/S 谱，制造 FFmpeg 没有的
     新偏差。要真正复刻必须连"量化完成后再做 M/S"一起改。
  2. ~~psy ctx 每声道一份，`fill_level` / `pe.min/max` 不共享~~ —— §281 已改成单
     ctx + 每声道 band 状态，水位与 PE 范围恢复元素级共享，与 `FFPsyContext` 一致。
  3. psy 每帧只分析一次；FFmpeg 的 λ 重试循环里每次迭代都重新 `analyze`
     （`fill_level` 因此一帧内被更新多次）——有意不复刻这个上游怪癖。
  4. `planned_max_sfb` 取 psy `bits > 0` 的最后一带（帧级固定）；FFmpeg 由 twoloop
     的 `max_sfb` 决定。这里固定是为了让 TNS 的 `mmm` 与解码端
     `aac_apply_tns` 一致。
- **要把感知路径转正，只剩一个决定**：是否接受 SNR 指标下降换掩蔽整形。仓库目前
  没有感知指标（PEAQ/ODG 之类），只有 SNR/PSNR，而这两个指标天然偏向 §271 的
  "填满预算"策略。要么引入客观感知指标来定论，要么就维持 opt-in 并在 README 写清
  "感知路径 SNR 更低是设计使然"。
- **检查**：`moon check --target wasm --deny-warn` 干净；`libavcodec/aac_encode_vectors`
  **7/7**（新增 `aac_lc_perceptual_rate_control` 覆盖感知路径：解码、单帧
  ≤6144×声道、均值不超名义码率、SNR>18 dB）；全量 `moon test --target wasm`
  **1389/1389** 通过。

## 280. AAC 编码器：用噪声-掩蔽比对两条码率控制做取舍

- **动机**：§279 留下一个悬而未决的问题——感知路径 SNR 低约 10 dB，但这未必意味着
  更差，因为 3GPP 模型的目标是"把噪声压到掩蔽阈值以下"，不是最小化平方误差。
  仓库里只有 SNR/PSNR，两者都天然偏向"把预算烧完"的 §271 策略，需要一个模型自己
  的度量来判。
- **做法**（`libavcodec/aac_encode_vectors/`，新测试 `aac_lc_noise_to_mask`）：
  - 同一段夹具（8 帧四音 + 抖动 + 静音 flush）分别用两条路径编码、解码；
  - 对每个稳态帧取 2048 点重叠窗（原始 / 解码后各一份），用
    `AacEncoder::spectrum`（本次新增的公开访问器：正弦窗 + 65536 缩放 MDCT，
    与编码器内部完全同一条路径）得到两套谱；
  - 用 `aac_psy_new(...).analyze(原始谱)` 取每带 `threshold`；
  - 每带噪声 = Σ(解码谱 − 原始谱)²，NMR = 10·log10(噪声 / 阈值)，对所有带取均值。
    负值表示噪声压到掩蔽以下。
- **结果**（44100 Hz / 128 kbps / 2 声道，帧 2–7、两声道）：

  | 路径 | 码率 | 谱 SNR | 平均 NMR |
  |---|---|---|---|
  | 简化模型（§271，默认） | 117.1 kbps | 31.58 dB | **14.45 dB** |
  | 3GPP + twoloop（§279，可选） | 122.7 kbps | 21.56 dB | **12.88 dB** |

  即：感知路径用多约 5% 的码率换来 **1.6 dB 更低的噪声-掩蔽比**，代价是 10 dB 的
  谱 SNR。两条路径的 NMR 都是正值（噪声仍在阈值之上 12–14 dB），说明在 128 kbps
  这个码率、这种极度音调化的输入下，两条路径都做不到透明——差别只在"离掩蔽多近"。
- **结论**：取舍是真实的但幅度不大（1.6 dB），不足以支撑把默认路径换掉；维持
  opt-in 是正确的。若将来引入真正的感知指标（PEAQ/ODG），这条测试就是现成的
  回归护栏：`assert_true(nmr_perceptual < nmr_plain)` 会挡住任何把 psy 分配的
  优势做没了的改动。
- **副产品**：`AacEncoder::spectrum` 现在是公开 API——测试要用"与量化器同一域"的谱
  才能把噪声和阈值放在同一把尺子上，重新实现一遍窗与 MDCT 只会引入偏差。
- **检查**：`moon check --target wasm --deny-warn` 干净；`libavcodec/aac_encode_vectors`
  **8/8**；全量 `moon test --target wasm` **1390/1390** 通过。

## 281. AAC psy：改成元素级上下文 + 每声道状态（对齐 `FFPsyContext`）

- **动机**：§277 的移植把模型写成了"单声道状态 + `channels` 参数"，§279 接入时只能
  每声道建一个 ctx，于是 `fill_level`（比特储备）和 `pe.min/max/previous` 被复制成
  两份，而 `aacpsy.c` 里它们是**元素级共享**的（`FFPsyContext.bitres` / `pctx->pe`，
  只有 `AacPsyChannel ch[]` 里的 band 记录、`entropy`、attack 状态是每声道）。这既是
  §279 记录的偏差 2，也是将来做 PNS 的障碍（`mark_pns` / `search_for_pns` 都按
  `s->psy.ch[cur_channel].psy_bands` 读，同时依赖共享的 `lambda`/水位）。
- **改动**（`aac_psy.mbt`）：
  - `AacPsyContext` 的 `bands` / `prev_band` / `entropy` 改为按声道索引
    （`Array[Array[AacPsyBandState]]` / `Array[Double]`），`fill_level`、`pe_*`、
    `alloc` 保持元素级单份。
  - `analyze(channel, coefs, prev_bits)` 增加声道号；内部把该声道的两个 band 数组
    取成局部别名，`calc_thr` 改为接收 `bands` 参数（原来是读 `self.bands`）。
  - `aac_psy_new` 按 `channels` 分配每声道状态；`sampling_index` 等参数不变。
  - 编码器侧 `AacEncoder.psy` 从 `Array[AacPsyContext]` 变成**单个**
    `AacPsyContext`，每声道调 `psy.analyze(ch, coefs[ch], prev_bits)`。语义随之
    变成"分析声道 1 时已经看到声道 0 动过的水位"——这正是上游的行为。
- **验证**：
  - §277 的 3 项黄金测试、§278 的 3 项全部**逐位不变**（单声道用例只碰 channel 0，
    共享量在单声道下与原来等价）——这条是本次重构的安全网。
  - 编码器（44100/128k/2ch，同一夹具）：感知路径往返 SNR **21.48 → 21.92 dB**，
    噪声-掩蔽比 12.88 → 12.90 dB（持平），帧长 356 → 355 字节；简化路径 29.05 dB /
    NMR 14.45 dB 完全未变（它不经过 psy）。
  - 即：更忠实的结构顺带把立体声的水位算对了，多出 0.44 dB。
- **仍未复刻**（有意）：FFmpeg 的 λ 重试循环里每次迭代都重新 `analyze`，因此
  `fill_level` 一帧内会被更新多次；本移植每帧只分析一次（`bitres.bits` 只喂上一次
  的真实帧长）。这条留在 §279 的偏差清单里，属于"上游自身的怪癖"，不跟着抄。
- **检查**：`moon check --target wasm --deny-warn` 干净；`aac_psy_vectors` 3/3、
  `aac_twoloop_vectors` 3/3、`aac_encode_vectors` 8/8；全量
  `moon test --target wasm` **1390/1390** 通过。

## 282. AAC 感知路径覆盖 LONG_START / LONG_STOP

- **动机**：§279 把感知路径限制在 `ONLY_LONG` 上，理由是"psy 只做长窗"。但
  LONG_START / LONG_STOP **同样是单个 1024 点变换**，psy 对它们的唯一区别是
  `aacpsy.c:672` 的 pre-echo 判断：

  ```c
  if (!(wi->window_type[0] == LONG_STOP_SEQUENCE || (!w && wi->window_type[1] == LONG_START_SEQUENCE)))
      thr = FFMAX(RPEMIN*thr, FFMIN(thr, RPELEV*prev_quiet));
  ```

  即 LONG_STOP 帧、以及紧跟 LONG_START 的那一帧**不做** pre-echo 阻尼（过渡窗自身
  已经把攻击包进去了）。所以这不是"短窗不支持"，只是少传两个窗型。
- **改动**：
  - `AacPsyContext::analyze(channel, coefs, prev_bits, window_type, prev_window_type)`
    增加两个窗型参数，按上面的条件跳过 pre-echo 阻尼；`skip_preecho` 只在这两种
    情况为真。
  - 编码器 `use_psy` 从 `seq == aac_only_long` 放宽到 `seq != aac_eight_short`，
    过渡帧不再回退到简化模型；psy 拿到的是 `seq` 与 `self.prev_sequence[ch]`。
  - 新增测试 `aac_lc_perceptual_transient`：瞬态夹具走感知路径，断言 LAME 窗链
    （LONG_START → EIGHT_SHORT×5 → LONG_STOP）仍然成立且往返 SNR > 25 dB。
- **验证**（44100/128k，同一夹具）：
  - §277 的 3 项黄金测试**逐位不变**（夹具只喂 `ONLY_LONG, ONLY_LONG`，
    `skip_preecho` 为假，与改动前等价）。
  - 瞬态夹具走感知路径：start=1 / short=5 / stop=1，往返 SNR **31.84 dB**。
  - 四音夹具：往返 SNR **21.92 → 20.14 dB**，噪声-掩蔽比 12.90 → **12.96**（持平），
    帧长 356 字节不变。少掉的 SNR 落在过渡帧上，而过渡帧正是掩蔽阈值最高的地方，
    所以模型自己的度量没有变差——这与 §280 的结论一致：SNR 不是这条路径的目标。
  - 剩下的回退范围只有 EIGHT_SHORT（短窗 psy 是另一片工作：8×128 的 band 计划、
    grouping、`PSY_3GPP_AH_THR_SHORT`）。注意短窗帧在 LAME 链里总是被
    LONG_START/LONG_STOP 夹着，所以现在"回退"只影响真正的攻击帧本身。
- **检查**：`moon check --target wasm --deny-warn` 干净；`aac_psy_vectors` 3/3、
  `aac_twoloop_vectors` 3/3、`aac_encode_vectors` 9/9；全量
  `moon test --target wasm` **1391/1391** 通过。

## 283. AAC PNS：可行性勘察（第 1 步已落地，见 §284；其余未实现）

- **⚠ 状态**：本条的六步清单里，**第 1 步（`mark_pns`）已在 §284 完成**；
  **六步已全部完成**：§284 mark_pns、§286 twoloop 关联、§287 search_for_pns、
  §288 码流 off_pns 链 + 编码器接线 + 端到端。

- **为什么单独记一条**：PNS 看起来只是"再加一个 aaccoder 函数"，实际是四个子系统
  联动，而且有一处反直觉的语义（下面第 1 条）。先把勘察结果写下来，避免下一轮重新
  踩一遍；本条目**不代表任何代码已经落地**。
- **三个非显然的事实**（都读过 C 源确认）：
  1. **噪声带的电平不来自 `pns_ener`，而来自 twoloop 选出的 `sf_idx`。**
     `search_for_pns`（`aaccoder.c:489`）只写 `sce->pns_ener[]` 和 `band_type`，从不
     改 `sf_idx`；真正进码流的是 `encode_scale_factors`（`aacenc.c`）里按 `sf_idx`
     写出的标度因子。所以那段 `energy_ratio` 补偿只影响"要不要用噪声"的判定，
     不影响最终电平——照搬这段逻辑时不要以为它在设定噪声能量。
  2. **噪声带在码流里有独立的标度因子链。** `encode_scale_factors` 用
     `off_pns = sf_idx[0] - NOISE_OFFSET(90)` 起算，第一个噪声带写 **9 位前导**
     （`NOISE_PRE_BITS=9`, `NOISE_PRE=256`），之后才回到常规 Huffman 差分；这条链
     与普通带的 `off_sf` 并行维护。我们的 `aac_encode_write_scalefactors` 只有
     `off_sf` 一条链，需要加第二条 + 前导。
  3. **`can_pns` 会反向改 twoloop 的行为，共三处**（`aaccoder_twoloop.h`）：
     - `ff_pns_bits()`（:57）：噪声带占 9 位、紧随噪声带只需 5 位，这个计数进
       `tbits`（:387 与 :468 两处零带分支）；
     - :190：`if (pns || intensity_stereo) frame_bit_rate *= 1.15` → 改 **cutoff**，
       进而改哪些带被置零；
     - :500-536：`its > maxits/2 && tbits > toofewbits` 时的 spread 排序置零块
       （`pns_start_pos`、`euplims`、`overdist`），我们移植时整块删掉了。
- **依赖**：
  - `search_for_pns` 用 `s->fdsp->scalarproduct_float` / `vector_fmul_scalar`；C 侧
    对拍要在 harness 里补 `avpriv_float_dsp_alloc`（`libavutil.a` 里有）。
  - `lcg_random` 的序列会穿过 `energy_ratio` 影响判定，语义是
    `PNS[i] = (float)s->random_state`（整数转浮点，**不是**位模式重解释）；要逐位
    对拍必须同种子同序。
  - 常量：`NOISE_LOW_LIMIT=4000`、`NOISE_SPREAD_THRESHOLD=0.9`、
    `NOISE_LAMBDA_REPLACE=1.948`、`NOISE_BT=13`；解码侧的 `NOISE_OFFSET/PRE/PRE_BITS`
    已存在（`aac_spectrum.mbt:106-113`），解码噪声带没问题。
- **集成清单**（每步的验收标准）：
  1. `mark_pns`（`aaccoder.c:631`）→ 产出 `can_pns`/`pns_ener`。**可逐位对拍**
     （不碰 fdsp）。harness 已支持 `--in`，加个 `--pns` 打印 `can_pns` 即可。
  2. twoloop 三处耦合（上面第 3 条）→ 现有 245 条黄金向量必须保持逐位一致
     （它们 `can_pns=0`，走不到新分支）。
  3. `search_for_pns` → `band_alt` 快照（twoloop 的 `band_type` 副本）要先加进
     通道数据结构。
  4. 码流：`off_pns` 链 + 9 位前导 + 噪声带跳过谱数据（`cb == 13`）。
  5. 端到端：解码 SNR 不塌、帧长下降。
  6. 夹具：现有夹具是纯音/脉冲，`spread` 阈值（0.9）几乎过不了，**必须新增带限
     噪声突发**才能让 PNS 真正触发；否则测试等于没测。
- **结论**：这是一整轮的量（≈300 行 + 一处码流改动 + 新夹具 + 两套对拍）。本轮
  只做勘察，没有动代码。

## 284. AAC PNS 第 1 步：`mark_pns` 移植 + 逐带向量

- **交付**：
  - `libavcodec/aac_pns.mbt`：`aac_pns_mark(bands, offsets, num_swb, sample_rate,
    bit_rate, channels, lambda)`，即 `aaccoder.c:631` 的 `mark_pns`（单长窗）。
    三道门限：4 kHz 以上且低于带宽 cutoff；能量不低于
    `threshold·sqrt(1.5/freq_boost)`；`spread ≥ spread_threshold`（λ=120 时 0.75）。
  - `libavcodec/aac_pns_vectors/`：5 帧 × 49 带向量（`can_pns` 精确、`pns_ener`
    相对 1e-6），由 `scripts/gen_aac_pns_vectors.py` 从
    `_ref/aacenc/aac_ref.exe --pns` 生成。
  - `_ref/aacenc/harness.c`：新增 `--pns` 模式——用 LCG 噪声谱（纯音谱的
    spread 到不了 0.9，PNS 永不触发）+ `options.pns=1` + `s->lambda=120`，在
    twoloop 之前调 `mark_pns` 并打印 `PSNMARK`。默认模式输出**零 diff**。
- **结果**：5 帧共 **245 带 `can_pns` 全等**（其中 99 带为 1，全部落在 4 kHz
  与 cutoff 之间），`pns_ener` 相对误差 < 1e-6。
- **两个新踩的坑**（都值得记住）：
  1. **psy 的 cutoff 是粘性的**。C 的 twoloop 每帧把 `bandwidth` 写回
     `s->psy.cutoff`；PNS 打开时它先给 per-channel 码率乘 1.15，于是第 1 帧起 psy
     用 16600 Hz（不乘是 16000）。我们的移植每帧重算，结果第 1 帧第 43 带的
     energy 被算成 0（C 是 4.04e7），`can_pns` 直接判错。已给 `AacPsyContext` 加
     `mut cutoff`（0 = 自动）+ `cutoff()` / `set_cutoff()`，`analyze` 优先用它；
     既有测试不设置，所以 §277 3/3、§278 3/3、§281 的编码器 9/9 **逐位不变**。
  2. `cutoff = bandwidth * 2 * wlen / sample_rate`，第一版漏了 `2`，从 band 32
     起全线判错（0 对 1）。
- **下一步**：§283 清单的第 2 步（twoloop 三处耦合：`ff_pns_bits` 进 `tbits`、
  `frame_bit_rate ×= 1.15` 影响 cutoff、`its > maxits/2` 的 spread 置零块）。
  现有 245 条 twoloop 黄金向量必须保持逐位一致（它们 `can_pns=0`）。
- **检查**：`moon check --target wasm --deny-warn` 干净；`aac_pns_vectors` 1/1、
  `aac_psy_vectors` 3/3、`aac_twoloop_vectors` 3/3、`aac_encode_vectors` 9/9；
  全量 **1392/1392** 通过。

## 285. PNS 第 2 步的拆解试验（**代码已回退**，结论：不可拆）

- **动机**：§283 把 twoloop 的 PNS 关联列成三处，想先搬"便宜"的两处
  （`ff_pns_bits` 的 9/5 位进 `tbits`、`frame_bit_rate *= 1.15` 改 cutoff），
  把 `its > maxits/2` 的 spread 排序置零块留到后面。
- **做法**：给 `aac_twoloop_search_pns` 加 `AacTwoPns { can_pns, on }`（老入口
  `aac_twoloop_search` 变薄封装，`can_pns` 全 0、`on=false`，保证 §278 的 245 条
  黄金向量不受影响）；用 §284 的噪声夹具跑 `mark_pns → twoloop`，与 C 的
  `--pns` 输出逐带比 `sf/cb/zeroes`。
- **结果**：**177 / 245 带不一致**，集中在 scalefactor 差 1–2（码本大多相同），
  说明只搬两处不够——预算轨迹从一开始就被 PNS 改变了。**已按惯例整体回退**
  （`git checkout -- libavcodec/aac_twoloop.mbt` 与对应测试），避免留下一个
  "接口在、结果不对"的半成品。
- **原因（把 C 源码重新扫了一遍）**：`can_pns` 不止影响那三处，还进入**初始
  band 设置**（`aaccoder_twoloop.h:248`）：

  ```c
  if (nz && sce->can_pns[w*16+g]) {
      spread_thr_r[w*16+g] = energy * nz / (uplim * spread);
      /* 同时维护 min_spread_thr_r / max_spread_thr_r */
  }
  ```

  这两个全局量后面喂给 `uplmax`（:590）和排序置零块（:500-590），
  还有 `pns_start_pos`（:201）与 `euplims`。换句话说：**PNS 从"算初始失真上界"
  那一刻起就在改 twoloop 的轨迹**，不是最后才介入。所以第 2 步必须一次性把
  初始设置 + 两处位计数 + 排序置零块全搬，然后一起对拍；分批搬必然得到
  "差 1 个 sf"这种最难定位的偏差。
- **保留下来的东西**（代码没白写）：
  - `scripts/gen_aac_pns_vectors.py` 现在会一并生成 `--pns` 模式的
    `TWOBAND` 结果（fixtures 里 `aac_pns_ref_sf` / `_cb` / `_zero`，
    5 帧 × 49 带）——下一轮实现完直接照着写断言即可。
  - `--pns` 模式下 C 的 `TWOPARM` 证实 cutoff 从 743 变 **770**（1.15 系数生效），
    与 §284 的 psy 粘性 cutoff 结论对上了。
- **检查**：回退后 `aac_pns_vectors` 1/1 + `aac_twoloop_vectors` 3/3 +
  `aac_psy_vectors` 3/3 复验绿（合计 7/7）；`moon check --target wasm
  --deny-warn` 干净。本条只改文档、生成器与夹具数据，**代码与 §284 提交时
  逐字节相同**，全量沿用该次 **1392/1392** 结果（本轮两次全量都撞上已知的
  编译期挂死，日志停在 56 KB 不再增长，已按工具坑记录杀掉）。

## 286. PNS 第 2 步完成：twoloop 的 PNS 关联一次性全搬，245 带逐位一致

- **做法**：按 §285 的结论不再拆分，把 `can_pns` 影响到的**所有**块补进
  `aac_twoloop.mbt`，由同一个 `aac_twoloop_search_pns(..., pns : AacTwoPns)`
  承载；老的 `aac_twoloop_search` 变成薄封装（全 0 `can_pns` + `on=false`），
  保证 §278 的黄金向量路径逐位不变。具体六块：
  1. **初始 band 设置**（`aaccoder_twoloop.h:248`）：累加 psy 的 `spread`，
     算出 `spread_thr_r[g] = energy*nz / (uplim*spread)`，并维护
     `min_spread_thr_r` / `max_spread_thr_r`。
  2. **`pns_start_pos`**（:201）= `NOISE_LOW_LIMIT * 2 * wlen / sample_rate`。
  3. **`ff_pns_bits`**（:57）：零带分支里 9 位 / 5 位（紧随噪声带）计入
     `tbits`；`AacTwoLoop.collect()` 共用，两处 collect 都自动生效。
  4. **1.15 带宽系数**（:190）：`frame_bit_rate *= 1.15` 后用
     `aac_psy_cutoff(fbr, 1, sample_rate)` 重算，cutoff 从 743 → 770。
  5. **`overdist` 循环重构**（:448）：恢复 C 的
     `for (i = 0; i < 2 && (overdist || recomprd); ++i)`，不再用
     "PNS 关所以只 collect 两次"的简化。
  6. **排序置零块**（:500-570）：`ovrfactor`、`zspread`（含 `FFMIN3` 与
     bit-starvation 插值）、`maxzeroed`、两遍 `zloop`（第一遍只挑超失真带、
     第二遍放宽 `loopminsf`），命中即 `zeroes=1 / cb=0`，
     `zeroed != 0` 时 `recomprd = fflag = 1`。
- **验收（两批黄金数据）**：
  - §278 的 **245 带 `sf/cb/zeroes` 逐位不变**（3/3 测试）。
  - §284/§285 夹具里 C `--pns` 的 **245 带 `sf/cb/zeroes` 0 差异**
    （上一轮只搬两处时是 177 差异）。编码器四个相关包合计 17/17，
    往返 SNR / NMR / 帧长全部不变（`aac_twoloop_search` 走的仍是无 PNS 路径）。
- **结论**：§285 的"不可拆"判断得到证实，且一次性全搬确实可行——关键是
  把 `spread_thr_r` 的初始化和 `overdist` 循环一起恢复，而不是只补位计数。
  现在 §283 清单的第 2 步已完成；`AacTwoPns` 与 `search_pns` 入口都有逐位
  黄金数据，不是半成品接口。
- **仍未做**（§283 第 3–6 步）：`search_for_pns`（需要 `avpriv_float_dsp_alloc`
  才能做 C 侧对拍）、`band_alt` 快照、码流的 `off_pns` 链 + 9 位前导、
  `can_pns` 接进编码器、带限噪声夹具端到端。
- **检查**：`moon check --target wasm --deny-warn` 干净；`aac_psy_vectors` 3/3、
  `aac_twoloop_vectors` 3/3、`aac_pns_vectors` 2/2、`aac_encode_vectors` 9/9
  （合计 17/17）；全量 `moon test --target wasm` **1393/1393** 通过。

## 287. PNS 第 3 步：`search_for_pns` 移植 + 逐带向量（0 / 245 差异）

- **C 侧准备**：`_ref/aacenc/harness.c` 的 `--pns` 模式现在在 twoloop 之后调用
  `ff_aac_coders[TWOLOOP].search_for_pns`，并打印 `PNSFINAL g band_type sf_idx
  zeroes pns_ener`。`search_for_pns` 需要 `s->fdsp`，用
  `avpriv_float_dsp_alloc(0)`（`libavutil/float_dsp.h`，已在 libavutil.a 里）补上；
  `s->random_state` 由 calloc 置 0 且跨帧累积。默认模式输出**零 diff**。
- **交付**：`libavcodec/aac_pns_search.mbt`，`aac_pns_search` 即
  `aaccoder.c:489` 的 `search_for_pns`（单长窗）。六道门限：
  1. 4 kHz 以上且在带宽 cutoff 内（带宽同样按 `frame_bit_rate × 1.15` 算）；
  2. `ff_sfdelta_can_remove_band` 允许摘掉该带；
  3. 能量既不低于 `threshold·sqrt(1/freq_boost)`，也不高于
     `threshold·thr_mult·freq_boost`（`thr_mult = 1.948·100/λ`）；
  4. psy `spread ≥ spread_threshold`（λ=120 时 0.75）；
  5. `noise_sfi` 与上一个噪声带的差落在 `±SCALE_MAX_DIFF` 内；
  6. 生成噪声的 `energy_ratio ∈ (0.85, 1.25)` 且估算 RD 代价更优。
  命中则 `band_type = NOISE_BT(13)`、`zeroes = 0`。
- **数值细节**（对拍一次就过的关键）：
  - `lcg_random` 的语义是 `PNS[i] = (float)state`——**整数转浮点，不是位模式重解释**；
    且 state 跨帧携带，测试里逐帧回灌。
  - `noise_amp = -pow2sf_tab[noise_sfi + 200]`；`roundf` 是 half-away-from-zero，
    不能用 `to_int` 直接截断（可能为负）。
  - `dist1` 的量纲与判定依赖传入的 `lambda`，`scalarproduct` 用 f32 逐次累加，
    不是 f64 求和。
  - **`sf_idx` 不受此函数影响**——噪声电平来自它（§283 第 1 条），`pns_ener`
    只喂代价估计；输出结构因此不返回 `sf`。
- **验收**：C `--pns` 的 `PNSFINAL` 5 帧 × 49 带，
  `band_type / sf_idx / zeroes` **0 差异**，其中 **50 带被判为 NOISE_BT**。
  夹具由 `scripts/gen_aac_pns_vectors.py` 一并生成
  （`aac_pns_ref_final_bt / _sf / _zero / _ener`），默认模式向量零 diff。
  §278 的 245 带、§286 的 245 带仍然全部逐位一致。
- **仍未做**（§283 第 4–6 步）：`band_alt` 快照接进编码器通道结构、码流的
  `off_pns` 独立标度因子链 + 9 位前导、噪声带跳过谱数据、带限噪声夹具端到端。
- **检查**：`moon check --target wasm --deny-warn` 干净；`aac_pns_vectors` 3/3、
  `aac_psy_vectors` 3/3、`aac_twoloop_vectors` 3/3、`aac_encode_vectors` 9/9；
  全量 `moon test --target wasm` **1394/1394** 通过。

## 288. PNS 第 4–6 步完成：接进编码器并端到端解码

- **交付**：PNS 现在能在 `aac_encode.mbt` 里真正产出噪声带。
  - `AacEncodeConfig.pns`（默认关）+ `aac_encode_config_with_pns(config)`；
    默认关闭是为了不改动 §279/§280 的往返基线，也避免静默改变码流格式。
  - `AacEncoder.pns_random_state` 跨帧携带（`s->random_state`）。
  - psy 分支按 `mark_pns → twoloop_search_pns → search_for_pns` 串联，
    `channel_data_psy` 接收最终的 `cb / zeroes / noise_sfi`；NOISE_BT 带不再
    量化谱、不写谱数据。
  - `aac_encode_write_scalefactors` 增加**独立 `off_pns` 差分链**（起点
    `global_gain - NOISE_OFFSET(90)`）与首个噪声带的 **9 位前导**
    （`diff + NOISE_PRE(256)`）；`aac_encode_write_spectral` 跳过 `cb == 13`。
    与 `aac_decode_scalefactors` 的实现逐位对应。
- **关键发现（本轮最值钱的一条）**：噪声带写进码流的 scalefactor **必须用
  `search_for_pns` 算出的 `noise_sfi`，不是 twoloop 的 `sf_idx`**。
  - 原理：`search_for_pns` 用 `noise_amp = -pow2sf_tab[sfi + 200]` 生成噪声，
    而解码器（FFmpeg `dequant_scalefactors` 与我们的 `aac_sf_dequant`）对
    NOISE_BT 用 `sf = -pow2sf_tab[sfo + 200]`。二者相等当且仅当
    **`sfo = sfi`**，所以噪声链里就写 `noise_sfi`。
  - 实测对照（宽带噪声夹具，解码后 PCM 峰值 / SNR）：写 `two.sf-4` →
    peak **137796**、SNR −111 dB；写 `two.sf-104` → peak 54.7、−39 dB；
    写经验值 `two.sf-138` → peak 0.318、+0.9 dB；写 `noise_sfi` →
    **peak 0.318、SNR 0.68 dB** 且**无需任何魔法常数**。
  - 因此 `AacPnsSearchOut` 新增 `noise_sfi` 数组（命中 NOISE_BT 时记录），
    编码器直接把它写进 `off_pns` 链。
- **端到端**（44100 / 128 kbps / 2ch，8 帧 LCG 白噪声）：不开 PNS 平均
  **357 字节/帧**，开启后 **344 字节/帧**（噪声带省掉谱数据）；解码输出峰值
  0.318（原信号幅度 0.15 量级），无 NaN / 无削波。SNR 约 0.7 dB 是 PNS 的
  固有性质——解码器合成的是**新的**噪声，只能保证感知等价，不保证波形等价，
  所以测试的护栏是"能干净解码 + 幅度有限 + 码长下降"，不是 SNR 阈值。
- **工具坑**：先写了一个 `libavcodec` 包内的白盒测试验证写出器往返，触发
  wasm 编译 `local count too large`（包 init 函数局部变量已达上限，新增任何
  test 都会越界）。已把该文件移入 `.trash/`，改为 `aac_encode_vectors` 包的
  端到端黑盒测试。**后续给 libavcodec 加白盒测试前要注意这个上限**。
- **验收**：§278 / §286 / §287 三批 245 带黄金数据仍全部 **0 差异**；
  `moon check --target wasm --deny-warn` 干净；`aac_pns_vectors` 3/3、
  `aac_encode_vectors` 10/10；全量 **1395/1395** 通过。
- **至此 §283 的六步清单全部完成**：`mark_pns`、twoloop 的 PNS 关联、
  `search_for_pns`、码流 `off_pns` 链、编码器接线、带限噪声夹具端到端。

## 289. AAC 编码器能力总览与配置矩阵（阶段性收口）

> 本节把 §271–§288 分散在各条里的接线、黄金数据与实测值汇总成一张表，
> 供后续接手的人按图索骥。**代码上只做了一处 API 加固**（见末尾）。

- **三个配置入口**（`libavcodec/aac_encode.mbt`）：

  | 入口 | `perceptual` | `pns` | 稳态长窗 / 过渡长窗 | 8 短窗帧 |
  |---|---|---|---|---|
  | `aac_encode_config`（默认） | false | false | 简化模型 + 全局 delta 二分 | 同左 |
  | `aac_encode_config_perceptual` | true | false | psy → twoloop → λ 严格重试 | 简化模型 + 二分 |
  | `aac_encode_config_with_pns(...)` | true | true | 上述 + `mark_pns` → `search_for_pns` → `off_pns` 码流 | 简化模型 + 二分 |

  过渡长窗（LONG_START / LONG_STOP）从 §282 起也走感知路径；只有
  EIGHT_SHORT 仍回退，因为 psy 移植只做单窗口、twoloop 只做 `num_windows=1`。
- **本轮 API 加固**：`aac_encode_config_with_pns` 现在强制 `perceptual = true`。
  此前若对默认配置调用它，`pns = true` 会因为 `use_psy` 为假而**静默失效**；
  新增 `aac_lc_pns_config_enables_perceptual` 断言这个组合。
- **每层算法的 C 源与黄金数据**：

  | 层 | C 源 | 黄金数据 | 验收 |
  |---|---|---|---|
  | 3GPP psy | `aacpsy.c:646` | 5 帧 × 49 带（§277） | 3/3，逐位 |
  | twoloop | `aaccoder_twoloop.h:65` | 245 带（§278） | 3/3，逐位 |
  | `mark_pns` | `aaccoder.c:631` | 245 带（§284） | 0 差异 |
  | twoloop 的 PNS 关联 | `aaccoder_twoloop.h:190/248/387/468/500` | 245 带（§286） | 0 差异 |
  | `search_for_pns` | `aaccoder.c:489` | 245 带（§287） | 0 差异，50 带为 NOISE_BT |
  | 码流 `off_pns` 链 | `aacenc.c encode_scale_factors` | 端到端（§288） | 解码干净、码长下降 |

  参考工具链：`_ref/aacenc/`（`bash build.sh`；`./aac_ref.exe` 默认向量模式，
  `--pns` 跑完整 PNS 链，`--in <dump>` 喂外部谱）。
- **实测**（44100 Hz / 128 kbps / 2ch，8 帧四音 + 静音 flush）：

  | 路径 | 码率 | 往返 SNR | 平均 NMR |
  |---|---|---|---|
  | 默认（简化模型） | 117.1 kbps | 29.05 dB | 14.45 dB |
  | perceptual（无 PNS） | 122.7 kbps | 20.14 dB | 12.96 dB |

  PNS 端到端用的是**另一套白噪声夹具**（纯音的 spread 到不了 0.75，PNS 不会
  触发）：344 B/帧 vs 不开启的 357 B/帧，解码峰值 0.318、SNR ≈ 0.7 dB。
  SNR 低是 PNS 的固有性质（解码器合成新噪声，只保感知等价），不能与上表直接比。
- **已知偏差（相对 `aacenc.c`，仍有意为之）**：
  1. psy 分析的是 M/S 之后的谱；FFmpeg 分析 L/R、量化后才做 M/S（§3 的
     A/B 实测无差别，保留自洽版本）。
  2. psy 每帧只分析一次；FFmpeg 的 λ 重试循环里每轮都重新 `analyze`。
  3. `planned_max_sfb` 取 psy `bits > 0` 的最后一带，帧级固定。
  4. TNS 在 twoloop/PNS 之前作用于谱；FFmpeg 在量化之后做 M/S/TNS。
  5. 噪声带的码流 scalefactor 写 `noise_sfi` 而非 twoloop 的 `sf_idx`
     （§288 有原理与实测对照——这是本移植标度体系下的正确值）。
- **剩余缺口**（按建议顺序）：短窗 psy（8×128 band 计划 + grouping +
  `PSY_3GPP_AH_THR_SHORT`，需同时扩展 twoloop 到多窗口）；强度立体声
  （`aacenc_is.c`，码流的 `off_is` 链我们解码器已支持）；是否把 PNS 设为默认
  （目前 opt-in，为保住上表基线）。
- **检查**：`moon check --target wasm --deny-warn` 干净；`aac_encode_vectors`
  11/11、`aac_pns_vectors` 3/3；全量 **1396/1396** 通过。

## 290. AAC 强度立体声（IS）：off_is 码流链 + 简化判据 + ffmpeg 外部解码对拍

- **交付**：`aacenc_is.c` 的强度立体声现在能在 `aac_encode.mbt` 里真正产出
  14/15 带。
  - `AacEncodeConfig.is_stereo`（默认关，保住 §279/§280/§288 的基线）+
    `aac_encode_config_with_is(config)`。
  - `aac_encode_is_search`：照搬 FFmpeg 的频率门
    `start * freq_mult > INT_STEREO_LOW_LIMIT * (lambda / 170)`
    （`INT_STEREO_LOW_LIMIT = 6100`，λ=120 时 44.1 kHz 下约 4306 Hz），
    带内互相关定相位（同相 `INTENSITY_BT=15`、反相 `INTENSITY_BT2=14`），
    左声道换成 `(L + p·R) · sqrt(ener0/ener01)`，与 `apply_intensity_stereo`
    逐式一致（能量对齐回原左声道）。
  - `aac_encode_mark_is` + `aac_encode_write_scalefactors` 的**第三条独立差分链
    `off_is`**（起点 0、逐带 ±60 裁剪；等价于上游
    `set_special_band_scalefactors` 的 `prevscaler_i` 链套上
    `encode_scale_factors` 的 `off_is`），`aac_encode_write_spectral` 跳过
    14/15——对齐 `quantize_and_encode_band_cost_STEREO`：`aaccoder.c:94` 的
    `if (BT_ZERO || BT_NOISE || BT_STEREO)` 分支一个谱位都不写。
- **带类型表（`aac.h:74`，最容易记反的一条）**：`RESERVED_BT = 12`、
  `NOISE_BT = 13`、**`INTENSITY_BT2 = 14` 是反相**、**`INTENSITY_BT = 15`
  是同相**；编码侧 `apply_intensity_stereo` 与解码侧
  `aacdec_dsp_template.c:131` 用的是同一个式子 `p = -1 + 2 * (bt - 14)`，
  本移植的 `aac_encode_apply_is` 与 `aac_apply_stereo` 也必须同式。
- **本轮最值钱的坑**：IS 带写进码流的不是标度因子，而是**强度位置**，
  范围 `[-155, 100]`，**正负一样常见**，于是本移植原先「`sf[b] >= 0` 表示该带
  被编码」的哨兵在 IS 带上直接失效。
  - `aac_encode_write_scalefactors` 必须**先用 `cb` 认出 14/15 带**再谈 `sf`，
    否则位置为负的 IS 带会被当成零带整体跳过，解码器读到的 section 与
    标度因子链从那条带起全部错位。
  - `aac_encode_global_gain` 同理必须跳过 IS 带，否则某条强度位置会变成整帧的
    全局增益，把常规链的起点带偏；顺带也跳过了噪声带（§288 的
    357→344 B/帧与 0.676 dB 指标实测不变）。
  - `off_is` 的 ±60 链裁剪保证 diff 落在 `[0,120]`，正好是标度因子 VLC 的
    定义域，所以写侧不需要再兜底（上游是 `av_assert0(diff >= 0 && diff <= 120)`）。
- **与 M/S 的关系——为什么这一版是"简化判据"**：FFmpeg 的顺序是
  `psy → mark_pns → twoloop → TNS → search_for_pns → search_for_is →
  search_for_ms → adjust_frame_information → 写码流`，即 IS 是在**左右声道各自
  的谱已被 twoloop 定过标度因子**之后跑的，`aac_is_encoding_err` 才能拿
  `quantize_band_cost(..., lambda / threshold)` 分别给 L、R 和 IS 和定价。
  本移植是 §289 偏差 1（先 M/S 再 psy/twoloop），那一刻已经没有"左右声道"可评，
  所以 IS 只能与 `aac_encode_ms_mask` 同级：先用带能量与互相关
  （`|rho| >= 0.95`）选带与相位，再把这些带从 `ms_mask` 里清掉——等价于上游
  `search_for_ms` 的 `if (!cpe->is_mask[g]) cpe->ms_mask[g] = 0` 加上
  `apply_mid_side_stereo` 的 `is_mask || band_type >= NOISE_BT` 双重排除
  （解码侧 `aac_apply_stereo` 用 `sect_cb < 13` 做同一排除）。
  取舍式子写在 `aac_encode_is_search` 的注释里：右带被重建为
  `(p·L + R) · m`，`m = sqrt(ener1 / ener01)`，误差能量
  `2·ener1 - 2m(|cross| + ener1)`，等能量双声道下即
  `ener1 · (2 - sqrt(2(1 + |rho|)))`——`|rho| = 0.95` 时是右带能量的 2.5%，
  `|rho| = 1` 时为 0。
- **与 PNS 互斥**：本移植的 IS 决策在 twoloop / `search_for_pns` **之前**，
  无法预知哪些带最终会变成 NOISE_BT（上游在那之后跑 IS 并显式排除），
  因此配置层双向挡死：`aac_encode_config_with_is` 清 `pns`，
  `aac_encode_config_with_pns` 清 `is_stereo`（`aac_lc_is_config` 断言）。
- **实测**（44100 / 128 kbps / 2ch，感知路径，4 帧 + 1 收尾帧）：

  | 夹具 | IS 带 | 越门 IS 带 | 左声道 IS 带 | 码长 | R 的 9 kHz 电平 | 往返 SNR |
  |---|---|---|---|---|---|---|
  | 共享 9 kHz（低音 L 440 / R 550 不同） | 10 | 0 | 0 | 1831 → 1802 B | **+0.38 dB** | 18.60 → 18.54 dB |
  | 高音不共享（L 9 kHz / R 11.7 kHz） | 0 | 0 | 0 | 1783 = 1783 B | — | — |

  - IS 只在"确实共享了高音"的带触发；去相关时**码流逐字节不变**，说明这个
    工具在没有把握时完全惰性（`aac_lc_intensity_stereo_skips_uncorrelated`
    断言的是相等而不是近似）。
  - 共享夹具 5 帧省 29 字节（-1.6%），SNR 只动 0.06 dB：IS 牺牲的是右声道
    该带的独立性，不是精度。音调电平 +0.38 dB 是 `off_is` 位置链的直接验证——
    位置错一步就是 6 dB。
- **外部对拍（本移植第一次让 ffmpeg 解我们自己写的 IS 码流）**：临时 main 包
  把 8 帧 IS 流与本解码器的 PCM 打到 stdout，交 docker
  `linuxserver/ffmpeg:9.0.2` 解码：
  - `9 packets read (3279 bytes); 9 frames decoded; **0 decode errors**
    (9216 samples)`，`-v info` 零告警（既没有 `Clipped intensity stereo
    position`，也没有 `invalid scalefactor code`）。
  - 本解码器 vs ffmpeg 的 PCM：**最大差 1.192e-7、RMS 差 2.49e-8、
    相对参考 SNR 138.76 dB**，即逐样本亚 LSB 一致。这说明 `off_is` 链的
    写法与上游 `dequant_scalefactors` / `apply_intensity_stereo` 的语义
    确实对得上，而不只是自洽。
- **工具坑**：
  - 临时 main 包不能放 `.scratch/` 下——moon 的包扫描跳过点目录，
    `moon run .scratch/xxx` 直接报 `does not match any known package`。
    本轮放在 `zz_is_dump/`，用完移入 `.trash/2026-11-05/`。
  - 原生目标在这台 Windows 上要求 MSVC（`cl.exe`/`clang-cl.exe`），本机没有，
    所以 `moon run` 只能走默认的 wasm 目标；写不了文件就 `println` 出来交给
    Python 解析（`Byte::to_string()` 打的是 `b'\xFF'`，Python 侧
    `ast.literal_eval` 可直接还原成字节）。
  - 包依赖只写在 `moon.pkg` 的 `import {}` 里；源码文件里再写一遍会报
    `[3001] Invalid import declaration here. Move this declaration to moon.pkg`。
    用 `@math` 也要在 `moon.pkg` 里 `import "moonbitlang/core/math"`。
  - MoonBit 不把 `a = x\n + y` 当续行（多行算术要写成独立 `let` 或加括号）。
- **仍未做**：
  1. 真正的 `aac_is_encoding_err` 成本判据。前置条件是把 M/S 挪到
     psy/twoloop 之后（顺带消掉 §289 偏差 1/4），那是一次要有度量、要有黄金
     数据的重构；`_ref/aacenc` 还需把 `aacenc_is.c` 纳入 harness
     （依赖 `find_max_val`/`find_min_book`/`quantize_band_cost`/
     `ff_init_nextband_map`/`ff_sfdelta_can_remove_band`，这些本移植在
     `aac_twoloop.mbt` / `aac_pns_search.mbt` 里都已有对应实现）。
  2. 短窗 IS：代码按 `num_windows` 分组求能量、门限用每窗带内偏移，结构上
     走得通，但没有专门的 8 短窗 IS 夹具验证过。
  3. `set_special_band_scalefactors` 的完整形态（上游一轮里同时管 IS 位置
     `round(log2(is_ener)*2)` 和噪声 `3 + ceil(log2(pns_ener)*2)`）——本移植的
     噪声值仍取 `search_for_pns` 的 `noise_sfi`（§288 的结论）。
- **检查**：`moon check --target wasm --deny-warn` 干净；`aac_encode_vectors`
  14/14（新增 `aac_lc_is_config`、`aac_lc_intensity_stereo_round_trip`、
  `aac_lc_intensity_stereo_skips_uncorrelated`）、`aac_pns_vectors` 3/3、
  §278/§286/§287 的三批 245 带仍逐位一致；全量 **1399/1399** 通过。

## 291. 感知路径改为 FFmpeg 的顺序：分析读左右谱，量化读混合后的谱

- **交付**：§290 留下的前置条件做掉了，`aacenc.c` 的立体声工具顺序在感知路径上
  对齐。
  - 之前（§289 偏差 1 与 4）：`M/S → psy → TNS → twoloop → PNS → 量化`。
  - 现在：`psy → TNS →（每轮 λ 重试：twoloop → PNS → 强度 → M/S → 量化 → 写）`。
    分析与定价消费**左右谱**，写码流的量化消费**混合后的谱**——这正是 FFmpeg 的
    做法：`search_for_quantizers` 跑在 `apply_mid_side_stereo` 之前，而码流阶段的
    `encode_spectral_coeffs` 拿最终的 `sce->coeffs` 重新量化一次。
  - 默认（简化）路径**顺序不动**：它的每带标度因子本来就是从"真正要编码的那份谱"
    上算出来的，先混合更准；也因此 §279/§280 的 117.064 kbps / 29.045 dB 基线
    逐字节不变（实测到小数点后 14 位一致）。两条路径的分歧只剩"标度因子从谁的谱
    算出来"，各自自洽。
  - 新增 `AacEncStereo`（`ms_present` / `ms_mask` / `is_bt` / `is_pos`）与
    `aac_encode_stereo`：按 FFmpeg 的顺序先 `apply_intensity_stereo`、后
    `apply_mid_side_stereo`，并复制上游的两条排除——强度带不进 M/S 掩码
    （`search_for_ms` 的 `if (!cpe->is_mask) cpe->ms_mask = 0`），**任一声道判成
    `NOISE_BT` 的带也不进**（`apply_mid_side_stereo` 自己的
    `band_type >= NOISE_BT` 守卫）。第二条是本移植先前缺的一块：解码侧
    `aac_apply_stereo` 对 `sect_cb >= 13` 的带根本不做 M/S 逆变换，编码侧若仍
    混合，两边就会对不上。
  - 噪声带随 λ 变，所以混合必须**每轮重试重做**：`coefs` 保持左右谱不被污染，
    每轮在 `coefs[ch].copy()` 上混合再量化。这顺手解开了 §290 里"IS 与 PNS 谁
    先谁后"的死结——两次重试之间噪声决策变了，掩码就得跟着变。
- **实测对照**（44.1 kHz / 128 kbps / 2ch，感知路径；默认路径与 §289 完全一致）：

  | 指标 | §289 | §291 |
  |---|---|---|
  | 感知往返 SNR（四音夹具） | 20.14 dB | **21.31 dB** |
  | 感知平均帧长 / 码率 | 356 B / 122.7 kbps | 364 B / 125.4 kbps（目标 371 B） |
  | 感知 NMR（噪声-掩蔽比） | 12.96 dB | 13.36 dB（默认路径 14.45，优势 1.49 → 1.09 dB） |
  | NMR 夹具的谱 SNR | 20.12 dB | 21.34 dB |
  | PNS 白噪声夹具 | 357 → 344 B/帧，SNR 0.68 dB | 344 → 332 B/帧，SNR 0.95 dB |
  | IS 共享高音夹具 | 10 带，1831 → 1802 B，电平 +0.38 dB，SNR 18.60 → 18.54 | 10 带，1744 → 1711 B，电平 **−0.03 dB**，SNR 22.92 → 22.35 |

- **读法**：分析改读左右谱以后，感知路径在三个夹具上分别拿到 **+1.17 dB**
  （四音）、**+0.27 dB**（PNS 白噪声）与 **+4.32 / +3.81 dB**（IS 夹具的
  不开/开 IS 两侧）的往返 SNR，且平均帧长仍落在预算内。IS 的音调重建电平从
  +0.38 dB 收到 −0.03 dB 是同一件事的另一面：标度因子现在是从该带**真实的左右
  能量**定出来的，不再是混合后的能量。§280 的取舍结论方向不变（感知路径 NMR 仍低于默认路径，
  `aac_lc_encoder_round_trip_noise_to_mask` 的断言原样通过），但优势从 1.49 dB
  缩到 1.09 dB——因为 SNR 追上来了，两条路径在靠近，这正是 §280 想看到的收敛。
- **外部对拍**（§290 那条链路重跑一遍）：8 帧 IS 流现在是 **3102 字节**（§290 时
  3279 字节），ffmpeg 9.0.2 报 `9 frames decoded; 0 decode errors (9216 samples)`
  且 `-v info` 零告警；ffmpeg 的 PCM 与本解码器的 PCM **最大差 1.49e-7、
  RMS 差 2.43e-8、相对参考 SNR 138.92 dB**——换了顺序、加了噪声带排除之后，
  码流仍然逐样本对得上参考解码器。
- **仍未做**：`aac_is_encoding_err` 的真成本判据（前置条件现已满足：左右谱的
  `sf_idx` / `band_type` / `zeroes` 与每带掩蔽阈值在 IS 决策时都拿得到）；
  IS 与 PNS 的组合仍保持互斥，虽然顺序已经允许打开，这一步要单独验证；
  `search_for_ms` 的完整形态（`sid_sf_boost` 四轮 + `ff_sfdelta_can_replace`）。
- **检查**：`moon check --target wasm --deny-warn` 干净；`aac_encode_vectors`
  14/14、`aac_pns_vectors` 3/3、§278/§286/§287 的三批 245 带仍逐位一致；
  默认路径的每条打印指标与 §289 的日志逐字符相同（两次全量日志 diff 只有
  感知路径的 5 行变化）；全量 **1399/1399** 通过。

## 292. 强度立体声换上 FFmpeg 的真成本判据（harness `--is` + 逐位向量）

- **交付**：§290 的简化门（频率门 + `|rho| >= 0.95`）在感知路径上被 FFmpeg 的
  成本比较替掉，而且这条比较是**逐位对拍**过的。
  - **C 侧**：`_ref/aacenc` 新增 `--is <dump>` 模式（`harness.c` 的
    `run_is_input`），编译真实的 `aacenc_is.c`（原先在 harness 里只是个空桩，
    现在 `build.sh` 产出 `aacenc_is_patched.o` 并链进去）；`patch.py` 把
    `aac_is_encoding_err` 的 `pass/dist1/dist2` 按带号与相位导出成全局，
    harness 打 `ISBAND`（决策）与 `ISERR`（两相位成本）两类行。踩到的一处关键：
    `ff_psy_init` 的 `group_map[0]` 要写 **1** 才得到"一个 2 声道的 group"
    （`psymodel.c:52` 是 `num_ch = group_map[i] + 1`），否则
    `s->psy.ch[1].psy_bands` 根本不是 CPE 的第二声道。默认模式与 `--pns`
    模式的输出在改动后逐字节不变（510 行向量 diff 为空）。
  - **MoonBit 侧**：`libavcodec/aac_is.mbt` 是 `ff_aac_search_for_is` +
    `aac_is_encoding_err` 的逐式移植，f32 建模沿用 `aac_two_*` 一族
    （`abs_pow34`/`find_max_val`/`find_min_book`/`quantize_band_cost`/
    `ff_init_nextband_map`/`ff_sfdelta_can_remove_band` 全部复用 §278/§287
    已对拍过的实现）。公共入口 `aac_is_in(...) -> aac_is_search(in, sample_rate,
    lambda)`。C 里唯一故意离开 `float` 的一步是
    `IS[i] = (L + phase*R) * sqrt(ener0/ener01)`：除法在 float，开方是 double
    的，乘积只在**存进数组**时才舍回 float——照抄这条才不会差 ulp。
  - **夹具链路**：`scripts/gen_aac_is_vectors.py` 把两端拉通——编码器先用临时
    `ISDUMP-` 探针打印它到达立体声阶段时的**真实状态**（左右谱、两声道掩蔽阈值、
    两声道 twoloop 的 `sf/cb/zeroes`），脚本落成 C 的 `--is` 输入、跑参考、
    再把两侧一起写进 `libavcodec/aac_is_vectors/fixtures.mbt`（4 帧 × 49 带）。
    探针用完已摘干净（`grep -c ISDUMP` 归零），重新生成的步骤写在
    `AGENT_HANDOFF_2026-11-05_AAC_IS.md`。夹具里的浮点都按 C 实际消费的
    **float** 存（`%.9e` 足以无损往返 float），否则"完全相等"的断言会假失败。
  - **接线**：感知路径走 `aac_encode_stereo_psy`（真判据 + 能量 M/S 掩码），
    默认路径仍用 §290 的简化门——它没有 twoloop 的 `sf/band_type/阈值`，
    成本判据无从算起。flip 带（`search_for_is` 翻相位那条规则）只写
    `ms_mask` 位、**不做 M/S 混合**，因为上游 `apply_mid_side_stereo` 对
    `is_mask` 带一律跳过，符号翻转是由解码器 `aac_apply_stereo` 读那个位来做的。
- **逐位结果**：`aac_is_vectors` 1/1。4 帧 × 49 带里 `is_mask`、`ms_mask`、
  右声道 `band_type`、`is_ener0`、`is_ener1`、两个相位的 `pass` 以及
  `cpe->is_mode` **全部与 FFmpeg 9.0.2 完全相等**（共 64 个强度带）。
  唯一有差的是 `dist1/dist2` 的**量值**：最大相对差 1.24e-6
  （`frame 2 band 35 phase-1 dist1`，1082.73486328125 vs 1082.7335205078125）。
  - 这条差距是**新暴露**而非新引入：§278 的对拍只断言过 twoloop 的逐带*决策*
    （`sf/cb/zeroes`）与 `maxval` 的相对误差 ≤1e-5，从没在**非单位 lambda**
    下断言过 `quantize_band_cost` 的量值——IS 是第一个传
    `s->lambda / band->threshold` 进去的调用方。两相位 `pass` 既然逐位一致，
    编码器的行为就一致；量值差距留作下一轮的二分项（把 `dist_spec_err` 与
    `band_cost` 两段拆开即可定位）。测试因此把决策字段设为**精确相等**、
    把量值设为**测量并界住 1e-5**，并在注释里写清为什么这么分。
- **实测对照**（44.1 kHz / 128 kbps / 2ch，感知路径，4 帧 + 1 收尾帧）：

  | 夹具 | IS 带（§290 门 → 真判据） | 码长 | 音调重建 | 往返 SNR |
  |---|---|---|---|---|
  | 高音共享（L 440 / R 550 低音不同） | 10 → **55** | 1744 → 1583 B（−9.2%） | R 的 9 kHz −0.089 dB | 22.92 → **23.26 dB** |
  | 高音不共享（L 9 kHz / R 11.7 kHz） | 0 → **43** | 1773 → 1726 B | 本道音 −0.13 dB；串到另一道 −53.6 dB | 23.72 → **25.27 dB** |

  - 真判据比相关性门**激进得多**：它会把去相关但"安静且被掩蔽"的带也换成强度
    位置——一个位置只花约 7 bit，两条编码带贵得多。这正是 §290 那条"去相关夹具
    必须 0 带、码流逐字节不变"的断言不再成立的原因。
  - 该断言换成成本判据下真正该成立的性质
    （`aac_lc_intensity_stereo_decorrelated_stays_transparent`）：切换只发生在
    频率门之上、只发生在右声道；帧必须变小；往返 SNR 不低于不开 IS 的 −1 dB；
    并且**每个声道自己的强音必须原样回来**（−0.13 dB）而**不串进另一声道**
    （−53.6 dB）。IS 位置错一步就是 6 dB，所以这条比"带数为 0"更有鉴别力。
  - 两个夹具的 SNR 都比不开 IS **更高**（+0.34 / +1.55 dB）：省下的位被 λ
    重试花回质量。§290 的简化门做不到这点——它只省位，位却花不出去。
- **外部对拍**：同一套 `--is` 流（8 帧 IS，**2726 字节**；§290 简化门 3102、
  §291 前 3279）交 docker `linuxserver/ffmpeg:9.0.2`：
  `9 frames decoded; 0 decode errors (9216 samples)`、`-v debug` 里既没有
  `Clipped intensity stereo position` 也没有 `invalid scalefactor code`；
  ffmpeg 的 PCM 与本解码器的 PCM **最大差 1.49e-7、RMS 2.50e-8、
  相对参考 SNR 138.61 dB**。也就是说 FFmpeg 自己的 IS 判据写出的码流，
  被 FFmpeg 自己的解码器原样接受。
- **仍未做**：`dist1/dist2` 量值的 ulp 二分（见上）；IS 与 PNS 的组合仍互斥
  （顺序已经允许，缺的只是一组 IS+PNS 夹具与逐位数据）；`search_for_ms` 的
  完整形态（`sid_sf_boost` 四轮 + `ff_sfdelta_can_replace`）；短窗 IS 的专门夹具。
- **检查**：`moon check --target wasm --deny-warn` 干净；`aac_is_vectors` 1/1、
  `aac_encode_vectors` 14/14、`aac_pns_vectors` 3/3，§278/§286/§287 三批 245 带
  与 §290 之后的 PNS 指标（344/332 B、0.947 dB、峰值 0.3182）均不变；
  `ref/aacenc` 的默认与 `--pns` 模式输出在加入 IS 对象后逐字节不变；
  全量 **1400/1400** 通过。

## 293. escape 码本的 `c*cbrtf(c)` 改为零转录表，IS 成本量值升到逐位相等

- **交付**：§292 留下的「`dist1/dist2` 量值最大相对差 1.24e-6」闭环，而且根因
  不在 IS，在共享的 escape 码本成本路径上。
  `quantize_and_encode_band_cost_ESC`（`aaccoder.c:137`）算的是
  `quantized = c*cbrtf(c)*IQ`，用的是**单精度** `cbrtf`；本移植原先按
  「double `cbrt` 再舍到 f32」建模。实测本机工具链的整数域 `c = 0..8191`
  里有 **2196 个值两者差 1 ulp**（例如 c=11：C 是 2.223979949951172，
  原建模是 2.223980188369751）。escape 带一旦参与成本求和，这点 ulp 误差就
  通过 `rd = (t - quantized)^2` 放大成 ~1.3e-3 的绝对差。
- **二分怎么做的**（值得记下来复用）：给 `aacenc_is.c` 再加一个探针，把
  `dist1` 在「只加了左带成本」的那一刻也导出来（harness 的 `ISD1L` 行）。
  一次就跑出结论：出问题那带（frame 2 band 35）的**左半逐位相同**、右半差
  1.3e-3，而两带唯一的差别就是 `cb0 = 1` 对 `cb1 = 11`——正是 escape 码本。
  以后遇到「决策对、量值差一点」的成本类对拍，先按调用边界拆开，别一上来
  就怀疑求和顺序。
- **修法**：`_ref/aacenc/dump_tables.c` 新增 `CBRT` 记录（`c*cbrtf(c)` 的 8192
  个 f32，`%.9e` 足以无损往返 binary32），`scripts/gen_aac_twoloop_tables.py`
  把它写成 `aac_twoloop_cbrt_prod`，`aac_two_band_cost` 的 escape 分支改成
  查表 + 一次乘法；`aac_two_cbrtf` 随之删掉（全仓库只有这一处在用）。
  `aac_twoloop_tables.mbt` 从 51 KB 长到 160 KB，**纯追加**（既有表 0 行改动：
  `git diff --stat` = 1366 insertions / 0 deletions）。
- **验证**：
  - `aac_is_vectors` 把 6 个成本量值（两相位的 `dist1`/`dist2` 加上 `dist1`
    的两个左半）从「测量并界住 1e-5」**改成精确相等**，连同本来就精确的决策
    字段，4 帧 × 49 带全部与 FFmpeg 9.0.2 逐位一致。§292 的开放项清零。
  - §278 的 245 带 twoloop 向量仍逐位一致；顺带解释并修掉了 §278 当年记下的
    那条残余（「成本误差 ≤6 位，只在 escape 带」）——那 6 位的来源就是这里。
  - 编码器侧指标**逐字节不变**：感知 21.309 dB / 364 B 均帧、IS 共享夹具 55 带
    22.92 → 23.26 dB、去相关夹具 43 带 23.72 → 25.27 dB、PNS 344/332 B 与
    0.947 dB。改的是量值而不是决策，所以行为一致——这正是 §292 那句
    「决策既然逐位一致，编码器的行为就一致」的实证。
- **工具坑**：
  - `dump_tables.c` 用 `cbrtf` 要补 `#include <math.h>`。
  - `gen_aac_twoloop_tables.py` 的 `parse()` 对未知 tag 直接 `raise`，
    加记录必须同时加分支，否则 `tables.txt` 一变就全红。
  - 用 `python - <<'PYEOF'` 往 C 源里写含 `"\n"` 的行时反斜杠会被 Python 吃掉
    一层（这次把 `printf("ISD1L %d %.9e %.9e\n", ...)` 的格式串写断行，gcc 报
    `missing terminating " character`）。这类改动要么用 Edit 工具，要么写完
    立刻 `grep` 验证生成的那一行。
- **检查**：`moon check --target wasm --deny-warn` 干净；`aac_is_vectors` 1/1
  （量值已改为精确相等）、`aac_twoloop_vectors` 3/3、`aac_encode_vectors`
  14/14、`aac_pns_vectors` 3/3；全量 **1400/1400** 通过。

## 294. IS 与 PNS 可以同时开：混合流的回归与外部对拍

- **交付**：拆掉 §290 留在配置层的互斥。`aac_encode_config_with_is` 与
  `aac_encode_config_with_pns` 都不再清对方的标志，`use_is` 也不再要求
  `!pns`。之所以现在能拆：§291 把立体声工具挪到了量化决策之后，
  `aac_encode_stereo_psy` 交给 `aac_is_search` 的 `cb`/`zeroes` 已经是
  **PNS 的终值**，而 `ff_aac_search_for_is` 的门本来就要求两个声道都
  `band_type != NOISE_BT && !zeroes`——排除规则是判据自带的，不是另外加的。
  默认（简化）路径不用担心：PNS 只在感知路径里跑（`with_pns` 强制
  `perceptual`），那里根本不会出现噪声带。
- **实测**（44.1 kHz / 128 kbps / 2ch，8 帧 LCG 白噪声 + 1 收尾帧）：

  | | 只开 PNS | PNS + IS |
  |---|---|---|
  | `NOISE_BT` 带（左 / 右） | 81 / 78 | **81 / 78**（一字不差） |
  | 右声道强度带 | 0 | **14** |
  | 码长（9 帧合计） | 2994 B | **2894 B**（−3.3%） |
  | 解码峰值 / 往返 SNR | 0.3182 / 0.947 dB | 0.3182 / 0.778 dB |

  噪声带数完全不动，说明两个工具确实正交：IS 只在「两声道都不是噪声带且非空」
  的带里选，PNS 的判定在 IS 之前完成且不回看。
- **外部对拍**：这种混合流（噪声带 + 强度带 + M/S 位）交 docker
  `linuxserver/ffmpeg:9.0.2`：`9 packets read (2894 bytes); 9 frames decoded;
  0 decode errors (9216 samples)`；ffmpeg 的 PCM 与本解码器
  **最大差 4.47e-8、RMS 9.11e-9、相对参考 SNR 138.41 dB**。
- **测试**：`aac_encode_vectors` 新增 `aac_encode_test_tool_bands`（按
  `section_data` 数三种工具带型）与 `aac_lc_pns_with_intensity_stereo`
  （两个工具的带同时出现、PNS-only 流 0 个强度带、码长不回增、解码干净有限）；
  `aac_lc_is_config` 改成断言**两个方向都能叠加**：
  `with_pns(with_is(x))` 与 `with_is(with_pns(x))` 都得到 `pns + is_stereo +
  perceptual`。
- **工具坑**：临时 main 包会被整模块的 `moon test` 构建图收进去——本轮在测试
  运行期间把 `zz_is_pns/` 搬走，构建图就报
  `input ...\zz_is_pns\main.mbt missing`，整轮测试白跑。要么先跑测试再清 scratch，
  要么先清 scratch 再起测试，别中途动。
- **仍未做**：`search_for_ms` 的完整形态；短窗 IS 夹具；§289 偏差 2
  （λ 重试里上游会重新 `analyze`，本移植每帧只分析一次）。
- **检查**：`moon check --target wasm --deny-warn` 干净；`aac_encode_vectors`
  15/15、`aac_is_vectors` 1/1、`aac_twoloop_vectors` 3/3、`aac_pns_vectors`
  3/3；IS 与 PNS 各自单开的指标与 §293 一致（PNS 344/332 B、0.947 dB；
  IS 55 带、23.26 dB）；全量 **1401/1401** 通过。

## 295. `search_for_ms` 完整形态：四轮 boost + 标度因子链守卫 + 逐位向量

- **交付**：新模块 [libavcodec/aac_ms.mbt](file:///D:/project/FFmpeg-n9.0.2/moonbit/libavcodec/aac_ms.mbt)
  搬 `aaccoder.c:702` 的 `search_for_ms`（本移植的感知路径只跑单长窗，就是那个
  形态）：每带四轮 `sid_sf_boost`（side 的标度因子每轮再降 3 步）、
  `ff_sfdelta_can_replace` 的 ±60 链守卫（左右各一条，配 `ff_init_nextband_map`
  的 nextband）、`bmax = bval2bmax(g*17/num_swb)/0.0045` 的 side lambda 折扣、
  `mslambda = min(1, λ/120)`，判据 `dist2 <= dist1 && B1 < B0`，命中就**改写两个
  声道的 `sf_idx`/`band_type`**——mid/side 对是在 `min(sf_L, sf_R)` 上一起量化的。
  - 接线：感知路径的立体声阶段变成
    `search_for_is → apply_intensity_stereo → search_for_ms → apply_mid_side_stereo`，
    与 `aacenc.c:983-995` 同序。`aac_encode_stereo_psy` 不再要求 IS 开着才走，
    `use_is` 降成它的一个参数——上游 `mid_side` 默认 -1（自动）与
    `intensity_stereo` 无关。能量版 `aac_encode_ms_mask` 只留在简化路径。
  - 因为 M/S 会改写标度因子，`channel_data_psy` 改为吃**终值**：入参
    `two : AacTwoResult` 换成 `final_sf : Array[Int32]`，调用方手里是
    `sf_ch[ch]`/`cb_ch[ch]`（默认取 twoloop 的，感知 + 双声道时取
    `aac_ms_search` 的）。
- **逐位结果**：`aac_is_vectors` 2/2，第二支就是 M/S。`_ref/aacenc --is` 现在把
  `aacenc.c` 剩下两步也重放：先调 harness 自带的 `ref_apply_intensity_stereo`
  （那份函数在 `aacenc.c` 里是 `static`），再调真实的 `s->coder->search_for_ms`，
  打印 `MSBAND`（搜索前/后各自的 `sf`/`cb`）与
  `MSCOST <g> <mask> <boost> <dist1> <dist2> <mid_cost> <B1> <bmax>`。
  4 帧 × 49 带里 `ms_mask`、`boost`、两条 `sf`、两条 `cb`、`dist1`、`dist2`、
  mid 带成本、`B1`、`bmax` **全部逐位相等**（67 个 M/S 带）。
- **二分抓出来的两个是真 bug，不是"浮点差一点"**：
  1. **C 的 `float` 字面量必须按 f32 值存**。`dist2` 差 2 ulp 拆到
     `mid_cost / B1 / bmax` 三层后，责任在 `bmax` 自己差 1 ulp：`bval2bmax` 里的
     `0.001f`、`0.0035f` 和调用点的 `0.0045f` 被我用十进制原值写进了 f32 链，
     而 `0.0035f` 的真实值是 `0.0035000001080334187`——拿 f64 常量去乘一个 f32
     值，结果就差 1 ulp。模块里现在存 f32 值本身
     （`aac_ms_b_offset/numerator/divisor`）。**同类残留**（本轮没动，已列全）：
     `aac_two_c_quant` 存 `0.4054`，而 `C_QUANT = 0.4054f = 0.40540000796318054`
     （§296 已清，且实测不改变任何输出）；
     `aac_pns_search.mbt` 的 `1.948 / 0.9 / 0.7 / 1.15 / 0.88 / 0.85` 与
     `aac_twoloop.mbt` 的 `1.15 / 0.00125 / 0.0125` 同理。现有夹具都没在翻转点上
     撞上它们，所以是本报告里唯一一处"已知但仍未清"的建模欠账。
  2. **`ms_mask` 是读改写，不是重建**。能量版把掩码整个重算，于是把
     `search_for_is` 存在那里的 **flip 位**清了；上游只在 `!is_mask` 时写 0，
     IS 带（含 flip）原样保留。解码侧 `aac_apply_stereo` 正是读那一位决定强度
     重建要不要取负号，所以这是行为差异而不是记账差异——向量的
     `frame 0 band 28: ms_mask 0 != 1` 就是它。`AacMsIn` 因此多一个 `ms_mask` 入参。
- **顺带补的三条**（都在这条接线的必经路上）：
  - `aac_two_band_cost` 原来只对 `cb == 0` 走"能量×λ、0 位"分支。C 的分派表
    （`aaccoder.c:229-245`）把 `NOISE_BT`/`INTENSITY_BT*` 也路由到
    `BT_ZERO || BT_NOISE || BT_STEREO` 那条，而 `search_for_ms` 的守卫只看
    `zeroes`/`is_mask`，**PNS 帧的噪声带一定以 `cb = 13` 撞进去**（表现为数组越界）。
    条件改成 `cb == 0 || cb >= 13`。
  - `apply_mid_side_stereo` 的 side 写成 `M - R` 而不是 `(L - R) * 0.5`：两者浮点
    不等价，而 M/S 定价用的正是前者。
  - `channel_data_psy` 的跳过条件从 `cb <= 0` 改成"可写码本 1..11 或 13"——M/S
    之后右声道里 IS 带的 `cb` 已经是 14/15，按旧条件会被送去量化（越界）。
- **实测对照**（44.1 kHz / 128 kbps / 2ch；"§294"一列不是记忆，是把同一套测试用
  `git worktree add --detach .scratch/head_wt HEAD` 在 §294 那次提交上重跑取的）：

  | 夹具 | §294 | §295 |
  |---|---|---|
  | 默认（简化）路径往返 | 339 B/帧、29.045006773915283 dB | **逐字符不变** |
  | 感知四音往返 | 364 B/帧（最大 375）、21.30931496598712 dB | 366 B/帧（最大 377）、20.9957997118289 dB |
  | 感知 NMR | 13.363791153781687 dB（谱 SNR 21.34233730385606） | 12.836482758794325 dB（谱 SNR 21.493286343967412） |
  | PNS 白噪声 | 344 / 332 B、0.9469831016493362 dB | 345 / 342 B、0.9319619143497199 dB |
  | PNS + IS | 81/78 噪声带、14 强度带、2994>2894 B、0.778484345306794 dB | 81/78、14、3078>2980 B、0.8124461374728804 dB |
  | IS 共享高音 | 55 带、1744>1583 B、电平 −0.0886 dB、22.922868 → 23.256901 dB | 55 带、1756>1599 B、电平 −0.0887 dB、22.923309 → **23.361290** dB |
  | IS 去相关 | 43 带、1773>1726 B、23.719339 → 25.267532 dB、本道 −0.1274 / 串道 −53.5549 dB | 43 带、1787>1720 B、22.122193 → 25.141611 dB、本道 −0.1031 / 串道 −51.8016 dB |

  读法：
  - 简化路径那一行**一个字符都没变**：它继续用能量掩码，`M - R` 的重写在 f64 下
    没有挪动任何打印位。
  - 感知路径的往返 SNR 降 0.31 dB、NMR 降 0.53 dB、码长 +2 B/帧。判据换人是原因，
    不是缺陷：能量掩码要 `rm + rs < 0.9*(rl + rr)`（我们的发明），上游是
    `dist2 <= dist1 && B1 < B0` 加"位数必须真减少"，并且命中后按
    `min(sf_L, sf_R)` 重写标度因子。它的目标函数是 λ 下的率失真，不是 PCM 信噪比。
  - 代价在噪声夹具上最明显（M/S 带 75 个、+84 B）：一旦有带被标记，整帧要付
    `max_sfb` 位掩码数组，且 mid/side 对共享一个较粗的标度因子。
  - IS 的两个夹具没有变差：共享高音 23.256901 → 23.361290 dB；去相关夹具里
    开 IS 相对不开 IS 的增益从 +1.55 dB 涨到 +3.02 dB。
- **外部对拍**：两条流交 docker `linuxserver/ffmpeg:9.0.2`——A = 感知 + M/S
  的音序夹具（3166 B），B = 噪声 + PNS + IS（2980 B）——两次都是
  `9 packets read; 9 frames decoded; 0 decode errors (9216 samples)`，`-v debug`
  里没有 `invalid scalefactor code`，也没有 `Clipped intensity stereo position`。
  ffmpeg 的 PCM 与本解码器：A 最大差 **1.49011612e-7**、RMS 2.41503e-8、
  相对参考 SNR **138.963 dB**；B 最大差 **4.47034836e-8**、RMS 9.05029e-9、
  SNR **138.404 dB**。即 M/S 改写后的标度因子链、噪声带上的掩码位、IS 的 flip 位
  这三种新行为，都被 FFmpeg 自己的解码器原样接受。
- **测试**：`aac_encode_vectors` 新增
  `aac_lc_mid_side_search_rewrites_scalefactor_pairs`——M/S 带在码流上有别的工具
  造不出来的签名：被改写的带两声道标度因子只差 **0 / 3 / 6** 步
  （`mididx - sididx = 3*boost`）。为此 `aac_encode_test_tool_bands` 现在把掩码位
  读进来而不是跳过，返回五元组（多 M/S 带数与违例数），实测 33（不开 IS）/ 15
  （开 IS）带、违例 0。第一版把这条性质套到所有帧上报了 10 个"违例"，其实是
  10 个**过渡帧**：短窗帧走简化路径的能量掩码，本来就没有这个签名。
- **仍未做**：短窗 IS 夹具；短窗感知路径（8×128 带计划 + grouping + 多窗 twoloop，
  `search_for_ms` 的 `w2` 外层循环也因此还没被多窗形态验证过）；§289 偏差 2
  （λ 重试里上游会重新 `analyze`）；上面列出的 f32 字面量残留。
- **检查**：`moon check --target wasm --deny-warn` 干净；`aac_is_vectors` 2/2
  （新增 M/S 逐位向量）、`aac_encode_vectors` 16/16、`aac_twoloop_vectors` 3/3、
  `aac_pns_vectors` 3/3；全量 **1403/1403** 通过。

## 296. 单精度保真清扫：量化器与 PNS 链上的 C `float` 常量与整型截断

- **交付**：把 §295 记下那笔"已知但仍未清"的建模欠账一次扫掉。规则很直白：
  **C 源码里的 `float` 字面量本身是单精度值**，按十进制原值（f64）喂进 f32 链就
  不是同一个操作数；同理 C 的 `int *= float` 是「整数 → 单精度乘 → 截回整数」。
  范围只圈在**有逐位向量把关**的链上（twoloop 量化器、`search_for_pns`、带宽规则）；
  psy 模块不在这一次里，它的口径本来就是「整数决策逐位、浮点相对误差 ≤5e-4」
  （§282/§283），要动得先决定要不要把整条 psy 做成逐位模型。
  - `aac_two_c_quant`：`C_QUANT = ROUND_STANDARD = 0.4054f` =
    `0.40540000796318054`。它进的是 `qmaxval = (int)(maxval*Q34 + C_QUANT)`
    与 `quant()` 的舍入项，两处后面立刻截成整数——每个带的码本选择与量化电平都过它。
  - 新常量 `aac_two_uplim_floor`（`0.00125f`，`aaccoder_twoloop.h:277`）、
    `aac_two_spread_scale`（`0.0125f`，`:532`）、`aac_two_tool_bitrate_boost`
    （`1.15f`）。`zspread` 整段改成逐步 f32：C 是
    `(maxspread-minspread)*0.0125f + minspread`，上限 `min_spread_thr_r*8.f`，
    饥饿项里两次乘、一次加、一次除**全是 float**，本移植先前只在最后一步舍。
  - **带宽规则两处**（`aaccoder_twoloop.h:186-197`、`aaccoder.c:512-520`）：
    `int frame_bit_rate = bit_rate / nb_channels` 是**整数除**，
    `frame_bit_rate *= 1.15f` 是单精度乘再截回 int。旧模型全程 f64，
    96 kbps / 2ch 上给 **55199** 而 C 给 **55200**（→ psy bandwidth 11039 vs
    11040）。同时补上条件：C 读的是 `options.pns || options.intensity_stereo`
    （"给效率工具多算一点带宽"），本移植原先只看 PNS——为此 `AacTwoPns` 多带
    `is_stereo`，构造器与三处调用跟着改，`search_for_pns` 里那条则是无条件补偿。
  - PNS 侧：`1.948f`/`0.9f`/`0.7f`/`0.88f`/`0.85f` 换成 f32 值，并把
    `thr_mult`、`spread_threshold` 内层、`freq = start*freq_mult`、
    `freq_boost = 0.88f*freq/6100.f`、`threshold*thr_mult*freq_boost`、
    `sqrtf(1.0f/freq_boost)`、`lambda/band->threshold`，以及
    `dist2 = energy/(spread*spread)*lambda*dist_thresh/threshold + 5|9`
    全按 C 的运算顺序逐位舍；`cutoff` 明确写成整数除法（C 三个操作数都是 int）。
- **实测：一条码都没变**，而且这是可证明的，不是"看起来没变"：
  - `aac_twoloop_vectors` 3/3、`aac_pns_vectors` 3/3（PNS-aware twoloop 与
    `search_for_pns` 各 **0 / 245** 带不一致）、`aac_is_vectors` 2/2
    （64 个强度带 + 67 个 M/S 带逐位）、`aac_encode_vectors` 16/16，
    每条打印指标与 §295 **逐字符相同**（14 位小数）。
  - 更硬的一条：把 §295 那个 scratch 程序在 `ae6937a` 和本提交上各跑一遍，
    产出的两条 ADTS（感知 + M/S 的 3166 B、噪声 + PNS + IS 的 2980 B）
    **`cmp` 逐字节相同**。所以 §295 的 ffmpeg 外部对拍结论（0 errors 零告警、
    最大差 1.49e-7 / 4.47e-8、SNR 138.96 / 138.40 dB）原样适用于本轮。
  - 为什么撞不上：`0.4054f` 与 `0.4054` 差 **7.96e-9**，而 1 附近的 f32 ulp 是
    1.19e-7——差口的宽度不到 ulp 的 1/15，`(int)(x + C_QUANT)` 要给出不同整数，
    需要 `x` 恰好落进那个 8e-9 的窗口。我在 200 个 `Q34` 表值 × 8192 个整数边界 ×
    maxval 上下游走 ±4 ulp，没有发现任何可达的分歧点。**会真打到的那一类是整型
    截断**：`1.15` 那次差 1（96 kbps），以及带宽条件补齐后 IS-only 的
    bandwidth 从 16000 走到 16600（`cutoff` 741 → 769）——我们的夹具里恰好没有
    带的边界落进这 28 行，所以输出仍未变；换码率/换夹具就不一定了。
- **仍未做**：`aac_psy.mbt` 那批从 C 抄来的十进制常量（`1.3219281`、
  `-0.46666667`、`0.00076`、`3.64`…），前提是先决定要不要把 psy 做成逐位模型；
  `aac_encode.mbt` 里 TNS 与简化路径的 `1.4/1.16/0.405/0.95` 不是 C 的操作数，
  是简化模型自己的常量，不参与逐位对拍。
- **检查**：`moon check --target wasm --deny-warn` 干净；`aac_twoloop_vectors`
  3/3、`aac_pns_vectors` 3/3、`aac_is_vectors` 2/2、`aac_encode_vectors` 16/16；
  全量 **1403/1403** 通过。

## 297. λ 重试里重新 `analyze`（§289 偏差 2 闭环）

- **交付**：`aacenc.c:916-945` 的整个 element 循环——**含 `analyze`**——就在码率
  重试的 `do { } while` 里面，而且每轮之前还重置
  `s->psy.bitres.alloc = -1`、`bitres.bits = last_frame_pb_count / channels`。
  也就是说上游对「装不下」的那一帧会分析**两次**，第二次读到的是第一次写进去的
  模型状态（`fill_level`、`prev_sfb_microphone`、缓存的 `linpow`）。本移植原先
  每帧只分析一次，这条就是 §289 的偏差 2。
  - [libavcodec/aac_encode.mbt](file:///D:/project/FFmpeg-n9.0.2/moonbit/libavcodec/aac_encode.mbt)：
    `psy_iter` 连同当轮的 `alloc`、掩蔽阈值、`bands` 一起搬进循环；第 0 轮沿用
    帧首那次分析（它同时供 `planned_max_sfb` 与 TNS 的带范围），`its > 0` 才逐
    声道重跑。
  - **口径是选的，不是照抄的**：C 的**严格分支**（`bit_rate_tolerance == 0`，
    §283 就是为它定的码率控制）在 `continue` 之前**不恢复** `sce->coeffs`——
    `memcpy(coeffs, pcoeffs, …)` 只出现在 ABR 分支（`aacenc.c:1075-1089`）。照字面
    搬过去，重试轮就会把 IS/MS 混合**再乘一遍**（M/S 每重试一次幅度对折一次、
    IS 带再乘一次 `scale`），分析读的是自己上一轮污染的谱。本移植保留 §291 定的
    「`coefs` 是干净的左右谱、混合写在 `work` 副本上」，即 **ABR 分支的恢复语义 +
    严格分支的 λ 规则**，这一轮只把「每轮重新分析」搬实。以后别把这条当 bug 改回去。
- **逐位把关的三条链都没动**：`aac_twoloop_vectors` 3/3、`aac_pns_vectors` 3/3
  （PNS-aware twoloop 与 `search_for_pns` 各 **0 / 245** 带）、`aac_is_vectors` 2/2。
- **夹具重新生成，顺带抓出回归集自己的一个 bug**：按 §2b 的步骤把 `ISDUMP-`
  探针加回去、跑一轮、再生成、再摘掉（`grep -c ISDUMP` 归零）。这一步暴露：
  `scripts/gen_aac_is_vectors.py` 里 `TAKE = (0, 1, 4, 5)` 是**手工下标**，§295
  新增的 M/S 测试插进执行序之后，这四个下标指到了别的帧上——同一条生成命令只覆盖到
  **9 个强度带**（原本 64），而测试照样全绿。现在 `choose()` 先看参考侧的答案，
  挑「强度带最多的两帧 + M/S 最富的两帧」，本次落在块 `[7, 8, 10, 14]`：
  **72 个强度带、79 个 M/S 带**，决策与成本量值仍逐位相等。
- **实测**（44.1 kHz / 128 kbps / 2ch；「§296」一列是在 `246bc4a` 工作树里跑同一套
  测试取的）：

  | 项 | §296 | §297 |
  |---|---|---|
  | 感知四音往返 | 366 B/帧（最大 377）、20.9957997118289 dB | 365 B/帧（最大 377）、**21.481440634673138 dB** |
  | 感知 NMR | 12.836482758794325 dB（谱 SNR 21.493286343967412） | 12.833586550558246 dB（谱 SNR 21.493571219746844） |
  | 默认（简化）路径 | 339 B/帧、29.045006773915283 dB | **逐字符不变** |
  | PNS 白噪声 | 345 / 342 B、0.9319619143497199 dB | 338 / 339 B、**1.01692907803258 dB** |
  | PNS + IS | 噪声带 81/78、3078 > 2980 B、0.8124461374728804 dB | 噪声带 75/78 → 81/78、3053 > 2980 B、同一 SNR |
  | IS 共享高音 | 55 带、1756 > 1599 B、22.9233 → 23.3613 dB | 55 带、1748 > 1599 B、24.0203 → 23.3613 dB |
  | IS 去相关 | 43 带、1787 > 1720 B、22.1222 → 25.1416 dB | 43 带、1785 > 1704 B、22.5247 → 24.9878 dB |

  - **只有发生过重试的帧会变**，per-frame 表是唯一证据（新增的打印）：PNS 夹具的
    plain/pns 由 `371/347 346/346 339/339 335/323 339/337 369/343 340/351 337/369 332/323`
    变成 `307/347 … 369/318 …`，其余七帧逐帧相同。plain 帧 0 需要第二次尝试，
    第二轮读到的是第一轮消费过的 reservoir → 分配更低 → 帧更小（371 → 307）；
    PNS 侧则是帧 5 重试（343 → 318）。
  - 于是感知路径 **+0.49 dB**、PNS SNR **+0.085 dB**、共享高音「不开 IS」那一侧
    **+1.10 dB**：重试轮的分配更贴近上游，位花得更准。副作用是 IS 相对不开 IS 的
    增益从 +0.44 dB 变成 −0.66 dB（这个夹具不开 IS 本来就够好），去相关夹具仍是有
    +2.46 dB 增益。
  - **`pns_bytes < plain_bytes` 这条断言换了，但不是放宽**。它断的是旧模型的码率
    轨迹，不是 PNS 的性质：两条流的 reservoir 历史一有人重试就分岔，总码量不再
    区分工具。换成三条站得住的性质——噪声带必须真的出现（75/78）、不开 PNS 的一侧
    必须一个噪声带都没有、PNS 流总码长不得超过不开 PNS 的 +1/16（实测 339 vs 338 B）；
    加上原有的「解码干净、有限幅度、SNR > −5 dB」。逐带的经济学仍由
    `aac_pns_vectors` 的 0 / 245 把住。
- **外部对拍**：§297 的两条流（A = 感知 + M/S，3163 B；B = 噪声 + PNS + IS，2980 B）
  交 docker `linuxserver/ffmpeg:9.0.2`，两次都是 `9 packets read; 9 frames decoded;
  0 decode errors (9216 samples)`，`-v debug` 里没有 `invalid scalefactor code` 也
  没有 `Clipped intensity stereo position`；PCM 与本解码器 A 最大差
  **1.49011612e-7**（RMS 2.41571e-8，**138.971 dB**）、B 最大差 **4.47034836e-8**
  （RMS 9.05029e-9，**138.404 dB**）。**探针摘掉之后重跑，两条 ADTS 与带探针那次
  逐字节相同**（`cmp` 通过），所以对拍结论落在要提交的这份实现上。
- **仍未做**：偏差 3（`planned_max_sfb` 仍帧级固定，上游每轮由
  `adjust_frame_information` 从量化结果算）与偏差 4（TNS 在 twoloop/PNS 之前、
  每帧只做一次，上游每轮 `search_for_tns` + `apply_tns_filt`）——这两条现在是互相
  咬着的：max_sfb 进循环就要求 TNS 也进循环，还要一份 `pcoeffs` 簿记，单独一轮。
  另有 §296 记下的 `aac_psy.mbt` f32 字面量清单，以及短窗感知路径。
- **检查**：`moon check --target wasm --deny-warn` 干净；`aac_is_vectors` 2/2
  （新夹具：72 强度带 + 79 M/S 带逐位）、`aac_encode_vectors` 16/16、
  `aac_twoloop_vectors` 3/3、`aac_pns_vectors` 3/3；全量 **1403/1403** 通过。

## 298. TNS 挪进 λ 重试循环（§289 偏差 4 闭环）

- **交付**：`aacenc.c:971-985` 的 `search_for_tns` + `apply_tns_filt` 在码率重试
  循环**内部**、`search_for_quantizers` **之后**、`search_for_pns` **之前**，而 ABR
  分支每轮先从 `pcoeffs` 恢复系数（`aacenc.c:1075-1077`）——所以滤波器既不跨重试
  叠加，也不参与量化的定价。本移植原先在帧首一次做完，而且**直接滤进 `coefs`**：
  twoloop 于是给「已经滤过的谱」定价，重试轮还复用同一份滤波结果（等于叠加滤波）。
  - [libavcodec/aac_encode.mbt](file:///D:/project/FFmpeg-n9.0.2/moonbit/libavcodec/aac_encode.mbt)：
    感知路径每轮逐声道在 twoloop 之后 `search_tns(coefs[ch], ...)`（读未滤波谱，
    与上游同），把滤波写进 `work[ch] = coefs[ch].copy()`，PNS 定价、IS/M-S 混合与
    `channel_data_psy` 都吃这份滤波后的谱；`coefs` 本身不再被污染，于是 §297 的
    「每一轮重新分析」读的也总是不滤波谱。简化/短窗路径保持帧首一次
    （§279/§280 的基线不动）。
  - `AacEncFrame` 的 `tns` 字段取消，改成 `build_frame`/`assemble_frame` 的显式
    入参（每轮一份 `tns_iter`），免得「帧首算的滤波器」和「本轮写进码流的位」
    成为两个真相。
  - **偏差 3 还留着，但现在看得更清楚**：上游 `search_for_tns` 的带范围用
    `ics.max_sfb`，那是**上一轮** `adjust_frame_information` 算出来、帧首再 clamp
    到 `num_swb` 的值；本移植仍传帧级 `planned_max_sfb`。下一轮把 max_sfb 改成每轮
    从 `zeroes` 反推时，TNS 的范围会一起对齐。
- **实测：只有一帧变**（8+1 帧共享高音夹具，感知路径、不开 IS；「§297」一列在
  `8743b8b` 的工作树里用同一个临时测试取的）：

  | | §297 | §298 |
  |---|---|---|
  | 逐帧码长 | 350 342 332 355 345 359 **366** 349 365 | 350 342 332 355 345 359 **369** 349 365 |
  | 合计 | 3163 B | 3166 B |
  | 往返 SNR | 22.146897668422 dB | 21.01332985515624 dB |

  变的就是 TNS 点火的那一帧（流里带 TNS 的帧 = 1/8），其余八帧逐字节相同。
  SNR 让出 1.13 dB 是 TNS 的固有交换：它把误差搬进谱 shaping 掩盖得住的地方，
  代价就是 PCM 距离；本轮改成「量化在滤波之前定价」之后，被滤波拿到的增益
  不再反映进标度因子，这正是上游的行为。其余夹具（四音、PNS 白噪声、IS 两条、
  NMR）逐字符不变——那些内容上 TNS 不点火，改动在它们身上本来就是 no-op。
- **回归**：新增 `aac_lc_perceptual_path_emits_tns`，断言感知路径的码流里
  **真的出现 TNS 滤波器**（`aac_encode_test_tool_bands` 多带一个从解码 `AacTns`
  读出的帧计数，只数 `ONLY_LONG` 帧）并且往返 SNR > 15 dB；把搜索从循环里摘掉，
  计数就归零。`aac_encode_vectors` 17/17。
- **夹具**：按 §297 的流程把 IS/M-S 向量重新生成一次，结果 `git diff` **为空**——
  选中的四帧（块 `[7, 8, 10, 14]`）都是 TNS 不点火的早期帧，谱与 `sf/cb` 一字不变，
  72 强度带 + 79 M-S 带仍逐位相等。既说明本轮不触及那三条逐位链，也说明
  数据驱动的 `choose()` 暂时不会因这个顺序改动而漂移。
- **外部对拍**：A（感知 + M/S，**3166 B**）与 B（噪声 + PNS + IS，2980 B）交
  docker `linuxserver/ffmpeg:9.0.2`，两次都是 `9 packets read; 9 frames decoded;
  0 decode errors (9216 samples)`，`-v debug` 里没有 `invalid scalefactor code`
  也没有 `Clipped intensity stereo position`；PCM 与本解码器 A 最大差
  **1.49011612e-7**（RMS 2.39967e-8，**138.994 dB**）、B 最大差
  **4.47034836e-8**（RMS 9.05029e-9，**138.404 dB**）。B 与 §297 完全一致 ✓
  噪声夹具不受影响；A 变了 3 字节仍被参考解码器原样接受。
- **仍未做**：偏差 3（每轮 `adjust_frame_information`：max_sfb 从 `zeroes` 反推 +
  窗口组 zeroes 归并 + `ms_mode = msc < max_sfb * num_windows ? 1 : 2`，我们现在
  只写 0/1）；短窗感知路径；`aac_psy.mbt` 的 f32 字面量。
- **检查**：`moon check --target wasm --deny-warn` 干净；`aac_is_vectors` 2/2、
  `aac_encode_vectors` 17/17、`aac_twoloop_vectors` 3/3、`aac_pns_vectors` 3/3；
  全量 **1404/1404** 通过。

## 299. 每轮 `adjust_frame_information`：max_sfb 由量化结果反推 + `ms_mode = 2`（§289 偏差 3 闭环）

- **交付**：把 §289 最后一条顺序/状态偏差关掉。`aacenc.c:503-547` 在**每轮**码率
  重试里做三件事，本移植原先是"帧首按 psy `bits > 0` 定一个帧级计划值"：
  - `max_sfb` = 最后一个 `!zeroes` 的带（从 `num_swb` 往下退），窗口组之间取
    max，CPE 两声道再取 max；
  - `ms_mode`（`aacenc.c:544-547`）：`msc == 0 || max_sfb == 0` → 0，
    `msc < max_sfb * num_windows` → 1，否则 **2**，而 mode 2 **不写掩码位**
    （`encode_ms_info`，`aacenc.c:489-498`）——我们以前只写 0/1；
  - TNS 的带范围是**上一轮/上一帧**留下的 `ics.max_sfb`（帧首只 clamp 到
    `num_swb`，`aacenc.c:864`），于是 `aacenc_tns.c` 在流的第一帧拿到 `mmm = 0` →
    `coef_len <= 0` → **首帧永远不发滤波器**。为此新增 `AacEncoder.last_max_sfb`
    （初值 0，与 C 的零初始化上下文同源）。
  - 窗口组 zeroes 归并（`aacenc.c:520-534`）在单长窗下是恒等，所以没搬，注释里
    写明了这一点。
  - 顺带把 `AacEncFrame` 瘦成 `{coefs, targets, sequences}`：`max_sfb` 与 `tns`
    都成为 `assemble_frame` 的显式入参（每轮各一份），`build_frame` 传帧级计划值；
    `assemble_frame` 不再需要 `frame` 参数。
- **实测：三条逐位链与所有端到端指标一字不变**（`aac_twoloop_vectors` 3/3、
  `aac_pns_vectors` 3/3 的 0 / 245、`aac_is_vectors` 2/2 的 72 强度带 + 79 M/S 带；
  四音 21.48 dB / 365 B、PNS 338/339、IS 1748 > 1599、perceptual TNS
  3166 B / 21.01 dB 全部与 §298 相同）。而且把同一支新测试注入 `9d08672`
  的工作树重跑，三个码率的 `max_sfb` 与 §299 **完全相同**（24 k → 19/49、
  64 k → 33/49、128 k → 43/49）：psy 的 `bits > 0` 计划在本移植的内容上恰好等于
  量化器的 trailing-zero 剪枝。所以这一轮是**保真闭环**，不是改行为；新暴露的两条
  规则在现有夹具上都够不到——TNS 从不在帧 0 点火，也没有哪一帧把全部带标记成
  M/S（`ms_mode2=0`）。
- **新增回归** `aac_lc_perceptual_max_sfb_and_tns_carryover`（3 个码率 × 8 帧，
  从解码侧语法读）：
  - `highest <= max_sfb`——任何带都不能落在声明的 `max_sfb` 之外（反向的
    `slack` 是允许的：`max_sfb` 剪的是 `zeroes[]`，不是"这一带有码本"，第一版
    断言 `slack == 0` 就是在 24 kbps 上被 5 帧的 slack 打回来的）；
  - `0 < max_sfb <= num_swb`，且 24 kbps 时 `max_sfb < num_swb / 2`（确实剪到 19）；
  - 帧 0 没有 TNS 而后续帧有（`tns_later = 5 / 2 / 2`）——首帧空范围这条规则的守卫。
- **外部对拍**：A（感知 + M/S，3166 B）与 B（噪声 + PNS + IS，2980 B）与 §298
  **逐字节相同**（`cmp` 通过），所以上一轮的对拍结论原样成立；新增 C = 24 kbps
  同内容（`max_sfb = 19` 的小帧，9 帧共 727 B），docker `linuxserver/ffmpeg:9.0.2`
  报 `9 frames decoded; 0 decode errors (9216 samples)`，无 `invalid scalefactor
  code` / `Clipped intensity stereo position`，与本解码器 PCM 最大差
  **1.1920929e-7**（RMS 2.37693e-8，**139.338 dB**）；A/B 复核 138.994 /
  138.404 dB。
- **意义**：到这里感知路径与 `aacenc.c` 的 element 循环**结构同构**——
  `analyze → mark_pns + twoloop → TNS → PNS → IS → M/S → adjust_frame_information
  → 写`，§289 的四条顺序/状态偏差 1-4 全部闭环。剩下的都是扩展而不是对齐：
  短窗（多窗 twoloop、`scale_factor_grouping`、多窗 IS/M-S）与 psy 的 f32 逐位化。
- **检查**：`moon check --target wasm --deny-warn` 干净；`aac_is_vectors` 2/2、
  `aac_encode_vectors` 18/18、`aac_twoloop_vectors` 3/3、`aac_pns_vectors` 3/3；
  全量 **1405/1405** 通过。

## 300. psy 的八短窗形态：8×128 带计划 + 逐窗阈值链

- **交付**：`aac_psy.mbt` 从"只跑 `num_windows = 1`"扩成
  `psy_3gpp_analyze_channel`（`aacpsy.c:646-840`）的两条形态。短窗形状牵动的
  六件事全部照抄：
  - `calc_thr_3gpp` 的 `w += 16` 外层——`start` 连续走完 1024 个系数，
    `wstart` 每窗归零，所以 `wstart < cutoff` 是**逐窗**判的（一个窗超出截止频率
    不连累下一个）；
  - 带计划与系数表按 `num_windows == 8` 切换（44.1 kHz 下 14 带 × 8 窗，
    `psy_coef[1]` 用的是短窗扩散常量 `en_spread_low_s` / `en_spread_hi_s`）；
  - `cutoff = bandwidth * 2048 / num_windows / sample_rate`（同一个 16 kHz 截止
    在短窗里只剩 92 条谱线）；
  - 孔避免阈值换 `PSY_3GPP_AH_THR_SHORT = 0.63f`，按 f32 存
    0.6299999952316284（§296 那条"C 的 `float` 字面量本身就是单精度值"）；
  - `calc_bit_demand` 换短窗那组斜率（`SAVE_SLOPE_S` / `SPEND_SLOPE_S` /
    `CLIP_HI_S`），同一份谱的 `bitres.alloc` 因此和长窗不同；
  - 预回声条件里的 `!w`：短窗帧只有**窗口 0** 读上一帧的 `prev_band.thr_quiet`。
- **两条上游怪癖**（都逐字抄，注释里点名）：①`aacpsy.c:670` 把本窗第一个带的
  能量写到 `spread_en[0]` 而不是 `spread_en[w]`，所以窗口 1..7 的扩散链是从数组
  初始的 0.0 起跳、band `w + 0` 自己的能量永远进不了孔避免判据；②5.6.1.3.4 把
  `pe` / `a` / `active_lines` 的重置放在**外层窗循环内部**，于是交给第二估计阶段的
  "全帧"量其实只是最后一个窗的。
- **形状从序列推导**：`analyze` 没有新增 `num_windows` 参数，短窗由
  `window_type == aac_eight_short` 推出来。上游两个 block switcher 都是按这个
  测试设 `wi->num_windows` 的（`aacpsy.c:475-486` 与 lame 版同一写法），
  "八个短窗但只分析一个窗"这种状态在移植里因此不可表示。
- **发布布局**：`AacPsyResult.bands` 换成 C 的槽位数组
  （`psy_bands[w * 16 + g]`，`aaccoder.c:366` 就是这么指的）——长窗仍是开头 49 个
  连续槽位，所以所有旧向量一字没动；短窗给 128 槽，结果里带上
  `num_windows` / `num_bands`。`prev_band` 按 `memcpy(pch->prev_band,
  pch->band, sizeof(pch->band))` 的语义整表抄 128 槽，长/短帧交替时读到的仍是
  上一个同形状帧的值。
- **C 侧**：`_ref/aacenc/harness.c` 加 `--short`——`wi.num_windows = 8` +
  `EIGHT_SHORT_SEQUENCE`，`window_type[1]` 逐帧在 only_long / long_start 之间交替
  把两条预回声分支都打到，谱用按帧轮转的 8 段攻击斜坡（相邻窗能量差一个数量级，
  不然逐窗链路分不出来）。**不带该开关时 harness 的输出与旧 `vectors.txt`
  逐字节相同**（`cmp` 通过），所以长窗 psy、twoloop、PNS、IS/M-S 四批向量都不受
  影响。
- **新向量**：`libavcodec/aac_psy_vectors/fixtures_short.mbt`（5 帧 × 8 窗 × 14 带
  = 560 条，含每条的槽位下标）由 `scripts/gen_aac_psy_short_vectors.py` 生成。
  `bits` / `alloc` / `fill_level` **逐位一致**（560 条 0 处失配）；实测最坏相对误差
  energy 1.73e-7、threshold 4.87e-6、spread 2.16e-7，比长窗那批（约 1e-4）还紧。
- **新增三条回归**（`aac_psy_short_test.mbt`）：560 条对拍并断言槽位就是
  `w * 16 + g`（布局是核对出来的，不是假设的）；未写过的槽位必须保持空（先跑一个
  长窗帧再跑短窗，防旧值渗进 padding）；同一份谱在两种形状下带数量、带计划与
  `alloc` 都必须不同（防形状塌陷）。
- **意义 / 仍未做**：编码器仍只把长窗帧交给感知路径
  （`use_psy = perceptual && seq != aac_eight_short`），而且**三条端到端流与 §299
  逐字节相同**（A 3166 B / B 2980 B / C 727 B，`cmp` 通过）——这一轮是纯地基。
  短窗真正接上还缺：多窗 twoloop（`psy_bands[(w+w2)*16+g]` 的消费者、
  `scale_factor_grouping` 的写入与 `group_len` 的带归并）、窗口组 zeroes 归并和
  `msc == max_sfb * num_windows`（§299 那条 `num_windows` 第一次真的等于 8）、
  多窗 IS/M-S，以及一组短窗夹具。
- **检查**：`moon check --target wasm --deny-warn` 干净；`aac_psy_vectors` 6/6；
  全量 **1408/1408** 通过。

## 301. twoloop 量化器的八短窗形态：按窗分组搜索

- **交付**：`aac_twoloop_search_pns` 从"钉死 `group_len = 1`"扩成
  `search_for_quantizers_twoloop`（`aaccoder_twoloop.h:65-757`）的两条形态。新入口
  `AacTwoShape = { num_windows, group_len }`（`aac_two_shape_long()` /
  `aac_two_shape_short(group_len)`，后者会检查各组窗数加起来正好 8，因为走错一步
  就整窗整窗地漏掉）。C 里 15 处 `for (w = 0; w < num_windows; w +=
  group_len[w])` 外层全部补上，配套的槽位/谱位偏移一起换成
  `w * 16 + g` 与 `w * 128 + w2 * 128`：
  - 零带/上界那一趟逐窗组跑两遍 `w2` 内循环（C 就是两遍：第一遍只判零，第二遍
    才累加 `uplim/energy/spread/nz`，且第二遍**不再看** `start >= cutoff`）；
  - `find_max_val` / `coef2minsf` 按组算一次，`minsf` 写给组内**每一个**窗的槽位
    （`(w+w2)*16+g`），后面按 128 槽平地扫的升/降 sf 循环会读到它们；
  - 带成本改成把组内 `group_len` 个窗的 `cost/bits/qenergy` 分别累加再
    `dist = cost - bits`，标度因子差链（`prev`）跨窗组**不重置**；
  - `ff_pns_bits(sce, w, g)` 的回看是 `(w<<4)+g-1`，即同组内上一条带，移植原来的
    `g-1` 在长窗下恰好没错，现在按槽位算；
  - `ff_init_nextband_map` 也吃同一个 shape（`aac_is.mbt` / `aac_ms.mbt` /
    `aac_pns_search.mbt` 三处长窗消费者显式传 `aac_two_shape_long()`）。
- **两条只有短窗才会露面的上游怪癖**（照抄并留注释）：形态因子那一趟里
  `find_form_factor(..., uplims[w*16+g] / (nzs[g] * sce->ics.swb_sizes[w]), ...)`
  ——分母用**窗号当带号索引** `swb_sizes[w]`，而守卫条件 `nzs[g] > 0` 只看第一个
  窗组的计数；长窗下 `w` 恒为 0，所以这两处此前都是恒等的。短窗另外还多一条
  DC 带保护（`aaccoder_twoloop.h:625`：`!g && num_windows > 1 && dists >= euplims`
  时收紧 `maxsf`）与 `de_psy_factor = 8 / group_len[w]` 的瞬态降权，二者以前都
  不可能触发。
- **C 侧**：`--short` 模式在 psy 之后接着跑真实的 `search_for_quantizers`，并逐帧
  轮转 `window_grouping[]` 的前五个字节（`aacpsy.c:398`）当窗分组，于是组大小为
  1/2/3 都被踩到；新增 `SHORTGRP <8 个 group_len>` 行与每帧 3~4 组 × 14 带
  = 266 条 `TWOBAND <slot> <sf> <cb> <zeroes> <maxval>`。**不带 `--short` 时
  harness 输出仍与旧 `vectors.txt` 逐字节相同**（`cmp` 通过）。
- **新向量**：`libavcodec/aac_twoloop_vectors/fixtures_short.mbt`（含 5 条谱、
  5 组 group_len、266 条带决定与其槽位下标）由
  `scripts/gen_aac_twoloop_short_vectors.py` 生成。`sf_idx` / `band_type` /
  `zeroes` **逐位一致**（266/266），`maxvals` 相对误差 ≤1e-5。
- **新增两条回归**（`aac_twoloop_short_test.mbt`）：把 §300 已钉住的
  `AacPsyContext::analyze`（短窗）输出直接喂给搜索并顺带断言 `alloc` 交接；再用
  同一帧分别按"真实分组"和"八窗各自一组"跑一遍，断言标度因子确实不同——防止
  `group_len` 被某处默默忽略而让分组变成 no-op。**咬合验证**：把
  `de_psy_factor` 临时改成常量 1.0，第一条立刻报
  `frame 0 slot 96: sf 115 != 116`，改回即全绿。
- **意义 / 仍未做**：搜索本身已经能吃短窗了，但编码器仍把短窗帧挡在感知路径外
  （`use_psy = perceptual && seq != aac_eight_short`），所以三条端到端流与 §300
  **逐字节相同**（3166 / 2980 / 727 B，`cmp` 通过）——本轮仍是地基。接上还缺：
  编码器侧的窗分组计划（`window_grouping[]` 的展开与 `scale_factor_grouping`
  写入）、逐窗组的 `zeroes` 归并与 `msc == max_sfb * num_windows`（§299 那条
  `num_windows` 第一次真的等于 8）、多窗 IS/M-S 与逐窗组量化的 `channel_data`。
- **检查**：`moon check --target wasm --deny-warn` 干净；`aac_twoloop_vectors`
  5/5、`aac_pns_vectors` 3/3（245 带 0 处失配）、`aac_is_vectors` 2/2、
  `aac_psy_vectors` 6/6；全量 **1410/1410** 通过。

## 302. 短窗感知路径接入编码器（opt-in）

§300 给了 3GPP psy 的八短窗形态、§301 给了 twoloop 的按窗分组搜索，但两轮都是
地基：编码器的门仍是 `use_psy = perceptual && seq != aac_eight_short`，短窗帧一直
走简化模型。本轮把这条链真正接到 `encode_frame`，并**做成显式开关**而不是改掉
既有 `perceptual` 的语义。

- **交付**
  - `AacEncodeConfig.perceptual_short`（默认 `false`）+
    `aac_encode_config_with_perceptual_short(config)`（像 `with_pns` 一样顺带把
    `perceptual` 打开，并保留 `pns` / `is_stereo`）。`use_psy` 变成
    `perceptual && (!is8 || perceptual_short)`：开关关闭时表达式与 §301 的门
    **逻辑等价**，短窗帧仍走简化模型。
  - `channel_data_psy` 由方法改成自由函数 `aac_encode_channel_data_psy`，新增
    `offsets` / `num_windows`。短窗按 `w * 128 + offsets[b]` 逐窗量化——**每个窗
    用自己的系数，共享组内的 `(sf, cb)`**；`AacEncChannel.num_windows = 8`，
    写侧 `write_spectral` 本来就按窗数展开，`write_ics_info` 本来就写
    `scale_factor_grouping = 0x7f`（一组八窗），两条路径复用同一套码流语法。
  - 感知循环里 `planned_max_sfb` 改按 C 的槽位布局扫描（`w * 16 + g`，
    长窗 stride 1 / 短窗 stride 16、8 窗）；`band_sizes_short()`、短窗
    `offsets`、`AacTwoShape = short([8,0,...])` 一起喂给 `mark_pns` /
    `twoloop_search_pns` / `search_for_pns` / TNS。
  - 短窗上显式关掉三件仍是**单窗移植**的工具：`use_pns = pns && !is8`、
    `use_stereo_psy = channels == 2 && !is8`。但 `aac_twoloop_pns(..., on)`
    里的 `on` 仍传 `config.pns`：C 的带宽补偿读
    `options.pns || options.intensity_stereo`，与窗形状无关，按窗形状改会引入
    上游没有的偏差（§296 记过这条）。
- **为什么是 opt-in**：一组八窗共用一个标度因子，而瞬态夹具故意让相邻窗能量差
  一个数量级。实测 `aac_lc_perceptual_transient`（6 帧）从 §282 的 31.84 dB 掉到
  **25.21 dB**；8 帧 mono 128 kbps 探针 23.31 dB、64 kbps 9.44 dB（简化路径
  12.35 dB）；stereo 128 kbps 10.83 dB。同时 §280 那把"用 SNR 换掩蔽余量"的尺子
  （`aac_encode_test_nmr`）是**长窗 spectrum**，短窗还没有对应的 NMR 评估，所以
  目前没有证据表明这 6~10 dB 换到了感知收益。按 PNS / IS 的先例设成 opt-in，
  基线不动，§303 拿短窗 NMR 再决定是否设为默认。
- **验证**
  - **长窗基线逐字节不变**：无 dither 的稳态 stereo 夹具 9 帧里，只有 1 个
    `EIGHT_SHORT` 帧变化，其余 8 个长窗帧（含短窗之后的帧，证明 psy 状态没有
    跨形状串扰）逐字节相同。三条端到端流仍是 §301 的 3166 B / 2980 B / 727 B。
  - **外部解码对拍**：临时 main 包导出两条开了开关的 ADTS —— mono 128 kbps
    瞬态夹具 3042 B、stereo 64 kbps 1626 B —— 交给 `linuxserver/ffmpeg:9.0.2`
    解成 f32 PCM，与本移植解码器逐样本比：RMS **3.537e-9 / 3.689e-9**、峰值
    7.648e-8 / 4.554e-8、相对 SNR **140.50 / 140.77 dB**，最佳延时偏移 **0**，
    ffmpeg 退出码 0 且 stderr 为空。即码流语法与 FFmpeg 解码器语义完全一致，
    SNR 低是量化取舍而非接线错误。开关打开后的两条流与本轮未提交时的半成品
    **逐字节相同**（3042 / 1626 B、同样的 RMS）。
  - **新增 4 条回归**（`aac_encode_vectors/aac_encode_short_window_test.mbt`）：
    配置组合（含与 PNS / IS 的三种顺序）、长窗逐字节基线、短窗 ICS 语法
    （`max_sfb <= 14`、`num_windows == 8`、`grouping == 0x7f`、5 个短窗帧全部
    与简化路径不同）+ 解码 PCM 有限有界、stereo CPE 解码。
  - `moon check --target wasm --deny-warn` 干净；包级 `aac_encode_vectors`
    **23/23**；全量 **1414/1414** 通过。
- **本轮踩到的坑**
  - `AacTwoResult.sf/cb/zeroes` 是 **128 槽**数组，只有窗组起始槽携带决策；
    单组八窗时前 14 个槽就是共享决定，`final_sf[b]`（b < max_sfb）拿来量化
    8 个窗是对的，`adjust_frame_information` 的窗口组归并在单组下确为恒等。
  - `aac_encode_test_tone` 带 LCG dither，**会触发 EIGHT_SHORT**，不能拿来做
    "纯长窗基线"；改用无 dither 的稳态夹具。更根本的是：编码器从静音历史起步，
    任何从零开始的夹具头三帧都是 `LONG_START → EIGHT_SHORT → LONG_STOP`，
    所以基线断言必须按窗口形状分帧，不能要求整条流相等。
  - 探针里的低 SNR **不是延时错位**：`aac_encode_test_pcm_snr` 已做 1024 样本
    对齐，额外扫过 960..1089 的所有延时，最佳仍是 `@1024`。
  - 临时 main 包会被整模块测试图收进去；全量测试前先把它和探针移进
    `.trash/<日期>/`（§294 记过同一条）。
- **仍未做（§303 / §304）**：`window_grouping[]` 展开成真正的多组计划并按
  `group_len` 写 `scale_factor_grouping`；多窗形态的 `mark_pns` /
  `search_for_pns` / `aac_is_search` / `aac_ms_search`；`adjust_frame_information`
  的窗口组 zeroes 归并（`msc == max_sfb * num_windows` 里 `num_windows` 第一次
  真的等于 8）；一组**短窗 NMR** 夹具来回答这轮 opt-in 该不该转正。

## 303. 短窗 NMR 夹具：`perceptual_short` 的转正裁决（不转正）

§302 留下的问题：短窗的噪声-掩蔽比没有尺子可量，`aac_encode_test_nmr` 是
1024 长窗 spectrum + 长窗阈值。本轮补上这把尺子并给出裁决：**保持 opt-in**。

- **夹具**（`aac_encode_test_nmr_short`，加在 §302 的
  `aac_encode_short_window_test.mbt`）：
  - 原始与解码信号各过一遍 `aac_encode_test_analysis(buf, seq=2)` 的
    **8×128 短窗谱**，阈值取 `psy.analyze(..., aac_eight_short, ...)` 发布的
    `psy_bands[w*16+g]`——模型自己在短窗形态下算出的逐窗逐带掩蔽，与量化
    噪声同域同尺。短窗带计划（14 带）由 `psy.band_sizes_short()` 累计成
    offsets，断言恰好铺满 128 线。
  - **重放整条流的每一帧**（长窗帧也喂 analyze）：预回声折减读的是上一帧的
    `thr_quiet`，而 `prev_band` 是整 128 槽跨形状拷贝的，漏喂中间帧会让阈值
    偏离编码器当时看到的值。`prev_bits` 按长窗夹具先例恒 0。
  - 帧 0 只喂 analyze 建立 pre-echo 历史、**不测量**：它的 overlap 前半是编码
    器的静音起步历史，priming 帧只是近似重建它。flush 帧不测（它的后半是
    补零，输入侧无对应样本）。
  - **窗计划恒等**是两路可比的前提：`aac_encode_test_window_plan` 逐帧读回
    每条流自己的 `window_sequence`，断言开关开/关两条流逐帧相同——攻击检测
    只读 PCM，这本来就该成立，测出来也确实成立。
- **数据（mono 瞬态 8 帧，5 个短窗帧 × 8 窗 × 14 带 = 560 条/流）**：

  | 码率 | 简化路径 mean NMR | 感知路径 mean NMR | 谱 SNR（简/感） |
  |---|---|---|---|
  | 128 kbps | **9.75 dB** | 14.21 dB | 33.35 / 25.49 dB |
  | 64 kbps | **15.62 dB** | 18.69 dB | 22.75 / 8.79 dB |

  逐窗拆开看没有例外：128 kbps 下八个窗的均值 5.5/9.0/7.1/11.8/10.8/11.3/
  11.1/11.4 dB（简化）对 10.1/14.0/11.5/15.9/15.3/15.7/15.5/15.7 dB（感知），
  **每个窗都更差**；逐带看差距集中在低频带（g0：-20.4 → +7.3 dB）——一组
  八窗共享一个标度因子，而夹具故意让相邻窗能量差一个数量级，安静窗被 loud
  窗的 sf 饿死，低频带首当其冲。64 kbps 同构（above-threshold 带数 462 →
  490/559）。
- **裁决**：长窗的「10 dB SNR 换 1.6 dB NMR」交易（§280）在短窗**不存在**——
  感知臂在模型自己的尺子上也输（两码率、八个窗全线），所以
  `perceptual_short` **保持 opt-in 不转正**。这不是 psy/twoloop 移植的错
  （§300/§301 已逐位对拍），是单组八窗的分配结构问题；§304 窗分组（真正的
  `scale_factor_grouping`，按攻击检测的 `group_len` 分组、逐组标度因子）才是
  能翻转这个排序的改动——到时把断言反过来即可。
- **回归**：`aac_lc_perceptual_short_noise_to_mask` 固化窗计划恒等 + 两码率的
  `nmr_on > nmr_off` + 有限性界（NaN 双向失败）；数字见 println。
- **坑**：`assert_eq`/`assert_true` 在**非 test 函数**里用要把该函数签名加
  `raise`（helper 里那两条断言各报一次 [4122]）。
- **验证**：`moon check --target wasm --deny-warn` 干净；全量
  **1415/1415**（§302 是 1414，正好新增 1 条）。

## 304. 短窗窗分组：LAME 分析器 + 真实 scale_factor_grouping（perceptual_short 仍 opt-in）

§302/§303 的遗留：感知短帧永远单组（`0x7f`），一组八窗共享一条 sf 链，安静窗被
loud 窗的标度因子饿死。本轮把 FFmpeg 真正注册的窗回调 `psy_lame_window`
（`aacpsy.c`，注意 `psy_3gpp_window` 是 `av_unused` 死代码）整体移植并接进
感知短帧，分组字节随首攻块落码流。

- **侦察**（上轮交接 `AGENT_HANDOFF_2026-11-05_AAC_WINDOW_GROUPING.md`）：C 侧
  真值行号地图 + MoonBit 侧触点清单 + 时序结构结论——C 的窗判决超前谱一个包
  （la 前瞻 544+12），本移植同步改写：判定 is8 的那一帧对本帧 MDCT 输入
  buf（`[prev|pcm]`）的 `buf[523..1568)` 跑 LAME 分析，该区域恰是 C 上一包
  前瞻覆盖的同一块样本（C 3072 布局 `firbuf = overlap+1547` ⇔ MB `buf+523`），
  攻击定位与生效谱自洽。
- **C 黄金向量（s1）**：`patch.py` 加探针导出 `psy_lame_window` 整数决策
  （级联后 `attacks[0..8]`、`att_sum`、`uselongblock`、首攻块、改写前
  `next_grouping`、`prev_attack`、init 时的 attack_threshold）；harness 新
  `--win [frames]` 模式复刻 `aacenc.c` 3072 滑窗几何逐帧逐通道调模型指针。
  PCM 是整数精确的：安静交替符号噪声（20..27，邻峰比远低于阈值 7.40）+
  5000 样本周期（非 64 整数倍，攻击位置漂移）的四段 burst，ch1 的 700/764
  背靠背双 burst 喂级联。26 帧 × 2 通道：首攻块覆盖 0..8 全表、uselong 两态、
  `att_sum` 大于终值和的帧证明级联/喇叭抑制都开过。64 kbps/ch 的阈值实测
  7.40（精确命中表项，非插值——`lame_calc_attack_threshold` 取较近端点）。
- **MoonBit 分析器（s2，`libavcodec/aac_enc_window.mbt`，纯新增）**：
  `AacLameWindow` 每通道一份跨帧状态（`prev_energy_subshort[16]` init 10.0、
  `prev_attack`、`next_attack0_zero`、`next_grouping`），`analyze(buf, start)`
  全链按 C float 语义逐运算 f32 化（`aac_two_*` 家族；FIR 系数/2.3f/阈值表按
  精确 f32 十进制转录）。**不移植** `lame_apply_block_type` 窗型状态机——编码器
  保留既有 early/late+pending 机（§302 已端到端验证过），分析器只出
  `uselongblock` 供向量对拍。la==NULL flush 路径不建模（文档化偏差：测试流以
  静音帧收尾）。
- **向量测试（s3，`libavcodec/aac_win_vectors/`）**：黑盒包回放同一整数 LCG
  PCM（掩码 Int 乘法模拟 UInt32，vp9 探针惯例），26 帧 × 2 通道逐帧整数断言
  全等（含 `next_grouping` 链与 `attacks[7]==prev_attack` 交叉验证）；外加
  ABR 阈值选端点测试（96 恰好命中表项 6.60、72 平手取上端 7.00 等 8 例）。
  **首跑即绿**。
- **接线（s4/s5）**：`AacEncoder` 挂 `lame[2]`（`perceptual && perceptual_short`
  时每帧跑，保持 C 的逐帧节奏）；buf 构造循环里取每通道首攻块，CPE 取
  **较早**首攻（与 seq 机“任一通道攻击”同口径；C 的 common_window=0 分叉不
  移植，文档化偏差）。消费门 `use_psy && is8`，其余路径（简化路径短帧、全部
  长帧）保持 `[8,0..]/[1,0..]` 单组。写侧多组化：`AacEncChannel` 加
  `group_len`，短帧 sf/cb 平铺 128 槽（`w*16+b`，组起点槽携带决策）；
  `channel_data_psy` 组→带→组内窗量化；sections 按组 run-length（3 位长度）、
  scalefactor 三条差分链**跨组连续**（off 初值=global_gain=首个 coded 槽）、
  spectral 组主序（组→带→组内窗）发射、`write_ics_info` 按 `!group_len[w]`
  写真实 7 位、`adjust` 等价块按 C `aacenc.c:503-547` 逐组尾零扫描 + 组内
  归并（新鲜数组下归并=恒等，C 的跨帧粘性 zeroes 是文档化偏差）。单组形状下
  所有写器路径与旧码逐位等价。
- **基线证明（硬验收）**：临时 `zz_win_probe` 黑盒包（stash 前后对拍，已入
  .trash）：default 与 perceptual 两条 12+1 帧流的字节计数+双校验和**完全一致**，
  psy_short 流按预期变化（4572→4554 字节）。
- **端到端（s7，§302 同款流程）**：`zz_win_ab` main 导出分组 ADTS（4554 字节
  13 帧）+ 本解码器 PCM；docker `linuxserver/ffmpeg:9.0.2` 解码 **0 errors**，
  双声道 13312 样本逐样本对拍 **max_diff = 1.19e-7（≈1 f32 ULP @1.0，138.5 dB）**，
  最优延时 0。分组语法（sections/sf 链/组主序 spectral）与两个解码器全部吻合。
- **NMR 复测（§303 尺子）与裁决**：分组把感知臂 NMR 从 14.21→**13.40 dB**
  （128k）/18.69→**18.26 dB**（64k），改善了 0.8/0.4 dB 但**没有翻转**对简化臂
  （9.75/15.62 dB）的劣势——§303 的 `nmr_on > nmr_off` 断言原样保持，`perceptual_short`
  **继续 opt-in**。波形 SNR 方面感知短帧 17.83 dB（守卫按数据下调 18.0→17.0）。
- **测试更新（s6）**：`aac_lc_perceptual_short_window_stream` 的 `0x7f` 断言改为
  “分组字节展开恰好铺满 8 窗 + 至少一帧非平凡分组 + plain 臂短帧仍 0x7f”。
- **本轮坑**：fixtures 顶层 `let` 在 test 视图**不可见**（须 `pub let`，且测试内
  引用要带 `@包名.` 前缀）；Int→Int32 没有转换方法（`to_int32`/`from_int` 皆无），
  直接用 Int32 算术内联展开；libavcodec 白盒 wasm 仍有 init local 上限（探针必须
  放独立黑盒包）；UInt 字面量后缀 `U` 是 UInt32（大常数写 hex）；`>>`/`&` 混用
  必须加括号（deny-warn）。
- **验证**：`moon check --target wasm --deny-warn` 干净；全量 **1417/1417**
  （§303 是 1415，新增 aac_win_vectors 2 条）。


## 305. PNS 的八短窗形态：mark/search 按窗分组 + 元素 sticky 槽状态（§304 续篇一）

§304 交接的首选下一步：`mark_pns`/`search_for_pns` 上游都是 `w += group_len[w]`
外层，分组真正进码流后把多窗形态补上，短帧才能开 PNS（IS/M-S 的多组化是续篇二）。

- **组形态移植（s1，`aac_pns.mbt`/`aac_pns_search.mbt`）**：两函数统一吃
  `AacTwoShape`（num_windows+group_len）。`mark_pns` 的门按组算——能量/阈值组内求和、
  spread 取最小、新增瞬态门 `min_energy < FFMIN(0.7, λ/140) * max_energy`（单窗下
  min==max 恒假，长窗行为逐位不变）；`cutoff = bandwidth*2*wlen/sr` 全整型除法。
  `search_for_pns` 的噪声**逐窗**生成与定价：每个 `w2` 独立拉 LCG、算
  `pns_energy` 贡献，`dist1` 按该窗 psy band 的阈值与该窗槽的
  `sf_idx[(w+w2)*16+g]`/`band_alt` 计价，`dist2` 逐窗累加
  `energy/spread²·λ·dist_thresh/threshold`，±5/9 开销每组一次；决策写组起点槽。
- **sticky 槽状态（s2）**：C 的 `SingleChannelElement` 把 `sf_idx/band_type/zeroes`
  三条 128 槽数组跨帧保留（`aacenc.c:929-933` 每轮元素循环只清 `>RESERVED_BT` 的
  band_type）。twoloop 只写组起点，但它的速率循环 `for i<128` 会缩放**全部**槽、
  zero-band 段会写组内被置零的内窗槽——而 `search_for_pns` 的逐窗定价恰恰读这些
  "twoloop 本帧没写过"的内窗槽（`aaccoder.c:597-599`，读的是前几帧的遗留值）。
  落地：`AacEncoder` 挂三条 per-channel sticky 数组（calloc 语义）；每轮先做
  `>12 → 0` 清理；`aac_twoloop_search_pns` 加 `sticky?` 可选参数（缺省全新零数组=
  §278/§301 向量的 plain-reset 状态，既有调用点零改动），原地写回；
  `search_for_pns` 直接吃 sticky 数组并把噪声决策写回。长帧每帧重写所有会读到的
  槽，sticky 不可见（§304 的「zeroes 跨帧粘性不复刻」偏差就此收口）。
- **harness 修正（s3，两处真偏差）**：`--short --pns` 模式的帧首清理写成
  `> 13`（漏 NOISE_BT，C 是 `> RESERVED_BT`=12）；且 §301 时代的 `g<num_bands`
  （49 槽）全复位循环还在跑，与「全 sticky」矛盾——slot<49 被清零、49..127 粘，
  恰好污染 `search_for_pns` 读得最多的窗 0-2 内窗槽。两处都修成 `aacenc.c` 语义，
  向量重新生成（`vectors_pns_short.txt`，`PNS`/`TWOBAND`/`STICKY` 行）。
- **黄金向量（s4，`aac_pns_vectors/fixtures_short.mbt` +
  `aac_pns_short_test.mbt`）**：5 帧 LCG 噪声谱 × 前五种 `window_grouping` 分组
  （1/2/3 窗组都有），复放整条元素循环（清理→mark→twoloop→search）并断言：
  twoloop 组起点 sf/cb/zeroes、mark 的 can_pns、search 的 band_type/zeroes/pns_ener
  （f32 容差）逐槽相等，**外加每帧末 128×3 的 STICKY 全等**（sticky 模型本身被
  向量钉住，不是假设）。266 槽决策全等（19 噪声带、71 个 can_pns 槽）。
- **接线与写侧（s5）**：`use_pns = pns && (!is8 || use_psy)`——短帧在感知短窗
  路径（perceptual_short 仍 opt-in）上开 PNS；`channel_data_psy` 的 `noise_sfi`
  改槽索引。立体声对（IS/M-S）仍长窗限定，多组化留给 §306。
- **端到端（s6）**：新增 `aac_lc_perceptual_short_pns_stream`——transient 流上
  短帧真实携带 NOISE_BT（7 帧 70 带，逐组解析 sections；plain 臂 0），本解码器
  SNR 16.8 dB、峰值有界。临时 `zz_pns_ab` 导出 mono 128k / stereo 64k 两条
  短窗+PNS ADTS：docker ffmpeg 9.0.2 解码 **0 errors**，与 MB 解码器 PCM 对拍
  max_diff 5.6e-8（≈1 f32 ULP，一致性 140 dB）。
- **基线（硬验收）**：临时 `zz_pns_base`（stash 前后对拍）——default / perceptual /
  perceptual+pns 三条长窗流的字节计数与 FNV 双校验和**完全一致**。
- **本轮坑**：MoonBit `Byte` 的 `to_string` 打印 `b'\xFF'` 字面量（探针输出解析
  要按字面量解）；UInt32 后缀必须大写 `U`；Git Bash 下 docker `-v` 挂载要
  `MSYS_NO_PATHCONV=1` + `D:/...` 绝对路径。`moon fmt` 顺带把全仓
  （Opus/探针等 30+ 文件）归一到当前 formatter（`///|` 补齐、`Int`→`Int32` 别名
  打印、短调用并行），行为无关，全量回归覆盖。
- **验证**：`moon check --target wasm --deny-warn` 干净；全量 **1419/1419**
  （§304 是 1417，新增短窗 PNS 向量 1 条 + 短窗 PNS 流 1 条）。

## 306. 立体声对的八短窗形态：search_for_is / search_for_ms 按窗分组 + MS 重写回流 sticky（§304 续篇二，短窗工具链收口）

§304 交接清单最后一项：`ff_aac_search_for_is`（`aacenc_is.c`）与 `search_for_ms`
（`aaccoder.c:702`）上游都是 `w += group_len[w]` 外层——本轮移植后短帧三件套
（PNS §305 + IS/MS 本轮）全部就位，`perceptual_short` 路径的短帧可以开全部工具。

- **`aac_is_search` 组形态**：能量门按组算（ener0/1/01/01p 组内逐窗累加）；
  `aac_is_encoding_err` 逐窗定价——每窗自己的 psy band 阈值、自己的
  `IS/I34/maxval/is_band_type`，而 `is_sf_idx` 与两侧 `sf_idx/band_type` 取**组起点槽**
  （twoloop 只写组起点）；`dist_spec_err` 逐窗累加并按该窗 `lambda/minthr` 缩放。
  `freq_mult = sr/(1024/num_windows)/2`（短窗分母是 128，不是 1024——长窗形态里写死的
  除法只在 num_windows=1 时等价）。`prev_sf1` 链的条件读**被 IS 改写后**的
  band_type（14/15 ≥ RESERVED_BT 被跳过）——搜索内部维护 `bt1_view` 视图（长窗下
  这是相对 §292 形态的一处向 C 的修正）。全部数组槽化（`w*16+g`，长窗 slot==g）。
- **`aac_ms_search` 组形态**：Mmax/Smax 取全组 companded 峰（mid/side 的 sf/cb 对整组
  固定）；boost 循环里 dist1/dist2 逐窗累加、**bits 逐窗扣除**（C 在 w2 循环内减，
  组>1 时 f32 舍入与"最后一次减"不同）；决策与 sf/cb 重写落组起点槽。
  输出四条重写数组规整到槽空间长度（`aac_ms_fit`）——sf 源恒 128 而 ch1 的 cb 视图
  长度随窗形变（长 49/短 128），调用方写回 sticky 时不再有长度错配。
- **MS 重写回流 sticky（本轮关键语义发现）**：`search_for_ms` 只写组起点，但**下一帧
  的分组会变**——上一帧的组起点可能变成这帧的内窗槽，其速率循环就从 MS 重写值起步
  缩放（向量帧 1 slot 53 实证：帧 0 的 MS 把它写成 156/3，帧 1 分组变后它是内窗，
  C 的 STICKY 停在 219/3 而"不回流"版本从 twoloop 旧值起步发散）。编码器在每次
  attempt 的 stereo 段后把 `ms.sf*/cb*` 写回 sticky（zeroes 不动——两工具都不碰）。
- **写侧组化**：`assemble_frame` 的 `ms_used` 按 `encode_ms_info` 组走（每组每带 1 位，
  简化路径单组下与旧平铺逐位相同）；`mark_is` 组走盖章；`ms_present` 计数按 C 的
  **逐窗**槽走（`w < num_windows*16; w += 16`，总数 max_sfb×num_windows——组内窗的
  槽恒 0，所以多组帧取不到 mode 2，这是 C 的原样行为）。
- **harness `--isms` 模式**：立体声对 + 前五种 `window_grouping` 循环 + 相位混合谱
  （ch1 = corr[w]·ch0 + ±300 独立噪声，corr 窗 2/3/6 为负→两个相位 + flip 候选），
  复放 `aacenc.c:946-1000` 元素循环（清理→per-channel mark/twoloop/pns→IS→mix→MS，
  `group_map[0]=1` 走 ff_psy_init 的 +1 技巧让对共享一次 analyze；补了
  `bitres.alloc /= chans`）。修了三处 harness 自身 bug：PSYALLOC 打印在 analyze 前、
  生成器 slot 列表在 PRE0/PRE1 各追加一次（84≠42）、ISDUMP 的 bt1 在未认领槽打印的是
  PNS 后码本（断言范围相应收窄）。
- **黄金向量（`aac_is_vectors/fixtures_short.mbt`，532 槽决策）**：PRE（两通道组起点
  sf/bt/z）+ STICKY（两通道 128×3）+ ISDUMP（is_mask/ms_mask/bt1/两 ener）+ MSDUMP
  （ms_mask + 双通道 sf/cb 重写）逐槽全等；夹具真实触发 is=77（flip 21）ms=140。
  MB 复放含 MS 回流 sticky。
- **端到端**：新增 `aac_lc_perceptual_short_is_ms_stream`（反相窗高频 tonal 夹具：
  短帧真实携带 34 个 IS 带、plain 臂 0、双解码峰值有界）；探针导出 128k/64k 两条
  pns+is+short 全开流：docker ffmpeg 9.0.2 解码 **0 errors**，与 MB 解码器 PCM 对拍
  max_diff 4.8e-8（一致性 139.7/140.0 dB）——分组 ms_used/IS sf 链/off_is 组主序全部
  被真实解码器认可。
- **基线（硬验收）**：stash 前后 default/perceptual/perceptual+pns/perceptual+pns+is
  四条长窗流字节计数与 FNV **完全一致**（MS 回流对长窗无行为差异——长窗全部槽数都是
  组起点，下一帧 twoloop 必然重写，回流值永不被读到）。
- **本轮坑**：`aac_decode_ics_with_info` 自读 global_gain 且不含 spectral——手工 CPE
  解析别多读 8 位、必须跟 `parse_spectrum`；`AacIcsInfo` 无 ms_mode 字段（读原始
  2 位）；moon fmt 会把 lambda 签名重排，脚本化替换要按 fmt 后形态锚定。
- **验证**：`moon check --target wasm --deny-warn` 干净；全量 **1421/1421**（§305 是
  1419，新增 isms 向量 1 条 + 短窗 IS/MS 流 1 条）。短窗工具链（PNS/IS/MS）就此收口。

## 307. 窗型状态机换 FFmpeg 语义：lame_apply_block_type 移植 + WI 向量（§304 续篇三，偏差①收口）

§304 留下的最后一条文档化偏差是「seq 状态机不换」（自制的 early/late+pending 机，
转移图与 C 不同：短窗后立即 LONG_STOP、LONG_STOP 后攻击仍走 LONG_START）。本轮把
FFmpeg 真正注册的窗型决策——`psy_lame_window` 的 `uselongblock` 判定 +
`lame_apply_block_type` 状态机（`aacpsy.c:860-875`）——移植进 `AacLameWindow` 并接
进编码器感知短窗路径，窗型序列逐帧对拍收口。

- **状态机移植（s1）**：`aac_lame_apply_block_type(state, uselong)` 纯函数——发射
  的是存储态（短窗路径先打补丁：`ONLY_LONG→LONG_START`、`LONG_STOP→EIGHT_SHORT`，
  即 STOP 后攻击直进短窗），存 computed block type（长路径下 `EIGHT_SHORT→LONG_STOP`，
  其余→`ONLY_LONG`——**短窗运行必多一帧安静的 EIGHT_SHORT 再经 LONG_STOP 回长窗**）。
  `AacLameWindow` 加 `mut next_window_seq`（初始 ONLY_LONG），`analyze` 末尾跑状态机、
  输出加 `window_type`。顺带确认 §304 的 `prev_attack == 2` 正确
  （`PSY_LAME_NUM_SUBBLOCKS == 2`，C 的 `== 8` 是 `AAC_NUM_BLOCKS_SHORT`——两常量易混）。
- **夹具重造（s2）**：§304 的 `--win` 夹具 5000 样本周期几乎逐帧有 burst，短窗运行
  永不收尾（wt 全是 1/2）。改为 **8000 样本周期、burst 只在前半**：安静的后半
  （≈3.9 帧）让每个周期完整走一遍 `EIGHT_SHORT 尾帧→LONG_STOP→ONLY_LONG→（下周期）
  LONG_START`——wt0 四态全覆盖（2:27 / 0:11 / 1:8 / 3:6），首攻块仍覆盖
  `window_grouping` 全表 0..8。harness `win_pcm`/事件表与 MB 测试镜像同步。
- **WI 向量转正（s3）**：生成器解析 `WI` 行的 `window_type[0]` →
  `aac_win_window_type`；复放测试逐帧断言（26×2 全等，首跑即绿）+ 新增四态/全表
  覆盖守卫。
- **编码器接线（s4）**：循环重排为 buf→analyze→seq（analyze 输入是 `[prev|pcm]` 拼
  接，不依赖 seq，重排无副作用）；`perceptual && perceptual_short` 时 seq =
  pair 合流机的发射值：`uselong = 所有通道 uselong 的 AND`（「任一通道短则短」，
  与分组取较早首攻同一合流口径——C 在两通道不一致时拆成两个 SCE，该分叉仍是
  §304 文档化偏差；两通道判定一致的帧上合流机与 C 的 per-channel 机逐位相同）。
  `AacEncoder.lame_pair_seq` 承载状态。其余路径（简化、长窗感知）保留旧机。
- **流测试更新（s5）**：`keeps_long_window_baseline` 改为「窗型相同的帧字节必须
  相同」（换机后过渡窗位置不同是预期，steady 夹具因分析器 10.0 初始峰值开跑即有
  一次短窗运行——与 FFmpeg 同输入的首帧行为一致）；NMR 测试删「两臂计划逐帧相等」
  （两臂是两台机器），改为两臂各自计划的**转移律合法性**检查（LONG_START→
  EIGHT_SHORT、EIGHT_SHORT→{EIGHT_SHORT,LONG_STOP} 等）+ 各自算 NMR。
  NMR 复测：12.07 vs 9.75（128k）/ 17.28 vs 15.62（64k）——perceptual 臂仍劣，
  `nmr_on > nmr_off` 裁决原样保持（opt-in 不变）。
- **基线（硬验收）**：四条长窗 noise 流（default/perceptual/+pns/+pns+is）字节与
  FNV **完全不变**（noise 无攻击 → 恒 ONLY_LONG，新机不触发）。
- **端到端（s6）**：transient 立体声三臂（short / +pns / +pns+is）经 docker
  ffmpeg 9.0.2 解码 **0 errors**，与 MB 解码器 PCM 对拍 **140.2/140.0/139.9 dB**
  （max_diff ≈ 7e-8）——含 LONG_STOP 后直进短窗、短运行尾帧等新转移的全部位流
  （ics_info 窗型字段、窗形状、短窗 sf 链）被真实解码器认可。
- **本轮坑**：CRLF 文件里多行 python 替换要 `newline=''` 双形态匹配；`AacIcsInfo`
  无 ms_mode 字段（读原始 2 位）；立体声流不能用 SCE 解析 helper。
- **验证**：`moon check --target wasm --deny-warn` 干净；全量 **1421/1421**。
  §304 的三条文档化偏差收口其一（①窗型状态机）；剩余：②CPE `common_window=0`
  分叉、④`la==NULL` flush 路径。

## 308. 短窗 NMR 悬案的第三方裁决：真实 ffmpeg 原生编码器对拍（opt-in 定案）

§303 以来的悬案：短窗感知臂 NMR 恒劣于简化臂（14.03 vs 12.74 dB @128k），无法区分
「移植缺陷」与「FFmpeg 短窗路径固有行为」。本轮引入第三方裁判——用 docker
`linuxserver/ffmpeg:9.0.2` 的**原生 AAC 编码器**编同一 transient 夹具（int16 WAV，
128k/64k 两组），解码回 f64 后在**同一把尺**（本仓库 psy 模型的逐带噪声-掩蔽比）
上三方对拍，并把结论固化为正式回归测试。

- **裁决数据**：128k——简化臂 12.74 / 感知臂 14.03 / **FFmpeg 原生 14.41**；
  64k——18.18 / 20.19 / **18.56**。逐带（128k）：FFmpeg low 12.96 / mid 17.63 /
  high 12.19，与感知臂（13.67/17.43/10.24）互有胜负、均值差 <0.4 dB（128k 感知臂
  还略好）；64k 差 1.63 dB（位预算紧时 trellis 层 §271 简化、noise-sf 公式 §288
  等文档化偏差的残差量级）。
- **结论**：①「感知短窗臂劣于简化臂」是 **FFmpeg psy+twoloop 短窗路径的固有行为**
  ——连原生编码器在这把尺上也劣于「八窗当一个 1024 线帧、每带独立 sf」的简化模型
  （后者完全不做感知决策，NMR/SNR 双赢但语义与 FFmpeg 无关）。`perceptual_short`
  的 opt-in 由此**定案**：它选的是「FFmpeg 语义」（窗型/分组/工具逐位对齐、可对拍
  可复现），不是「更优 NMR」——该取舍已写进测试注释与 §289 能力总览的口径。
  ②本移植感知臂与原生编码器同量级（两码率差 <1.7 dB、128k 反超），短窗感知路径
  的移植质量获得第三方背书。
- **正式测试**（`aac_encode_vectors/aac_ffmpeg_ref_test.mbt` +
  `fixtures_ff_ref.mbt`）：内嵌 ffmpeg 的 ADTS 流与 f64 解码（各 9 帧），断言
  (a) 原生流可被本仓库解码器逐帧解码（1024 样本/帧）；(b) |感知臂 − ffmpeg| < 2.0 dB
  （两码率）；(c) ffmpeg 也劣于简化臂 ±2 dB（固有序。全确定性：夹具/流/解码全内嵌，
  尺子是 f32 确定性 psy）。
- **踩坑**（写进了测试注释）：①FIL 元素**没有** element_instance_tag——规范是
  id(3)+count(4)+esc+payload，套用 SCE 的 tag(4) 会错位 4 位（ffmpeg 首帧带
  `put_bitstream_info` 的版本字符串 fill，count=namelen=14）；②本仓库 ffmpeg.exe
  是无 file 协议的定制构建，外部对拍一律走 docker。
- **验证**：`moon check --target wasm --deny-warn` 干净；全量 **1422/1422**（新增
  裁决测试 1 条）。§304 交接清单三项（工具多组化、窗型对拍、NMR 分析）全部收口。

## 309. CPE common_window=0 分叉：per-channel 窗计划 + 双独立 ICS 写侧（§304 偏差②收口）

§304 偏差清单的最后一项实质分叉：FFmpeg 在两通道的窗类型/形状/分组不完全一致时把
CPE 写成 `common_window = 0`——两套独立的 global_gain + ics_info + body，无 M/S。
此前本移植把 pair 合并成一台状态机 + 一份分组（§307 的合流机），流合法但与 FFmpeg
语义不同。本轮把窗决策彻底 per-channel 化。

- **`AacEncPlan` per-channel 化**：序列/带计划/分组/窗数/`use_psy` 每通道一份
  （`plans[ch]`），attempt 循环全部吃自己的 plan——psy analyze（含重试）、thr 行、
  sizes/two_shapes、tns_mmm（`last_max_sfb` 改 per-channel 数组）、PNS mark/search、
  twoloop sticky、apply_tns、adjust 的逐组扫描、`channel_data_psy`。合流只发生在
  `common_window = 1` 时（max_sfb 取大者、共享 ics_info/ms），判定 = 类型相等 &&
  （非短窗 || 分组展开相等；形状位恒 0 因为 C 的 `use_kb_window` 永不置位）。
- **每通道窗型机**：`lame_seq` 数组替代 §307 的 `lame_pair_seq`，感知短窗路径每通道
  跑自己的 `aac_lame_apply_block_type`（两通道判定一致时与合流机逐位相同，§307 的
  26×2 向量原样全绿即证）；其余路径共享决策 → 恒 common_window。
- **写侧**：`assemble_frame` 增 `common_window` 参数与 forked 分支——每通道
  gg + 自己的 ics_info + body（`encode_individual_channel` 布局），无 ms_used；
  `max_sfb` 参数改 per-channel 数组。解码器早已支持（`decode_pair` 的
  `common=None` 路径各读各的 ics），无需改。
- **门控修正**：stereo 对（IS/MS）只在 common_window 跑（C 的两个 search 本就
  early-return）；`ms_mode` 计数、`mark_is`、max_sfb 合并全部加 common_window 门
  （期间修了三个 mono/forked 越界：msc 计数、iter_max_sfb 合并、census 的
  `infos[0]`）。
- **端到端**：非对称瞬态（点击按通道错位 512 样本）→ **8/11 帧 forked**，
  本仓解码器逐帧解码 + docker ffmpeg 9.0.2 解码 0 errors，双解码器 PCM 对拍
  **max_diff 4.4e-8（140.2 dB）**——双独立 ics_info 布局被真实解码器认可。
  正式测试 `aac_lc_perceptual_short_forked_cpe_stream`（forked > 0 且 < 全部、
  峰值有界、PNS 容差下 SNR > 5 dB）。
- **基线（硬验收）**：四条长窗 noise 流（default/perceptual/+pns/+pns+is）stash
  前后字节与 FNV **完全不变**（无攻击 → 恒 ONLY_LONG → 恒 common_window）。
- **验证**：`moon check --target wasm --deny-warn` 干净；全量 **1423/1423**（新增
  forked CPE 流测试 1 条）。§304 偏差②收口；剩 la==NULL flush 一条（需编码器
  flush API，本移植无此入口，保持文档化）。

## 310. 统一播放 API 骨架（申报书交付①第一期：MediaFile/StreamInfo/MediaPacket/AudioDecoder）

申报书「本期拟实现」①的第一期（音频线）：在 §270 探测层之上提供统一入口
`MediaFile::open(bytes)`——probe 定格式、开对应 demuxer、建 `StreamInfo` 表；
`next_packet` 按流序吐 `MediaPacket`；`AudioDecoder::open(StreamInfo)` 按编解码名
分发到已移植解码器，`decode` 产 `AudioFrame`。错误为闭合分类 `MediaError`
（FormatNotFound/UnsupportedFormat/DemuxError/UnsupportedCodec/DecodeError，
`message()` 渲染），failable API 走 MoonBit 的 `raise` 风格。

- **四容器适配器**：MP4（`av_mov_open`，样本按 stco 从原文件切片，跨轨按 DTS
  K 路归并 = C 的 `AVPacket.stream_index` 乱序交错）、Matroska/WebM
  （`av_matroska_open`，直接吃 `mh.packets` 队列）、Ogg Opus（`av_ogg_opus_open`）、
  WAV（`av_wav_open`，data 块切分包）。流表从各容器轨道参数建（codec_id 统一成
  FFmpeg 名：esds/CodecID/`ff_wav_codec_id`）。
- **解码分发**："aac" → `new_aac_decoder_from_asc(extradata)`（AacPcm 每元素一个
  平坦声道数组，按元素序铺进 `AudioFrame.samples`）；"opus" → `opus_parse_head` +
  `OpusDecoder`；"pcm_s16le/be" → facade 内直接解释（逐样本 s16→double）。其余编
  解码名抛 `UnsupportedCodec`。
- **端到端（黑盒 5 测）**：WebM/Opus 与 Ogg/Opus（经验证 muxer 现做 + 真实 Opus
  包）、WAV/PCM（§270 探针夹具，解出 PCM 与 data 块逐字节一致）、MKV/AAC
  （**本仓 AAC 编码器现做**：编码→剥 ADTS 头→mkv_aac_muxer→facade 打开→解码，
  与直连路径逐样本全等 + 对源正弦 SNR 60.3 dB——编码↔复用↔解码全链展示）、
  未知字节 → `FormatNotFound`。
- **过程坑**：①`av_probe_input_format` 的夹具是 base64，直接喂文本得分 0；
  ②probe 夹具是探测样例（512 B 截断页），demux 级测试要用 muxer 现做或完整夹具；
  ③`probe_fixture_aac` 的首帧 AU 携带 LFE（id 3）——ASC 声明布局（mono）与流不符
  时 `decode_frame` 严格报错，而 ADTS 路径因布局为空而放行——这正是布局检查的设
  计语义，测试改用布局一致的编码器现做流；④python 写 MoonBit 转义字节字面量时
  JSON/heredoc 双层转义会产出裸控制字节（`chr(92)` 绕开）。
- **验证**：`moon check --target wasm --deny-warn` 干净；全量 **1428/1428**
  （新增 5 条 playback 测试）。下一期：视频流解码头接入、时间戳/时长口径、
  seek、JSON CLI 的 `decode-audio` 命令（③）与 libswresample。

## 311. 播放 API 第二期：容器时长口径 + decode_audio_file 引擎 + WAV 导出（申报书③地基）

- **`MediaFile::duration_seconds() -> Double?`**：四适配器各自的时长口径——MP4
  `mvhd duration / timescale`；Matroska Info Duration × TimecodeScale / 1e9；
  Ogg Opus 末包 granule − pre_skip（÷48000）；WAV sample_count / rate。容器没带
  时长则 None。
- **`decode_audio_file(bytes) -> DecodedAudio`**：`decode-audio` 命令（申报书②）
  的引擎——open → 选第一条音频流 → 全量解码 → 按声道铺平，附 format/codec/时长。
  **`DecodedAudio::to_wav_s16`**：限幅、交错、经 `WavMuxer` 产出 16-bit LE WAV
  （CLI ③ 的输出格式）。
- **黑盒 3 测（累计 9 条 playback）**：WAV 夹具 `decode_audio_file` → `to_wav_s16`
  的 data 块与源**逐字节一致**（PCM 恒等路径）且时长 = 样本数/率；Ogg/Opus 三包
  2880 样本、时长 = (granule−pre_skip)/48000 精确到毫秒、WAV 导出样本数一致；
  四适配器时长各自断言（MKV/AAC 3×23 ms 落窗、Ogg granule 链、WAV 计数）。
- **验证**：`moon check --target wasm --deny-warn` 干净；全量 **1431/1431**。
  下一期：视频流解码头接入（H264 `decode_avc_sample` 入口已侦察）、seek、
  CLI ② 的 probe/dump-packets 命令。

## 312. 播放 API 第三期：视频流接入（H264 经 VideoDecoder 分发，像素级对拍）

- **`VideoDecoder::open(StreamInfo)` / `decode(pkt) -> Array[VideoFrame]`**："h264"
  分发到 `new_h264_decoder` + `ff_h264_decode_extradata`（吃 avcC 或 Annex B
  extradata，顺带把 SPS/PPS 装进参数集）；`is_avc` 时逐包走 `decode_avc_sample`
  （长度前缀样本，`None` = 重排缓冲未出帧），否则 `decode_annexb`。`VideoFrame`
  包装 `H264Frame`（y/u/v 平面 + stride/crop/poc）不复制像素。
- **端到端**：真实 ffmpeg MKV（H.264+AAC，`cluster_real_mkv_b64` 转正进黑盒）——
  facade 打开两流（h264 + aac），H.264 轨经 `VideoDecoder` 解出的帧与直连路径
  （Matroska demuxer + avcC + `decode_avc_sample`）**帧数/poc/尺寸/luma 平面逐帧
  逐像素全等**（32×32 帧）。
- **验证**：`moon check --target wasm --deny-warn` 干净；全量 **1432/1432**。
  播放 API 音频线 + 视频线首条路径就绪；下一期：hevc/vp9/av1 视频分发（入口
  形态同 H264）、seek、CLI ② probe 命令。

## 313. 播放 API 第四期：视频分发补齐（VP9 真实流 MD5 对拍 + HEVC 批式 + AV1）

- **`VideoFrame` 重构为 tagged union**（H264/Hevc/Vp9/Av1）+ 统一访问器：
  width/height/poc（VP9/AV1 输出序即显示序，返回 -1）/bit_depth 与
  `yuv_bytes()`——各家打包成 planar YUV（H264 现场拼可见区含 crop/stride、
  HEVC `yuvp`、VP9 `frame_bytes`、AV1 FixedArray 拼接含 >8bit LE16），
  `extract-frame` 的统一出口。`StreamInfo` 补 width/height（MP4 tkhd、
  MKV PixelWidth/Height）。
- **三家分发**：VP9 = `vp9_decoder_new` + `decode_packet`（superframe 拆分
  内建，`Vp9Frame{width,height,data}` 每包 0..n 帧）；AV1 = `Av1Decoder::new`
  + `decode_packet`（OBU 解析内建，`Av1FrameOutput` 平面）；HEVC = 批式——
  移植入口 `ff_hevc_decode_frames_ex` 每调用新会话（跨包 DPB 不保留），facade
  缓冲包到 `finish_video` 拼接一次喂（与上游批式语义一致），hvcC 的
  lengthSizeMinusOne 从固定偏移 21 取。
- **端到端**：VP9——真实 libvpx 五帧（64×48，首关键帧）经 webm_av_muxer 现做
  → facade `VideoDecoder` 逐帧 `yuv_bytes` 的 MD5 **与 FFmpeg 解码源 IVF 的
  五个参考 MD5 逐帧一致**；HEVC/AV1 分发接线测试（批式缓冲/错误面/
  音视频互斥拒绝）；像素级 e2e 留待轻量真实位流（AV1 向量数十 KB）。
- **验证**：`moon check --target wasm --deny-warn` 干净；全量 **1434/1434**
  （playback 累计 11 条）。四家视频解码分发全部接通；下一期：seek、
  CLI ② probe 命令、HEVC/AV1 真实流 e2e。

## 314. 播放 API 第五期 + CLI ①：probe_json 渲染与 probe 命令（申报书②首命令）

- **`MediaFile::probe_json()`**：ffprobe `-show_format -show_streams` 风格的
  JSON 渲染——`format`（format_name/nb_streams/duration）+ 每流一项（index/
  codec_type/codec_name + 音频 sample_rate/channels 或视频 width/height +
  has_extradata）。自带 RFC 8259 字符串转义（`"`/`\`/C0 控制字符 →
  `\u00XX`，经 `"\u005C"` 码点常量取反斜杠避开源码转义层）。
- **`cmd/probe`**（申报书②五命令之首）：`moon run cmd/probe -- <file>...`——
  逐文件读取（`moonbitlang/x/fs`）、facade 打开、打印 JSON；错误打
  `probe: <file>: <message>`（`MediaError::message`）并整体以非零退出，
  与 ffprobe 一致地继续处理其余参数。`moon.mod` 新增 `moonbitlang/x` 依赖。
- **测试**（黑盒 +2，playback 累计 13）：`@json.parse` 证明渲染是合法 JSON +
  逐字段文本断言（WAV：单音频流全部字段；MKV：双流 + h264 宽高 + aac）；
  转义单测（引号/反斜杠/LF 混合输入的逐字符期望）。
- **命令实跑**（真实字节落盘）：WAV → 完整 JSON（duration 0.027125）；
  MKV（H264+AAC）→ 双流 JSON（duration 1.023 与 §311 断言一致）；
  垃圾字节 → `probe: ...: Format not found` + 非零退出。
- **本轮坑**（写进记忆）：heredoc/python 补丁链写含反斜杠的 MoonBit 字面量
  会被逐层转义吞噬（`\` → `\` → 空），最终一律改码点比较（`code == 34/92`）
  + `"\u005C"` 取反斜杠；`Int.to_char` 字面量直连 `.to_char()` 是语法错误
  （需括号）且 `92.to_char()` 返回 `Char?`（用 `unsafe_to_char`/`Char::from_int`）；
  core `Json` 枚举非 `pub(all)` 不可外部模式匹配（测试改 `@json.parse` 合法性
  + 文本断言）。
- **验证**：`moon check --target wasm --deny-warn` 干净；全量 **1436/1436**。
  CLI ② 剩余四命令（dump-packets/decode-audio/extract-frame/extract-cover）
  的引擎已全部就绪（§311 decode_audio_file + to_wav_s16、§313 yuv_bytes）。

## 315. CLI ②③：dump-packets / decode-audio + 三处 ffmpeg 对齐修复（ogg pts / opus 空帧 / pre-skip 裁剪）

- **`MediaFile::dump_packets_json`**（`libavformat/playback_dump.mbt`，申报书②第二
  命令引擎）：每包一行 JSONL——`stream_index`/`pts_ms`（按容器自身时间基换算，
  `stream_timebase`：MP4 mdhd timescale、MKV 1e9/time_scale、Ogg 48 kHz、WAV 采样率）
  /`size`/`flags`（K/-）；`stream=`/`limit=` 过滤（仅发射行计入 limit）。NaN/失控
  时间戳渲染 `null`（ffprobe 的 N/A 口径）。
- **`cmd/dump-packets` / `cmd/decode-audio`**：照 §314 cmd/probe 结构（x/fs + core/env）。
  dump-packets 解析 `--stream N`/`--limit N`（`@string.parse_int`）；decode-audio 调
  §311 `decode_audio_file` + `to_wav_s16` 写 `--output` WAV，stdout 打
  `DecodedAudio::summary_json`（codec/rate/channels/样本数/容器时长/解码时长）。
- **ogg.mbt pts 重写为 FFmpeg 语义**（对拍驱动的正确性修复）：逐行核对
  oggdec.c `ogg_calc_pts` + oggparseopus.c `opus_packet`——非 EOS 页且 gp≥0 时每包
  pts = 页 gp − 后缀和(duration) − pre_skip（后缀和数组实现）；EOS 页/无 gp 页走
  lastpts/cur_dts 链（锚点 = 上一页原始 gp，流首为 0）+ 末包按闭页 granule
  end-trim。效果：RFC 7845 gp 含 pre_skip 的文件（v1.opus）首包 pts=0 与 ffprobe
  一致；ffmpeg mux 的 gp-不含-pre_skip 文件（wbtest 钉值 -312/648/…）逐位不变。
- **opus 空熵帧修复**：`opus_rc_init` 原拒绝空帧 → TOC-only 包（1 字节，CELT
  config 31）走 concealment 全零。放开后 BitReader 越界读零（ffmpeg 容忍语义），
  空 CELT 帧解码出 MDCT 重叠的上帧衰减拖尾（145 样本），docker 对拍 v1.opus
  12360 样本/ch max|diff|=1（全部 ±1 = ffmpeg float→s16 默认三角 dither）。
- **decode_audio_file Ogg pre-skip 裁剪**：OpusHead pre_skip 是编码 priming，
  ffmpeg 在解码器侧丢弃（initial_padding）；我们在拼接后裁掉每通道头部 N 样本。
- **实跑验证**（docker linuxserver/ffmpeg:9.0.2 交叉）：dump-packets 四容器对齐
  ffprobe `-show_packets`（Opus 0/20/40ms、MKV 0/40/80 + flags、M4A 含 -21.333ms
  edit-list 起点、PCM WAV 1024@0ms/576@64ms 两包）；decode-audio 输出 WAV 与
  ffmpeg 参考解码逐样本比对收敛。已知限制：AAC-in-WAV ffmpeg 走 AAC parser 按帧
  分包（738/383），我们按 block_align 分块——PCM 完全一致。
- **测试**：黑盒 +2（dump 三容器 + 过滤/上限钉值、summary_json 合法性+字段），
  fixture +2（playback_ogg_file_b64 = v1.opus 实文件、playback_pcm_wav_b64）；
  随修复更新 4 处旧预期（opus_rc 空帧 Ok、celt 空 b"" Ok、failed-frames 状态保持
  去掉空帧行、drain 测试 2880→2568）。
- **本轮坑**：标签参数调用是 `f(x=v)` 不是 `f(x~: v)`（parse error）；core 无
  `print`/无普通 `split`（逐行 println + 自写 playback_split_lines）；`Ref.new`
  直接作函数调用被禁（本仓库惯例 `let mut` 计数器 + match-break 即可）；
  Int32 可直接索引数组且 Int32/Int 可混合比较（`p.stream_index != audio`）。
- **验证**：`moon check --target wasm` 干净；全量 **1438/1438**。CLI 剩
  extract-frame/extract-cover（§313 yuv_bytes 已就绪）。
