Audio container toolkit in pure MoonBit: Ogg read/write and CRC verification, WAV full-format read/write, Opus-in-MP4 muxing and demuxing, transcoding pipeline, and a 15-command CLI
作者:WangYF0829(王轶凡)|创建日期:2026-10-13|用途:moonmux 项目文档
| 项 | 状态 |
|---|---|
| MP4 / ISOBMFF 写入(Opus-in-MP4,新 mp4/ 包:ftyp/moov/mdat、stbl 表、dOps) | 已完成 |
| remux 命令:Ogg Opus → .m4a | 已完成 |
| info 识别 MP4 | 已完成 |
| CRC-32 查表优化(输出与 RFC 3533 逐字节一致) | 已完成 |
| WAV 全格式兼容:IEEE float 32/64、WAVE_FORMAT_EXTENSIBLE、24-in-4 容器;新增 encode_wav_ex | 已完成(v0.4.0) |
| 流式页读取:增量 PageReader(分块喂入、按需取页、失步重同步) | 已完成(v0.4.0) |
| to-ogg 命令:MP4(Opus-in-MP4)→ Ogg Opus,与 remux 互逆 | 已完成(v0.5.0) |
| 性能基准 scripts/bench.mjs:verify/info/dump/remux/to-ogg/demux 端到端计时,CI 运行并写 job summary | 已完成(v0.6.0) |
| 确定性变异测试 scripts/fuzz.mjs:真实 Ogg/Vorbis/MP4/WAV × 6 类变异,CI 400 次/固定种子 | 已完成(v0.6.0) |
| WAV 块保真:LIST/fact 等扩展块原样保留,WavFile::to_bytes 逐字节还原(含 24-in-4 容器与 channelMask) | 已完成(v0.7.0) |
| 流式写接口 WavWriter:增量追加 PCM、可选扩展块,finish 生成 WAV | 已完成(v0.7.0) |
| 正确性修复(v0.8.0):remux 输入闸门 + 拒绝链式/多流、MP4 读侧防越界 panic、stsz 定长样本与样本偏移上界、多声道 OpusHead StreamCount/CoupledCount、MP4→Ogg granule 语义、dOps OutputGain 透传、头包页隔离、跨页包替换、WAV 原始 fmt 布局保真、manifest/mux 自检 | 已完成(v0.8.0) |
| 正确性/契约修复(v0.9.0):MP4 写侧补 edts/elst + roll 样本组与 end-trim、WAV 浮点 fact 块与重复块拒绝、Ogg 头包预筛重同步 + 页校验序列化、CLI 诊断走 stderr / JSON 错误对象 / --hex 宽容 / 提取 --force / 选项任意位置 / tags 控制字符转义、2 GiB 聚合闸门、PCM 预算显式校验、POSIX 路径与 8.3 短名、manifest 重复头拒绝 | 已完成(v0.9.0) |
| 补丁(v0.9.1):mux 的 manifest.txt 为目录/管道/设备时按 I/O 错误退出 2;平台专属 8.3 短名测试 | 已完成(v0.9.1) |
| 安全/资源边界修复(v0.9.2):MP4 stts/stsz 先校验后分配(C-01/H-02)、co64 64 位偏移校验、inspect 缺 mvhd/stsz 高位拒绝;Ogg 重同步零拷贝 CRC + 确定性预算(H-01);解码前统一 gate + 按解码工作量估算 PCM 预算(H-03/H-04);extract --force 收紧(H-05);demux 清单先行(H-06);发布包内容门禁(H-07) | 已完成(v0.9.2) |
| 测试 / 示例 / 文档 / CHANGELOG / v0.9.2 发布(mooncakes + GitHub Release) | 已完成 |
| 环节 | 生态现状 |
|---|---|
| 读容器(demux) | 有:isobmff(MP4 解析)、moontsinspect(TS 检查) |
| 解码 | 有:moonopus、moonvorbis、mp3.mbt、moon-wav |
| 播放 | 有:moon_rodio |
| 写容器(mux) | 没有 |
| 转码管线 | 没有 |
| 模块 | 内容 | 状态 |
|---|---|---|
| ogg/ | 页 / 段 / 包解析与序列化;Ogg 专用 CRC-32;多逻辑流交织;物理流链接与拆分;按大小攒页;granule 二分定位;结构体检(分级 + 坏页重同步);页级补丁;增量页读取 PageReader(v0.4.0,流式/失步重同步) | 已完成 |
| codec/ | 编解码头解析(OpusHead / Vorbis ID)、时长换算、VorbisComment / OpusTags 读写 | 已完成 |
| manifest/ | demux/mux 的流目录清单(流号、包数、链序号、逐包粒度)的解析与生成 | 已完成 |
| wav/ | WAV(RIFF)读写:PCM 8/16/24/32、IEEE float 32/64、WAVE_FORMAT_EXTENSIBLE、24-in-4 容器读取;整数 PCM 编码与 encode_wav_ex(float/Extensible);v0.7.0 块保真(LIST/fact 扩展块、channelMask)+ WavFile::to_bytes + WavWriter | 已完成(v0.4.0 全格式 / v0.7.0 块保真) |
| pipeline/ | Ogg(Opus / Vorbis)→ WAV 转码管线 | 已完成 |
| mp4/ | MP4/ISOBMFF:写侧 ftyp/moov/mdat/stbl 五表/dOps(Opus-in-MP4,v0.3.x);读侧 inspect + demux_mp4_opus 解封装回 Ogg(v0.5.0,读写闭环) | 已完成(v0.3.x 写 / v0.5.0 读) |
| cmd/main/ | 命令行工具:info、demux、mux、cat、split、extract、dump、tags、tag、seek、verify、to-wav、chop、remux、to-ogg | 已完成 |
moon add WangYF0829/moonmuximport {
"WangYF0829/moonmux/ogg",
"WangYF0829/moonmux/wav",
}fn main {
// WAV:编码 → 解析往返
let bytes = @wav.encode_wav(
{ channels: 1, sample_rate: 8000, bits_per_sample: 16 },
b"\x00\x00\x01\x00",
)
match @wav.WavFile::parse(bytes) {
Some(f) => println("wav ok: " + f.format.sample_rate.to_string())
None => println("wav parse failed")
}
// Ogg:页起始识别
println("ogg page: " + @ogg.is_page_start(b"OggS", 0).to_string())
}| 退出码 | 含义 |
|---|---|
| 0 | 成功 |
| 1 | 数据校验未通过(输入不是合法 Ogg、被截断、清单与目录不符、verify 的 error / CRC 失败) |
| 2 | 用法或 I/O 错误(参数错、文件读不到、拒绝覆盖非空目录等) |
| 项 | 上限 | 原因 |
|---|---|---|
| 单文件大小 | 2 GiB | MoonBit Int 是 32 位,页偏移与长度都用它;@fs 也只有整文件 API;聚合写命令在分配前显式估算并拒绝越界 |
| MP4 写侧 | 仅 Opus-in-MP4、单音轨、单 chunk | 只补 Opus;Vorbis-in-MP4 非主流且需解析 Vorbis setup,明确不做;remux 只接受 Ogg Opus 输入 |
| MP4 读侧 | 仅解封装 Opus-in-MP4(单音轨、mdhd timescale=48000) | to-ogg 是 remux 的逆操作;注释不随 MP4 保存,输出使用最小 OpusTags |
| demux 包数 | 999999 | 包文件名固定 6 位,超过后字典序错乱、mux 读回顺序不对 |
| to-wav 预算 | 256 MiB(MOONMUX_MAX_PCM_MB 可调,1..2048) | 预算是解码期内存峰值估算:Opus 按逐包 TOC 时长 × 声道 ×(输出字节 + 8 B/样本解码器缓冲),不信任页 granule;Vorbis 无逐包时长,按 granule 近似且坏 granule 直接拒绝。默认 256 MiB ≈ 9 分钟 16 位单声道 Opus;进程实际峰值约预算的 1.5–2 倍 |
| chop Opus 时间戳 | 页精度,约 ±1 页 | 不重写 OpusHead 的 pre-skip;裁剪后首个数据页可能多带不到一页的前导样本 |
| to-wav 速度 | Opus 约 30× 实时(6.02 s 音频 ≈0.2 s);Vorbis 1.0 s 音频约需 60–70 s | 纯 MoonBit 解码器;Vorbis 路径明显更慢,长文件请先 split(release native / Windows x64 实测) |
| chained 拆分 | 整段都由单页流组成时无法拆分 | 与「一段内交织两条单页流」字节相同,规格上不可判定(保守不切) |
| 裁剪流时长 | 上界 | 容器层拿不到首个音频页的样本数,精确值需解码 |
| MP4 编辑列表 | 已写 edts/elst(pre-skip 裁剪)与 roll 样本组 | 不做章节/多轨编辑;mdhd 保留总样本数,mvhd/tkhd 用裁剪后时长 |
| remux 输入 | 单段、单逻辑流 Ogg | 链式/多流输入显式拒绝(exit 1),不再静默只取第一段 |
| PageReader 重同步 | 无候选步进上限 | 大量伪造 OggS 的恶意输入可能造成 CPU / gaps 放大;普通文件不受影响 |
| 写侧公开 API | serialize_pages_checked / validate_page 可用 | CLI 写命令已全部走校验入口;库用户直接调用 serialize_pages 时仍需自行保证 Page 合法 |
| WAV 块保真布局 | fmt 在 data 之前、扩展块在 fmt 之后的标准布局 | 原始 fmt 与扩展块逐字节写回;fmt 之前出现未知块或非零填充字节的极端布局会按语义规范化(扩展块保留、采样数据无损) |
| extract 幂等重跑 | 需显式 --force,且只允许幂等覆盖 | 默认拒绝“输出与输入内容相同”的别名;--force 仍要求已有输出与待写内容相同(或待写内容就是整个输入文件),不再允许覆盖不同内容——硬链接/别名截断多流源文件会被拒绝 |
| 覆盖/别名检查的内存 | 无 stat/inode API | 覆盖已有输出时把旧文件整体读入内存做内容比较(峰值约旧文件 + 新输出);不能流式比较或原子替换 |
irm https://cli.moonbitlang.cn/install/powershell.ps1 | iexmoon versionmoon build
moon testmoon build --release --target native
node scripts/bench.mjs --runs 5| 操作 | 输入 | 最小耗时 | 吞吐 |
|---|---|---|---|
| verify(CRC + 结构体检) | 1.02 MiB | 23.1 ms | 44 MB/s |
| info --json | 1.02 MiB | 23.4 ms | 43 MB/s |
| dump(页级转储) | 1.02 MiB | 18.6 ms | 55 MB/s |
| remux(Ogg → MP4) | 65 KiB | 17.0 ms | — |
| to-ogg(MP4 → Ogg) | 66 KiB | 19.6 ms | — |
| demux(拆包写目录) | 65 KiB | 68.8 ms | — |
moon build --target native
node scripts/fuzz.mjs --iterations 400 --seed 20261109# 查看容器信息(自动识别 Ogg 与 WAV)
# Ogg:格式、流号、页数、包数、负载字节、各流末粒度;能识别音频流时再给编码/声道/采样率/时长
# WAV:声道、采样率、位深、PCM 字节数、帧数与时长
moonmux info input.ogg
moonmux info input.wav
moonmux info input.ogg --json # 单行 JSON,便于脚本处理
# 注:时长为「末粒度 − 首粒度 − pre-skip」,对裁剪过的流是上界
# 结构体检:逐页 CRC + 页序号连续性 + BOS / EOS 完备性 + 段表一致性
# 用于判断文件是容器损坏还是编码问题,并给出损坏的具体位置
moonmux verify input.ogg
moonmux verify input.ogg --json # 完整 findings(文本只铺前 10 条)
# 解复用:按逻辑流拆出包,每条流一个子目录(s1/、s2/……)
moonmux demux input.ogg ./packets/
# 复用:把包重新封装成 Ogg
# 目录里有子目录 → 每个子目录一条逻辑流,按 Ogg 规则交织(BOS 页置顶、数据页按粒度升序)
# 目录里没有子目录 → 整个目录一条逻辑流
moonmux mux ./packets/ output.ogg
# 链接:把多个 Ogg 文件首尾相接成一个物理流
# 流号不冲突时逐页原样透传(字节级无损);冲突时把后一个文件相撞的流换新流号
moonmux cat a.ogg b.ogg c.ogg output.ogg
# 拆分:把链接流按链接点切回各自独立的 Ogg 文件(输出 part1.ogg、part2.ogg……)
# 页原样沿用,字节与 CRC 一字节不改 —— 非链接流得到的 part1 与输入完全相同
moonmux split chained.ogg ./parts/
# 抽流:把第 n 条逻辑流单独取出来(页级原样,可与输入逐字节对账)
moonmux extract input.ogg 2 single.ogg
# 页级转储:偏移 / 流号 / 页序 / 粒度 / 标志 / 段数 / 负载
moonmux dump input.ogg
moonmux dump input.ogg --hex # xxd 兼容,可 `| xxd -r` 还原
# 注释:读取与改写(KEY= 表示删除该键)
moonmux tags input.ogg
moonmux tag input.ogg tagged.ogg TITLE=月光 ARTIST=王轶凡
# 定位:按毫秒找到不晚于该时刻的页(打印页下标、字节偏移与该页起始时间)
# 链接流可用第 3 个参数指定第几段(默认第 1 段)
moonmux seek input.ogg 2500
moonmux seek chained.ogg 500 2
# 转码:Ogg(Opus / Vorbis)→ WAV;默认 16 位,可选 24 位
moonmux to-wav input.ogg output.wav
moonmux to-wav input.ogg output24.wav 24
# 重新封装:Ogg(Opus)→ MP4 / ISOBMFF 音频容器(.m4a,v0.3.x 新增)
# 不做转码:Opus 数据包原样搬运,只重建容器;info 可识别产物
moonmux remux input.ogg output.m4a
moonmux info output.m4a
# 反向解封装:MP4(Opus)→ Ogg(v0.5.0 新增,remux 的逆操作)
# 音频包逐字节还原;注释未随 MP4 保存,输出最小 OpusTags
moonmux to-ogg output.m4a back.ogg
moonmux verify back.ogg
# 裁剪:按毫秒区间裁剪(页精度,头页保留,时间轴归零)
moonmux chop input.ogg clip.ogg 1000 3000moon build --target nativemoonmux/
├── moon.mod / moon.pkg / moonmux.mbt 模块配置与根包
├── ogg/ Ogg 容器(页 / 段 / 包、CRC、交织、链接、结构体检、PageReader)
├── wav/ WAV 容器(全格式读写、块保真、WavWriter)
├── codec/ 编解码头解析(OpusHead / Vorbis ID / OpusTags / VorbisComment / TOC)
├── manifest/ demux / mux 的流目录清单
├── pipeline/ 转码管线(唯一引入三方解码器的包)
├── mp4/ MP4 / ISOBMFF 写入与解封装
├── cli/ 命令行参数解析(纯逻辑)
├── cmd/main/ 命令行入口
├── docs/architecture.md 模块边界、关键不变量、测试策略、能力边界
├── examples/ 三个可运行示例
├── scripts/ smoke / verify_mp4 / bench / fuzz / install_check
└── testdata/ 真实音频样本| 示例 | 命令 | 说明 |
|---|---|---|
| WAV 往返 | moon run examples/wav_roundtrip | 最小库用法:encode_wav → WavFile::parse |
| Ogg 信息 | moon run examples/ogg_info | 用 ogg/ + codec/ 公开 API 读取容器信息 |
| Ogg → MP4 | moon run examples/remux_ogg_to_mp4 | mp4/ 写—读对称:remux 后 inspect 读回 |
| MP4 → Ogg | smoke.sh / verify_mp4.sh 的 4e/6 段 | to-ogg 后 verify + 音频包逐字节比对 |
source scripts/moonmux.bash # 子命令与文件路径补全| 项目 | 参考了什么 | 没有照搬什么 |
|---|---|---|
| Xiph oggz / liboggz | 子命令的语义(rip/dump/validate/chop 的职责划分)、多流合并按呈现时间 | 不做 Skeleton,不解析 Theora/FLAC 等头;dump 不可逆 |
| libogg | 页大小与 flush 时机(4096 阈值、首页/EOS 强制刷出) | 不把 BOS 顺序交给调用方;写侧自己维护不变量 |
| mutagen(Python) | 注释改写时用 padding 保持旧包等长、复用旧页布局 | 不做 high/low 区间公式 |
| ffmpeg | 作为行为对照(转码产物用独立工具回读验证) | 不用时间驱动的分页策略 |
| libopusfile | seek 在 granule 上插值二分、80 ms pre-roll | 只定位到页,不解码 |
| Rust hound(Apache-2.0) | WAV fmt 标签 1/3/0xFFFE、Extensible 字段布局与 SubFormat GUID、24-in-4 容器语义、float 32/64、未知块跳过 | 不复制代码;v0.7.0 额外保留 channelMask 与扩展块以支持逐字节往返(hound 不暴露这些字段) |
| RustAudio ogg crate | 增量页读取状态机语义:先同步捕获模式、按段表等数据、CRC 失步后重同步 | 不复制代码;不实现其异步/seek 组合器 |
| hyperfine / cargo bench | 基准的「多次运行取最小值 + 表格化输出」呈现方式 | 不引入第三方依赖、不设跨机器阈值(CI 只记录) |
| cargo-fuzz / AFL++ | 「种子 + 确定性变异 + 退出码/超时断言」的变异测试模式 | 不使用其 fuzz 引擎;无覆盖率引导 |
Install
Download zipAudio container toolkit in pure MoonBit: Ogg read/write and CRC verification, WAV full-format read/write, Opus-in-MP4 muxing and demuxing, transcoding pipeline, and a 15-command CLI