moonflac

    纯 MoonBit 实现的 FLAC 无损音频解码器,不依赖任何 C 库或系统编解码器

    flac
    audio
    decoder
    codec
    lossless
    wasm
    Download zip
    Author
    Version
    0.3.4
    License
    Apache-2.0
    Last updated
    5 days ago
    Downloads
    75

    #moonflac

    CI

    纯 MoonBit 实现的 FLAC 无损音频解码器:零 FFI、不依赖任何 C 库或系统编解码器,可编译到 wasm-gc / js / native 三后端(CI 验证 wasm-gc 与 js;native 需 MSVC/系统链接器,见「已知限制」)。

    #项目目标

    把原生 FLAC(.flac)解码为 PCM:moonflac 只做解码,不包含编码器、标签写入与播放输出。与进程内 FFI 方案(如 moon_rodio 的 C 封装)不同,本库可以随项目一起编译进浏览器——这是它存在的核心理由。

    #支持范围(已实现并经差分验证)

    按 RFC 9639 交付以下解码能力。下表列出的结构组合都有与参考解码器逐样本比对的自动化测试 (语料矩阵见「验证方法」):

    • 元数据:fLaC 魔数、STREAMINFO 全字段、元数据块链遍历(保留块类型按规范拒绝);
    • 帧头:帧同步、全部块长码(表码 + 8/16 位扩展,块长上限 65535;65536 按 RFC 9639 §9.1.6 拒绝)、全部采样率码(表码 + 12/13/14 扩展字段)、声道分配码、位深码(4–32 bps)、UTF-8 形变变长帧号/样本号(含可变块长)、CRC-8;
    • 子帧:CONSTANT、VERBATIM、FIXED(阶 0–4)、LPC(阶 1–32,系数精度 1–15、预测右移按 s(5) 解释即 -16..15,负值为左移);
    • 残差:熵编码方法 0/1(Rice 4 位参数 / Rice2 5 位参数)、任意分区序、分区参数与数据交错、参数转义(15/31 → 5 位原码位宽)、wasted bits;
    • 声道去相关:left/side、right/side、mid/side(side 位宽恒为 bps+1,含 32 bps 下的 33 位);
    • 容错:帧头级损坏向后扫描重同步;子帧/CRC-16 损坏返回结构化错误(绝不 panic);total_samples 已知时输出精确截断、不足报错;
    • 输出:planar PCM 接口 + WAV 序列化(8/16/24/32 容器,12/20 位高位对齐左移);
    • 流式与定位:FlacStream 逐块解码(不一次性物化整条流);seek 按 SEEKTABLE 跳帧, 无点表时从流起点线性推进;
    • 元数据读取:read_seektable / read_vorbis_comment / read_picture / read_application / read_cuesheet(CUESHEET 全字段:目录号、ISRC、轨道、索引点, 保留位按规范强制校验为零);
    • 增量输入:FlacFeeder 支持按任意块大小边喂边解(feed/next/finish), 数据不足时等待更多输入而不误报错误;
    • 完整性:STREAMINFO 记录了 MD5 时对解码 PCM 做 MD5 校验(交织小端、每样本符号扩展到 ceil(bps/8) 字节,符合 RFC 9639 §8.4,覆盖 4–32 全位深;8/16/24/32 位的打包方式此前已与 ffmpeg 写入值交叉验证,非 8 倍数位深按规范原文实现),不符报 Md5Mismatch。

    #明确不做

    • FLAC 编码器(只解码);
    • 标签(ID3/Vorbis Comment)读写;
    • Ogg 封装的 FLAC(Ogg FLAC);
    • 重采样、混音、音频设备输出。

    #已知限制

    • LPC 系数精度支持 1–15 bit 全档(精度字段 0..14)。字段值为 0b1111 时按 RFC 9639 Table 22 的 "0b1111 is forbidden" 判为非法码流,报 BadSubframe—— 这不是"未实现的合法编码",符合规范的编码器不会产出它 (实测 ffmpeg 编码器精度上限为 15,写入字段 14)。
    • FlacStream::seek 在无 SEEKTABLE 时为线性推进(正确优先);按帧号扫描的快速路径留作后续增量。
    • native 后端未做端到端验证:moon check --target native 通过(类型检查层面),但 moon build --target native 在 Windows 上需要 MSVC Build Tools(C++ 工具 + Windows SDK), 在 macOS/Linux 需要可用的系统 C 工具链;本项目 CI 只在 ubuntu 上跑 wasm-gc 与 js 两个后端, native 的构建与测试以使用者本地环境为准。
    • 完整语料不在仓库内:只随仓库带一个 9 KB 的最小样例 examples/sample.flac; tools/corpus/(85 个文件,含 4–32 位深与 6 个负例)已被 .gitignore 排除, 用 python3 tools/gen_corpus.py 现场生成(需 ffmpeg 与 libFLAC 的 flac),见「验证方法」。

    #安装

    moon add Xcsa-ee/moonflac

    #用法

    #命令行(可运行示例)

    仓库自带一个 9 KB 的样例,clone 下来即可原样执行:

    moon run cmd/main -- examples/sample.flac output.wav

    成功时输出采样率、声道、位深与样本数并写入 WAV;失败时输出结构化错误:

    采样率: 44100 Hz, 声道: 1, 位深: 16, 样本: 2000 已写入: output.wav 解码失败: Xcsa-ee/moonflac.FlacError.BadCrc(kind="CRC-16", ...)

    examples/sample.flac 是 44.1 kHz / 16 bit / 单声道 / 2000 样本的正弦波,由 tools/gen_corpus.py 用 ffmpeg 从合成 PCM 编码而来,可随时重新生成(见「验证方法」); 完整语料(85 个文件,含 4–32 位深与 6 个负例)不随仓库分发。

    #作为库使用

    import {
    "Xcsa-ee/moonflac",
    }

    // 整流解码
    let file = @moonflac.decode(data) // raise FlacError
    let wav = @moonflac.wav_encode(
    file.channels,
    file.info.bits_per_sample,
    file.info.sample_rate,
    )

    // 流式解码 + 定位
    let s = @moonflac.FlacStream::new(data)
    s.seek(44100) // 跳到第 1 秒
    let block = s.next() // Option:每声道一个块的样本

    file.channels 为 planar 布局:channels[声道][样本],样本值在 file.info.bits_per_sample 位有符号范围内。流式 next 同样返回 planar 块。

    #验证方法(可复现)

    仓库自带三级验证,任何一次改动都应跑全:

    # 1. 单元测试(白盒 63 用例,双后端)+ 变异验证(6 判据改错必须变红) moon test --target wasm-gc moon test --target js python3 tools/mutation_check.py # 2. 差分测试:现场生成语料,与 FFmpeg 逐样本比对 # 本地需要 ffmpeg 与 libFLAC 的 flac(可用 FLAC_FFMPEG / FLAC_BIN 指定路径, # gen_corpus.py 也接受 --ffmpeg <路径>);CI 使用 apt 安装的 ffmpeg + flac python3 tools/gen_corpus.py python3 tools/diff_oracle.py --target wasm-gc python3 tools/diff_oracle.py --target js # 3. 兼容性验收报告:xiph 官方测试文件 + 自建语料, # 三方(本解码器 / FFmpeg / libFLAC)逐样本与 STREAMINFO MD5 比对 git clone --depth 1 --branch 1.5.0 https://github.com/xiph/flac.git /tmp/xiph-flac python3 tools/compat_report.py \ --xiph /tmp/xiph-flac/test/flac-to-flac-metadata-test-files \ --corpus tools/corpus --out COMPATIBILITY.md

    Windows 用户:本机 python3 是 Microsoft Store 的占位符,把上面命令里的 python3 换成 python 即可;其余命令与 Linux 相同(git clone 的目标路径换成任意空目录)。

    当前结果见 COMPATIBILITY.md:88 个文件(xiph 官方 9 + 自建 79) 全部与 FFmpeg 逐样本一致并通过 flac -t 的 MD5 校验;其中 82 个另通过 libFLAC 裸 PCM 逐样本比对,其余 6 个(4/12/20 位)因 libFLAC 拒绝非 8 倍数位深的 raw 输出而不做该项(已在报告中如实标注)。

    (库使用示例已于 2026-10-18 实拉 0.1.0、2026-10-21 实拉 0.2.0 在全新工程复验通过:整流解码、WAV 写出、流式 seek/next 与 FlacFeeder 增量输入均按上文运行。2026-10-29 又在全新工程 moon add Xcsa-ee/moonflac 实拉 0.3.0 通过:decode_flac_base64 对合法 FLAC 返回非空 base64 WAV、对坏输入按契约返回空串,wav_encode 签名与上文一致;该次复验未覆盖流式接口。)

    语料矩阵覆盖:4/8/12/16/20/24/32 bps(12/20 位为 libFLAC 真实编码,4 位无参考编码器、 为手造帧)、1–8 声道、四种声道分配、四种采样率码、全部子帧类型与预测阶、分区序扫描、 LPC 精度 4/9/15、wasted bits、Rice 转义、Rice2、变长帧号与可变块长、多帧长流,以及 魔数错误/空文件/中途截断/CRC-8 损坏/CRC-16 损坏/MD5 篡改六类负例(负例必须报错)。

    关键判据的变异验证已脚本化:python3 tools/mutation_check.py 会把 CRC-16 覆盖起点、mid/side 还原公式、Rice unary 方向、wasted bits 左移、LPC 预测右移、块长上限逐个改错,并断言单元测试变红。当前 6/6 变异全部被抓到(约 8 秒,CI 也会跑这一步;脚本自己复制仓库到临时目录,不动工作区)。

    #生态定位

    按 2026-10-16 对 mooncakes.io 全量 3047 个模块的检索,flac 关键词唯一命中 Milky2018/moon_rodio,其 FLAC 路径是 decoder/flac_vorbis_native.c 中 DR_FLAC_IMPLEMENTATION 的 C FFI 封装,仅声明 native 目标;本库为纯 MoonBit 实现,无外部导入,可在 wasm-gc 下运行。

    2026-10-21 起生态新增 Sakuraqwq1/moonaudio(多格式音频工具箱,含 FLAC 解码)。两者定位不同:moonaudio 面向 DSP 流水线,输出归一化 Double、MD5 仅展示不校验、SEEKTABLE/CUESHEET/PICTURE 块只跳过不解析、无 seek 与增量输入;本库面向位精确解码与归档校验——整数 PCM/WAV 输出、4–32 位深 STREAMINFO MD5 校验、真实增量流式输入、SEEKTABLE seek 与全部元数据块内容解析。本库核心实现(0.1.0 于 2026-10-18 发布)早于 moonaudio 仓库创建(2026-10-21),为独立实现。

    截至 2026-10-31 另有两个纯 MoonBit 的 FLAC 相关包:CXREN/flac(解码器,2026-10 检索:无 seek/增量输入说明,定位为逐帧解码到交织 PCM)与 Sakuraqwq1/moonflac-encoder(编码器,方向与本库互补)。本库与二者无代码关系,差异化在位精确解码 + 全位深 MD5 校验 + 增量输入 + SEEKTABLE seek + 元数据块内容解析 + 88 文件三方对拍。检索命令:moon search flac --limit 10(本机 moon 0.1.20261012)。

    #移植或参考说明

    本项目为原创实现,依据 RFC 9639(Free Lossless Audio Codec,2024-12)从零开发,未移植任何上游源文件——git ls-files 内没有一个 .c/.h/.cpp,也不链接 libFLAC。

    参考对象上游许可参考范围(用到什么)没用到什么
    xiph/flac(libFLAC)库代码 BSD-3-Clause(其 COPYING.Xiph);flac/metaflac 命令行工具为 GPLv2行为对照:错误分类与边界条件的取舍;以 flac -t 作为独立的第三方校验器(含 STREAMINFO MD5 全量校验)未取任何 C 源码,未链接其库,未复用其 API 或数据结构
    mackron/dr_libs(dr_flac)public domain / MIT-0行为对照:帧头损坏后的向后扫描重同步这类容错策略未取源码,未移植其 API
    FFmpegLGPL/GPL(按构建选项)仅作差分测试 oracle:解码为 f64le 裸流后逐样本比对,不在解码实现里起作用不参与实现,不进仓库
    RFC 9639IETF 的 RFC 文本使用条款格式定义与全部强制校验项的规范依据—

    测试素材的来源与许可:COMPATIBILITY.md 中的 xiph 官方文件取自 xiph/flac 的 test/flac-to-flac-metadata-test-files/,CI 按 tag 1.5.0(commit 1507800d)现场 clone,按其仓库自带 COPYING.* 使用,不随本仓库再分发;自建语料由 tools/gen_corpus.py 用 ffmpeg 与 libFLAC 现场生成,tools/corpus/ 已被 .gitignore 排除。手写的 hand_*.flac 由仓库内脚本按 RFC 9639 构造,可用 gen_corpus.py 复现。

    对拍用的参考解码器版本需要在日志里能追溯:本地复跑用的是 libFLAC 1.5.0;CI 的 flac 由 apt 提供(实测 1.4.3,ffmpeg 6.1.1),因此 CI 会打印 ffmpeg -version 与 flac --version,xiph 素材的 tag 与工具版本都不随上游漂移。

    本项目采用 Apache-2.0 许可证。

    #目录结构

    路径内容
    metadata.mbt frame.mbt元数据块链与帧头解析
    metadata_ext.mbtSEEKTABLE / VORBIS_COMMENT / CUESHEET / PICTURE / APPLICATION 内容解析
    subframe.mbt residual.mbt子帧与分区 Rice 残差
    flac.mbt顶层解码编排与声道去相关
    bitreader.mbt crc.mbt位级读取(含 UTF-8 形变变长帧号)与 CRC-8/16
    stream.mbt incremental.mbtFlacStream 逐块/seek 与 FlacFeeder 增量输入
    wasm_api.mbt wav.mbtbase64 导入导出的 wasm 调用面与 WAV 序列化
    flac_error.mbt结构化错误类型
    cmd/main命令行示例
    examples/sample.flac最小可运行样例(9 KB,44.1 kHz / 16 bit / 单声道 / 2000 样本)
    tools/差分测试工具链(语料生成 + 逐样本比对 + 兼容性报告 + wasm 冒烟)
    DEVELOPMENT.md开发回顾:架构决策、验证判据、参考边界、AI 参与说明
    CHANGELOG.md变更记录(按版本)

    FlacError

    pub suberror FlacError {
    Truncated(offset~ : Int, need~ : Int, actual~ : Int)
    NotFlac
    BadMetadata(reason~ : String)
    BadFrameHeader(reason~ : String)
    BadCrc(kind~ : String, expected~ : Int, actual~ : Int)
    BadSubframe(reason~ : String)
    BadResidual(reason~ : String)
    Md5Mismatch(expected~ : String, actual~ : String)
    }

    解码过程中可能出现的错误。

    ApplicationBlock

    pub struct ApplicationBlock {
    app_id : String
    data : Bytes
    }

    APPLICATION 块:4 字节应用标识 + 剩余数据。

    BitReader

    type BitReader

    MSB-first 位读取器。

    BitReader::read_bit

    fn BitReader::read_bit(self : BitReader) -> Int

    读 1 位(MSB-first)。越界时返回 0 并置 truncated。

    BitReader::remaining_bits

    fn BitReader::remaining_bits(self : BitReader) -> Int

    剩余可读位数。

    BitReader::total_bits

    fn BitReader::total_bits(self : BitReader) -> Int

    总位数。

    CueSheet

    pub struct CueSheet {
    media_catalog_number : String
    lead_in_samples : Int64
    is_cd : Bool
    tracks : Array[CueSheetTrack]
    }

    CUESHEET 块(RFC 9639 §8.7):CD-DA 轨道结构,或通用的"流内关注点" 定位机制。

    CueSheetIndex

    pub struct CueSheetIndex {
    offset : Int64
    index_number : Int
    }

    CUESHEET 轨道索引点(RFC 9639 §8.7.1.1)。

    CueSheetTrack

    pub struct CueSheetTrack {
    offset : Int64
    track_number : Int
    isrc : String
    is_non_audio : Bool
    pre_emphasis : Bool
    indexes : Array[CueSheetIndex]
    }

    CUESHEET 轨道(RFC 9639 §8.7.1)。

    FlacFeeder

    pub struct FlacFeeder {
    buf : Array[Byte]
    base_offset : Int
    info : StreamInfo?
    blocks : Array[MetadataBlock]
    next_frame_byte : Int
    meta_done : Bool
    finished : Bool
    decoded : Int64
    eof : Bool
    }

    增量 FLAC 解码器。

    FlacFeeder::feed

    fn FlacFeeder::feed(self : FlacFeeder, chunk : Bytes) -> Unit

    追加一段输入字节。可在任意时刻调用任意次,块大小不限。

    FlacFeeder::finish

    fn FlacFeeder::finish(self : FlacFeeder) -> Unit raise FlacError

    声明不会再有新输入。若 total_samples 已知且尝试解完缓冲区内 剩余完整帧后仍未解满,抛 Truncated;total_samples 未知时, 缓冲区尾部不足一帧的数据按整文件 decode 的口径静默忽略。

    FlacFeeder::is_done

    fn FlacFeeder::is_done(self : FlacFeeder) -> Bool

    是否已确定不会再有更多块。

    FlacFeeder::new

    fn FlacFeeder::new() -> FlacFeeder

    创建空的增量解码器,等待第一次 feed。

    FlacFeeder::next

    fn FlacFeeder::next(self : FlacFeeder) -> Array[Array[Int]]? raise FlacError

    取下一块样本(planar,[声道][样本])。

    • Some(block):解出了一个完整块,继续调用 next 取下一块。
    • None:当前无法再产出一个完整块。可能是"还需要更多数据" (is_done() 为 false),也可能是"流已结束"(is_done() 为 true)。
    • 抛 Truncated:仅在 finish() 已调用、total_samples 已知 且未解满时发生(与整文件 decode 对截断文件的处理一致)。

    FlacFile

    pub struct FlacFile {
    info : StreamInfo
    channels : Array[Array[Int]]
    }

    整文件解码结果(planar:每声道一个样本数组,样本为 bps 位宽的有符号值)。

    FlacStream

    pub struct FlacStream {
    info : StreamInfo
    br : BitReader
    blocks : Array[MetadataBlock]
    frame_offset : Int
    decoded : Int64
    pending : Array[Array[Int]]
    eof : Bool
    }

    按块流式读取的 FLAC 解码器。

    FlacStream::new

    fn FlacStream::new(data : Bytes) -> FlacStream raise FlacError

    解析元数据并就绪于第一个音频帧。

    FlacStream::next

    fn FlacStream::next(self : FlacStream) -> Array[Array[Int]]? raise FlacError

    取下一块样本(planar,[声道][样本])。返回 None 表示流已结束。 total_samples 已知且不足时抛 Truncated,与 decode 口径一致。

    FlacStream::seek

    fn FlacStream::seek(self : FlacStream, sample : Int64) -> Unit raise FlacError

    跳到第 sample 个样本处;之后 next 返回的样本从这里开始。 已到流尾(total_samples 已知且 sample 不小于它)时之后 next 返回 None。

    MetadataBlock

    pub struct MetadataBlock {
    block_type : Int
    offset : Int
    length : Int
    }

    STREAMINFO 之外的元数据块(只记录位置,不拷贝数据)。

    ParsedMetadata

    pub struct ParsedMetadata {
    stream_info : StreamInfo
    blocks : Array[MetadataBlock]
    frame_offset : Int
    }

    元数据解析结果。

    Picture

    pub struct Picture {
    picture_type : Int
    mime : String
    description : String
    width : Int
    height : Int
    depth : Int
    colors : Int
    data : Bytes
    }

    PICTURE 块(内嵌图片)。

    SeekPoint

    pub struct SeekPoint {
    sample_position : Int64
    byte_offset : Int64
    frame_samples : Int
    }

    SEEKTABLE 的一个定位点。

    StreamInfo

    pub struct StreamInfo {
    min_block_size : Int
    max_block_size : Int
    min_frame_size : Int
    max_frame_size : Int
    sample_rate : Int
    channels : Int
    bits_per_sample : Int
    total_samples : Int64
    md5 : Bytes
    }

    STREAMINFO 的解析结果。

    VorbisComment

    pub struct VorbisComment {
    vendor : String
    comments : Array[(String, String)]
    }

    VORBIS_COMMENT:小端长度前缀的 UTF-8 文本,注释项为 KEY=VALUE。

    decode

    fn decode(data : Bytes) -> FlacFile raise FlacError

    解码整条 FLAC 流。

    • 帧头级失败会向后扫描同步码尝试重同步(容忍尾部附带的标签等杂字节);
    • 子帧或 CRC-16 失败直接报错,不跳帧,保证错误可见;
    • STREAMINFO.total_samples 非 0 时以此为准截断输出,且帧数不足会报 Truncated 而不是静默少样本;
    • STREAMINFO 记录了 MD5 时校验解码 PCM 的 MD5(4–32 位全支持)。

    decode_flac_base64

    fn decode_flac_base64(flac_b64 : String) -> String

    解码 base64 编码的 FLAC 数据,返回 base64 编码的 PCM WAV (容器位宽 = 流的位深,8/16/24/32)。 解码失败返回空字符串,调用方据此判断是否成功。

    parse_metadata

    fn parse_metadata(data : Bytes) -> ParsedMetadata raise FlacError

    解析 fLaC 魔数与全部元数据块,返回 STREAMINFO、其余块的位置表与 第一个音频帧的偏移。

    read_application

    fn read_application(data : Bytes, block : MetadataBlock) -> ApplicationBlock raise FlacError

    读 APPLICATION 块。

    read_cuesheet

    fn read_cuesheet(data : Bytes, block : MetadataBlock) -> CueSheet raise FlacError

    读 CUESHEET 块。保留位区(轨道头/索引点)按规范强制校验为全零。

    read_picture

    fn read_picture(data : Bytes, block : MetadataBlock) -> Picture raise FlacError

    读 PICTURE 块。

    read_seektable

    fn read_seektable(data : Bytes, block : MetadataBlock) -> Array[SeekPoint] raise FlacError

    读 SEEKTABLE:每点 18 字节(大端 样本号 8 + 字节偏移 8 + 帧长 2)。 样本号全 1 的占位点按规范忽略;点数必须与块长整除。

    read_vorbis_comment

    fn read_vorbis_comment(data : Bytes, block : MetadataBlock) -> VorbisComment raise FlacError

    读 VORBIS_COMMENT 块。

    wav_encode

    fn wav_encode(channels : Array[Array[Int]], bits_per_sample : Int, sample_rate : Int) -> Bytes

    把 planar 样本序列化为 WAV 字节。8 位容器按 WAV 惯例存无符号值, 其余容器存有符号小端二补码;样本超位宽会先被视图限制在合法范围内 (解码器保证样本本身在 bps 位宽内)。